Skip to content

Commit 13e5c0e

Browse files
fix(cli): an os create --in-repo plugin passes its own typecheck and build (#26395)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"} --> _Requested by **Leehom** · [project thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1X28gvnxDESme7pSsouQvPf4)_ Fixes #26332 Clause-②: no Metadata-protocol: none ## 维护者速读 - **改了什么**:`os create plugin NAME --in-repo` 生成的包改为声明工作区的 TypeScript(`^6.0.3`,与仓根和所有兄弟插件一致);starter 的 `init(context)` 改为 `init(_context)` 并加一行说明。独立(默认)脚手架的 TypeScript 范围不变,仍是 `^5.3.0`。 - **为什么改**:在仓内新建插件后,第一条 `pnpm typecheck` / `pnpm build` 就红:TS5103(5.x 不认 base 的 `ignoreDeprecations: "6.0"`)和 TS6133(未用的 `context` 撞上 `noUnusedParameters`)。贡献者的第一步命令就失败。 - **风险与回滚**:低。只动脚手架输出和一个新测试;另把 `tsconfig.base.json` 登记为 cli 测试任务的输入(turbo.json 与两处门禁名单各一行,席位已批)。回滚即 revert 本 PR。 - **先看**:`packages/cli/src/commands/create.ts`(`IN_REPO_TYPESCRIPT_RANGE` / `typescriptDependencySpec` 与 starter 的 `init` 行);`packages/cli/test/create-in-repo-scaffold-typechecks.test.ts`;`turbo.json` 新增的一行输入。 - **两分钟验证**:`pnpm --filter @objectstack/cli exec vitest run test/create-in-repo-scaffold-typechecks.test.ts`,预期 5/5 通过(在 `main` 上同一文件 3 红)。 - **需要你做的**:无,席位落地。 ## What changes - **TS5103.** The `plugin` template's `devDependencies.typescript` now follows the placement through `typescriptDependencySpec(placement)`, the same shape as `objectstackDependencySpec`. `in-repo` declares `IN_REPO_TYPESCRIPT_RANGE` (`^6.0.3`), the range the repository root, `@objectstack/cli` and every sibling under `packages/plugins/` declare. `standalone` still declares `SCAFFOLD_TYPESCRIPT_RANGE` (`^5.3.0`), unchanged, and `init.ts` is not touched. - **TS6133.** The starter `src/index.ts` declares `async init(_context)`, with a two-line comment above it: the parameter is the kernel's plugin context, and the underscore goes once `init` reads it. The starter is one template for both placements, so the standalone `src/index.ts` takes the same rename. The standalone manifest, `tsconfig.json` and TypeScript range are byte-unchanged. - **Test-input declaration.** The new test copies the repo-root `tsconfig.base.json` into its sandbox, so `check:cross-package-test-inputs` requires that read declared. Three one-line additions, approved as claim amendments 6431310360 and 6431339243: `$TURBO_ROOT$/tsconfig.base.json` in `turbo.json`'s `@objectstack/cli#test` inputs, `tsconfig.base.json` in the `@objectstack/cli` roster globs (`scripts/cross-package-test-inputs.mjs`), and in the pinned `TURBO_ROOT_FILE_INPUTS` (`scripts/check-cross-package-test-inputs.mjs`). The file is already a turbo `globalDependencies` entry, which the gate does not read. - `.changeset/26332-create-in-repo-typescript.md`: `@objectstack/cli` patch, with the before/after and what to do for an in-repo plugin scaffolded earlier. ## Tests `packages/cli/test/create-in-repo-scaffold-typechecks.test.ts` (unit tier: it spawns `tsc`, not the CLI). It materialises the in-repo emission through `create`'s own renderers into a sandbox laid out like this repository (the real `tsconfig.base.json` at its root, the scaffold at `packages/plugins/plugin-probe`), then spawns the emitted `typecheck` and `build` scripts with the workspace's `tsc` 6.0.3. - Control: a planted unused parameter fails `typecheck` with TS6133, so the scaffold really compiles under the base's strictness. - `typecheck` and `build` exit 0; `build` writes `dist/index.js` and `dist/index.d.ts` and no `dist/index.test.js`. - The in-repo `typescript` range equals the range `@objectstack/cli` declares for the `tsc` the cases spawn, and its floor major accepts the base's `ignoreDeprecations` (read through `ts.readConfigFile`). - Standalone still declares `SCAFFOLD_TYPESCRIPT_RANGE`. Module line 2.6 s red and 3.2 s green on this box (a shared container), under the 10 s line, so no `test-budget` header. **Red first, on `main` `6390fe52c`** (the test file alone, source untouched): 3 failed, 2 passed. <details><summary>red run on main</summary> ```text × `typecheck` and `build` exit 0, and `build` leaves the starter test out of dist src/index.ts(10,14): error TS6133: 'context' is declared but its value is never read. × in-repo declares the workspace's TypeScript ... Expected: "^6.0.3" Received: "^5.3.0" × in-repo's TypeScript floor accepts the base's ignoreDeprecations (TS5103) Tests 3 failed | 2 passed (5) ``` </details> **Ablation** (one-shot, through `scripts/ablation-replace.mjs`, each leg restored to the HEAD blob with `git diff HEAD` empty; the test imports `src/`, so no build is involved): - `typescript: typescriptDependencySpec(placement)` back to `SCAFFOLD_TYPESCRIPT_RANGE`: 2 failed (the range and floor cases), 3 passed. - `async init(_context)` back to `async init(context)`: 1 failed (`typecheck`, TS6133), 4 passed. ## Acceptance notes - `IN_REPO_TYPESCRIPT_RANGE` is a literal pinned against `@objectstack/cli`'s own `devDependencies.typescript`. A workspace TypeScript bump reddens the pin until the constant moves with it. Reading the root manifest at scaffold time was not taken: the template renderers take only a placement, and a missing root range would need a fallback. - The standalone scaffold's `src/index.ts` changes by the `_context` rename (see What changes). Placement-gating the starter would teach two shapes of one plugin. - `@objectstack/cli`'s published entry re-exports only the command's default export, so the two new named exports in `create.ts` do not reach the published surface. - No open pull request touches `packages/cli/src/commands/create.ts` (20 open PRs listed, each one's files read). ## Verification Head `89522f6b5`. - `pnpm --filter @objectstack/cli exec vitest run --project unit` (at `aafab270`, the fix commit; later commits touch only the new test file and input declarations): 336/336 files, 4894/4894 tests. Re-run on the final head for the touched files: `create-in-repo-scaffold-typechecks`, `create`, `scaffold-test-script-passes`, 3/3 files, 31/31 tests. The `integration` tier is declared to CI. - `pnpm --filter @objectstack/cli build` exit 0; `pnpm --filter @objectstack/cli typecheck` (tsc plus `check:test-typecheck`) exit 0. - `eslint --no-inline-config packages/cli`: exit 0. - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 90 commands on this diff; each ran with its exit code captured by redirect before any pipe. All 90 exited 0, among them `check:cross-package-test-inputs` (red until `tsconfig.base.json` was declared), `check:turbo-task-graph`, `check:ci-filter-parity`, `check:pm-dispatch-gates` (fast tier, 1901 cases) and `check-plugin-teardown-shape --self-test` (after fetching its pinned positive-control commit). `check:dual-build-cjs-loads`, `check:dts-closure` and `check:i18n-coverage` measured on a tree an earlier sweep had built. - `--ran` reconciliation: "90 derived, 90 run, 0 NOT-MEASURED, 0 UNRUN", exit 0. - `node scripts/pm/check-governed-merges.mjs --branch claude/project-thread-z61um0`: 0 of 6 paths governed, ordinary queue landing. `origin/main` is 11 commits ahead at `abb904605`, none on this diff's files, and `git merge-tree` is clean; not merged in. --- _Generated by [Claude Code](https://claude.ai/code/session_019uZ84EhCM1NaBkJM8y1sjE)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent c51aed4 commit 13e5c0e

6 files changed

Lines changed: 212 additions & 3 deletions

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
'@objectstack/cli': patch
3+
---
4+
5+
fix(cli): an `os create plugin NAME --in-repo` scaffold passes its own `typecheck` and `build`
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
- **Before.** The in-repo plugin declared `typescript` as the standalone scaffold range (`^5.3.0`, installed as 5.9.3). It extends `tsconfig.base.json`, whose `ignoreDeprecations: "6.0"` only TypeScript 6 accepts, so `tsc` stopped with TS5103. With the workspace's TypeScript 6 it then stopped on the starter `init(context)` with TS6133, under the base's `noUnusedParameters`.
11+
- **After.** The in-repo placement declares the workspace's TypeScript (`^6.0.3`, the range the repository root and every sibling plugin declare). The starter's `init` parameter is `_context`, with a comment saying what it is for. A fresh in-repo scaffold's `typecheck` and `build` both exit 0.
12+
- The standalone (default) placement still declares `^5.3.0`, the documented TypeScript floor. Its starter `src/index.ts` takes the same `_context` rename, since both placements share one starter.
13+
14+
What to do: nothing for new scaffolds. An in-repo plugin scaffolded earlier: set its `devDependencies.typescript` to `^6.0.3`, and rename `init(context)` to `init(_context)` unless `init` reads it.

‎packages/cli/src/commands/create.ts‎

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,26 @@ export function objectstackDependencySpec(placement: ScaffoldPlacement): string
168168
return placement === 'in-repo' ? 'workspace:*' : getCliVersion();
169169
}
170170

