Skip to content

Commit e7191ce

Browse files
Elon Muskclaude
andauthored
fix(build): give each exports condition its own types target in 27 dual-build packages (#13299)
* fix(build): give each exports condition its own types target in the 28 dual-build packages * feat(gates): check:dual-build-cjs-loads asserts the require condition resolves a CommonJS-flavoured types file that exists * fix(gates): TYPED vacuity floor counts entries judged, not entries clean * chore: changeset for the per-condition types declaration * fix(build): mirror the per-condition types shape on the 37 CJS-first packages (beyond the ruled scope; see PR body) * Revert the mirror half — beyond the ruled scope and it does not reach green (see PR body) * fix(build): hold @objectstack/core on the sibling-types shape — splitting its declarations splits ObjectKernel's identity * feat(gates): declare @objectstack/core's TYPED exemption with its measurement * fix(tests): read exports conditions through the nested shape in two guards --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent bcef1cf commit e7191ce

31 files changed

Lines changed: 786 additions & 123 deletions

File tree

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
---
2+
"@objectstack/cloud-connection": patch
3+
"@objectstack/lint": patch
4+
"@objectstack/mcp": patch
5+
"@objectstack/metadata-core": patch
6+
"@objectstack/metadata-fs": patch
7+
"@objectstack/metadata-protocol": patch
8+
"@objectstack/metadata": patch
9+
"@objectstack/observability": patch
10+
"@objectstack/plugin-webhooks": patch
11+
"@objectstack/rest": patch
12+
"@objectstack/runtime": patch
13+
"@objectstack/service-analytics": patch
14+
"@objectstack/service-automation": patch
15+
"@objectstack/service-cache": patch
16+
"@objectstack/service-cluster-redis": patch
17+
"@objectstack/service-cluster": patch
18+
"@objectstack/service-datasource": patch
19+
"@objectstack/service-i18n": patch
20+
"@objectstack/service-job": patch
21+
"@objectstack/service-knowledge": patch
22+
"@objectstack/service-messaging": patch
23+
"@objectstack/service-package": patch
24+
"@objectstack/service-queue": patch
25+
"@objectstack/service-realtime": patch
26+
"@objectstack/service-settings": patch
27+
"@objectstack/service-storage": patch
28+
"@objectstack/verify": patch
29+
---
30+
31+
fix(build): give each `exports` condition its own `types` target in the 28 dual-build packages (#13112)
32+
33+
**Published-surface change, zero runtime change.** No emitted byte moves; what
34+
moves is which declaration file a resolver READS. Maintainer ruling 2026-08-29
35+
(decision batch #3, verbatim 「同意」) chose declaring the files over deleting
36+
them.
37+
38+
## What was wrong
39+
40+
These 28 packages are `"type": "module"` and dual-built, and each spelled one
41+
`types` condition as a **sibling** of `import`/`require`:
42+
43+
```json
44+
"exports": { ".": {
45+
"types": "./dist/index.d.ts", "import": "./dist/index.js", "require": "./dist/index.cjs"
46+
} }
47+
```
48+
49+
A sibling `types` answers for **both** conditions, so a CommonJS consumer was
50+
handed `dist/index.d.ts` — an ES-module declaration, because the package is
51+
`"type": "module"` — for an entry point it reaches with `require`. Measured with
52+
`tsc --traceResolution` on a `"type": "commonjs"` fixture at `moduleResolution:
53+
node16`:
54+
55+
```
56+
error TS1479: The current file is a CommonJS module whose imports will produce
57+
'require' calls; however, the referenced file is an ECMAScript module and cannot
58+
be imported with 'require'.
59+
```
60+
61+
The JavaScript at `dist/index.cjs` loads perfectly (`check:dual-build-cjs-loads`
62+
has asserted that for months). It is the **types** that told the consumer the
63+
supported `require` entry point could not be required. The `dist/index.d.cts`
64+
twin tsup emits beside it — 36 files, 5,517,701 B on this build — was named by
65+
no condition at all and shipped in every tarball unreachable.
66+
67+
## What changed
68+
69+
Each condition now names its own declaration, the shape TypeScript documents:
70+
71+
```json
72+
"exports": { ".": {
73+
"import": { "types": "./dist/index.d.ts", "default": "./dist/index.js" },
74+
"require": { "types": "./dist/index.d.cts", "default": "./dist/index.cjs" }
75+
} }
76+
```
77+
78+
33 entry points across 27 packages, subpaths included. The root `types` field is
79+
untouched, so `node10` resolvers are unaffected; the `import` condition resolves
80+
exactly what it resolved before, measured as an unchanged control in the same
81+
run.
82+
83+
## `@objectstack/core` is deliberately NOT changed
84+
85+
Splitting a declaration in two makes TypeScript compare it nominally, and
86+
`ObjectKernel` carries a `private plugins` member that reaches every plugin
87+
through `PluginContext.getKernel()`. With core split, whole-repo `pnpm build`
88+
fails in `@objectstack/verify` with 5 × TS2345 ("Types have separate
89+
declarations of a private property 'plugins'"); with core held back and the
90+
other 27 split, 71/71 tasks pass. So core keeps the sibling-`types` shape and
91+
its two `.d.cts` files (220,854 B) stay unreachable, declared as such in
92+
`check:dual-build-cjs-loads`. Splitting it needs a decision about core's public
93+
types, not about an exports map.
94+
95+
## For consumers
96+
97+
- **ESM consumers: nothing changes.** Same declaration file, byte for byte.
98+
- **CJS consumers under `node16`/`nodenext`: TS1479 goes away** and the
99+
declarations they get are the ones built for CommonJS.
100+
- **`node10` / `moduleResolution: node` consumers: nothing changes** — they never
101+
read `exports`.
102+
- Nothing is removed: every path that resolved before still resolves.
103+
104+
Packages that are CJS-first (`require``./dist/index.js`, no `"type": "module"`)
105+
were already correct and are untouched — their `dist/index.d.ts` really is the
106+
CommonJS declaration. Their ESM mirror (an unreachable `.d.mts` under the
107+
`import` condition) is a separate, larger population and is filed separately per
108+
the ruling, not fixed here.
109+
110+
`check:dual-build-cjs-loads` grew a fourth invariant (TYPED) that reds on the old
111+
shape, so the drift cannot return silently.

packages/cli/test/serve-app-anchored-optional-import.e2e.test.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -289,12 +289,18 @@ function frameworkClusterEsmEntry(): string {
289289
const clusterEntry = createRequire(runtimeEntry).resolve(CLUSTER);
290290
let dir = dirname(clusterEntry);
291291
for (;;) {
292-
let manifest: { name?: string; exports?: Record<string, { import?: string }> } | undefined;
292+
let manifest: { name?: string; exports?: Record<string, { import?: string | { default?: string } }> } | undefined;
293293
try {
294294
manifest = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'));
295295
} catch { /* not a package root, or unreadable — keep climbing */ }
296296
if (manifest?.name === CLUSTER) {
297-
const entry = manifest.exports?.['.']?.import;
297+
// [#13112] The `import` condition is EITHER the ESM target itself or a
298+
// nested conditions object carrying it under `default` (the
299+
// per-condition `types` shape). Both spellings ship in this repo, so the
300+
// entry is read through the nesting; the refusal below still fires when
301+
// neither yields a path, which is the fact this guard exists to state.
302+
const importCondition = manifest.exports?.['.']?.import;
303+
const entry = typeof importCondition === 'string' ? importCondition : importCondition?.default;
298304
if (typeof entry !== 'string') {
299305
throw new Error(
300306
`${CLUSTER} at ${dir} declares no \`exports["."].import\` — there is no ESM entry to `

packages/cloud-connection/package.json

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,14 @@
88
"types": "dist/index.d.ts",
99
"exports": {
1010
".": {
11-
"types": "./dist/index.d.ts",
12-
"import": "./dist/index.js",
13-
"require": "./dist/index.cjs"
11+
"import": {
12+
"types": "./dist/index.d.ts",
13+
"default": "./dist/index.js"
14+
},
15+
"require": {
16+
"types": "./dist/index.d.cts",
17+
"default": "./dist/index.cjs"
18+
}
1419
}
1520
},
1621
"scripts": {

packages/lint/package.json

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,24 @@
88
"types": "dist/index.d.ts",
99
"exports": {
1010
".": {
11-
"types": "./dist/index.d.ts",
12-
"import": "./dist/index.js",
13-
"require": "./dist/index.cjs"
11+
"import": {
12+
"types": "./dist/index.d.ts",
13+
"default": "./dist/index.js"
14+
},
15+
"require": {
16+
"types": "./dist/index.d.cts",
17+
"default": "./dist/index.cjs"
18+
}
1419
},
1520
"./runtime": {
16-
"types": "./dist/runtime.d.ts",
17-
"import": "./dist/runtime.js",
18-
"require": "./dist/runtime.cjs"
21+
"import": {
22+
"types": "./dist/runtime.d.ts",
23+
"default": "./dist/runtime.js"
24+
},
25+
"require": {
26+
"types": "./dist/runtime.d.cts",
27+
"default": "./dist/runtime.cjs"
28+
}
1929
}
2030
},
2131
"scripts": {

packages/lint/src/rule-id-barrel-exports.test.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,11 +122,21 @@ function declaredRuleIds(): RuleId[] {
122122
describe('rule id constants are reachable from a published barrel (#5648)', () => {
123123
it('barrel entries match the published exports map', () => {
124124
const pkg = JSON.parse(readFileSync(join(pkgDir, 'package.json'), 'utf8')) as {
125-
exports: Record<string, { import: string }>;
125+
exports: Record<string, { import: string | { default?: string } }>;
126126
};
127127
// "." -> dist/index.js -> "index"; "./runtime" -> dist/runtime.js -> "runtime"
128+
//
129+
// [#13112] A condition's value is EITHER the target path itself, or a
130+
// nested conditions object carrying it under `default` (the per-condition
131+
// `types` shape). Both spellings ship in this repo — packages held at the
132+
// sibling-`types` shape keep the flat one — so read through the nesting
133+
// rather than assuming either. ⛔ Reading `e.import` blind stringifies an
134+
// object to "[object Object]", which matches nothing and silently empties
135+
// this census instead of failing loudly.
136+
const importTarget = (e: { import: string | { default?: string } }): string =>
137+
typeof e.import === 'string' ? e.import : e.import?.default ?? '';
128138
const published = Object.values(pkg.exports)
129-
.map((e) => /^\.\/dist\/(.+)\.js$/.exec(e.import)?.[1])
139+
.map((e) => /^\.\/dist\/(.+)\.js$/.exec(importTarget(e))?.[1])
130140
.filter((n): n is string => !!n)
131141
.sort();
132142
expect(published.length).toBe(Object.keys(pkg.exports).length);

packages/mcp/package.json

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,14 @@
88
"types": "dist/index.d.ts",
99
"exports": {
1010
".": {
11-
"types": "./dist/index.d.ts",
12-
"import": "./dist/index.js",
13-
"require": "./dist/index.cjs"
11+
"import": {
12+
"types": "./dist/index.d.ts",
13+
"default": "./dist/index.js"
14+
},
15+
"require": {
16+
"types": "./dist/index.d.cts",
17+
"default": "./dist/index.cjs"
18+
}
1419
}
1520
},
1621
"scripts": {

packages/metadata-core/package.json

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,14 @@
88
"types": "dist/index.d.ts",
99
"exports": {
1010
".": {
11-
"types": "./dist/index.d.ts",
12-
"import": "./dist/index.js",
13-
"require": "./dist/index.cjs"
11+
"import": {
12+
"types": "./dist/index.d.ts",
13+
"default": "./dist/index.js"
14+
},
15+
"require": {
16+
"types": "./dist/index.d.cts",
17+
"default": "./dist/index.cjs"
18+
}
1419
},
1520
"./testing": {
1621
"types": "./dist/testing.d.ts",

packages/metadata-fs/package.json

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,14 @@
88
"types": "dist/index.d.ts",
99
"exports": {
1010
".": {
11-
"types": "./dist/index.d.ts",
12-
"import": "./dist/index.js",
13-
"require": "./dist/index.cjs"
11+
"import": {
12+
"types": "./dist/index.d.ts",
13+
"default": "./dist/index.js"
14+
},
15+
"require": {
16+
"types": "./dist/index.d.cts",
17+
"default": "./dist/index.cjs"
18+
}
1419
}
1520
},
1621
"files": [

packages/metadata-protocol/package.json

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,14 @@
88
"types": "dist/index.d.ts",
99
"exports": {
1010
".": {
11-
"types": "./dist/index.d.ts",
12-
"import": "./dist/index.js",
13-
"require": "./dist/index.cjs"
11+
"import": {
12+
"types": "./dist/index.d.ts",
13+
"default": "./dist/index.js"
14+
},
15+
"require": {
16+
"types": "./dist/index.d.cts",
17+
"default": "./dist/index.cjs"
18+
}
1419
}
1520
},
1621
"files": [

packages/metadata/package.json

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,44 @@
88
"types": "dist/index.d.ts",
99
"exports": {
1010
".": {
11-
"types": "./dist/index.d.ts",
12-
"import": "./dist/index.js",
13-
"require": "./dist/index.cjs"
11+
"import": {
12+
"types": "./dist/index.d.ts",
13+
"default": "./dist/index.js"
14+
},
15+
"require": {
16+
"types": "./dist/index.d.cts",
17+
"default": "./dist/index.cjs"
18+
}
1419
},
1520
"./node": {
16-
"types": "./dist/node.d.ts",
17-
"import": "./dist/node.js",
18-
"require": "./dist/node.cjs"
21+
"import": {
22+
"types": "./dist/node.d.ts",
23+
"default": "./dist/node.js"
24+
},
25+
"require": {
26+
"types": "./dist/node.d.cts",
27+
"default": "./dist/node.cjs"
28+
}
1929
},
2030
"./migrations": {
21-
"types": "./dist/migrations/index.d.ts",
22-
"import": "./dist/migrations/index.js",
23-
"require": "./dist/migrations/index.cjs"
31+
"import": {
32+
"types": "./dist/migrations/index.d.ts",
33+
"default": "./dist/migrations/index.js"
34+
},
35+
"require": {
36+
"types": "./dist/migrations/index.d.cts",
37+
"default": "./dist/migrations/index.cjs"
38+
}
2439
},
2540
"./errors": {
26-
"types": "./dist/errors.d.ts",
27-
"import": "./dist/errors.js",
28-
"require": "./dist/errors.cjs"
41+
"import": {
42+
"types": "./dist/errors.d.ts",
43+
"default": "./dist/errors.js"
44+
},
45+
"require": {
46+
"types": "./dist/errors.d.cts",
47+
"default": "./dist/errors.cjs"
48+
}
2949
}
3050
},
3151
"files": [

0 commit comments

Comments
 (0)