Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .changeset/26209-create-in-repo-tsconfig-base.md
Original file line number Diff line number Diff line change
@@ -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"]`.
37 changes: 25 additions & 12 deletions packages/cli/src/commands/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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-<name>`
* landing under `packages/plugins/`. Explicit and
* `tsconfig.json` that extends this repo's shared
* `tsconfig.base.json`, a publishable
* `@objectstack/plugin-<name>` landing under
* `packages/plugins/`. Explicit and
* documented, never the default — its output
* installs nowhere else.
*
Expand Down Expand Up @@ -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. */
Expand Down Expand Up @@ -387,7 +394,7 @@ export const templates: Record<string, CreateTemplate> = {
// `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',
Expand Down Expand Up @@ -432,12 +439,18 @@ export const templates: Record<string, CreateTemplate> = {
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`
Expand Down
24 changes: 18 additions & 6 deletions packages/cli/test/create.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ import {
templates,
emittedPackageName,
objectstackDependencySpec,
rootTsconfigExtends,
baseTsconfigExtends,
DEFAULT_PLACEMENT,
type ScaffoldPlacement,
} from '../src/commands/create.js';
Expand Down Expand Up @@ -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();
Expand All @@ -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', () => {
Expand Down Expand Up @@ -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');
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down
Loading