171+
/**
172+
* The TypeScript range an emitted manifest declares, by placement.
173+
*
174+
* `standalone` is `SCAFFOLD_TYPESCRIPT_RANGE`, the support floor the docs
175+
* promise a developer's own project. `in-repo` is this workspace's TypeScript:
176+
* the scaffold extends `tsconfig.base.json`, whose `ignoreDeprecations: "6.0"`
177+
* only TypeScript 6 accepts, so the standalone floor (installed as 5.x) failed
178+
* the new package's first `tsc` with TS5103. ⛔ Never `SCAFFOLD_TYPESCRIPT_RANGE`
179+
* here, and never this range for `standalone`.
180+
*
181+
* `test/create-in-repo-scaffold-typechecks.test.ts` pins it equal to the range
182+
* this package declares (the root and every sibling package declare the same
183+
* one), so a workspace TypeScript bump reddens there until this moves with it.
184+
*/
185+
export const IN_REPO_TYPESCRIPT_RANGE = '^6.0.3';
186+
187+
export function typescriptDependencySpec(placement: ScaffoldPlacement): string {
188+
return placement === 'in-repo' ? IN_REPO_TYPESCRIPT_RANGE : SCAFFOLD_TYPESCRIPT_RANGE;
189+
}
190+
171191
/**
172192
* The `extends` an in-repo scaffold needs to reach this repo's shared
173193
* `tsconfig.base.json` — the file every package here extends — DERIVED from
@@ -421,7 +441,7 @@ export const templates: Record<string, CreateTemplate> = {
421441
},
422442
devDependencies: {
423443
'@types/node': SCAFFOLD_TYPES_NODE_RANGE,
424-
typescript: SCAFFOLD_TYPESCRIPT_RANGE,
444+
typescript: typescriptDependencySpec(placement),
425445
vitest: SCAFFOLD_VITEST_RANGE,
426446
},
427447
// The same npm override `os init` emits, from the same constant, so
@@ -475,7 +495,9 @@ export const ${sanitizeIdentifier(name)}Plugin: Plugin = {
475495
name: '${name}',
476496
version: '0.1.0',
477497
478-
async init(context) {
498+
// \`_context\` is the kernel's plugin context: register services and hooks
499+
// through it. Drop the underscore once init reads it.
500+
async init(_context) {
479501
console.log('Initializing ${name} plugin...');
480502
// Plugin initialization logic
481503
},
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* PIN (#26332) — a fresh `os create plugin NAME --in-repo` passes its own
5+
* `typecheck` and `build` scripts.
6+
*
7+
* ## The defect
8+
*
9+
* Once the in-repo scaffold extended `tsconfig.base.json` and reached its own
10+
* `src/`, its first `pnpm typecheck` in the new package was red twice over:
11+
*
12+
* - TS5103 "Invalid value for '--ignoreDeprecations'": the manifest declared
13+
* `typescript` as `SCAFFOLD_TYPESCRIPT_RANGE` (`^5.3.0`, installed as
14+
* 5.9.3), while the base sets `ignoreDeprecations: "6.0"`, a value only
15+
* TypeScript 6 accepts. Every sibling package declares the workspace's
16+
* TypeScript 6 range.
17+
* - TS6133 "'context' is declared but its value is never read": the starter
18+
* `init(context)` against the base's `noUnusedParameters`.
19+
*
20+
* ## What is run, and where
21+
*
22+
* The in-repo emission is materialised through `create`'s own renderers into a
23+
* sandbox laid out like this repository: the real `tsconfig.base.json` copied
24+
* to the sandbox root, and the scaffold at `packages/plugins/plugin-NAME`
25+
* beneath it, so the emitted `extends` resolves exactly as it does in the
26+
* monorepo. The emitted `typecheck` and `build` scripts are then spawned with
27+
* the workspace's own `tsc`, the compiler the in-repo manifest now declares.
28+
*
29+
* The sandbox lives under this package's `node_modules` (git-ignored), the
30+
* placement `scaffold-emission-typechecks.test.ts` documents: an emitted
31+
* `import … from '@objectstack/spec/…'` resolves by the ordinary upward walk.
32+
* TS2307 on the spec package means its `dist/` is not built; build it with
33+
* `pnpm --filter '@objectstack/cli^...' build`.
34+
*
35+
* The TypeScript VERSION a contributor installs cannot be spawned here without
36+
* an install, so the TS5103 half is pinned on the declaration instead: the
37+
* in-repo range is the range this package declares for the very `tsc` the
38+
* cases spawn (the workspace's TypeScript, which the root and every sibling
39+
* declare too), and its floor is a major that accepts the base's
40+
* `ignoreDeprecations`. The standalone range is pinned unchanged beside it.
41+
*
42+
* ## The control
43+
*
44+
* A planted unused parameter must fail the same `typecheck` with TS6133. Green
45+
* there would mean the scaffold no longer compiles under the base's
46+
* strictness, and the verdict on the starter would mean nothing.
47+
*/
48+
49+
import { afterAll, describe, expect, it } from 'vitest';
50+
import { spawnSync } from 'node:child_process';
51+
import fs from 'node:fs';
52+
import { createRequire } from 'node:module';
53+
import path from 'node:path';
54+
import { fileURLToPath } from 'node:url';
55+
import { IN_REPO_TYPESCRIPT_RANGE, templates } from '../src/commands/create.js';
56+
import { SCAFFOLD_TYPESCRIPT_RANGE } from '../src/commands/init.js';
57+
import { childEnv } from './helpers/serve-process.js';
58+
59+
const HERE = path.dirname(fileURLToPath(import.meta.url));
60+
const PKG = path.resolve(HERE, '..');
61+
const BASE_TSCONFIG = path.resolve(HERE, '../../..', 'tsconfig.base.json');
62+
63+
const require = createRequire(import.meta.url);
64+
const TSC = require.resolve('typescript/bin/tsc');
65+
const ts = require('typescript') as typeof import('typescript');
66+
67+
const PROJECT = 'probe';
68+
const template = templates.plugin;
69+
70+
const TMP_ROOT = fs.mkdtempSync(path.join(HERE, '..', 'node_modules', '.create-in-repo-typecheck-'));
71+
afterAll(() => {
72+
fs.rmSync(TMP_ROOT, { recursive: true, force: true });
73+
});
74+
75+
interface Manifest {
76+
scripts: Record<string, string>;
77+
devDependencies: Record<string, string>;
78+
}
79+
80+
function manifestFor(placement: 'in-repo' | 'standalone'): Manifest {
81+
return template.filesFor(placement)['package.json'](PROJECT) as Manifest;
82+
}
83+
84+
/** Materialise the in-repo emission under a copy of this repo's base; returns the scaffold dir. */
85+
function emitInRepo(): string {
86+
const workspace = fs.mkdtempSync(path.join(TMP_ROOT, 'ws-'));
87+
fs.copyFileSync(BASE_TSCONFIG, path.join(workspace, 'tsconfig.base.json'));
88+
const dir = path.join(workspace, template.inRepoDir, template.dirName(PROJECT));
89+
for (const [filePath, render] of Object.entries(template.filesFor('in-repo'))) {
90+
const content = render(PROJECT);
91+
fs.mkdirSync(path.dirname(path.join(dir, filePath)), { recursive: true });
92+
fs.writeFileSync(
93+
path.join(dir, filePath),
94+
typeof content === 'string' ? content : JSON.stringify(content, null, 2) + '\n',
95+
);
96+
}
97+
return dir;
98+
}
99+
100+
/** Spawn one emitted script, its `tsc` resolved to the workspace's compiler. */
101+
function runScript(dir: string, script: string): { code: number | null; output: string } {
102+
const [bin, ...args] = manifestFor('in-repo').scripts[script].trim().split(/\s+/);
103+
expect(bin, `the in-repo \`${script}\` script no longer runs tsc`).toBe('tsc');
104+
const res = spawnSync(process.execPath, [TSC, '--pretty', 'false', ...args], {
105+
cwd: dir,
106+
encoding: 'utf-8',
107+
env: childEnv({ NO_COLOR: '1' }),
108+
});
109+
return { code: res.status, output: `${res.stdout ?? ''}${res.stderr ?? ''}` };
110+
}
111+
112+
/** The `ignoreDeprecations` value the base sets, read through the compiler's own config parser. */
113+
function baseIgnoreDeprecations(): string {
114+
const read = ts.readConfigFile(BASE_TSCONFIG, ts.sys.readFile);
115+
expect(read.error, 'tsconfig.base.json did not parse').toBeUndefined();
116+
return String(read.config.compilerOptions.ignoreDeprecations);
117+
}
118+
119+
describe('os create plugin --in-repo: the scaffold passes its own scripts', () => {
120+
it('a planted unused parameter fails `typecheck` with TS6133 (control)', () => {
121+
const dir = emitInRepo();
122+
fs.writeFileSync(path.join(dir, 'src', 'canary.ts'), 'export function canary(unusedProbe: string) {}\n');
123+
124+
const { code, output } = runScript(dir, 'typecheck');
125+
126+
expect(code, `the canary compiled, so the scaffold is not under the base's strictness:\n${output}`).not.toBe(0);
127+
expect(output).toContain('TS6133');
128+
expect(output).toContain('unusedProbe');
129+
}, 120_000);
130+
131+
it('`typecheck` and `build` exit 0, and `build` leaves the starter test out of dist', () => {
132+
const dir = emitInRepo();
133+
134+
const typecheck = runScript(dir, 'typecheck');
135+
expect(typecheck.code, `a fresh in-repo scaffold fails its own \`typecheck\`:\n${typecheck.output}`).toBe(0);
136+
137+
const build = runScript(dir, 'build');
138+
expect(build.code, `a fresh in-repo scaffold fails its own \`build\`:\n${build.output}`).toBe(0);
139+
expect(fs.existsSync(path.join(dir, 'dist', 'index.js'))).toBe(true);
140+
expect(fs.existsSync(path.join(dir, 'dist', 'index.d.ts'))).toBe(true);
141+
expect(fs.existsSync(path.join(dir, 'dist', 'index.test.js'))).toBe(false);
142+
}, 120_000);
143+
});
144+
145+
describe('os create plugin: the TypeScript each placement declares', () => {
146+
it('in-repo declares the workspace\'s TypeScript, the range of the tsc the cases above spawn', () => {
147+
const own = JSON.parse(fs.readFileSync(path.join(PKG, 'package.json'), 'utf8')) as Manifest;
148+
expect(manifestFor('in-repo').devDependencies.typescript).toBe(own.devDependencies.typescript);
149+
expect(IN_REPO_TYPESCRIPT_RANGE).toBe(own.devDependencies.typescript);
150+
});
151+
152+
it('in-repo\'s TypeScript floor accepts the base\'s ignoreDeprecations (TS5103)', () => {
153+
const floorMajor = Number(/^\^?(\d+)\./.exec(manifestFor('in-repo').devDependencies.typescript)?.[1]);
154+
const acceptedFrom = Number(/^(\d+)\./.exec(baseIgnoreDeprecations())?.[1]);
155+
expect(Number.isFinite(acceptedFrom), 'the base sets no numeric ignoreDeprecations').toBe(true);
156+
expect(floorMajor).toBeGreaterThanOrEqual(acceptedFrom);
157+
});
158+
159+
it('standalone still declares SCAFFOLD_TYPESCRIPT_RANGE', () => {
160+
expect(manifestFor('standalone').devDependencies.typescript).toBe(SCAFFOLD_TYPESCRIPT_RANGE);
161+
});
162+
});

