@astroscope/wormhole 2.0.0 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,9 +6,11 @@ Share dynamic server data with React islands and client scripts — typed, strea
6
6
 
7
7
  Astro recommends [nanostores](https://docs.astro.build/en/recipes/sharing-state/) for sharing state between islands, but nanostores are client-only — there's no built-in way to hydrate them with server data during SSR.
8
8
 
9
- `@astroscope/wormhole` bridges this gap: define your wormholes in a registry, resolve their values per request in a single middleware, and read them anywhere — Astro frontmatter, React islands, `<script>` blocks — through one typed `wormholes` proxy. Client code imports nothing of yours: the data streams into the HTML exactly where it is needed.
9
+ `@astroscope/wormhole` bridges this gap: define your wormholes in a registry — each with the handler that resolves its value per request — and read them anywhere — Astro frontmatter, React islands, `<script>` blocks — through one typed `wormholes` proxy. Client code imports nothing of yours: the data streams into the HTML exactly where it is needed.
10
10
 
11
- **Delivery is sliced per island.** The build scans your client code to see which wormholes each component reads; at runtime every island receives exactly those, embedded in the HTML right before it. A wormhole no client code reads ships **zero bytes**.
11
+ **Loading is scoped per route.** The build scans server and client code to see which wormholes each route can reach; at runtime only those handlers run for a request. A wormhole nothing on the route reads costs **nothing** — no database call, no bytes.
12
+
13
+ **Delivery is sliced per island.** Every island receives exactly the wormholes its code reads, embedded in the HTML right before it. A wormhole no client code reads ships **zero bytes**.
12
14
 
13
15
  **Typical use cases:**
14
16
 
@@ -20,11 +22,11 @@ Astro recommends [nanostores](https://docs.astro.build/en/recipes/sharing-state/
20
22
 
21
23
  ## Important notes
22
24
 
23
- - **No secrets in wormholes.** Wormhole data is serialized into inline `<script>` tags and sent to the browser. Never store tokens, API keys, credentials, or any sensitive data in a wormhole. (In dev, *all* open wormholes ship with every page; production slicing is an optimization, not a security boundary.)
25
+ - **No secrets in wormholes.** Wormhole data is serialized into inline `<script>` tags and sent to the browser. Never store tokens, API keys, credentials, or any sensitive data in a wormhole. (In dev, _all_ open wormholes ship with every page; production slicing is an optimization, not a security boundary.)
24
26
  - **Requires `@astroscope/node`** — per-island delivery uses its islands pipeline in production.
25
- - **The registry is server-only.** Client code must never import `src/wormholes.ts` — it accesses values through the `wormholes` proxy instead. (Importing it in client code throws at once.)
26
- - **`set()` is client-only.** Server values are request-scoped and come from the middleware.
27
- - **Values are deeply readonly.** `get()`, `subscribe()` and `useWormhole()` expose the value as `DeepReadonly<T>` — in-place mutation would silently bypass subscribers. The only way to update is `set()` with a new value: `wormholes.counter.set({ ...wormholes.counter.get(), count: 1 })`.
27
+ - **The registry is server-only.** It carries the handlers, and with them your server code. Client code must never import `src/wormholes.ts` — it accesses values through the `wormholes` proxy instead. (The client build fails on such an import, and the eslint plugin's `no-registry-import` rule flags it in island code.)
28
+ - **`set()` is client-only.** Server values are request-scoped and come from the handlers.
29
+ - **Values are deeply readonly.** `get()`, `subscribe()` and `useWormhole()` expose the value as `ReadonlyDeep<T>` — in-place mutation would silently bypass subscribers. The only way to update is `set()` with a new value: `wormholes.counter.set({ ...wormholes.counter.get(), count: 1 })`.
28
30
  - **Prerendered pages get no wormhole data** — values are per-request by definition.
29
31
 
30
32
  ## Examples
@@ -39,6 +41,7 @@ npm install @astroscope/wormhole
39
41
 
40
42
  ```ts
41
43
  // astro.config.ts
44
+ import { defineConfig } from 'astro/config';
42
45
  import node from '@astroscope/node';
43
46
  import wormhole from '@astroscope/wormhole';
44
47
 
@@ -49,13 +52,13 @@ export default defineConfig({
49
52
  });
50
53
  ```
51
54
 
52
- The integration registers the build-time scanner and generates a type stub from your registry, so `wormholes.<name>` is fully typed everywhere.
55
+ The integration registers the build-time scanner, injects the middleware after your own `src/middleware.ts` (so handlers see everything it put on `locals`), and generates a type stub from your registry, so `wormholes.<name>` is fully typed everywhere. `wormhole({ exclude })` takes the same patterns as other astroscope middlewares (defaults to `RECOMMENDED_EXCLUDES`).
53
56
 
54
57
  ## Usage
55
58
 
56
59
  ### 1. Define the registry
57
60
 
58
- One file, one export named `wormholes` — the keys are the wormhole names (same pattern as Astro Actions' `src/actions/index.ts`):
61
+ One file, one export named `wormholes` — the keys are the wormhole names, each entry carries its handler (same pattern as Astro Actions' `src/actions/index.ts`):
59
62
 
60
63
  ```ts
61
64
  // src/wormholes.ts
@@ -67,30 +70,18 @@ export type Session = {
67
70
  };
68
71
 
69
72
  export const wormholes = {
70
- session: defineWormhole<Session>(),
71
- cart: defineWormhole<{ items: string[] }>(),
72
- };
73
- ```
74
-
75
- `src/wormholes/index.ts` works too.
76
-
77
- ### 2. Resolve values in middleware
78
-
79
- ```ts
80
- // src/middleware.ts
81
- import { createWormholeMiddleware } from '@astroscope/wormhole/server';
82
-
83
- export const onRequest = createWormholeMiddleware({
84
- values: async (ctx) => ({
85
- session: { user: 'Alice', role: 'admin' },
86
- cart: await loadCart(ctx),
73
+ session: defineWormhole({
74
+ handler: (ctx): Session => ({ user: ctx.locals.user, role: ctx.locals.role }),
87
75
  }),
88
- });
76
+ cart: defineWormhole({
77
+ handler: async (ctx) => loadCart(ctx),
78
+ }),
79
+ };
89
80
  ```
90
81
 
91
- The `values` return type is checked against your registry. Omit a key (or return `undefined` for it) to leave that wormhole closed for the request. Each request is isolated.
82
+ `src/wormholes/index.ts` works too. A handler receives the request's `APIContext` and runs only for routes the build found a reader on; return `undefined` to leave the wormhole closed for the request. Each request is isolated.
92
83
 
93
- ### 3. Read anywhere
84
+ ### 2. Read anywhere
94
85
 
95
86
  #### Astro frontmatter (SSR)
96
87
 
@@ -137,7 +128,7 @@ export function UserBadge() {
137
128
  </script>
138
129
  ```
139
130
 
140
- ### 4. Update from the client
131
+ ### 3. Update from the client
141
132
 
142
133
  Call `set()` to update the wormhole — every `useWormhole()` hook and `subscribe()` callback on the page reacts, across islands:
143
134
 
@@ -171,21 +162,17 @@ export function RoleToggle() {
171
162
 
172
163
  The single access point, typed from your registry. `wormholes.cart` returns a `Wormhole<T>`:
173
164
 
174
- | Method | Description |
175
- | ------------------------- | -------------------------------------------------------------------------------- |
176
- | `wormholes.x.get()` | Read the current value as `DeepReadonly<T>` (server: request scope; client: streamed data) |
177
- | `wormholes.x.set(data)` | Replace the value and notify all subscribers (client only) |
178
- | `wormholes.x.subscribe(fn)` | Listen for `set()` updates, returns an unsubscribe function |
165
+ | Method | Description |
166
+ | --------------------------- | ------------------------------------------------------------------------------------------ |
167
+ | `wormholes.x.get()` | Read the current value as `ReadonlyDeep<T>` (server: request scope; client: streamed data) |
168
+ | `wormholes.x.set(data)` | Replace the value and notify all subscribers (client only) |
169
+ | `wormholes.x.subscribe(fn)` | Listen for `set()` updates, returns an unsubscribe function |
179
170
 
180
171
  Accesses must be static (`wormholes.cart`, `wormholes['cart']`). A dynamic access (`wormholes[name]`) still works, but the build can no longer tell which wormholes that chunk needs and delivers all open ones to its islands.
181
172
 
182
- ### `defineWormhole<T>()` <sub>registry only</sub>
173
+ ### `defineWormhole({ handler, eager? })` <sub>registry only</sub>
183
174
 
184
- Creates a typed wormhole for the registry; the name comes from the registry key. Registry entries are full `Wormhole<T>` objects, so server code can also import `wormholes` from your own registry file directly.
185
-
186
- ### `createWormholeMiddleware({ values, exclude? })` <sub>server only</sub>
187
-
188
- Creates the middleware: resolves `values(ctx)` per request, opens them for server-side `get()`, and delivers them to the client. `exclude` accepts the same patterns as other astroscope middlewares (defaults to `RECOMMENDED_EXCLUDES`).
175
+ Creates a typed wormhole for the registry; the value type is inferred from the handler (or given as `defineWormhole<T>(...)`), the name comes from the registry key. `eager: true` runs the handler on every request instead of only where the build found a reader — for wormholes read by islands the build cannot attribute to a route (components chosen dynamically at render time). Registry entries are full `Wormhole<T>` objects, so server code can also import `wormholes` from your own registry file directly.
189
176
 
190
177
  ### `openWormholes(wh, data, fn)` <sub>server only</sub>
191
178
 
@@ -195,13 +182,15 @@ Provides values to server code that runs outside the request pipeline — tests,
195
182
 
196
183
  React hook that reads a wormhole and re-renders on `set()`. During SSR it reads the request scope, so server and client render identically.
197
184
 
198
- ### `DeepReadonly<T>` / `UnwrapWormholes<R>`
185
+ ### `UnwrapWormholes<R>`
199
186
 
200
- Exported helper types. `DeepReadonly<T>` is the recursively-readonly value type; `UnwrapWormholes` maps a registry shape to its value types.
187
+ Exported helper type mapping a registry shape to its value types. The recursively-readonly value type is `ReadonlyDeep<T>` from `@entwico/dash`.
201
188
 
202
189
  ## How it works
203
190
 
204
- At build time the integration scans your client code to see which wormholes each component reads, and generates the types for the `wormholes` proxy from your registry. At runtime the middleware resolves your values once per request, and each island's data is embedded in the HTML right before it — so it is always there before the island's code runs, even while the page is still streaming. On the client, `set()` updates the shared data and notifies subscribers, keeping islands and scripts in sync.
191
+ Routes the build could not attribute (dev, astro's own routes) load everything. On the client, `set()` updates the shared data and notifies subscribers, keeping islands and scripts in sync.
192
+
193
+ Each handler runs under its own `wormhole <name>` span beneath the request span — they run in parallel, and the trace shows which one holds the page's first byte — and records `astro.wormhole.handler.duration` {`astro.wormhole.name`}. A throwing handler is counted on `astro.wormhole.handler.failures` {`astro.wormhole.name`, `error.type`}, logged with its name, and fails the request. All through the adapter's telemetry, so nothing runs without its SDK.
205
194
 
206
195
  ## License
207
196
 
@@ -0,0 +1,7 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ //#region src/als.ts
3
+ const als = globalThis[Symbol.for("@astroscope/wormhole/als")] ??= new AsyncLocalStorage();
4
+ //#endregion
5
+ export { als as t };
6
+
7
+ //# sourceMappingURL=als-CntRqWdO.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"als-CntRqWdO.js","names":[],"sources":["../src/als.ts"],"sourcesContent":["import { AsyncLocalStorage } from 'node:async_hooks';\n\n// a single ALS holds an immutable map of all open wormholes (keyed by wormhole key);\n// keyed on globalThis so the vite-runner and native module instances share one context\n// eslint-disable-next-line unicorn/no-global-object-property-assignment -- `Symbol.for` slot on globalThis, shared by the vite-runner and native module instances\nexport const als: AsyncLocalStorage<ReadonlyMap<string, unknown>> = ((globalThis as any)[\n Symbol.for('@astroscope/wormhole/als')\n] ??= new AsyncLocalStorage());\n"],"mappings":";;AAKA,MAAa,MAAwD,WACnE,OAAO,IAAI,0BAA0B,OACjC,IAAI,kBAAkB"}
@@ -1,19 +1,37 @@
1
+ import { t as als } from "./als-CntRqWdO.js";
1
2
  import { t as wormholeKey } from "./key-BqTy1gM3.js";
2
- import { AsyncLocalStorage } from "node:async_hooks";
3
- //#region src/als.ts
4
- const als = globalThis[Symbol.for("@astroscope/wormhole/als")] ??= new AsyncLocalStorage();
5
- //#endregion
6
3
  //#region src/define.ts
7
4
  const ASSIGN = Symbol.for("@astroscope/wormhole.assign");
5
+ const SOURCE = Symbol.for("@astroscope/wormhole.source");
8
6
  /**
9
- * Define a wormhole for the `src/wormholes.ts` registry. The wormhole's name is the
10
- * registry key — it is assigned when the registry is passed to
11
- * `createWormholeMiddleware()` (or `assignWormholeNames()`).
7
+ * Define a wormhole for the `src/wormholes.ts` registry, actions-style. The
8
+ * handler resolves the value per request; the middleware calls it only for routes
9
+ * the build found a reader on (frontmatter, islands, scripts) — or on every
10
+ * request with `eager: true`. The wormhole's name is the registry key, assigned
11
+ * when the registry is passed to `createWormholeMiddleware()` (or
12
+ * `assignWormholeNames()`).
12
13
  *
13
14
  * **Security:** wormhole data is serialized into the HTML and sent to the browser.
14
15
  * Never store secrets (tokens, API keys, credentials) in a wormhole.
16
+ *
17
+ * @example
18
+ * ```typescript
19
+ * export const wormholes = {
20
+ * cart: defineWormhole({ handler: (ctx) => ctx.locals.cart }),
21
+ * config: defineWormhole({ handler: () => loadConfig(), eager: true }),
22
+ * };
23
+ * ```
15
24
  */
16
- function defineWormhole() {
25
+ function defineWormhole(definition) {
26
+ const wormhole = createWormhole();
27
+ wormhole[SOURCE] = {
28
+ handler: definition.handler,
29
+ eager: definition.eager ?? false
30
+ };
31
+ return wormhole;
32
+ }
33
+ /** the server-side wormhole object without a handler — the `wormholes` proxy stubs (internal) */
34
+ function createWormhole() {
17
35
  let name;
18
36
  const requireName = () => {
19
37
  if (name === void 0) throw new Error("wormhole is not registered — pass its registry to createWormholeMiddleware()");
@@ -44,6 +62,10 @@ function defineWormhole() {
44
62
  }
45
63
  };
46
64
  }
65
+ /** the definition a registry entry was created with — undefined for proxy stubs */
66
+ function getWormholeSource(wormhole) {
67
+ return wormhole[SOURCE];
68
+ }
47
69
  /**
48
70
  * Assign each wormhole its registry key as name. Called by `createWormholeMiddleware()`;
49
71
  * exposed for setups that read wormholes on the server without the middleware.
@@ -51,11 +73,11 @@ function defineWormhole() {
51
73
  function assignWormholeNames(registry) {
52
74
  for (const [name, wormhole] of Object.entries(registry)) {
53
75
  const assign = wormhole[ASSIGN];
54
- if (typeof assign !== "function") throw new Error(`registry entry "${name}" is not a wormhole — the registry must contain defineWormhole() values only`);
76
+ if (typeof assign !== "function") throw new TypeError(`registry entry "${name}" is not a wormhole — the registry must contain defineWormhole() values only`);
55
77
  assign(name);
56
78
  }
57
79
  }
58
80
  //#endregion
59
- export { defineWormhole as n, als as r, assignWormholeNames as t };
81
+ export { getWormholeSource as i, createWormhole as n, defineWormhole as r, assignWormholeNames as t };
60
82
 
61
- //# sourceMappingURL=define-BO-pYQc1.js.map
83
+ //# sourceMappingURL=define-DAoqsUVA.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"define-DAoqsUVA.js","names":[],"sources":["../src/define.ts"],"sourcesContent":["import type { ReadonlyDeep } from '@entwico/dash';\nimport { als } from './als.js';\nimport { wormholeKey } from './key.js';\nimport type { Wormhole, WormholeDefinition, WormholeHandler } from './types.js';\n\n// name assignment and the handler must survive dual module instances (vite runner +\n// native), so both ride the wormhole object itself under Symbol.for keys\nconst ASSIGN = Symbol.for('@astroscope/wormhole.assign');\nconst SOURCE = Symbol.for('@astroscope/wormhole.source');\n\n/** how the middleware opens a wormhole: its handler and whether every route loads it */\nexport type WormholeSource = { handler: WormholeHandler<unknown>; eager: boolean };\n\ntype Internal = Wormhole<unknown> & { [ASSIGN](next: string): void; [SOURCE]?: WormholeSource | undefined };\n\n/**\n * Define a wormhole for the `src/wormholes.ts` registry, actions-style. The\n * handler resolves the value per request; the middleware calls it only for routes\n * the build found a reader on (frontmatter, islands, scripts) — or on every\n * request with `eager: true`. The wormhole's name is the registry key, assigned\n * when the registry is passed to `createWormholeMiddleware()` (or\n * `assignWormholeNames()`).\n *\n * **Security:** wormhole data is serialized into the HTML and sent to the browser.\n * Never store secrets (tokens, API keys, credentials) in a wormhole.\n *\n * @example\n * ```typescript\n * export const wormholes = {\n * cart: defineWormhole({ handler: (ctx) => ctx.locals.cart }),\n * config: defineWormhole({ handler: () => loadConfig(), eager: true }),\n * };\n * ```\n */\nexport function defineWormhole<T>(definition: WormholeDefinition<T>): Wormhole<T> {\n const wormhole = createWormhole() as Internal;\n\n wormhole[SOURCE] = { handler: definition.handler as WormholeHandler<unknown>, eager: definition.eager ?? false };\n\n return wormhole as Wormhole<T>;\n}\n\n/** the server-side wormhole object without a handler — the `wormholes` proxy stubs (internal) */\nexport function createWormhole<T>(): Wormhole<T> {\n let name: string | undefined;\n\n const requireName = (): string => {\n if (name === undefined) {\n throw new Error('wormhole is not registered — pass its registry to createWormholeMiddleware()');\n }\n\n return name;\n };\n\n const key = (): string => wormholeKey(requireName());\n\n // methods must not rely on `this` — consumers pass them around detached\n // (e.g. useSyncExternalStore(wormhole.subscribe, wormhole.get, wormhole.get))\n const wormhole: Wormhole<T> & { [ASSIGN](next: string): void } = {\n get name() {\n return requireName();\n },\n\n get key() {\n return key();\n },\n\n get(): ReadonlyDeep<T> {\n const value = als.getStore()?.get(key());\n\n if (value === undefined) {\n throw new Error(\n `wormhole \"${requireName()}\" is not open for this request — is it provided by the wormhole middleware?`,\n );\n }\n\n return value as ReadonlyDeep<T>;\n },\n\n set(): void {\n throw new Error(\n `wormhole \"${requireName()}\" set() cannot be called on the server — values are request-scoped and provided by the middleware`,\n );\n },\n\n subscribe(): () => void {\n // server values never change within a request — nothing to notify\n return () => {};\n },\n\n [ASSIGN](next: string): void {\n if (name !== undefined && name !== next) {\n throw new Error(`wormhole is registered under two names: \"${name}\" and \"${next}\"`);\n }\n\n name = next;\n },\n };\n\n return wormhole;\n}\n\n/** the definition a registry entry was created with — undefined for proxy stubs */\nexport function getWormholeSource<T>(wormhole: Wormhole<T>): WormholeSource | undefined {\n return (wormhole as unknown as Internal)[SOURCE];\n}\n\n/**\n * Assign each wormhole its registry key as name. Called by `createWormholeMiddleware()`;\n * exposed for setups that read wormholes on the server without the middleware.\n */\nexport function assignWormholeNames(registry: Record<string, object>): void {\n for (const [name, wormhole] of Object.entries(registry)) {\n const assign = (wormhole as { [ASSIGN]?: (next: string) => void })[ASSIGN];\n\n if (typeof assign !== 'function') {\n throw new TypeError(\n `registry entry \"${name}\" is not a wormhole — the registry must contain defineWormhole() values only`,\n );\n }\n\n assign(name);\n }\n}\n"],"mappings":";;;AAOA,MAAM,SAAS,OAAO,IAAI,6BAA6B;AACvD,MAAM,SAAS,OAAO,IAAI,6BAA6B;;;;;;;;;;;;;;;;;;;;AA0BvD,SAAgB,eAAkB,YAAgD;CAChF,MAAM,WAAW,eAAe;CAEhC,SAAS,UAAU;EAAE,SAAS,WAAW;EAAqC,OAAO,WAAW,SAAS;CAAM;CAE/G,OAAO;AACT;;AAGA,SAAgB,iBAAiC;CAC/C,IAAI;CAEJ,MAAM,oBAA4B;EAChC,IAAI,SAAS,KAAA,GACX,MAAM,IAAI,MAAM,8EAA8E;EAGhG,OAAO;CACT;CAEA,MAAM,YAAoB,YAAY,YAAY,CAAC;CA6CnD,OAAO;EAxCL,IAAI,OAAO;GACT,OAAO,YAAY;EACrB;EAEA,IAAI,MAAM;GACR,OAAO,IAAI;EACb;EAEA,MAAuB;GACrB,MAAM,QAAQ,IAAI,SAAS,CAAC,EAAE,IAAI,IAAI,CAAC;GAEvC,IAAI,UAAU,KAAA,GACZ,MAAM,IAAI,MACR,aAAa,YAAY,EAAE,4EAC7B;GAGF,OAAO;EACT;EAEA,MAAY;GACV,MAAM,IAAI,MACR,aAAa,YAAY,EAAE,kGAC7B;EACF;EAEA,YAAwB;GAEtB,aAAa,CAAC;EAChB;EAEA,CAAC,QAAQ,MAAoB;GAC3B,IAAI,SAAS,KAAA,KAAa,SAAS,MACjC,MAAM,IAAI,MAAM,4CAA4C,KAAK,SAAS,KAAK,EAAE;GAGnF,OAAO;EACT;CAGY;AAChB;;AAGA,SAAgB,kBAAqB,UAAmD;CACtF,OAAQ,SAAiC;AAC3C;;;;;AAMA,SAAgB,oBAAoB,UAAwC;CAC1E,KAAK,MAAM,CAAC,MAAM,aAAa,OAAO,QAAQ,QAAQ,GAAG;EACvD,MAAM,SAAU,SAAmD;EAEnE,IAAI,OAAO,WAAW,YACpB,MAAM,IAAI,UACR,mBAAmB,KAAK,6EAC1B;EAGF,OAAO,IAAI;CACb;AACF"}
@@ -1,9 +1,9 @@
1
- import { a as WormholeRegistry, i as WormholeMap, n as UnwrapWormholes, r as Wormhole, t as DeepReadonly } from "./types-okMd-V3s.js";
1
+ import { a as WormholeMap, i as WormholeHandler, n as Wormhole, o as WormholeRegistry, r as WormholeDefinition, t as UnwrapWormholes } from "./types-v_ftArqH.js";
2
2
  //#region src/index.browser.d.ts
3
- declare const wormholes: WormholeMap;
4
- declare function defineWormhole(): never;
5
- declare function assignWormholeNames(): never;
6
- declare function wormholeIntegration(): never;
3
+ export declare const wormholes: WormholeMap;
4
+ export declare function defineWormhole(): never;
5
+ export declare function assignWormholeNames(): never;
6
+ export default function wormholeIntegration(): never;
7
7
  //#endregion
8
- export { type DeepReadonly, type UnwrapWormholes, type Wormhole, type WormholeMap, type WormholeRegistry, assignWormholeNames, wormholeIntegration as default, defineWormhole, wormholes };
8
+ export type { UnwrapWormholes, Wormhole, WormholeDefinition, WormholeHandler, WormholeMap, WormholeRegistry };
9
9
  //# sourceMappingURL=index.browser.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.browser.d.ts","names":[],"sources":["../src/index.browser.ts"],"mappings":";;cAiEa,WAAW;iBAMR;iBAMA;iBAIQ"}
1
+ {"version":3,"file":"index.browser.d.ts","names":[],"sources":["../src/index.browser.ts"],"mappings":";;qBA0Ea,WAAW;wBAMR;wBAMA;wBAIQ"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.browser.js","names":[],"sources":["../src/index.browser.ts"],"sourcesContent":["import { wormholeKey } from './key.js';\nimport type { DeepReadonly, Wormhole, WormholeMap } from './types.js';\n\nexport type { DeepReadonly, UnwrapWormholes, Wormhole, WormholeMap, WormholeRegistry } from './types.js';\n\n/**\n * Browser side of the `wormholes` proxy. Values live on `self.__wormholes__` — written\n * by the inline merge scripts the server emits before each island (and at stream end\n * for script consumers), or by client-side `set()` calls. The entry protocol is\n * `{ v: value, l: listeners }`, shared with the emitted scripts, so writes land in\n * either order.\n */\n\ntype Entry = { v?: unknown; l?: ((value: unknown) => void)[] | undefined };\n\nconst store: Record<string, Entry> = ((globalThis as any).__wormholes__ ??= {});\n\nconst stubs = new Map<string, Wormhole<unknown>>();\n\nfunction stub(name: string): Wormhole<unknown> {\n let wormhole = stubs.get(name);\n\n if (wormhole) {\n return wormhole;\n }\n\n const entry = (): Entry => (store[name] ??= {});\n\n wormhole = {\n name,\n key: wormholeKey(name),\n\n get(): DeepReadonly<unknown> {\n const value = entry().v;\n\n if (value === undefined) {\n throw new Error(`wormhole \"${name}\" has no data — is it provided by the wormhole middleware?`);\n }\n\n return value;\n },\n\n set(data): void {\n const current = entry();\n\n current.v = data;\n current.l?.forEach((fn) => fn(data));\n },\n\n subscribe(fn): () => void {\n const current = entry();\n\n (current.l ??= []).push(fn);\n\n return () => {\n current.l = current.l?.filter((listener) => listener !== fn);\n };\n },\n };\n\n stubs.set(name, wormhole);\n\n return wormhole;\n}\n\nexport const wormholes: WormholeMap = new Proxy({} as WormholeMap, {\n get(_target, prop) {\n return typeof prop === 'string' ? stub(prop) : undefined;\n },\n});\n\nexport function defineWormhole(): never {\n throw new Error(\n 'defineWormhole() is server-only — do not import the wormhole registry in client code; access wormholes via the `wormholes` proxy instead',\n );\n}\n\nexport function assignWormholeNames(): never {\n throw new Error('assignWormholeNames() is server-only');\n}\n\nexport default function wormholeIntegration(): never {\n throw new Error('the @astroscope/wormhole integration is server-only');\n}\n"],"mappings":";;AAeA,MAAM,QAAgC,WAAoB,kBAAkB,CAAC;AAE7E,MAAM,wBAAQ,IAAI,IAA+B;AAEjD,SAAS,KAAK,MAAiC;CAC7C,IAAI,WAAW,MAAM,IAAI,IAAI;CAE7B,IAAI,UACF,OAAO;CAGT,MAAM,cAAsB,MAAM,UAAU,CAAC;CAE7C,WAAW;EACT;EACA,KAAK,YAAY,IAAI;EAErB,MAA6B;GAC3B,MAAM,QAAQ,MAAM,CAAC,CAAC;GAEtB,IAAI,UAAU,KAAA,GACZ,MAAM,IAAI,MAAM,aAAa,KAAK,2DAA2D;GAG/F,OAAO;EACT;EAEA,IAAI,MAAY;GACd,MAAM,UAAU,MAAM;GAEtB,QAAQ,IAAI;GACZ,QAAQ,GAAG,SAAS,OAAO,GAAG,IAAI,CAAC;EACrC;EAEA,UAAU,IAAgB;GACxB,MAAM,UAAU,MAAM;GAEtB,CAAC,QAAQ,MAAM,CAAC,EAAA,CAAG,KAAK,EAAE;GAE1B,aAAa;IACX,QAAQ,IAAI,QAAQ,GAAG,QAAQ,aAAa,aAAa,EAAE;GAC7D;EACF;CACF;CAEA,MAAM,IAAI,MAAM,QAAQ;CAExB,OAAO;AACT;AAEA,MAAa,YAAyB,IAAI,MAAM,CAAC,GAAkB,EACjE,IAAI,SAAS,MAAM;CACjB,OAAO,OAAO,SAAS,WAAW,KAAK,IAAI,IAAI,KAAA;AACjD,EACF,CAAC;AAED,SAAgB,iBAAwB;CACtC,MAAM,IAAI,MACR,0IACF;AACF;AAEA,SAAgB,sBAA6B;CAC3C,MAAM,IAAI,MAAM,sCAAsC;AACxD;AAEA,SAAwB,sBAA6B;CACnD,MAAM,IAAI,MAAM,qDAAqD;AACvE"}
1
+ {"version":3,"file":"index.browser.js","names":[],"sources":["../src/index.browser.ts"],"sourcesContent":["import type { ReadonlyDeep } from '@entwico/dash';\nimport { wormholeKey } from './key.js';\nimport type { Wormhole, WormholeMap } from './types.js';\n\nexport type {\n UnwrapWormholes,\n Wormhole,\n WormholeDefinition,\n WormholeHandler,\n WormholeMap,\n WormholeRegistry,\n} from './types.js';\n\n/**\n * Browser side of the `wormholes` proxy. Values live on `self.__wormholes__` — written\n * by the inline merge scripts the server emits before each island (and at stream end\n * for script consumers), or by client-side `set()` calls. The entry protocol is\n * `{ v: value, l: listeners }`, shared with the emitted scripts, so writes land in\n * either order.\n */\n\ntype Entry = { v?: unknown; l?: ((value: unknown) => void)[] | undefined };\n\n// eslint-disable-next-line unicorn/no-global-object-property-assignment -- `self.__wormholes__` is the entry store shared with the emitted merge scripts\nconst store: Record<string, Entry> = ((globalThis as any).__wormholes__ ??= {});\n\nconst stubs = new Map<string, Wormhole<unknown>>();\n\nfunction stub(name: string): Wormhole<unknown> {\n let wormhole = stubs.get(name);\n\n if (wormhole) {\n return wormhole;\n }\n\n const entry = (): Entry => (store[name] ??= {});\n\n wormhole = {\n name,\n key: wormholeKey(name),\n\n get(): ReadonlyDeep<unknown> {\n const value = entry().v;\n\n if (value === undefined) {\n throw new Error(`wormhole \"${name}\" has no data — is it provided by the wormhole middleware?`);\n }\n\n return value;\n },\n\n set(data): void {\n const current = entry();\n\n current.v = data;\n current.l?.forEach((fn) => fn(data));\n },\n\n subscribe(fn): () => void {\n const current = entry();\n\n (current.l ??= []).push(fn);\n\n return () => {\n current.l = current.l?.filter((listener) => listener !== fn);\n };\n },\n };\n\n stubs.set(name, wormhole);\n\n return wormhole;\n}\n\nexport const wormholes: WormholeMap = new Proxy({} as WormholeMap, {\n get(_target, prop) {\n return typeof prop === 'string' ? stub(prop) : undefined;\n },\n});\n\nexport function defineWormhole(): never {\n throw new Error(\n 'defineWormhole() is server-only — do not import the wormhole registry in client code; access wormholes via the `wormholes` proxy instead',\n );\n}\n\nexport function assignWormholeNames(): never {\n throw new Error('assignWormholeNames() is server-only');\n}\n\nexport default function wormholeIntegration(): never {\n throw new Error('the @astroscope/wormhole integration is server-only');\n}\n"],"mappings":";;AAwBA,MAAM,QAAgC,WAAoB,kBAAkB,CAAC;AAE7E,MAAM,wBAAQ,IAAI,IAA+B;AAEjD,SAAS,KAAK,MAAiC;CAC7C,IAAI,WAAW,MAAM,IAAI,IAAI;CAE7B,IAAI,UACF,OAAO;CAGT,MAAM,cAAsB,MAAM,UAAU,CAAC;CAE7C,WAAW;EACT;EACA,KAAK,YAAY,IAAI;EAErB,MAA6B;GAC3B,MAAM,QAAQ,MAAM,CAAC,CAAC;GAEtB,IAAI,UAAU,KAAA,GACZ,MAAM,IAAI,MAAM,aAAa,KAAK,2DAA2D;GAG/F,OAAO;EACT;EAEA,IAAI,MAAY;GACd,MAAM,UAAU,MAAM;GAEtB,QAAQ,IAAI;GACZ,QAAQ,GAAG,SAAS,OAAO,GAAG,IAAI,CAAC;EACrC;EAEA,UAAU,IAAgB;GACxB,MAAM,UAAU,MAAM;GAEtB,CAAC,QAAQ,MAAM,CAAC,EAAA,CAAG,KAAK,EAAE;GAE1B,aAAa;IACX,QAAQ,IAAI,QAAQ,GAAG,QAAQ,aAAa,aAAa,EAAE;GAC7D;EACF;CACF;CAEA,MAAM,IAAI,MAAM,QAAQ;CAExB,OAAO;AACT;AAEA,MAAa,YAAyB,IAAI,MAAM,CAAC,GAAkB,EACjE,IAAI,SAAS,MAAM;CACjB,OAAO,OAAO,SAAS,WAAW,KAAK,IAAI,IAAI,KAAA;AACjD,EACF,CAAC;AAED,SAAgB,iBAAwB;CACtC,MAAM,IAAI,MACR,0IACF;AACF;AAEA,SAAgB,sBAA6B;CAC3C,MAAM,IAAI,MAAM,sCAAsC;AACxD;AAEA,SAAwB,sBAA6B;CACnD,MAAM,IAAI,MAAM,qDAAqD;AACvE"}
package/dist/index.d.ts CHANGED
@@ -1,33 +1,51 @@
1
- import { a as WormholeRegistry, i as WormholeMap, n as UnwrapWormholes, r as Wormhole, t as DeepReadonly } from "./types-okMd-V3s.js";
1
+ import { a as WormholeMap, i as WormholeHandler, n as Wormhole, o as WormholeRegistry, r as WormholeDefinition, t as UnwrapWormholes } from "./types-v_ftArqH.js";
2
+ import { ExcludePattern } from "@astroscope/node/excludes";
2
3
  import { AstroIntegration } from "astro";
3
4
  //#region src/integration.d.ts
5
+ type WormholeIntegrationOptions = {
6
+ /** patterns the middleware skips — no handler runs, nothing is delivered; defaults to RECOMMENDED_EXCLUDES */
7
+ exclude?: ExcludePattern[] | undefined;
8
+ };
4
9
  /**
5
- * Astro integration for wormholes with per-island payload slicing.
10
+ * Astro integration for wormholes with per-route loading and per-island delivery.
6
11
  *
7
12
  * Resolves the `src/wormholes.ts` registry and exposes it to the middleware as a
8
- * virtual module, registers the vite plugin that scans client code for
9
- * `wormholes.<name>` accesses and maps them onto the emitted chunks, and generates
10
- * the registry type stub that types the `wormholes` proxy. Delivery happens at
11
- * runtime: the wormhole middleware opens per-request values, and the islands
12
- * emitter (via `@astroscope/node`) writes each island's slice before its tag.
13
+ * virtual module, injects the middleware after the project's own (so handlers see
14
+ * everything earlier middleware put on `locals`), registers the vite plugin that
15
+ * scans server and client code for `wormholes.<name>` accesses and maps them onto
16
+ * routes and chunks, and generates the registry type stub that types the
17
+ * `wormholes` proxy. Delivery happens at runtime: the middleware runs the handlers
18
+ * the route needs, and the islands emitter (via `@astroscope/node`) writes each
19
+ * island's slice before its tag.
13
20
  */
14
- declare function wormholeIntegration(): AstroIntegration;
21
+ export default function wormholeIntegration(options?: WormholeIntegrationOptions): AstroIntegration;
15
22
  //#endregion
16
23
  //#region src/define.d.ts
17
24
  /**
18
- * Define a wormhole for the `src/wormholes.ts` registry. The wormhole's name is the
19
- * registry key — it is assigned when the registry is passed to
20
- * `createWormholeMiddleware()` (or `assignWormholeNames()`).
25
+ * Define a wormhole for the `src/wormholes.ts` registry, actions-style. The
26
+ * handler resolves the value per request; the middleware calls it only for routes
27
+ * the build found a reader on (frontmatter, islands, scripts) — or on every
28
+ * request with `eager: true`. The wormhole's name is the registry key, assigned
29
+ * when the registry is passed to `createWormholeMiddleware()` (or
30
+ * `assignWormholeNames()`).
21
31
  *
22
32
  * **Security:** wormhole data is serialized into the HTML and sent to the browser.
23
33
  * Never store secrets (tokens, API keys, credentials) in a wormhole.
34
+ *
35
+ * @example
36
+ * ```typescript
37
+ * export const wormholes = {
38
+ * cart: defineWormhole({ handler: (ctx) => ctx.locals.cart }),
39
+ * config: defineWormhole({ handler: () => loadConfig(), eager: true }),
40
+ * };
41
+ * ```
24
42
  */
25
- declare function defineWormhole<T>(): Wormhole<T>;
43
+ export declare function defineWormhole<T>(definition: WormholeDefinition<T>): Wormhole<T>;
26
44
  /**
27
45
  * Assign each wormhole its registry key as name. Called by `createWormholeMiddleware()`;
28
46
  * exposed for setups that read wormholes on the server without the middleware.
29
47
  */
30
- declare function assignWormholeNames(registry: Record<string, object>): void;
48
+ export declare function assignWormholeNames(registry: Record<string, object>): void;
31
49
  //#endregion
32
50
  //#region src/index.d.ts
33
51
  /**
@@ -37,7 +55,7 @@ declare function assignWormholeNames(registry: Record<string, object>): void;
37
55
  * server-rendered islands alike. Typed via the registry type stub the integration
38
56
  * generates from `src/wormholes.ts`.
39
57
  */
40
- declare const wormholes: WormholeMap;
58
+ export declare const wormholes: WormholeMap;
41
59
  //#endregion
42
- export { type DeepReadonly, type UnwrapWormholes, type Wormhole, type WormholeMap, type WormholeRegistry, assignWormholeNames, wormholeIntegration as default, defineWormhole, wormholes };
60
+ export type { UnwrapWormholes, Wormhole, WormholeDefinition, WormholeHandler, WormholeIntegrationOptions, WormholeMap, WormholeRegistry };
43
61
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/integration.ts","../src/define.ts","../src/index.ts"],"mappings":";;;;;;;;;;;;;iBAgBwB,uBAAuB;;;;;;;;;;;iBCA/B,eAAe,MAAM,SAAS;;;;;iBA+D9B,oBAAoB,UAAU;;;;;;;;;;cClDjC,WAAW"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/integration.ts","../src/define.ts","../src/index.ts"],"mappings":";;;;KAQY;;EAEV,UAAU;;;;;;;;;;;;;;wBAeY,oBAAoB,UAAS,6BAAkC;;;;;;;;;;;;;;;;;;;;;;wBCSvE,eAAe,GAAG,YAAY,mBAAmB,KAAK,SAAS;;;;;wBA6E/D,oBAAoB,UAAU;;;;;;;;;;qBC1EjC,WAAW"}
package/dist/index.js CHANGED
@@ -1,12 +1,16 @@
1
- import { n as defineWormhole, t as assignWormholeNames } from "./define-BO-pYQc1.js";
1
+ import { n as createWormhole, r as defineWormhole, t as assignWormholeNames } from "./define-DAoqsUVA.js";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
+ import { collectRouteIslands, routeEntrypoints, stripQuery } from "@astroscope/node/islands";
6
+ import { serializeExcludePatterns } from "@astroscope/node/excludes";
5
7
  //#region src/extraction/vite-plugin.ts
6
8
  const WORMHOLE_VIRTUAL_MODULE_ID = "virtual:@astroscope/wormhole/manifest";
7
9
  const REGISTRY_VIRTUAL_MODULE_ID = "virtual:@astroscope/wormhole/registry";
10
+ const CONFIG_VIRTUAL_MODULE_ID = "virtual:@astroscope/wormhole/config";
8
11
  const RESOLVED_VIRTUAL_MODULE_ID = `\0${WORMHOLE_VIRTUAL_MODULE_ID}`;
9
12
  const RESOLVED_REGISTRY_STUB_ID = `\0${REGISTRY_VIRTUAL_MODULE_ID}`;
13
+ const RESOLVED_CONFIG_VIRTUAL_MODULE_ID = `\0${CONFIG_VIRTUAL_MODULE_ID}`;
10
14
  const MANIFEST_FILE_NAME = "wormhole-manifest.json";
11
15
  const SCAN_EXTENSIONS = [
12
16
  ".ts",
@@ -14,13 +18,17 @@ const SCAN_EXTENSIONS = [
14
18
  ".js",
15
19
  ".jsx"
16
20
  ];
21
+ const SERVER_SCAN_EXTENSIONS = [...SCAN_EXTENSIONS, ".astro"];
17
22
  /** chunk file name → manifest key, e.g. `_astro/Counter.abc.js` → `Counter.abc` */
18
23
  function chunkFileNameToName(fileName) {
19
24
  return fileName.slice(fileName.lastIndexOf("/") + 1).replace(/\.js$/, "");
20
25
  }
21
26
  function wormholeVitePlugin(options) {
22
27
  let isBuild = false;
23
- const moduleScans = /* @__PURE__ */ new Map();
28
+ const clientScans = /* @__PURE__ */ new Map();
29
+ const serverScans = /* @__PURE__ */ new Map();
30
+ const routeNames = /* @__PURE__ */ new Map();
31
+ const patternsByFile = /* @__PURE__ */ new Map();
24
32
  return {
25
33
  name: "@astroscope/wormhole/extract",
26
34
  configResolved(config) {
@@ -28,10 +36,13 @@ function wormholeVitePlugin(options) {
28
36
  },
29
37
  resolveId(id) {
30
38
  if (id === "virtual:@astroscope/wormhole/manifest") return RESOLVED_VIRTUAL_MODULE_ID;
39
+ if (id === "virtual:@astroscope/wormhole/config") return RESOLVED_CONFIG_VIRTUAL_MODULE_ID;
31
40
  if (id === "virtual:@astroscope/wormhole/registry") return options.registryPath ?? RESOLVED_REGISTRY_STUB_ID;
32
41
  },
33
42
  load(id) {
34
43
  if (id === RESOLVED_REGISTRY_STUB_ID) return "export const wormholes = {};";
44
+ if (id === RESOLVED_CONFIG_VIRTUAL_MODULE_ID) return `export const exclude = ${options.exclude ? serializeExcludePatterns(options.exclude) : "null"};`;
45
+ if (id === options.registryPath && this.environment?.name === "client") throw new Error(`[@astroscope/wormhole] ${path.relative(process.cwd(), id)} is server-only — client code reads wormholes through the \`wormholes\` proxy from '@astroscope/wormhole'`);
35
46
  if (id !== RESOLVED_VIRTUAL_MODULE_ID) return;
36
47
  if (!isBuild) return "export const manifest = null;";
37
48
  return `
@@ -60,13 +71,37 @@ export { manifest };
60
71
  `;
61
72
  },
62
73
  async transform(code, id) {
63
- if (!isBuild || this.environment && this.environment.name !== "client") return;
74
+ if (!isBuild || !this.environment) return;
64
75
  if (id.includes("node_modules")) return;
65
- if (!SCAN_EXTENSIONS.some((ext) => id.endsWith(ext))) return;
76
+ const client = this.environment.name === "client";
77
+ const extensions = client ? SCAN_EXTENSIONS : SERVER_SCAN_EXTENSIONS;
78
+ const file = extensions.some((ext) => id.endsWith(ext)) ? id : stripQuery(id);
79
+ if (extensions.every((ext) => !file.endsWith(ext))) return;
66
80
  if (!code.includes("@astroscope/wormhole")) return;
67
- const { scanWormholeAccess } = await import("./scan-4GXufA9_.js");
68
- const scan = scanWormholeAccess(code, id);
69
- if (scan) moduleScans.set(id, scan);
81
+ const { scanWormholeAccess } = await import("./scan-BDYo2M_0.js");
82
+ const scan = scanWormholeAccess(code, file);
83
+ if (scan) (client ? clientScans : serverScans).set(client ? id : stripQuery(id), scan);
84
+ },
85
+ async generateBundle() {
86
+ if (this.environment.name === "client") return;
87
+ const { routes, patternsByModule } = await collectRouteIslands(this, options.pages());
88
+ for (const pattern of routes.keys()) routeNames.set(pattern, routeNames.get(pattern) ?? /* @__PURE__ */ new Set());
89
+ for (const [id, patterns] of patternsByModule) {
90
+ const file = stripQuery(id);
91
+ let owners = patternsByFile.get(file);
92
+ if (!owners) {
93
+ owners = /* @__PURE__ */ new Set();
94
+ patternsByFile.set(file, owners);
95
+ }
96
+ patterns.forEach((pattern) => owners.add(pattern));
97
+ const scan = serverScans.get(file);
98
+ if (!scan) continue;
99
+ for (const pattern of patterns) {
100
+ const names = routeNames.get(pattern);
101
+ scan.names.forEach((name) => names.add(name));
102
+ if (scan.dynamic) names.add("*");
103
+ }
104
+ }
70
105
  },
71
106
  writeBundle(outputOptions, bundle) {
72
107
  if (this.environment.name !== "client" || !outputOptions.dir) return;
@@ -79,7 +114,7 @@ export { manifest };
79
114
  const names = /* @__PURE__ */ new Set();
80
115
  let dynamic = false;
81
116
  for (const moduleId of chunk.moduleIds) {
82
- const scan = moduleScans.get(moduleId);
117
+ const scan = clientScans.get(moduleId);
83
118
  scan?.names.forEach((name) => names.add(name));
84
119
  dynamic ||= scan?.dynamic ?? false;
85
120
  }
@@ -90,17 +125,30 @@ export { manifest };
90
125
  if ((chunk.isEntry || chunk.isDynamicEntry) && chunk.facadeModuleId?.includes("astro&type=script")) scriptEntries.push(chunk.fileName);
91
126
  }
92
127
  const scriptNames = /* @__PURE__ */ new Set();
93
- const visited = /* @__PURE__ */ new Set();
94
- const walkScripts = (fileName) => {
95
- if (visited.has(fileName)) return;
96
- visited.add(fileName);
97
- chunkNamesByFileName.get(fileName)?.forEach((name) => scriptNames.add(name));
98
- chunkEdges.get(fileName)?.forEach(walkScripts);
99
- };
100
- scriptEntries.forEach(walkScripts);
128
+ for (const entry of scriptEntries) {
129
+ const names = /* @__PURE__ */ new Set();
130
+ const visited = /* @__PURE__ */ new Set();
131
+ const walk = (fileName) => {
132
+ if (visited.has(fileName)) return;
133
+ visited.add(fileName);
134
+ chunkNamesByFileName.get(fileName)?.forEach((name) => names.add(name));
135
+ chunkEdges.get(fileName)?.forEach((imported) => walk(imported));
136
+ };
137
+ walk(entry);
138
+ names.forEach((name) => scriptNames.add(name));
139
+ const owner = bundle[entry]?.type === "chunk" ? stripQuery(bundle[entry].facadeModuleId ?? "") : "";
140
+ const owners = patternsByFile.get(owner) ?? routeNames.keys();
141
+ for (const pattern of owners) {
142
+ const route = routeNames.get(pattern);
143
+ names.forEach((name) => route?.add(name));
144
+ }
145
+ }
146
+ const routes = {};
147
+ for (const [pattern, names] of routeNames) routes[pattern] = [...names];
101
148
  const manifest = {
102
149
  chunks,
103
- scripts: [...scriptNames]
150
+ scripts: [...scriptNames],
151
+ routes
104
152
  };
105
153
  const chunksDir = path.resolve(outputOptions.dir, "..", "server", "chunks");
106
154
  if (fs.existsSync(chunksDir)) fs.writeFileSync(path.join(chunksDir, MANIFEST_FILE_NAME), JSON.stringify(manifest));
@@ -111,22 +159,38 @@ export { manifest };
111
159
  //#endregion
112
160
  //#region src/integration.ts
113
161
  /**
114
- * Astro integration for wormholes with per-island payload slicing.
162
+ * Astro integration for wormholes with per-route loading and per-island delivery.
115
163
  *
116
164
  * Resolves the `src/wormholes.ts` registry and exposes it to the middleware as a
117
- * virtual module, registers the vite plugin that scans client code for
118
- * `wormholes.<name>` accesses and maps them onto the emitted chunks, and generates
119
- * the registry type stub that types the `wormholes` proxy. Delivery happens at
120
- * runtime: the wormhole middleware opens per-request values, and the islands
121
- * emitter (via `@astroscope/node`) writes each island's slice before its tag.
165
+ * virtual module, injects the middleware after the project's own (so handlers see
166
+ * everything earlier middleware put on `locals`), registers the vite plugin that
167
+ * scans server and client code for `wormholes.<name>` accesses and maps them onto
168
+ * routes and chunks, and generates the registry type stub that types the
169
+ * `wormholes` proxy. Delivery happens at runtime: the middleware runs the handlers
170
+ * the route needs, and the islands emitter (via `@astroscope/node`) writes each
171
+ * island's slice before its tag.
122
172
  */
123
- function wormholeIntegration() {
173
+ function wormholeIntegration(options = {}) {
124
174
  const resolveRegistryPath = (srcDir) => ["wormholes.ts", path.join("wormholes", "index.ts")].map((candidate) => path.join(fileURLToPath(srcDir), candidate)).find((candidate) => fs.existsSync(candidate)) ?? null;
175
+ let root = "";
176
+ let resolvedRoutes = [];
125
177
  return {
126
178
  name: "@astroscope/wormhole",
127
179
  hooks: {
128
- "astro:config:setup": ({ config, updateConfig }) => {
129
- updateConfig({ vite: { plugins: [wormholeVitePlugin({ registryPath: resolveRegistryPath(config.srcDir) })] } });
180
+ "astro:config:setup": ({ config, updateConfig, addMiddleware }) => {
181
+ root = fileURLToPath(config.root);
182
+ addMiddleware({
183
+ order: "post",
184
+ entrypoint: "@astroscope/wormhole/middleware"
185
+ });
186
+ updateConfig({ vite: { plugins: [wormholeVitePlugin({
187
+ registryPath: resolveRegistryPath(config.srcDir),
188
+ exclude: options.exclude ?? null,
189
+ pages: () => routeEntrypoints(root, resolvedRoutes)
190
+ })] } });
191
+ },
192
+ "astro:routes:resolved": ({ routes }) => {
193
+ resolvedRoutes = routes;
130
194
  },
131
195
  "astro:config:done": ({ config, injectTypes, logger }) => {
132
196
  const registryPath = resolveRegistryPath(config.srcDir);
@@ -154,7 +218,7 @@ const stubs = /* @__PURE__ */ new Map();
154
218
  function stub(name) {
155
219
  let wormhole = stubs.get(name);
156
220
  if (!wormhole) {
157
- wormhole = defineWormhole();
221
+ wormhole = createWormhole();
158
222
  assignWormholeNames({ [name]: wormhole });
159
223
  stubs.set(name, wormhole);
160
224
  }