Skip to content

ci(scripts): 用独立 tsconfig.scripts.json 给 scripts/ 补上类型门 (#3494) - #3498

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-3494-scripts-typecheck
Aug 6, 2026
Merged

ci(scripts): 用独立 tsconfig.scripts.json 给 scripts/ 补上类型门 (#3494)#3498
yinlianghui merged 1 commit into
mainfrom
claude/issue-3494-scripts-typecheck

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#3494

前提核验(先于实现)

--listFilesOnly 扫遍仓库 76 个 tsconfig,确认 scripts/__tests__/** 不在任何一个 program 里。

但 issue 的表述需要收窄一格 —— 「scripts/ 在零 tsconfig 覆盖内」并不完全成立:

文件origin/main 上是否被编译
scripts/vite-crypto-stub.ts✅ 是 —— apps/console/tsconfig.node.json(#3305 接进了 console 的 type-check)
scripts/vite-maplibre-worker.ts✅ 同上
scripts/__tests__/*.ts(10 个门禁 pin 测试)零覆盖

也就是说:两个非测试源文件其实有门,10 个 pin 测试一个都没有。issue 的实质结论(门禁 pin 测试自身无类型门)成立,落点不变;这里如实记下差异,避免下一个人按「整个 scripts/ 都没编译」去推断。

错误数:实测 8 条,不是 3 条

#3384 当时测得 3 条。此后 check-control-bytes.test.ts / docs-links-workflow.test.ts / shadcn-local-patches.test.ts 等陆续落地,现况:

lint-workflow.test.ts(57,37): TS7016 ../../eslint.config.js 无声明
shadcn-local-patches.test.ts(6,1): TS2578 Unused '@ts-expect-error'
shadcn-local-patches.test.ts(13,8): TS7016 ../shadcn-local-patches.mjs 无声明
shadcn-local-patches.test.ts(69,68): TS2345 it.each 回调签名(any[] 溢出)
shadcn-local-patches.test.ts(168,71): TS2345 同上
shadcn-local-patches.test.ts(184,79): TS2345 同上
vitest-invocation-guard.test.ts(6,1): TS2578 Unused '@ts-expect-error'
vitest-invocation-guard.test.ts(11,8): TS7016 ../vitest-invocation-guard.mjs 无声明

顺带查清了那两条 TS2578 的成因:多行 import { … } from '…mjs' 里,TS 把「缺声明」报在说明符所在行(13 / 11 行),而 @ts-expect-error 挂在 import { 上一行 —— 这两个指令从来就没生效过,只是没人编译过所以没人知道。

allowJs 的取舍:两个方向都真跑了

issue 提示「allowJs/checkJs 会翻转既有 @ts-expect-error 的成立性」。对同一批文件实测:

报错数要付的代价
allowJs: false8得给 5 个 .mjs 手写 .d.mts第二份事实来源,可以无声漂移 —— 正是 check-spec-symbol-derivation.mjs 存在的理由。而且 any 会外溢:3 条 TS2345 并非真缺陷,只是 any 的连带
allowJs: true5全部是「删掉一行已经变假的注释」。类型从 helper 源码本身推断,构造上无法漂移;patchedComponents() 真的是 string[],3 条 TS2345 自动消失

allowJs: true + checkJs: false。后者是刻意的边界:本项目消费 helper 的推断类型,不接管 8 个纯 JS 门禁脚本内部的类型整洁 —— 那是另一件大得多的事。

副作用是这条现在更强了:改动门禁 helper 的导出签名,它的 pin 测试会红

tsconfig.jsonallowJs: false 不受影响;vitest.config.mts 上同类的 @ts-expect-error 在那里依然正确,未动。

落点

  • tsconfig.scripts.json(新增) —— 独立,刻意不 extendstsconfig.base.json:那是包构建配置,其 exclude 列了 test glob,继承过来会一个测试文件都编不到,空转通过,正是 check-type-check-coverage.mjs 5b 段在上一层要抓的形态。
    • 按 glob 覆盖整个目录,不排除已被 console 覆盖的两个 vite-*.ts:排除清单是第二件要维护的事,而且 console 哪天不再 import 就会无声掉出所有 program。重叠的代价用对齐 console 那份的选项集付掉(strict / ESNext / bundler,以及不开noImplicitReturns),这样共享文件不会一个项目绿另一个红。
    • 注释用 // 而非 /* */:glob 里的双星紧跟斜杠会提前闭合块注释,而解析失败的 tsconfig 不会响亮报错 —— 它退回默认值,tsc -p 转头去编译整个仓库。这不是假设,是写这个文件时真踩到的,已由 pin 测试钉住。
  • .github/workflows/ci.yml —— type-check job 加一步 pnpm type-check:scripts,放在 install 之后、Turbo 之前:program 里没有任何 @object-ui/* import,不需要 ^build,便宜且快速失败。
  • package.json —— 加 type-check:scripts 根脚本。pnpm type-checkturbo run type-check,结构上够不到无 package.json 的目录;没有这个命名脚本,这道门就只能在 CI 上跑,而本地复现不了的门,人会学会忽略它。(与 type-check:coverage / check:spec-symbols / check:control-bytes 同一惯例。)
  • scripts/__tests__/scripts-type-check.test.ts(新增) —— 断言的是行为不是拼写:配置能否解析、磁盘上每个 scripts/**/*.ts 是否真落进 program、CI 是否真跑、是否排在 install 之后,以及「不需要 workspace 构建」这个放置前提。
  • content/docs/guide/ci-cd-pipeline.md —— job 表里 type-check逐项列出了它跑什么。不更新就会复刻 ci-cd-pipeline.md 的 ci.yml job 表格漂移:写「Seven jobs」并列了一个不存在的 dev-server job(实际 6 个) #3451 那种「页面少报一道门」的漂移,而钉住这张表的 pin 测试恰好就在本 PR 新覆盖的目录里。

验证(方向先声明,再执行)

1. 主方向 —— 预测:改前 RED、改后 GREEN。

改前:tsc -p tsconfig.scripts.json -> 8 errors(上表)
改后:pnpm type-check:scripts -> exit 0

2. 反向核验 A —— 把删掉的某个 @ts-expect-error 装回去。预测 RED,且原因必须是 TS2578(证明这些指令本就已失效,不是我删掉了一道有效的抑制):

render-budget-comment.test.ts(9,1): error TS2578: Unused '@ts-expect-error' directive.

3. 反向核验 B(#3181 绊线) —— 往 ci-cd-pipeline-doc.test.ts 追加一个必假的类型层断言(Assert< Equal< 1, 2 > >)。这里两个方向都要看,因为它们方向相反:

新门: ci-cd-pipeline-doc.test.ts(381,28): error TS2344: Type 'false' does not satisfy 'true' <- RED
vitest: Test Files 1 passed (1) / Tests 13 passed (13) <- 依旧全绿

vitest 那一侧的绿才是要点:类型断言在运行时被擦除,跑测试永远看不见它。「pin 测试全绿」+「没有任何 program 读它」= 一个可以钉住已失效契约还照样报绿的测试。

4. pin 测试自身的空转核验 —— 把 include 改窄成 scripts/vite-*.ts。预测 RED 并点名漏掉的文件(而不是空转变绿):

× resolves every TypeScript source under scripts/, with none left out
× really does cover the gate pin tests, by name
- scripts/__tests__/ci-cd-pipeline-doc.test.ts

5. 回归与门禁

pnpm exec vitest run scripts/__tests__ eslint-rules -> 15 files / 196 tests passed
ci.yml YAML 解析 -> jobs 键不变,type-check 步序:… install(5) → spec-symbols(6) → Type-check scripts/(7) → Turbo(8) → type-check(9)
node scripts/check-control-bytes.mjs -> OK(3674 文件)
node scripts/check-type-check-coverage.mjs -> OK
node scripts/check-lint-coverage.mjs -> OK
node scripts/check-doc-links.mjs -> Docs links are valid
eslint(改动文件) -> exit 0
控制字符自扫(改动文件,超出 gate 扫描面) -> clean

⚠️ 合并顺序:与 #3496 冲突(不是文件冲突,是门禁冲突)

#3496 新增的 scripts/__tests__/shadcn-sync-fetch-cache.test.ts 带着这一行:

// @ts-expect-error — plain-JS CI helper, intentionally untyped
import { fetchUrl, … } from '../shadcn-sync.js';

本 PR 之后,该目录下的 .mjs/.js import 会由 allowJs 推断出真类型,于是这个指令变成 TS2578(Unused),新加的 CI 步骤会红。两个 PR 各自都绿,合并后才红 —— git 不会报冲突。

后合的一方删掉那一行注释即可(与本 PR 对另外 5 处的处理一致)。#3497 只动 shadcn-check.yml,与本 PR 无交集。

无 changeset

纯 CI/工具链改动,不影响任何已发布包;根 package.json 是 private,新增的是根脚本而非依赖(先例 #3437)。


Generated by Claude Code

…on (#3494)
`scripts/` is not a workspace package, so `pnpm type-check` (turbo, which walks
package.json `scripts`) structurally cannot reach it, and
check-type-check-coverage.mjs decides coverage per PACKAGE so it could not see
the gap either. Every file in `scripts/__tests__/` was therefore compiled by
nothing at all - ten pin tests holding ci.yml, docs-links.yml, lint.yml, the
changeset guard, the control-byte scanner and the shadcn local patches in place.
A pin test the compiler never reads can assert a contract that no longer
type-checks and still print green. Measured here: a provably-false type-level
assertion appended to ci-cd-pipeline-doc.test.ts left `vitest run` at 13 passed,
because type assertions are erased at runtime.
- tsconfig.scripts.json: standalone (NOT extending tsconfig.base.json, whose
`exclude` lists the test globs and would have made the project vacuous),
strict, noEmit, covering `scripts/**/*.ts` by glob.
- allowJs:true / checkJs:false, chosen by measurement rather than assumption:
allowJs:false left 8 errors needing hand-written .d.mts files (a second source
of truth, free to drift); allowJs:true left 5, each a now-false
`@ts-expect-error` comment, and gives the pin tests types inferred from the
helper itself. Comments updated accordingly.
- ci.yml `type-check` job runs `pnpm type-check:scripts` after the install; it
needs no workspace build, so it stays in the cheap, fail-fast half.
- scripts/__tests__/scripts-type-check.test.ts pins the coverage (every .ts on
disk under scripts/ resolves into the program), that the config parses at all,
and that CI actually runs it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
@vercel

vercelBot commented Aug 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
objectuiIgnoredIgnoredAug 6, 2026 1:35pm

Request Review

yinlianghui pushed a commit that referenced this pull request Aug 6, 2026
…ype gate
objectui#3494 (PR #3498) adds `tsconfig.scripts.json` with `allowJs: true`,
whose `include` glob covers this branch's new test file too. Neither PR can
see the other, and the two land green individually while the merge is red —
no git conflict, so nothing warns.
Two separate problems, both found by running #3498's compilerOptions against
this branch:
- the `@ts-expect-error` above the `../shadcn-sync.js` import becomes TS2578
once the import has an inferred type. Removed (the sibling test files get
the same treatment inside #3498).
- `fetchRegistry`'s `get` option was the one destructured option without a
default, so it is absent from the function's inferred signature and passing
it from a `.ts` caller is TS2353. Fixed at the producer by giving it its
real default (`https.get`), which also drops the `get ? { get } : undefined`
dance at the call site — not by suppressing it at the consumer.
Behaviour is unchanged: production still resolves `https.get`, the tests still
inject `http.get`. Verified against a local copy of #3498's config — zero
errors in either file; the only remaining diagnostics are the five stale
directives that #3498 itself removes.
Refs objectstack-ai/objectstack#5803
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
@yinlianghui
yinlianghui marked this pull request as ready for review August 6, 2026 13:51
@yinlianghui
yinlianghui added this pull request to the merge queueAug 6, 2026
Merged via the queue into main with commit f995a45Aug 6, 2026
17 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-3494-scripts-typecheck branch August 6, 2026 13:52
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Aug 10, 2026
…i#3496)
* fix(scripts): never cache a non-2xx or malformed shadcn registry response
`fetchUrl` never inspected `res.statusCode`, and a body that failed
`JSON.parse` was resolved as a raw string. A 403 from an egress allowlist
therefore resolved as if it were a component, and `fetchRegistry` wrote it to
disk unconditionally with a 1h TTL — so one blocked run poisoned every
`pnpm shadcn:check` for the next hour, reporting "46 cached, 0 fetched"
while never retrying.
Two independent defences:
- transport: a non-2xx (redirects included) and a 2xx that is not JSON now
reject, with the status and a sanitised body excerpt in the message.
- semantic: only a response passing `isRegistryEntry` (files[0].content) is
written to the cache, and the same check runs on READ — an entry poisoned
by an older build is dropped on first contact instead of being trusted
until its TTL expires, so recovery is immediate.
`--update` semantics are unchanged (still never reads the cache, still
refuses to write on a failed fetch); the summary line now reports failed and
evicted separately so a fully-blocked run cannot read as a success. `main()`
runs only when the file is the process entry point, so the offline tests can
import the fetch/cache internals.
Refs objectstack-ai/objectstack#5803
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
* fix(scripts): make the shadcn-sync test import survive the scripts/ type gate
objectui#3494 (PR objectstack-ai#3498) adds `tsconfig.scripts.json` with `allowJs: true`,
whose `include` glob covers this branch's new test file too. Neither PR can
see the other, and the two land green individually while the merge is red —
no git conflict, so nothing warns.
Two separate problems, both found by running objectstack-ai#3498's compilerOptions against
this branch:
- the `@ts-expect-error` above the `../shadcn-sync.js` import becomes TS2578
once the import has an inferred type. Removed (the sibling test files get
the same treatment inside objectstack-ai#3498).
- `fetchRegistry`'s `get` option was the one destructured option without a
default, so it is absent from the function's inferred signature and passing
it from a `.ts` caller is TS2353. Fixed at the producer by giving it its
real default (`https.get`), which also drops the `get ? { get } : undefined`
dance at the call site — not by suppressing it at the consumer.
Behaviour is unchanged: production still resolves `https.get`, the tests still
inject `http.get`. Verified against a local copy of objectstack-ai#3498's config — zero
errors in either file; the only remaining diagnostics are the five stale
directives that objectstack-ai#3498 itself removes.
Refs objectstack-ai/objectstack#5803
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
---------
Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Aug 10, 2026
…k-ai#3504) (objectstack-ai#3505)
净 main 上 `pnpm type-check:scripts` 红:
scripts/__tests__/check-doc-links.test.ts(7,1):
error TS2578: Unused '@ts-expect-error' directive.
这是两个各自绿的 PR 合并后产生的语义冲突:
- objectstack-ai#3489(449227d)新增该测试时带 `@ts-expect-error`,当时正确 ——
彼时没有任何 tsc 程序 include `scripts/`,该导入确实无类型。
- objectstack-ai#3498(f995a45)引入 `tsconfig.scripts.json`(`allowJs: true`),
首次把 `scripts/**/*.ts` 纳入类型检查。`allowJs` 让 `.mjs` 助手的
类型可被推断,于是所有这类压制注释同时变成 Unused。objectstack-ai#3498 修掉了它
base 上的 5 处同类,而 objectstack-ai#3489 的第 6 处恰好落在其窗口之间。
两 PR 的 CI 各自为绿,合并结果为红 —— 当前所有 PR 的 Type Check 全部误红。
仅删除该行。运行期行为从不依赖该指令(它只影响 tsc,不影响 vitest),
`check-doc-links.test.ts` 22 个用例保持全绿。
Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Aug 10, 2026
…dicated tsconfig (objectstack-ai#3550)
* ci(root): type-check the four repo-root vitest.setup.* files via a dedicated tsconfig
四个仓库根文件 `vitest.setup.base.ts` / `vitest.setup.dom.tsx` /
`vitest.setup.dom-light.tsx` / `vitest.setup.tsx`此前不在任何 tsc program 里
(objectui#3515)。它们位于所有 workspace package 之外,`turbo run type-check`
按 package.json 的 scripts 驱动,结构上够不到;根 `tsconfig.json` 只 include
packages/examples/apps;`tsconfig.scripts.json` 只 include `scripts/**`。也没有
任何文件 import 它们——每个使用方都把它们写成 Vitest `setupFiles` 的运行时路径
字符串,所以也没有传递性的类型边。唯一包含它们的根 `tsconfig.node.json` 没有
`include`(默认整个仓库,今天报 21616 个错),且没有任何脚本运行它,不是门禁。
按 objectstack-ai#3494/PR objectstack-ai#3498 处理 `scripts/` 的先例,新增专用 root project
`tsconfig.vitest-setup.json` + `pnpm type-check:vitest-setup`,并在 ci.yml 的
type-check job 里接一步。
Fixesobjectstack-ai#3515
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
* docs(tsconfig): 更正注释里对 program 内容的描述
原注释说 `paths` 拖进来的东西「全是 .d.ts,被 skipLibCheck 跳过」。实测有且
只有一个例外:`@reduxjs/toolkit` 自带的 `dist/uncheckedindexed.ts`,被某个
依赖声明引用进来,因此是被真正检查的(当前干净)。pin test 的 node_modules
过滤也是为它开的,注释和测试现在说同一件事。
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTRjn8xBqp75dk7kFupVRt
---------
Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Aug 10, 2026
…t path filtering into the jobs (objectstack-ai#3523 steps 1-2) (objectstack-ai#3722)
* ci(step 1): subscribe ci/lint/control-bytes/docs-links to merge_group (objectstack-ai#3523)
Step 1 of objectui#3523, on its own commit as the issue's ruling requires:
this is a pure addition and changes nothing about which pull requests or
pushes run CI.
The merge queue is enforced on this repository by a ruleset (objectstack-ai#3243 measured a
direct push to `main` returning 405 `Changes must be made through the merge
queue`), but not one of the 22 workflows subscribed `merge_group` — repo-wide
`event=merge_group` runs stood at total_count = 0. A queue nothing subscribes
to can only carry an empty required-check set, so it rebuilt each PR on the
current `main` and let it through without validating anything. On 2026-08-07
that was cashed in: objectstack-ai#3503 / objectstack-ai#3510 / objectstack-ai#3516 merged between 02:11Z and 02:15Z
with `Type Check` at conclusion=failure, on a `main` poisoned by objectstack-ai#3498, and
objectstack-ai#3505 had to hot-fix it.
The four workflows named by the issue now carry the trigger. It is spelled
`merge_group: types: [checks_requested]`; `checks_requested` is the only
activity type GitHub defines for this event today, so this is equivalent to
objectstack's bare `merge_group:` and merely refuses to inherit a second type
added later.
Two `ci.yml` jobs additionally needed to be told the third event exists —
both changes are no-ops for `pull_request` and `push`:
- `test` moves from `if: github.event_name == 'pull_request'` to
`!= 'push'`, so a queue build actually runs the suite instead of skipping
every shard.
- `docs`'s `should_run` treated anything that is not `push` as a pull request
and diffed `github.event.pull_request.*`, which is null on a queue build —
an empty revision range, i.e. the site build silently skipped on the last
check before `main`.
Verified rather than assumed: `concurrency` needs no merge-queue case. On
`merge_group` the group expression falls back to `github.ref`, which is the
queue's own generation — measured on objectstack, whose 3552 queue runs report
head_branch `gh-readonly-queue/main/pr-6594-251e888a…`. That can collide with
neither a PR group (a bare number) nor a push group (`refs/heads/main`).
Refs objectstack-ai#3523
* ci(step 2): move ci/lint path filtering out of the PR trigger into the jobs (objectstack-ai#3523)
Step 2 of objectui#3523, deliberately a separate commit from step 1: step 1
adds runs that did not exist, this one changes which pull requests start CI,
and mixing them would make that impossible to review apart.
`on.pull_request.paths-ignore` skips the WHOLE workflow when every changed
file matches, and GitHub has no per-job path filter. A docs-only or
changeset-only PR therefore started neither `ci.yml` nor `lint.yml` — objectstack-ai#3509
measured zero check runs from them. A check that is never *created* does not
fail a required-status-check rule, it leaves the pull request pending; inside
the merge queue it fails on the ruleset's 60-minute status-check timeout. So
none of `Lint`, `Type Check`, `Test (shard N/4)`, `Build & E2E` or
`Changeset Fixed Group Check` could be made required while the filter lived
on the trigger — which is why the queue's required set was empty to begin
with.
The filter is not deleted, it moves. `type-check`, `test`, `e2e` (ci.yml) and
`lint` (lint.yml) each open with a `Decide whether this change needs a full
run` step, and every step after it carries
`if: steps.relevant.outputs.should_run == 'true'`. The job always runs and
always reports; the paths decide only whether it does any work. That is the
shape `ci.yml`'s `docs` job has used since objectstack-ai#3450 — not a new mechanism — and
the exclusion lists are byte-for-byte the `paths-ignore` they replace, so
which PRs pay for a full run is exactly as before.
Two deliberate narrowings, both stated so they can be argued with:
- `paths-ignore` stays on the `push` trigger. Branch protection and the merge
queue judge pull requests and queue builds, never pushes to `main`, so the
push lane gains nothing from losing it and would cost a full CI run on
every docs merge. It also gives the ignore list one authored home, which
`merge-queue-reporting.test.ts` then pins the in-job copies against.
- `changeset-check` is not gated. It is a checkout and one `node` call, so
short-circuiting it would cost more in complexity than it saves.
The gate fails OPEN: an unresolvable diff runs everything rather than
reporting green having built nothing (objectstack#4928's filter contract).
Measured both ways against a fixture repository — as shipped an unreachable
base sha yields should_run=true; rewritten with the `|| echo ""` spelling
`ci.yml`'s older `docs` gate still uses, the same input yields
should_run=false, i.e. a silent full skip. That pre-existing `docs` gate is
reported separately rather than changed here.
`scripts/__tests__/merge-queue-reporting.test.ts` pins both steps; against
the pre-change workflows 9 of its 10 assertions go red, each naming its own
regression. `lint-workflow.test.ts`'s TypeScript-exclusion tripwire now reads
the in-job list as well as the trigger, which is where that list now lives.
Refs objectstack-ai#3523
---------
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[ci] scripts/ 在零 tsconfig 覆盖内:turbo type-check 从不检查 scripts/__tests__/*.ts——一批门禁 pin 测试自身无类型门

2 participants

@yinlianghui@claude