diff --git a/.changeset/plugins-node-subpath-types.md b/.changeset/plugins-node-subpath-types.md new file mode 100644 index 000000000..0fdef69c9 --- /dev/null +++ b/.changeset/plugins-node-subpath-types.md @@ -0,0 +1,10 @@ +--- +'@xnetjs/plugins': patch +--- + +`@xnetjs/plugins/node` now reliably ships its type declarations. The two bundles +were built concurrently into overlapping output directories, so the main +bundle's clean step could delete `dist/services/node.d.ts` after the Node bundle +had written it — and the build still exited 0. A published package could +therefore carry `dist/services/node.js` with no declarations beside it, leaving +consumers to resolve the subpath as untyped JavaScript. diff --git a/packages/plugins/package.json b/packages/plugins/package.json index b6ccc344b..72fa87cda 100644 --- a/packages/plugins/package.json +++ b/packages/plugins/package.json @@ -29,7 +29,7 @@ "provenance": true }, "scripts": { - "build": "tsup", + "build": "tsup && tsup --config tsup.node.config.ts", "test": "vitest run", "test:watch": "vitest", "typecheck": "tsc --noEmit", diff --git a/packages/plugins/tsup.config.ts b/packages/plugins/tsup.config.ts index 62523c5d4..b904d43ec 100644 --- a/packages/plugins/tsup.config.ts +++ b/packages/plugins/tsup.config.ts @@ -1,25 +1,30 @@ import { defineConfig } from 'tsup' -export default defineConfig([ - // Browser-compatible bundle (main entry) - { - entry: ['src/index.ts'], - format: ['esm'], - dts: true, - clean: true, - outDir: 'dist', - splitting: false, - // Mark workspace packages as external - they're bundled by the consumer - external: ['@xnetjs/core', '@xnetjs/data'] - }, - // Node.js-only bundle (server-side code) - { - entry: ['src/services/node.ts'], - format: ['esm'], - dts: true, - outDir: 'dist/services', - splitting: false, - // Mark Node.js built-ins as external - external: ['http', 'child_process', 'net', 'readline', 'url', 'crypto', 'fs/promises', 'path'] - } -]) +// Browser-compatible bundle (main entry). +// +// The Node-only bundle lives in `tsup.node.config.ts` and is built as a +// SEPARATE, SEQUENTIAL tsup invocation (see the `build` script) rather than as a +// second element of an exported array. tsup runs array configs CONCURRENTLY, +// and this config's `clean` covers the whole of `dist/` — a strict superset of +// the Node bundle's `dist/services/` output. tsup cleans twice: `**/*` before +// the ESM phase, then `**/*.d.{ts,mts,cts}` RECURSIVELY before the DTS phase. +// Whichever of those landed after the Node bundle had written a file silently +// deleted it, and the build still exited 0 — so a green build could ship +// `dist/services/node.js` with no `node.d.ts` beside it. Consumers then +// resolved `@xnetjs/plugins/node` to bare JS and degraded it to `any` +// (TS7016), which surfaced downstream as a pile of unrelated-looking TS7006 +// "implicitly has an 'any' type" errors on every callback parameter in +// `@xnetjs/cli`. Turbo cached the truncated dist as a success on top of that. +// +// Keep these two builds in separate files and sequential: the clean and the +// write must never be able to interleave. +export default defineConfig({ + entry: ['src/index.ts'], + format: ['esm'], + dts: true, + clean: true, + outDir: 'dist', + splitting: false, + // Mark workspace packages as external - they're bundled by the consumer + external: ['@xnetjs/core', '@xnetjs/data'] +}) diff --git a/packages/plugins/tsup.node.config.ts b/packages/plugins/tsup.node.config.ts new file mode 100644 index 000000000..fe8166a61 --- /dev/null +++ b/packages/plugins/tsup.node.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from 'tsup' + +// Node.js-only bundle (server-side code), published as `@xnetjs/plugins/node`. +// +// Built by its own tsup invocation AFTER `tsup.config.ts`, never alongside it — +// see the comment there for why concurrency here silently truncated the output. +// `clean` is scoped to this outDir, so it can only ever remove what this config +// produces. +export default defineConfig({ + entry: ['src/services/node.ts'], + format: ['esm'], + dts: true, + clean: true, + outDir: 'dist/services', + splitting: false, + // Mark Node.js built-ins as external + external: ['http', 'child_process', 'net', 'readline', 'url', 'crypto', 'fs/promises', 'path'] +})