From cedb864ff7f2d07522e66845980e910dc5259f87 Mon Sep 17 00:00:00 2001 From: Dustin Kelley Date: Thu, 20 Aug 2026 13:35:26 -0500 Subject: [PATCH] fix(ui): write generated i18n resources atomically CI typecheck and build both regenerate resources.generated.ts in parallel. writeFileSync truncates the file first, so tsc can read an empty module and fail with TS2305. Co-authored-by: Cursor --- .changeset/small-rules-exist.md | 4 +++ .../ui/scripts/generate-i18n-resources.mjs | 35 +++++++++++++++++-- 2 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 .changeset/small-rules-exist.md diff --git a/.changeset/small-rules-exist.md b/.changeset/small-rules-exist.md new file mode 100644 index 00000000..01f91d5b --- /dev/null +++ b/.changeset/small-rules-exist.md @@ -0,0 +1,4 @@ +--- +--- + +Internal generator fix so parallel CI typecheck/build cannot truncate `resources.generated.ts`. diff --git a/packages/ui/scripts/generate-i18n-resources.mjs b/packages/ui/scripts/generate-i18n-resources.mjs index e1e855a9..04fd121b 100644 --- a/packages/ui/scripts/generate-i18n-resources.mjs +++ b/packages/ui/scripts/generate-i18n-resources.mjs @@ -6,7 +6,7 @@ * node scripts/generate-i18n-resources.mjs # one-shot (build / CI) * node scripts/generate-i18n-resources.mjs --watch # watch locale dir in dev */ -import { readdirSync, watch, writeFileSync } from 'node:fs'; +import { readFileSync, readdirSync, renameSync, unlinkSync, watch, writeFileSync } from 'node:fs'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -70,10 +70,41 @@ ${resourceEntries} export const supportedLngs = Object.keys(resources); `; - writeFileSync(outputPath, content, 'utf8'); + writeGeneratedFile(content); console.log(`Generated ${outputPath} (${locales.length} locales: ${locales.join(', ')})`); } +/** + * Replace resources.generated.ts without truncating it in place. + * + * CI runs UI typecheck and UI build in parallel; both call this generator. + * writeFileSync empties the destination first, so tsc can read a zero-length + * module and fail with TS2305 (no exported member 'resources' / 'supportedLngs'). + * Skip the write when content is unchanged so parallel no-op runs do not race. + */ +function writeGeneratedFile(content) { + try { + if (readFileSync(outputPath, 'utf8') === content) { + return; + } + } catch { + // Missing or unreadable — write a fresh copy. + } + + const tempPath = `${outputPath}.${process.pid}.tmp`; + writeFileSync(tempPath, content, 'utf8'); + try { + renameSync(tempPath, outputPath); + } catch (error) { + try { + unlinkSync(tempPath); + } catch { + // Best-effort cleanup; the rename error is the one to surface. + } + throw error; + } +} + function shouldRegenerate(filename) { // macOS directory watches often omit filename; regenerate to stay safe. if (filename == null) {