@astroscope/wormhole 1.1.0 → 2.0.0

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/dist/index.js CHANGED
@@ -1,38 +1,176 @@
1
- //#region src/define.ts
2
- /**
3
- * Define a wormhole that transfers state from server middleware to client components.
4
- *
5
- * **Security:** wormhole data is serialized into the HTML and sent to the browser.
6
- * Never store secrets (tokens, API keys, credentials) in a wormhole.
7
- */
8
- function defineWormhole(name) {
9
- const listeners = /* @__PURE__ */ new Set();
10
- const key = `__wormhole_${name}__`;
11
- let store;
1
+ import { n as defineWormhole, t as assignWormholeNames } from "./define-BO-pYQc1.js";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ //#region src/extraction/vite-plugin.ts
6
+ const WORMHOLE_VIRTUAL_MODULE_ID = "virtual:@astroscope/wormhole/manifest";
7
+ const REGISTRY_VIRTUAL_MODULE_ID = "virtual:@astroscope/wormhole/registry";
8
+ const RESOLVED_VIRTUAL_MODULE_ID = `\0${WORMHOLE_VIRTUAL_MODULE_ID}`;
9
+ const RESOLVED_REGISTRY_STUB_ID = `\0${REGISTRY_VIRTUAL_MODULE_ID}`;
10
+ const MANIFEST_FILE_NAME = "wormhole-manifest.json";
11
+ const SCAN_EXTENSIONS = [
12
+ ".ts",
13
+ ".tsx",
14
+ ".js",
15
+ ".jsx"
16
+ ];
17
+ /** chunk file name → manifest key, e.g. `_astro/Counter.abc.js` → `Counter.abc` */
18
+ function chunkFileNameToName(fileName) {
19
+ return fileName.slice(fileName.lastIndexOf("/") + 1).replace(/\.js$/, "");
20
+ }
21
+ function wormholeVitePlugin(options) {
22
+ let isBuild = false;
23
+ const moduleScans = /* @__PURE__ */ new Map();
12
24
  return {
13
- name,
14
- key,
15
- get() {
16
- if (store !== void 0) return store;
17
- const getter = globalThis[key];
18
- const value = typeof getter === "function" ? getter() : void 0;
19
- if (value === void 0) throw new Error(`wormhole "${name}" is not initialized`);
20
- return value;
25
+ name: "@astroscope/wormhole/extract",
26
+ configResolved(config) {
27
+ isBuild = config.command === "build";
21
28
  },
22
- set(data) {
23
- if (typeof globalThis.window === "undefined") throw new Error(`wormhole "${name}" set() cannot be called on the server as it is not request-scoped; use open(wormhole, data, fn) from "@astroscope/wormhole/server" instead`);
24
- store = data;
25
- for (const fn of listeners) fn(data);
29
+ resolveId(id) {
30
+ if (id === "virtual:@astroscope/wormhole/manifest") return RESOLVED_VIRTUAL_MODULE_ID;
31
+ if (id === "virtual:@astroscope/wormhole/registry") return options.registryPath ?? RESOLVED_REGISTRY_STUB_ID;
26
32
  },
27
- subscribe(fn) {
28
- listeners.add(fn);
29
- return () => {
30
- listeners.delete(fn);
33
+ load(id) {
34
+ if (id === RESOLVED_REGISTRY_STUB_ID) return "export const wormholes = {};";
35
+ if (id !== RESOLVED_VIRTUAL_MODULE_ID) return;
36
+ if (!isBuild) return "export const manifest = null;";
37
+ return `
38
+ import { readFileSync } from 'node:fs';
39
+ import { fileURLToPath } from 'node:url';
40
+ import { dirname, join } from 'node:path';
41
+
42
+ const dir = dirname(fileURLToPath(import.meta.url));
43
+
44
+ let manifest = null;
45
+
46
+ for (const candidate of [
47
+ join(dir, '${MANIFEST_FILE_NAME}'),
48
+ join(dir, 'chunks', '${MANIFEST_FILE_NAME}'),
49
+ join(dir, '..', 'chunks', '${MANIFEST_FILE_NAME}'),
50
+ ]) {
51
+ try {
52
+ manifest = JSON.parse(readFileSync(candidate, 'utf-8'));
53
+ break;
54
+ } catch {
55
+ manifest = null;
56
+ }
57
+ }
58
+
59
+ export { manifest };
60
+ `;
61
+ },
62
+ async transform(code, id) {
63
+ if (!isBuild || this.environment && this.environment.name !== "client") return;
64
+ if (id.includes("node_modules")) return;
65
+ if (!SCAN_EXTENSIONS.some((ext) => id.endsWith(ext))) return;
66
+ if (!code.includes("@astroscope/wormhole")) return;
67
+ const { scanWormholeAccess } = await import("./scan-4GXufA9_.js");
68
+ const scan = scanWormholeAccess(code, id);
69
+ if (scan) moduleScans.set(id, scan);
70
+ },
71
+ writeBundle(outputOptions, bundle) {
72
+ if (this.environment.name !== "client" || !outputOptions.dir) return;
73
+ const chunks = {};
74
+ const chunkNamesByFileName = /* @__PURE__ */ new Map();
75
+ const chunkEdges = /* @__PURE__ */ new Map();
76
+ const scriptEntries = [];
77
+ for (const chunk of Object.values(bundle)) {
78
+ if (chunk.type !== "chunk") continue;
79
+ const names = /* @__PURE__ */ new Set();
80
+ let dynamic = false;
81
+ for (const moduleId of chunk.moduleIds) {
82
+ const scan = moduleScans.get(moduleId);
83
+ scan?.names.forEach((name) => names.add(name));
84
+ dynamic ||= scan?.dynamic ?? false;
85
+ }
86
+ const chunkNames = [...dynamic ? ["*"] : [], ...names];
87
+ if (chunkNames.length > 0) chunks[chunkFileNameToName(chunk.fileName)] = chunkNames;
88
+ chunkNamesByFileName.set(chunk.fileName, chunkNames);
89
+ chunkEdges.set(chunk.fileName, [...chunk.imports, ...chunk.dynamicImports]);
90
+ if ((chunk.isEntry || chunk.isDynamicEntry) && chunk.facadeModuleId?.includes("astro&type=script")) scriptEntries.push(chunk.fileName);
91
+ }
92
+ const scriptNames = /* @__PURE__ */ new Set();
93
+ const visited = /* @__PURE__ */ new Set();
94
+ const walkScripts = (fileName) => {
95
+ if (visited.has(fileName)) return;
96
+ visited.add(fileName);
97
+ chunkNamesByFileName.get(fileName)?.forEach((name) => scriptNames.add(name));
98
+ chunkEdges.get(fileName)?.forEach(walkScripts);
99
+ };
100
+ scriptEntries.forEach(walkScripts);
101
+ const manifest = {
102
+ chunks,
103
+ scripts: [...scriptNames]
31
104
  };
105
+ const chunksDir = path.resolve(outputOptions.dir, "..", "server", "chunks");
106
+ if (fs.existsSync(chunksDir)) fs.writeFileSync(path.join(chunksDir, MANIFEST_FILE_NAME), JSON.stringify(manifest));
107
+ else console.error(`[@astroscope/wormhole] server chunks directory not found, cannot write manifest to ${chunksDir}`);
32
108
  }
33
109
  };
34
110
  }
35
111
  //#endregion
36
- export { defineWormhole };
112
+ //#region src/integration.ts
113
+ /**
114
+ * Astro integration for wormholes with per-island payload slicing.
115
+ *
116
+ * Resolves the `src/wormholes.ts` registry and exposes it to the middleware as a
117
+ * virtual module, registers the vite plugin that scans client code for
118
+ * `wormholes.<name>` accesses and maps them onto the emitted chunks, and generates
119
+ * the registry type stub that types the `wormholes` proxy. Delivery happens at
120
+ * runtime: the wormhole middleware opens per-request values, and the islands
121
+ * emitter (via `@astroscope/node`) writes each island's slice before its tag.
122
+ */
123
+ function wormholeIntegration() {
124
+ const resolveRegistryPath = (srcDir) => ["wormholes.ts", path.join("wormholes", "index.ts")].map((candidate) => path.join(fileURLToPath(srcDir), candidate)).find((candidate) => fs.existsSync(candidate)) ?? null;
125
+ return {
126
+ name: "@astroscope/wormhole",
127
+ hooks: {
128
+ "astro:config:setup": ({ config, updateConfig }) => {
129
+ updateConfig({ vite: { plugins: [wormholeVitePlugin({ registryPath: resolveRegistryPath(config.srcDir) })] } });
130
+ },
131
+ "astro:config:done": ({ config, injectTypes, logger }) => {
132
+ const registryPath = resolveRegistryPath(config.srcDir);
133
+ if (!registryPath) {
134
+ logger.warn("no src/wormholes.ts registry found — the wormholes proxy stays untyped");
135
+ return;
136
+ }
137
+ const stubFile = path.join(fileURLToPath(config.root), ".astro", "integrations", "_astroscope_wormhole", "registry.d.ts");
138
+ const specifier = path.relative(path.dirname(stubFile), registryPath.replace(/([/\\]index)?\.ts$/, "")).split(path.sep).join("/");
139
+ const written = injectTypes({
140
+ filename: "registry.d.ts",
141
+ content: `import type { UnwrapWormholes } from '@astroscope/wormhole';
142
+
143
+ declare module '@astroscope/wormhole' {
144
+ interface WormholeRegistry extends UnwrapWormholes<typeof import(${JSON.stringify(specifier)}).wormholes> {}\n}\n`
145
+ });
146
+ if (path.resolve(fileURLToPath(written)) !== path.resolve(stubFile)) logger.error(`registry type stub landed at ${fileURLToPath(written)} instead of the expected ${stubFile} — its relative import is now wrong and the wormholes proxy is untyped; please report this to @astroscope/wormhole`);
147
+ }
148
+ }
149
+ };
150
+ }
151
+ //#endregion
152
+ //#region src/index.ts
153
+ const stubs = /* @__PURE__ */ new Map();
154
+ function stub(name) {
155
+ let wormhole = stubs.get(name);
156
+ if (!wormhole) {
157
+ wormhole = defineWormhole();
158
+ assignWormholeNames({ [name]: wormhole });
159
+ stubs.set(name, wormhole);
160
+ }
161
+ return wormhole;
162
+ }
163
+ /**
164
+ * Access wormholes by name — `wormholes.cart.get()`. This is the server side of the
165
+ * proxy (the browser build ships its own): values come from the request's
166
+ * wormhole middleware scope, so it works in frontmatter, endpoints, and
167
+ * server-rendered islands alike. Typed via the registry type stub the integration
168
+ * generates from `src/wormholes.ts`.
169
+ */
170
+ const wormholes = new Proxy({}, { get(_target, prop) {
171
+ return typeof prop === "string" ? stub(prop) : void 0;
172
+ } });
173
+ //#endregion
174
+ export { assignWormholeNames, wormholeIntegration as default, defineWormhole, wormholes };
37
175
 
38
176
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/define.ts"],"sourcesContent":["import type { DeepReadonly, Wormhole } from './types.js';\n\n/**\n * Define a wormhole that transfers state from server middleware to client components.\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 */\nexport function defineWormhole<T>(name: string): Wormhole<T> {\n const listeners = new Set<(data: DeepReadonly<T>) => void>();\n const key = `__wormhole_${name}__`;\n let store: DeepReadonly<T> | undefined;\n\n return {\n name,\n key,\n\n get(): DeepReadonly<T> {\n if (store !== undefined) return store;\n\n const getter = (globalThis as any)[key];\n const value = typeof getter === 'function' ? (getter() as DeepReadonly<T> | undefined) : undefined;\n\n if (value === undefined) {\n throw new Error(`wormhole \"${name}\" is not initialized`);\n }\n\n return value;\n },\n\n set(data: DeepReadonly<T>): void {\n if (typeof (globalThis as any).window === 'undefined') {\n throw new Error(\n `wormhole \"${name}\" set() cannot be called on the server as it is not request-scoped; use open(wormhole, data, fn) from \"@astroscope/wormhole/server\" instead`,\n );\n }\n\n store = data;\n\n for (const fn of listeners) fn(data);\n },\n\n subscribe(fn: (data: DeepReadonly<T>) => void): () => void {\n listeners.add(fn);\n\n return () => {\n listeners.delete(fn);\n };\n },\n };\n}\n"],"mappings":";;;;;;;AAQA,SAAgB,eAAkB,MAA2B;CAC3D,MAAM,4BAAY,IAAI,IAAqC;CAC3D,MAAM,MAAM,cAAc,KAAK;CAC/B,IAAI;CAEJ,OAAO;EACL;EACA;EAEA,MAAuB;GACrB,IAAI,UAAU,KAAA,GAAW,OAAO;GAEhC,MAAM,SAAU,WAAmB;GACnC,MAAM,QAAQ,OAAO,WAAW,aAAc,OAAO,IAAoC,KAAA;GAEzF,IAAI,UAAU,KAAA,GACZ,MAAM,IAAI,MAAM,aAAa,KAAK,qBAAqB;GAGzD,OAAO;EACT;EAEA,IAAI,MAA6B;GAC/B,IAAI,OAAQ,WAAmB,WAAW,aACxC,MAAM,IAAI,MACR,aAAa,KAAK,4IACpB;GAGF,QAAQ;GAER,KAAK,MAAM,MAAM,WAAW,GAAG,IAAI;EACrC;EAEA,UAAU,IAAiD;GACzD,UAAU,IAAI,EAAE;GAEhB,aAAa;IACX,UAAU,OAAO,EAAE;GACrB;EACF;CACF;AACF"}
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 { 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';\n\nconst RESOLVED_VIRTUAL_MODULE_ID = `\\0${WORMHOLE_VIRTUAL_MODULE_ID}`;\nconst RESOLVED_REGISTRY_STUB_ID = `\\0${REGISTRY_VIRTUAL_MODULE_ID}`;\n\nconst MANIFEST_FILE_NAME = 'wormhole-manifest.json';\n\nconst SCAN_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx'];\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 client modules for `wormholes.<name>`\n * accesses, maps them onto the emitted chunks, and drops the manifest next to the\n * server chunks — where the virtual module reads it back at runtime, mirroring the\n * islands and i18n manifests. Dev needs none of this: without a manifest the\n * middleware ships all open 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};\n\nexport function wormholeVitePlugin(options: WormholeVitePluginOptions): Plugin {\n let isBuild = false;\n const moduleScans = new Map<string, WormholeScan>();\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\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 rejects every value name)\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_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 && this.environment.name !== 'client')) return;\n if (id.includes('node_modules')) return;\n if (!SCAN_EXTENSIONS.some((ext) => id.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, id);\n\n if (scan) {\n moduleScans.set(id, scan);\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 = moduleScans.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 const visited = new Set<string>();\n\n const walkScripts = (fileName: string): void => {\n if (visited.has(fileName)) return;\n\n visited.add(fileName);\n chunkNamesByFileName.get(fileName)?.forEach((name) => scriptNames.add(name));\n chunkEdges.get(fileName)?.forEach(walkScripts);\n };\n\n scriptEntries.forEach(walkScripts);\n\n const manifest: WormholeManifest = { chunks, scripts: [...scriptNames] };\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 { AstroIntegration } from 'astro';\nimport { wormholeVitePlugin } from './extraction/vite-plugin.js';\n\n/**\n * Astro integration for wormholes with per-island payload slicing.\n *\n * Resolves the `src/wormholes.ts` registry and exposes it to the middleware as a\n * virtual module, registers the vite plugin that scans client code for\n * `wormholes.<name>` accesses and maps them onto the emitted chunks, and generates\n * the registry type stub that types the `wormholes` proxy. Delivery happens at\n * runtime: the wormhole middleware opens per-request values, and the islands\n * emitter (via `@astroscope/node`) writes each island's slice before its tag.\n */\nexport default function wormholeIntegration(): 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 return {\n name: '@astroscope/wormhole',\n hooks: {\n 'astro:config:setup': ({ config, updateConfig }) => {\n updateConfig({\n vite: {\n plugins: [wormholeVitePlugin({ registryPath: resolveRegistryPath(config.srcDir) })],\n },\n });\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, defineWormhole } from './define.js';\nimport type { Wormhole, WormholeMap } from './types.js';\n\nexport { default } from './integration.js';\nexport { defineWormhole, assignWormholeNames } from './define.js';\nexport type { DeepReadonly, UnwrapWormholes, Wormhole, WormholeMap, WormholeRegistry } 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 = defineWormhole();\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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"}
@@ -0,0 +1,9 @@
1
+ //#region src/key.ts
2
+ /** the shared key format for a wormhole's ALS entry — kept free of node imports for the browser build */
3
+ function wormholeKey(name) {
4
+ return `__wormhole_${name}__`;
5
+ }
6
+ //#endregion
7
+ export { wormholeKey as t };
8
+
9
+ //# sourceMappingURL=key-BqTy1gM3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"key-BqTy1gM3.js","names":[],"sources":["../src/key.ts"],"sourcesContent":["/** the shared key format for a wormhole's ALS entry — kept free of node imports for the browser build */\nexport function wormholeKey(name: string): string {\n return `__wormhole_${name}__`;\n}\n"],"mappings":";;AACA,SAAgB,YAAY,MAAsB;CAChD,OAAO,cAAc,KAAK;AAC5B"}
@@ -1,5 +1,4 @@
1
- import { n as Wormhole, t as DeepReadonly } from "../types-Q2gaO_N_.js";
2
-
1
+ import { r as Wormhole, t as DeepReadonly } from "../types-okMd-V3s.js";
3
2
  //#region src/react/index.d.ts
4
3
  declare function useWormhole<T>(wormhole: Wormhole<T>): DeepReadonly<T>;
5
4
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/react/index.ts"],"mappings":";;;iBAGgB,WAAA,IAAe,QAAA,EAAU,QAAA,CAAS,CAAA,IAAK,YAAA,CAAa,CAAA"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/react/index.ts"],"mappings":";;iBAGgB,YAAY,GAAG,UAAU,SAAS,KAAK,aAAa"}
@@ -0,0 +1,79 @@
1
+ import { parse } from "@babel/parser";
2
+ //#region src/extraction/scan.ts
3
+ const PACKAGE_NAME = "@astroscope/wormhole";
4
+ const TYPE_ONLY_NODES = /* @__PURE__ */ new Set([
5
+ "TSTypeAnnotation",
6
+ "TSTypeParameterDeclaration",
7
+ "TSTypeParameterInstantiation",
8
+ "TSTypeQuery",
9
+ "TSTypeAliasDeclaration",
10
+ "TSInterfaceDeclaration"
11
+ ]);
12
+ function isNode(value) {
13
+ return typeof value === "object" && value !== null && typeof value.type === "string";
14
+ }
15
+ function walk(node, visit, parent, key) {
16
+ if (TYPE_ONLY_NODES.has(node.type)) return;
17
+ visit(node, parent, key);
18
+ for (const [childKey, value] of Object.entries(node)) {
19
+ if (childKey === "loc") continue;
20
+ if (isNode(value)) walk(value, visit, node, childKey);
21
+ else if (Array.isArray(value)) {
22
+ for (const item of value) if (isNode(item)) walk(item, visit, node, childKey);
23
+ }
24
+ }
25
+ }
26
+ function isMember(node) {
27
+ return node?.type === "MemberExpression" || node?.type === "OptionalMemberExpression";
28
+ }
29
+ function scanWormholeAccess(code, id) {
30
+ const isTs = id.endsWith(".ts") || id.endsWith(".tsx");
31
+ const plugins = [];
32
+ if (isTs) plugins.push("typescript");
33
+ if (!id.endsWith(".ts")) plugins.push("jsx");
34
+ let program;
35
+ try {
36
+ program = parse(code, {
37
+ sourceType: "module",
38
+ plugins
39
+ }).program;
40
+ } catch {
41
+ return {
42
+ names: /* @__PURE__ */ new Set(),
43
+ dynamic: true
44
+ };
45
+ }
46
+ const locals = /* @__PURE__ */ new Set();
47
+ const scan = {
48
+ names: /* @__PURE__ */ new Set(),
49
+ dynamic: false
50
+ };
51
+ for (const statement of program["body"]) {
52
+ if (statement["source"]?.["value"] !== PACKAGE_NAME) continue;
53
+ if (statement.type === "ImportDeclaration") {
54
+ for (const specifier of statement["specifiers"]) if (specifier.type === "ImportSpecifier") {
55
+ const imported = specifier["imported"];
56
+ if ((imported.type === "Identifier" ? imported["name"] : imported["value"]) === "wormholes") locals.add(specifier["local"]["name"]);
57
+ } else if (specifier.type === "ImportNamespaceSpecifier") scan.dynamic = true;
58
+ } else scan.dynamic = true;
59
+ }
60
+ if (locals.size === 0 && !scan.dynamic) return null;
61
+ walk(program, (node, parent, key) => {
62
+ if (node.type !== "Identifier" || !locals.has(node["name"]) || !parent) return;
63
+ if (parent.type === "ImportSpecifier") return;
64
+ if (isMember(parent) && key === "object") {
65
+ const property = parent["property"];
66
+ if (!parent["computed"] && property.type === "Identifier") scan.names.add(property["name"]);
67
+ else if (parent["computed"] && property.type === "StringLiteral") scan.names.add(property["value"]);
68
+ else scan.dynamic = true;
69
+ return;
70
+ }
71
+ if ((key === "property" || key === "key") && !parent["computed"]) return;
72
+ scan.dynamic = true;
73
+ }, null, "");
74
+ return scan;
75
+ }
76
+ //#endregion
77
+ export { scanWormholeAccess };
78
+
79
+ //# sourceMappingURL=scan-4GXufA9_.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scan-4GXufA9_.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');\n const plugins: ('typescript' | 'jsx')[] = [];\n\n if (isTs) {\n plugins.push('typescript');\n }\n\n if (!id.endsWith('.ts')) {\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;CACrD,MAAM,UAAoC,CAAC;CAE3C,IAAI,MACF,QAAQ,KAAK,YAAY;CAG3B,IAAI,CAAC,GAAG,SAAS,KAAK,GACpB,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"}
package/dist/server.d.ts CHANGED
@@ -1,24 +1,58 @@
1
- import { n as Wormhole, t as DeepReadonly } from "./types-Q2gaO_N_.js";
2
-
3
- //#region src/server.d.ts
4
- /** One [wormhole, data] pair per element — data is checked against its own wormhole's T. */
5
- type WormholeEntries<Ts extends readonly unknown[]> = { [K in keyof Ts]: readonly [Wormhole<Ts[K]>, DeepReadonly<Ts[K]>] };
1
+ import { a as WormholeRegistry, r as Wormhole, t as DeepReadonly } from "./types-okMd-V3s.js";
2
+ import { ExcludePattern } from "@astroscope/node/excludes";
3
+ import { APIContext, MiddlewareHandler } from "astro";
4
+ //#region src/middleware.d.ts
6
5
  /**
7
- * Open a wormhole for the duration of `fn` — `wormhole.get()` returns `data`
8
- * anywhere in the async execution rooted at `fn`.
6
+ * Per-request values keyed by registry name — omit a key (or pass undefined) to
7
+ * leave that wormhole closed. Typed from the `WormholeRegistry` interface the
8
+ * integration generates from `src/wormholes.ts`.
9
9
  */
10
- declare function open<T, R>(wormhole: Wormhole<T>, data: DeepReadonly<T>, fn: () => R): R;
10
+ type WormholeValues = { [K in keyof WormholeRegistry]?: DeepReadonly<WormholeRegistry[K]> | undefined; };
11
+ type WormholeMiddlewareOptions = {
12
+ /** resolve the request's wormhole values */
13
+ values: (context: APIContext) => WormholeValues | Promise<WormholeValues>;
14
+ /**
15
+ * Patterns to exclude from wormhole setup. Defaults to RECOMMENDED_EXCLUDES.
16
+ */
17
+ exclude?: ExcludePattern[] | ((context: APIContext) => boolean) | undefined;
18
+ };
11
19
  /**
12
- * Open several wormholes at once for the duration of `fn`.
20
+ * Create the wormhole middleware: resolves the per-request values, makes them
21
+ * readable on the server (frontmatter, endpoints, SSR islands) for the duration of
22
+ * the request, and delivers them to the client — sliced per island through the
23
+ * `@astroscope/node` islands pipeline, at stream end for astro `<script>` consumers,
24
+ * and in full in dev. The registry comes from `src/wormholes.ts` via the
25
+ * integration, so values are simply keyed by name.
13
26
  *
27
+ * @example
14
28
  * ```typescript
15
- * return open([
16
- * [cartStore, cart],
17
- * [sessionStore, session],
18
- * ], () => next());
29
+ * // src/middleware.ts
30
+ * import { createWormholeMiddleware } from '@astroscope/wormhole/server';
31
+ *
32
+ * export const onRequest = createWormholeMiddleware({
33
+ * values: (ctx) => ({
34
+ * session: { loggedIn: ctx.locals.user !== undefined },
35
+ * }),
36
+ * });
19
37
  * ```
20
38
  */
21
- declare function open<Ts extends readonly unknown[], R>(entries: readonly [...WormholeEntries<Ts>], fn: () => R): R;
39
+ declare function createWormholeMiddleware(options: WormholeMiddlewareOptions): MiddlewareHandler;
40
+ //#endregion
41
+ //#region src/server.d.ts
42
+ /** One [wormhole, data] pair per element — data is checked against its own wormhole's T. */
43
+ type WormholeEntries<Ts extends readonly unknown[]> = { [K in keyof Ts]: readonly [Wormhole<Ts[K]>, DeepReadonly<Ts[K]>]; };
44
+ /**
45
+ * Provide wormhole values to server code that runs outside the request pipeline —
46
+ * tests above all, or out-of-request rendering (emails, jobs). `wormhole.get()`
47
+ * returns `data` anywhere in the async execution rooted at `fn`. Inside the app,
48
+ * the middleware is the way; values opened here are never delivered to the client.
49
+ */
50
+ declare function openWormholes<T, R>(wormhole: Wormhole<T>, data: DeepReadonly<T>, fn: () => R): R;
51
+ /**
52
+ * Open several wormholes at once for the duration of `fn` — pass an array of
53
+ * `[wormhole, data]` pairs, each checked against its own wormhole.
54
+ */
55
+ declare function openWormholes<Ts extends readonly unknown[], R>(entries: readonly [...WormholeEntries<Ts>], fn: () => R): R;
22
56
  //#endregion
23
- export { open };
57
+ export { type WormholeMiddlewareOptions, type WormholeValues, createWormholeMiddleware, openWormholes };
24
58
  //# sourceMappingURL=server.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","names":[],"sources":["../src/server.ts"],"mappings":";;;;KAIK,eAAA,gDACS,EAAA,aAAe,QAAA,CAAS,EAAA,CAAG,CAAA,IAAK,YAAA,CAAa,EAAA,CAAG,CAAA;;;;;iBAa9C,IAAA,OAAW,QAAA,EAAU,QAAA,CAAS,CAAA,GAAI,IAAA,EAAM,YAAA,CAAa,CAAA,GAAI,EAAA,QAAU,CAAA,GAAI,CAAA;;;;;;;;;;;iBAWvE,IAAA,mCAAuC,OAAA,eAAsB,eAAA,CAAgB,EAAA,IAAM,EAAA,QAAU,CAAA,GAAI,CAAA"}
1
+ {"version":3,"file":"server.d.ts","names":[],"sources":["../src/middleware.ts","../src/server.ts"],"mappings":";;;;;;;;;KAcY,oBACT,WAAW,oBAAoB,aAAa,iBAAiB;KAGpD;;EAEV,SAAS,SAAS,eAAe,iBAAiB,QAAQ;;;;EAI1D,UAAU,qBAAqB,SAAS;;;;;;;;;;;;;;;;;;;;;;iBAuB1B,yBAAyB,SAAS,4BAA4B;;;;KCxCzE,gBAAgB,oCAClB,WAAW,eAAe,SAAS,GAAG,KAAK,aAAa,GAAG;;;;;;;iBAS9C,cAAc,GAAG,GAAG,UAAU,SAAS,IAAI,MAAM,aAAa,IAAI,UAAU,IAAI;;;;;iBAKhF,cAAc,+BAA+B,GAC3D,sBAAsB,gBAAgB,MACtC,UAAU,IACT"}
package/dist/server.js CHANGED
@@ -1,16 +1,162 @@
1
- import { AsyncLocalStorage } from "node:async_hooks";
1
+ import { r as als, t as assignWormholeNames } from "./define-BO-pYQc1.js";
2
+ import { RECOMMENDED_EXCLUDES, shouldExclude } from "@astroscope/node/excludes";
3
+ import { wormholes } from "virtual:@astroscope/wormhole/registry";
4
+ import { registerDocumentEmitter, registerIslandEmitter } from "@astroscope/node/islands";
5
+ import { log } from "@astroscope/node/log";
6
+ import { manifest } from "virtual:@astroscope/wormhole/manifest";
7
+ //#region src/client-state.ts
8
+ /**
9
+ * Script generation for wormhole delivery, shipped through the `@astroscope/node`
10
+ * islands pipeline (see islands-emitter.ts for the placement policy).
11
+ */
12
+ /** values may contain `<\/script>` — escape `<` so they cannot end the tag */
13
+ function jsonForScript(value) {
14
+ return JSON.stringify(value).replace(/</g, "\\u003c");
15
+ }
16
+ /**
17
+ * Inline script writing entries onto `self.__wormholes__` and notifying listeners —
18
+ * the counterpart of the browser proxy's entry protocol, safe in either order.
19
+ */
20
+ function createWormholeMergeScript(entries) {
21
+ return `<script>{const s=self.__wormholes__??={},d=${jsonForScript(entries)};for(const n in d){const e=s[n]??={};e.v=d[n];e.l&&e.l.forEach(f=>f(e.v))}}<\/script>`;
22
+ }
23
+ //#endregion
24
+ //#region src/manifest.ts
25
+ /** null in dev and when the build produced no manifest (broken deploy → full delivery fallback) */
26
+ function getWormholeManifest() {
27
+ return manifest;
28
+ }
29
+ //#endregion
30
+ //#region src/request-store.ts
31
+ const requestWormholes = /* @__PURE__ */ new WeakMap();
32
+ function setRequestWormholes(request, wormholes) {
33
+ requestWormholes.set(request, wormholes);
34
+ }
35
+ function getRequestWormholes(request) {
36
+ return requestWormholes.get(request);
37
+ }
38
+ //#endregion
39
+ //#region src/islands-emitter.ts
40
+ /**
41
+ * Contributes each island's wormhole slice: an inline merge script with the values
42
+ * of every open wormhole the island's full chunk closure (dynamic imports included)
43
+ * can reach, emitted before the island tag — parsed strictly before the island
44
+ * connects, even mid-stream. Names already written earlier in the document are
45
+ * skipped; a `*` chunk (dynamic proxy access) degrades to all open wormholes.
46
+ */
47
+ const IS_DEV = !!import.meta.env?.DEV;
48
+ let registered = false;
49
+ let warnedMissingManifest = false;
50
+ /** public url of a chunk → manifest chunk name, e.g. `…/_astro/Counter.abc.js` → `Counter.abc` */
51
+ function urlToChunkName(url) {
52
+ return url.slice(url.lastIndexOf("/") + 1).replace(/\.js$/, "");
53
+ }
54
+ /**
55
+ * The document-level wormhole delivery, exported for tests: everything open as
56
+ * `head` content in dev (or without a manifest), script-entry names as `end`
57
+ * content in prod — running after the rewriter, names the islands emitter
58
+ * already wrote are skipped race-free.
59
+ */
60
+ function createWormholeDocumentEmitter(dev = IS_DEV) {
61
+ return (context) => {
62
+ const request = getRequestWormholes(context.request);
63
+ if (!request || request.values.size === 0) return null;
64
+ if (dev) return { head: createWormholeMergeScript(Object.fromEntries(request.values)) };
65
+ const manifest = getWormholeManifest();
66
+ if (!manifest) {
67
+ if (!warnedMissingManifest) {
68
+ warnedMissingManifest = true;
69
+ log.error("wormhole manifest not found — delivering all open wormholes with every page (is the integration configured?)");
70
+ }
71
+ return { head: createWormholeMergeScript(Object.fromEntries(request.values)) };
72
+ }
73
+ const names = manifest.scripts.includes("*") ? [...request.values.keys()] : manifest.scripts.filter((name) => request.values.has(name));
74
+ if (names.length === 0) return null;
75
+ return { end: () => {
76
+ const fresh = names.filter((name) => !request.emitted.has(name));
77
+ if (fresh.length === 0) return null;
78
+ return createWormholeMergeScript(Object.fromEntries(fresh.map((name) => [name, request.values.get(name)])));
79
+ } };
80
+ };
81
+ }
82
+ function registerWormholeEmitters() {
83
+ if (registered) return;
84
+ registered = true;
85
+ registerDocumentEmitter(createWormholeDocumentEmitter());
86
+ registerIslandEmitter((island, context) => {
87
+ if (!context) return null;
88
+ const request = getRequestWormholes(context.request);
89
+ const manifest = getWormholeManifest();
90
+ if (!request || request.values.size === 0 || !manifest) return null;
91
+ let all = false;
92
+ const reachable = /* @__PURE__ */ new Set();
93
+ for (const url of island.fullClosure) for (const name of manifest.chunks[urlToChunkName(url)] ?? []) if (name === "*") all = true;
94
+ else reachable.add(name);
95
+ const entries = {};
96
+ let count = 0;
97
+ for (const name of all ? request.values.keys() : reachable) {
98
+ if (!request.values.has(name) || request.emitted.has(name)) continue;
99
+ request.emitted.add(name);
100
+ entries[name] = request.values.get(name);
101
+ count++;
102
+ }
103
+ if (count === 0) return null;
104
+ return { html: createWormholeMergeScript(entries) };
105
+ });
106
+ }
107
+ //#endregion
108
+ //#region src/middleware.ts
109
+ /**
110
+ * Create the wormhole middleware: resolves the per-request values, makes them
111
+ * readable on the server (frontmatter, endpoints, SSR islands) for the duration of
112
+ * the request, and delivers them to the client — sliced per island through the
113
+ * `@astroscope/node` islands pipeline, at stream end for astro `<script>` consumers,
114
+ * and in full in dev. The registry comes from `src/wormholes.ts` via the
115
+ * integration, so values are simply keyed by name.
116
+ *
117
+ * @example
118
+ * ```typescript
119
+ * // src/middleware.ts
120
+ * import { createWormholeMiddleware } from '@astroscope/wormhole/server';
121
+ *
122
+ * export const onRequest = createWormholeMiddleware({
123
+ * values: (ctx) => ({
124
+ * session: { loggedIn: ctx.locals.user !== undefined },
125
+ * }),
126
+ * });
127
+ * ```
128
+ */
129
+ function createWormholeMiddleware(options) {
130
+ assignWormholeNames(wormholes);
131
+ registerWormholeEmitters();
132
+ return async (ctx, next) => {
133
+ if (shouldExclude(ctx, options.exclude ?? RECOMMENDED_EXCLUDES)) return next();
134
+ const resolved = await options.values(ctx);
135
+ const open = /* @__PURE__ */ new Map();
136
+ const scope = new Map(als.getStore());
137
+ for (const [name, data] of Object.entries(resolved)) {
138
+ if (data === void 0) continue;
139
+ const wormhole = wormholes[name];
140
+ if (!wormhole) throw new Error(`wormhole "${name}" is not in the src/wormholes.ts registry`);
141
+ open.set(name, data);
142
+ scope.set(wormhole.key, data);
143
+ }
144
+ setRequestWormholes(ctx.request, {
145
+ values: open,
146
+ emitted: /* @__PURE__ */ new Set()
147
+ });
148
+ return als.run(scope, () => next());
149
+ };
150
+ }
151
+ //#endregion
2
152
  //#region src/server.ts
3
- const als = globalThis[Symbol.for("@astroscope/wormhole/als")] ??= new AsyncLocalStorage();
4
- function open(...args) {
153
+ function openWormholes(...args) {
5
154
  const [entries, fn] = Array.isArray(args[0]) ? args : [[[args[0], args[1]]], args[2]];
6
155
  const ctx = new Map(als.getStore());
7
- for (const [wormhole, data] of entries) {
8
- ctx.set(wormhole.key, data);
9
- globalThis[wormhole.key] ??= () => als.getStore()?.get(wormhole.key);
10
- }
156
+ for (const [wormhole, data] of entries) ctx.set(wormhole.key, data);
11
157
  return als.run(ctx, fn);
12
158
  }
13
159
  //#endregion
14
- export { open };
160
+ export { createWormholeMiddleware, openWormholes };
15
161
 
16
162
  //# sourceMappingURL=server.js.map