docs(website): correct the library build, and record slice 0
Slice 0 built the module bundle skeleton against the contract and found that §3.6 does not work as written. It shows Rollup's `external` alongside the resolve aliases, and the two do not compose: Rollup asks `external` BEFORE Vite's alias resolver runs, so a specifier in both is marked external and never aliased. The chunk then emits bare `import "react"`, which no browser can resolve without an import map, and CSP forbids the inline script an import map has to be. It built cleanly and emitted exactly that. §3.6 is corrected: alias only, `external` empty, with the alias table shown in full because the anchoring is what stops `react` also capturing `react/jsx-runtime`. What `external` was guarding -- a missed alias welding a second React into the chunk -- moves to a resolution-time build plugin, and two properties of that plugin are now contract because both were wrong first: it hooks `transform` rather than `load` (first-wins, so it never ran), and its forbidden-package list is stated rather than derived from the alias list (deriving it means deleting an alias also deletes the guard). Also records slice 0's outcome in §2.7.1, including the finding that generalises past this repo: the boundary check failed on its own documentation, because the comments describing what it catches are written in the syntax it catches. Slice 8's §5.2 grep has the same problem waiting for it. And the loader skips a SYMLINKED module directory silently, which is the first thing to check when a module fails to appear locally. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -663,27 +663,33 @@ owns the paths it calls, which is correct: it owns the routes at the other end.
|
||||
|
||||
### 3.6 Vite library-mode build
|
||||
|
||||
The module's `vite.config.js`, and the four externals are the whole contract:
|
||||
The module's `vite.config.js`, and the shared-dependency aliases are the whole contract:
|
||||
|
||||
```js
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
plugins: [react(), assertSharedNotBundled()],
|
||||
resolve: {
|
||||
// ARRAY form with ANCHORED regexes. The object form does PREFIX matching, so
|
||||
// a `react` key silently also rewrites `react/jsx-runtime`.
|
||||
alias: [
|
||||
{ find: /^react$/, replacement: shim('react') },
|
||||
{ find: /^react\/jsx-runtime$/, replacement: shim('jsx-runtime') },
|
||||
{ find: /^react\/jsx-dev-runtime$/, replacement: shim('jsx-runtime') },
|
||||
{ find: /^react-dom$/, replacement: shim('react-dom') },
|
||||
{ find: /^react-dom\/client$/, replacement: shim('react-dom') },
|
||||
{ find: /^react-router-dom$/, replacement: shim('react-router-dom') },
|
||||
],
|
||||
},
|
||||
build: {
|
||||
lib: { entry: 'src/entry.jsx', formats: ['es'], fileName: () => 'entry.js' },
|
||||
outDir: 'dist',
|
||||
modulePreload: { polyfill: false }, // same reason as core: no inline bootstrap under CSP
|
||||
rollupOptions: {
|
||||
external: ['react', 'react-dom', 'react-dom/client', 'react-router-dom'],
|
||||
output: { paths: { /* rewritten to window.__rg by the shim below */ } },
|
||||
},
|
||||
rollupOptions: { external: [] }, // deliberately empty — see below
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
Rollup's `external` alone emits bare `import 'react'` specifiers, which the browser cannot resolve
|
||||
without an import map — and CSP forbids the inline `<script type="importmap">` that would provide
|
||||
one (`MODULE_SYSTEM.md` §1.14). The module therefore ships a two-line shim module that re-exports
|
||||
from the global, and aliases the four externals to it:
|
||||
Each aliased specifier resolves to a two-line shim that re-exports from the global:
|
||||
|
||||
```js
|
||||
// src/shim/react.js
|
||||
@@ -691,9 +697,35 @@ export default window.__rg.react
|
||||
export const { useState, useEffect, useMemo, useCallback, useRef, createElement, Fragment } = window.__rg.react
|
||||
```
|
||||
|
||||
**This is the highest-risk mechanical detail in the whole plan and it is exactly what the Phase 1
|
||||
spike exists to prove.** If it does not hold, §2.6 of the design of record is wrong and the client
|
||||
half needs rethinking before Phase 2 builds on it.
|
||||
**Corrected 2026-08-11, Phase 3 slice 0: `external` and the aliases do not compose, and this section
|
||||
used to show both.** Rollup asks `external` *before* Vite's alias resolver runs, so a specifier
|
||||
listed there is marked external and never aliased. The chunk then emits bare `import 'react'`
|
||||
specifiers, which the browser cannot resolve without an import map — and CSP forbids the inline
|
||||
`<script type="importmap">` that would provide one (`MODULE_SYSTEM.md` §1.14). Slice 0 shipped with
|
||||
both, built cleanly, and emitted exactly that chunk. So: **alias only, and `external` empty.**
|
||||
|
||||
What `external` was there to guard is real — an alias that misses means a second React welded into
|
||||
the chunk, which loads fine and then throws about an invalid hook call somewhere unrelated. That is
|
||||
guarded instead by a **resolution-time plugin that fails the build if a shared dependency resolves
|
||||
into `node_modules`**. Two things about it are contract, because both were wrong first:
|
||||
|
||||
- **It hooks `transform`, not `load`.** `load` is first-wins, so an earlier plugin returning the
|
||||
module's contents means the guard is never called for it. Written against `load` it sat in the
|
||||
build doing nothing, and a deliberately-broken alias produced a 24 kB chunk with react-router
|
||||
bundled and a green build.
|
||||
- **Its forbidden-package list is stated, not derived from the alias list.** Deriving it "so the two
|
||||
cannot disagree" means deleting an alias also deletes the guard against what that alias prevented
|
||||
— which is exactly when it is needed. What may not be bundled is a fact about `window.__rg`; a
|
||||
test asserts the aliases stay inside it.
|
||||
|
||||
The module's own boundary checks are `scripts/checkImports.js` (§5.1) and `scripts/checkExternals.js`,
|
||||
which asks the **built chunk** whether any bare specifier survived. That question cannot be asked of
|
||||
source: `import { useState } from 'react'` is correct in every file, and which React it becomes is
|
||||
decided here.
|
||||
|
||||
**This is the highest-risk mechanical detail in the whole plan.** The Phase 1 spike proved the
|
||||
approach; slice 0 proved the configuration, in a browser, under the enforced `script-src 'self'`,
|
||||
by checking each imported binding is **identity-equal** to the one core published.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user