Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e819c5739e | ||
|
|
ac164789bd | ||
|
|
88ec3a5cb6 | ||
|
|
28085b5a43 | ||
|
|
5d7bd81601 | ||
|
|
5e114d867f | ||
|
|
10421465df | ||
|
|
52cce2c628 | ||
|
|
afa2a8c093 |
@@ -1,3 +1,3 @@
|
||||
@wrnexus:registry=https://registry.npmjs.org/
|
||||
@wrnexus:registry=https://registry.workroot.in/repository/npm/
|
||||
audit=true
|
||||
fund=false
|
||||
|
||||
@@ -272,7 +272,7 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@wrnexus/cli",
|
||||
"version": "0.8.36",
|
||||
"version": "0.8.41",
|
||||
"bin": {
|
||||
"wrnexus": "src/index.ts",
|
||||
},
|
||||
@@ -287,6 +287,7 @@
|
||||
"@wrnexus/mcp": "workspace:*",
|
||||
"@wrnexus/playground": "workspace:*",
|
||||
"@wrnexus/plugin": "workspace:*",
|
||||
"@wrnexus/react": "workspace:*",
|
||||
"@wrnexus/router": "workspace:*",
|
||||
"@wrnexus/security": "workspace:*",
|
||||
"@wrnexus/styles": "workspace:*",
|
||||
@@ -299,9 +300,8 @@
|
||||
},
|
||||
"packages/compiler": {
|
||||
"name": "@wrnexus/compiler",
|
||||
"version": "0.8.10",
|
||||
"version": "0.8.11",
|
||||
"dependencies": {
|
||||
"@wrnexus/core": "workspace:*",
|
||||
"@wrnexus/csr": "workspace:*",
|
||||
"@wrnexus/store": "workspace:*",
|
||||
"@wrnexus/syntax": "workspace:*",
|
||||
@@ -337,7 +337,7 @@
|
||||
},
|
||||
"packages/dev-server": {
|
||||
"name": "@wrnexus/dev-server",
|
||||
"version": "0.8.33",
|
||||
"version": "0.8.37",
|
||||
"dependencies": {
|
||||
"@wrnexus/authz": "workspace:*",
|
||||
"@wrnexus/cache": "workspace:*",
|
||||
@@ -364,7 +364,7 @@
|
||||
},
|
||||
"packages/dev-toolbar": {
|
||||
"name": "@wrnexus/dev-toolbar",
|
||||
"version": "0.8.10",
|
||||
"version": "0.8.12",
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.3.14",
|
||||
"typescript": "^6.0.3",
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -19,10 +19,10 @@
|
||||
"@wrnexus/validation": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify-json/lucide": "^1.2.0",
|
||||
"@iconify/tailwind4": "^1.0.0",
|
||||
"@tailwindcss/cli": "^4.0.0",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"typescript": "^5.9.2"
|
||||
"@iconify-json/lucide": "^1.2.123",
|
||||
"@iconify/tailwind4": "^1.2.3",
|
||||
"@tailwindcss/cli": "^4.3.3",
|
||||
"tailwindcss": "^4.3.3",
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,12 +23,12 @@
|
||||
"devDependencies": {
|
||||
"@wrnexus/test": "workspace:*",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@iconify-json/lucide": "^1.2.118",
|
||||
"@iconify-json/lucide": "^1.2.123",
|
||||
"@iconify/tailwind4": "^1.2.3",
|
||||
"@tailwindcss/cli": "^4.0.0",
|
||||
"eslint": "^10.8.0",
|
||||
"prettier": "^3.9.4",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"typescript-eslint": "^8.65.0"
|
||||
"@tailwindcss/cli": "^4.3.3",
|
||||
"eslint": "^10.8.1",
|
||||
"prettier": "^3.9.6",
|
||||
"tailwindcss": "^4.3.3",
|
||||
"typescript-eslint": "^8.67.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,10 +16,10 @@
|
||||
"@wrnexus/validation": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify-json/lucide": "^1.2.0",
|
||||
"@iconify/tailwind4": "^1.0.0",
|
||||
"@tailwindcss/cli": "^4.0.0",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"typescript": "^5.9.2"
|
||||
"@iconify-json/lucide": "^1.2.123",
|
||||
"@iconify/tailwind4": "^1.2.3",
|
||||
"@tailwindcss/cli": "^4.3.3",
|
||||
"tailwindcss": "^4.3.3",
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,10 +18,10 @@
|
||||
"@wrnexus/ui": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify-json/lucide": "^1.2.118",
|
||||
"@iconify-json/lucide": "^1.2.123",
|
||||
"@iconify/tailwind4": "^1.2.3",
|
||||
"@tailwindcss/cli": "^4.0.0",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"typescript": "^5.5.0"
|
||||
"@tailwindcss/cli": "^4.3.3",
|
||||
"tailwindcss": "^4.3.3",
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,10 +16,10 @@
|
||||
"@wrnexus/ui": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iconify-json/lucide": "^1.2.118",
|
||||
"@iconify-json/lucide": "^1.2.123",
|
||||
"@iconify/tailwind4": "^1.2.3",
|
||||
"@tailwindcss/cli": "^4.0.0",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"typescript": "^5.9.2"
|
||||
"@tailwindcss/cli": "^4.3.3",
|
||||
"tailwindcss": "^4.3.3",
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,40 +24,40 @@
|
||||
"check": "bun run typecheck && bun run lint && bun run test && bun run format:check"
|
||||
},
|
||||
"dependencies": {
|
||||
"@wrnexus/ai": "0.8.6",
|
||||
"@wrnexus/auth": "0.8.6",
|
||||
"@wrnexus/captcha": "0.8.6",
|
||||
"@wrnexus/core": "0.8.6",
|
||||
"@wrnexus/csr": "0.8.6",
|
||||
"@wrnexus/db": "0.8.6",
|
||||
"@wrnexus/dev-server": "0.8.6",
|
||||
"@wrnexus/encryption": "0.8.6",
|
||||
"@wrnexus/helpers": "0.8.6",
|
||||
"@wrnexus/i18n": "0.8.6",
|
||||
"@wrnexus/image": "0.8.6",
|
||||
"@wrnexus/jwt": "0.8.6",
|
||||
"@wrnexus/observability": "0.8.6",
|
||||
"@wrnexus/realtime": "0.8.6",
|
||||
"@wrnexus/security": "0.8.6",
|
||||
"@wrnexus/store": "0.8.6",
|
||||
"@wrnexus/styles": "0.8.6",
|
||||
"@wrnexus/tracking": "0.8.6",
|
||||
"@wrnexus/ui": "0.8.6",
|
||||
"@wrnexus/uploader": "0.8.6",
|
||||
"@wrnexus/validation": "0.8.6",
|
||||
"@wrnexus/authz": "0.8.6"
|
||||
"@wrnexus/ai": "^0.8.9",
|
||||
"@wrnexus/auth": "^0.8.12",
|
||||
"@wrnexus/captcha": "^0.8.11",
|
||||
"@wrnexus/core": "^0.8.9",
|
||||
"@wrnexus/csr": "^0.8.21",
|
||||
"@wrnexus/db": "^0.8.15",
|
||||
"@wrnexus/dev-server": "^0.8.33",
|
||||
"@wrnexus/encryption": "^0.8.9",
|
||||
"@wrnexus/helpers": "^0.8.8",
|
||||
"@wrnexus/i18n": "^0.8.11",
|
||||
"@wrnexus/image": "^0.8.10",
|
||||
"@wrnexus/jwt": "^0.8.9",
|
||||
"@wrnexus/observability": "^0.8.8",
|
||||
"@wrnexus/realtime": "^0.8.10",
|
||||
"@wrnexus/security": "^0.8.8",
|
||||
"@wrnexus/store": "^0.8.8",
|
||||
"@wrnexus/styles": "^0.8.15",
|
||||
"@wrnexus/tracking": "^0.8.8",
|
||||
"@wrnexus/ui": "^0.8.19",
|
||||
"@wrnexus/uploader": "^0.8.10",
|
||||
"@wrnexus/validation": "^0.8.10",
|
||||
"@wrnexus/authz": "^0.8.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@wrnexus/cli": "0.8.6",
|
||||
"@eslint/js": "^9.0.0",
|
||||
"@iconify-json/lucide": "^1.2.118",
|
||||
"@wrnexus/cli": "^0.8.36",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@iconify-json/lucide": "^1.2.123",
|
||||
"@iconify/tailwind4": "^1.2.3",
|
||||
"@tailwindcss/cli": "^4.0.0",
|
||||
"@tailwindcss/cli": "^4.3.3",
|
||||
"@types/bun": "^1.3.14",
|
||||
"eslint": "^9.0.0",
|
||||
"eslint": "^10.8.1",
|
||||
"prettier": "^3.9.6",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"typescript": "^5.5.0",
|
||||
"typescript-eslint": "^8.65.0"
|
||||
"tailwindcss": "^4.3.3",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.67.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,40 +24,40 @@
|
||||
"check": "bun run typecheck && bun run lint && bun run test && bun run format:check"
|
||||
},
|
||||
"dependencies": {
|
||||
"@wrnexus/ai": "0.8.6",
|
||||
"@wrnexus/auth": "0.8.6",
|
||||
"@wrnexus/captcha": "0.8.6",
|
||||
"@wrnexus/core": "0.8.6",
|
||||
"@wrnexus/csr": "0.8.6",
|
||||
"@wrnexus/db": "0.8.6",
|
||||
"@wrnexus/dev-server": "0.8.6",
|
||||
"@wrnexus/encryption": "0.8.6",
|
||||
"@wrnexus/helpers": "0.8.6",
|
||||
"@wrnexus/i18n": "0.8.6",
|
||||
"@wrnexus/image": "0.8.6",
|
||||
"@wrnexus/jwt": "0.8.6",
|
||||
"@wrnexus/observability": "0.8.6",
|
||||
"@wrnexus/realtime": "0.8.6",
|
||||
"@wrnexus/security": "0.8.6",
|
||||
"@wrnexus/store": "0.8.6",
|
||||
"@wrnexus/styles": "0.8.6",
|
||||
"@wrnexus/tracking": "0.8.6",
|
||||
"@wrnexus/ui": "0.8.6",
|
||||
"@wrnexus/uploader": "0.8.6",
|
||||
"@wrnexus/validation": "0.8.6",
|
||||
"@wrnexus/authz": "0.8.6"
|
||||
"@wrnexus/ai": "^0.8.9",
|
||||
"@wrnexus/auth": "^0.8.12",
|
||||
"@wrnexus/captcha": "^0.8.11",
|
||||
"@wrnexus/core": "^0.8.9",
|
||||
"@wrnexus/csr": "^0.8.21",
|
||||
"@wrnexus/db": "^0.8.15",
|
||||
"@wrnexus/dev-server": "^0.8.33",
|
||||
"@wrnexus/encryption": "^0.8.9",
|
||||
"@wrnexus/helpers": "^0.8.8",
|
||||
"@wrnexus/i18n": "^0.8.11",
|
||||
"@wrnexus/image": "^0.8.10",
|
||||
"@wrnexus/jwt": "^0.8.9",
|
||||
"@wrnexus/observability": "^0.8.8",
|
||||
"@wrnexus/realtime": "^0.8.10",
|
||||
"@wrnexus/security": "^0.8.8",
|
||||
"@wrnexus/store": "^0.8.8",
|
||||
"@wrnexus/styles": "^0.8.15",
|
||||
"@wrnexus/tracking": "^0.8.8",
|
||||
"@wrnexus/ui": "^0.8.19",
|
||||
"@wrnexus/uploader": "^0.8.10",
|
||||
"@wrnexus/validation": "^0.8.10",
|
||||
"@wrnexus/authz": "^0.8.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@wrnexus/cli": "0.8.6",
|
||||
"@eslint/js": "^9.0.0",
|
||||
"@iconify-json/lucide": "^1.2.118",
|
||||
"@wrnexus/cli": "^0.8.36",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@iconify-json/lucide": "^1.2.123",
|
||||
"@iconify/tailwind4": "^1.2.3",
|
||||
"@tailwindcss/cli": "^4.0.0",
|
||||
"@tailwindcss/cli": "^4.3.3",
|
||||
"@types/bun": "^1.3.14",
|
||||
"eslint": "^9.0.0",
|
||||
"eslint": "^10.8.1",
|
||||
"prettier": "^3.9.6",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"typescript": "^5.5.0",
|
||||
"typescript-eslint": "^8.65.0"
|
||||
"tailwindcss": "^4.3.3",
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.67.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,12 +21,12 @@
|
||||
"check": "bun run typecheck && bun run lint && bun run test && bun run format:check"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@wrnexus/cli": "0.8.6",
|
||||
"@eslint/js": "^9.0.0",
|
||||
"@wrnexus/cli": "^0.8.36",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@types/bun": "^1.3.14",
|
||||
"eslint": "^9.0.0",
|
||||
"eslint": "^10.8.1",
|
||||
"prettier": "^3.9.6",
|
||||
"typescript": "^5.5.0",
|
||||
"typescript-eslint": "^8.65.0"
|
||||
"typescript": "^6.0.3",
|
||||
"typescript-eslint": "^8.67.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,6 @@
|
||||
"test": "bun test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@wrnexus/pubsub": "0.8.6"
|
||||
"@wrnexus/pubsub": "^0.8.9"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/auth",
|
||||
"version": "0.8.11",
|
||||
"version": "0.8.12",
|
||||
"description": "Complete authentication, account security, MFA, passkeys, recovery, devices, risk, and audit system for WRNexusJS.",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
@@ -54,7 +54,7 @@
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.3.14",
|
||||
"@wrnexus/syntax": "workspace:*",
|
||||
"typescript": "^5.9.2"
|
||||
"typescript": "^6.0.3"
|
||||
},
|
||||
"wrnexus": {
|
||||
"plugin": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/captcha",
|
||||
"version": "0.8.10",
|
||||
"version": "0.8.11",
|
||||
"description": "First-class CAPTCHA challenges, providers, verification guards, page gates, and WRNexusJS UI.",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
@@ -48,7 +48,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.3.14",
|
||||
"typescript": "^5.9.2",
|
||||
"typescript": "^6.0.3",
|
||||
"@wrnexus/syntax": "workspace:*"
|
||||
},
|
||||
"wrnexus": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/cli",
|
||||
"version": "0.8.36",
|
||||
"version": "0.8.41",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
@@ -28,6 +28,7 @@
|
||||
"@wrnexus/db": "workspace:*",
|
||||
"@wrnexus/authz": "workspace:*",
|
||||
"@wrnexus/plugin": "workspace:*",
|
||||
"@wrnexus/react": "workspace:*",
|
||||
"@wrnexus/syntax": "workspace:*",
|
||||
"@wrnexus/typecheck": "workspace:*",
|
||||
"@wrnexus/security": "workspace:*",
|
||||
|
||||
@@ -620,10 +620,14 @@ export async function runBuild(appRoot: string): Promise<void> {
|
||||
appDir,
|
||||
appRoot: root,
|
||||
mode: "production",
|
||||
sources: [
|
||||
...componentDirs,
|
||||
...pluginContributions.styles.flatMap((style) => (style.source ? [style.source] : [])),
|
||||
],
|
||||
// Component discovery is not style discovery. Built-in and plugin
|
||||
// components own their CSS; scanning every available component makes
|
||||
// Tailwind/Iconify generate rules for packages and components the app
|
||||
// never renders. Packages that intentionally use app utilities opt in
|
||||
// through an explicit styles.source contribution.
|
||||
sources: pluginContributions.styles.flatMap((style) =>
|
||||
style.source ? [style.source] : [],
|
||||
),
|
||||
entries: pluginContributions.styles.flatMap((style) => (style.entry ? [style.entry] : [])),
|
||||
},
|
||||
config.styles,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/compiler",
|
||||
"version": "0.8.10",
|
||||
"version": "0.8.11",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/content",
|
||||
"version": "0.8.8",
|
||||
"version": "0.8.9",
|
||||
"type": "module",
|
||||
"description": "Typed Markdown content collections, loaders, indexes, feeds, and preview workflows for WRNexusJS.",
|
||||
"main": "./src/index.ts",
|
||||
@@ -14,6 +14,6 @@
|
||||
],
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.3.14",
|
||||
"typescript": "^5.9.2"
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/db",
|
||||
"version": "0.8.14",
|
||||
"version": "0.8.15",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
@@ -26,6 +26,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.3.14",
|
||||
"typescript": "^5.9.2"
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,9 +226,7 @@ export function generateQueriesFile(
|
||||
);
|
||||
}
|
||||
|
||||
const imports = [
|
||||
`import type { Db${usesExecResult ? ", ExecResult" : ""} } from "@wrnexus/db";`,
|
||||
];
|
||||
const imports = [`import type { Db${usesExecResult ? ", ExecResult" : ""} } from "@wrnexus/db";`];
|
||||
if (usedModels.size > 0) {
|
||||
imports.push(`import { ${[...usedModels].sort().join(", ")} } from "./schema.ts";`);
|
||||
}
|
||||
|
||||
@@ -91,9 +91,10 @@ function additiveColumnTarget(sql: string): { table: string; column: string } |
|
||||
.replace(/\/\*[\s\S]*?\*\//g, " ")
|
||||
.replace(/--[^\r\n]*/g, " ")
|
||||
.trim();
|
||||
const match = /^ALTER\s+TABLE\s+([A-Za-z_][A-Za-z0-9_]*)\s+ADD\s+COLUMN\s+([A-Za-z_][A-Za-z0-9_]*)\b[\s\S]*;?\s*$/i.exec(
|
||||
executable,
|
||||
);
|
||||
const match =
|
||||
/^ALTER\s+TABLE\s+([A-Za-z_][A-Za-z0-9_]*)\s+ADD\s+COLUMN\s+([A-Za-z_][A-Za-z0-9_]*)\b[\s\S]*;?\s*$/i.exec(
|
||||
executable,
|
||||
);
|
||||
return match ? { table: match[1]!, column: match[2]! } : undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/dev-server",
|
||||
"version": "0.8.33",
|
||||
"version": "0.8.37",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -34,6 +34,7 @@ import type { AssetServer } from "./runtime.ts";
|
||||
import { servePublicAsset } from "./public.ts";
|
||||
import { servePluginAsset, type ServedPluginAsset } from "./plugin-assets.ts";
|
||||
import { serveIslandArtifact, serveWrnBrowserArtifact } from "./pipeline.ts";
|
||||
import { HMR_CLIENT_HREF, HMR_CLIENT_JS } from "./runtime.ts";
|
||||
|
||||
/** Style inputs the dev asset server needs to build `/__wrnexus/styles.css`. */
|
||||
export interface DevStyles {
|
||||
@@ -99,6 +100,7 @@ export function createDevAssetServer(
|
||||
if (pathname.startsWith("/__wrnexus/island/")) {
|
||||
return serveIslandArtifact(pathname) ?? new Response("Not Found", { status: 404 });
|
||||
}
|
||||
if (pathname === HMR_CLIENT_HREF) return jsResponse(HMR_CLIENT_JS);
|
||||
if (pathname === "/__wrnexus/islands.js") return jsResponse(getIslandRuntime(true));
|
||||
if (pathname === "/__wrnexus/reactive.js") return jsResponse(getReactiveRuntime(true));
|
||||
if (pathname === "/__wrnexus/controllers.js")
|
||||
|
||||
@@ -113,6 +113,7 @@ interface Target extends GatewayApp {
|
||||
interface WsBridge {
|
||||
origin: string;
|
||||
path: string;
|
||||
headers: Record<string, string>;
|
||||
backend?: WebSocket;
|
||||
queue: Array<string | ArrayBuffer>;
|
||||
maxMessageBytes: number;
|
||||
@@ -433,6 +434,30 @@ export function gatewayProxyHeaders(
|
||||
return headers;
|
||||
}
|
||||
|
||||
/** Forward application identity to the child while leaving WebSocket framing to Bun. */
|
||||
export function gatewayWebSocketBackendHeaders(
|
||||
req: Request,
|
||||
url: URL,
|
||||
ip: string,
|
||||
forwardedHeaders: boolean,
|
||||
backendOrigin: string,
|
||||
): Record<string, string> {
|
||||
const headers = stripUntrustedInternalHeaders(
|
||||
gatewayProxyHeaders(req, url, ip, forwardedHeaders),
|
||||
);
|
||||
headers.delete("host");
|
||||
headers.delete("connection");
|
||||
headers.delete("upgrade");
|
||||
headers.delete("accept-encoding");
|
||||
for (const name of [...headers.keys()]) {
|
||||
if (name.startsWith("sec-websocket-")) headers.delete(name);
|
||||
}
|
||||
// The public origin was validated at the gateway edge. The child receives a
|
||||
// new, trusted same-origin connection from its private gateway listener.
|
||||
headers.set("origin", backendOrigin);
|
||||
return Object.fromEntries(headers);
|
||||
}
|
||||
|
||||
/** Remove headers that only a direct workspace-to-app request may supply. */
|
||||
export function stripUntrustedInternalHeaders(headers: Headers): Headers {
|
||||
const sanitized = new Headers(headers);
|
||||
@@ -692,6 +717,13 @@ export async function startGateway(opts: GatewayOptions): Promise<RunningGateway
|
||||
data: {
|
||||
origin: target.origin,
|
||||
path: url.pathname + url.search,
|
||||
headers: gatewayWebSocketBackendHeaders(
|
||||
req,
|
||||
url,
|
||||
ip,
|
||||
forwardedHeaders,
|
||||
target.origin,
|
||||
),
|
||||
queue: [],
|
||||
maxMessageBytes: websocketSecurity.maxMessageBytes ?? 64 * 1024,
|
||||
maxQueuedMessages: websocketSecurity.maxQueuedMessages ?? 100,
|
||||
@@ -753,7 +785,11 @@ export async function startGateway(opts: GatewayOptions): Promise<RunningGateway
|
||||
websocket: {
|
||||
open(ws) {
|
||||
const backendUrl = ws.data.origin.replace(/^http/, "ws") + ws.data.path;
|
||||
const backend = new WebSocket(backendUrl);
|
||||
const BackendWebSocket = WebSocket as unknown as new (
|
||||
url: string,
|
||||
options: Bun.WebSocketOptions,
|
||||
) => WebSocket;
|
||||
const backend = new BackendWebSocket(backendUrl, { headers: ws.data.headers });
|
||||
ws.data.backend = backend;
|
||||
backend.addEventListener("open", () => {
|
||||
for (const m of ws.data.queue) backend.send(m);
|
||||
|
||||
@@ -440,10 +440,9 @@ export async function startServer(opts: ServeOptions): Promise<RunningServer> {
|
||||
config: opts.stylesConfig,
|
||||
appRoot,
|
||||
publicDir: join(appRoot, "public"),
|
||||
sources: [
|
||||
...componentDirs,
|
||||
...pluginContributions.styles.flatMap((style) => (style.source ? [style.source] : [])),
|
||||
],
|
||||
// Component discovery is separate from utility-source discovery.
|
||||
// Packages that need Tailwind scanning opt in via styles.source.
|
||||
sources: pluginContributions.styles.flatMap((style) => (style.source ? [style.source] : [])),
|
||||
entries: pluginContributions.styles.flatMap((style) => (style.entry ? [style.entry] : [])),
|
||||
},
|
||||
theme,
|
||||
|
||||
@@ -878,8 +878,19 @@ function randomNonce(): string {
|
||||
}
|
||||
|
||||
/** The dev HMR client as a nonce-tagged inline script (strict-CSP friendly). */
|
||||
function hmrClientTag(nonce: string): string {
|
||||
return `<script nonce="${nonce}">${HMR_CLIENT_JS}</script>`;
|
||||
/** Path the dev asset server publishes the HMR client on. */
|
||||
export const HMR_CLIENT_HREF = "/__wrnexus/hmr-client.js";
|
||||
|
||||
/**
|
||||
* The HMR client is served as an external module rather than inlined.
|
||||
*
|
||||
* A document's CSP nonce is fixed at load, so an inline script arriving from a
|
||||
* later response — which is exactly what an HMR reload produces — can never
|
||||
* carry a nonce this document accepts. An external file is covered by
|
||||
* script-src 'self' and needs no nonce at all.
|
||||
*/
|
||||
function hmrClientTag(_nonce: string): string {
|
||||
return `<script src="${HMR_CLIENT_HREF}"></script>`;
|
||||
}
|
||||
|
||||
/** 403 for a rejected cross-site WebSocket handshake. */
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
forwardAuthFailure,
|
||||
forwardAuthHeaders,
|
||||
gatewayProxyHeaders,
|
||||
gatewayWebSocketBackendHeaders,
|
||||
stripUntrustedInternalHeaders,
|
||||
gatewayRestartDelay,
|
||||
internalError,
|
||||
@@ -37,6 +38,35 @@ test("gateway disables compression for its internal proxy hop", () => {
|
||||
expect(headers.get("x-forwarded-for")).toBe("127.0.0.1");
|
||||
});
|
||||
|
||||
test("gateway WebSocket bridge forwards validated application identity", () => {
|
||||
const request = new Request("http://web.localhost:3000/__wrnexus/hmr", {
|
||||
headers: {
|
||||
host: "web.localhost:3000",
|
||||
origin: "http://web.localhost:3000",
|
||||
cookie: "session=abc",
|
||||
connection: "Upgrade",
|
||||
upgrade: "websocket",
|
||||
"sec-websocket-key": "test-key",
|
||||
},
|
||||
});
|
||||
const headers = gatewayWebSocketBackendHeaders(
|
||||
request,
|
||||
new URL(request.url),
|
||||
"127.0.0.1",
|
||||
true,
|
||||
"http://127.0.0.1:3001",
|
||||
);
|
||||
|
||||
expect(headers.origin).toBe("http://127.0.0.1:3001");
|
||||
expect(headers.cookie).toBe("session=abc");
|
||||
expect(headers["x-forwarded-host"]).toBe("web.localhost:3000");
|
||||
expect(headers["x-forwarded-proto"]).toBe("http");
|
||||
expect(headers.host).toBeUndefined();
|
||||
expect(headers.connection).toBeUndefined();
|
||||
expect(headers.upgrade).toBeUndefined();
|
||||
expect(headers["sec-websocket-key"]).toBeUndefined();
|
||||
});
|
||||
|
||||
test("gateway proxy headers do not preserve the RPC internal marker", () => {
|
||||
const request = new Request("http://localhost:3000/path", {
|
||||
headers: { "x-wrnexus-internal": "1" },
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/dev-toolbar",
|
||||
"version": "0.8.9",
|
||||
"version": "0.8.12",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
@@ -20,6 +20,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.3.14",
|
||||
"typescript": "^5.9.2"
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,8 +43,8 @@ export const DEV_TOOLBAR_RUNTIME = String.raw`(() => {
|
||||
try{for(let i=0;i<localStorage.length;i++){const key=localStorage.key(i)||"";if(/(?:token|secret|password|session|credential|authorization)/i.test(key))found.push(issue("security/sensitive-local-storage","security","error","Sensitive value may be stored in localStorage","Storage key “"+key+"” looks authentication- or secret-related.",null,"Keep sessions and credentials in Secure, HttpOnly cookies."));}}catch{}
|
||||
if(document.documentElement.scrollWidth>innerWidth+2)found.push(issue("responsive/document-overflow","responsive","error","Page has horizontal overflow","Document width exceeds the viewport.",null,"Inspect fixed widths, long text and overflowing media."));
|
||||
q("body *").filter(visible).slice(0,2500).forEach(el=>{const r=el.getBoundingClientRect();if((r.right>innerWidth+8||r.left<-8)&&found.filter(x=>x.ruleId==="responsive/element-overflow").length<20)found.push(issue("responsive/element-overflow","responsive","warning","Element extends outside the viewport","Element bounds exceed the current viewport.",el,"Use fluid sizing, wrapping, max-width or an intentional scroll container."));});
|
||||
const resources=performance.getEntriesByType("resource");const total=resources.reduce((s,e)=>s+(e.transferSize||0),0);const jsBytes=resources.filter(e=>/(?:\.m?js)(?:\?|$)/i.test(e.name)).reduce((sum,e)=>sum+(e.transferSize||0),0);if(resources.length>150)found.push(issue("performance/resource-count","performance","warning","Page loads many resources","Found "+resources.length+" resource requests.",null,"Remove duplicates and defer non-critical resources."));if(total>5000000)found.push(issue("performance/transfer-size","performance",total>10000000?"error":"warning","Page transfer size is large","Observed transfer size is about "+(total/1000000).toFixed(2)+" MB.",null,"Compress and optimize page resources."));if(jsBytes>150000)found.push(issue("performance/javascript-budget","javascript",jsBytes>300000?"error":"warning","JavaScript budget exceeded","JavaScript transfer is about "+(jsBytes/1000).toFixed(1)+" KB.",null,"Split routes and defer optional hydration."));const hydrationRoots=q("[data-wrn-client-module],[data-wrn-hydrate]");if(hydrationRoots.length>50)found.push(issue("performance/hydration-count","runtime","warning","Many components hydrate",hydrationRoots.length+" hydration boundaries were found.",null,"Use visible, idle or interaction hydration."));if(state.runtimeMetrics.longTasks.length)found.push(issue("performance/long-tasks","javascript","warning","Long main-thread tasks detected",state.runtimeMetrics.longTasks.length+" task(s) exceeded 50 ms.",null,"Split expensive work and reduce hydration.","high",{longestMs:Math.max(...state.runtimeMetrics.longTasks)}));
|
||||
let checkedSelectors=0,unusedSelectors=0;for(const sheet of [...document.styleSheets]){let rules;try{rules=[...(sheet.cssRules||[])]}catch{continue}for(const rule of rules){if(checkedSelectors>=2000)break;const selector=rule.selectorText;if(!selector||selector.includes(":"))continue;checkedSelectors++;try{if(!document.querySelector(selector))unusedSelectors++}catch{}}}if(unusedSelectors)found.push(issue("css/unused-selectors","css","suggestion","Potentially unused CSS",unusedSelectors+" of "+checkedSelectors+" inspected selectors do not match this page.",null,"Review across routes before removing selectors.","medium",{checkedSelectors,unusedSelectors}));const memory=performance.memory;if(memory&&memory.jsHeapSizeLimit&&memory.usedJSHeapSize/memory.jsHeapSizeLimit>.8)found.push(issue("performance/memory-pressure","performance","warning","High JavaScript heap usage",Math.round(memory.usedJSHeapSize/1048576)+" MiB of "+Math.round(memory.jsHeapSizeLimit/1048576)+" MiB is in use.",null,"Inspect retained objects and repeated hydration."));
|
||||
const resources=performance.getEntriesByType("resource");const total=resources.reduce((s,e)=>s+(e.transferSize||0),0);const jsResources=resources.filter(e=>/(?:\.m?js)(?:\?|$)/i.test(e.name)&&!/\/__wrnexus\/dev-toolbar\.js(?:\?|$)/.test(e.name));const jsBytes=jsResources.reduce((sum,e)=>sum+(e.transferSize||0),0);if(resources.length>150)found.push(issue("performance/resource-count","performance","warning","Page loads many resources","Found "+resources.length+" resource requests.",null,"Remove duplicates and defer non-critical resources."));if(total>5000000)found.push(issue("performance/transfer-size","performance",total>10000000?"error":"warning","Page transfer size is large","Observed transfer size is about "+(total/1000000).toFixed(2)+" MB.",null,"Compress and optimize page resources."));if(jsBytes>300000)found.push(issue("performance/javascript-budget","javascript",jsBytes>600000?"error":"warning","Development JavaScript is large","Application JavaScript transfer is about "+(jsBytes/1000).toFixed(1)+" KB in development.",null,"Check the production build report before splitting routes; development modules are unminified.","medium",{javascriptBytes:jsBytes,mode:"development"}));const hydrationRoots=q("[data-wrn-client-module],[data-wrn-hydrate]");if(hydrationRoots.length>50)found.push(issue("performance/hydration-count","runtime","warning","Many components hydrate",hydrationRoots.length+" hydration boundaries were found.",null,"Use visible, idle or interaction hydration."));if(state.runtimeMetrics.longTasks.length)found.push(issue("performance/long-tasks","javascript","warning","Long main-thread tasks detected",state.runtimeMetrics.longTasks.length+" task(s) exceeded 50 ms.",null,"Split expensive work and reduce hydration.","high",{longestMs:Math.max(...state.runtimeMetrics.longTasks)}));
|
||||
let checkedSelectors=0,unusedSelectors=0;for(const sheet of [...document.styleSheets]){const href=sheet.href||"";if(/\/__wrnexus\/(?:ui|framework)\.css(?:\?|$)|\/__wrnexus\/theme\/[^/?]+\.css(?:\?|$)/.test(href))continue;let rules;try{rules=[...(sheet.cssRules||[])]}catch{continue}for(const rule of rules){if(checkedSelectors>=2000)break;const selector=rule.selectorText;if(!selector||selector.includes(":"))continue;checkedSelectors++;try{if(!document.querySelector(selector))unusedSelectors++}catch{}}}const unusedRatio=checkedSelectors?unusedSelectors/checkedSelectors:0;if(checkedSelectors>=20&&unusedRatio>=.8)found.push(issue("css/unused-selectors","css","suggestion","Low current-page CSS coverage",unusedSelectors+" of "+checkedSelectors+" inspected application selectors do not match this page.",null,"Review across routes before removing selectors. WRNexus UI and theme styles are excluded.","medium",{checkedSelectors,unusedSelectors,unusedRatio}));const memory=performance.memory;if(memory&&memory.jsHeapSizeLimit&&memory.usedJSHeapSize/memory.jsHeapSizeLimit>.8)found.push(issue("performance/memory-pressure","performance","warning","High JavaScript heap usage",Math.round(memory.usedJSHeapSize/1048576)+" MiB of "+Math.round(memory.jsHeapSizeLimit/1048576)+" MiB is in use.",null,"Inspect retained objects and repeated hydration."));
|
||||
q("[data-wrn-client-module]").forEach(el=>found.push(issue("runtime/client-module","runtime","info","Client function module",el.getAttribute("data-wrn-client-module")||"Unknown module",el,"Loaded according to the component hydration strategy.","high",{hydration:el.getAttribute("data-wrn-hydrate"),runtime:el.getAttribute("data-wrn-runtime")})));
|
||||
const storeContainer=window.__wrnexusStoreContainer;
|
||||
if(storeContainer&&typeof storeContainer.inspect==="function"){
|
||||
|
||||
@@ -9,6 +9,12 @@ test("exports usable development assets", () => {
|
||||
expect(DEV_TOOLBAR_RUNTIME).toContain('data-category="accessibility"');
|
||||
expect(DEV_TOOLBAR_RUNTIME).toContain('issue("plugin/"+app.id');
|
||||
expect(DEV_TOOLBAR_RUNTIME).toContain("apps.appendChild(button)");
|
||||
expect(DEV_TOOLBAR_RUNTIME).toContain("Low current-page CSS coverage");
|
||||
expect(DEV_TOOLBAR_RUNTIME).toContain("WRNexus UI and theme styles are excluded");
|
||||
expect(DEV_TOOLBAR_RUNTIME).toContain("checkedSelectors>=20&&unusedRatio>=.8");
|
||||
expect(DEV_TOOLBAR_RUNTIME).toContain("Development JavaScript is large");
|
||||
expect(DEV_TOOLBAR_RUNTIME).toContain("dev-toolbar\\.js");
|
||||
expect(DEV_TOOLBAR_RUNTIME).toContain("jsBytes>300000");
|
||||
expect(DEV_TOOLBAR_CSS).toContain(".wrn-panel");
|
||||
expect(DEV_TOOLBAR_CSS).toContain(".wrn-plugin-panel");
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/encryption",
|
||||
"version": "0.8.8",
|
||||
"version": "0.8.9",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
@@ -20,7 +20,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.3.14",
|
||||
"typescript": "^5.9.2"
|
||||
"typescript": "^6.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@wrnexus/core": "workspace:*"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/i18n",
|
||||
"version": "0.8.10",
|
||||
"version": "0.8.11",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
@@ -23,7 +23,7 @@
|
||||
],
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.3.14",
|
||||
"typescript": "^5.9.2",
|
||||
"typescript": "^6.0.3",
|
||||
"@wrnexus/syntax": "workspace:*"
|
||||
},
|
||||
"wrnexus": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/image",
|
||||
"version": "0.8.9",
|
||||
"version": "0.8.10",
|
||||
"type": "module",
|
||||
"description": "Responsive image planning, secure remote image policies, and performance auditing for WRNexusJS.",
|
||||
"main": "./src/index.ts",
|
||||
@@ -22,7 +22,7 @@
|
||||
],
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.3.14",
|
||||
"typescript": "^5.9.2",
|
||||
"typescript": "^6.0.3",
|
||||
"@wrnexus/syntax": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/jwt",
|
||||
"version": "0.8.8",
|
||||
"version": "0.8.9",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
@@ -20,7 +20,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.3.14",
|
||||
"typescript": "^5.9.2"
|
||||
"typescript": "^6.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@wrnexus/core": "workspace:*"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/pwa",
|
||||
"version": "0.8.8",
|
||||
"version": "0.8.9",
|
||||
"type": "module",
|
||||
"description": "Service workers, manifests, offline queues, background sync, push, and conflict resolution for WRNexusJS.",
|
||||
"main": "./src/index.ts",
|
||||
@@ -14,6 +14,6 @@
|
||||
],
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.3.14",
|
||||
"typescript": "^5.9.2"
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/realtime",
|
||||
"version": "0.8.9",
|
||||
"version": "0.8.10",
|
||||
"description": "Typed realtime rooms, message helpers, presence utilities, package UI blocks, and WRNexusJS integration.",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
@@ -29,7 +29,7 @@
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.3.14",
|
||||
"@wrnexus/syntax": "workspace:*",
|
||||
"typescript": "^5.9.2"
|
||||
"typescript": "^6.0.3"
|
||||
},
|
||||
"wrnexus": {
|
||||
"plugin": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/rpc",
|
||||
"version": "0.8.9",
|
||||
"version": "0.8.10",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
@@ -26,6 +26,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.3.14",
|
||||
"typescript": "^5.9.2"
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/typecheck",
|
||||
"version": "0.8.9",
|
||||
"version": "0.8.10",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
@@ -10,6 +10,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@wrnexus/syntax": "workspace:*",
|
||||
"typescript": "^5.5.0"
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/uploader",
|
||||
"version": "0.8.9",
|
||||
"version": "0.8.10",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
@@ -23,7 +23,7 @@
|
||||
],
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.3.14",
|
||||
"typescript": "^5.9.2",
|
||||
"typescript": "^6.0.3",
|
||||
"@wrnexus/syntax": "workspace:*"
|
||||
},
|
||||
"wrnexus": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/validation",
|
||||
"version": "0.8.9",
|
||||
"version": "0.8.10",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
@@ -23,7 +23,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.3.14",
|
||||
"typescript": "^5.9.2",
|
||||
"typescript": "^6.0.3",
|
||||
"@wrnexus/syntax": "workspace:*"
|
||||
},
|
||||
"wrnexus": {
|
||||
|
||||
@@ -48,7 +48,8 @@ export function validateStagedManifest(stage: string, manifest: Record<string, u
|
||||
}
|
||||
if (!/^@wrnexus\/[a-z0-9-]+$/.test(name)) throw new Error(`${name} has an invalid package name.`);
|
||||
if (manifest.private !== undefined) throw new Error(`${name} staged manifest must omit private.`);
|
||||
if ((manifest.publishConfig as any)?.registry !== "https://registry.npmjs.org/") {
|
||||
const expectedRegistry = process.env.WRNEXUS_NPM_REGISTRY ?? "https://registry.npmjs.org/";
|
||||
if ((manifest.publishConfig as any)?.registry !== expectedRegistry) {
|
||||
throw new Error(`${name} staged manifest has an unexpected registry.`);
|
||||
}
|
||||
if (!["restricted", "public"].includes(String((manifest.publishConfig as any)?.access))) {
|
||||
|
||||
@@ -32,7 +32,7 @@ import { validateAndHashStage, type StagedPackageIntegrity } from "./lib/package
|
||||
const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const packagesDir = join(repoRoot, "packages");
|
||||
const stageRoot = join(repoRoot, ".publish");
|
||||
const REGISTRY = "https://registry.npmjs.org/";
|
||||
const REGISTRY = process.env.WRNEXUS_NPM_REGISTRY ?? "https://registry.npmjs.org/";
|
||||
// First release is published PRIVATE ("restricted") so we can test before the
|
||||
// world sees it; flip to public with `npm access public @wrnexus/<name>` (or set
|
||||
// WRNEXUS_NPM_ACCESS=public to stage public manifests). Requires the `wrnexus`
|
||||
|
||||
@@ -14,6 +14,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.3.14",
|
||||
"typescript": "^5.9.2"
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"lib": ["ESNext", "DOM", "DOM.Iterable"],
|
||||
"types": ["bun"],
|
||||
"strict": true,
|
||||
"ignoreDeprecations": "6.0",
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
|
||||
Reference in New Issue
Block a user