Skip to content

Commit 189373b

Browse files
os-zhuangclaude
andauthored
feat(spec): declare the browser boundary — a browser export condition excluding the pg-grammar arm (#11072) (#11098)
* feat(spec): declare the browser boundary — browser export condition excluding the pg-grammar arm (#11072) Option A per the maintainer's 2026-08-22 ruling: the five entries whose graph reaches the driver-config validators (., ./data, ./system, ./kernel, ./cloud) gain a browser export condition pointing at dist/browser/ bundles in which the postgres url refinement's pg-grammar arm (pg-connection-string, whose parse statically resolves require('fs')) is swapped for a dependency-free twin. Node-side resolution and behaviour are unchanged. check:browser-reachable-entries grows a second rule: every bundle a browser bundler resolves (browser-conditioned or not) must link no Node builtin and no server-only package, with a positive control on the Node side. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MsbKEG4LtERSLaDrbehM3e * docs(spec): README boundary rule + changeset for the browser condition (#11072) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MsbKEG4LtERSLaDrbehM3e --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 82cb6e8 commit 189373b

9 files changed

Lines changed: 701 additions & 92 deletions
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
Declare the package's browser boundary in the `exports` map (#11072): the five
6+
entries whose module graph reaches the driver-config validators (`.`, `./data`,
7+
`./system`, `./kernel`, `./cloud`) now carry a `browser` export condition
8+
pointing at bundles (`dist/browser/**`) in which the postgres `url`
9+
refinement's pg-grammar arm is excluded. `pg-connection-string` — the parser
10+
`pg` itself uses, and the one the #9091 refusal deliberately asks — statically
11+
resolves `require('fs')`, so any browser bundler whose client graph reached one
12+
of these entries failed on `Can't resolve 'fs'` (measured on objectui's docs
13+
site, Next.js/Turbopack).
14+
15+
Patch, not minor/major, because the change is additive resolution surface with
16+
zero Node-side movement: Node's resolver never matches `browser`, every
17+
existing `import`/`require` condition still points at the same files, and the
18+
full #9091 DSN refusal (multi-host, non-numeric port, scheme-less non-URL)
19+
still runs for every Node consumer — the existing `postgres.test.ts` pins hold
20+
it. In the browser-conditioned bundles the refinement degrades to the
21+
shape-only checks it already performs before `parse` (the unix-socket
22+
short-circuit and the fs-reading `?sslcert=`/`?sslkey=`/`?sslrootcert=`
23+
refusal); publish-time validation never legitimately runs in a browser.
24+
25+
The boundary is enforced at this producer from now on:
26+
`check:browser-reachable-entries` refuses any browser-resolvable bundle —
27+
browser-conditioned or not — that links a Node builtin or a declared
28+
server-only package, with a positive control on the Node side, so the next
29+
Node-only import fails this package's own CI instead of a downstream bundler.

packages/spec/README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,21 @@ bundlers may drop what a consumer does not reach — module-scope side effects
3838
in any published module are therefore also a defect (measured, not assumed;
3939
see objectstack#10031).
4040

41+
**Node-only imports are refused everywhere a browser bundler resolves**
42+
(#11072). Every entry is loadable from a browser build by construction —
43+
schema-bearing entries included — so no published bundle may link a Node
44+
builtin or a server-only package. The one sanctioned exception is the
45+
pg-grammar arm of the postgres `url` refinement (`pg-connection-string`, whose
46+
`parse` statically resolves `require('fs')`): the entries that reach it carry a
47+
`browser` export condition pointing at `dist/browser/**` bundles in which that
48+
arm is swapped for a dependency-free twin (`src/data/driver/pg-url-grammar.*`,
49+
`tsup.config.ts``swapServerOnlyGrammarArm`), and Node consumers keep the
50+
full DSN refusal unchanged. `check:browser-reachable-entries` enforces both
51+
halves — a browser-conditioned bundle that still links the parser, and a
52+
non-conditioned bundle that links anything Node-only, are each a red — with a
53+
positive control on the Node side. Need Node-only work in a schema? Route it
54+
through a seam module the browser pass swaps, never import it directly.
55+
4156
## Usage
4257

4358
**Recommended: Use `ObjectSchema.create()` with `Field.*` helpers for strict TypeScript validation:**

packages/spec/package.json

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,16 @@
88
"sideEffects": false,
99
"exports": {
1010
".": {
11+
"browser": {
12+
"import": {
13+
"types": "./dist/index.d.mts",
14+
"default": "./dist/browser/index.mjs"
15+
},
16+
"require": {
17+
"types": "./dist/index.d.ts",
18+
"default": "./dist/browser/index.js"
19+
}
20+
},
1121
"import": {
1222
"types": "./dist/index.d.mts",
1323
"default": "./dist/index.mjs"
@@ -18,6 +28,16 @@
1828
}
1929
},
2030
"./data": {
31+
"browser": {
32+
"import": {
33+
"types": "./dist/data/index.d.mts",
34+
"default": "./dist/browser/data/index.mjs"
35+
},
36+
"require": {
37+
"types": "./dist/data/index.d.ts",
38+
"default": "./dist/browser/data/index.js"
39+
}
40+
},
2141
"import": {
2242
"types": "./dist/data/index.d.mts",
2343
"default": "./dist/data/index.mjs"
@@ -28,6 +48,16 @@
2848
}
2949
},
3050
"./system": {
51+
"browser": {
52+
"import": {
53+
"types": "./dist/system/index.d.mts",
54+
"default": "./dist/browser/system/index.mjs"
55+
},
56+
"require": {
57+
"types": "./dist/system/index.d.ts",
58+
"default": "./dist/browser/system/index.js"
59+
}
60+
},
3161
"import": {
3262
"types": "./dist/system/index.d.mts",
3363
"default": "./dist/system/index.mjs"
@@ -38,6 +68,16 @@
3868
}
3969
},
4070
"./kernel": {
71+
"browser": {
72+
"import": {
73+
"types": "./dist/kernel/index.d.mts",
74+
"default": "./dist/browser/kernel/index.mjs"
75+
},
76+
"require": {
77+
"types": "./dist/kernel/index.d.ts",
78+
"default": "./dist/browser/kernel/index.js"
79+
}
80+
},
4181
"import": {
4282
"types": "./dist/kernel/index.d.mts",
4383
"default": "./dist/kernel/index.mjs"
@@ -128,6 +168,16 @@
128168
}
129169
},
130170
"./cloud": {
171+
"browser": {
172+
"import": {
173+
"types": "./dist/cloud/index.d.mts",
174+
"default": "./dist/browser/cloud/index.mjs"
175+
},
176+
"require": {
177+
"types": "./dist/cloud/index.d.ts",
178+
"default": "./dist/browser/cloud/index.js"
179+
}
180+
},
131181
"import": {
132182
"types": "./dist/cloud/index.d.mts",
133183
"default": "./dist/cloud/index.mjs"

0 commit comments

Comments
 (0)