Skip to content

Commit a8e7393

Browse files
fix(cli): os create --in-repo scaffolds extend tsconfig.base.json (#26331)
<!-- claude-projects-attribution: {"github_login":"os-steve"} --> _Requested by **Steve** · [project thread](https://claude.ai/code/project/chan_01FdL1XeUyvVNxLreh3F4Qi9?thread=cmsg_01FdL1XeUyvVNxLreh3F4Qi9S9xCvZEBTTjGaAh4xaZyrt)_ Fixes #26209 Clause-②: no Metadata-protocol: none ## What `os create plugin NAME --in-repo` wrote a `tsconfig.json` that extended the monorepo root `tsconfig.json`. Since #26203 that root file is the type-check program for `scripts/`, and its `exclude` (`packages`, `apps`, `examples`, `ui`) resolves against the root. The scaffold declared `include` but no `exclude`, so it inherited that list and matched none of its own `src/`. - `rootTsconfigExtends` is renamed `baseTsconfigExtends` and now derives the ascent to `tsconfig.base.json` (call site in the plugin template updated). - The in-repo template now declares the sibling shape (`packages/plugins/plugin-email` is the reference): `types: ["node"]` and its own `exclude: ["dist", "node_modules"]`. - The standalone (default) placement is unchanged. - Root `.changeset/` entry: `@objectstack/cli` patch. ## Measured **Before, on `origin/main` 817cb89** (scaffolded with `packages/cli/bin/run-dev.js create plugin probe26209 --in-repo`, then `tsc --listFilesOnly -p tsconfig.json` in the scaffold, tsc 6.0.3): ```text error TS18003: No inputs were found in config file '.../packages/plugins/plugin-probe26209/tsconfig.json'. Specified 'include' paths were '["src/**/*"]' and 'exclude' paths were '["../../../node_modules","../../../packages","../../../apps","../../../examples","../../../ui"]'. exit 1 ``` So the inherited `exclude` dropped the scaffold's whole `src/`, and the template now carries its own `exclude`. **After, on this branch** (same scaffold command): ```text tsc --listFilesOnly -p tsconfig.json exit 0 SCAFFOLD/src/index.ts SCAFFOLD/src/index.test.ts ``` `tsc --showConfig` of the scaffold against `packages/plugins/plugin-email`, keys compared one by one: **the only difference is `files`** (2 files against 59, each package's own `src/`). `compilerOptions`, `include` and `exclude` are byte-equal after showConfig's path normalisation (`outDir` `./dist`, `rootDir` `./src` on both). That is inside the card's allowance (`include`, `outDir`, `rootDir`). ## Tests - `packages/cli/test/create.test.ts`: the in-repo `extends` arithmetic now resolves to `tsconfig.base.json`; a new case pins that the template extends the base (never the root program), owns `exclude: ["dist", "node_modules"]` and declares `types: ["node"]`; the function test follows the rename. - `packages/cli/test/init-template-comments-self-contained.test.ts`: comment only, the renamed function and the measured in-repo value; it carries no assertion on the target. - Ablation (one-shot, not kept): `scripts/ablation-replace.mjs` reverted the ascent target to `tsconfig.json`; `create.test.ts` went red on 3 cases (the arithmetic, the new base pin, the function test), and the restore was byte-identical to HEAD (`git diff HEAD` empty). - `pnpm --filter @objectstack/cli exec vitest run --project unit`: 332/334 files on the first pass; the 2 red files (`published-subpath-console.pin`, `published-subpath-hook-body.pin`) refused because `packages/cli/dist` was not built yet. After a full `pnpm build` (119/119 tasks) those two plus both touched files: 4/4 files, 84/84 tests. - `pnpm --filter @objectstack/cli typecheck`: exit 0. - `node scripts/pm/dispatch-gates.mjs --commands` derived 67 families at e4865e1; all 67 run with exit codes recorded, `--ran` reconciliation: 0 NOT-MEASURED. `check-plugin-teardown-shape --self-test` first exited 3 (shallow clone could not read its pinned fixture commit); after fetching that commit it exited 0. The `integration` CLI tier and `test:repo` are left to CI. ## Acceptance notes - With the base fixed, the in-repo scaffold reaches its own `src/`, but its own `pnpm typecheck` and `pnpm build` are still red on two things this PR does not change (both were hidden behind TS18003 before): 1. TS5103 `Invalid value for '--ignoreDeprecations'`: the in-repo `package.json` declares `typescript` `SCAFFOLD_TYPESCRIPT_RANGE` (`^5.3.0`, resolves to 5.9.3), while `tsconfig.base.json` sets `ignoreDeprecations: "6.0"`, which only TypeScript 6 accepts. Siblings declare `^6.0.3`. The range is a deliberate scaffold policy in `init.ts`, so it is reported, not changed here. 2. TS6133 `'context' is declared but its value is never read` in the emitted `src/index.ts`, under the base's `noUnusedParameters: true` (measured with the workspace tsc 6.0.3). Both are reported to the seat for a follow-up card. #26209 is not blocked by them: its acceptance diff (`showConfig` against a sibling) holds. - `check:type-check-debt` prints a surplus of 26 below the root ledger entry's ceiling; that entry is not touched here. - `origin/main` moved 11 commits since the branch point, touching other `packages/cli` files but none of this diff's; `git merge-tree` is clean. Not merged in. --- _Generated by [Claude Code](https://claude.ai/code/session_01B9rx6JYFJTqGy5tXRwwRfM)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent aea3443 commit a8e7393

4 files changed

Lines changed: 60 additions & 21 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): `os create plugin NAME --in-repo` scaffolds extend `tsconfig.base.json`, so `tsc` reaches the scaffold's own `src/`
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
- **Before.** The in-repo plugin's `tsconfig.json` extended the monorepo root `tsconfig.json`. That file is the type-check program for `scripts/`, and its `exclude` (`packages`, `apps`, `examples`, `ui`) resolves against the root, so a scaffold under `packages/plugins/` inherited it and matched none of its own files: `tsc` stopped with TS18003, "No inputs were found".
11+
- **After.** The emitted `tsconfig.json` extends `tsconfig.base.json`, the file every package in the monorepo extends, declares `types: ["node"]` and its own `exclude: ["dist", "node_modules"]`, the same shape as a sibling such as `plugin-email`. `tsc --showConfig` for the scaffold and for `plugin-email` differs only in each package's own file list.
12+
- The default (standalone) placement is unchanged.
13+
14+
What to do: nothing for new scaffolds. An in-repo plugin scaffolded earlier: change its `tsconfig.json` `extends` from `../../../tsconfig.json` to `../../../tsconfig.base.json`, and add `"types": ["node"]` and `"exclude": ["dist", "node_modules"]`.

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

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,10 @@
7373
* `private`, and the project lands in the
7474
* developer's own directory.
7575
* `in-repo` (--in-repo) the platform-work shape: `workspace:*` deps, a
76-
* `tsconfig.json` that extends this repo's root
77-
* config, a publishable `@objectstack/plugin-<name>`
78-
* landing under `packages/plugins/`. Explicit and
76+
* `tsconfig.json` that extends this repo's shared
77+
* `tsconfig.base.json`, a publishable
78+
* `@objectstack/plugin-<name>` landing under
79+
* `packages/plugins/`. Explicit and
7980
* documented, never the default — its output
8081
* installs nowhere else.
8182
*
@@ -168,15 +169,21 @@ export function objectstackDependencySpec(placement: ScaffoldPlacement): string
168169
}
169170

