Add sdk build (#17335)
## Summary Adds `app:build` command as a one-shot version of `app:dev` - builds manifest, functions, and front components once then exits (no watching). **Changes:** - Added `watch` option to `FunctionsWatcher` and `FrontComponentsWatcher` to support both watch and one-shot modes - Created `AppBuildCommand` reusing the same build logic as `app:dev` with `watch: false` - Added integration tests for `app:build` on both `rich-app` and `root-app` - Updated manifest types: `handlerPath` → `sourceHandlerPath` + `builtHandlerPath`, `componentPath` → `sourceComponentPath` + `builtComponentPath` - Updated test app `package.json` scripts to match `create-twenty-app` template
This commit is contained in:
@@ -198,6 +198,7 @@ const createPackageJson = async ({
|
||||
'auth:switch': 'twenty auth:switch',
|
||||
'auth:list': 'twenty auth:list',
|
||||
'app:dev': 'twenty app:dev',
|
||||
'app:build': 'twenty app:build',
|
||||
'app:sync': 'twenty app:sync',
|
||||
'entity:add': 'twenty entity:add',
|
||||
'app:generate': 'twenty app:generate',
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@ export type FrontComponentType = React.ComponentType<any>;
|
||||
|
||||
export type FrontComponentConfig = Omit<
|
||||
FrontComponentManifest,
|
||||
'componentPath' | 'componentName'
|
||||
'sourceComponentPath' | 'builtComponentPath' | 'componentName'
|
||||
> & {
|
||||
name?: string;
|
||||
description?: string;
|
||||
|
||||
@@ -7,7 +7,7 @@ export type FunctionHandler = (...args: any[]) => any | Promise<any>;
|
||||
|
||||
export type FunctionConfig = Omit<
|
||||
ServerlessFunctionManifest,
|
||||
'handlerPath' | 'handlerName'
|
||||
'sourceHandlerPath' | 'builtHandlerPath' | 'handlerName'
|
||||
> & {
|
||||
name?: string;
|
||||
description?: string;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "invalid-app",
|
||||
"version": "0.0.1",
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^24.5.0",
|
||||
@@ -9,13 +9,32 @@
|
||||
},
|
||||
"packageManager": "yarn@4.9.2",
|
||||
"scripts": {
|
||||
"dev": "twenty app dev",
|
||||
"sync": "twenty app sync"
|
||||
"auth:login": "twenty auth:login",
|
||||
"auth:logout": "twenty auth:logout",
|
||||
"auth:status": "twenty auth:status",
|
||||
"auth:switch": "twenty auth:switch",
|
||||
"auth:list": "twenty auth:list",
|
||||
"app:dev": "twenty app:dev",
|
||||
"app:build": "twenty app:build",
|
||||
"app:sync": "twenty app:sync",
|
||||
"entity:add": "twenty entity:add",
|
||||
"app:generate": "twenty app:generate",
|
||||
"function:logs": "twenty function:logs",
|
||||
"function:execute": "twenty function:execute",
|
||||
"app:uninstall": "twenty app:uninstall",
|
||||
"help": "twenty help",
|
||||
"lint": "eslint",
|
||||
"lint:fix": "eslint --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"twenty-sdk": "latest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.7.2"
|
||||
"typescript": "^5.9.3",
|
||||
"@types/node": "^24.7.2",
|
||||
"@types/react": "^19.0.2",
|
||||
"react": "^19.0.2",
|
||||
"eslint": "^9.32.0",
|
||||
"typescript-eslint": "^8.50.0"
|
||||
}
|
||||
}
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { runAppBuild } from '@/cli/__tests__/integration/utils/run-app-build.util';
|
||||
import { type RunCliCommandResult } from '@/cli/__tests__/integration/utils/run-cli-command.util';
|
||||
import { join } from 'path';
|
||||
|
||||
import { defineFrontComponentsTests } from '../app-dev/tests/front-components.tests';
|
||||
import { defineFunctionsTests } from '../app-dev/tests/functions.tests';
|
||||
import { defineManifestTests } from '../app-dev/tests/manifest.tests';
|
||||
import { defineConsoleOutputTests } from './tests/console-output.tests';
|
||||
|
||||
const APP_PATH = join(__dirname, '../..');
|
||||
|
||||
describe('rich-app app:build', () => {
|
||||
let result: RunCliCommandResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runAppBuild({ appPath: APP_PATH });
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
}, 60000);
|
||||
|
||||
defineConsoleOutputTests(() => result);
|
||||
defineManifestTests(APP_PATH);
|
||||
defineFunctionsTests(APP_PATH);
|
||||
defineFrontComponentsTests(APP_PATH);
|
||||
});
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
import { getOutputByPrefix } from '@/cli/__tests__/integration/utils/get-output-by-prefix.util';
|
||||
import { type RunCliCommandResult } from '@/cli/__tests__/integration/utils/run-cli-command.util';
|
||||
|
||||
export const defineConsoleOutputTests = (
|
||||
getResult: () => RunCliCommandResult,
|
||||
): void => {
|
||||
describe('console output', () => {
|
||||
it('should contain init messages', () => {
|
||||
const output = getOutputByPrefix(getResult().output, 'init');
|
||||
|
||||
expect(output).toContain('[init] 🚀 Building Twenty Application');
|
||||
expect(output).toContain('[init] 📁 App Path:');
|
||||
expect(output).toContain('[init] ✅ Build completed successfully');
|
||||
});
|
||||
|
||||
it('should contain manifest-watch messages', () => {
|
||||
const output = getOutputByPrefix(getResult().output, 'manifest-watch');
|
||||
|
||||
expect(output).toContain('[manifest-watch] 🔄 Building...');
|
||||
expect(output).toContain('[manifest-watch] ✓ Loaded "Hello World"');
|
||||
expect(output).toContain('[manifest-watch] ✓ Found 2 object(s)');
|
||||
expect(output).toContain('[manifest-watch] ✓ Found 4 function(s)');
|
||||
expect(output).toContain('[manifest-watch] ✓ Found 4 front component(s)');
|
||||
expect(output).toContain('[manifest-watch] ✓ Found 2 role(s)');
|
||||
expect(output).toContain('[manifest-watch] ✓ Written to');
|
||||
});
|
||||
|
||||
it('should contain functions-watch messages', () => {
|
||||
const output = getOutputByPrefix(getResult().output, 'functions-watch');
|
||||
|
||||
expect(output).toContain('[functions-watch] 📦 Building...');
|
||||
expect(output).toContain('[functions-watch] ✓ Built');
|
||||
});
|
||||
|
||||
it('should contain front-components-watch messages', () => {
|
||||
const output = getOutputByPrefix(getResult().output, 'front-components-watch');
|
||||
|
||||
expect(output).toContain('[front-components-watch] 🎨 Building...');
|
||||
expect(output).toContain('[front-components-watch] ✓ Built');
|
||||
});
|
||||
|
||||
it('should not contain watching messages', () => {
|
||||
const output = getResult().output;
|
||||
|
||||
expect(output).not.toContain('👀 Watching for changes...');
|
||||
expect(output).not.toContain('📂 Watcher started');
|
||||
});
|
||||
});
|
||||
};
|
||||
+39
-16
@@ -16,31 +16,35 @@
|
||||
},
|
||||
"frontComponents": [
|
||||
{
|
||||
"builtComponentPath": "front-components/src/root.front-component.mjs",
|
||||
"componentName": "RootComponent",
|
||||
"componentPath": "src/root.front-component.tsx",
|
||||
"description": "A root-level front component",
|
||||
"name": "root-component",
|
||||
"sourceComponentPath": "src/root.front-component.tsx",
|
||||
"universalIdentifier": "a0a1a2a3-a4a5-4000-8000-000000000001"
|
||||
},
|
||||
{
|
||||
"builtComponentPath": "front-components/src/components/card.front-component.mjs",
|
||||
"componentName": "CardDisplay",
|
||||
"componentPath": "src/components/card.front-component.tsx",
|
||||
"description": "A component using an external component file",
|
||||
"name": "card-component",
|
||||
"sourceComponentPath": "src/components/card.front-component.tsx",
|
||||
"universalIdentifier": "i0i1i2i3-i4i5-4000-8000-000000000001"
|
||||
},
|
||||
{
|
||||
"builtComponentPath": "front-components/src/components/greeting.front-component.mjs",
|
||||
"componentName": "GreetingComponent",
|
||||
"componentPath": "src/components/greeting.front-component.tsx",
|
||||
"description": "A component that uses greeting utility",
|
||||
"name": "greeting-component",
|
||||
"sourceComponentPath": "src/components/greeting.front-component.tsx",
|
||||
"universalIdentifier": "h0h1h2h3-h4h5-4000-8000-000000000001"
|
||||
},
|
||||
{
|
||||
"builtComponentPath": "front-components/src/components/test.front-component.mjs",
|
||||
"componentName": "TestComponent",
|
||||
"componentPath": "src/components/test.front-component.tsx",
|
||||
"description": "A test front component",
|
||||
"name": "test-component",
|
||||
"sourceComponentPath": "src/components/test.front-component.tsx",
|
||||
"universalIdentifier": "f1234567-abcd-4000-8000-000000000001"
|
||||
}
|
||||
],
|
||||
@@ -190,7 +194,7 @@
|
||||
],
|
||||
"packageJson": {
|
||||
"name": "rich-app",
|
||||
"version": "0.0.1",
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^24.5.0",
|
||||
@@ -199,18 +203,33 @@
|
||||
},
|
||||
"packageManager": "yarn@4.9.2",
|
||||
"scripts": {
|
||||
"create-entity": "twenty app add",
|
||||
"dev": "twenty app dev",
|
||||
"generate": "twenty app generate",
|
||||
"sync": "twenty app sync",
|
||||
"uninstall": "twenty app uninstall",
|
||||
"auth": "twenty auth login"
|
||||
"auth:login": "twenty auth:login",
|
||||
"auth:logout": "twenty auth:logout",
|
||||
"auth:status": "twenty auth:status",
|
||||
"auth:switch": "twenty auth:switch",
|
||||
"auth:list": "twenty auth:list",
|
||||
"app:dev": "twenty app:dev",
|
||||
"app:build": "twenty app:build",
|
||||
"app:sync": "twenty app:sync",
|
||||
"entity:add": "twenty entity:add",
|
||||
"app:generate": "twenty app:generate",
|
||||
"function:logs": "twenty function:logs",
|
||||
"function:execute": "twenty function:execute",
|
||||
"app:uninstall": "twenty app:uninstall",
|
||||
"help": "twenty help",
|
||||
"lint": "eslint",
|
||||
"lint:fix": "eslint --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"twenty-sdk": "latest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.7.2"
|
||||
"typescript": "^5.9.3",
|
||||
"@types/node": "^24.7.2",
|
||||
"@types/react": "^19.0.2",
|
||||
"react": "^19.0.2",
|
||||
"eslint": "^9.32.0",
|
||||
"typescript-eslint": "^8.50.0"
|
||||
}
|
||||
},
|
||||
"roles": [
|
||||
@@ -263,9 +282,10 @@
|
||||
],
|
||||
"serverlessFunctions": [
|
||||
{
|
||||
"builtHandlerPath": "functions/src/root.function.mjs",
|
||||
"handlerName": "rootHandler",
|
||||
"handlerPath": "src/root.function.ts",
|
||||
"name": "root-function",
|
||||
"sourceHandlerPath": "src/root.function.ts",
|
||||
"timeoutSeconds": 5,
|
||||
"triggers": [
|
||||
{
|
||||
@@ -279,9 +299,10 @@
|
||||
"universalIdentifier": "f0f1f2f3-f4f5-4000-8000-000000000001"
|
||||
},
|
||||
{
|
||||
"builtHandlerPath": "functions/src/functions/greeting.function.mjs",
|
||||
"handlerName": "greetingHandler",
|
||||
"handlerPath": "src/functions/greeting.function.ts",
|
||||
"name": "greeting-function",
|
||||
"sourceHandlerPath": "src/functions/greeting.function.ts",
|
||||
"timeoutSeconds": 5,
|
||||
"triggers": [
|
||||
{
|
||||
@@ -295,9 +316,10 @@
|
||||
"universalIdentifier": "g0g1g2g3-g4g5-4000-8000-000000000001"
|
||||
},
|
||||
{
|
||||
"builtHandlerPath": "functions/src/utils/test-function-2.util.mjs",
|
||||
"handlerName": "testFunction2",
|
||||
"handlerPath": "src/utils/test-function-2.util.ts",
|
||||
"name": "test-function-2",
|
||||
"sourceHandlerPath": "src/utils/test-function-2.util.ts",
|
||||
"timeoutSeconds": 2,
|
||||
"triggers": [
|
||||
{
|
||||
@@ -309,9 +331,10 @@
|
||||
"universalIdentifier": "eb3ffc98-88ec-45d4-9b4a-56833b219ccb"
|
||||
},
|
||||
{
|
||||
"builtHandlerPath": "functions/src/functions/test-function.function.mjs",
|
||||
"handlerName": "handler",
|
||||
"handlerPath": "src/functions/test-function.function.ts",
|
||||
"name": "test-function",
|
||||
"sourceHandlerPath": "src/functions/test-function.function.ts",
|
||||
"timeoutSeconds": 2,
|
||||
"triggers": [
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "rich-app",
|
||||
"version": "0.0.1",
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^24.5.0",
|
||||
@@ -9,17 +9,32 @@
|
||||
},
|
||||
"packageManager": "yarn@4.9.2",
|
||||
"scripts": {
|
||||
"create-entity": "twenty app add",
|
||||
"dev": "twenty app dev",
|
||||
"generate": "twenty app generate",
|
||||
"sync": "twenty app sync",
|
||||
"uninstall": "twenty app uninstall",
|
||||
"auth": "twenty auth login"
|
||||
"auth:login": "twenty auth:login",
|
||||
"auth:logout": "twenty auth:logout",
|
||||
"auth:status": "twenty auth:status",
|
||||
"auth:switch": "twenty auth:switch",
|
||||
"auth:list": "twenty auth:list",
|
||||
"app:dev": "twenty app:dev",
|
||||
"app:build": "twenty app:build",
|
||||
"app:sync": "twenty app:sync",
|
||||
"entity:add": "twenty entity:add",
|
||||
"app:generate": "twenty app:generate",
|
||||
"function:logs": "twenty function:logs",
|
||||
"function:execute": "twenty function:execute",
|
||||
"app:uninstall": "twenty app:uninstall",
|
||||
"help": "twenty help",
|
||||
"lint": "eslint",
|
||||
"lint:fix": "eslint --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"twenty-sdk": "latest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.7.2"
|
||||
"typescript": "^5.9.3",
|
||||
"@types/node": "^24.7.2",
|
||||
"@types/react": "^19.0.2",
|
||||
"react": "^19.0.2",
|
||||
"eslint": "^9.32.0",
|
||||
"typescript-eslint": "^8.50.0"
|
||||
}
|
||||
}
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { join } from 'path';
|
||||
|
||||
import { runAppBuild } from '../../../../integration/utils/run-app-build.util';
|
||||
import { type RunCliCommandResult } from '../../../../integration/utils/run-cli-command.util';
|
||||
import { defineConsoleOutputTests } from './tests/console-output.tests';
|
||||
import { defineFrontComponentsTests } from '../app-dev/tests/front-components.tests';
|
||||
import { defineFunctionsTests } from '../app-dev/tests/functions.tests';
|
||||
import { defineManifestTests } from '../app-dev/tests/manifest.tests';
|
||||
|
||||
const APP_PATH = join(__dirname, '../..');
|
||||
|
||||
describe('root-app app:build', () => {
|
||||
let result: RunCliCommandResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runAppBuild({ appPath: APP_PATH });
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
}, 60000);
|
||||
|
||||
defineConsoleOutputTests(() => result);
|
||||
defineManifestTests(APP_PATH);
|
||||
defineFunctionsTests(APP_PATH);
|
||||
defineFrontComponentsTests(APP_PATH);
|
||||
});
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
import { getOutputByPrefix } from '@/cli/__tests__/integration/utils/get-output-by-prefix.util';
|
||||
import { type RunCliCommandResult } from '@/cli/__tests__/integration/utils/run-cli-command.util';
|
||||
|
||||
export const defineConsoleOutputTests = (
|
||||
getResult: () => RunCliCommandResult,
|
||||
): void => {
|
||||
describe('console output', () => {
|
||||
it('should contain init messages', () => {
|
||||
const output = getOutputByPrefix(getResult().output, 'init');
|
||||
|
||||
expect(output).toContain('[init] 🚀 Building Twenty Application');
|
||||
expect(output).toContain('[init] 📁 App Path:');
|
||||
expect(output).toContain('[init] ✅ Build completed successfully');
|
||||
});
|
||||
|
||||
it('should contain manifest-watch messages', () => {
|
||||
const output = getOutputByPrefix(getResult().output, 'manifest-watch');
|
||||
|
||||
expect(output).toContain('[manifest-watch] 🔄 Building...');
|
||||
expect(output).toContain('[manifest-watch] ✓ Loaded "Root App"');
|
||||
expect(output).toContain('[manifest-watch] ✓ Found 1 object(s)');
|
||||
expect(output).toContain('[manifest-watch] ✓ Found 1 function(s)');
|
||||
expect(output).toContain('[manifest-watch] ✓ Found 1 front component(s)');
|
||||
expect(output).toContain('[manifest-watch] ✓ Found 1 role(s)');
|
||||
expect(output).toContain('[manifest-watch] ✓ Written to');
|
||||
});
|
||||
|
||||
it('should contain functions-watch messages', () => {
|
||||
const output = getOutputByPrefix(getResult().output, 'functions-watch');
|
||||
|
||||
expect(output).toContain('[functions-watch] 📦 Building...');
|
||||
expect(output).toContain('[functions-watch] ✓ Built');
|
||||
});
|
||||
|
||||
it('should contain front-components-watch messages', () => {
|
||||
const output = getOutputByPrefix(getResult().output, 'front-components-watch');
|
||||
|
||||
expect(output).toContain('[front-components-watch] 🎨 Building...');
|
||||
expect(output).toContain('[front-components-watch] ✓ Built');
|
||||
});
|
||||
|
||||
it('should not contain watching messages', () => {
|
||||
const output = getResult().output;
|
||||
|
||||
expect(output).not.toContain('👀 Watching for changes...');
|
||||
expect(output).not.toContain('📂 Watcher started');
|
||||
});
|
||||
});
|
||||
};
|
||||
+4
-2
@@ -40,7 +40,8 @@
|
||||
}
|
||||
],
|
||||
"handlerName": "myHandler",
|
||||
"handlerPath": "my.function.ts"
|
||||
"sourceHandlerPath": "my.function.ts",
|
||||
"builtHandlerPath": "functions/my.function.mjs"
|
||||
}
|
||||
],
|
||||
"frontComponents": [
|
||||
@@ -49,7 +50,8 @@
|
||||
"name": "my-component",
|
||||
"description": "A root-level front component",
|
||||
"componentName": "MyComponent",
|
||||
"componentPath": "my.front-component.tsx"
|
||||
"sourceComponentPath": "my.front-component.tsx",
|
||||
"builtComponentPath": "front-components/my.front-component.mjs"
|
||||
}
|
||||
],
|
||||
"roles": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "root-app",
|
||||
"version": "0.0.1",
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^24.5.0",
|
||||
@@ -9,17 +9,32 @@
|
||||
},
|
||||
"packageManager": "yarn@4.9.2",
|
||||
"scripts": {
|
||||
"create-entity": "twenty app add",
|
||||
"dev": "twenty app dev",
|
||||
"generate": "twenty app generate",
|
||||
"sync": "twenty app sync",
|
||||
"uninstall": "twenty app uninstall",
|
||||
"auth": "twenty auth login"
|
||||
"auth:login": "twenty auth:login",
|
||||
"auth:logout": "twenty auth:logout",
|
||||
"auth:status": "twenty auth:status",
|
||||
"auth:switch": "twenty auth:switch",
|
||||
"auth:list": "twenty auth:list",
|
||||
"app:dev": "twenty app:dev",
|
||||
"app:build": "twenty app:build",
|
||||
"app:sync": "twenty app:sync",
|
||||
"entity:add": "twenty entity:add",
|
||||
"app:generate": "twenty app:generate",
|
||||
"function:logs": "twenty function:logs",
|
||||
"function:execute": "twenty function:execute",
|
||||
"app:uninstall": "twenty app:uninstall",
|
||||
"help": "twenty help",
|
||||
"lint": "eslint",
|
||||
"lint:fix": "eslint --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"twenty-sdk": "latest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.7.2"
|
||||
"typescript": "^5.9.3",
|
||||
"@types/node": "^24.7.2",
|
||||
"@types/react": "^19.0.2",
|
||||
"react": "^19.0.2",
|
||||
"eslint": "^9.32.0",
|
||||
"typescript-eslint": "^8.50.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { runCliCommand, type RunCliCommandResult } from './run-cli-command.util';
|
||||
|
||||
export type RunAppBuildOptions = {
|
||||
appPath: string;
|
||||
timeout?: number;
|
||||
};
|
||||
|
||||
export const runAppBuild = (options: RunAppBuildOptions): Promise<RunCliCommandResult> => {
|
||||
const { appPath, timeout = 30000 } = options;
|
||||
|
||||
// app:build runs once and exits, so we don't wait for specific output
|
||||
return runCliCommand({
|
||||
command: 'app:build',
|
||||
args: [appPath],
|
||||
timeout,
|
||||
});
|
||||
};
|
||||
@@ -54,15 +54,21 @@ export const runCliCommand = (
|
||||
? [waitForOutput]
|
||||
: [];
|
||||
|
||||
let isResolved = false;
|
||||
child.stdout?.on('data', (data: Buffer) => {
|
||||
output += data.toString();
|
||||
if (
|
||||
!isResolved &&
|
||||
waitForOutputs.length > 0 &&
|
||||
waitForOutputs.every((w) => output.includes(w))
|
||||
) {
|
||||
isResolved = true;
|
||||
clearTimeout(timeoutId);
|
||||
child.kill();
|
||||
resolve({ success: true, output });
|
||||
// Wait a bit before killing to allow any in-progress file writes to complete
|
||||
setTimeout(() => {
|
||||
child.kill();
|
||||
resolve({ success: true, output });
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -94,9 +94,7 @@ export const registerCommands = (program: Command): void => {
|
||||
...options,
|
||||
appPath: formatPath(appPath),
|
||||
});
|
||||
if (!result.success) {
|
||||
process.exit(1);
|
||||
}
|
||||
process.exit(result.success ? 0 : 1);
|
||||
} catch {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
@@ -1,28 +1,79 @@
|
||||
import { type ApiResponse } from '@/cli/utilities/api/types/api-response.types';
|
||||
import { runManifestBuild } from '@/cli/utilities/build/manifest/manifest-build';
|
||||
import { createLogger } from '@/cli/utilities/build/common/logger';
|
||||
import { FrontComponentsWatcher } from '@/cli/utilities/build/front-components/front-component-watcher';
|
||||
import { FunctionsWatcher } from '@/cli/utilities/build/functions/function-watcher';
|
||||
import { runManifestBuild, type ManifestBuildResult } from '@/cli/utilities/build/manifest/manifest-build';
|
||||
import { manifestExtractFromFileServer } from '@/cli/utilities/build/manifest/manifest-extract-from-file-server';
|
||||
import { CURRENT_EXECUTION_DIRECTORY } from '@/cli/utilities/config/constants/current-execution-directory';
|
||||
import chalk from 'chalk';
|
||||
|
||||
export type BuildCommandOptions = {
|
||||
const initLogger = createLogger('init');
|
||||
|
||||
export type AppBuildOptions = {
|
||||
appPath?: string;
|
||||
};
|
||||
|
||||
export class AppBuildCommand {
|
||||
async execute(options: BuildCommandOptions): Promise<ApiResponse<null>> {
|
||||
const appPath = options.appPath ?? CURRENT_EXECUTION_DIRECTORY;
|
||||
private functionsBuilder: FunctionsWatcher | null = null;
|
||||
private frontComponentsBuilder: FrontComponentsWatcher | null = null;
|
||||
|
||||
console.log(chalk.blue('🚀 Building Twenty Application'));
|
||||
console.log(chalk.gray(`📁 App Path: ${appPath}`));
|
||||
private appPath: string = '';
|
||||
|
||||
async execute(options: AppBuildOptions): Promise<ApiResponse<null>> {
|
||||
this.appPath = options.appPath ?? CURRENT_EXECUTION_DIRECTORY;
|
||||
|
||||
initLogger.log('🚀 Building Twenty Application');
|
||||
initLogger.log(`📁 App Path: ${this.appPath}`);
|
||||
console.log('');
|
||||
|
||||
const { manifest } = await runManifestBuild(appPath);
|
||||
const buildResult = await this.runBuild();
|
||||
|
||||
if (!manifest) {
|
||||
if (!buildResult) {
|
||||
return { success: false, error: 'Build failed' };
|
||||
}
|
||||
|
||||
console.log(chalk.green('✅ Build completed successfully'));
|
||||
initLogger.success('✅ Build completed successfully');
|
||||
|
||||
return { success: true, data: null };
|
||||
}
|
||||
|
||||
private async runBuild(): Promise<ManifestBuildResult | null> {
|
||||
const buildResult = await runManifestBuild(this.appPath);
|
||||
|
||||
if (!buildResult.manifest) {
|
||||
return null;
|
||||
}
|
||||
|
||||
await this.buildFunctions(buildResult);
|
||||
await this.buildFrontComponents(buildResult);
|
||||
|
||||
await this.cleanup();
|
||||
|
||||
return buildResult;
|
||||
}
|
||||
|
||||
private async buildFunctions(buildResult: ManifestBuildResult): Promise<void> {
|
||||
this.functionsBuilder = new FunctionsWatcher({
|
||||
appPath: this.appPath,
|
||||
buildResult,
|
||||
watch: false,
|
||||
});
|
||||
|
||||
await this.functionsBuilder.start();
|
||||
}
|
||||
|
||||
private async buildFrontComponents(buildResult: ManifestBuildResult): Promise<void> {
|
||||
this.frontComponentsBuilder = new FrontComponentsWatcher({
|
||||
appPath: this.appPath,
|
||||
buildResult,
|
||||
watch: false,
|
||||
});
|
||||
|
||||
await this.frontComponentsBuilder.start();
|
||||
}
|
||||
|
||||
private async cleanup(): Promise<void> {
|
||||
await this.functionsBuilder?.close();
|
||||
await this.frontComponentsBuilder?.close();
|
||||
await manifestExtractFromFileServer.closeViteServer();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,4 +10,5 @@ export interface RestartableWatcher {
|
||||
export type RestartableWatcherOptions = {
|
||||
appPath: string;
|
||||
buildResult: ManifestBuildResult | null;
|
||||
watch?: boolean;
|
||||
};
|
||||
|
||||
+32
-9
@@ -29,10 +29,13 @@ export class FrontComponentsWatcher implements RestartableWatcher {
|
||||
private componentPaths: string[];
|
||||
private esBuildContext: esbuild.BuildContext | null = null;
|
||||
private isRestarting = false;
|
||||
private watchMode: boolean;
|
||||
private lastInputsSignature: string | null = null;
|
||||
|
||||
constructor(options: RestartableWatcherOptions) {
|
||||
this.appPath = options.appPath;
|
||||
this.componentPaths = options.buildResult?.filePaths.frontComponents ?? [];
|
||||
this.watchMode = options.watch ?? true;
|
||||
}
|
||||
|
||||
shouldRestart(result: ManifestBuildResult): boolean {
|
||||
@@ -51,7 +54,9 @@ export class FrontComponentsWatcher implements RestartableWatcher {
|
||||
await this.createContext();
|
||||
} else {
|
||||
logger.log('No front components to build');
|
||||
logger.log('👀 Watching for changes...');
|
||||
if (this.watchMode) {
|
||||
logger.log('👀 Watching for changes...');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,6 +101,11 @@ export class FrontComponentsWatcher implements RestartableWatcher {
|
||||
entryPoints[entryName] = path.join(this.appPath, componentPath);
|
||||
}
|
||||
|
||||
const watchMode = this.watchMode;
|
||||
|
||||
// Capture reference for use in plugin callbacks
|
||||
const watcher = this;
|
||||
|
||||
this.esBuildContext = await esbuild.context({
|
||||
entryPoints,
|
||||
bundle: true,
|
||||
@@ -119,14 +129,25 @@ export class FrontComponentsWatcher implements RestartableWatcher {
|
||||
for (const error of result.errors) {
|
||||
logger.error(` ${error.text}`);
|
||||
}
|
||||
} else {
|
||||
const outputs = Object.keys(result.metafile?.outputs ?? {})
|
||||
.filter((file) => file.endsWith('.mjs'))
|
||||
.map((file) => path.relative(outputDir, file));
|
||||
return;
|
||||
}
|
||||
|
||||
for (const output of outputs) {
|
||||
logger.success(`✓ Built ${output}`);
|
||||
}
|
||||
const inputs = Object.keys(result.metafile?.inputs ?? {}).sort();
|
||||
const inputsSignature = inputs.join(',');
|
||||
|
||||
if (watcher.lastInputsSignature === inputsSignature) {
|
||||
return;
|
||||
}
|
||||
watcher.lastInputsSignature = inputsSignature;
|
||||
|
||||
const outputs = Object.keys(result.metafile?.outputs ?? {})
|
||||
.filter((file) => file.endsWith('.mjs'))
|
||||
.map((file) => path.relative(outputDir, file));
|
||||
|
||||
for (const output of outputs) {
|
||||
logger.success(`✓ Built ${output}`);
|
||||
}
|
||||
if (watchMode) {
|
||||
logger.log('👀 Watching for changes...');
|
||||
}
|
||||
});
|
||||
@@ -137,6 +158,8 @@ export class FrontComponentsWatcher implements RestartableWatcher {
|
||||
|
||||
await this.esBuildContext.rebuild();
|
||||
|
||||
await this.esBuildContext.watch();
|
||||
if (this.watchMode) {
|
||||
await this.esBuildContext.watch();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,10 +43,13 @@ export class FunctionsWatcher implements RestartableWatcher {
|
||||
private functionPaths: string[];
|
||||
private esBuildContext: esbuild.BuildContext | null = null;
|
||||
private isRestarting = false;
|
||||
private watchMode: boolean;
|
||||
private lastInputsSignature: string | null = null;
|
||||
|
||||
constructor(options: RestartableWatcherOptions) {
|
||||
this.appPath = options.appPath;
|
||||
this.functionPaths = options.buildResult?.filePaths.functions ?? [];
|
||||
this.watchMode = options.watch ?? true;
|
||||
}
|
||||
|
||||
shouldRestart(result: ManifestBuildResult): boolean {
|
||||
@@ -65,7 +68,9 @@ export class FunctionsWatcher implements RestartableWatcher {
|
||||
await this.createContext();
|
||||
} else {
|
||||
logger.log('No functions to build');
|
||||
logger.log('👀 Watching for changes...');
|
||||
if (this.watchMode) {
|
||||
logger.log('👀 Watching for changes...');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,6 +115,11 @@ export class FunctionsWatcher implements RestartableWatcher {
|
||||
entryPoints[entryName] = path.join(this.appPath, functionPath);
|
||||
}
|
||||
|
||||
const watchMode = this.watchMode;
|
||||
|
||||
// Capture reference for use in plugin callbacks
|
||||
const watcher = this;
|
||||
|
||||
this.esBuildContext = await esbuild.context({
|
||||
entryPoints,
|
||||
bundle: true,
|
||||
@@ -143,14 +153,25 @@ export class FunctionsWatcher implements RestartableWatcher {
|
||||
for (const error of result.errors) {
|
||||
logger.error(` ${error.text}`);
|
||||
}
|
||||
} else {
|
||||
const outputs = Object.keys(result.metafile?.outputs ?? {})
|
||||
.filter((file) => file.endsWith('.mjs'))
|
||||
.map((file) => path.relative(outputDir, file));
|
||||
return;
|
||||
}
|
||||
|
||||
for (const output of outputs) {
|
||||
logger.success(`✓ Built ${output}`);
|
||||
}
|
||||
const inputs = Object.keys(result.metafile?.inputs ?? {}).sort();
|
||||
const inputsSignature = inputs.join(',');
|
||||
|
||||
if (watcher.lastInputsSignature === inputsSignature) {
|
||||
return;
|
||||
}
|
||||
watcher.lastInputsSignature = inputsSignature;
|
||||
|
||||
const outputs = Object.keys(result.metafile?.outputs ?? {})
|
||||
.filter((file) => file.endsWith('.mjs'))
|
||||
.map((file) => path.relative(outputDir, file));
|
||||
|
||||
for (const output of outputs) {
|
||||
logger.success(`✓ Built ${output}`);
|
||||
}
|
||||
if (watchMode) {
|
||||
logger.log('👀 Watching for changes...');
|
||||
}
|
||||
});
|
||||
@@ -161,6 +182,8 @@ export class FunctionsWatcher implements RestartableWatcher {
|
||||
|
||||
await this.esBuildContext.rebuild();
|
||||
|
||||
await this.esBuildContext.watch();
|
||||
if (this.watchMode) {
|
||||
await this.esBuildContext.watch();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { glob } from 'fast-glob';
|
||||
import { type FrontComponentManifest } from 'twenty-shared/application';
|
||||
import { createLogger } from '../../common/logger';
|
||||
import { FRONT_COMPONENTS_DIR } from '../../front-components/constants';
|
||||
import { manifestExtractFromFileServer } from '../manifest-extract-from-file-server';
|
||||
import { type ValidationError } from '../manifest.types';
|
||||
import {
|
||||
@@ -12,7 +13,10 @@ import {
|
||||
|
||||
const logger = createLogger('manifest-watch');
|
||||
|
||||
type FrontComponentConfig = Omit<FrontComponentManifest, 'componentPath' | 'componentName'> & {
|
||||
type FrontComponentConfig = Omit<
|
||||
FrontComponentManifest,
|
||||
'sourceComponentPath' | 'builtComponentPath' | 'componentName'
|
||||
> & {
|
||||
component: { name: string };
|
||||
};
|
||||
|
||||
@@ -36,11 +40,13 @@ export class FrontComponentEntityBuilder
|
||||
);
|
||||
|
||||
const { component, ...rest } = config;
|
||||
const builtComponentPath = this.computeBuiltComponentPath(filePath);
|
||||
|
||||
manifests.push({
|
||||
...rest,
|
||||
componentName: component.name,
|
||||
componentPath: filePath,
|
||||
sourceComponentPath: filePath,
|
||||
builtComponentPath,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
@@ -52,6 +58,12 @@ export class FrontComponentEntityBuilder
|
||||
return { manifests, filePaths: componentFiles };
|
||||
}
|
||||
|
||||
private computeBuiltComponentPath(sourceComponentPath: string): string {
|
||||
const builtPath = sourceComponentPath.replace(/\.tsx?$/, '.mjs');
|
||||
|
||||
return `${FRONT_COMPONENTS_DIR}/${builtPath}`;
|
||||
}
|
||||
|
||||
validate(
|
||||
components: FrontComponentManifest[],
|
||||
errors: ValidationError[],
|
||||
@@ -75,7 +87,7 @@ export class FrontComponentEntityBuilder
|
||||
logger.log('📍 Entry points:');
|
||||
for (const component of components) {
|
||||
const name = component.name || component.universalIdentifier;
|
||||
logger.log(` - ${name} (${component.componentPath})`);
|
||||
logger.log(` - ${name} (${component.sourceComponentPath})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { glob } from 'fast-glob';
|
||||
import { type ServerlessFunctionManifest } from 'twenty-shared/application';
|
||||
import { createLogger } from '../../common/logger';
|
||||
import { FUNCTIONS_DIR } from '../../functions/constants';
|
||||
import { manifestExtractFromFileServer } from '../manifest-extract-from-file-server';
|
||||
import { type ValidationError } from '../manifest.types';
|
||||
import {
|
||||
@@ -12,6 +13,13 @@ import {
|
||||
|
||||
const logger = createLogger('manifest-watch');
|
||||
|
||||
type ExtractedFunctionManifest = Omit<
|
||||
ServerlessFunctionManifest,
|
||||
'sourceHandlerPath' | 'builtHandlerPath'
|
||||
> & {
|
||||
handlerPath: string;
|
||||
};
|
||||
|
||||
export class FunctionEntityBuilder
|
||||
implements ManifestEntityBuilder<ServerlessFunctionManifest>
|
||||
{
|
||||
@@ -27,12 +35,20 @@ export class FunctionEntityBuilder
|
||||
try {
|
||||
const absolutePath = `${appPath}/${filePath}`;
|
||||
|
||||
manifests.push(
|
||||
await manifestExtractFromFileServer.extractManifestFromFile<ServerlessFunctionManifest>(
|
||||
const extracted =
|
||||
await manifestExtractFromFileServer.extractManifestFromFile<ExtractedFunctionManifest>(
|
||||
absolutePath,
|
||||
{ entryProperty: 'handler' },
|
||||
),
|
||||
);
|
||||
);
|
||||
|
||||
const { handlerPath, ...rest } = extracted;
|
||||
const builtHandlerPath = this.computeBuiltHandlerPath(handlerPath);
|
||||
|
||||
manifests.push({
|
||||
...rest,
|
||||
sourceHandlerPath: handlerPath,
|
||||
builtHandlerPath,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to load function from ${filePath}: ${error instanceof Error ? error.message : String(error)}`,
|
||||
@@ -43,6 +59,12 @@ export class FunctionEntityBuilder
|
||||
return { manifests, filePaths: functionFiles };
|
||||
}
|
||||
|
||||
private computeBuiltHandlerPath(sourceHandlerPath: string): string {
|
||||
const builtPath = sourceHandlerPath.replace(/\.tsx?$/, '.mjs');
|
||||
|
||||
return `${FUNCTIONS_DIR}/${builtPath}`;
|
||||
}
|
||||
|
||||
validate(
|
||||
functions: ServerlessFunctionManifest[],
|
||||
errors: ValidationError[],
|
||||
@@ -120,7 +142,7 @@ export class FunctionEntityBuilder
|
||||
logger.log('📍 Entry points:');
|
||||
for (const fn of functions) {
|
||||
const name = fn.name || fn.universalIdentifier;
|
||||
logger.log(` - ${name} (${fn.handlerPath})`);
|
||||
logger.log(` - ${name} (${fn.sourceHandlerPath})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -947,7 +947,7 @@ export class ApplicationSyncService {
|
||||
name,
|
||||
code,
|
||||
timeoutSeconds: serverlessFunctionToSync.timeoutSeconds,
|
||||
handlerPath: serverlessFunctionToSync.handlerPath,
|
||||
handlerPath: serverlessFunctionToSync.sourceHandlerPath,
|
||||
handlerName: serverlessFunctionToSync.handlerName,
|
||||
toolInputSchema: serverlessFunctionToSync.toolInputSchema,
|
||||
isTool: serverlessFunctionToSync.isTool,
|
||||
@@ -991,7 +991,7 @@ export class ApplicationSyncService {
|
||||
code,
|
||||
universalIdentifier: serverlessFunctionToCreate.universalIdentifier,
|
||||
timeoutSeconds: serverlessFunctionToCreate.timeoutSeconds,
|
||||
handlerPath: serverlessFunctionToCreate.handlerPath,
|
||||
handlerPath: serverlessFunctionToCreate.sourceHandlerPath,
|
||||
handlerName: serverlessFunctionToCreate.handlerName,
|
||||
applicationId,
|
||||
serverlessFunctionLayerId,
|
||||
|
||||
@@ -2,6 +2,7 @@ export type FrontComponentManifest = {
|
||||
universalIdentifier: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
componentPath: string;
|
||||
sourceComponentPath: string;
|
||||
builtComponentPath: string;
|
||||
componentName: string;
|
||||
};
|
||||
|
||||
@@ -24,8 +24,8 @@ export type ServerlessFunctionManifest = SyncableEntityOptions & {
|
||||
description?: string;
|
||||
timeoutSeconds?: number;
|
||||
triggers: ServerlessFunctionTriggerManifest[];
|
||||
handlerPath: string;
|
||||
builtHandlerPath?: string; // Should be required when build mode implemented
|
||||
sourceHandlerPath: string;
|
||||
builtHandlerPath: string;
|
||||
handlerName: string;
|
||||
toolInputSchema?: InputJsonSchema;
|
||||
isTool?: boolean;
|
||||
|
||||
Reference in New Issue
Block a user