Skip to content

Commit 703c5ff

Browse files
committed
fix(scripts): check-i18n-coverage 的构建前置改为真检查,不再让第一个 example 顶罪
`scripts/check-i18n-coverage.mjs` 与 #5217 修的门禁是 lint.yml 里紧邻的两步, 带着同一句只写在注释里的前置("Requires the workspace build")。在装完依赖但 未构建的 worktree 里,它抛一个未捕获异常 + node 栈: Error: os lint produced no output for examples/app-crm/objectstack.config.ts 成因只有一个,而且不在那个文件里:门禁跑的是构建产物,oclif 从 dist/commands 解析 `os lint`,CLI 没 build 时什么也不输出 —— app-crm 只是恰好第一个被处理。 诊断把读者送进一个完全无辜的示例配置。 改动与 #5217 落地形状同构: - 进入 per-config 循环之前一次前置判定(探测 oclif 真正要加载的命令文件), 失败时一条前置结论 + 一句修法,并声明「什么都没测量」; - 循环内保留签名安全网,覆盖探测看不见的 stale/partial dist,首个 config 即 中止,并明说该 config 不是成因; - 两个纯分类器(oclifCommandFileFor / looksLikeMissingCliCommand)提取到 scripts/cli-build-prerequisite.mjs 共享,check-i18n-bundles.mjs 改为 import —— 两份拷贝就是两套事实来源(#5186),而 oclif 硬换行这类知识只会被改一边; - 补 --self-test(语料逐字录自本仓未构建实跑),并按仓内惯例接进 package.json 的 check:i18n-coverage,否则自测永远不跑。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BDmDsu2575gDxeMCxXhDE3
1 parent f192981 commit 703c5ff

4 files changed

Lines changed: 380 additions & 72 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
"lint": "eslint . --no-inline-config",
3232
"i18n:extract": "tsx packages/cli/bin/run-dev.js i18n extract packages/platform-objects/scripts/i18n-extract.config.ts --locales=zh-CN,ja-JP,es-ES --fill=default --out=packages/platform-objects/src/apps/translations",
3333
"check:i18n": "node scripts/check-i18n-bundles.mjs --self-test && node scripts/check-i18n-bundles.mjs",
34-
"check:i18n-coverage": "node scripts/check-i18n-coverage.mjs",
34+
"check:i18n-coverage": "node scripts/check-i18n-coverage.mjs --self-test && node scripts/check-i18n-coverage.mjs",
3535
"check:nul-bytes": "node scripts/check-nul-bytes.mjs --self-test && node scripts/check-nul-bytes.mjs",
3636
"check:doc-authoring": "node scripts/check-doc-authoring.mjs --self-test && node scripts/check-doc-authoring.mjs",
3737
"check:docs-audit-scope": "node scripts/docs-audit/affected-docs.mjs --self-test && node scripts/docs-audit/check-audit-scope.mjs --self-test && node scripts/docs-audit/check-audit-scope.mjs",

scripts/check-i18n-bundles.mjs

Lines changed: 17 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -66,19 +66,22 @@
6666
// "Prefer failing to falling back" (AGENTS.md, route & surface ownership §3):
6767
// the prerequisite verdict is a HARD failure that states it checked nothing —
6868
// never a skip, and never anything a reader can mistake for "bundles are fine".
69+
//
70+
// The two pure functions that answer it moved to `scripts/cli-build-prerequisite.mjs`
71+
// when #5862 found the same missing precondition in `check-i18n-coverage.mjs`, one
72+
// lint.yml step away. They are imported, not copied: see that module's header.
6973
import { spawnSync } from 'node:child_process';
7074
import { readFileSync, existsSync } from 'node:fs';
7175
import { readdirSync, statSync } from 'node:fs';
7276
import { join } from 'node:path';
77+
import {
78+
CLI,
79+
CLI_BUILD_FIX,
80+
looksLikeMissingCliCommand,
81+
oclifCommandFileFor,
82+
resolveCliCommandFile,
83+
} from './cli-build-prerequisite.mjs';
7384

74-
const CLI = 'packages/cli/bin/run.js';
75-
/**
76-
* `CLI` is a SOURCE file — four lines handing off to `@oclif/core` — so it is
77-
* present in an unbuilt tree and proves nothing. What the gate actually depends
78-
* on is the built command surface `bin/run.js` makes oclif resolve, which is why
79-
* the prerequisite probe below reads the package rather than the bin stub.
80-
*/
81-
const CLI_PKG = 'packages/cli';
8285
/** The one command this gate invokes per package, as oclif topic/command parts. */
8386
const EXTRACT_COMMAND_ID = ['i18n', 'extract'];
8487
const write = process.argv.includes('--write');
@@ -173,55 +176,10 @@ function collectDriftedBundles(text) {
173176
return [...String(text ?? '').matchAll(/(?:out of date|missing):\s+(\S+)/g)].map((m) => m[1]);
174177
}
175178

