diff --git a/.changeset/26209-create-in-repo-tsconfig-base.md b/.changeset/26209-create-in-repo-tsconfig-base.md new file mode 100644 index 00000000000..8aea05cbe4f --- /dev/null +++ b/.changeset/26209-create-in-repo-tsconfig-base.md @@ -0,0 +1,14 @@ +--- +'@objectstack/cli': patch +--- + +fix(cli): `os create plugin NAME --in-repo` scaffolds extend `tsconfig.base.json`, so `tsc` reaches the scaffold's own `src/` + +Clause-②: no +Metadata-protocol: none + +- **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". +- **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. +- The default (standalone) placement is unchanged. + +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"]`. diff --git a/packages/cli/src/commands/create.ts b/packages/cli/src/commands/create.ts index 1cad9883fd5..8dbe328d3b0 100644 --- a/packages/cli/src/commands/create.ts +++ b/packages/cli/src/commands/create.ts @@ -73,9 +73,10 @@ * `private`, and the project lands in the * developer's own directory. * `in-repo` (--in-repo) the platform-work shape: `workspace:*` deps, a - * `tsconfig.json` that extends this repo's root - * config, a publishable `@objectstack/plugin-` - * landing under `packages/plugins/`. Explicit and + * `tsconfig.json` that extends this repo's shared + * `tsconfig.base.json`, a publishable + * `@objectstack/plugin-` landing under + * `packages/plugins/`. Explicit and * documented, never the default — its output * installs nowhere else. * @@ -168,15 +169,21 @@ export function objectstackDependencySpec(placement: ScaffoldPlacement): string } /** - * The `extends` an in-repo scaffold needs to reach this repo's root - * `tsconfig.json`, DERIVED from where the template lands rather than written - * down. Writing it down is how the `plugin` template came to declare - * `'../../tsconfig.json'` from a directory two levels below `packages/`, which - * resolves to a file that does not exist. + * The `extends` an in-repo scaffold needs to reach this repo's shared + * `tsconfig.base.json` — the file every package here extends — DERIVED from + * where the template lands rather than written down. Writing it down is how the + * `plugin` template came to declare `'../../tsconfig.json'` from a directory two + * levels below `packages/`, which resolves to a file that does not exist. + * + * ⛔ Never the root `tsconfig.json`: that is the type-check program for + * `scripts/` and the top-level configs, and its `exclude` (`packages`, `apps`, + * `examples`, `ui`) resolves against the root, so a scaffold that inherited it + * matched none of its own `src/` — `tsc` stopped with TS18003, "No inputs were + * found". */ -export function rootTsconfigExtends(inRepoDir: string, projectDirName: string): string { +export function baseTsconfigExtends(inRepoDir: string, projectDirName: string): string { const depth = path.posix.join(inRepoDir, projectDirName).split('/').filter(Boolean).length; - return `${'../'.repeat(depth)}tsconfig.json`; + return `${'../'.repeat(depth)}tsconfig.base.json`; } /** A rendered file: JSON objects are stringified on write, strings land as-is. */ @@ -387,7 +394,7 @@ export const templates: Record = { // `tsc` emits ES modules under the compiler options below, so the // manifest has to declare the project as ESM or Node refuses the // emitted `dist/index.js`. The in-repo placement inherits its module - // semantics from the root config it extends, so it does not. + // semantics from the shared base config it extends, so it does not. ...(standalone ? { type: 'module' } : {}), main: 'dist/index.js', types: 'dist/index.d.ts', @@ -432,12 +439,18 @@ export const templates: Record = { include: SCAFFOLD_TSCONFIG_INCLUDE_SRC_ONLY, }) : { - extends: rootTsconfigExtends(PLUGIN_IN_REPO_DIR, pluginDirName(name)), + // The shape a sibling under `packages/plugins/` declares + // (`plugin-email` is one): the shared base, Node's types, and + // an `exclude` of its own, so nothing it inherits resolves + // against another directory. + extends: baseTsconfigExtends(PLUGIN_IN_REPO_DIR, pluginDirName(name)), compilerOptions: { outDir: 'dist', rootDir: 'src', + types: ['node'], }, include: ['src/**/*'], + exclude: ['dist', 'node_modules'], }, // The config `build` and `dev` compile with (#25175). `tsconfig.json` // includes all of `src/`, the starter test with it, so `typecheck` diff --git a/packages/cli/test/create.test.ts b/packages/cli/test/create.test.ts index 89512dc364b..7ab22cf677d 100644 --- a/packages/cli/test/create.test.ts +++ b/packages/cli/test/create.test.ts @@ -46,7 +46,7 @@ import { templates, emittedPackageName, objectstackDependencySpec, - rootTsconfigExtends, + baseTsconfigExtends, DEFAULT_PLACEMENT, type ScaffoldPlacement, } from '../src/commands/create.js'; @@ -191,7 +191,7 @@ describe.each(TEMPLATE_KEYS)('os create %s --in-repo — the platform-work emiss expect(objectstackDependencySpec('in-repo')).toBe('workspace:*'); }); - it('extends a tsconfig that resolves to the monorepo ROOT from where it lands', () => { + it('extends a tsconfig that resolves to the monorepo root from where it lands', () => { const t = templates[key]; const cfg = tsconfig(key, 'in-repo'); 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 // arithmetic, not a transcription: a template that moves takes its own // `extends` with it. const landedIn = `${t.inRepoDir}/${t.dirName(PROJECT)}`; - expect(normalizeJoin(landedIn, cfg.extends)).toBe('tsconfig.json'); + expect(normalizeJoin(landedIn, cfg.extends)).toBe('tsconfig.base.json'); + }); + + it('extends the shared base, never the root scripts/ program, and owns its exclude', () => { + const cfg = tsconfig(key, 'in-repo'); + // The root `tsconfig.json` is the type-check program for `scripts/`; its + // `exclude` names `packages`, resolved against the ROOT, so a scaffold + // under `packages/plugins/` that inherited it matched none of its own + // `src/` and `tsc` stopped with TS18003. The base declares no `exclude`; + // the scaffold's own one keeps it that way if the base ever grows one. + expect(cfg.extends).toMatch(/(^|\/)tsconfig\.base\.json$/); + expect(cfg.exclude, `${key}: --in-repo tsconfig inherits its exclude`).toEqual(['dist', 'node_modules']); + expect(cfg.compilerOptions?.types).toEqual(['node']); }); 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 }); }); -describe('rootTsconfigExtends derives the ascent from where a template lands', () => { +describe('baseTsconfigExtends derives the ascent from where a template lands', () => { it('counts the project directory itself', () => { - expect(rootTsconfigExtends('packages/plugins', 'plugin-x')).toBe('../../../tsconfig.json'); - expect(rootTsconfigExtends('examples', 'my-app')).toBe('../../tsconfig.json'); + expect(baseTsconfigExtends('packages/plugins', 'plugin-x')).toBe('../../../tsconfig.base.json'); + expect(baseTsconfigExtends('examples', 'my-app')).toBe('../../tsconfig.base.json'); }); }); diff --git a/packages/cli/test/init-template-comments-self-contained.test.ts b/packages/cli/test/init-template-comments-self-contained.test.ts index a7194a658c7..61549015830 100644 --- a/packages/cli/test/init-template-comments-self-contained.test.ts +++ b/packages/cli/test/init-template-comments-self-contained.test.ts @@ -266,9 +266,9 @@ const MONOREPO_ONLY = [ // and `defineTemplate`'s `files` getter — the map `renderAll()` reads below // — is `filesFor(DEFAULT_PLACEMENT)`, i.e. `standalone`, whose emitted // `tsconfig.json` is self-contained. The one legitimate escaping `../` this - // package can produce, `rootTsconfigExtends`, is emitted ONLY on the + // package can produce, `baseTsconfigExtends`, is emitted ONLY on the // `in-repo` branch and so is never in this sweep. Measured: `filesFor` - // `('in-repo')` yields `"../../../tsconfig.json"`; `template.files` and + // `('in-repo')` yields `"../../../tsconfig.base.json"`; `template.files` and // `filesFor('standalone')` yield no `../` at all. // // ⚠️ THAT IS THE POPULATION ARGUMENT, NOT A PROPERTY OF THE PATTERN, and the @@ -282,7 +282,7 @@ const MONOREPO_ONLY = [ // So there are TWO conditions to re-read this on, not one, and the second is // the likelier: // 1. the population widens to the `in-repo` placement, which emits - // `rootTsconfigExtends`; or + // `baseTsconfigExtends`; or // 2. a template grows an intra-project relative climb — an emitted source // file nested deeply enough to reach back up toward its own project // root. This needs NO placement change at all.