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:
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user