Fix twenty-sdk typecheck race condition (#18246)
## Fix SDK first-run build failure when generated API client is missing ### Problem When running `twenty app:dev` for the first time, the build pipeline crashes because: 1. The esbuild front-component watcher tries to resolve `twenty-sdk/generated`, but the generated API client doesn't exist yet — it's only created later in the pipeline after syncing with the server. 2. The typecheck plugin (`tsc --noEmit`) runs as a blocking esbuild `onStart` hook, so even with stub files, type errors on the empty client classes (e.g. `Property 'query' does not exist on type 'CoreApiClient'`) cause the build to fail before the real client can be generated. This creates a chicken-and-egg problem: the watchers need the generated client to build, but the client is generated after the watchers produce their output. ### Solution **1. Stub generated client on startup** Added `ensureGeneratedClientStub` to `ClientService` that writes minimal placeholder files (`CoreApiClient`, `MetadataApiClient`) into `node_modules/twenty-sdk/generated/` if the directory doesn't already exist. This is called in `DevModeOrchestrator.start()` before any watchers are created, so the `twenty-sdk/generated` import always resolves. **2. Skip typecheck on first sync round** Made the esbuild typecheck plugin accept a `shouldSkipTypecheck` callback. The orchestrator starts with `skipTypecheck = true` and passes `() => this.skipTypecheck` through the watcher chain. After the real API client is generated, the flag is flipped to `false`, so subsequent rebuilds enforce full type checking with the real generated types.
This commit is contained in:
@@ -29,6 +29,8 @@ export class DevModeOrchestrator {
|
||||
private syncTimer: NodeJS.Timeout | null = null;
|
||||
private serverCheckInterval: NodeJS.Timeout | null = null;
|
||||
|
||||
private clientService: ClientService;
|
||||
private skipTypecheck = true;
|
||||
private checkServerStep: CheckServerOrchestratorStep;
|
||||
private ensureValidTokensStep: EnsureValidTokensOrchestratorStep;
|
||||
private buildManifestStep: BuildManifestOrchestratorStep;
|
||||
@@ -44,7 +46,7 @@ export class DevModeOrchestrator {
|
||||
|
||||
const apiService = new ApiService({ disableInterceptors: true });
|
||||
const configService = new ConfigService();
|
||||
const clientService = new ClientService();
|
||||
this.clientService = new ClientService();
|
||||
const stepDeps = { state: this.state, notify: () => this.state.notify() };
|
||||
|
||||
this.checkServerStep = new CheckServerOrchestratorStep({
|
||||
@@ -64,7 +66,7 @@ export class DevModeOrchestrator {
|
||||
this.uploadFilesStep = new UploadFilesOrchestratorStep(stepDeps);
|
||||
this.generateApiClientStep = new GenerateApiClientOrchestratorStep({
|
||||
...stepDeps,
|
||||
clientService,
|
||||
clientService: this.clientService,
|
||||
configService,
|
||||
});
|
||||
this.syncApplicationStep = new SyncApplicationOrchestratorStep({
|
||||
@@ -75,6 +77,7 @@ export class DevModeOrchestrator {
|
||||
...stepDeps,
|
||||
scheduleSync: this.scheduleSync.bind(this),
|
||||
onFileBuilt: this.handleFileBuilt.bind(this),
|
||||
shouldSkipTypecheck: () => this.skipTypecheck,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -84,6 +87,10 @@ export class DevModeOrchestrator {
|
||||
await fs.ensureDir(outputDir);
|
||||
await fs.emptyDir(outputDir);
|
||||
|
||||
await this.clientService.ensureGeneratedClientStub({
|
||||
appPath: this.state.appPath,
|
||||
});
|
||||
|
||||
await this.startWatchersStep.start();
|
||||
|
||||
this.serverCheckInterval = setInterval(() => {
|
||||
@@ -200,6 +207,8 @@ export class DevModeOrchestrator {
|
||||
appPath: this.state.appPath,
|
||||
});
|
||||
|
||||
this.skipTypecheck = false;
|
||||
|
||||
await this.uploadFilesStep.copyAndUploadApiClientFiles(
|
||||
this.state.appPath,
|
||||
);
|
||||
|
||||
+5
@@ -30,6 +30,7 @@ export class StartWatchersOrchestratorStep {
|
||||
private scheduleSync: () => void;
|
||||
private notify: () => void;
|
||||
private onFileBuilt: (event: FileBuiltEvent) => void;
|
||||
private shouldSkipTypecheck: () => boolean;
|
||||
|
||||
private manifestWatcher: ManifestWatcher | null = null;
|
||||
private logicFunctionsWatcher: EsbuildWatcher | null = null;
|
||||
@@ -43,11 +44,13 @@ export class StartWatchersOrchestratorStep {
|
||||
scheduleSync: () => void;
|
||||
notify: () => void;
|
||||
onFileBuilt: (event: FileBuiltEvent) => void;
|
||||
shouldSkipTypecheck: () => boolean;
|
||||
}) {
|
||||
this.state = options.state;
|
||||
this.scheduleSync = options.scheduleSync;
|
||||
this.notify = options.notify;
|
||||
this.onFileBuilt = options.onFileBuilt;
|
||||
this.shouldSkipTypecheck = options.shouldSkipTypecheck;
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
@@ -166,6 +169,7 @@ export class StartWatchersOrchestratorStep {
|
||||
this.logicFunctionsWatcher = createLogicFunctionsWatcher({
|
||||
appPath: this.state.appPath,
|
||||
sourcePaths,
|
||||
shouldSkipTypecheck: this.shouldSkipTypecheck,
|
||||
handleBuildError: this.handleFileBuildError.bind(this),
|
||||
handleFileBuilt: this.handleFileBuilt.bind(this),
|
||||
});
|
||||
@@ -179,6 +183,7 @@ export class StartWatchersOrchestratorStep {
|
||||
this.frontComponentsWatcher = createFrontComponentsWatcher({
|
||||
appPath: this.state.appPath,
|
||||
sourcePaths,
|
||||
shouldSkipTypecheck: this.shouldSkipTypecheck,
|
||||
handleBuildError: this.handleFileBuildError.bind(this),
|
||||
handleFileBuilt: this.handleFileBuilt.bind(this),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user