170171
/**
171-
* The `extends` an in-repo scaffold needs to reach this repo's root
172-
* `tsconfig.json`, DERIVED from where the template lands rather than written
173-
* down. Writing it down is how the `plugin` template came to declare
174-
* `'../../tsconfig.json'` from a directory two levels below `packages/`, which
175-
* resolves to a file that does not exist.
172+
* The `extends` an in-repo scaffold needs to reach this repo's shared
173+
* `tsconfig.base.json` — the file every package here extends — DERIVED from
174+
* where the template lands rather than written down. Writing it down is how the
175+
* `plugin` template came to declare `'../../tsconfig.json'` from a directory two
176+
* levels below `packages/`, which resolves to a file that does not exist.
177+
*
178+
* ⛔ Never the root `tsconfig.json`: that is the type-check program for
179+
* `scripts/` and the top-level configs, and its `exclude` (`packages`, `apps`,
180+
* `examples`, `ui`) resolves against the root, so a scaffold that inherited it
181+
* matched none of its own `src/` — `tsc` stopped with TS18003, "No inputs were
182+
* found".
176183
*/
177-
export function rootTsconfigExtends(inRepoDir: string, projectDirName: string): string {
184+
export function baseTsconfigExtends(inRepoDir: string, projectDirName: string): string {
178185
const depth = path.posix.join(inRepoDir, projectDirName).split('/').filter(Boolean).length;
179-
return `${'../'.repeat(depth)}tsconfig.json`;
186+
return `${'../'.repeat(depth)}tsconfig.base.json`;
180187
}
181188

182189
/** A rendered file: JSON objects are stringified on write, strings land as-is. */
@@ -387,7 +394,7 @@ export const templates: Record<string, CreateTemplate> = {
387394
// `tsc` emits ES modules under the compiler options below, so the
388395
// manifest has to declare the project as ESM or Node refuses the
389396
// emitted `dist/index.js`. The in-repo placement inherits its module
390-
// semantics from the root config it extends, so it does not.
397+
// semantics from the shared base config it extends, so it does not.
391398
...(standalone ? { type: 'module' } : {}),
392399
main: 'dist/index.js',
393400
types: 'dist/index.d.ts',
@@ -432,12 +439,18 @@ export const templates: Record<string, CreateTemplate> = {
432439
include: SCAFFOLD_TSCONFIG_INCLUDE_SRC_ONLY,
433440
})
434441
: {
435-
extends: rootTsconfigExtends(PLUGIN_IN_REPO_DIR, pluginDirName(name)),
442+
// The shape a sibling under `packages/plugins/` declares
443+
// (`plugin-email` is one): the shared base, Node's types, and
444+
// an `exclude` of its own, so nothing it inherits resolves
445+
// against another directory.
446+
extends: baseTsconfigExtends(PLUGIN_IN_REPO_DIR, pluginDirName(name)),
436447
compilerOptions: {
437448
outDir: 'dist',
438449
rootDir: 'src',
450+
types: ['node'],
439451
},
440452
include: ['src/**/*'],
453+
exclude: ['dist', 'node_modules'],
441454
},
442455
// The config `build` and `dev` compile with (#25175). `tsconfig.json`
443456
// includes all of `src/`, the starter test with it, so `typecheck`

‎packages/cli/test/create.test.ts‎

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ import {
4646
templates,
4747
emittedPackageName,
4848
objectstackDependencySpec,
49-
rootTsconfigExtends,
49+
baseTsconfigExtends,
5050
DEFAULT_PLACEMENT,
5151
type ScaffoldPlacement,
5252
} from '../src/commands/create.js';
@@ -191,7 +191,7 @@ describe.each(TEMPLATE_KEYS)('os create %s --in-repo — the platform-work emiss
191191
expect(objectstackDependencySpec('in-repo')).toBe('workspace:*');
192192
});
193193

