@astroscope/wormhole 3.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
@@ -41,6 +41,7 @@ npm install @astroscope/wormhole
41
41
 
42
42
  ```ts
43
43
  // astro.config.ts
44
+ import { defineConfig } from 'astro/config';
44
45
  import node from '@astroscope/node';
45
46
  import wormhole from '@astroscope/wormhole';
46
47
 
@@ -187,7 +188,7 @@ Exported helper type mapping a registry shape to its value types. The recursivel
187
188
 
188
189
  ## How it works
189
190
 
190
- At build time the integration scans the code for `wormholes.<name>` reads: server modules are attributed to the routes whose page or endpoint reaches them, client modules to the emitted chunks. It also generates the types for the `wormholes` proxy from your registry. At runtime the middleware runs, in parallel, the handlers of everything the request's route can reach — frontmatter and endpoint reads, the islands the page hydrates (via `@astroscope/node`'s route map), its `<script>` blocks — 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. 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.
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.
191
192
 
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.
193
194
 
@@ -1 +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\nexport const als: AsyncLocalStorage<ReadonlyMap<string, unknown>> = ((globalThis as any)[\n Symbol.for('@astroscope/wormhole/als')\n] ??= new AsyncLocalStorage());\n"],"mappings":";;AAIA,MAAa,MAAwD,WACnE,OAAO,IAAI,0BAA0B,OACjC,IAAI,kBAAkB"}
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"}
@@ -73,11 +73,11 @@ function getWormholeSource(wormhole) {
73
73
  function assignWormholeNames(registry) {
74
74
  for (const [name, wormhole] of Object.entries(registry)) {
75
75
  const assign = wormhole[ASSIGN];
76
- 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`);
77
77
  assign(name);
78
78
  }
79
79
  }
80
80
  //#endregion
81
81
  export { getWormholeSource as i, createWormhole as n, defineWormhole as r, assignWormholeNames as t };
82
82
 
83
- //# sourceMappingURL=define-1UkPnsXV.js.map
83
+ //# sourceMappingURL=define-DAoqsUVA.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"define-1UkPnsXV.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 Error(\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,MACR,mBAAmB,KAAK,6EAC1B;EAGF,OAAO,IAAI;CACb;AACF"}
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,4 +1,4 @@
1
- import { a as WormholeMap, i as WormholeHandler, n as Wormhole, o as WormholeRegistry, r as WormholeDefinition, t as UnwrapWormholes } from "./types-DiWa8aQg.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
3
  export declare const wormholes: WormholeMap;
4
4
  export declare function defineWormhole(): never;
@@ -1 +1 @@
1
- {"version":3,"file":"index.browser.d.ts","names":[],"sources":["../src/index.browser.ts"],"mappings":";;qBAyEa,WAAW;wBAMR;wBAMA;wBAIQ"}
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 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\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":";;AAuBA,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,4 +1,4 @@
1
- import { a as WormholeMap, i as WormholeHandler, n as Wormhole, o as WormholeRegistry, r as WormholeDefinition, t as UnwrapWormholes } from "./types-DiWa8aQg.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
  import { ExcludePattern } from "@astroscope/node/excludes";
3
3
  import { AstroIntegration } from "astro";
4
4
  //#region src/integration.d.ts
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { n as createWormhole, r as defineWormhole, t as assignWormholeNames } from "./define-1UkPnsXV.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";
@@ -76,9 +76,9 @@ export { manifest };
76
76
  const client = this.environment.name === "client";
77
77
  const extensions = client ? SCAN_EXTENSIONS : SERVER_SCAN_EXTENSIONS;
78
78
  const file = extensions.some((ext) => id.endsWith(ext)) ? id : stripQuery(id);
79
- if (!extensions.some((ext) => file.endsWith(ext))) return;
79
+ if (extensions.every((ext) => !file.endsWith(ext))) return;
80
80
  if (!code.includes("@astroscope/wormhole")) return;
81
- const { scanWormholeAccess } = await import("./scan-CczEOWpD.js");
81
+ const { scanWormholeAccess } = await import("./scan-BDYo2M_0.js");
82
82
  const scan = scanWormholeAccess(code, file);
83
83
  if (scan) (client ? clientScans : serverScans).set(client ? id : stripQuery(id), scan);
84
84
  },
@@ -132,7 +132,7 @@ export { manifest };
132
132
  if (visited.has(fileName)) return;
133
133
  visited.add(fileName);
134
134
  chunkNamesByFileName.get(fileName)?.forEach((name) => names.add(name));
135
- chunkEdges.get(fileName)?.forEach(walk);
135
+ chunkEdges.get(fileName)?.forEach((imported) => walk(imported));
136
136
  };
137
137
  walk(entry);