‎scripts/check-cross-package-test-inputs.mjs‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1458,8 +1458,11 @@ export const TOP_LEVEL_DIRECTORIES_AWAITING_IMPORT = Object.freeze({});
14581458
* Repo-root FILES declared as `$TURBO_ROOT$` inputs -- the fourth shape, pinned, not collected.
14591459
* `vitest.preset.mts` is the shared vitest preset the package configs re-export by
14601460
* relative import; it is the `test` task's input so an edit to it reruns their suites.
1461+
* `tsconfig.base.json` is `@objectstack/cli#test`'s: the in-repo scaffold test copies
1462+
* it into its sandbox. It is a turbo `globalDependencies` entry too, which this gate
1463+
* does not read, so the task declares it as well.
14611464
*/
1462-
export const TURBO_ROOT_FILE_INPUTS = Object.freeze(['sdui.manifest.json', 'vitest.preset.mts']);
1465+
export const TURBO_ROOT_FILE_INPUTS = Object.freeze(['sdui.manifest.json', 'tsconfig.base.json', 'vitest.preset.mts']);
14631466

14641467
/** `.` is the only regex metacharacter these names carry today; escape the class anyway. */
14651468
const TOP_LEVEL_ALTERNATION = RECOGNISED_TOP_LEVEL_DIRECTORIES

