Improve API Client usage and add Typescript check (#18023)
## Summary https://github.com/user-attachments/assets/1e75cc9d-d9d2-4ef2-99f9-34450f5d8de7 Add background incremental type checking (`tsc --watch`) to the SDK dev mode, so type regressions are caught when the generated API client changes — without requiring a full rebuild of source files. Previously, removing a field from the data model would regenerate the API client, but existing front components/logic functions referencing the removed field wouldn't surface type errors (since their source didn't change, esbuild wouldn't rebuild them). ## What changed - **Background `tsc --watch`**: a long-lived TypeScript watcher runs alongside esbuild watchers, incrementally re-checking all files when the generated client changes. Only logs on state transitions (errors appear / errors clear) to stay quiet. - **Atomic client generation**: API client is now generated into a temp directory and swapped in atomically, avoiding a race condition where `tsc --watch` could see an empty `generated/` directory mid-regeneration. - **Step decoupling**: orchestrator steps no longer receive `uploadFilesStep` directly. Instead, they use callbacks (`onFileBuilt`, `onApiClientGenerated`), and each step manages its own `builtFileInfos` state. - **`apiClientChecksum` omitted from `ApplicationConfig`**: it's a build-time computed value, same as `packageJsonChecksum`. <img width="327" height="177" alt="image" src="https://github.com/user-attachments/assets/02bd25bb-fa41-42b0-8d96-01c51bd4580c" /> <img width="529" height="452" alt="image" src="https://github.com/user-attachments/assets/61f6e968-365b-4a5b-8f2b-a8419d6b1bd3" />
This commit is contained in:
+1
@@ -28,6 +28,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
|
||||
yarnLockChecksum: 'd41d8cd98f00b204e9800998ecf8427e',
|
||||
packageJsonChecksum: '2851d0e2c3621a57e1fd103a245b6fde',
|
||||
apiClientChecksum: null,
|
||||
},
|
||||
frontComponents: [
|
||||
{
|
||||
|
||||
+1
@@ -11,6 +11,7 @@ export const EXPECTED_MANIFEST: Manifest = {
|
||||
defaultRoleUniversalIdentifier: 'e1e2e3e4-e5e6-4000-8000-000000000002',
|
||||
packageJsonChecksum: '93ae1e2eb3db18351d06f43550700dcc',
|
||||
yarnLockChecksum: 'd41d8cd98f00b204e9800998ecf8427e',
|
||||
apiClientChecksum: null,
|
||||
},
|
||||
publicAssets: [],
|
||||
fields: [],
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
type RestartableWatcher,
|
||||
type RestartableWatcherOptions,
|
||||
} from '@/cli/utilities/build/common/restartable-watcher-interface';
|
||||
import { createTypecheckPlugin } from '@/cli/utilities/build/common/typecheck-plugin';
|
||||
import * as esbuild from 'esbuild';
|
||||
import path from 'path';
|
||||
import { OUTPUT_DIR, NODE_ESM_CJS_BANNER } from 'twenty-shared/application';
|
||||
@@ -214,7 +215,10 @@ export const createLogicFunctionsWatcher = (
|
||||
externalModules: LOGIC_FUNCTION_EXTERNAL_MODULES,
|
||||
fileFolder: FileFolder.BuiltLogicFunction,
|
||||
platform: 'node',
|
||||
extraPlugins: [createSdkGeneratedResolverPlugin(options.appPath)],
|
||||
extraPlugins: [
|
||||
createTypecheckPlugin(options.appPath),
|
||||
createSdkGeneratedResolverPlugin(options.appPath),
|
||||
],
|
||||
banner: NODE_ESM_CJS_BANNER,
|
||||
},
|
||||
});
|
||||
@@ -229,6 +233,7 @@ export const createFrontComponentsWatcher = (
|
||||
fileFolder: FileFolder.BuiltFrontComponent,
|
||||
jsx: 'automatic',
|
||||
extraPlugins: [
|
||||
createTypecheckPlugin(options.appPath),
|
||||
createSdkGeneratedResolverPlugin(options.appPath),
|
||||
...getFrontComponentBuildPlugins(),
|
||||
],
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import { spawn, type ChildProcess } from 'node:child_process';
|
||||
import * as fs from 'fs-extra';
|
||||
import path from 'node:path';
|
||||
|
||||
import {
|
||||
parseTscOutputLine,
|
||||
type TypecheckError,
|
||||
} from '@/cli/utilities/build/common/typecheck-plugin';
|
||||
|
||||
export type TscWatcherOptions = {
|
||||
appPath: string;
|
||||
onErrors: (errors: TypecheckError[]) => void;
|
||||
};
|
||||
|
||||
export class TscWatcher {
|
||||
private appPath: string;
|
||||
private onErrors: (errors: TypecheckError[]) => void;
|
||||
private process: ChildProcess | null = null;
|
||||
private pendingErrors: TypecheckError[] = [];
|
||||
private buffer = '';
|
||||
private hasErrors = false;
|
||||
|
||||
constructor(options: TscWatcherOptions) {
|
||||
this.appPath = options.appPath;
|
||||
this.onErrors = options.onErrors;
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
const tscPath = path.join(this.appPath, 'node_modules', '.bin', 'tsc');
|
||||
|
||||
if (!(await fs.pathExists(tscPath))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tsconfigPath = path.join(this.appPath, 'tsconfig.json');
|
||||
|
||||
this.process = spawn(
|
||||
tscPath,
|
||||
['--watch', '--noEmit', '--pretty', 'false', '-p', tsconfigPath],
|
||||
{ cwd: this.appPath, stdio: ['ignore', 'pipe', 'pipe'] },
|
||||
);
|
||||
|
||||
this.process.on('error', () => {
|
||||
this.process = null;
|
||||
});
|
||||
|
||||
this.process.stdout?.on('data', (chunk: Buffer) => {
|
||||
this.handleOutput(chunk.toString());
|
||||
});
|
||||
|
||||
this.process.stderr?.on('data', (chunk: Buffer) => {
|
||||
this.handleOutput(chunk.toString());
|
||||
});
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.process?.kill();
|
||||
this.process = null;
|
||||
}
|
||||
|
||||
private handleOutput(data: string): void {
|
||||
this.buffer += data;
|
||||
|
||||
const lines = this.buffer.split('\n');
|
||||
|
||||
this.buffer = lines.pop() ?? '';
|
||||
|
||||
for (const line of lines) {
|
||||
this.processLine(line);
|
||||
}
|
||||
}
|
||||
|
||||
private processLine(line: string): void {
|
||||
if (
|
||||
line.includes('Starting compilation in watch mode...') ||
|
||||
line.includes('Starting incremental compilation...')
|
||||
) {
|
||||
this.pendingErrors = [];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (line.includes('Watching for file changes.')) {
|
||||
const hadErrors = this.hasErrors;
|
||||
|
||||
this.hasErrors = this.pendingErrors.length > 0;
|
||||
|
||||
if (this.hasErrors || hadErrors) {
|
||||
this.onErrors(this.pendingErrors);
|
||||
}
|
||||
|
||||
this.pendingErrors = [];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const error = parseTscOutputLine(line);
|
||||
|
||||
if (error) {
|
||||
this.pendingErrors.push(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { execFile } from 'node:child_process';
|
||||
import type * as esbuild from 'esbuild';
|
||||
import path from 'node:path';
|
||||
|
||||
export type TypecheckError = {
|
||||
text: string;
|
||||
file: string;
|
||||
line: number;
|
||||
column: number;
|
||||
};
|
||||
|
||||
const TSC_ERROR_REGEX = /^(.+)\((\d+),(\d+)\): error TS\d+: (.+)$/;
|
||||
|
||||
export const parseTscOutputLine = (line: string): TypecheckError | null => {
|
||||
const match = line.match(TSC_ERROR_REGEX);
|
||||
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [, filePath, lineStr, columnStr, text] = match;
|
||||
|
||||
return {
|
||||
text,
|
||||
file: filePath,
|
||||
line: Number(lineStr),
|
||||
column: Number(columnStr) - 1,
|
||||
};
|
||||
};
|
||||
|
||||
const parseTscOutput = (output: string): TypecheckError[] => {
|
||||
const errors: TypecheckError[] = [];
|
||||
|
||||
for (const line of output.split('\n')) {
|
||||
const error = parseTscOutputLine(line);
|
||||
|
||||
if (error) {
|
||||
errors.push(error);
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
export const runTypecheck = (appPath: string): Promise<TypecheckError[]> => {
|
||||
const tsconfigPath = path.join(appPath, 'tsconfig.json');
|
||||
const tscPath = path.join(appPath, 'node_modules', '.bin', 'tsc');
|
||||
|
||||
return new Promise((resolve) => {
|
||||
execFile(
|
||||
tscPath,
|
||||
['--noEmit', '--pretty', 'false', '-p', tsconfigPath],
|
||||
{ cwd: appPath },
|
||||
(_error, stdout, stderr) => {
|
||||
resolve(parseTscOutput(stderr || stdout));
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const toEsbuildErrors = (errors: TypecheckError[]): esbuild.PartialMessage[] =>
|
||||
errors.map((error) => ({
|
||||
text: error.text,
|
||||
location: {
|
||||
file: error.file,
|
||||
line: error.line,
|
||||
column: error.column,
|
||||
lineText: '',
|
||||
length: 0,
|
||||
namespace: '',
|
||||
suggestion: '',
|
||||
},
|
||||
}));
|
||||
|
||||
export const createTypecheckPlugin = (appPath: string): esbuild.Plugin => ({
|
||||
name: 'typecheck',
|
||||
setup: (build) => {
|
||||
build.onStart(async () => {
|
||||
const errors = await runTypecheck(appPath);
|
||||
|
||||
return { errors: toEsbuildErrors(errors) };
|
||||
});
|
||||
},
|
||||
});
|
||||
+1
@@ -13,6 +13,7 @@ const validApplication: ApplicationManifest = {
|
||||
defaultRoleUniversalIdentifier: '68bb56f3-8300-4cb5-8cc3-8da9ee66f1b2',
|
||||
packageJsonChecksum: '98592af7-4be9-4655-b5c4-9bef307a996c',
|
||||
yarnLockChecksum: '580ee05f-15fe-4146-bac2-6c382483c94e',
|
||||
apiClientChecksum: null,
|
||||
};
|
||||
|
||||
const validField: FieldManifest = {
|
||||
|
||||
@@ -101,6 +101,7 @@ export const buildManifest = async (
|
||||
...extract.config,
|
||||
yarnLockChecksum: null,
|
||||
packageJsonChecksum: null,
|
||||
apiClientChecksum: null,
|
||||
};
|
||||
errors.push(...extract.errors);
|
||||
applicationFilePaths.push(relativePath);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import crypto from 'crypto';
|
||||
import { relative } from 'path';
|
||||
import { type Manifest, OUTPUT_DIR } from 'twenty-shared/application';
|
||||
import { FileFolder } from 'twenty-shared/types';
|
||||
@@ -88,5 +89,30 @@ export const manifestUpdateChecksums = ({
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const apiClientChecksums: string[] = [];
|
||||
|
||||
for (const [builtPath, { fileFolder }] of builtFileInfos.entries()) {
|
||||
const rootBuiltPath = relative(OUTPUT_DIR, builtPath);
|
||||
|
||||
if (
|
||||
fileFolder === FileFolder.Dependencies &&
|
||||
rootBuiltPath.startsWith('api-client/')
|
||||
) {
|
||||
const entry = builtFileInfos.get(builtPath);
|
||||
|
||||
if (entry) {
|
||||
apiClientChecksums.push(entry.checksum);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (apiClientChecksums.length > 0) {
|
||||
result.application.apiClientChecksum = crypto
|
||||
.createHash('md5')
|
||||
.update(apiClientChecksums.sort().join(''))
|
||||
.digest('hex');
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -22,6 +22,7 @@ export class ClientService {
|
||||
authToken?: string;
|
||||
}): Promise<void> {
|
||||
const outputPath = this.resolveGeneratedPath(appPath);
|
||||
const tempPath = `${outputPath}.tmp`;
|
||||
|
||||
const getSchemaResponse = await this.apiService.getSchema({ authToken });
|
||||
|
||||
@@ -33,12 +34,12 @@ export class ClientService {
|
||||
|
||||
const { data: schema } = getSchemaResponse;
|
||||
|
||||
await fs.ensureDir(outputPath);
|
||||
await fs.emptyDir(outputPath);
|
||||
await fs.ensureDir(tempPath);
|
||||
await fs.emptyDir(tempPath);
|
||||
|
||||
await generate({
|
||||
schema,
|
||||
output: outputPath,
|
||||
output: tempPath,
|
||||
scalarTypes: {
|
||||
DateTime: 'string',
|
||||
JSON: 'Record<string, unknown>',
|
||||
@@ -46,7 +47,10 @@ export class ClientService {
|
||||
},
|
||||
});
|
||||
|
||||
await this.injectTwentyClient(outputPath);
|
||||
await this.injectTwentyClient(tempPath);
|
||||
|
||||
await fs.remove(outputPath);
|
||||
await fs.move(tempPath, outputPath);
|
||||
}
|
||||
|
||||
private resolveGeneratedPath(appPath: string): string {
|
||||
@@ -70,22 +74,16 @@ const defaultOptions: ClientOptions = {
|
||||
|
||||
export default class Twenty {
|
||||
private client: Client;
|
||||
private apiUrl: string;
|
||||
private authorizationToken: string;
|
||||
|
||||
constructor(options?: ClientOptions) {
|
||||
const merged: ClientOptions = {
|
||||
this.client = createClient({
|
||||
...defaultOptions,
|
||||
...options,
|
||||
headers: {
|
||||
...defaultOptions.headers,
|
||||
...(options?.headers ?? {}),
|
||||
},
|
||||
};
|
||||
|
||||
this.client = createClient(merged);
|
||||
this.apiUrl = merged.url;
|
||||
this.authorizationToken = merged.headers.Authorization;
|
||||
});
|
||||
}
|
||||
|
||||
query<R extends QueryGenqlSelection>(request: R & { __name?: string }) {
|
||||
@@ -95,41 +93,6 @@ export default class Twenty {
|
||||
mutation<R extends MutationGenqlSelection>(request: R & { __name?: string }) {
|
||||
return this.client.mutation(request);
|
||||
}
|
||||
|
||||
async uploadFile(
|
||||
fileBuffer: Buffer,
|
||||
filename: string,
|
||||
contentType: string = 'application/octet-stream',
|
||||
fileFolder: string = 'Attachment',
|
||||
): Promise<{ path: string; token: string }> {
|
||||
const form = new FormData();
|
||||
|
||||
form.append('operations', JSON.stringify({
|
||||
query: \`mutation UploadFile($file: Upload!, $fileFolder: FileFolder) {
|
||||
uploadFile(file: $file, fileFolder: $fileFolder) { path token }
|
||||
}\`,
|
||||
variables: { file: null, fileFolder },
|
||||
}));
|
||||
form.append('map', JSON.stringify({ '0': ['variables.file'] }));
|
||||
form.append('0', new Blob([fileBuffer], { type: contentType }), filename);
|
||||
|
||||
|
||||
const response = await fetch(\`\${this.apiUrl}/graphql\`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: this.authorizationToken,
|
||||
},
|
||||
body: form,
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.errors) {
|
||||
throw new GenqlError(result.errors, result.data);
|
||||
}
|
||||
|
||||
return result.data.uploadFile;
|
||||
}
|
||||
}
|
||||
|
||||
`;
|
||||
|
||||
@@ -7,7 +7,10 @@ import { CheckServerOrchestratorStep } from '@/cli/utilities/dev/orchestrator/st
|
||||
import { EnsureValidTokensOrchestratorStep } from '@/cli/utilities/dev/orchestrator/steps/ensure-valid-tokens-orchestrator-step';
|
||||
import { GenerateApiClientOrchestratorStep } from '@/cli/utilities/dev/orchestrator/steps/generate-api-client-orchestrator-step';
|
||||
import { ResolveApplicationOrchestratorStep } from '@/cli/utilities/dev/orchestrator/steps/resolve-application-orchestrator-step';
|
||||
import { StartWatchersOrchestratorStep } from '@/cli/utilities/dev/orchestrator/steps/start-watchers-orchestrator-step';
|
||||
import {
|
||||
StartWatchersOrchestratorStep,
|
||||
type FileBuiltEvent,
|
||||
} from '@/cli/utilities/dev/orchestrator/steps/start-watchers-orchestrator-step';
|
||||
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 * as fs from 'fs-extra';
|
||||
@@ -69,7 +72,7 @@ export class DevModeOrchestrator {
|
||||
this.startWatchersStep = new StartWatchersOrchestratorStep({
|
||||
...stepDeps,
|
||||
scheduleSync: this.scheduleSync.bind(this),
|
||||
uploadFilesStep: this.uploadFilesStep,
|
||||
onFileBuilt: this.handleFileBuilt.bind(this),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -90,6 +93,16 @@ export class DevModeOrchestrator {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
private handleFileBuilt(event: FileBuiltEvent): void {
|
||||
if (this.state.steps.uploadFiles.output.fileUploader) {
|
||||
this.uploadFilesStep.uploadFile(
|
||||
event.builtPath,
|
||||
event.sourcePath,
|
||||
event.fileFolder,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private scheduleSync(): void {
|
||||
if (this.syncTimer) {
|
||||
clearTimeout(this.syncTimer);
|
||||
@@ -150,11 +163,9 @@ export class DevModeOrchestrator {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.state.hasObjectsOrFieldsChanged(buildResult.manifest!)) {
|
||||
await this.generateApiClientStep.execute({
|
||||
appPath: this.state.appPath,
|
||||
});
|
||||
}
|
||||
const objectsOrFieldsChanged = this.state.hasObjectsOrFieldsChanged(
|
||||
buildResult.manifest!,
|
||||
);
|
||||
|
||||
await this.uploadFilesStep.waitForUploads();
|
||||
|
||||
@@ -163,6 +174,16 @@ export class DevModeOrchestrator {
|
||||
builtFileInfos: this.state.steps.uploadFiles.output.builtFileInfos,
|
||||
appPath: this.state.appPath,
|
||||
});
|
||||
|
||||
if (objectsOrFieldsChanged) {
|
||||
await this.generateApiClientStep.execute({
|
||||
appPath: this.state.appPath,
|
||||
});
|
||||
|
||||
await this.uploadFilesStep.copyAndUploadApiClientFiles(
|
||||
this.state.appPath,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async initializePipeline(manifest: Manifest): Promise<boolean> {
|
||||
|
||||
+51
-24
@@ -4,15 +4,23 @@ import {
|
||||
type EsbuildWatcher,
|
||||
} from '@/cli/utilities/build/common/esbuild-watcher';
|
||||
import { FileUploadWatcher } from '@/cli/utilities/build/common/file-upload-watcher';
|
||||
import { TscWatcher } from '@/cli/utilities/build/common/tsc-watcher';
|
||||
import { type TypecheckError } from '@/cli/utilities/build/common/typecheck-plugin';
|
||||
import { type ManifestBuildResult } from '@/cli/utilities/build/manifest/manifest-update-checksums';
|
||||
import { ManifestWatcher } from '@/cli/utilities/build/manifest/manifest-watcher';
|
||||
import { type OrchestratorState } from '@/cli/utilities/dev/orchestrator/dev-mode-orchestrator-state';
|
||||
import { type UploadFilesOrchestratorStep } from '@/cli/utilities/dev/orchestrator/steps/upload-files-orchestrator-step';
|
||||
import type { Location } from 'esbuild';
|
||||
import { type EventName } from 'chokidar/handler.js';
|
||||
import { ASSETS_DIR } from 'twenty-shared/application';
|
||||
import { FileFolder } from 'twenty-shared/types';
|
||||
|
||||
export type FileBuiltEvent = {
|
||||
fileFolder: FileFolder;
|
||||
builtPath: string;
|
||||
sourcePath: string;
|
||||
checksum: string;
|
||||
};
|
||||
|
||||
export type StartWatchersOrchestratorStepOutput = {
|
||||
watchersStarted: boolean;
|
||||
};
|
||||
@@ -21,24 +29,25 @@ export class StartWatchersOrchestratorStep {
|
||||
private state: OrchestratorState;
|
||||
private scheduleSync: () => void;
|
||||
private notify: () => void;
|
||||
private uploadFilesStep: UploadFilesOrchestratorStep;
|
||||
private onFileBuilt: (event: FileBuiltEvent) => void;
|
||||
|
||||
private manifestWatcher: ManifestWatcher | null = null;
|
||||
private logicFunctionsWatcher: EsbuildWatcher | null = null;
|
||||
private frontComponentsWatcher: EsbuildWatcher | null = null;
|
||||
private assetWatcher: FileUploadWatcher | null = null;
|
||||
private dependencyWatcher: FileUploadWatcher | null = null;
|
||||
private tscWatcher: TscWatcher | null = null;
|
||||
|
||||
constructor(options: {
|
||||
state: OrchestratorState;
|
||||
scheduleSync: () => void;
|
||||
notify: () => void;
|
||||
uploadFilesStep: UploadFilesOrchestratorStep;
|
||||
onFileBuilt: (event: FileBuiltEvent) => void;
|
||||
}) {
|
||||
this.state = options.state;
|
||||
this.scheduleSync = options.scheduleSync;
|
||||
this.notify = options.notify;
|
||||
this.uploadFilesStep = options.uploadFilesStep;
|
||||
this.onFileBuilt = options.onFileBuilt;
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
@@ -74,6 +83,8 @@ export class StartWatchersOrchestratorStep {
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
this.tscWatcher?.close();
|
||||
|
||||
await Promise.all([
|
||||
this.manifestWatcher?.close(),
|
||||
this.logicFunctionsWatcher?.close(),
|
||||
@@ -117,32 +128,20 @@ export class StartWatchersOrchestratorStep {
|
||||
this.notify();
|
||||
}
|
||||
|
||||
private handleFileBuilt({
|
||||
fileFolder,
|
||||
builtPath,
|
||||
sourcePath,
|
||||
checksum,
|
||||
}: {
|
||||
fileFolder: FileFolder;
|
||||
builtPath: string;
|
||||
sourcePath: string;
|
||||
checksum: string;
|
||||
}): void {
|
||||
private handleFileBuilt(event: FileBuiltEvent): void {
|
||||
this.state.addEvent({
|
||||
message: `Successfully built ${builtPath}`,
|
||||
message: `Successfully built ${event.builtPath}`,
|
||||
status: 'success',
|
||||
});
|
||||
|
||||
this.state.steps.uploadFiles.output.builtFileInfos.set(builtPath, {
|
||||
checksum,
|
||||
builtPath,
|
||||
sourcePath,
|
||||
fileFolder,
|
||||
this.state.steps.uploadFiles.output.builtFileInfos.set(event.builtPath, {
|
||||
checksum: event.checksum,
|
||||
builtPath: event.builtPath,
|
||||
sourcePath: event.sourcePath,
|
||||
fileFolder: event.fileFolder,
|
||||
});
|
||||
|
||||
if (this.state.steps.uploadFiles.output.fileUploader) {
|
||||
this.uploadFilesStep.uploadFile(builtPath, sourcePath, fileFolder);
|
||||
}
|
||||
this.onFileBuilt(event);
|
||||
|
||||
this.notify();
|
||||
this.scheduleSync();
|
||||
@@ -153,6 +152,7 @@ export class StartWatchersOrchestratorStep {
|
||||
frontComponents: string[],
|
||||
): Promise<void> {
|
||||
await Promise.all([
|
||||
this.startTscWatcher(),
|
||||
this.startLogicFunctionsWatcher(logicFunctions),
|
||||
this.startFrontComponentsWatcher(frontComponents),
|
||||
this.startAssetWatcher(),
|
||||
@@ -207,4 +207,31 @@ export class StartWatchersOrchestratorStep {
|
||||
|
||||
this.dependencyWatcher.start();
|
||||
}
|
||||
|
||||
private async startTscWatcher(): Promise<void> {
|
||||
this.tscWatcher = new TscWatcher({
|
||||
appPath: this.state.appPath,
|
||||
onErrors: this.handleTypecheckErrors.bind(this),
|
||||
});
|
||||
|
||||
await this.tscWatcher.start();
|
||||
}
|
||||
|
||||
private handleTypecheckErrors(errors: TypecheckError[]): void {
|
||||
if (errors.length === 0) {
|
||||
this.state.addEvent({
|
||||
message: 'Typecheck passed',
|
||||
status: 'success',
|
||||
});
|
||||
} else {
|
||||
this.state.applyStepEvents(
|
||||
errors.map((error) => ({
|
||||
message: `Type error in ${error.file}(${error.line},${error.column}): ${error.text}`,
|
||||
status: 'error' as const,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
this.notify();
|
||||
}
|
||||
}
|
||||
|
||||
+49
-1
@@ -3,7 +3,13 @@ import {
|
||||
type OrchestratorStateBuiltFileInfo,
|
||||
} from '@/cli/utilities/dev/orchestrator/dev-mode-orchestrator-state';
|
||||
import { FileUploader } from '@/cli/utilities/file/file-uploader';
|
||||
import { type FileFolder } from 'twenty-shared/types';
|
||||
import crypto from 'crypto';
|
||||
import * as fs from 'fs-extra';
|
||||
import { join } from 'path';
|
||||
import { OUTPUT_DIR } from 'twenty-shared/application';
|
||||
import { FileFolder } from 'twenty-shared/types';
|
||||
|
||||
const API_CLIENT_FILES = ['types.ts', 'schema.ts'];
|
||||
|
||||
export type UploadFilesOrchestratorStepOutput = {
|
||||
fileUploader: FileUploader | null;
|
||||
@@ -103,6 +109,48 @@ export class UploadFilesOrchestratorStep {
|
||||
this.notify();
|
||||
}
|
||||
|
||||
async copyAndUploadApiClientFiles(appPath: string): Promise<void> {
|
||||
const generatedDir = join(
|
||||
appPath,
|
||||
'node_modules',
|
||||
'twenty-sdk',
|
||||
'generated',
|
||||
);
|
||||
|
||||
if (!(await fs.pathExists(generatedDir))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const outputDir = join(appPath, OUTPUT_DIR, 'api-client');
|
||||
|
||||
await fs.ensureDir(outputDir);
|
||||
|
||||
for (const fileName of API_CLIENT_FILES) {
|
||||
const absoluteSourcePath = join(generatedDir, fileName);
|
||||
|
||||
if (!(await fs.pathExists(absoluteSourcePath))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await fs.copy(absoluteSourcePath, join(outputDir, fileName));
|
||||
|
||||
const content = await fs.readFile(absoluteSourcePath);
|
||||
const checksum = crypto.createHash('md5').update(content).digest('hex');
|
||||
|
||||
const builtPath = join(OUTPUT_DIR, 'api-client', fileName);
|
||||
const sourcePath = join('api-client', fileName);
|
||||
|
||||
this.state.steps.uploadFiles.output.builtFileInfos.set(builtPath, {
|
||||
checksum,
|
||||
builtPath,
|
||||
sourcePath,
|
||||
fileFolder: FileFolder.Dependencies,
|
||||
});
|
||||
|
||||
this.uploadFile(builtPath, sourcePath, FileFolder.Dependencies);
|
||||
}
|
||||
}
|
||||
|
||||
private uploadPendingFiles(): void {
|
||||
for (const [
|
||||
builtPath,
|
||||
|
||||
@@ -2,5 +2,5 @@ import { type ApplicationManifest } from 'twenty-shared/application';
|
||||
|
||||
export type ApplicationConfig = Omit<
|
||||
ApplicationManifest,
|
||||
'packageJsonChecksum' | 'yarnLockChecksum'
|
||||
'packageJsonChecksum' | 'yarnLockChecksum' | 'apiClientChecksum'
|
||||
>;
|
||||
|
||||
+1
@@ -70,6 +70,7 @@ describe('syncApplication', () => {
|
||||
applicationVariables: {},
|
||||
packageJsonChecksum: null,
|
||||
yarnLockChecksum: null,
|
||||
apiClientChecksum: null,
|
||||
},
|
||||
roles: [
|
||||
{
|
||||
|
||||
@@ -21,4 +21,5 @@ export type ApplicationManifest = SyncableEntityOptions & {
|
||||
marketplaceData?: ApplicationMarketplaceData;
|
||||
packageJsonChecksum: string | null;
|
||||
yarnLockChecksum: string | null;
|
||||
apiClientChecksum: string | null;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user