Skip to content

Commit d23ebb9

Browse files
Elon Muskclaude
andauthored
fix(metadata-core,service-cluster): stop emitting and publishing the CJS half of ./testing (#13114)
* wip: split tsup config by format so ./testing stops emitting CJS * changeset: dead testing.cjs no longer emitted or published --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 56470d8 commit d23ebb9

5 files changed

Lines changed: 123 additions & 15 deletions

File tree

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
---
2+
'@objectstack/metadata-core': patch
3+
'@objectstack/service-cluster': patch
4+
---
5+
6+
fix(metadata-core,service-cluster): stop emitting and publishing the CJS half of `./testing` (#13013)
7+
8+
#13001 made both `./testing` subpaths ESM-only, dropping the `require` condition
9+
that pointed at `dist/testing.cjs`. The build kept emitting those files and
10+
`files: ["dist"]` kept packing them, so every release shipped bytes no exports
11+
condition could reach. Measured with `npm pack --dry-run`, before → after:
12+
13+
| package | files | unpacked | dropped |
14+
|---|---|---|---|
15+
| `@objectstack/metadata-core` | 22 → 16 | 3.3 MB → 3.2 MB | `testing.cjs` (28.0 kB), `testing.cjs.map` (48.4 kB), `testing.d.cts` (9.4 kB), `chunk-H2D6OJ76.cjs` (4.2 kB) + map (10.6 kB), `repository-*.d.cts` |
16+
| `@objectstack/service-cluster` | 15 → 12 | 364.1 kB → 336.9 kB | `testing.cjs` (11.9 kB), `testing.cjs.map` (14.5 kB), `testing.d.cts` (794 B) |
17+
18+
Nothing reachable changed. The whole ESM surface of both packages — `index.js`,
19+
`testing.js`, their maps, the shared chunk, and every declaration the manifest
20+
names — is **byte-for-byte identical** to the previous build (sha256, before vs
21+
after). `index.cjs` changes only because what was a shared CJS chunk is now
22+
inlined into the sole remaining CJS entry.
23+
24+
Each `tsup.config.ts` becomes an array of two configs split **by format**
25+
ESM keeps both entries, CJS takes `src/index.ts` alone. The split is by format
26+
and never by entry: `index` and `testing` share a chunk carrying the error
27+
classes, and one config per entry would give `testing.js` its own copies, so
28+
`ConflictError` reached through `@objectstack/metadata-core/testing` would stop
29+
being the class thrown by `@objectstack/metadata-core` — which the published
30+
contract suite asserts (`.rejects.toBeInstanceOf(ConflictError)`).
31+
32+
`clean` moves out of tsup and into the `build` script (`rm -rf dist && tsup`).
33+
tsup runs an array config through `Promise.all`, so the halves build
34+
concurrently and a `clean` in either races the other's writes; the script-level
35+
clean is also stronger than tsup's own, which preserves `*.d.{ts,cts,mts}` and
36+
would therefore have left a stale `dist/testing.d.cts` behind on every rebuild
37+
of an existing worktree.

packages/metadata-core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
"CHANGELOG.md"
2424
],
2525
"scripts": {
26-
"build": "tsup && node ../../scripts/check-dts-emitted.mjs",
26+
"build": "rm -rf dist && tsup && node ../../scripts/check-dts-emitted.mjs",
2727
"dev": "tsc --watch",
2828
"clean": "rm -rf dist",
2929
"test": "vitest run",

packages/metadata-core/tsup.config.ts

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,25 @@
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+
]);

packages/services/service-cluster/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
}
1919
},
2020
"scripts": {
21-
"build": "tsup && node ../../../scripts/check-dts-emitted.mjs",
21+
"build": "rm -rf dist && tsup && node ../../../scripts/check-dts-emitted.mjs",
2222
"test": "vitest run"
2323
},
2424
"dependencies": {
Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,44 @@
11
// Copyright (c) 2026 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
external: ['vitest'],
14-
});
25+
};
26+
27+
// [#13013] The split is by FORMAT, never by ENTRY.
28+
//
29+
// `./testing` lost its `require` condition in #13001, so `dist/testing.cjs`,
30+
// its map and `dist/testing.d.cts` became unreachable through the manifest
31+
// while `files: ["dist"]` kept packing them for npm. Only the CJS half needs
32+
// to drop that entry.
33+
//
34+
// ⛔ Do NOT "simplify" this into one config per ENTRY. Both entries stay
35+
// together in the ESM half so that anything they share stays a shared chunk
36+
// rather than two copies with two module identities. Nothing is shared here
37+
// today — `src/testing.ts` imports only `vitest` and type-only symbols, and
38+
// the build emits no chunk at all — but that is a property of today's source,
39+
// not of this config, and the sibling `packages/metadata-core` carries the
40+
// measured version of what per-entry splitting costs when it stops holding.
41+
export default defineConfig([
42+
{ ...shared, entry: ['src/index.ts', 'src/testing.ts'], format: ['esm'] },
43+
{ ...shared, entry: ['src/index.ts'], format: ['cjs'] },
44+
]);

0 commit comments

Comments
 (0)