11// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
22
3- import { defineConfig } from 'tsup' ;
3+ import { defineConfig , type Options } from 'tsup' ;
44
5- export default defineConfig ( {
6- entry : [ 'src/index.ts' , 'src/testing.ts' ] ,
5+ // Everything both halves below share. Spelled once so the two cannot drift in
6+ // anything except the two properties they exist to differ in: `entry`/`format`.
7+ //
8+ // [#13013] `clean` is NOT here, and is `false` in both halves — deliberately.
9+ // tsup runs an array config through `Promise.all` (`tsup/dist/index.js`, the
10+ // `Array.isArray(configData)` map), so the halves build CONCURRENTLY: a `clean`
11+ // in either one races the other's writes and can delete output that has already
12+ // landed, in either direction. The output folder is emptied once, before tsup
13+ // starts, by the `build` script in package.json. That is also a STRONGER clean
14+ // than tsup's own, which unshifts `!**/*.d.{ts,cts,mts}` and so PRESERVES stale
15+ // declarations — including exactly the `dist/testing.d.cts` this split exists
16+ // to stop emitting, which would otherwise survive every rebuild of an existing
17+ // worktree.
18+ const shared : Options = {
719 splitting : true ,
820 sourcemap : true ,
9- clean : true ,
21+ clean : false ,
1022 dts : ! process . env . OS_SKIP_DTS ,
11- format : [ 'esm' , 'cjs' ] ,
1223 target : 'es2020' ,
1324 // [#12971] LOAD-BEARING. `artifact-forward-conversion.ts` anchors its
1425 // `@objectstack/spec` version lookup with `createRequire(import.meta.url)`
@@ -32,7 +43,37 @@ export default defineConfig({
3243 // history. `pnpm check:dual-build-cjs-loads` holds the class: it
3344 // `require()`s every dual-built package's CJS entry point and reds on this
3445 // exact SyntaxError. Need-based injection — nothing here references
35- // `__dirname`/`__filename`, so the ESM build's shim path is a no-op.
46+ // `__dirname`/`__filename`, so the ESM build's shim path is a no-op, which
47+ // is why it stays on BOTH halves rather than only the CJS one: identical
48+ // options mean the ESM output is byte-for-byte what the single config
49+ // emitted before the split.
3650 shims : true ,
3751 external : [ 'vitest' ] ,
38- } ) ;
52+ } ;
53+
54+ // [#13013] The split is by FORMAT, never by ENTRY — that distinction is the
55+ // whole design and reversing it is a silent breaking change.
56+ //
57+ // `./testing` lost its `require` condition in #13001, so `dist/testing.cjs`,
58+ // its map and `dist/testing.d.cts` became unreachable through the manifest
59+ // while `files: ["dist"]` kept packing them for npm. Only the CJS half needs
60+ // to drop that entry.
61+ //
62+ // ⛔ Do NOT "simplify" this into one config per ENTRY. Both entries stay
63+ // together in the ESM half because they SHARE A CHUNK
64+ // (`src/errors.ts` + `src/canonicalize.ts`), and that chunk carries the error
65+ // CLASSES. One config per entry gives `testing.js` its own copy of them, so
66+ // `ConflictError` reached through `@objectstack/metadata-core/testing` stops
67+ // being the class thrown by `@objectstack/metadata-core` — and the contract
68+ // suite this entry point exists to publish asserts exactly that identity
69+ // (`src/contract-suite.ts`: `.rejects.toBeInstanceOf(ConflictError)`). Every
70+ // downstream driver package running the suite would fail on a change that
71+ // looks like a build-config tidy-up.
72+ //
73+ // The CJS half needs no such care: `.` is its only entry point, so there is
74+ // exactly one copy of those modules in the CJS output either way (with one
75+ // entry esbuild inlines what used to be a shared chunk).
76+ export default defineConfig ( [
77+ { ...shared , entry : [ 'src/index.ts' , 'src/testing.ts' ] , format : [ 'esm' ] } ,
78+ { ...shared , entry : [ 'src/index.ts' ] , format : [ 'cjs' ] } ,
79+ ] ) ;
0 commit comments