style(docs): apply Prettier to the React islands spec and plan
Formatting only; no content change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -30,26 +30,26 @@
|
||||
|
||||
**New package `packages/react/`:**
|
||||
|
||||
| File | Responsibility |
|
||||
|---|---|
|
||||
| `package.json` | Package manifest; optional peer deps |
|
||||
| `src/snapshot-cache.ts` | Referentially-stable snapshot + selector caching. No React import. |
|
||||
| `src/store-bridge.ts` | `useWrnStore` hook over `useSyncExternalStore` |
|
||||
| `src/error-boundary.tsx` | Per-island React error boundary |
|
||||
| `src/island-runtime.ts` | Mount/unmount, strategies, root registry |
|
||||
| `src/runtime-source.ts` | `getIslandRuntime()` returning browser JS (mirrors `@wrnexus/csr`) |
|
||||
| `src/index.ts` | Public exports |
|
||||
| File | Responsibility |
|
||||
| ------------------------ | ------------------------------------------------------------------ |
|
||||
| `package.json` | Package manifest; optional peer deps |
|
||||
| `src/snapshot-cache.ts` | Referentially-stable snapshot + selector caching. No React import. |
|
||||
| `src/store-bridge.ts` | `useWrnStore` hook over `useSyncExternalStore` |
|
||||
| `src/error-boundary.tsx` | Per-island React error boundary |
|
||||
| `src/island-runtime.ts` | Mount/unmount, strategies, root registry |
|
||||
| `src/runtime-source.ts` | `getIslandRuntime()` returning browser JS (mirrors `@wrnexus/csr`) |
|
||||
| `src/index.ts` | Public exports |
|
||||
|
||||
**Modified:**
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `packages/compiler/src/import-resolver.ts` | Resolve `.tsx`; tag `kind: "island"` |
|
||||
| `packages/compiler/src/island-codegen.ts` *(new)* | Marker emission, props serialization, diagnostics |
|
||||
| `packages/compiler/src/island-bundle.ts` *(new)* | Island entry generation + `Bun.build` with shared React chunk |
|
||||
| `packages/dev-server/src/assets.ts` | Serve island routes in dev |
|
||||
| `packages/dev-server/src/prod.ts` | Serve island routes in prod |
|
||||
| `packages/cli/src/build.ts` | Emit island assets in static build |
|
||||
| File | Change |
|
||||
| ------------------------------------------------- | ------------------------------------------------------------- |
|
||||
| `packages/compiler/src/import-resolver.ts` | Resolve `.tsx`; tag `kind: "island"` |
|
||||
| `packages/compiler/src/island-codegen.ts` _(new)_ | Marker emission, props serialization, diagnostics |
|
||||
| `packages/compiler/src/island-bundle.ts` _(new)_ | Island entry generation + `Bun.build` with shared React chunk |
|
||||
| `packages/dev-server/src/assets.ts` | Serve island routes in dev |
|
||||
| `packages/dev-server/src/prod.ts` | Serve island routes in prod |
|
||||
| `packages/cli/src/build.ts` | Emit island assets in static build |
|
||||
|
||||
---
|
||||
|
||||
@@ -58,11 +58,13 @@
|
||||
The load-bearing piece. `readonlySnapshot` in `@wrnexus/store` returns a fresh `Object.freeze(clone(state))` on every call; `useSyncExternalStore` requires a stable reference or it throws and infinite-loops. This task builds the cache with **no React dependency**, so it is testable in isolation.
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `packages/react/package.json`
|
||||
- Create: `packages/react/src/snapshot-cache.ts`
|
||||
- Test: `packages/react/test/snapshot-cache.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
|
||||
- Consumes: nothing.
|
||||
- Produces:
|
||||
- `createSnapshotCache<S extends object>(source: SnapshotSource<S>): SnapshotCache<S>`
|
||||
@@ -193,9 +195,7 @@ export interface SnapshotCache<S extends object> {
|
||||
* and spins if given a fresh object each call, which `@wrnexus/store`'s
|
||||
* `readonlySnapshot` does by design.
|
||||
*/
|
||||
export function createSnapshotCache<S extends object>(
|
||||
source: SnapshotSource<S>,
|
||||
): SnapshotCache<S> {
|
||||
export function createSnapshotCache<S extends object>(source: SnapshotSource<S>): SnapshotCache<S> {
|
||||
let cached: Readonly<S> | undefined;
|
||||
let dirty = true;
|
||||
|
||||
@@ -259,12 +259,14 @@ git commit -m "feat(react): add referentially-stable snapshot and selector cache
|
||||
### Task 2: Store bridge hook
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `packages/react/src/store-bridge.ts`
|
||||
- Create: `packages/react/src/index.ts`
|
||||
- Modify: `package.json` (root) — add `react`, `react-dom` to devDependencies for tests
|
||||
- Test: `packages/react/test/store-bridge.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
|
||||
- Consumes: `createSnapshotCache`, `createSelectorCache` from Task 1.
|
||||
- Produces:
|
||||
- `useWrnStore<S extends object, R = Readonly<S>>(name: string, selector?: (state: Readonly<S>) => R): R`
|
||||
@@ -436,10 +438,12 @@ git commit -m "feat(react): add useWrnStore bridge over useSyncExternalStore"
|
||||
### Task 3: Island marker codegen
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `packages/compiler/src/island-codegen.ts`
|
||||
- Test: `packages/compiler/test/island-codegen.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
|
||||
- Consumes: `escapeHtml` from `@wrnexus/core` (`packages/core/src/security.ts`).
|
||||
- Produces:
|
||||
- `type IslandStrategy = "only" | "load" | "visible" | "idle"`
|
||||
@@ -596,10 +600,12 @@ git commit -m "feat(compiler): add island marker codegen and props contract"
|
||||
### Task 4: Resolve `.tsx` imports as islands
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `packages/compiler/src/import-resolver.ts`
|
||||
- Test: `packages/compiler/test/island-resolution.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
|
||||
- Consumes: nothing from prior tasks.
|
||||
- Produces: `ResolvedImport` gains an optional `kind?: "island"` field, set when the resolved path ends in `.tsx`.
|
||||
|
||||
@@ -705,12 +711,12 @@ export interface ResolvedImport {
|
||||
Then change the success return inside `resolveWrnImport` from `return { declaration, resolved: realpathSync(found) };` to:
|
||||
|
||||
```ts
|
||||
if (found) {
|
||||
const resolved = realpathSync(found);
|
||||
return resolved.endsWith(".tsx")
|
||||
? { declaration, resolved, kind: "island" }
|
||||
: { declaration, resolved };
|
||||
}
|
||||
if (found) {
|
||||
const resolved = realpathSync(found);
|
||||
return resolved.endsWith(".tsx")
|
||||
? { declaration, resolved, kind: "island" }
|
||||
: { declaration, resolved };
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Run test to verify it passes**
|
||||
@@ -735,10 +741,12 @@ git commit -m "feat(compiler): resolve .tsx imports and tag them as islands"
|
||||
### Task 5: Island error boundary
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `packages/react/src/error-boundary.tsx`
|
||||
- Test: `packages/react/test/error-boundary.test.tsx`
|
||||
|
||||
**Interfaces:**
|
||||
|
||||
- Consumes: nothing from prior tasks.
|
||||
- Produces: `IslandErrorBoundary` — a React component with props `{ name: string; development: boolean; children: ReactNode }`.
|
||||
|
||||
@@ -857,11 +865,13 @@ git commit -m "feat(react): add per-island error boundary"
|
||||
### Task 6: Island runtime — mount, strategies, unmount
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `packages/react/src/island-runtime.ts`
|
||||
- Modify: `packages/react/src/index.ts`
|
||||
- Test: `packages/react/test/island-runtime.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
|
||||
- Consumes: `IslandErrorBoundary` (Task 5), `setStoreResolver` (Task 2).
|
||||
- Produces:
|
||||
- `mountIslands(root: ParentNode, options: MountOptions): Promise<void>`
|
||||
@@ -1070,10 +1080,12 @@ git commit -m "feat(react): add island mount strategies and navigation-safe unmo
|
||||
### Task 7: Island bundling with a shared React chunk
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `packages/compiler/src/island-bundle.ts`
|
||||
- Test: `packages/compiler/test/island-bundle.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
|
||||
- Consumes: nothing from prior tasks.
|
||||
- Produces:
|
||||
- `generateIslandEntry(input: { name: string; sourcePath: string }): string`
|
||||
@@ -1226,6 +1238,7 @@ git commit -m "feat(compiler): bundle islands with a shared React chunk"
|
||||
### Task 8: Serve island assets in dev, prod, and static build
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `packages/react/src/runtime-source.ts`
|
||||
- Modify: `packages/dev-server/src/assets.ts`
|
||||
- Modify: `packages/dev-server/src/prod.ts`
|
||||
@@ -1233,6 +1246,7 @@ git commit -m "feat(compiler): bundle islands with a shared React chunk"
|
||||
- Test: `packages/react/test/runtime-source.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
|
||||
- Consumes: `mountIslands`, `unmountIslands` (Task 6).
|
||||
- Produces: `getIslandRuntime(development?: boolean): string` — browser JS served at `/__wrnexus/islands.js`, mirroring `getReactiveRuntime` in `@wrnexus/csr`.
|
||||
|
||||
@@ -1323,15 +1337,15 @@ import { getIslandRuntime } from "@wrnexus/react/runtime";
|
||||
Then inside `serve(pathname)`, next to the other `/__wrnexus/*.js` lines:
|
||||
|
||||
```ts
|
||||
if (pathname === "/__wrnexus/islands.js") return jsResponse(getIslandRuntime(true));
|
||||
if (pathname === "/__wrnexus/islands.js") return jsResponse(getIslandRuntime(true));
|
||||
```
|
||||
|
||||
The bootstrap dynamically imports `/__wrnexus/island/runtime.js` (the bundled mount runtime) and `/__wrnexus/island/<name>.js` (per-island bundles). Both live under the `/__wrnexus/island/` prefix, so add one prefix handler beside the existing `/__wrnexus/client/` handler at the top of `serve(pathname)`:
|
||||
|
||||
```ts
|
||||
if (pathname.startsWith("/__wrnexus/island/")) {
|
||||
return serveIslandArtifact(pathname) ?? new Response("Not Found", { status: 404 });
|
||||
}
|
||||
if (pathname.startsWith("/__wrnexus/island/")) {
|
||||
return serveIslandArtifact(pathname) ?? new Response("Not Found", { status: 404 });
|
||||
}
|
||||
```
|
||||
|
||||
where `serveIslandArtifact` reads from the island build output directory produced by `buildIslands` (Task 7), mirroring how `serveWrnBrowserArtifact` serves `/__wrnexus/client/`.
|
||||
@@ -1341,8 +1355,8 @@ where `serveIslandArtifact` reads from the island build output directory produce
|
||||
In `packages/dev-server/src/prod.ts`, add the same import, then beside the existing `getReactiveRuntime()` route:
|
||||
|
||||
```ts
|
||||
if (pathname === "/__wrnexus/islands.js")
|
||||
return new Response(getIslandRuntime(), { headers: JS_HEADERS });
|
||||
if (pathname === "/__wrnexus/islands.js")
|
||||
return new Response(getIslandRuntime(), { headers: JS_HEADERS });
|
||||
```
|
||||
|
||||
Add the same `/__wrnexus/island/` prefix handler here, serving the built island assets from the production output directory.
|
||||
@@ -1375,10 +1389,12 @@ git commit -m "feat(islands): serve the island runtime in dev, prod, and static
|
||||
A route containing an island is no longer zero-JS static — it is static-interactive. Without this, the framework's own performance reporting is wrong.
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `packages/compiler/src/analysis.ts`
|
||||
- Test: `packages/compiler/test/island-classification.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
|
||||
- Consumes: `ResolvedImport.kind` (Task 4).
|
||||
- Produces: `routeNeedsIslands(imports: ResolvedImport[]): boolean`, exported from `analysis.ts`.
|
||||
|
||||
@@ -1457,10 +1473,12 @@ git commit -m "feat(compiler): classify island routes as static-interactive"
|
||||
Two tests protecting the project's core promise. These must fail loudly if a future change regresses them.
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `examples/basic-app/app/islands/Counter.tsx`
|
||||
- Test: `packages/compiler/test/island-integration.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
|
||||
- Consumes: `buildIslands` (Task 7), `renderIslandMarker` (Task 3), `routeNeedsIslands` (Task 9).
|
||||
- Produces: nothing consumed downstream.
|
||||
|
||||
@@ -1493,10 +1511,7 @@ import { join, resolve } from "node:path";
|
||||
import { buildIslands } from "../src/island-bundle.ts";
|
||||
import { routeNeedsIslands } from "../src/analysis.ts";
|
||||
|
||||
const COUNTER = resolve(
|
||||
import.meta.dir,
|
||||
"../../../examples/basic-app/app/islands/Counter.tsx",
|
||||
);
|
||||
const COUNTER = resolve(import.meta.dir, "../../../examples/basic-app/app/islands/Counter.tsx");
|
||||
|
||||
test("a route with no islands ships zero framework JavaScript", async () => {
|
||||
const outDir = mkdtempSync(join(tmpdir(), "wrnexus-nojs-"));
|
||||
@@ -1522,8 +1537,8 @@ test("a page with multiple islands ships React exactly once", async () => {
|
||||
.filter((file) => file.endsWith(".js"))
|
||||
.map((file) => readFileSync(join(outDir, file), "utf8"));
|
||||
|
||||
const withReactInternals = bundles.filter((source) =>
|
||||
source.includes("react.development") || source.includes("REACT_ELEMENT_TYPE"),
|
||||
const withReactInternals = bundles.filter(
|
||||
(source) => source.includes("react.development") || source.includes("REACT_ELEMENT_TYPE"),
|
||||
);
|
||||
|
||||
expect(result.assets).toHaveLength(2);
|
||||
@@ -1564,12 +1579,14 @@ git commit -m "test(islands): guard zero-JS routes and single-React bundling"
|
||||
The spec's one author-facing rule — writes only from handlers or effects — is enforced in dev, not merely documented. React's own warning for this is too generic to diagnose quickly.
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `packages/react/src/render-phase.ts`
|
||||
- Modify: `packages/react/src/store-bridge.ts` (wrap actions in `useWrnActions`)
|
||||
- Modify: `packages/react/src/index.ts`
|
||||
- Test: `packages/react/test/render-phase.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
|
||||
- Consumes: `useWrnActions` (Task 2).
|
||||
- Produces:
|
||||
- `beginRenderPhase(): void`
|
||||
@@ -1730,12 +1747,14 @@ git commit -m "feat(react): fail loudly on store writes during island render"
|
||||
On island source change, unmount the root and re-mount with the new bundle. Component state resets on edit; that is the accepted v1 trade-off, and the concrete trigger for reconsidering Fast Refresh later.
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `packages/react/src/island-runtime.ts`
|
||||
- Modify: `packages/react/src/runtime-source.ts`
|
||||
- Modify: `packages/react/src/index.ts`
|
||||
- Test: `packages/react/test/island-hmr.test.ts`
|
||||
|
||||
**Interfaces:**
|
||||
|
||||
- Consumes: `mountIslands`, `unmountIslands` (Task 6).
|
||||
- Produces: `remountIslands(root: ParentNode, options: MountOptions): Promise<void>`
|
||||
|
||||
@@ -1751,8 +1770,7 @@ import { islandRootCount, mountIslands, remountIslands } from "../src/island-run
|
||||
|
||||
test("remount replaces island output without leaking roots", async () => {
|
||||
const window = new Window();
|
||||
window.document.body.innerHTML =
|
||||
`<div data-wrn-island="Chart" data-wrn-island-strategy="only"
|
||||
window.document.body.innerHTML = `<div data-wrn-island="Chart" data-wrn-island-strategy="only"
|
||||
data-wrn-island-props='{}'></div>`;
|
||||
(globalThis as any).window = window;
|
||||
(globalThis as any).document = window.document;
|
||||
|
||||
@@ -28,14 +28,14 @@ byte-for-byte unchanged, and a route with no islands must ship no React.
|
||||
|
||||
## Decisions
|
||||
|
||||
| Question | Decision |
|
||||
|---|---|
|
||||
| Purpose | npm ecosystem access |
|
||||
| Server rendering | Client-only by default; SSR opt-in deferred to v2 |
|
||||
| Authoring | `import Chart from "./Chart.tsx"` in `.wrn` frontmatter, used as `<Chart client:only />` |
|
||||
| Data flow | Two-way store access via `useSyncExternalStore` (read + write through actions) |
|
||||
| Bundling | Extend the existing Bun pipeline |
|
||||
| Packaging | New isolated package `@wrnexus/react` |
|
||||
| Question | Decision |
|
||||
| ---------------- | ---------------------------------------------------------------------------------------- |
|
||||
| Purpose | npm ecosystem access |
|
||||
| Server rendering | Client-only by default; SSR opt-in deferred to v2 |
|
||||
| Authoring | `import Chart from "./Chart.tsx"` in `.wrn` frontmatter, used as `<Chart client:only />` |
|
||||
| Data flow | Two-way store access via `useSyncExternalStore` (read + write through actions) |
|
||||
| Bundling | Extend the existing Bun pipeline |
|
||||
| Packaging | New isolated package `@wrnexus/react` |
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -100,7 +100,7 @@ separate island hydration channel is introduced.
|
||||
`readonlySnapshot` in `packages/store/src/index.ts` returns `Object.freeze(clone(state))` — a
|
||||
**new reference on every call**. `useSyncExternalStore` requires `getSnapshot()` to return a
|
||||
referentially identical value when nothing has changed; otherwise React throws
|
||||
*"The result of getSnapshot should be cached to avoid an infinite loop"* and spins.
|
||||
_"The result of getSnapshot should be cached to avoid an infinite loop"_ and spins.
|
||||
|
||||
**The cache lives in the `@wrnexus/react` adapter, not in `@wrnexus/store`.** The adapter holds
|
||||
one cached snapshot per store instance, returns the same reference until the store's `subscribe`
|
||||
@@ -182,15 +182,15 @@ point.
|
||||
|
||||
## Error handling
|
||||
|
||||
| Condition | Behavior |
|
||||
|---|---|
|
||||
| `react`/`react-dom` not installed | Compiler diagnostic `WRN-ISLAND-REACT-MISSING`, naming the install command — not a raw module-resolution failure |
|
||||
| Island throws during render | Per-island error boundary. Dev: render error in place with component name and stack. Prod: log, render nothing, leave surrounding server HTML intact |
|
||||
| Island bundle fails to load | Placeholder remains, warning logged; page stays functional because everything else was server-rendered |
|
||||
| Non-serializable props | Compile-time `WRN-ISLAND-PROPS` |
|
||||
| Unknown store name | Dev: throw, listing available store names. Prod: warn, return undefined |
|
||||
| Action fired during render | Left to React. See "Dropped: the write-during-render guard" below. |
|
||||
| Cleanup throws on unmount | Caught and logged; navigation must not break |
|
||||
| Condition | Behavior |
|
||||
| --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `react`/`react-dom` not installed | Compiler diagnostic `WRN-ISLAND-REACT-MISSING`, naming the install command — not a raw module-resolution failure |
|
||||
| Island throws during render | Per-island error boundary. Dev: render error in place with component name and stack. Prod: log, render nothing, leave surrounding server HTML intact |
|
||||
| Island bundle fails to load | Placeholder remains, warning logged; page stays functional because everything else was server-rendered |
|
||||
| Non-serializable props | Compile-time `WRN-ISLAND-PROPS` |
|
||||
| Unknown store name | Dev: throw, listing available store names. Prod: warn, return undefined |
|
||||
| Action fired during render | Left to React. See "Dropped: the write-during-render guard" below. |
|
||||
| Cleanup throws on unmount | Caught and logged; navigation must not break |
|
||||
|
||||
Islands failing **locally** is the most valuable property of this model: a crashed chart leaves
|
||||
the rest of the page working.
|
||||
|
||||
Reference in New Issue
Block a user