[SDK] Pure ESM (#18427)

# Introduction
While testing the sdk and overall apps in
https://github.com/prastoin/twenty-app-hello-world
Faced a lot of pure `CJS` external dependencies import issue

Replaced all the cjs deps to either esm equivalent or node native
replacement
This commit is contained in:
Paul Rastoin
2026-03-05 17:19:01 +01:00
committed by GitHub
parent cfeea43eaf
commit 57d8954973
41 changed files with 574 additions and 243 deletions
+53
View File
@@ -0,0 +1,53 @@
---
description: ESM dependency guidelines for twenty-sdk and create-twenty-app packages
globs: ["packages/twenty-sdk/**", "packages/create-twenty-app/**"]
alwaysApply: false
---
# ESM Dependency Guidelines
## Context
`twenty-sdk` and `create-twenty-app` are published as dual-format npm packages (ESM `.mjs` + CJS `.cjs`). Dependencies listed in `dependencies` are **externalized** by the Vite/Rollup build — they are not bundled, and consumers resolve them from `node_modules` at runtime.
This means **CJS-only dependencies break the ESM output**. When Rollup emits `import { foo } from 'cjs-package'`, Node.js ESM cannot resolve named exports from CommonJS modules, causing `SyntaxError: Named export 'foo' not found`.
## Rules
### Only add ESM-compatible dependencies
Before adding a new dependency to `package.json`, verify it supports ESM:
- Check for `"type": "module"` in its `package.json`
- Or check for an `"exports"` map with ESM entries
- Or check for a `"module"` field pointing to an ESM build
### Use native `node:fs/promises` for standard fs operations
```typescript
// ✅ Import native fs functions directly
import { readFile, writeFile, mkdir, rm, cp } from 'node:fs/promises';
import { createWriteStream, existsSync } from 'node:fs';
// ✅ Import only custom helpers from fs-utils (no native re-exports)
import { pathExists, ensureDir, emptyDir, copy, move, remove, readJson, writeJson, ensureFile } from '@/cli/utilities/file/fs-utils';
// ❌ Don't use fs-extra (CJS-only, breaks ESM bundle)
import * as fs from 'fs-extra';
// ❌ Don't use import * as fs from fs-utils (it doesn't re-export native fs)
import * as fs from '@/cli/utilities/file/fs-utils';
```
### Use `@/cli/utilities/string/kebab-case` instead of lodash
```typescript
// ✅ Use internal utility
import { kebabCase } from '@/cli/utilities/string/kebab-case';
// ❌ Don't use lodash single-function packages (CJS-only, unmaintained)
import kebabCase from 'lodash.kebabcase';
```
### When no ESM alternative exists
If a CJS-only package has no ESM replacement (e.g. `archiver`), add it to the `cjsOnlyPackages` list in `vite.config.node.ts` so it gets inlined into the bundle instead of externalized.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "create-twenty-app",
"version": "0.6.3",
"version": "0.6.4",
"description": "Command-line interface to create Twenty application",
"main": "dist/cli.cjs",
"bin": "dist/cli.cjs",
+2 -11
View File
@@ -1,6 +1,6 @@
{
"name": "twenty-sdk",
"version": "0.6.3",
"version": "0.6.4",
"main": "dist/index.cjs",
"module": "dist/index.mjs",
"types": "dist/sdk/index.d.ts",
@@ -71,26 +71,21 @@
"@remote-dom/core": "^1.10.1",
"@remote-dom/react": "^1.2.2",
"@sniptt/guards": "^0.2.0",
"archiver": "^7.0.1",
"axios": "^1.13.5",
"chalk": "^5.3.0",
"chokidar": "^4.0.0",
"commander": "^12.0.0",
"dotenv": "^16.4.0",
"esbuild": "^0.25.0",
"fast-glob": "^3.3.0",
"form-data": "^4.0.5",
"fs-extra": "^11.2.0",
"graphql": "^16.8.1",
"graphql-sse": "^2.5.4",
"ink": "^5.1.1",
"inquirer": "^10.0.0",
"jsonc-parser": "^3.2.0",
"lodash.camelcase": "^4.3.0",
"lodash.kebabcase": "^4.1.1",
"preact": "^10.28.3",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"tinyglobby": "^0.2.15",
"typescript": "^5.9.2",
"uuid": "^13.0.0",
"vite": "^7.0.0",
@@ -103,11 +98,7 @@
"@prettier/sync": "^0.5.2",
"@storybook/addon-vitest": "^10.2.13",
"@storybook/react-vite": "^10.2.13",
"@types/archiver": "^6.0.0",
"@types/fs-extra": "^11.0.0",
"@types/inquirer": "^9.0.0",
"@types/lodash.camelcase": "^4.3.7",
"@types/lodash.kebabcase": "^4.1.9",
"@types/node": "^24.0.0",
"@types/react": "18.2.66",
"@types/react-dom": "18.2.22",
@@ -1,8 +1,9 @@
import { runAppDevInProcess } from '@/cli/__tests__/integration/utils/run-app-dev-in-process.util';
import * as fs from 'fs-extra';
import { join } from 'path';
import { OUTPUT_DIR } from 'twenty-shared/application';
import { runAppDevInProcess } from '@/cli/__tests__/integration/utils/run-app-dev-in-process.util';
import { pathExists } from '@/cli/utilities/file/fs-utils';
const APP_PATH = join(__dirname, '..');
const MANIFEST_OUTPUT_PATH = join(APP_PATH, OUTPUT_DIR, 'manifest.json');
@@ -15,7 +16,7 @@ describe('invalid-app manifest', () => {
expect(result.success).toBe(false);
const manifestExists = await fs.pathExists(MANIFEST_OUTPUT_PATH);
const manifestExists = await pathExists(MANIFEST_OUTPUT_PATH);
expect(manifestExists).toBe(false);
}, 30000);
@@ -1,4 +1,4 @@
import * as fs from 'fs-extra';
import { readdir } from 'node:fs/promises';
import { join } from 'path';
import { OUTPUT_DIR } from 'twenty-shared/application';
@@ -6,7 +6,7 @@ export const defineEntitiesTests = (appPath: string): void => {
const outputDir = join(appPath, OUTPUT_DIR);
describe('logicFunctions', () => {
it('should have built logicFunctions preserving source path structure', async () => {
const files = await fs.readdir(outputDir, { recursive: true });
const files = await readdir(outputDir, { recursive: true });
const sortedFiles = files.map((f) => f.toString()).sort();
expect(sortedFiles).toEqual([
@@ -43,7 +43,7 @@ export const defineEntitiesTests = (appPath: string): void => {
});
it('should not create shared chunk files for utilities', async () => {
const files = await fs.readdir(outputDir, { recursive: true });
const files = await readdir(outputDir, { recursive: true });
// Chunk files have a hash suffix like "greeting.util-CipJsYK0.mjs"
const chunkFiles = files
@@ -1,7 +1,8 @@
import * as fs from 'fs-extra';
import { join } from 'path';
import { normalizeManifestForComparison } from '@/cli/__tests__/integration/utils/normalize-manifest.util';
import { readJson } from '@/cli/utilities/file/fs-utils';
import { type Manifest } from 'twenty-shared/application';
import { EXPECTED_MANIFEST } from '../expected-manifest';
export const defineManifestTests = (appPath: string): void => {
@@ -9,7 +10,7 @@ export const defineManifestTests = (appPath: string): void => {
describe('manifest', () => {
it('should build manifest matching expected JSON', async () => {
const manifest = await fs.readJson(manifestOutputPath);
const manifest = await readJson<Manifest>(manifestOutputPath);
expect(manifest).not.toBeNull();
@@ -1,11 +1,11 @@
import * as fs from 'fs-extra';
import { readdir } from 'node:fs/promises';
import { join } from 'path';
export const defineFrontComponentsTests = (appPath: string): void => {
describe('front-components', () => {
it('should have built front components at root level', async () => {
const outputDir = join(appPath, '.twenty/output');
const files = await fs.readdir(outputDir, { recursive: true });
const files = await readdir(outputDir, { recursive: true });
const componentFiles = files
.map((f) => f.toString())
.filter((f) => f.includes('.front-component.'))
@@ -1,11 +1,11 @@
import * as fs from 'fs-extra';
import { readdir } from 'node:fs/promises';
import { join } from 'path';
export const defineLogicFunctionsTests = (appPath: string): void => {
describe('logicFunctions', () => {
it('should have built logicFunctions at root level', async () => {
const outputDir = join(appPath, '.twenty/output');
const files = await fs.readdir(outputDir, { recursive: true });
const files = await readdir(outputDir, { recursive: true });
const functionFiles = files
.map((f) => f.toString())
.filter((f) => f.includes('.function.'))
@@ -1,15 +1,15 @@
import * as fs from 'fs-extra';
import { join } from 'path';
import { type Manifest } from 'twenty-shared/application';
import { normalizeManifestForComparison } from '@/cli/__tests__/integration/utils/normalize-manifest.util';
import { EXPECTED_MANIFEST } from '@/cli/__tests__/apps/root-app/__integration__/app-dev/expected-manifest';
import { normalizeManifestForComparison } from '@/cli/__tests__/integration/utils/normalize-manifest.util';
import { pathExists, readJson } from '@/cli/utilities/file/fs-utils';
export const defineManifestTests = (appPath: string): void => {
describe('manifest', () => {
it('should have generated manifest.json', async () => {
const manifestPath = join(appPath, '.twenty/output/manifest.json');
const exists = await fs.pathExists(manifestPath);
const exists = await pathExists(manifestPath);
expect(exists).toBe(true);
});
@@ -17,7 +17,7 @@ export const defineManifestTests = (appPath: string): void => {
it('should have correct manifest content', async () => {
const manifestPath = join(appPath, '.twenty/output/manifest.json');
const manifest: Manifest = normalizeManifestForComparison(
await fs.readJSON(manifestPath),
await readJson(manifestPath),
);
expect(manifest).toEqual(EXPECTED_MANIFEST);
@@ -1,12 +1,14 @@
import { getConfigPath } from '@/cli/utilities/config/get-config-path';
import * as fs from 'fs-extra';
import { writeFile } from 'node:fs/promises';
import * as path from 'path';
import { beforeAll } from 'vitest';
import { ensureDir } from '@/cli/utilities/file/fs-utils';
import { getConfigPath } from '@/cli/utilities/config/get-config-path';
const testConfigPath = getConfigPath();
beforeAll(async () => {
await fs.ensureDir(path.dirname(testConfigPath));
await ensureDir(path.dirname(testConfigPath));
const configFile = {
profiles: {
@@ -17,5 +19,5 @@ beforeAll(async () => {
},
};
await fs.writeFile(testConfigPath, JSON.stringify(configFile, null, 2));
await writeFile(testConfigPath, JSON.stringify(configFile, null, 2));
});
@@ -1,8 +1,9 @@
import { AppDevCommand } from '@/cli/commands/app/app-dev';
import * as fs from 'fs-extra';
import { join } from 'path';
import { OUTPUT_DIR } from 'twenty-shared/application';
import { AppDevCommand } from '@/cli/commands/app/app-dev';
import { pathExists } from '@/cli/utilities/file/fs-utils';
export type RunAppDevResult = {
success: boolean;
events?: { message: string; status: string }[];
@@ -23,7 +24,7 @@ export const runAppDevInProcess = async (options: {
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
if (await fs.pathExists(manifestPath)) {
if (await pathExists(manifestPath)) {
await new Promise((resolve) => setTimeout(resolve, 500));
await command.close();
@@ -1,23 +1,25 @@
import chalk from 'chalk';
import inquirer from 'inquirer';
import { writeFile } from 'node:fs/promises';
import { join, relative } from 'path';
import { SyncableEntity } from 'twenty-shared/application';
import { FieldMetadataType } from 'twenty-shared/types';
import { assertUnreachable } from 'twenty-shared/utils';
import { v4 } from 'uuid';
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/current-execution-directory';
import { convertToLabel } from '@/cli/utilities/entity/entity-label';
import { getFieldBaseFile } from '@/cli/utilities/entity/entity-field-template';
import { getFrontComponentBaseFile } from '@/cli/utilities/entity/entity-front-component-template';
import { getLogicFunctionBaseFile } from '@/cli/utilities/entity/entity-logic-function-template';
import { getNavigationMenuItemBaseFile } from '@/cli/utilities/entity/entity-navigation-menu-item-template';
import { convertToLabel } from '@/cli/utilities/entity/entity-label';
import { getObjectBaseFile } from '@/cli/utilities/entity/entity-object-template';
import { getPageLayoutBaseFile } from '@/cli/utilities/entity/entity-page-layout-template';
import { getRoleBaseFile } from '@/cli/utilities/entity/entity-role-template';
import { getSkillBaseFile } from '@/cli/utilities/entity/entity-skill-template';
import { getViewBaseFile } from '@/cli/utilities/entity/entity-view-template';
import chalk from 'chalk';
import * as fs from 'fs-extra';
import inquirer from 'inquirer';
import kebabcase from 'lodash.kebabcase';
import { join, relative } from 'path';
import { SyncableEntity } from 'twenty-shared/application';
import { FieldMetadataType } from 'twenty-shared/types';
import { assertUnreachable } from 'twenty-shared/utils';
import { getFieldBaseFile } from '@/cli/utilities/entity/entity-field-template';
import { v4 } from 'uuid';
import { ensureDir, pathExists } from '@/cli/utilities/file/fs-utils';
import { kebabCase } from '@/cli/utilities/string/kebab-case';
const APP_FOLDER = 'src';
@@ -34,20 +36,20 @@ export class EntityAddCommand {
? join(CURRENT_EXECUTION_DIRECTORY, path)
: join(CURRENT_EXECUTION_DIRECTORY, APP_FOLDER, entityName);
await fs.ensureDir(appPath);
await ensureDir(appPath);
const { name, file } = await this.getEntityData(entity);
const filePath = join(appPath, this.getFileName(name, entity));
if (await fs.pathExists(filePath)) {
if (await pathExists(filePath)) {
const { overwrite } = await this.handleFileExist();
if (!overwrite) {
return;
}
}
await fs.writeFile(filePath, file);
await writeFile(filePath, file);
console.log(
chalk.green(`✓ Created ${entityName}:`),
@@ -197,7 +199,7 @@ export class EntityAddCommand {
const viewUniversalIdentifier = v4();
const viewFile = getViewBaseFile({
name: `all-${kebabcase(objectName)}`,
name: `all-${kebabCase(objectName)}`,
universalIdentifier: viewUniversalIdentifier,
objectUniversalIdentifier: this.lastObjectUniversalIdentifier,
});
@@ -210,12 +212,12 @@ export class EntityAddCommand {
this.getFolderName(SyncableEntity.View),
);
await fs.ensureDir(viewFolderPath);
await ensureDir(viewFolderPath);
const viewFileName = `all-${kebabcase(objectName)}.ts`;
const viewFileName = `all-${kebabCase(objectName)}.ts`;
const viewFilePath = join(viewFolderPath, viewFileName);
if (await fs.pathExists(viewFilePath)) {
if (await pathExists(viewFilePath)) {
const { overwrite } = await this.handleFileExist();
if (!overwrite) {
@@ -223,7 +225,7 @@ export class EntityAddCommand {
}
}
await fs.writeFile(viewFilePath, viewFile);
await writeFile(viewFilePath, viewFile);
console.log(
chalk.green(`✓ Created view:`),
@@ -243,12 +245,12 @@ export class EntityAddCommand {
this.getFolderName(SyncableEntity.NavigationMenuItem),
);
await fs.ensureDir(navFolderPath);
await ensureDir(navFolderPath);
const navFileName = `${kebabcase(objectName)}.ts`;
const navFileName = `${kebabCase(objectName)}.ts`;
const navFilePath = join(navFolderPath, navFileName);
if (await fs.pathExists(navFilePath)) {
if (await pathExists(navFilePath)) {
const { overwrite } = await this.handleFileExist();
if (!overwrite) {
@@ -256,7 +258,7 @@ export class EntityAddCommand {
}
}
await fs.writeFile(navFilePath, navFile);
await writeFile(navFilePath, navFile);
console.log(
chalk.green(`✓ Created navigation menu item:`),
@@ -471,16 +473,16 @@ export class EntityAddCommand {
}
getFolderName(entity: SyncableEntity) {
return `${kebabcase(entity)}s`;
return `${kebabCase(entity)}s`;
}
getFileName(name: string, entity: SyncableEntity) {
switch (entity) {
case SyncableEntity.FrontComponent: {
return `${kebabcase(name)}.tsx`;
return `${kebabCase(name)}.tsx`;
}
default: {
return `${kebabcase(name)}.ts`;
return `${kebabCase(name)}.ts`;
}
}
}
@@ -1,3 +1,13 @@
import crypto from 'crypto';
import { readFile } from 'node:fs/promises';
import { dirname, join } from 'path';
import {
NODE_ESM_CJS_BANNER,
OUTPUT_DIR,
type Manifest,
} from 'twenty-shared/application';
import { FileFolder } from 'twenty-shared/types';
import { esbuildOneShotBuild } from '@/cli/utilities/build/common/esbuild-one-shot-build';
import {
LOGIC_FUNCTION_EXTERNAL_MODULES,
@@ -7,15 +17,13 @@ import { FRONT_COMPONENT_EXTERNAL_MODULES } from '@/cli/utilities/build/common/f
import { getFrontComponentBuildPlugins } from '@/cli/utilities/build/common/front-component-build/utils/get-front-component-build-plugins';
import { type OnFileBuiltCallback } from '@/cli/utilities/build/common/restartable-watcher-interface';
import { type EntityFilePaths } from '@/cli/utilities/build/manifest/manifest-extract-config';
import crypto from 'crypto';
import * as fs from 'fs-extra';
import { dirname, join } from 'path';
import {
NODE_ESM_CJS_BANNER,
OUTPUT_DIR,
type Manifest,
} from 'twenty-shared/application';
import { FileFolder } from 'twenty-shared/types';
copy,
emptyDir,
ensureDir,
pathExists,
pathExistsSync,
} from '@/cli/utilities/file/fs-utils';
export type AppBuildOptions = {
appPath: string;
@@ -39,8 +47,8 @@ export const buildApplication = async (
): Promise<AppBuildResult> => {
const outputDir = join(options.appPath, OUTPUT_DIR);
await fs.ensureDir(outputDir);
await fs.emptyDir(outputDir);
await ensureDir(outputDir);
await emptyDir(outputDir);
const builtFileInfos = new Map<string, BuiltFileInfo>();
@@ -112,7 +120,7 @@ export const buildApplication = async (
appPath: options.appPath,
fileFolder: FileFolder.Dependencies,
filePaths: ['package.json', 'yarn.lock'].filter((filePath) =>
fs.pathExistsSync(join(options.appPath, filePath)),
pathExistsSync(join(options.appPath, filePath)),
),
collectFileBuilt,
});
@@ -134,17 +142,17 @@ const copyStaticFiles = async ({
for (const sourcePath of filePaths) {
const absoluteSourcePath = join(appPath, sourcePath);
if (!(await fs.pathExists(absoluteSourcePath))) {
if (!(await pathExists(absoluteSourcePath))) {
continue;
}
const builtPath = join(OUTPUT_DIR, sourcePath);
const absoluteBuiltPath = join(appPath, builtPath);
await fs.ensureDir(dirname(absoluteBuiltPath));
await fs.copy(absoluteSourcePath, absoluteBuiltPath);
await ensureDir(dirname(absoluteBuiltPath));
await copy(absoluteSourcePath, absoluteBuiltPath);
const content = await fs.readFile(absoluteBuiltPath);
const content = await readFile(absoluteBuiltPath);
const checksum = crypto.createHash('md5').update(content).digest('hex');
collectFileBuilt({
@@ -1,6 +1,7 @@
import * as fs from 'fs-extra';
import path from 'path';
import { remove } from '@/cli/utilities/file/fs-utils';
export const cleanupRemovedFiles = async (
outputDir: string,
oldPaths: string[],
@@ -14,7 +15,7 @@ export const cleanupRemovedFiles = async (
const outputFile = path.join(outputDir, outputBaseName);
const sourceMapFile = `${outputFile}.map`;
await fs.remove(outputFile);
await fs.remove(sourceMapFile);
await remove(outputFile);
await remove(sourceMapFile);
}
};
@@ -1,6 +1,6 @@
import crypto from 'crypto';
import type * as esbuild from 'esbuild';
import * as fs from 'fs-extra';
import { readFile } from 'node:fs/promises';
import path from 'path';
import { type OnFileBuiltCallback } from '@/cli/utilities/build/common/restartable-watcher-interface';
import { type FileFolder } from 'twenty-shared/types';
@@ -31,7 +31,7 @@ export const processEsbuildResult = async ({
result.metafile?.outputs?.[outputFile]?.entryPoint || '';
const relativeSourcePath = path.relative(appPath, absoluteSourcePath);
const content = await fs.readFile(absoluteBuiltFile);
const content = await readFile(absoluteBuiltFile);
const checksum = crypto.createHash('md5').update(content).digest('hex');
const lastChecksum = lastChecksums.get(relativeBuiltPath);
@@ -1,9 +1,16 @@
import chokidar, { type FSWatcher } from 'chokidar';
import crypto from 'crypto';
import * as fs from 'fs-extra';
import { readFile } from 'node:fs/promises';
import { dirname, join, relative } from 'path';
import { type FileFolder } from 'twenty-shared/types';
import { OUTPUT_DIR } from 'twenty-shared/application';
import { type FileFolder } from 'twenty-shared/types';
import {
copy,
ensureDir,
pathExists,
remove,
} from '@/cli/utilities/file/fs-utils';
export type AssetWatcherOptions = {
appPath: string;
@@ -37,7 +44,7 @@ export class FileUploadWatcher {
);
for (const rootPath of rootPaths) {
const exists = await fs.pathExists(rootPath);
const exists = await pathExists(rootPath);
if (!exists) {
return;
}
@@ -74,10 +81,10 @@ export class FileUploadWatcher {
const outputPath = join(OUTPUT_DIR, sourcePath);
const absoluteOutputPath = join(this.appPath, outputPath);
await fs.ensureDir(dirname(absoluteOutputPath));
await fs.copy(absoluteFilePath, absoluteOutputPath);
await ensureDir(dirname(absoluteOutputPath));
await copy(absoluteFilePath, absoluteOutputPath);
const content = await fs.readFile(absoluteOutputPath);
const content = await readFile(absoluteOutputPath);
const checksum = crypto.createHash('md5').update(content).digest('hex');
this.handleFileBuilt({
@@ -93,6 +100,6 @@ export class FileUploadWatcher {
const builtPath = join(OUTPUT_DIR, sourcePath);
const absoluteBuiltPath = join(this.appPath, builtPath);
await fs.remove(absoluteBuiltPath);
await remove(absoluteBuiltPath);
}
}
@@ -1,5 +1,5 @@
import type * as esbuild from 'esbuild';
import * as fs from 'node:fs/promises';
import { readFile } from 'node:fs/promises';
import { unwrapDefineFrontComponentToDirectExport } from './utils/unwrap-define-front-component-to-direct-export';
@@ -10,7 +10,7 @@ export const jsxTransformToRemoteDomWorkerFormatPlugin: esbuild.Plugin = {
{ filter: /\.tsx$/ },
async ({ path }): Promise<esbuild.OnLoadResult> => {
try {
const frontComponentSourceCode = await fs.readFile(path, 'utf8');
const frontComponentSourceCode = await readFile(path, 'utf8');
const transformedContents = unwrapDefineFrontComponentToDirectExport(
frontComponentSourceCode,
@@ -1,4 +1,4 @@
import * as fs from 'fs/promises';
import { readFile, writeFile } from 'node:fs/promises';
import path from 'path';
import type * as esbuild from 'esbuild';
@@ -19,11 +19,11 @@ export const stripCommentsPlugin: esbuild.Plugin = {
for (const outputFile of outputFiles) {
const absolutePath = path.resolve(outputFile);
const content = await fs.readFile(absolutePath, 'utf-8');
const content = await readFile(absolutePath, 'utf-8');
const stripped = content.replace(SINGLE_LINE_COMMENT_PATTERN, '');
if (stripped !== content) {
await fs.writeFile(absolutePath, stripped, 'utf-8');
await writeFile(absolutePath, stripped, 'utf-8');
}
}
});
@@ -1,7 +1,8 @@
import { spawn, type ChildProcess } from 'node:child_process';
import * as fs from 'fs-extra';
import path from 'node:path';
import { pathExists } from '@/cli/utilities/file/fs-utils';
import {
parseTscOutputLine,
type TypecheckError,
@@ -28,7 +29,7 @@ export class TscWatcher {
async start(): Promise<void> {
const tscPath = path.join(this.appPath, 'node_modules', '.bin', 'tsc');
if (!(await fs.pathExists(tscPath))) {
if (!(await pathExists(tscPath))) {
return;
}
@@ -15,9 +15,9 @@ import {
import { type ObjectConfig } from '@/sdk/objects/object-config';
import { type PageLayoutConfig } from '@/sdk/page-layouts/page-layout-config';
import { type ViewConfig } from '@/sdk/views/view-config';
import { glob } from 'fast-glob';
import { readFile } from 'fs-extra';
import { readFile } from 'node:fs/promises';
import { basename, extname, relative } from 'path';
import { glob } from 'tinyglobby';
import {
type ApplicationManifest,
type AssetManifest,
@@ -1,7 +1,8 @@
import { conditionalAvailabilityTransformPlugin } from '@/cli/utilities/build/common/conditional-availability/conditional-availability-transform-plugin';
import { type ValidationResult } from '@/sdk';
import { pathExists, remove } from '@/cli/utilities/file/fs-utils';
import * as esbuild from 'esbuild';
import * as fs from 'fs-extra';
import { mkdtemp, writeFile } from 'node:fs/promises';
import { createRequire } from 'module';
import os from 'os';
import path from 'path';
@@ -48,7 +49,7 @@ const loadModule = async ({
appPath: string;
}): Promise<Record<string, unknown>> => {
const tsconfigPath = path.join(appPath, 'tsconfig.json');
const hasTsconfig = await fs.pathExists(tsconfigPath);
const hasTsconfig = await pathExists(tsconfigPath);
// Resolve react from the app's node_modules for the alias
const appRequire = createRequire(path.join(appPath, 'package.json'));
@@ -81,15 +82,15 @@ const loadModule = async ({
const code = result.outputFiles[0].text;
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'twenty-manifest-'));
const tempDir = await mkdtemp(path.join(os.tmpdir(), 'twenty-manifest-'));
const tempFile = path.join(tempDir, 'module.cjs');
try {
await fs.writeFile(tempFile, code);
await writeFile(tempFile, code);
return require(tempFile) as Record<string, unknown>;
return appRequire(tempFile) as Record<string, unknown>;
} finally {
await fs.remove(tempDir);
await remove(tempDir);
}
};
@@ -1,21 +1,21 @@
import * as fs from 'fs-extra';
import { buildManifest } from '@/cli/utilities/build/manifest/manifest-build';
import { ensureDir, pathExists, readJson } from '@/cli/utilities/file/fs-utils';
import path from 'path';
import { type Manifest, OUTPUT_DIR } from 'twenty-shared/application';
import { buildManifest } from '@/cli/utilities/build/manifest/manifest-build';
export const readManifestFromFile = async (
appPath: string,
): Promise<Manifest | null> => {
const outputDir = path.join(appPath, OUTPUT_DIR);
await fs.ensureDir(outputDir);
await ensureDir(outputDir);
const manifestPath = path.join(outputDir, 'manifest.json');
if (!(await fs.pathExists(manifestPath))) {
if (!(await pathExists(manifestPath))) {
const { manifest } = await buildManifest(appPath);
return manifest;
}
return await fs.readJson(manifestPath);
return await readJson(manifestPath);
};
@@ -1,5 +1,6 @@
import * as fs from 'fs-extra';
import path from 'path';
import { ensureDir, writeJson } from '@/cli/utilities/file/fs-utils';
import { type Manifest, OUTPUT_DIR } from 'twenty-shared/application';
export const writeManifestToOutput = async (
@@ -7,10 +8,10 @@ export const writeManifestToOutput = async (
manifest: Manifest,
): Promise<string> => {
const outputDir = path.join(appPath, OUTPUT_DIR);
await fs.ensureDir(outputDir);
await ensureDir(outputDir);
const manifestPath = path.join(outputDir, 'manifest.json');
await fs.writeJSON(manifestPath, manifest, { spaces: 2 });
await writeJson(manifestPath, manifest);
return manifestPath;
};
@@ -1,8 +1,16 @@
import { appendFile, writeFile } from 'node:fs/promises';
import { join } from 'path';
import { ApiService } from '@/cli/utilities/api/api-service';
import {
emptyDir,
ensureDir,
move,
pathExists,
remove,
} from '@/cli/utilities/file/fs-utils';
import twentyClientTemplateSource from '@/cli/utilities/client/twenty-client-template.ts?raw';
import { generate } from '@genql/cli';
import * as fs from 'fs-extra';
import { join } from 'path';
import { DEFAULT_API_URL_NAME, GENERATED_DIR } from 'twenty-shared/application';
type ClientWrapperOptions = {
@@ -94,8 +102,8 @@ export class ClientService {
);
}
await fs.ensureDir(tempPath);
await fs.emptyDir(tempPath);
await ensureDir(tempPath);
await emptyDir(tempPath);
await Promise.all([
generate({
@@ -127,8 +135,8 @@ export class ClientService {
await this.writeBarrelIndex(tempPath);
await fs.remove(outputPath);
await fs.move(tempPath, outputPath);
await remove(outputPath);
await move(tempPath, outputPath);
}
async ensureGeneratedClientStub({
@@ -138,18 +146,18 @@ export class ClientService {
}): Promise<void> {
const outputPath = this.resolveGeneratedPath(appPath);
if (await fs.pathExists(join(outputPath, 'index.ts'))) {
if (await pathExists(join(outputPath, 'index.ts'))) {
return;
}
await fs.ensureDir(join(outputPath, 'core'));
await fs.ensureDir(join(outputPath, 'metadata'));
await ensureDir(join(outputPath, 'core'));
await ensureDir(join(outputPath, 'metadata'));
await fs.writeFile(
await writeFile(
join(outputPath, 'core', 'index.ts'),
'export class CoreApiClient {}\n',
);
await fs.writeFile(
await writeFile(
join(outputPath, 'metadata', 'index.ts'),
'export class MetadataApiClient {}\n',
);
@@ -167,7 +175,7 @@ export * as CoreSchema from './core/schema';
export * as MetadataSchema from './metadata/schema';
`;
await fs.writeFile(join(outputDir, 'index.ts'), barrelContent);
await writeFile(join(outputDir, 'index.ts'), barrelContent);
}
private async injectClientWrapper(
@@ -176,6 +184,6 @@ export * as MetadataSchema from './metadata/schema';
): Promise<void> {
const clientContent = buildClientWrapperSource(options);
await fs.appendFile(join(output, 'index.ts'), clientContent);
await appendFile(join(output, 'index.ts'), clientContent);
}
}
@@ -1,6 +1,8 @@
import * as fs from 'fs-extra';
import { readFile, writeFile } from 'node:fs/promises';
import * as path from 'path';
import { ensureDir, ensureFile } from '@/cli/utilities/file/fs-utils';
import { getConfigPath } from '@/cli/utilities/config/get-config-path';
export type TwentyConfig = {
@@ -40,8 +42,8 @@ export class ConfigService {
}
private async readRawConfig(): Promise<PersistedConfig> {
await fs.ensureFile(this.configPath);
const content = await fs.readFile(this.configPath, 'utf8');
await ensureFile(this.configPath);
const content = await readFile(this.configPath, 'utf8');
return JSON.parse(content || '{}');
}
@@ -90,8 +92,8 @@ export class ConfigService {
raw.profiles[profile] = { ...currentProfile, ...config };
await fs.ensureDir(path.dirname(this.configPath));
await fs.writeFile(this.configPath, JSON.stringify(raw, null, 2));
await ensureDir(path.dirname(this.configPath));
await writeFile(this.configPath, JSON.stringify(raw, null, 2));
}
async clearConfig(): Promise<void> {
@@ -114,8 +116,8 @@ export class ConfigService {
raw.apiUrl = defaultConfig.apiUrl;
}
await fs.ensureDir(path.dirname(this.configPath));
await fs.writeFile(this.configPath, JSON.stringify(raw, null, 2));
await ensureDir(path.dirname(this.configPath));
await writeFile(this.configPath, JSON.stringify(raw, null, 2));
}
private getDefaultConfig(): TwentyConfig {
@@ -155,7 +157,7 @@ export class ConfigService {
async setDefaultWorkspace(name: string): Promise<void> {
const raw = await this.readRawConfig();
raw.defaultWorkspace = name;
await fs.ensureDir(path.dirname(this.configPath));
await fs.writeFile(this.configPath, JSON.stringify(raw, null, 2));
await ensureDir(path.dirname(this.configPath));
await writeFile(this.configPath, JSON.stringify(raw, null, 2));
}
}
@@ -15,7 +15,7 @@ import {
import { SyncApplicationOrchestratorStep } from '@/cli/utilities/dev/orchestrator/steps/sync-application-orchestrator-step';
import { UploadFilesOrchestratorStep } from '@/cli/utilities/dev/orchestrator/steps/upload-files-orchestrator-step';
import { serializeError } from '@/cli/utilities/error/serialize-error';
import * as fs from 'fs-extra';
import { emptyDir, ensureDir } from '@/cli/utilities/file/fs-utils';
import path from 'path';
import { OUTPUT_DIR, type Manifest } from 'twenty-shared/application';
@@ -91,8 +91,8 @@ export class DevModeOrchestrator {
async start(): Promise<void> {
const outputDir = path.join(this.state.appPath, OUTPUT_DIR);
await fs.ensureDir(outputDir);
await fs.emptyDir(outputDir);
await ensureDir(outputDir);
await emptyDir(outputDir);
await this.clientService.ensureGeneratedClientStub({
appPath: this.state.appPath,
@@ -3,8 +3,9 @@ import {
type OrchestratorStateBuiltFileInfo,
} from '@/cli/utilities/dev/orchestrator/dev-mode-orchestrator-state';
import { FileUploader } from '@/cli/utilities/file/file-uploader';
import { copy, ensureDir, pathExists } from '@/cli/utilities/file/fs-utils';
import crypto from 'crypto';
import * as fs from 'fs-extra';
import { readFile } from 'node:fs/promises';
import { join } from 'path';
import {
OUTPUT_DIR,
@@ -126,24 +127,24 @@ export class UploadFilesOrchestratorStep {
GENERATED_DIR,
);
if (!(await fs.pathExists(generatedDir))) {
if (!(await pathExists(generatedDir))) {
return;
}
const outputDir = join(appPath, OUTPUT_DIR, API_CLIENT_DIR);
await fs.ensureDir(outputDir);
await ensureDir(outputDir);
for (const fileName of API_CLIENT_FILES) {
const absoluteSourcePath = join(generatedDir, fileName);
if (!(await fs.pathExists(absoluteSourcePath))) {
if (!(await pathExists(absoluteSourcePath))) {
continue;
}
await fs.copy(absoluteSourcePath, join(outputDir, fileName));
await copy(absoluteSourcePath, join(outputDir, fileName));
const content = await fs.readFile(absoluteSourcePath);
const content = await readFile(absoluteSourcePath);
const checksum = crypto.createHash('md5').update(content).digest('hex');
const builtPath = join(OUTPUT_DIR, API_CLIENT_DIR, fileName);
@@ -1,4 +1,4 @@
import kebabCase from 'lodash.kebabcase';
import { kebabCase } from '@/cli/utilities/string/kebab-case';
import { v4 } from 'uuid';
export const getFrontComponentBaseFile = ({
@@ -1,4 +1,4 @@
import kebabCase from 'lodash.kebabcase';
import { kebabCase } from '@/cli/utilities/string/kebab-case';
import { v4 } from 'uuid';
export const getLogicFunctionBaseFile = ({
@@ -1,4 +1,4 @@
import kebabCase from 'lodash.kebabcase';
import { kebabCase } from '@/cli/utilities/string/kebab-case';
import { v4 } from 'uuid';
export const getNavigationMenuItemBaseFile = ({
@@ -1,4 +1,4 @@
import kebabCase from 'lodash.kebabcase';
import { kebabCase } from '@/cli/utilities/string/kebab-case';
import { v4 } from 'uuid';
export const getRoleBaseFile = ({
@@ -1,4 +1,4 @@
import kebabCase from 'lodash.kebabcase';
import { kebabCase } from '@/cli/utilities/string/kebab-case';
import { v4 } from 'uuid';
export const getSkillBaseFile = ({
@@ -1,4 +1,4 @@
import kebabCase from 'lodash.kebabcase';
import { kebabCase } from '@/cli/utilities/string/kebab-case';
import { v4 } from 'uuid';
export const getViewBaseFile = ({
@@ -0,0 +1,234 @@
import { mkdtemp, readFile, writeFile, readdir, stat } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { join } from 'node:path';
import os from 'node:os';
import {
copy,
emptyDir,
ensureDir,
ensureFile,
move,
pathExists,
pathExistsSync,
readJson,
remove,
writeJson,
} from '@/cli/utilities/file/fs-utils';
let tmpDir: string;
beforeEach(async () => {
tmpDir = await mkdtemp(join(os.tmpdir(), 'fs-utils-test-'));
});
afterEach(async () => {
await remove(tmpDir);
});
describe('pathExists', () => {
it('should return true for an existing file', async () => {
const filePath = join(tmpDir, 'exists.txt');
await writeFile(filePath, 'hello');
expect(await pathExists(filePath)).toBe(true);
});
it('should return false for a non-existent path', async () => {
expect(await pathExists(join(tmpDir, 'nope.txt'))).toBe(false);
});
});
describe('pathExistsSync', () => {
it('should return true for an existing directory', () => {
expect(pathExistsSync(tmpDir)).toBe(true);
});
it('should return false for a non-existent path', () => {
expect(pathExistsSync(join(tmpDir, 'nope'))).toBe(false);
});
});
describe('ensureDir', () => {
it('should create a deeply nested directory', async () => {
const deepPath = join(tmpDir, 'a', 'b', 'c');
await ensureDir(deepPath);
expect(existsSync(deepPath)).toBe(true);
const dirStat = await stat(deepPath);
expect(dirStat.isDirectory()).toBe(true);
});
it('should not throw when the directory already exists', async () => {
await expect(ensureDir(tmpDir)).resolves.not.toThrow();
});
});
describe('ensureFile', () => {
it('should create the file and parent directories when missing', async () => {
const filePath = join(tmpDir, 'deep', 'nested', 'file.txt');
await ensureFile(filePath);
expect(existsSync(filePath)).toBe(true);
const content = await readFile(filePath, 'utf-8');
expect(content).toBe('');
});
it('should not overwrite an existing file', async () => {
const filePath = join(tmpDir, 'existing.txt');
await writeFile(filePath, 'keep me');
await ensureFile(filePath);
const content = await readFile(filePath, 'utf-8');
expect(content).toBe('keep me');
});
});
describe('emptyDir', () => {
it('should remove all contents including nested subdirectories', async () => {
await writeFile(join(tmpDir, 'a.txt'), 'a');
const subDir = join(tmpDir, 'nested');
await ensureDir(subDir);
await writeFile(join(subDir, 'deep.txt'), 'deep');
await emptyDir(tmpDir);
const entries = await readdir(tmpDir);
expect(entries).toEqual([]);
expect(existsSync(tmpDir)).toBe(true);
});
it('should create the directory when it does not exist', async () => {
const newDir = join(tmpDir, 'new-dir');
await emptyDir(newDir);
expect(existsSync(newDir)).toBe(true);
const entries = await readdir(newDir);
expect(entries).toEqual([]);
});
it('should rethrow non-ENOENT errors', async () => {
const filePath = join(tmpDir, 'not-a-dir.txt');
await writeFile(filePath, 'file');
await expect(emptyDir(filePath)).rejects.toMatchObject({
code: 'ENOTDIR',
});
});
});
describe('copy', () => {
it('should copy a file without removing the source', async () => {
const src = join(tmpDir, 'source.txt');
const dest = join(tmpDir, 'dest.txt');
await writeFile(src, 'content');
await copy(src, dest);
expect(await readFile(dest, 'utf-8')).toBe('content');
expect(await readFile(src, 'utf-8')).toBe('content');
});
it('should recursively copy a directory', async () => {
const srcDir = join(tmpDir, 'src-dir');
await ensureDir(srcDir);
await writeFile(join(srcDir, 'inner.txt'), 'inner');
const destDir = join(tmpDir, 'dest-dir');
await copy(srcDir, destDir);
expect(await readFile(join(destDir, 'inner.txt'), 'utf-8')).toBe('inner');
});
});
describe('move', () => {
it('should move a file to a new location', async () => {
const src = join(tmpDir, 'to-move.txt');
const dest = join(tmpDir, 'moved.txt');
await writeFile(src, 'data');
await move(src, dest);
expect(existsSync(src)).toBe(false);
expect(await readFile(dest, 'utf-8')).toBe('data');
});
it('should rethrow non-EXDEV errors', async () => {
const src = join(tmpDir, 'does-not-exist.txt');
const dest = join(tmpDir, 'dest.txt');
await expect(move(src, dest)).rejects.toThrow();
});
});
describe('remove', () => {
it('should delete a file', async () => {
const filePath = join(tmpDir, 'to-delete.txt');
await writeFile(filePath, 'bye');
await remove(filePath);
expect(existsSync(filePath)).toBe(false);
});
it('should recursively delete a directory', async () => {
const dirPath = join(tmpDir, 'to-delete-dir');
await ensureDir(dirPath);
await writeFile(join(dirPath, 'child.txt'), 'child');
await remove(dirPath);
expect(existsSync(dirPath)).toBe(false);
});
it('should not throw when the path does not exist', async () => {
await expect(remove(join(tmpDir, 'already-gone'))).resolves.not.toThrow();
});
});
describe('readJson', () => {
it('should parse a JSON file and return typed data', async () => {
const filePath = join(tmpDir, 'data.json');
await writeFile(filePath, JSON.stringify({ key: 'value', count: 42 }));
const result = await readJson<{ key: string; count: number }>(filePath);
expect(result).toEqual({ key: 'value', count: 42 });
});
it('should throw on invalid JSON', async () => {
const filePath = join(tmpDir, 'bad.json');
await writeFile(filePath, '{ broken }');
await expect(readJson(filePath)).rejects.toThrow();
});
it('should throw when the file does not exist', async () => {
await expect(readJson(join(tmpDir, 'nope.json'))).rejects.toThrow();
});
});
describe('writeJson', () => {
it('should write pretty-printed JSON with a trailing newline', async () => {
const filePath = join(tmpDir, 'out.json');
await writeJson(filePath, { hello: 'world' });
const raw = await readFile(filePath, 'utf-8');
expect(raw).toBe('{\n "hello": "world"\n}\n');
});
it('should produce valid JSON that readJson can parse', async () => {
const filePath = join(tmpDir, 'roundtrip.json');
const data = { nested: { array: [1, 2, 3] } };
await writeJson(filePath, data);
const result = await readJson(filePath);
expect(result).toEqual(data);
});
});
@@ -1,5 +1,6 @@
import path from 'path';
import * as fs from 'fs-extra';
import { pathExists } from '@/cli/utilities/file/fs-utils';
export const findPathFile = async (
appPath: string,
@@ -7,7 +8,7 @@ export const findPathFile = async (
): Promise<string> => {
const jsonPath = path.join(appPath, fileName);
if (await fs.pathExists(jsonPath)) {
if (await pathExists(jsonPath)) {
return jsonPath;
}
@@ -1,4 +1,4 @@
import * as fs from 'fs-extra';
import { readFile } from 'node:fs/promises';
import { type ParseError, parse as parseJsonc } from 'jsonc-parser';
export interface JsoncParseOptions {
@@ -48,7 +48,7 @@ export const parseJsoncFile = async <T = object>(
options: JsoncParseOptions = {},
): Promise<T> => {
try {
const content = await fs.readFile(filePath, 'utf8');
const content = await readFile(filePath, 'utf8');
return parseJsoncString(content, options);
} catch (error) {
if (error instanceof JsoncParseError) {
@@ -1,67 +0,0 @@
import * as fs from 'fs-extra';
import path from 'path';
import archiver from 'archiver';
/**
* TarballService creates distributable .tar.gz archives from built applications.
*/
export class TarballService {
/**
* Create a tarball from the build output directory.
*
* @param sourceDir - The directory containing built files
* @param outputPath - The path for the output .tar.gz file
* @returns The absolute path to the created tarball
*/
async create(options: {
sourceDir: string;
outputPath: string;
}): Promise<string> {
const { sourceDir, outputPath } = options;
// Ensure the source directory exists
if (!(await fs.pathExists(sourceDir))) {
throw new Error(`Source directory does not exist: ${sourceDir}`);
}
// Ensure the output directory exists
await fs.ensureDir(path.dirname(outputPath));
// Normalize the output path to have .tar.gz extension
const normalizedOutputPath = outputPath.endsWith('.tar.gz')
? outputPath
: `${outputPath}.tar.gz`;
return new Promise((resolve, reject) => {
const output = fs.createWriteStream(normalizedOutputPath);
const archive = archiver('tar', {
gzip: true,
gzipOptions: { level: 9 }, // Maximum compression
});
output.on('close', () => {
resolve(normalizedOutputPath);
});
archive.on('error', (err: Error) => {
reject(err);
});
archive.on('warning', (err: Error & { code?: string }) => {
if (err.code === 'ENOENT') {
// Log warnings about missing files
console.warn('Archive warning:', err.message);
} else {
reject(err);
}
});
archive.pipe(output);
// Add the entire source directory to the archive
archive.directory(sourceDir, false);
archive.finalize();
});
}
}
@@ -0,0 +1,93 @@
// ESM-native helpers that have no direct native fs equivalent.
// For standard fs operations (readFile, writeFile, mkdir, etc.),
// import directly from 'node:fs/promises' or 'node:fs'.
import {
access,
cp,
mkdir,
readFile,
readdir,
rename as fsRename,
rm,
writeFile,
} from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { dirname, join } from 'node:path';
export const pathExists = async (filePath: string): Promise<boolean> => {
try {
await access(filePath);
return true;
} catch {
return false;
}
};
export const pathExistsSync = (filePath: string): boolean =>
existsSync(filePath);
export const ensureDir = (dirPath: string) =>
mkdir(dirPath, { recursive: true });
export const ensureFile = async (filePath: string): Promise<void> => {
await mkdir(dirname(filePath), { recursive: true });
try {
await access(filePath);
} catch {
await writeFile(filePath, '');
}
};
export const emptyDir = async (dirPath: string): Promise<void> => {
let entries: string[];
try {
entries = await readdir(dirPath);
} catch (error: unknown) {
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
await mkdir(dirPath, { recursive: true });
return;
}
throw error;
}
await Promise.all(
entries.map((entry) =>
rm(join(dirPath, entry), { recursive: true, force: true }),
),
);
};
export const copy = (src: string, dest: string) =>
cp(src, dest, { recursive: true });
// Falls back to copy+delete when rename fails across devices
export const move = async (src: string, dest: string): Promise<void> => {
try {
await fsRename(src, dest);
} catch (error: unknown) {
if (error instanceof Error && 'code' in error && error.code === 'EXDEV') {
await cp(src, dest, { recursive: true });
await rm(src, { recursive: true, force: true });
} else {
throw error;
}
}
};
export const remove = (filePath: string) =>
rm(filePath, { recursive: true, force: true });
export const readJson = async <T = unknown>(filePath: string): Promise<T> => {
const content = await readFile(filePath, 'utf-8');
return JSON.parse(content) as T;
};
export const writeJson = async (
filePath: string,
data: unknown,
): Promise<void> => {
await writeFile(filePath, JSON.stringify(data, null, 2) + '\n');
};
@@ -0,0 +1,9 @@
export const kebabCase = (input: string): string =>
input
.replace(/([a-z])([A-Z])/g, '$1-$2')
.replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
.replace(/([a-zA-Z])(\d)/g, '$1-$2')
.replace(/(\d)([a-zA-Z])/g, '$1-$2')
.replace(/[\s_]+/g, '-')
.replace(/[^a-zA-Z0-9-]+/g, '-')
.toLowerCase();
+1
View File
@@ -192,6 +192,7 @@
"@nestjs/devtools-integration": "^0.2.1",
"@nestjs/schematics": "^11.0.9",
"@nestjs/testing": "^11.1.15",
"@types/archiver": "^6.0.0",
"@types/babel__preset-env": "7.10.0",
"@types/bytes": "^3.1.1",
"@types/dompurify": "^3.0.5",
+3 -24
View File
@@ -24212,7 +24212,7 @@ __metadata:
languageName: node
linkType: hard
"@types/lodash.kebabcase@npm:^4.1.7, @types/lodash.kebabcase@npm:^4.1.9":
"@types/lodash.kebabcase@npm:^4.1.7":
version: 4.1.9
resolution: "@types/lodash.kebabcase@npm:4.1.9"
dependencies:
@@ -37121,19 +37121,6 @@ __metadata:
languageName: node
linkType: hard
"fast-glob@npm:^3.3.0":
version: 3.3.3
resolution: "fast-glob@npm:3.3.3"
dependencies:
"@nodelib/fs.stat": "npm:^2.0.2"
"@nodelib/fs.walk": "npm:^1.2.3"
glob-parent: "npm:^5.1.2"
merge2: "npm:^1.3.0"
micromatch: "npm:^4.0.8"
checksum: 10c0/f6aaa141d0d3384cf73cbcdfc52f475ed293f6d5b65bfc5def368b09163a9f7e5ec2b3014d80f733c405f58e470ee0cc451c2937685045cddcdeaa24199c43fe
languageName: node
linkType: hard
"fast-json-patch@npm:^3.0.0-1":
version: 3.1.1
resolution: "fast-json-patch@npm:3.1.1"
@@ -57509,37 +57496,28 @@ __metadata:
"@sniptt/guards": "npm:^0.2.0"
"@storybook/addon-vitest": "npm:^10.2.13"
"@storybook/react-vite": "npm:^10.2.13"
"@types/archiver": "npm:^6.0.0"
"@types/fs-extra": "npm:^11.0.0"
"@types/inquirer": "npm:^9.0.0"
"@types/lodash.camelcase": "npm:^4.3.7"
"@types/lodash.kebabcase": "npm:^4.1.9"
"@types/node": "npm:^24.0.0"
"@types/react": "npm:18.2.66"
"@types/react-dom": "npm:18.2.22"
"@vitest/browser-playwright": "npm:^4.0.18"
archiver: "npm:^7.0.1"
axios: "npm:^1.13.5"
chalk: "npm:^5.3.0"
chokidar: "npm:^4.0.0"
commander: "npm:^12.0.0"
dotenv: "npm:^16.4.0"
esbuild: "npm:^0.25.0"
fast-glob: "npm:^3.3.0"
form-data: "npm:^4.0.5"
fs-extra: "npm:^11.2.0"
graphql: "npm:^16.8.1"
graphql-sse: "npm:^2.5.4"
ink: "npm:^5.1.1"
inquirer: "npm:^10.0.0"
jsonc-parser: "npm:^3.2.0"
lodash.camelcase: "npm:^4.3.0"
lodash.kebabcase: "npm:^4.1.1"
playwright: "npm:^1.56.1"
preact: "npm:^10.28.3"
react: "npm:^18.2.0"
react-dom: "npm:^18.2.0"
storybook: "npm:^10.2.13"
tinyglobby: "npm:^0.2.15"
ts-morph: "npm:^25.0.0"
tsx: "npm:^4.7.0"
twenty-shared: "workspace:*"
@@ -57630,6 +57608,7 @@ __metadata:
"@sentry/node": "npm:^10.27.0"
"@sentry/profiling-node": "npm:^10.27.0"
"@sniptt/guards": "npm:0.2.0"
"@types/archiver": "npm:^6.0.0"
"@types/babel__preset-env": "npm:7.10.0"
"@types/bytes": "npm:^3.1.1"
"@types/dompurify": "npm:^3.0.5"