138
138
  names.forEach((name) => scriptNames.add(name));
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/extraction/vite-plugin.ts","../src/integration.ts","../src/index.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\nimport { type ExcludePattern, serializeExcludePatterns } from '@astroscope/node/excludes';\nimport { collectRouteIslands, stripQuery } from '@astroscope/node/islands';\nimport type { Plugin } from 'vite';\nimport type { WormholeScan } from './scan.js';\nimport type { WormholeManifest } from './types.js';\n\nexport const WORMHOLE_VIRTUAL_MODULE_ID = 'virtual:@astroscope/wormhole/manifest';\nexport const REGISTRY_VIRTUAL_MODULE_ID = 'virtual:@astroscope/wormhole/registry';\nexport const CONFIG_VIRTUAL_MODULE_ID = 'virtual:@astroscope/wormhole/config';\n\nconst RESOLVED_VIRTUAL_MODULE_ID = `\\0${WORMHOLE_VIRTUAL_MODULE_ID}`;\nconst RESOLVED_REGISTRY_STUB_ID = `\\0${REGISTRY_VIRTUAL_MODULE_ID}`;\nconst RESOLVED_CONFIG_VIRTUAL_MODULE_ID = `\\0${CONFIG_VIRTUAL_MODULE_ID}`;\n\nconst MANIFEST_FILE_NAME = 'wormhole-manifest.json';\n\nconst SCAN_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx'];\n// compiled astro components are plain modules in the server build\nconst SERVER_SCAN_EXTENSIONS = [...SCAN_EXTENSIONS, '.astro'];\n\n/** chunk file name → manifest key, e.g. `_astro/Counter.abc.js` → `Counter.abc` */\nfunction chunkFileNameToName(fileName: string): string {\n return fileName.slice(fileName.lastIndexOf('/') + 1).replace(/\\.js$/, '');\n}\n\n/**\n * Build-time side of wormhole slicing: scans modules for `wormholes.<name>`\n * accesses — server modules are attributed to the routes whose page or endpoint\n * reaches them (what the middleware must load), client modules to the emitted\n * chunks (what each island receives) — and drops the manifest next to the server\n * chunks, where the virtual module reads it back at runtime, mirroring the islands\n * and i18n manifests. Dev needs none of this: without a manifest the middleware\n * loads and ships all wormholes with every page.\n */\nexport type WormholeVitePluginOptions = {\n /** absolute path of `src/wormholes.ts` (or `wormholes/index.ts`), null when the project has none */\n registryPath: string | null;\n /** the middleware's exclude patterns, null for the default */\n exclude: ExcludePattern[] | null;\n /** route modules (absolute ids) → the route patterns they serve, see `routeEntrypoints` of `@astroscope/node/islands` */\n pages: () => Map<string, string[]>;\n};\n\nexport function wormholeVitePlugin(options: WormholeVitePluginOptions): Plugin {\n let isBuild = false;\n const clientScans = new Map<string, WormholeScan>();\n const serverScans = new Map<string, WormholeScan>();\n // names read on the server per route pattern, and the routes reaching each server\n // module — carried from the server build to the client build, where `<script>`\n // entries are attributed to routes through the component file owning them\n const routeNames = new Map<string, Set<string>>();\n const patternsByFile = new Map<string, Set<string>>();\n\n return {\n name: '@astroscope/wormhole/extract',\n\n configResolved(config) {\n isBuild = config.command === 'build';\n },\n\n resolveId(id) {\n if (id === WORMHOLE_VIRTUAL_MODULE_ID) return RESOLVED_VIRTUAL_MODULE_ID;\n if (id === CONFIG_VIRTUAL_MODULE_ID) return RESOLVED_CONFIG_VIRTUAL_MODULE_ID;\n\n // the registry virtual resolves straight to the project's own module, so the\n // middleware and user imports share one instance; without a registry file it\n // falls back to an empty stub (the middleware then opens nothing)\n if (id === REGISTRY_VIRTUAL_MODULE_ID) return options.registryPath ?? RESOLVED_REGISTRY_STUB_ID;\n },\n\n load(id) {\n if (id === RESOLVED_REGISTRY_STUB_ID) {\n return 'export const wormholes = {};';\n }\n\n if (id === RESOLVED_CONFIG_VIRTUAL_MODULE_ID) {\n // exclude patterns may contain RegExp — serialized as code, not JSON\n return `export const exclude = ${options.exclude ? serializeExcludePatterns(options.exclude) : 'null'};`;\n }\n\n // the registry carries the handlers, and with them the project's server code\n if (id === options.registryPath && this.environment?.name === 'client') {\n throw new Error(\n `[@astroscope/wormhole] ${path.relative(process.cwd(), id)} is server-only — client code reads wormholes through the \\`wormholes\\` proxy from '@astroscope/wormhole'`,\n );\n }\n\n if (id !== RESOLVED_VIRTUAL_MODULE_ID) return;\n\n if (!isBuild) {\n return 'export const manifest = null;';\n }\n\n // the manifest only exists after the client build — read it at runtime. astro\n // bundles middleware at the server root and other code under chunks/, so\n // probe both relative to wherever this module ended up\n return `\nimport { readFileSync } from 'node:fs';\nimport { fileURLToPath } from 'node:url';\nimport { dirname, join } from 'node:path';\n\nconst dir = dirname(fileURLToPath(import.meta.url));\n\nlet manifest = null;\n\nfor (const candidate of [\n join(dir, '${MANIFEST_FILE_NAME}'),\n join(dir, 'chunks', '${MANIFEST_FILE_NAME}'),\n join(dir, '..', 'chunks', '${MANIFEST_FILE_NAME}'),\n]) {\n try {\n manifest = JSON.parse(readFileSync(candidate, 'utf-8'));\n break;\n } catch {\n manifest = null;\n }\n}\n\nexport { manifest };\n`;\n },\n\n async transform(code, id) {\n if (!isBuild || !this.environment) return;\n if (id.includes('node_modules')) return;\n\n const client = this.environment.name === 'client';\n const extensions = client ? SCAN_EXTENSIONS : SERVER_SCAN_EXTENSIONS;\n // astro `<script>` modules carry their language in the query (`?astro&type=script&…&lang.ts`),\n // other virtual variants (`?astroPropagatedAssets`) keep the file's own extension\n const file = extensions.some((ext) => id.endsWith(ext)) ? id : stripQuery(id);\n\n if (!extensions.some((ext) => file.endsWith(ext))) return;\n if (!code.includes('@astroscope/wormhole')) return;\n\n // dynamic import keeps the parser out of the SSR runtime bundle\n const { scanWormholeAccess } = await import('./scan.js');\n const scan = scanWormholeAccess(code, file);\n\n if (scan) {\n (client ? clientScans : serverScans).set(client ? id : stripQuery(id), scan);\n }\n },\n\n async generateBundle() {\n if (this.environment.name === 'client') return;\n\n const { routes, patternsByModule } = await collectRouteIslands(this, options.pages());\n\n for (const pattern of routes.keys()) {\n routeNames.set(pattern, routeNames.get(pattern) ?? new Set());\n }\n\n for (const [id, patterns] of patternsByModule) {\n const file = stripQuery(id);\n let owners = patternsByFile.get(file);\n\n if (!owners) {\n owners = new Set();\n patternsByFile.set(file, owners);\n }\n\n patterns.forEach((pattern) => owners.add(pattern));\n\n const scan = serverScans.get(file);\n\n if (!scan) continue;\n\n for (const pattern of patterns) {\n const names = routeNames.get(pattern)!;\n\n scan.names.forEach((name) => names.add(name));\n\n if (scan.dynamic) {\n names.add('*');\n }\n }\n }\n },\n\n writeBundle(outputOptions, bundle) {\n if (this.environment.name !== 'client' || !outputOptions.dir) return;\n\n const chunks: WormholeManifest['chunks'] = {};\n const chunkNamesByFileName = new Map<string, string[]>();\n const chunkEdges = new Map<string, string[]>();\n const scriptEntries: string[] = [];\n\n for (const chunk of Object.values(bundle)) {\n if (chunk.type !== 'chunk') continue;\n\n const names = new Set<string>();\n let dynamic = false;\n\n for (const moduleId of chunk.moduleIds) {\n const scan = clientScans.get(moduleId);\n\n scan?.names.forEach((name) => names.add(name));\n dynamic ||= scan?.dynamic ?? false;\n }\n\n const chunkNames = [...(dynamic ? ['*'] : []), ...names];\n\n if (chunkNames.length > 0) {\n chunks[chunkFileNameToName(chunk.fileName)] = chunkNames;\n }\n\n chunkNamesByFileName.set(chunk.fileName, chunkNames);\n chunkEdges.set(chunk.fileName, [...chunk.imports, ...chunk.dynamicImports]);\n\n // astro `<script>` entries are not islands, so no island emission covers\n // them — their reachable names ship at stream end instead\n if ((chunk.isEntry || chunk.isDynamicEntry) && chunk.facadeModuleId?.includes('astro&type=script')) {\n scriptEntries.push(chunk.fileName);\n }\n }\n\n const scriptNames = new Set<string>();\n\n for (const entry of scriptEntries) {\n const names = new Set<string>();\n const visited = new Set<string>();\n\n const walk = (fileName: string): void => {\n if (visited.has(fileName)) return;\n\n visited.add(fileName);\n chunkNamesByFileName.get(fileName)?.forEach((name) => names.add(name));\n chunkEdges.get(fileName)?.forEach(walk);\n };\n\n walk(entry);\n names.forEach((name) => scriptNames.add(name));\n\n // the script belongs to the routes reaching the component it sits in;\n // an owner the server build never saw means every route may need it\n const owner = bundle[entry]?.type === 'chunk' ? stripQuery(bundle[entry].facadeModuleId ?? '') : '';\n const owners = patternsByFile.get(owner) ?? routeNames.keys();\n\n for (const pattern of owners) {\n const route = routeNames.get(pattern);\n\n names.forEach((name) => route?.add(name));\n }\n }\n\n const routes: WormholeManifest['routes'] = {};\n\n for (const [pattern, names] of routeNames) {\n routes[pattern] = [...names];\n }\n\n const manifest: WormholeManifest = { chunks, scripts: [...scriptNames], routes };\n const chunksDir = path.resolve(outputOptions.dir, '..', 'server', 'chunks');\n\n if (fs.existsSync(chunksDir)) {\n fs.writeFileSync(path.join(chunksDir, MANIFEST_FILE_NAME), JSON.stringify(manifest));\n } else {\n console.error(\n `[@astroscope/wormhole] server chunks directory not found, cannot write manifest to ${chunksDir}`,\n );\n }\n },\n };\n}\n","import fs from 'node:fs';\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport type { ExcludePattern } from '@astroscope/node/excludes';\nimport { routeEntrypoints } from '@astroscope/node/islands';\nimport type { AstroIntegration, IntegrationResolvedRoute } from 'astro';\nimport { wormholeVitePlugin } from './extraction/vite-plugin.js';\n\nexport type WormholeIntegrationOptions = {\n /** patterns the middleware skips — no handler runs, nothing is delivered; defaults to RECOMMENDED_EXCLUDES */\n exclude?: ExcludePattern[] | undefined;\n};\n\n/**\n * Astro integration for wormholes with per-route loading and per-island delivery.\n *\n * Resolves the `src/wormholes.ts` registry and exposes it to the middleware as a\n * virtual module, injects the middleware after the project's own (so handlers see\n * everything earlier middleware put on `locals`), registers the vite plugin that\n * scans server and client code for `wormholes.<name>` accesses and maps them onto\n * routes and chunks, and generates the registry type stub that types the\n * `wormholes` proxy. Delivery happens at runtime: the middleware runs the handlers\n * the route needs, and the islands emitter (via `@astroscope/node`) writes each\n * island's slice before its tag.\n */\nexport default function wormholeIntegration(options: WormholeIntegrationOptions = {}): AstroIntegration {\n const resolveRegistryPath = (srcDir: URL): string | null =>\n ['wormholes.ts', path.join('wormholes', 'index.ts')]\n .map((candidate) => path.join(fileURLToPath(srcDir), candidate))\n .find((candidate) => fs.existsSync(candidate)) ?? null;\n\n let root = '';\n let resolvedRoutes: IntegrationResolvedRoute[] = [];\n\n return {\n name: '@astroscope/wormhole',\n hooks: {\n 'astro:config:setup': ({ config, updateConfig, addMiddleware }) => {\n root = fileURLToPath(config.root);\n\n addMiddleware({ order: 'post', entrypoint: '@astroscope/wormhole/middleware' });\n\n updateConfig({\n vite: {\n plugins: [\n wormholeVitePlugin({\n registryPath: resolveRegistryPath(config.srcDir),\n exclude: options.exclude ?? null,\n pages: () => routeEntrypoints(root, resolvedRoutes),\n }),\n ],\n },\n });\n },\n\n 'astro:routes:resolved': ({ routes }) => {\n resolvedRoutes = routes;\n },\n\n 'astro:config:done': ({ config, injectTypes, logger }) => {\n const registryPath = resolveRegistryPath(config.srcDir);\n\n if (!registryPath) {\n logger.warn('no src/wormholes.ts registry found — the wormholes proxy stays untyped');\n\n return;\n }\n\n // the specifier is relative to the stub's own directory, whose documented\n // location is `.astro/integrations/<normalized integration name>/` — the\n // url injectTypes returns verifies the assumption below, so a layout change\n // in astro fails loudly at sync time instead of silently untyping the proxy\n const stubFile = path.join(\n fileURLToPath(config.root),\n '.astro',\n 'integrations',\n '_astroscope_wormhole',\n 'registry.d.ts',\n );\n const relative = path.relative(path.dirname(stubFile), registryPath.replace(/([/\\\\]index)?\\.ts$/, ''));\n const specifier = relative.split(path.sep).join('/');\n\n const written = injectTypes({\n filename: 'registry.d.ts',\n content:\n `import type { UnwrapWormholes } from '@astroscope/wormhole';\\n\\n` +\n `declare module '@astroscope/wormhole' {\\n` +\n ` interface WormholeRegistry extends UnwrapWormholes<typeof import(${JSON.stringify(specifier)}).wormholes> {}\\n` +\n `}\\n`,\n });\n\n if (path.resolve(fileURLToPath(written)) !== path.resolve(stubFile)) {\n logger.error(\n `registry type stub landed at ${fileURLToPath(written)} instead of the expected ${stubFile} — ` +\n `its relative import is now wrong and the wormholes proxy is untyped; please report this to @astroscope/wormhole`,\n );\n }\n },\n },\n };\n}\n","import { assignWormholeNames, createWormhole } from './define.js';\nimport type { Wormhole, WormholeMap } from './types.js';\n\nexport { default } from './integration.js';\nexport type { WormholeIntegrationOptions } from './integration.js';\nexport { defineWormhole, assignWormholeNames } from './define.js';\nexport type {\n UnwrapWormholes,\n Wormhole,\n WormholeDefinition,\n WormholeHandler,\n WormholeMap,\n WormholeRegistry,\n} from './types.js';\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 wormhole = createWormhole();\n\n assignWormholeNames({ [name]: wormhole });\n stubs.set(name, wormhole);\n }\n\n return wormhole;\n}\n\n/**\n * Access wormholes by name — `wormholes.cart.get()`. This is the server side of the\n * proxy (the browser build ships its own): values come from the request's\n * wormhole middleware scope, so it works in frontmatter, endpoints, and\n * server-rendered islands alike. Typed via the registry type stub the integration\n * generates from `src/wormholes.ts`.\n */\nexport const wormholes: WormholeMap = new Proxy({} as WormholeMap, {\n get(_target, prop) {\n return typeof prop === 'string' ? stub(prop) : undefined;\n },\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/extraction/vite-plugin.ts","../src/integration.ts","../src/index.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\nimport { type ExcludePattern, serializeExcludePatterns } from '@astroscope/node/excludes';\nimport { collectRouteIslands, stripQuery } from '@astroscope/node/islands';\nimport type { Plugin } from 'vite';\nimport type { WormholeScan } from './scan.js';\nimport type { WormholeManifest } from './types.js';\n\nexport const WORMHOLE_VIRTUAL_MODULE_ID = 'virtual:@astroscope/wormhole/manifest';\nexport const REGISTRY_VIRTUAL_MODULE_ID = 'virtual:@astroscope/wormhole/registry';\nexport const CONFIG_VIRTUAL_MODULE_ID = 'virtual:@astroscope/wormhole/config';\n\nconst RESOLVED_VIRTUAL_MODULE_ID = `\\0${WORMHOLE_VIRTUAL_MODULE_ID}`;\nconst RESOLVED_REGISTRY_STUB_ID = `\\0${REGISTRY_VIRTUAL_MODULE_ID}`;\nconst RESOLVED_CONFIG_VIRTUAL_MODULE_ID = `\\0${CONFIG_VIRTUAL_MODULE_ID}`;\n\nconst MANIFEST_FILE_NAME = 'wormhole-manifest.json';\n\nconst SCAN_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx'];\n// compiled astro components are plain modules in the server build\nconst SERVER_SCAN_EXTENSIONS = [...SCAN_EXTENSIONS, '.astro'];\n\n/** chunk file name → manifest key, e.g. `_astro/Counter.abc.js` → `Counter.abc` */\nfunction chunkFileNameToName(fileName: string): string {\n return fileName.slice(fileName.lastIndexOf('/') + 1).replace(/\\.js$/, '');\n}\n\n/**\n * Build-time side of wormhole slicing: scans modules for `wormholes.<name>`\n * accesses — server modules are attributed to the routes whose page or endpoint\n * reaches them (what the middleware must load), client modules to the emitted\n * chunks (what each island receives) — and drops the manifest next to the server\n * chunks, where the virtual module reads it back at runtime, mirroring the islands\n * and i18n manifests. Dev needs none of this: without a manifest the middleware\n * loads and ships all wormholes with every page.\n */\nexport type WormholeVitePluginOptions = {\n /** absolute path of `src/wormholes.ts` (or `wormholes/index.ts`), null when the project has none */\n registryPath: string | null;\n /** the middleware's exclude patterns, null for the default */\n exclude: ExcludePattern[] | null;\n /**\n * route modules (absolute ids) → the route patterns they serve, see `routeEntrypoints`\n * of `@astroscope/node/islands`\n */\n pages: () => Map<string, string[]>;\n};\n\nexport function wormholeVitePlugin(options: WormholeVitePluginOptions): Plugin {\n let isBuild = false;\n const clientScans = new Map<string, WormholeScan>();\n const serverScans = new Map<string, WormholeScan>();\n // names read on the server per route pattern, and the routes reaching each server\n // module — carried from the server build to the client build, where `<script>`\n // entries are attributed to routes through the component file owning them\n const routeNames = new Map<string, Set<string>>();\n const patternsByFile = new Map<string, Set<string>>();\n\n return {\n name: '@astroscope/wormhole/extract',\n\n configResolved(config) {\n isBuild = config.command === 'build';\n },\n\n resolveId(id) {\n if (id === WORMHOLE_VIRTUAL_MODULE_ID) return RESOLVED_VIRTUAL_MODULE_ID;\n if (id === CONFIG_VIRTUAL_MODULE_ID) return RESOLVED_CONFIG_VIRTUAL_MODULE_ID;\n\n // the registry virtual resolves straight to the project's own module, so the\n // middleware and user imports share one instance; without a registry file it\n // falls back to an empty stub (the middleware then opens nothing)\n if (id === REGISTRY_VIRTUAL_MODULE_ID) return options.registryPath ?? RESOLVED_REGISTRY_STUB_ID;\n },\n\n load(id) {\n if (id === RESOLVED_REGISTRY_STUB_ID) {\n return 'export const wormholes = {};';\n }\n\n if (id === RESOLVED_CONFIG_VIRTUAL_MODULE_ID) {\n // exclude patterns may contain RegExp — serialized as code, not JSON\n return `export const exclude = ${options.exclude ? serializeExcludePatterns(options.exclude) : 'null'};`;\n }\n\n // the registry carries the handlers, and with them the project's server code\n // eslint-disable-next-line unicorn/no-this-outside-of-class -- `this` is the vite plugin context api\n if (id === options.registryPath && this.environment?.name === 'client') {\n throw new Error(\n `[@astroscope/wormhole] ${path.relative(process.cwd(), id)} is server-only — client code reads wormholes through the \\`wormholes\\` proxy from '@astroscope/wormhole'`,\n );\n }\n\n if (id !== RESOLVED_VIRTUAL_MODULE_ID) return;\n\n if (!isBuild) {\n return 'export const manifest = null;';\n }\n\n // the manifest only exists after the client build — read it at runtime. astro\n // bundles middleware at the server root and other code under chunks/, so\n // probe both relative to wherever this module ended up\n return `\nimport { readFileSync } from 'node:fs';\nimport { fileURLToPath } from 'node:url';\nimport { dirname, join } from 'node:path';\n\nconst dir = dirname(fileURLToPath(import.meta.url));\n\nlet manifest = null;\n\nfor (const candidate of [\n join(dir, '${MANIFEST_FILE_NAME}'),\n join(dir, 'chunks', '${MANIFEST_FILE_NAME}'),\n join(dir, '..', 'chunks', '${MANIFEST_FILE_NAME}'),\n]) {\n try {\n manifest = JSON.parse(readFileSync(candidate, 'utf-8'));\n break;\n } catch {\n manifest = null;\n }\n}\n\nexport { manifest };\n`;\n },\n\n async transform(code, id) {\n // eslint-disable-next-line unicorn/no-this-outside-of-class -- `this` is the vite plugin context api\n if (!isBuild || !this.environment) return;\n if (id.includes('node_modules')) return;\n\n // eslint-disable-next-line unicorn/no-this-outside-of-class -- `this` is the vite plugin context api\n const client = this.environment.name === 'client';\n const extensions = client ? SCAN_EXTENSIONS : SERVER_SCAN_EXTENSIONS;\n // astro `<script>` modules carry their language in the query (`?astro&type=script&…&lang.ts`),\n // other virtual variants (`?astroPropagatedAssets`) keep the file's own extension\n const file = extensions.some((ext) => id.endsWith(ext)) ? id : stripQuery(id);\n\n if (extensions.every((ext) => !file.endsWith(ext))) return;\n if (!code.includes('@astroscope/wormhole')) return;\n\n // dynamic import keeps the parser out of the SSR runtime bundle\n const { scanWormholeAccess } = await import('./scan.js');\n const scan = scanWormholeAccess(code, file);\n\n if (scan) {\n (client ? clientScans : serverScans).set(client ? id : stripQuery(id), scan);\n }\n },\n\n async generateBundle() {\n // eslint-disable-next-line unicorn/no-this-outside-of-class -- `this` is the vite plugin context api\n if (this.environment.name === 'client') return;\n\n // eslint-disable-next-line unicorn/no-this-outside-of-class -- `this` is the vite plugin context api\n const { routes, patternsByModule } = await collectRouteIslands(this, options.pages());\n\n for (const pattern of routes.keys()) {\n routeNames.set(pattern, routeNames.get(pattern) ?? new Set());\n }\n\n for (const [id, patterns] of patternsByModule) {\n const file = stripQuery(id);\n let owners = patternsByFile.get(file);\n\n if (!owners) {\n owners = new Set();\n patternsByFile.set(file, owners);\n }\n\n patterns.forEach((pattern) => owners.add(pattern));\n\n const scan = serverScans.get(file);\n\n if (!scan) continue;\n\n for (const pattern of patterns) {\n const names = routeNames.get(pattern)!;\n\n scan.names.forEach((name) => names.add(name));\n\n if (scan.dynamic) {\n names.add('*');\n }\n }\n }\n },\n\n writeBundle(outputOptions, bundle) {\n // eslint-disable-next-line unicorn/no-this-outside-of-class -- `this` is the vite plugin context api\n if (this.environment.name !== 'client' || !outputOptions.dir) return;\n\n const chunks: WormholeManifest['chunks'] = {};\n const chunkNamesByFileName = new Map<string, string[]>();\n const chunkEdges = new Map<string, string[]>();\n const scriptEntries: string[] = [];\n\n for (const chunk of Object.values(bundle)) {\n if (chunk.type !== 'chunk') continue;\n\n const names = new Set<string>();\n let dynamic = false;\n\n for (const moduleId of chunk.moduleIds) {\n const scan = clientScans.get(moduleId);\n\n scan?.names.forEach((name) => names.add(name));\n dynamic ||= scan?.dynamic ?? false;\n }\n\n const chunkNames = [...(dynamic ? ['*'] : []), ...names];\n\n if (chunkNames.length > 0) {\n chunks[chunkFileNameToName(chunk.fileName)] = chunkNames;\n }\n\n chunkNamesByFileName.set(chunk.fileName, chunkNames);\n chunkEdges.set(chunk.fileName, [...chunk.imports, ...chunk.dynamicImports]);\n\n // astro `<script>` entries are not islands, so no island emission covers\n // them — their reachable names ship at stream end instead\n if ((chunk.isEntry || chunk.isDynamicEntry) && chunk.facadeModuleId?.includes('astro&type=script')) {\n scriptEntries.push(chunk.fileName);\n }\n }\n\n const scriptNames = new Set<string>();\n\n for (const entry of scriptEntries) {\n const names = new Set<string>();\n const visited = new Set<string>();\n\n const walk = (fileName: string): void => {\n if (visited.has(fileName)) return;\n\n visited.add(fileName);\n chunkNamesByFileName.get(fileName)?.forEach((name) => names.add(name));\n chunkEdges.get(fileName)?.forEach((imported) => walk(imported));\n };\n\n walk(entry);\n names.forEach((name) => scriptNames.add(name));\n\n // the script belongs to the routes reaching the component it sits in;\n // an owner the server build never saw means every route may need it\n const owner = bundle[entry]?.type === 'chunk' ? stripQuery(bundle[entry].facadeModuleId ?? '') : '';\n const owners = patternsByFile.get(owner) ?? routeNames.keys();\n\n for (const pattern of owners) {\n const route = routeNames.get(pattern);\n\n names.forEach((name) => route?.add(name));\n }\n }\n\n const routes: WormholeManifest['routes'] = {};\n\n for (const [pattern, names] of routeNames) {\n routes[pattern] = [...names];\n }\n\n const manifest: WormholeManifest = { chunks, scripts: [...scriptNames], routes };\n const chunksDir = path.resolve(outputOptions.dir, '..', 'server', 'chunks');\n\n if (fs.existsSync(chunksDir)) {\n fs.writeFileSync(path.join(chunksDir, MANIFEST_FILE_NAME), JSON.stringify(manifest));\n } else {\n console.error(\n `[@astroscope/wormhole] server chunks directory not found, cannot write manifest to ${chunksDir}`,\n );\n }\n },\n };\n}\n","import fs from 'node:fs';\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport type { ExcludePattern } from '@astroscope/node/excludes';\nimport { routeEntrypoints } from '@astroscope/node/islands';\nimport type { AstroIntegration, IntegrationResolvedRoute } from 'astro';\nimport { wormholeVitePlugin } from './extraction/vite-plugin.js';\n\nexport type WormholeIntegrationOptions = {\n /** patterns the middleware skips — no handler runs, nothing is delivered; defaults to RECOMMENDED_EXCLUDES */\n exclude?: ExcludePattern[] | undefined;\n};\n\n/**\n * Astro integration for wormholes with per-route loading and per-island delivery.\n *\n * Resolves the `src/wormholes.ts` registry and exposes it to the middleware as a\n * virtual module, injects the middleware after the project's own (so handlers see\n * everything earlier middleware put on `locals`), registers the vite plugin that\n * scans server and client code for `wormholes.<name>` accesses and maps them onto\n * routes and chunks, and generates the registry type stub that types the\n * `wormholes` proxy. Delivery happens at runtime: the middleware runs the handlers\n * the route needs, and the islands emitter (via `@astroscope/node`) writes each\n * island's slice before its tag.\n */\nexport default function wormholeIntegration(options: WormholeIntegrationOptions = {}): AstroIntegration {\n const resolveRegistryPath = (srcDir: URL): string | null =>\n ['wormholes.ts', path.join('wormholes', 'index.ts')]\n .map((candidate) => path.join(fileURLToPath(srcDir), candidate))\n .find((candidate) => fs.existsSync(candidate)) ?? null;\n\n let root = '';\n let resolvedRoutes: IntegrationResolvedRoute[] = [];\n\n return {\n name: '@astroscope/wormhole',\n hooks: {\n 'astro:config:setup': ({ config, updateConfig, addMiddleware }) => {\n root = fileURLToPath(config.root);\n\n addMiddleware({ order: 'post', entrypoint: '@astroscope/wormhole/middleware' });\n\n updateConfig({\n vite: {\n plugins: [\n wormholeVitePlugin({\n registryPath: resolveRegistryPath(config.srcDir),\n exclude: options.exclude ?? null,\n pages: () => routeEntrypoints(root, resolvedRoutes),\n }),\n ],\n },\n });\n },\n\n 'astro:routes:resolved': ({ routes }) => {\n resolvedRoutes = routes;\n },\n\n 'astro:config:done': ({ config, injectTypes, logger }) => {\n const registryPath = resolveRegistryPath(config.srcDir);\n\n if (!registryPath) {\n logger.warn('no src/wormholes.ts registry found — the wormholes proxy stays untyped');\n\n return;\n }\n\n // the specifier is relative to the stub's own directory, whose documented\n // location is `.astro/integrations/<normalized integration name>/` — the\n // url injectTypes returns verifies the assumption below, so a layout change\n // in astro fails loudly at sync time instead of silently untyping the proxy\n const stubFile = path.join(\n fileURLToPath(config.root),\n '.astro',\n 'integrations',\n '_astroscope_wormhole',\n 'registry.d.ts',\n );\n const relative = path.relative(path.dirname(stubFile), registryPath.replace(/([/\\\\]index)?\\.ts$/, ''));\n const specifier = relative.split(path.sep).join('/');\n\n const written = injectTypes({\n filename: 'registry.d.ts',\n content:\n `import type { UnwrapWormholes } from '@astroscope/wormhole';\\n\\n` +\n `declare module '@astroscope/wormhole' {\\n` +\n ` interface WormholeRegistry extends UnwrapWormholes<typeof import(${JSON.stringify(specifier)}).wormholes> {}\\n` +\n `}\\n`,\n });\n\n if (path.resolve(fileURLToPath(written)) !== path.resolve(stubFile)) {\n logger.error(\n `registry type stub landed at ${fileURLToPath(written)} instead of the expected ${stubFile} — ` +\n `its relative import is now wrong and the wormholes proxy is untyped; please report this to @astroscope/wormhole`,\n );\n }\n },\n },\n };\n}\n","import { assignWormholeNames, createWormhole } from './define.js';\nimport type { Wormhole, WormholeMap } from './types.js';\n\nexport { default } from './integration.js';\nexport type { WormholeIntegrationOptions } from './integration.js';\nexport { defineWormhole, assignWormholeNames } from './define.js';\nexport type {\n UnwrapWormholes,\n Wormhole,\n WormholeDefinition,\n WormholeHandler,\n WormholeMap,\n WormholeRegistry,\n} from './types.js';\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 wormhole = createWormhole();\n\n assignWormholeNames({ [name]: wormhole });\n stubs.set(name, wormhole);\n }\n\n return wormhole;\n}\n\n/**\n * Access wormholes by name — `wormholes.cart.get()`. This is the server side of the\n * proxy (the browser build ships its own): values come from the request's\n * wormhole middleware scope, so it works in frontmatter, endpoints, and\n * server-rendered islands alike. Typed via the registry type stub the integration\n * generates from `src/wormholes.ts`.\n */\nexport const wormholes: WormholeMap = new Proxy({} as WormholeMap, {\n get(_target, prop) {\n return typeof prop === 'string' ? stub(prop) : undefined;\n },\n});\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { t as als } from "./als-CntRqWdO.js";
2
- import { i as getWormholeSource, t as assignWormholeNames } from "./define-1UkPnsXV.js";
2
+ import { i as getWormholeSource, t as assignWormholeNames } from "./define-DAoqsUVA.js";
3
3
  import { getRouteIslands, registerDocumentEmitter, registerIslandEmitter } from "@astroscope/node/islands";
4
4
  import { RECOMMENDED_EXCLUDES, shouldExclude } from "@astroscope/node/excludes";
5
5
  import { exclude } from "virtual:@astroscope/wormhole/config";
@@ -15,7 +15,7 @@ import { DURATION_BUCKETS, createCounter, createHistogram, errorType, withSpan }
15
15
  */
16
16
  /** values may contain `<\/script>` — escape `<` so they cannot end the tag */
17
17
  function jsonForScript(value) {
18
- return JSON.stringify(value).replace(/</g, "\\u003c");
18
+ return JSON.stringify(value).replaceAll("<", String.raw`\u003c`);
19
19
  }
20
20
  /**
21
21
  * Inline script writing entries onto `self.__wormholes__` and notifying listeners —
@@ -32,7 +32,10 @@ function getWormholeManifest() {
32
32
  }
33
33
  //#endregion
34
34
  //#region src/reachable.ts
35
- /** public url or dist-relative file name of a chunk → manifest chunk name, e.g. `…/_astro/Counter.abc.js` → `Counter.abc` */
35
+ /**
36
+ * public url or dist-relative file name of a chunk → manifest chunk name,
37
+ * e.g. `…/_astro/Counter.abc.js` → `Counter.abc`
38
+ */
36
39
  function chunkName(url) {
37
40
  return url.slice(url.lastIndexOf("/") + 1).replace(/\.js$/, "");
38
41
  }
@@ -42,9 +45,12 @@ function chunkName(url) {
42
45
  */
43
46
  function namesInClosure(closure, manifest) {
44
47
  const names = /* @__PURE__ */ new Set();
45
- for (const url of closure) for (const name of manifest.chunks[chunkName(url)] ?? []) {
46
- if (name === "*") return null;
47
- names.add(name);
48
+ for (const url of closure) {
49
+ const chunkReads = manifest.chunks[chunkName(url)] ?? [];
50
+ for (const name of chunkReads) {
51
+ if (name === "*") return null;
52
+ names.add(name);
53
+ }
48
54
  }
49
55
  return [...names];
50
56
  }
@@ -101,7 +107,7 @@ function createWormholeDocumentEmitter(dev = IS_DEV) {
101
107
  }
102
108
  return { head: createWormholeMergeScript(Object.fromEntries(request.values)) };
103
109
  }
104
- const names = manifest.scripts.includes("*") ? [...request.values.keys()] : manifest.scripts.filter((name) => request.values.has(name));
110
+ const names = manifest.scripts.includes("*") ? request.values.keys().toArray() : manifest.scripts.filter((name) => request.values.has(name));
105
111
  if (names.length === 0) return null;
106
112
  return { end: () => {
107
113
  const fresh = names.filter((name) => !request.emitted.has(name));
@@ -118,11 +124,11 @@ function registerWormholeEmitters() {
118
124
  if (!context) return null;
119
125
  const request = getRequestWormholes(context.request);
120
126
  const manifest = getWormholeManifest();
121
- if (!request || request.values.size === 0 || !manifest) return null;
122
- const reachable = reachableFor(island, manifest);
127
+ if (!request || !manifest || request.values.size === 0) return null;
128
+ const candidates = reachableFor(island, manifest) ?? request.values.keys();
123
129
  const entries = {};
124
130
  let count = 0;
125
- for (const name of reachable ?? request.values.keys()) {
131
+ for (const name of candidates) {
126
132
  if (!request.values.has(name) || request.emitted.has(name)) continue;
127
133
  request.emitted.add(name);
128
134
  entries[name] = request.values.get(name);
@@ -205,7 +211,7 @@ function createWormholeMiddleware(options = {}) {
205
211
  if (!source) throw new Error(`wormhole "${name}" has no handler — define it with defineWormhole({ handler: (ctx) => ... })`);
206
212
  sources.set(name, source);
207
213
  }
208
- const everything = [...sources.keys()];
214
+ const everything = sources.keys().toArray();
209
215
  const eager = everything.filter((name) => sources.get(name).eager);
210
216
  const entriesByRoute = /* @__PURE__ */ new Map();
211
217
  const routeNames = (pattern) => {
@@ -240,11 +246,11 @@ function createWormholeMiddleware(options = {}) {
240
246
  return maybeThen(maybeAll(entries.map((entry) => measureHandler(entry.name, () => entry.handler(ctx)))), (loaded) => {
241
247
  const open = /* @__PURE__ */ new Map();
242
248
  const scope = new Map(als.getStore());
243
- for (let i = 0; i < entries.length; i++) {
249
+ for (const [i, entry] of entries.entries()) {
244
250
  const data = loaded[i];
245
251
  if (data === void 0) continue;
246
- open.set(entries[i].name, data);
247
- scope.set(entries[i].key, data);
252
+ open.set(entry.name, data);
253
+ scope.set(entry.key, data);
248
254
  }
249
255
  setRequestWormholes(ctx.request, {
250
256
  values: open,
@@ -1 +1 @@
1
- {"version":3,"file":"middleware-entrypoint.js","names":["registry"],"sources":["../src/client-state.ts","../src/manifest.ts","../src/reachable.ts","../src/request-store.ts","../src/islands-emitter.ts","../src/telemetry.ts","../src/middleware.ts","../src/middleware-entrypoint.ts"],"sourcesContent":["/**\n * Script generation for wormhole delivery, shipped through the `@astroscope/node`\n * islands pipeline (see islands-emitter.ts for the placement policy).\n */\n\n/** values may contain `</script>` — escape `<` so they cannot end the tag */\nexport function jsonForScript(value: unknown): string {\n return JSON.stringify(value).replace(/</g, '\\\\u003c');\n}\n\n/**\n * Inline script writing entries onto `self.__wormholes__` and notifying listeners —\n * the counterpart of the browser proxy's entry protocol, safe in either order.\n */\nexport function createWormholeMergeScript(entries: Record<string, unknown>): string {\n return (\n `<script>{const s=self.__wormholes__??={},d=${jsonForScript(entries)};` +\n `for(const n in d){const e=s[n]??={};e.v=d[n];e.l&&e.l.forEach(f=>f(e.v))}}</script>`\n );\n}\n","import { manifest } from 'virtual:@astroscope/wormhole/manifest';\nimport type { WormholeManifest } from './extraction/types.js';\n\n/** null in dev and when the build produced no manifest (broken deploy → full delivery fallback) */\nexport function getWormholeManifest(): WormholeManifest | null {\n return manifest;\n}\n","import type { WormholeManifest } from './extraction/types.js';\n\n/** public url or dist-relative file name of a chunk → manifest chunk name, e.g. `…/_astro/Counter.abc.js` → `Counter.abc` */\nexport function chunkName(url: string): string {\n return url.slice(url.lastIndexOf('/') + 1).replace(/\\.js$/, '');\n}\n\n/**\n * The wormhole names the chunks of a closure can reach; `null` when one of them\n * reads dynamically (`*` — all open wormholes).\n */\nexport function namesInClosure(closure: Iterable<string>, manifest: WormholeManifest): string[] | null {\n const names = new Set<string>();\n\n for (const url of closure) {\n for (const name of manifest.chunks[chunkName(url)] ?? []) {\n if (name === '*') {\n return null;\n }\n\n names.add(name);\n }\n }\n\n return [...names];\n}\n","/**\n * Per-request wormhole values for the islands emitter and the stream-end script —\n * both run while the response streams, outside the middleware's ALS scope, so the\n * request object is the only stable handle (same pattern as the i18n locale).\n */\nexport type RequestWormholes = {\n /** wormhole name → serializable value, as resolved by the middleware */\n values: ReadonlyMap<string, unknown>;\n /** names already written into the document by the islands emitter */\n emitted: Set<string>;\n};\n\nconst requestWormholes = new WeakMap<Request, RequestWormholes>();\n\nexport function setRequestWormholes(request: Request, wormholes: RequestWormholes): void {\n requestWormholes.set(request, wormholes);\n}\n\nexport function getRequestWormholes(request: Request): RequestWormholes | undefined {\n return requestWormholes.get(request);\n}\n","import {\n type DocumentEmitter,\n type IslandInfo,\n registerDocumentEmitter,\n registerIslandEmitter,\n} from '@astroscope/node/islands';\nimport { log } from '@astroscope/node/log';\nimport { createWormholeMergeScript } from './client-state.js';\nimport type { WormholeManifest } from './extraction/types.js';\nimport { getWormholeManifest } from './manifest.js';\nimport { namesInClosure } from './reachable.js';\nimport { getRequestWormholes } from './request-store.js';\n\n/**\n * Contributes each island's wormhole slice: an inline merge script with the values\n * of every open wormhole the island's full chunk closure (dynamic imports included)\n * can reach, emitted before the island tag — parsed strictly before the island\n * connects, even mid-stream. Names already written earlier in the document are\n * skipped; a `*` chunk (dynamic proxy access) degrades to all open wormholes.\n */\n\n// vite defines import.meta.env when bundling the middleware; typed structurally\n// so the package compiles without vite's global client types\nconst IS_DEV = !!(import.meta as { env?: { DEV?: boolean } }).env?.DEV;\n\nlet registered = false;\nlet warnedMissingManifest = false;\n\n/** the wormhole names an island's full closure can reach; `null` when a chunk reads dynamically (all open) */\ntype Reachable = string[] | null;\n\n// islands arrive as the same object per component/renderer/directive and the\n// manifest is loaded once, so the intersection is computed once per island\nconst reachableByManifest = new WeakMap<WormholeManifest, WeakMap<IslandInfo, Reachable>>();\n\nfunction reachableFor(island: IslandInfo, manifest: WormholeManifest): Reachable {\n let byIsland = reachableByManifest.get(manifest);\n\n if (!byIsland) {\n byIsland = new WeakMap();\n reachableByManifest.set(manifest, byIsland);\n }\n\n const cached = byIsland.get(island);\n\n if (cached !== undefined) {\n return cached;\n }\n\n const reachable = namesInClosure(island.fullClosure, manifest);\n\n byIsland.set(island, reachable);\n\n return reachable;\n}\n\n/**\n * The document-level wormhole delivery, exported for tests: everything open as\n * `head` content in dev (or without a manifest), script-entry names as `end`\n * content in prod — running after the rewriter, names the islands emitter\n * already wrote are skipped race-free.\n */\nexport function createWormholeDocumentEmitter(dev: boolean = IS_DEV): DocumentEmitter {\n return (context) => {\n const request = getRequestWormholes(context.request);\n\n if (!request || request.values.size === 0) {\n return null;\n }\n\n if (dev) {\n return { head: createWormholeMergeScript(Object.fromEntries(request.values)) };\n }\n\n const manifest = getWormholeManifest();\n\n if (!manifest) {\n if (!warnedMissingManifest) {\n warnedMissingManifest = true;\n log.error(\n 'wormhole manifest not found — delivering all open wormholes with every page (is the integration configured?)',\n );\n }\n\n return { head: createWormholeMergeScript(Object.fromEntries(request.values)) };\n }\n\n const names = manifest.scripts.includes('*')\n ? [...request.values.keys()]\n : manifest.scripts.filter((name) => request.values.has(name));\n\n if (names.length === 0) {\n return null;\n }\n\n return {\n end: () => {\n const fresh = names.filter((name) => !request.emitted.has(name));\n\n if (fresh.length === 0) {\n return null;\n }\n\n return createWormholeMergeScript(Object.fromEntries(fresh.map((name) => [name, request.values.get(name)])));\n },\n };\n };\n}\n\nexport function registerWormholeEmitters(): void {\n if (registered) {\n return;\n }\n\n registered = true;\n\n registerDocumentEmitter(createWormholeDocumentEmitter());\n\n registerIslandEmitter((island, context) => {\n // no request context (prerendered build pass) — values are per-request, nothing to emit\n if (!context) {\n return null;\n }\n\n const request = getRequestWormholes(context.request);\n const manifest = getWormholeManifest();\n\n if (!request || request.values.size === 0 || !manifest) {\n return null;\n }\n\n const reachable = reachableFor(island, manifest);\n const entries: Record<string, unknown> = {};\n let count = 0;\n\n for (const name of reachable ?? request.values.keys()) {\n if (!request.values.has(name) || request.emitted.has(name)) {\n continue;\n }\n\n request.emitted.add(name);\n entries[name] = request.values.get(name);\n count++;\n }\n\n if (count === 0) {\n return null;\n }\n\n return { html: createWormholeMergeScript(entries) };\n });\n}\n","import { log } from '@astroscope/node/log';\nimport { DURATION_BUCKETS, createCounter, createHistogram, errorType, withSpan } from '@astroscope/node/telemetry';\nimport { type MaybePromise, maybeThen } from '@entwico/dash';\n\nconst SCOPE = '@astroscope/wormhole';\n\nconst handlerDuration = createHistogram('astro.wormhole.handler.duration', {\n scope: SCOPE,\n description: 'Wormhole handler execution per request',\n unit: 's',\n buckets: DURATION_BUCKETS.io,\n});\n\nconst handlerFailures = createCounter('astro.wormhole.handler.failures', {\n scope: SCOPE,\n description: 'Wormhole handlers that threw',\n unit: '{handler}',\n});\n\n/**\n * Runs one handler under its own span (child of the request span — the\n * handlers run in parallel, so the trace shows which one holds the page's\n * first byte), records its duration and counts and logs a failure. A\n * synchronous handler stays synchronous.\n */\nexport function measureHandler<T>(name: string, run: () => MaybePromise<T>): MaybePromise<T> {\n const attributes = { 'astro.wormhole.name': name };\n const started = performance.now();\n\n const fail = (error: unknown): never => {\n handlerFailures.add(1, { ...attributes, 'error.type': errorType(error) });\n log.error(\n error instanceof Error ? { err: error, wormhole: name } : { reason: error, wormhole: name },\n 'wormhole handler failed',\n );\n\n throw error;\n };\n\n return withSpan(`wormhole ${name}`, { scope: SCOPE, attributes }, () => {\n let result: MaybePromise<T>;\n\n try {\n result = run();\n } catch (error) {\n return fail(error);\n }\n\n return maybeThen(\n result,\n (value) => {\n handlerDuration.record((performance.now() - started) / 1000, attributes);\n\n return value;\n },\n fail,\n );\n });\n}\n","import { type ExcludePattern, RECOMMENDED_EXCLUDES, shouldExclude } from '@astroscope/node/excludes';\nimport { getRouteIslands } from '@astroscope/node/islands';\nimport { maybeAll, maybeThen } from '@entwico/dash';\nimport type { MiddlewareHandler } from 'astro';\nimport { wormholes as registry } from 'virtual:@astroscope/wormhole/registry';\nimport { als } from './als.js';\nimport { type WormholeSource, assignWormholeNames, getWormholeSource } from './define.js';\nimport { registerWormholeEmitters } from './islands-emitter.js';\nimport { getWormholeManifest } from './manifest.js';\nimport { namesInClosure } from './reachable.js';\nimport { setRequestWormholes } from './request-store.js';\nimport { measureHandler } from './telemetry.js';\nimport type { WormholeHandler } from './types.js';\n\nexport type WormholeMiddlewareOptions = {\n /** patterns to exclude from wormhole loading; defaults to RECOMMENDED_EXCLUDES */\n exclude?: ExcludePattern[] | undefined;\n};\n\n/**\n * The wormhole middleware, injected by the integration after the project's own\n * middleware: runs the handlers of the wormholes the request's route can read —\n * server and `<script>` reads from the manifest, island reads through the route's\n * islands from `@astroscope/node`, and eager wormholes — makes the values\n * readable on the server for the duration of the request, and hands them to the\n * islands pipeline for delivery. Routes the build could not attribute (dev, no\n * manifest, astro's own routes) load everything.\n */\nexport function createWormholeMiddleware(options: WormholeMiddlewareOptions = {}): MiddlewareHandler {\n assignWormholeNames(registry);\n registerWormholeEmitters();\n\n const sources = new Map<string, WormholeSource>();\n\n for (const [name, wormhole] of Object.entries(registry)) {\n const source = getWormholeSource(wormhole);\n\n if (!source) {\n throw new Error(`wormhole \"${name}\" has no handler — define it with defineWormhole({ handler: (ctx) => ... })`);\n }\n\n sources.set(name, source);\n }\n\n const everything = [...sources.keys()];\n const eager = everything.filter((name) => sources.get(name)!.eager);\n\n // what a route opens, resolved once per route: the name, the handler and the ALS key\n type Entry = { name: string; key: string; handler: WormholeHandler<unknown> };\n\n const entriesByRoute = new Map<string, Entry[]>();\n\n // null: the route's readers are unknown, load everything\n const routeNames = (pattern: string): string[] | null => {\n const manifest = getWormholeManifest();\n\n if (!manifest) {\n return null;\n }\n\n const server = manifest.routes[pattern];\n const islands = getRouteIslands(pattern);\n\n if (!server || !islands || server.includes('*')) {\n return null;\n }\n\n const names = new Set([...eager, ...server]);\n\n for (const island of islands) {\n const reachable = namesInClosure(island.fullClosure, manifest);\n\n if (!reachable) {\n return null;\n }\n\n reachable.forEach((name) => names.add(name));\n }\n\n return [...names].filter((name) => sources.has(name));\n };\n\n const entriesFor = (pattern: string): Entry[] => {\n let entries = entriesByRoute.get(pattern);\n\n if (!entries) {\n entries = (routeNames(pattern) ?? everything).map((name) => ({\n name,\n key: registry[name]!.key,\n handler: sources.get(name)!.handler,\n }));\n entriesByRoute.set(pattern, entries);\n }\n\n return entries;\n };\n\n return (ctx, next) => {\n if (shouldExclude(ctx, options.exclude ?? RECOMMENDED_EXCLUDES)) {\n return next();\n }\n\n const entries = entriesFor(ctx.routePattern);\n\n // synchronous handlers — the common case — open the scope without a promise\n // allocation or a microtask hop; async ones settle together\n return maybeThen(\n maybeAll(entries.map((entry) => measureHandler(entry.name, () => entry.handler(ctx)))),\n (loaded) => {\n const open = new Map<string, unknown>();\n const scope = new Map<string, unknown>(als.getStore());\n\n for (let i = 0; i < entries.length; i++) {\n const data = loaded[i];\n\n if (data === undefined) {\n continue;\n }\n\n open.set(entries[i]!.name, data);\n scope.set(entries[i]!.key, data);\n }\n\n // the islands emitter and the stream-end script run while the response streams,\n // outside this ALS scope — the request object carries the values to them\n setRequestWormholes(ctx.request, { values: open, emitted: new Set() });\n\n return als.run(scope, () => next());\n },\n );\n };\n}\n","import { exclude } from 'virtual:@astroscope/wormhole/config';\nimport { createWormholeMiddleware } from './middleware.js';\n\nexport const onRequest = createWormholeMiddleware({ exclude: exclude ?? undefined });\n"],"mappings":"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"}
1
+ {"version":3,"file":"middleware-entrypoint.js","names":["registry"],"sources":["../src/client-state.ts","../src/manifest.ts","../src/reachable.ts","../src/request-store.ts","../src/islands-emitter.ts","../src/telemetry.ts","../src/middleware.ts","../src/middleware-entrypoint.ts"],"sourcesContent":["/**\n * Script generation for wormhole delivery, shipped through the `@astroscope/node`\n * islands pipeline (see islands-emitter.ts for the placement policy).\n */\n\n/** values may contain `</script>` — escape `<` so they cannot end the tag */\nexport function jsonForScript(value: unknown): string {\n return JSON.stringify(value).replaceAll('<', String.raw`\\u003c`);\n}\n\n/**\n * Inline script writing entries onto `self.__wormholes__` and notifying listeners —\n * the counterpart of the browser proxy's entry protocol, safe in either order.\n */\nexport function createWormholeMergeScript(entries: Record<string, unknown>): string {\n return (\n `<script>{const s=self.__wormholes__??={},d=${jsonForScript(entries)};` +\n `for(const n in d){const e=s[n]??={};e.v=d[n];e.l&&e.l.forEach(f=>f(e.v))}}</script>`\n );\n}\n","import { manifest } from 'virtual:@astroscope/wormhole/manifest';\nimport type { WormholeManifest } from './extraction/types.js';\n\n/** null in dev and when the build produced no manifest (broken deploy → full delivery fallback) */\nexport function getWormholeManifest(): WormholeManifest | null {\n return manifest;\n}\n","import type { WormholeManifest } from './extraction/types.js';\n\n/**\n * public url or dist-relative file name of a chunk → manifest chunk name,\n * e.g. `…/_astro/Counter.abc.js` → `Counter.abc`\n */\nexport function chunkName(url: string): string {\n return url.slice(url.lastIndexOf('/') + 1).replace(/\\.js$/, '');\n}\n\n/**\n * The wormhole names the chunks of a closure can reach; `null` when one of them\n * reads dynamically (`*` — all open wormholes).\n */\nexport function namesInClosure(closure: Iterable<string>, manifest: WormholeManifest): string[] | null {\n const names = new Set<string>();\n\n for (const url of closure) {\n const chunkReads = manifest.chunks[chunkName(url)] ?? [];\n\n for (const name of chunkReads) {\n if (name === '*') {\n return null;\n }\n\n names.add(name);\n }\n }\n\n return [...names];\n}\n","/**\n * Per-request wormhole values for the islands emitter and the stream-end script —\n * both run while the response streams, outside the middleware's ALS scope, so the\n * request object is the only stable handle (same pattern as the i18n locale).\n */\nexport type RequestWormholes = {\n /** wormhole name → serializable value, as resolved by the middleware */\n values: ReadonlyMap<string, unknown>;\n /** names already written into the document by the islands emitter */\n emitted: Set<string>;\n};\n\nconst requestWormholes = new WeakMap<Request, RequestWormholes>();\n\nexport function setRequestWormholes(request: Request, wormholes: RequestWormholes): void {\n requestWormholes.set(request, wormholes);\n}\n\nexport function getRequestWormholes(request: Request): RequestWormholes | undefined {\n return requestWormholes.get(request);\n}\n","import {\n type DocumentEmitter,\n type IslandInfo,\n registerDocumentEmitter,\n registerIslandEmitter,\n} from '@astroscope/node/islands';\nimport { log } from '@astroscope/node/log';\nimport { createWormholeMergeScript } from './client-state.js';\nimport type { WormholeManifest } from './extraction/types.js';\nimport { getWormholeManifest } from './manifest.js';\nimport { namesInClosure } from './reachable.js';\nimport { getRequestWormholes } from './request-store.js';\n\n/**\n * Contributes each island's wormhole slice: an inline merge script with the values\n * of every open wormhole the island's full chunk closure (dynamic imports included)\n * can reach, emitted before the island tag — parsed strictly before the island\n * connects, even mid-stream. Names already written earlier in the document are\n * skipped; a `*` chunk (dynamic proxy access) degrades to all open wormholes.\n */\n\n// vite defines import.meta.env when bundling the middleware; typed structurally\n// so the package compiles without vite's global client types\nconst IS_DEV = !!(import.meta as { env?: { DEV?: boolean } }).env?.DEV;\n\nlet registered = false;\nlet warnedMissingManifest = false;\n\n/** the wormhole names an island's full closure can reach; `null` when a chunk reads dynamically (all open) */\ntype Reachable = string[] | null;\n\n// islands arrive as the same object per component/renderer/directive and the\n// manifest is loaded once, so the intersection is computed once per island\nconst reachableByManifest = new WeakMap<WormholeManifest, WeakMap<IslandInfo, Reachable>>();\n\nfunction reachableFor(island: IslandInfo, manifest: WormholeManifest): Reachable {\n let byIsland = reachableByManifest.get(manifest);\n\n if (!byIsland) {\n byIsland = new WeakMap();\n reachableByManifest.set(manifest, byIsland);\n }\n\n const cached = byIsland.get(island);\n\n if (cached !== undefined) {\n return cached;\n }\n\n const reachable = namesInClosure(island.fullClosure, manifest);\n\n byIsland.set(island, reachable);\n\n return reachable;\n}\n\n/**\n * The document-level wormhole delivery, exported for tests: everything open as\n * `head` content in dev (or without a manifest), script-entry names as `end`\n * content in prod — running after the rewriter, names the islands emitter\n * already wrote are skipped race-free.\n */\nexport function createWormholeDocumentEmitter(dev: boolean = IS_DEV): DocumentEmitter {\n return (context) => {\n const request = getRequestWormholes(context.request);\n\n if (!request || request.values.size === 0) {\n return null;\n }\n\n if (dev) {\n return { head: createWormholeMergeScript(Object.fromEntries(request.values)) };\n }\n\n const manifest = getWormholeManifest();\n\n if (!manifest) {\n if (!warnedMissingManifest) {\n warnedMissingManifest = true;\n log.error(\n 'wormhole manifest not found — delivering all open wormholes with every page (is the integration configured?)',\n );\n }\n\n return { head: createWormholeMergeScript(Object.fromEntries(request.values)) };\n }\n\n const names = manifest.scripts.includes('*')\n ? request.values.keys().toArray()\n : manifest.scripts.filter((name) => request.values.has(name));\n\n if (names.length === 0) {\n return null;\n }\n\n return {\n end: () => {\n const fresh = names.filter((name) => !request.emitted.has(name));\n\n if (fresh.length === 0) {\n return null;\n }\n\n return createWormholeMergeScript(Object.fromEntries(fresh.map((name) => [name, request.values.get(name)])));\n },\n };\n };\n}\n\nexport function registerWormholeEmitters(): void {\n if (registered) {\n return;\n }\n\n registered = true;\n\n registerDocumentEmitter(createWormholeDocumentEmitter());\n\n registerIslandEmitter((island, context) => {\n // no request context (prerendered build pass) — values are per-request, nothing to emit\n if (!context) {\n return null;\n }\n\n const request = getRequestWormholes(context.request);\n const manifest = getWormholeManifest();\n\n if (!request || !manifest || request.values.size === 0) {\n return null;\n }\n\n const candidates = reachableFor(island, manifest) ?? request.values.keys();\n const entries: Record<string, unknown> = {};\n let count = 0;\n\n for (const name of candidates) {\n if (!request.values.has(name) || request.emitted.has(name)) {\n continue;\n }\n\n request.emitted.add(name);\n entries[name] = request.values.get(name);\n count++;\n }\n\n if (count === 0) {\n return null;\n }\n\n return { html: createWormholeMergeScript(entries) };\n });\n}\n","import { log } from '@astroscope/node/log';\nimport { DURATION_BUCKETS, createCounter, createHistogram, errorType, withSpan } from '@astroscope/node/telemetry';\nimport { type MaybePromise, maybeThen } from '@entwico/dash';\n\nconst SCOPE = '@astroscope/wormhole';\n\nconst handlerDuration = createHistogram('astro.wormhole.handler.duration', {\n scope: SCOPE,\n description: 'Wormhole handler execution per request',\n unit: 's',\n buckets: DURATION_BUCKETS.io,\n});\n\nconst handlerFailures = createCounter('astro.wormhole.handler.failures', {\n scope: SCOPE,\n description: 'Wormhole handlers that threw',\n unit: '{handler}',\n});\n\n/**\n * Runs one handler under its own span (child of the request span — the\n * handlers run in parallel, so the trace shows which one holds the page's\n * first byte), records its duration and counts and logs a failure. A\n * synchronous handler stays synchronous.\n */\nexport function measureHandler<T>(name: string, run: () => MaybePromise<T>): MaybePromise<T> {\n const attributes = { 'astro.wormhole.name': name };\n const started = performance.now();\n\n const fail = (error: unknown): never => {\n handlerFailures.add(1, { ...attributes, 'error.type': errorType(error) });\n log.error(\n error instanceof Error ? { err: error, wormhole: name } : { reason: error, wormhole: name },\n 'wormhole handler failed',\n );\n\n throw error;\n };\n\n return withSpan(`wormhole ${name}`, { scope: SCOPE, attributes }, () => {\n let result: MaybePromise<T>;\n\n try {\n result = run();\n } catch (error) {\n return fail(error);\n }\n\n return maybeThen(\n result,\n (value) => {\n handlerDuration.record((performance.now() - started) / 1000, attributes);\n\n return value;\n },\n fail,\n );\n });\n}\n","import { type ExcludePattern, RECOMMENDED_EXCLUDES, shouldExclude } from '@astroscope/node/excludes';\nimport { getRouteIslands } from '@astroscope/node/islands';\nimport { maybeAll, maybeThen } from '@entwico/dash';\nimport type { MiddlewareHandler } from 'astro';\nimport { wormholes as registry } from 'virtual:@astroscope/wormhole/registry';\nimport { als } from './als.js';\nimport { type WormholeSource, assignWormholeNames, getWormholeSource } from './define.js';\nimport { registerWormholeEmitters } from './islands-emitter.js';\nimport { getWormholeManifest } from './manifest.js';\nimport { namesInClosure } from './reachable.js';\nimport { setRequestWormholes } from './request-store.js';\nimport { measureHandler } from './telemetry.js';\nimport type { WormholeHandler } from './types.js';\n\nexport type WormholeMiddlewareOptions = {\n /** patterns to exclude from wormhole loading; defaults to RECOMMENDED_EXCLUDES */\n exclude?: ExcludePattern[] | undefined;\n};\n\n/**\n * The wormhole middleware, injected by the integration after the project's own\n * middleware: runs the handlers of the wormholes the request's route can read —\n * server and `<script>` reads from the manifest, island reads through the route's\n * islands from `@astroscope/node`, and eager wormholes — makes the values\n * readable on the server for the duration of the request, and hands them to the\n * islands pipeline for delivery. Routes the build could not attribute (dev, no\n * manifest, astro's own routes) load everything.\n */\nexport function createWormholeMiddleware(options: WormholeMiddlewareOptions = {}): MiddlewareHandler {\n assignWormholeNames(registry);\n registerWormholeEmitters();\n\n const sources = new Map<string, WormholeSource>();\n\n for (const [name, wormhole] of Object.entries(registry)) {\n const source = getWormholeSource(wormhole);\n\n if (!source) {\n throw new Error(`wormhole \"${name}\" has no handler — define it with defineWormhole({ handler: (ctx) => ... })`);\n }\n\n sources.set(name, source);\n }\n\n const everything = sources.keys().toArray();\n const eager = everything.filter((name) => sources.get(name)!.eager);\n\n // what a route opens, resolved once per route: the name, the handler and the ALS key\n type Entry = { name: string; key: string; handler: WormholeHandler<unknown> };\n\n const entriesByRoute = new Map<string, Entry[]>();\n\n // null: the route's readers are unknown, load everything\n const routeNames = (pattern: string): string[] | null => {\n const manifest = getWormholeManifest();\n\n if (!manifest) {\n return null;\n }\n\n const server = manifest.routes[pattern];\n const islands = getRouteIslands(pattern);\n\n if (!server || !islands || server.includes('*')) {\n return null;\n }\n\n const names = new Set([...eager, ...server]);\n\n for (const island of islands) {\n const reachable = namesInClosure(island.fullClosure, manifest);\n\n if (!reachable) {\n return null;\n }\n\n reachable.forEach((name) => names.add(name));\n }\n\n return [...names].filter((name) => sources.has(name));\n };\n\n const entriesFor = (pattern: string): Entry[] => {\n let entries = entriesByRoute.get(pattern);\n\n if (!entries) {\n entries = (routeNames(pattern) ?? everything).map((name) => ({\n name,\n key: registry[name]!.key,\n handler: sources.get(name)!.handler,\n }));\n entriesByRoute.set(pattern, entries);\n }\n\n return entries;\n };\n\n return (ctx, next) => {\n if (shouldExclude(ctx, options.exclude ?? RECOMMENDED_EXCLUDES)) {\n return next();\n }\n\n const entries = entriesFor(ctx.routePattern);\n\n // synchronous handlers — the common case — open the scope without a promise\n // allocation or a microtask hop; async ones settle together\n return maybeThen(\n maybeAll(entries.map((entry) => measureHandler(entry.name, () => entry.handler(ctx)))),\n (loaded) => {\n const open = new Map<string, unknown>();\n const scope = new Map<string, unknown>(als.getStore());\n\n for (const [i, entry] of entries.entries()) {\n const data = loaded[i];\n\n if (data === undefined) {\n continue;\n }\n\n open.set(entry!.name, data);\n scope.set(entry!.key, data);\n }\n\n // the islands emitter and the stream-end script run while the response streams,\n // outside this ALS scope — the request object carries the values to them\n setRequestWormholes(ctx.request, { values: open, emitted: new Set() });\n\n return als.run(scope, () => next());\n },\n );\n };\n}\n","import { exclude } from 'virtual:@astroscope/wormhole/config';\nimport { createWormholeMiddleware } from './middleware.js';\n\nexport const onRequest = createWormholeMiddleware({ exclude: exclude ?? undefined });\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { n as Wormhole } from "../types-DiWa8aQg.js";
1
+ import { n as Wormhole } from "../types-v_ftArqH.js";
2
2
  import { ReadonlyDeep } from "@entwico/dash";
3
3
  //#region src/react/index.d.ts
4
4
  export declare function useWormhole<T>(wormhole: Wormhole<T>): ReadonlyDeep<T>;
@@ -48,10 +48,12 @@ function scanWormholeAccess(code, id) {
48
48
  names: /* @__PURE__ */ new Set(),
49
49
  dynamic: false
50
50
  };
51
- for (const statement of program["body"]) {
51
+ const body = program["body"];
52
+ for (const statement of body) {
52
53
  if (statement["source"]?.["value"] !== PACKAGE_NAME) continue;
53
54
  if (statement.type === "ImportDeclaration") {
54
- for (const specifier of statement["specifiers"]) if (specifier.type === "ImportSpecifier") {
55
+ const specifiers = statement["specifiers"];
56
+ for (const specifier of specifiers) if (specifier.type === "ImportSpecifier") {
55
57
  const imported = specifier["imported"];
56
58
  if ((imported.type === "Identifier" ? imported["name"] : imported["value"]) === "wormholes") locals.add(specifier["local"]["name"]);
57
59
  } else if (specifier.type === "ImportNamespaceSpecifier") scan.dynamic = true;
@@ -59,9 +61,9 @@ function scanWormholeAccess(code, id) {
59
61
  }
60
62
  if (locals.size === 0 && !scan.dynamic) return null;
61
63
  walk(program, (node, parent, key) => {
62
- if (node.type !== "Identifier" || !locals.has(node["name"]) || !parent) return;
64
+ if (!parent || node.type !== "Identifier" || !locals.has(node["name"])) return;
63
65
  if (parent.type === "ImportSpecifier") return;
64
- if (isMember(parent) && key === "object") {
66
+ if (key === "object" && isMember(parent)) {
65
67
  const property = parent["property"];
66
68
  if (!parent["computed"] && property.type === "Identifier") scan.names.add(property["name"]);
67
69
  else if (parent["computed"] && property.type === "StringLiteral") scan.names.add(property["value"]);
@@ -76,4 +78,4 @@ function scanWormholeAccess(code, id) {
76
78
  //#endregion
77
79
  export { scanWormholeAccess };
78
80
 
79
- //# sourceMappingURL=scan-CczEOWpD.js.map
81
+ //# sourceMappingURL=scan-BDYo2M_0.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scan-BDYo2M_0.js","names":[],"sources":["../src/extraction/scan.ts"],"sourcesContent":["import { parse } from '@babel/parser';\n\nconst PACKAGE_NAME = '@astroscope/wormhole';\n\n/**\n * Which wormhole names a module can reach through the `wormhole` proxy.\n *\n * `wormholes.cart` and `wormholes['cart']` yield names; anything the scan cannot\n * resolve statically — computed access, aliasing the proxy into a variable,\n * namespace imports, re-exports — sets `dynamic`, which the manifest records as\n * `*` (all open wormholes). The failure mode is therefore over-emission, never\n * missing data.\n */\nexport type WormholeScan = { names: Set<string>; dynamic: boolean };\n\n// type-only subtrees are erased at runtime — identifiers inside them are not accesses\nconst TYPE_ONLY_NODES = new Set([\n 'TSTypeAnnotation',\n 'TSTypeParameterDeclaration',\n 'TSTypeParameterInstantiation',\n 'TSTypeQuery',\n 'TSTypeAliasDeclaration',\n 'TSInterfaceDeclaration',\n]);\n\ntype Node = { type: string; [key: string]: unknown };\n\nfunction isNode(value: unknown): value is Node {\n return typeof value === 'object' && value !== null && typeof (value as Node).type === 'string';\n}\n\nfunction walk(\n node: Node,\n visit: (node: Node, parent: Node | null, key: string) => void,\n parent: Node | null,\n key: string,\n): void {\n if (TYPE_ONLY_NODES.has(node.type)) {\n return;\n }\n\n visit(node, parent, key);\n\n for (const [childKey, value] of Object.entries(node)) {\n if (childKey === 'loc') {\n continue;\n }\n\n if (isNode(value)) {\n walk(value, visit, node, childKey);\n } else if (Array.isArray(value)) {\n for (const item of value) {\n if (isNode(item)) {\n walk(item, visit, node, childKey);\n }\n }\n }\n }\n}\n\nfunction isMember(node: Node | null): node is Node {\n return node?.type === 'MemberExpression' || node?.type === 'OptionalMemberExpression';\n}\n\nexport function scanWormholeAccess(code: string, id: string): WormholeScan | null {\n const isTs = id.endsWith('.ts') || id.endsWith('.tsx') || id.endsWith('.astro');\n const plugins: ('typescript' | 'jsx')[] = [];\n\n if (isTs) {\n plugins.push('typescript');\n }\n\n // compiled astro output is not jsx\n if (!id.endsWith('.ts') && !id.endsWith('.astro')) {\n plugins.push('jsx');\n }\n\n let program: Node;\n\n try {\n program = parse(code, { sourceType: 'module', plugins }).program as unknown as Node;\n } catch {\n // an unparseable module that mentions the package cannot be scanned — degrade soundly\n return { names: new Set(), dynamic: true };\n }\n\n const locals = new Set<string>();\n const scan: WormholeScan = { names: new Set(), dynamic: false };\n const body = program['body'] as Node[];\n\n for (const statement of body) {\n const source = (statement['source'] as Node | null)?.['value'];\n\n if (source !== PACKAGE_NAME) {\n continue;\n }\n\n if (statement.type === 'ImportDeclaration') {\n const specifiers = statement['specifiers'] as Node[];\n\n for (const specifier of specifiers) {\n if (specifier.type === 'ImportSpecifier') {\n const imported = specifier['imported'] as Node;\n const name = imported.type === 'Identifier' ? (imported['name'] as string) : (imported['value'] as string);\n\n if (name === 'wormholes') {\n locals.add((specifier['local'] as Node)['name'] as string);\n }\n } else if (specifier.type === 'ImportNamespaceSpecifier') {\n scan.dynamic = true;\n }\n }\n } else {\n // export { wormhole } from '...' / export * from '...' — accesses happen elsewhere\n scan.dynamic = true;\n }\n }\n\n if (locals.size === 0 && !scan.dynamic) {\n return null;\n }\n\n walk(\n program,\n (node, parent, key) => {\n if (!parent || node.type !== 'Identifier' || !locals.has(node['name'] as string)) {\n return;\n }\n\n if (parent.type === 'ImportSpecifier') {\n return;\n }\n\n if (key === 'object' && isMember(parent)) {\n const property = parent['property'] as Node;\n\n if (!parent['computed'] && property.type === 'Identifier') {\n scan.names.add(property['name'] as string);\n } else if (parent['computed'] && property.type === 'StringLiteral') {\n scan.names.add(property['value'] as string);\n } else {\n scan.dynamic = true;\n }\n\n return;\n }\n\n // a same-named identifier in a non-computed name position cannot reference the proxy value\n if ((key === 'property' || key === 'key') && !(parent['computed'] as boolean)) {\n return;\n }\n\n // the proxy escapes (aliased, passed, re-exported) — its accesses are untrackable\n scan.dynamic = true;\n },\n null,\n '',\n );\n\n return scan;\n}\n"],"mappings":";;AAEA,MAAM,eAAe;AAcrB,MAAM,kCAAkB,IAAI,IAAI;CAC9B;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;AAID,SAAS,OAAO,OAA+B;CAC7C,OAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,OAAQ,MAAe,SAAS;AACxF;AAEA,SAAS,KACP,MACA,OACA,QACA,KACM;CACN,IAAI,gBAAgB,IAAI,KAAK,IAAI,GAC/B;CAGF,MAAM,MAAM,QAAQ,GAAG;CAEvB,KAAK,MAAM,CAAC,UAAU,UAAU,OAAO,QAAQ,IAAI,GAAG;EACpD,IAAI,aAAa,OACf;EAGF,IAAI,OAAO,KAAK,GACd,KAAK,OAAO,OAAO,MAAM,QAAQ;OAC5B,IAAI,MAAM,QAAQ,KAAK,GACvB;QAAA,MAAM,QAAQ,OACjB,IAAI,OAAO,IAAI,GACb,KAAK,MAAM,OAAO,MAAM,QAAQ;EAAA;CAIxC;AACF;AAEA,SAAS,SAAS,MAAiC;CACjD,OAAO,MAAM,SAAS,sBAAsB,MAAM,SAAS;AAC7D;AAEA,SAAgB,mBAAmB,MAAc,IAAiC;CAChF,MAAM,OAAO,GAAG,SAAS,KAAK,KAAK,GAAG,SAAS,MAAM,KAAK,GAAG,SAAS,QAAQ;CAC9E,MAAM,UAAoC,CAAC;CAE3C,IAAI,MACF,QAAQ,KAAK,YAAY;CAI3B,IAAI,CAAC,GAAG,SAAS,KAAK,KAAK,CAAC,GAAG,SAAS,QAAQ,GAC9C,QAAQ,KAAK,KAAK;CAGpB,IAAI;CAEJ,IAAI;EACF,UAAU,MAAM,MAAM;GAAE,YAAY;GAAU;EAAQ,CAAC,CAAC,CAAC;CAC3D,QAAQ;EAEN,OAAO;GAAE,uBAAO,IAAI,IAAI;GAAG,SAAS;EAAK;CAC3C;CAEA,MAAM,yBAAS,IAAI,IAAY;CAC/B,MAAM,OAAqB;EAAE,uBAAO,IAAI,IAAI;EAAG,SAAS;CAAM;CAC9D,MAAM,OAAO,QAAQ;CAErB,KAAK,MAAM,aAAa,MAAM;EAG5B,IAFgB,UAAU,SAAS,GAAmB,aAEvC,cACb;EAGF,IAAI,UAAU,SAAS,qBAAqB;GAC1C,MAAM,aAAa,UAAU;GAE7B,KAAK,MAAM,aAAa,YACtB,IAAI,UAAU,SAAS,mBAAmB;IACxC,MAAM,WAAW,UAAU;IAG3B,KAFa,SAAS,SAAS,eAAgB,SAAS,UAAsB,SAAS,cAE1E,aACX,OAAO,IAAK,UAAU,QAAQ,CAAU,OAAiB;GAE7D,OAAO,IAAI,UAAU,SAAS,4BAC5B,KAAK,UAAU;EAGrB,OAEE,KAAK,UAAU;CAEnB;CAEA,IAAI,OAAO,SAAS,KAAK,CAAC,KAAK,SAC7B,OAAO;CAGT,KACE,UACC,MAAM,QAAQ,QAAQ;EACrB,IAAI,CAAC,UAAU,KAAK,SAAS,gBAAgB,CAAC,OAAO,IAAI,KAAK,OAAiB,GAC7E;EAGF,IAAI,OAAO,SAAS,mBAClB;EAGF,IAAI,QAAQ,YAAY,SAAS,MAAM,GAAG;GACxC,MAAM,WAAW,OAAO;GAExB,IAAI,CAAC,OAAO,eAAe,SAAS,SAAS,cAC3C,KAAK,MAAM,IAAI,SAAS,OAAiB;QACpC,IAAI,OAAO,eAAe,SAAS,SAAS,iBACjD,KAAK,MAAM,IAAI,SAAS,QAAkB;QAE1C,KAAK,UAAU;GAGjB;EACF;EAGA,KAAK,QAAQ,cAAc,QAAQ,UAAU,CAAE,OAAO,aACpD;EAIF,KAAK,UAAU;CACjB,GACA,MACA,EACF;CAEA,OAAO;AACT"}
package/dist/server.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { n as Wormhole } from "./types-DiWa8aQg.js";
1
+ import { n as Wormhole } from "./types-v_ftArqH.js";
2
2
  import { ReadonlyDeep } from "@entwico/dash";
3
3
  //#region src/server.d.ts
4
4
  /** One [wormhole, data] pair per element — data is checked against its own wormhole's T. */
@@ -7,8 +7,8 @@ import { APIContext } from "astro";
7
7
  * **Security:** wormhole data is serialized into an inline `<script>` tag and sent to the browser.
8
8
  * Never store secrets (tokens, API keys, credentials) in a wormhole.
9
9
  *
10
- * The stored value is exposed as deeply readonly (`ReadonlyDeep` of `@entwico/dash`) — the only way to change it is `set()`
11
- * (client) or the middleware's per-request values (server), which keeps subscribers in sync.
10
+ * The stored value is exposed as deeply readonly (`ReadonlyDeep` of `@entwico/dash`) — the only way to
11
+ * change it is `set()` (client) or the middleware's per-request values (server), which keeps subscribers in sync.
12
12
  */
13
13
  interface Wormhole<in out T> {
14
14
  readonly name: string;
@@ -50,4 +50,4 @@ type UnwrapWormholes<R> = { [K in keyof R]: R[K] extends Wormhole<infer T> ? T :
50
50
  type WormholeMap = { readonly [K in keyof WormholeRegistry]: Wormhole<WormholeRegistry[K]>; };
51
51
  //#endregion
52
52
  export { WormholeMap as a, WormholeHandler as i, Wormhole as n, WormholeRegistry as o, WormholeDefinition as r, UnwrapWormholes as t };
53
- //# sourceMappingURL=types-DiWa8aQg.d.ts.map
53
+ //# sourceMappingURL=types-v_ftArqH.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types-DiWa8aQg.d.ts","names":[],"sources":["../src/types.ts"],"mappings":";;;;;;;;;;;;UAciB,gBAAgB;WACtB;WACA;EACT,OAAO,aAAa;;;;;;EAMpB,IAAI,MAAM,aAAa;EACvB,UAAU,KAAK,MAAM,aAAa;;;;;;;KAQxB,gBAAgB,MAAM,SAAS,eAAe,gBAAgB,QAAQ;;KAGtE,mBAAmB;EAC7B,SAAS,gBAAgB;;;;;;EAMzB;;;;;;;UASe;;KAGL,gBAAgB,QAAQ,WAAW,IAAI,EAAE,WAAW,eAAe,KAAK;;KAGxE,0BAA0B,WAAW,mBAAmB,SAAS,iBAAiB"}
1
+ {"version":3,"file":"types-v_ftArqH.d.ts","names":[],"sources":["../src/types.ts"],"mappings":";;;;;;;;;;;;UAciB,gBAAgB;WACtB;WACA;EACT,OAAO,aAAa;;;;;;EAMpB,IAAI,MAAM,aAAa;EACvB,UAAU,KAAK,MAAM,aAAa;;;;;;;KAQxB,gBAAgB,MAAM,SAAS,eAAe,gBAAgB,QAAQ;;KAGtE,mBAAmB;EAC7B,SAAS,gBAAgB;;;;;;EAMzB;;;;;;;UASe;;KAGL,gBAAgB,QAAQ,WAAW,IAAI,EAAE,WAAW,eAAe,KAAK;;KAGxE,0BAA0B,WAAW,mBAAmB,SAAS,iBAAiB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astroscope/wormhole",
3
- "version": "3.0.0",
3
+ "version": "3.0.1",
4
4
  "description": "Share dynamic server data with React islands and client scripts — typed, loaded per route, streamed with the HTML, sliced per island",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -51,21 +51,22 @@
51
51
  },
52
52
  "homepage": "https://github.com/entwico/astroscope/tree/main/packages/wormhole#readme",
53
53
  "dependencies": {
54
- "@babel/parser": "^8.0.4",
54
+ "@babel/parser": "^8.0.7",
55
55
  "@entwico/dash": "^1.3.0"
56
56
  },
57
57
  "devDependencies": {
58
- "@astroscope/node": "^3.0.0",
59
- "@types/node": "^26.5.0",
60
- "@types/react": "^19.2.18",
61
- "astro": "^7.3.2",
62
- "react": "^19.2.8",
58
+ "@astroscope/node": "^4.0.0",
59
+ "@types/node": "^26.6.4",
60
+ "@types/react": "^19.3.0",
61
+ "astro": "^7.3.7",
62
+ "react": "^19.3.0",
63
63
  "tsdown": "^0.23.0",
64
64
  "typescript": "^6.0.3",
65
- "vite": "^8.2.2"
65
+ "vite": "^8.3.4",
66
+ "vitest": "^5.0.3"
66
67
  },
67
68
  "peerDependencies": {
68
- "@astroscope/node": "^3.0.0",
69
+ "@astroscope/node": "^4.0.0",
69
70
  "astro": "^7.0.0",
70
71
  "react": "^18.0.0 || ^19.0.0",
71
72
  "vite": "^8.0.0"
@@ -1 +0,0 @@
1
- {"version":3,"file":"scan-CczEOWpD.js","names":[],"sources":["../src/extraction/scan.ts"],"sourcesContent":["import { parse } from '@babel/parser';\n\nconst PACKAGE_NAME = '@astroscope/wormhole';\n\n/**\n * Which wormhole names a module can reach through the `wormhole` proxy.\n *\n * `wormholes.cart` and `wormholes['cart']` yield names; anything the scan cannot\n * resolve statically — computed access, aliasing the proxy into a variable,\n * namespace imports, re-exports — sets `dynamic`, which the manifest records as\n * `*` (all open wormholes). The failure mode is therefore over-emission, never\n * missing data.\n */\nexport type WormholeScan = { names: Set<string>; dynamic: boolean };\n\n// type-only subtrees are erased at runtime — identifiers inside them are not accesses\nconst TYPE_ONLY_NODES = new Set([\n 'TSTypeAnnotation',\n 'TSTypeParameterDeclaration',\n 'TSTypeParameterInstantiation',\n 'TSTypeQuery',\n 'TSTypeAliasDeclaration',\n 'TSInterfaceDeclaration',\n]);\n\ntype Node = { type: string; [key: string]: unknown };\n\nfunction isNode(value: unknown): value is Node {\n return typeof value === 'object' && value !== null && typeof (value as Node).type === 'string';\n}\n\nfunction walk(\n node: Node,\n visit: (node: Node, parent: Node | null, key: string) => void,\n parent: Node | null,\n key: string,\n): void {\n if (TYPE_ONLY_NODES.has(node.type)) {\n return;\n }\n\n visit(node, parent, key);\n\n for (const [childKey, value] of Object.entries(node)) {\n if (childKey === 'loc') {\n continue;\n }\n\n if (isNode(value)) {\n walk(value, visit, node, childKey);\n } else if (Array.isArray(value)) {\n for (const item of value) {\n if (isNode(item)) {\n walk(item, visit, node, childKey);\n }\n }\n }\n }\n}\n\nfunction isMember(node: Node | null): node is Node {\n return node?.type === 'MemberExpression' || node?.type === 'OptionalMemberExpression';\n}\n\nexport function scanWormholeAccess(code: string, id: string): WormholeScan | null {\n const isTs = id.endsWith('.ts') || id.endsWith('.tsx') || id.endsWith('.astro');\n const plugins: ('typescript' | 'jsx')[] = [];\n\n if (isTs) {\n plugins.push('typescript');\n }\n\n // compiled astro output is not jsx\n if (!id.endsWith('.ts') && !id.endsWith('.astro')) {\n plugins.push('jsx');\n }\n\n let program: Node;\n\n try {\n program = parse(code, { sourceType: 'module', plugins }).program as unknown as Node;\n } catch {\n // an unparseable module that mentions the package cannot be scanned — degrade soundly\n return { names: new Set(), dynamic: true };\n }\n\n const locals = new Set<string>();\n const scan: WormholeScan = { names: new Set(), dynamic: false };\n\n for (const statement of program['body'] as Node[]) {\n const source = (statement['source'] as Node | null)?.['value'];\n\n if (source !== PACKAGE_NAME) {\n continue;\n }\n\n if (statement.type === 'ImportDeclaration') {\n for (const specifier of statement['specifiers'] as Node[]) {\n if (specifier.type === 'ImportSpecifier') {\n const imported = specifier['imported'] as Node;\n const name = imported.type === 'Identifier' ? (imported['name'] as string) : (imported['value'] as string);\n\n if (name === 'wormholes') {\n locals.add((specifier['local'] as Node)['name'] as string);\n }\n } else if (specifier.type === 'ImportNamespaceSpecifier') {\n scan.dynamic = true;\n }\n }\n } else {\n // export { wormhole } from '...' / export * from '...' — accesses happen elsewhere\n scan.dynamic = true;\n }\n }\n\n if (locals.size === 0 && !scan.dynamic) {\n return null;\n }\n\n walk(\n program,\n (node, parent, key) => {\n if (node.type !== 'Identifier' || !locals.has(node['name'] as string) || !parent) {\n return;\n }\n\n if (parent.type === 'ImportSpecifier') {\n return;\n }\n\n if (isMember(parent) && key === 'object') {\n const property = parent['property'] as Node;\n\n if (!parent['computed'] && property.type === 'Identifier') {\n scan.names.add(property['name'] as string);\n } else if (parent['computed'] && property.type === 'StringLiteral') {\n scan.names.add(property['value'] as string);\n } else {\n scan.dynamic = true;\n }\n\n return;\n }\n\n // a same-named identifier in a non-computed name position cannot reference the proxy value\n if ((key === 'property' || key === 'key') && !(parent['computed'] as boolean)) {\n return;\n }\n\n // the proxy escapes (aliased, passed, re-exported) — its accesses are untrackable\n scan.dynamic = true;\n },\n null,\n '',\n );\n\n return scan;\n}\n"],"mappings":";;AAEA,MAAM,eAAe;AAcrB,MAAM,kCAAkB,IAAI,IAAI;CAC9B;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;AAID,SAAS,OAAO,OAA+B;CAC7C,OAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,OAAQ,MAAe,SAAS;AACxF;AAEA,SAAS,KACP,MACA,OACA,QACA,KACM;CACN,IAAI,gBAAgB,IAAI,KAAK,IAAI,GAC/B;CAGF,MAAM,MAAM,QAAQ,GAAG;CAEvB,KAAK,MAAM,CAAC,UAAU,UAAU,OAAO,QAAQ,IAAI,GAAG;EACpD,IAAI,aAAa,OACf;EAGF,IAAI,OAAO,KAAK,GACd,KAAK,OAAO,OAAO,MAAM,QAAQ;OAC5B,IAAI,MAAM,QAAQ,KAAK,GACvB;QAAA,MAAM,QAAQ,OACjB,IAAI,OAAO,IAAI,GACb,KAAK,MAAM,OAAO,MAAM,QAAQ;EAAA;CAIxC;AACF;AAEA,SAAS,SAAS,MAAiC;CACjD,OAAO,MAAM,SAAS,sBAAsB,MAAM,SAAS;AAC7D;AAEA,SAAgB,mBAAmB,MAAc,IAAiC;CAChF,MAAM,OAAO,GAAG,SAAS,KAAK,KAAK,GAAG,SAAS,MAAM,KAAK,GAAG,SAAS,QAAQ;CAC9E,MAAM,UAAoC,CAAC;CAE3C,IAAI,MACF,QAAQ,KAAK,YAAY;CAI3B,IAAI,CAAC,GAAG,SAAS,KAAK,KAAK,CAAC,GAAG,SAAS,QAAQ,GAC9C,QAAQ,KAAK,KAAK;CAGpB,IAAI;CAEJ,IAAI;EACF,UAAU,MAAM,MAAM;GAAE,YAAY;GAAU;EAAQ,CAAC,CAAC,CAAC;CAC3D,QAAQ;EAEN,OAAO;GAAE,uBAAO,IAAI,IAAI;GAAG,SAAS;EAAK;CAC3C;CAEA,MAAM,yBAAS,IAAI,IAAY;CAC/B,MAAM,OAAqB;EAAE,uBAAO,IAAI,IAAI;EAAG,SAAS;CAAM;CAE9D,KAAK,MAAM,aAAa,QAAQ,SAAmB;EAGjD,IAFgB,UAAU,SAAS,GAAmB,aAEvC,cACb;EAGF,IAAI,UAAU,SAAS,qBAChB;QAAA,MAAM,aAAa,UAAU,eAChC,IAAI,UAAU,SAAS,mBAAmB;IACxC,MAAM,WAAW,UAAU;IAG3B,KAFa,SAAS,SAAS,eAAgB,SAAS,UAAsB,SAAS,cAE1E,aACX,OAAO,IAAK,UAAU,QAAQ,CAAU,OAAiB;GAE7D,OAAO,IAAI,UAAU,SAAS,4BAC5B,KAAK,UAAU;EAAA,OAKnB,KAAK,UAAU;CAEnB;CAEA,IAAI,OAAO,SAAS,KAAK,CAAC,KAAK,SAC7B,OAAO;CAGT,KACE,UACC,MAAM,QAAQ,QAAQ;EACrB,IAAI,KAAK,SAAS,gBAAgB,CAAC,OAAO,IAAI,KAAK,OAAiB,KAAK,CAAC,QACxE;EAGF,IAAI,OAAO,SAAS,mBAClB;EAGF,IAAI,SAAS,MAAM,KAAK,QAAQ,UAAU;GACxC,MAAM,WAAW,OAAO;GAExB,IAAI,CAAC,OAAO,eAAe,SAAS,SAAS,cAC3C,KAAK,MAAM,IAAI,SAAS,OAAiB;QACpC,IAAI,OAAO,eAAe,SAAS,SAAS,iBACjD,KAAK,MAAM,IAAI,SAAS,QAAkB;QAE1C,KAAK,UAAU;GAGjB;EACF;EAGA,KAAK,QAAQ,cAAc,QAAQ,UAAU,CAAE,OAAO,aACpD;EAIF,KAAK,UAAU;CACjB,GACA,MACA,EACF;CAEA,OAAO;AACT"}