[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:
@@ -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,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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
+4
-3
@@ -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);
|
||||
|
||||
+3
-3
@@ -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
|
||||
|
||||
+3
-2
@@ -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();
|
||||
|
||||
|
||||
+2
-2
@@ -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.'))
|
||||
|
||||
+2
-2
@@ -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.'))
|
||||
|
||||
+4
-4
@@ -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));
|
||||
});
|
||||
|
||||
+4
-3
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+3
-3
@@ -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,
|
||||
|
||||
+7
-6
@@ -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,
|
||||
|
||||
+7
-6
@@ -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
-1
@@ -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();
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user