Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-genui-api-extractor-dist-race.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---

---

Fix a flaky CI failure in genui API extraction where `@lynx-js/genui-cli`'s `tsc` (and `@lynx-js/genui#api-extractor`) could fail with `TS2307` / `TS7016` ("Cannot find module `@lynx-js/genui-a2ui-prompt`"). `run-api-extractor.mjs` rebuilt each package in-script (`pnpm run build`), rewriting its `dist/` while turbo-scheduled consumer builds read the same `dist/`, so `tsc` could observe `index.js` without its freshly-cleaned `index.d.ts`. The script no longer builds — turbo's task graph builds each package, and the api-extractor task now depends on the package build for the rust-free genui packages — and the file lock that only existed to serialize those in-script builds is removed.
7 changes: 7 additions & 0 deletions packages/genui/a2ui-catalog-extractor/turbo.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@
"outputs": [
"dist/**"
]
},
"api-extractor": {
"dependsOn": [
"//#build",
"build"
],
"cache": false
}
}
}
3 changes: 1 addition & 2 deletions packages/genui/a2ui-prompt/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@
],
"scripts": {
"api-extractor": "node ../scripts/run-api-extractor.mjs",
"build": "rslib build",
"build:api": "rslib build"
"build": "rslib build"
},
"devDependencies": {
"@microsoft/api-extractor": "catalog:",
Expand Down
18 changes: 3 additions & 15 deletions packages/genui/a2ui-prompt/turbo.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,24 +23,12 @@
"dist/**"
]
},
"build:api": {
"api-extractor": {
"dependsOn": [
"//#build",
"build"
],
"inputs": [
"src/**",
"../server/agent/a2ui-catalog.ts",
"../server/agent/a2ui-examples.ts",
"../server/agent/a2ui-prompt.ts",
"../server/agent/catalog/**/*.json",
"package.json",
"rslib.config.ts",
"tsconfig.build.json",
"tsconfig.json"
],
"outputs": [
"dist/**"
]
"cache": false
}
}
}
75 changes: 13 additions & 62 deletions packages/genui/scripts/run-api-extractor.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,63 +3,14 @@
// LICENSE file in the root directory of this source tree.
import { spawnSync } from 'node:child_process';
import { existsSync } from 'node:fs';
import { open, readFile, rm } from 'node:fs/promises';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';

const genuiRoot = dirname(dirname(fileURLToPath(import.meta.url)));
const lockPath = join(genuiRoot, '.api-extractor.lock');
const lockTimeoutMs = 10 * 60 * 1000;
const entryPointTimeoutMs = 5 * 1000;
const retryDelayMs = 500;

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

const isProcessAlive = (pid) => {
try {
process.kill(pid, 0);
return true;
} catch {
return false;
}
};

const acquireLock = async () => {
const start = Date.now();

while (Date.now() - start < lockTimeoutMs) {
try {
const file = await open(lockPath, 'wx');
await file.writeFile(JSON.stringify({
cwd: process.cwd(),
pid: process.pid,
startedAt: new Date().toISOString(),
}));
await file.close();
return;
} catch (error) {
if (error?.code !== 'EEXIST') {
throw error;
}

try {
const current = JSON.parse(await readFile(lockPath, 'utf8'));
if (typeof current.pid === 'number' && !isProcessAlive(current.pid)) {
await rm(lockPath, { force: true });
continue;
}
} catch {
await rm(lockPath, { force: true });
continue;
}

await sleep(retryDelayMs);
}
}

throw new Error(`Timed out waiting for ${lockPath}`);
};

const run = (command, args) => {
const result = spawnSync(command, args, {
shell: process.platform === 'win32',
Expand Down Expand Up @@ -127,14 +78,14 @@ const ensureMainEntryPoint = async () => {
);
};

await acquireLock();

try {
run('pnpm', ['run', 'build']);
await ensureMainEntryPoint();
run('api-extractor', ['run', '--verbose']);
} finally {
if (existsSync(lockPath)) {
await rm(lockPath, { force: true });
}
}
// No lock is needed: turbo's task graph builds each package before its
// `api-extractor` task (which depends on `build`) and before every consumer
// build, so api-extractor only ever reads a finished `dist/`. Do NOT build
// here — a rebuild would re-clean and rewrite `dist/` while turbo-scheduled
// consumer builds (e.g. `genui-cli#build`) read the same `dist/`, transiently
// removing the `.d.ts` and breaking their `tsc` (TS2307/TS7016).
// `ensureMainEntryPoint` stays only as a last-resort build if the entry point
// is somehow missing. Concurrent api-extractor runs across packages touch only
// their own per-package outputs, so they need no mutual exclusion.
await ensureMainEntryPoint();
Comment thread
upupming marked this conversation as resolved.
Comment thread
upupming marked this conversation as resolved.
run('api-extractor', ['run', '--verbose']);
2 changes: 1 addition & 1 deletion packages/genui/turbo.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
"//#build",
"@lynx-js/genui-a2ui#build:api",
"@lynx-js/genui-a2ui-catalog-extractor#build:api",
"@lynx-js/genui-a2ui-prompt#build:api",
"@lynx-js/genui-a2ui-prompt#build",
"@lynx-js/genui-openui#build:api"
],
"cache": false
Expand Down
Loading