‎scripts/cross-package-test-inputs.mjs‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -728,6 +728,13 @@ export const CROSS_PACKAGE_TEST_INPUTS = {
728728
// their trees: the pin reads nothing else from either package.
729729
'packages/spec/src/data/hook.zod.ts',
730730
'packages/runtime/src/sandbox/body-runner.ts',
731+
// The repo-root base every package extends. test/create-in-repo-scaffold-typechecks.test.ts
732+
// copies it into a sandbox laid out like this repo and type-checks an
733+
// `os create plugin --in-repo` scaffold under it, so a strictness or
734+
// `ignoreDeprecations` change in the base is exactly what must re-run
735+
// this suite. It is a turbo `globalDependencies` entry as well, which
736+
// this roster does not read.
737+
'tsconfig.base.json',
731738
],
732739
heldBy: {
733740
// The parity pin joins each barrel path from `SCAFFOLD_WIRED_BARRELS`, a

‎turbo.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,7 @@
255255
"!**/node_modules/**",
256256
"$TURBO_ROOT$/scripts/nightly-tiers.mjs",
257257
"$TURBO_ROOT$/scripts/nightly-tiers.d.mts",
258+
"$TURBO_ROOT$/tsconfig.base.json",
258259
"$TURBO_ROOT$/packages/verify/src/**",
259260
"$TURBO_ROOT$/packages/plugins/plugin-security/src/**",
260261
"$TURBO_ROOT$/packages/services/service-cluster/src/**",

0 commit comments

Comments
 (0)