194-
it('extends a tsconfig that resolves to the monorepo ROOT from where it lands', () => {
194+
it('extends a tsconfig that resolves to the monorepo root from where it lands', () => {
195195
const t = templates[key];
196196
const cfg = tsconfig(key, 'in-repo');
197197
expect(cfg.extends, `${key}: --in-repo tsconfig extends nothing`).toBeTruthy();
@@ -201,7 +201,19 @@ describe.each(TEMPLATE_KEYS)('os create %s --in-repo — the platform-work emiss
201201
// arithmetic, not a transcription: a template that moves takes its own
202202
// `extends` with it.
203203
const landedIn = `${t.inRepoDir}/${t.dirName(PROJECT)}`;
204-
expect(normalizeJoin(landedIn, cfg.extends)).toBe('tsconfig.json');
204+
expect(normalizeJoin(landedIn, cfg.extends)).toBe('tsconfig.base.json');
205+
});
206+
207+
it('extends the shared base, never the root scripts/ program, and owns its exclude', () => {
208+
const cfg = tsconfig(key, 'in-repo');
209+
// The root `tsconfig.json` is the type-check program for `scripts/`; its
210+
// `exclude` names `packages`, resolved against the ROOT, so a scaffold
211+
// under `packages/plugins/` that inherited it matched none of its own
212+
// `src/` and `tsc` stopped with TS18003. The base declares no `exclude`;
213+
// the scaffold's own one keeps it that way if the base ever grows one.
214+
expect(cfg.extends).toMatch(/(^|\/)tsconfig\.base\.json$/);
215+
expect(cfg.exclude, `${key}: --in-repo tsconfig inherits its exclude`).toEqual(['dist', 'node_modules']);
216+
expect(cfg.compilerOptions?.types).toEqual(['node']);
205217
});
206218

207219
it('emits no pnpm-workspace.yaml, which would sever the workspace it joins', () => {
@@ -319,10 +331,10 @@ describe('os create plugin: the emitted package name follows the placement (#155
319331
});
320332
});
321333

322-
describe('rootTsconfigExtends derives the ascent from where a template lands', () => {
334+
describe('baseTsconfigExtends derives the ascent from where a template lands', () => {
323335
it('counts the project directory itself', () => {
324-
expect(rootTsconfigExtends('packages/plugins', 'plugin-x')).toBe('../../../tsconfig.json');
325-
expect(rootTsconfigExtends('examples', 'my-app')).toBe('../../tsconfig.json');
336+
expect(baseTsconfigExtends('packages/plugins', 'plugin-x')).toBe('../../../tsconfig.base.json');
337+
expect(baseTsconfigExtends('examples', 'my-app')).toBe('../../tsconfig.base.json');
326338
});
327339
});
328340

‎packages/cli/test/init-template-comments-self-contained.test.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -266,9 +266,9 @@ const MONOREPO_ONLY = [
266266
// and `defineTemplate`'s `files` getter — the map `renderAll()` reads below
267267
// — is `filesFor(DEFAULT_PLACEMENT)`, i.e. `standalone`, whose emitted
268268
// `tsconfig.json` is self-contained. The one legitimate escaping `../` this
269-
// package can produce, `rootTsconfigExtends`, is emitted ONLY on the
269+
// package can produce, `baseTsconfigExtends`, is emitted ONLY on the
270270
// `in-repo` branch and so is never in this sweep. Measured: `filesFor`
271-
// `('in-repo')` yields `"../../../tsconfig.json"`; `template.files` and
271+
// `('in-repo')` yields `"../../../tsconfig.base.json"`; `template.files` and
272272
// `filesFor('standalone')` yield no `../` at all.
273273
//
274274
// ⚠️ THAT IS THE POPULATION ARGUMENT, NOT A PROPERTY OF THE PATTERN, and the
@@ -282,7 +282,7 @@ const MONOREPO_ONLY = [
282282
// So there are TWO conditions to re-read this on, not one, and the second is
283283
// the likelier:
284284
// 1. the population widens to the `in-repo` placement, which emits
285-
// `rootTsconfigExtends`; or
285+
// `baseTsconfigExtends`; or
286286
// 2. a template grows an intra-project relative climb — an emitted source
287287
// file nested deeply enough to reach back up toward its own project
288288
// root. This needs NO placement change at all.

0 commit comments

Comments
 (0)