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: false 8 得给 5 个 .mjs 手写 .d.mts第二份事实来源,可以无声漂移 —— 正是 check-spec-symbol-derivation.mjs 存在的理由。而且 any 会外溢:3 条 TS2345 并非真缺陷,只是 any 的连带
allowJs: true 5 全部是「删掉一行已经变假的注释」。类型从 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(新增) —— 独立,刻意不 extends tsconfig.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

vercel Bot commented Aug 6, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectui Ignored Ignored Aug 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 queue Aug 6, 2026
Merged via the queue into main with commit f995a45 Aug 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 里接一步。

Fixes objectstack-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 free to 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