From 8b7f02a79fbd9b3254423338ea74c48b10c54df9 Mon Sep 17 00:00:00 2001 From: xNet Test Date: Sat, 1 Aug 2026 15:49:34 -0700 Subject: [PATCH] fix(plugins): stop the main bundle's clean from deleting the node bundle's types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `packages/plugins/tsup.config.ts` exported an array of two configs. tsup runs array configs CONCURRENTLY, and the first config's `clean` covers all of `dist/` — a strict superset of the second config's `dist/services/` output. tsup cleans twice per config: `**/*` before the ESM phase, then `**/*.d.{ts,mts,cts}` RECURSIVELY before the DTS phase (`cleanDtsFiles`). Whichever of those landed after the node bundle had already written a file silently deleted it, and the build still exited 0. The narrow window where the first clean lands before the node bundle's JS write but the DTS clean lands after its `.d.ts` write leaves exactly `dist/services/node.js` with no `node.d.ts` beside it — a green, cacheable, truncated build. Consumers then resolved `@xnetjs/plugins/node` to bare JavaScript and degraded it to `any` (TS7016), which surfaced downstream as ~25 unrelated-looking TS7006 "implicitly has an 'any' type" errors on callback parameters across `packages/cli/src/utils/{agent-remote,agent-local,vector-tier}.ts`. That made `turbo typecheck --affected` fail in the pre-commit hook for reasons pointing at innocent files, which pushes people toward `--no-verify`. Split the node bundle into `tsup.node.config.ts` and run the two builds sequentially, so a clean and a write can never interleave. Bundle contents are unchanged: identical entry, format, externals and output sizes. Verified: 5/5 consecutive builds emit both `.d.ts` files; `pnpm --filter @xnetjs/cli typecheck` passes; `pnpm check:packaging` passes (publint flags the missing declaration, so the poisoned state was reachable in CI too); 833 plugins tests pass. Co-Authored-By: Claude Opus 5 Signed-off-by: xNet Test --- .changeset/plugins-node-subpath-types.md | 10 +++++ packages/plugins/package.json | 2 +- packages/plugins/tsup.config.ts | 51 +++++++++++++----------- packages/plugins/tsup.node.config.ts | 18 +++++++++ 4 files changed, 57 insertions(+), 24 deletions(-) create mode 100644 .changeset/plugins-node-subpath-types.md create mode 100644 packages/plugins/tsup.node.config.ts 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'] +})