176-
/**
177-
* Where oclif will look for the command this gate runs, derived from the CLI
178-
* package's own `oclif.commands.target` (#5217). Pure: takes the parsed
179-
* package.json, returns a repo-relative path or a reason it cannot tell.
180-
*
181-
* Derived rather than hardcoded for the same reason the extract flags come from
182-
* each config's docstring: `dist/commands` is the CLI's declaration of where its
183-
* commands live, and a gate that restates it would keep probing the old path for
184-
* a release after someone moves it — passing while checking nothing.
185-
*/
186-
function oclifCommandFileFor(pkgJson, commandId) {
187-
const target = pkgJson?.oclif?.commands?.target ?? pkgJson?.oclif?.commands;
188-
if (typeof target !== 'string' || !target) {
189-
return { unknown: `${CLI_PKG}/package.json declares no oclif.commands.target` };
190-
}
191-
const rel = target.replace(/^\.\//, '').replace(/\/+$/, '');
192-
return { file: join(CLI_PKG, rel, ...commandId.slice(0, -1), `${commandId.at(-1)}.js`) };
193-
}
194-
195-
/**
196-
* oclif's own "command <id> not found", which is what an unbuilt (or half-built)
197-
* CLI answers with. The in-loop safety net for the prerequisite probe, and it has
198-
* to survive oclif's line wrapping to be worth anything: oclif hard-wraps that
199-
* one sentence across two or three ` › `-prefixed lines, and it wraps at a width
200-
* that depends on the config path's length, so the real corpus contains BOTH
201-
*
202-
* " › Error: command \n › i18n:extract:<path> not \n › found"
203-
* " › Error: command i18n:extract:<path-broken\n › -mid-token> not found"
204-
*
205-
* — the second one split inside the path itself. A per-line regex (the obvious
206-
* first implementation, and the one that reads as correct) matches NEITHER. So
207-
* the prefixes come off and the whole text is flattened before matching.
208-
*
209-
* Returns the matched SENTENCE (re-joined into one readable line) so the caller
210-
* can quote it as evidence, or '' for no match. Returning the whole flattened
211-
* text instead is a trap this returned from once in review: a stale-dist run
212-
* also carries a node `Warning:` block above the error, and quoting the flattened
213-
* text put that unrelated block in the report while the actual sentence sat past
214-
* the truncation.
215-
*/
216-
function looksLikeMissingCliCommand(text) {
217-
const flat = String(text ?? '')
218-
.split('\n')
219-
.map((l) => l.replace(/^\s*\s*/, ''))
220-
.join(' ')
221-
.replace(/\s+/g, ' ')
222-
.trim();
223-
return flat.match(/Error:\s*command\b.*?\bnot found\b/)?.[0] ?? '';
224-
}
179+
// `oclifCommandFileFor` and `looksLikeMissingCliCommand` — the two classifiers the
180+
// prerequisite is built from — now live in `./cli-build-prerequisite.mjs`, shared
181+
// with `check-i18n-coverage.mjs` (#5862). The self-test below still drives them
182+
// directly, so this gate's corpus keeps proving them from here.
225183

226184
/** stderr lines that are neither the lint signature nor blank — pass them through. */
227185
function passthroughStderrLines(text) {
@@ -404,7 +362,7 @@ function reportPrerequisiteNotMet(headline, detail) {
404362
console.error(
405363
`\ncheck-i18n-bundles: PREREQUISITE NOT MET — ${headline}\n\n` +
406364
detail.map((l) => (l ? ` ${l}` : '')).join('\n') +
407-
`\n\n Fix: pnpm exec turbo run build --filter=@objectstack/cli\n\n` +
365+
`\n\n Fix: ${CLI_BUILD_FIX}\n\n` +
408366
` Nothing was checked: no bundle was compared and no config was parsed, so this\n` +
409367
` result says NOTHING about whether the committed translation bundles are in sync.\n` +
410368
` (Exit code 1 — but piping this gate reports the PIPE's status, so\n` +
@@ -429,14 +387,7 @@ function reportPrerequisiteNotMet(headline, detail) {
429387
* is only the cheap early answer.
430388
*/
431389
function checkCliBuildPrerequisite() {
432-
let pkgJson;
433-
try {
434-
pkgJson = JSON.parse(readFileSync(join(CLI_PKG, 'package.json'), 'utf8'));
435-
} catch (e) {
436-
console.error(`check-i18n-bundles: could not read ${CLI_PKG}/package.json (${e.message}) — build prerequisite not pre-checked`);
437-
return;
438-
}
439-
const resolved = oclifCommandFileFor(pkgJson, EXTRACT_COMMAND_ID);
390+
const resolved = resolveCliCommandFile(EXTRACT_COMMAND_ID);
440391
if (resolved.unknown) {
441392
console.error(`check-i18n-bundles: ${resolved.unknown} — build prerequisite not pre-checked`);
442393
return;

0 commit comments

Comments
 (0)