@astroscope/wormhole 1.1.0 → 3.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/README.md +70 -126
- package/dist/als-CntRqWdO.js +7 -0
- package/dist/als-CntRqWdO.js.map +1 -0
- package/dist/define-1UkPnsXV.js +83 -0
- package/dist/define-1UkPnsXV.js.map +1 -0
- package/dist/index.browser.d.ts +9 -0
- package/dist/index.browser.d.ts.map +1 -0
- package/dist/index.browser.js +48 -0
- package/dist/index.browser.js.map +1 -0
- package/dist/index.d.ts +53 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +230 -28
- package/dist/index.js.map +1 -1
- package/dist/key-BqTy1gM3.js +9 -0
- package/dist/key-BqTy1gM3.js.map +1 -0
- package/dist/middleware-entrypoint.d.ts +4 -0
- package/dist/middleware-entrypoint.d.ts.map +1 -0
- package/dist/middleware-entrypoint.js +263 -0
- package/dist/middleware-entrypoint.js.map +1 -0
- package/dist/react/index.d.ts +3 -4
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js.map +1 -1
- package/dist/scan-CczEOWpD.js +79 -0
- package/dist/scan-CczEOWpD.js.map +1 -0
- package/dist/server.d.ts +11 -16
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +4 -8
- package/dist/server.js.map +1 -1
- package/dist/types-DiWa8aQg.d.ts +53 -0
- package/dist/types-DiWa8aQg.d.ts.map +1 -0
- package/package.json +25 -16
- package/dist/types-Q2gaO_N_.d.ts +0 -31
- package/dist/types-Q2gaO_N_.d.ts.map +0 -1
- package/src/astro/WormholeScript.astro +0 -29
- package/src/astro/index.ts +0 -2
package/dist/index.js
CHANGED
|
@@ -1,38 +1,240 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
1
|
+
import { n as createWormhole, r as defineWormhole, t as assignWormholeNames } from "./define-1UkPnsXV.js";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { collectRouteIslands, routeEntrypoints, stripQuery } from "@astroscope/node/islands";
|
|
6
|
+
import { serializeExcludePatterns } from "@astroscope/node/excludes";
|
|
7
|
+
//#region src/extraction/vite-plugin.ts
|
|
8
|
+
const WORMHOLE_VIRTUAL_MODULE_ID = "virtual:@astroscope/wormhole/manifest";
|
|
9
|
+
const REGISTRY_VIRTUAL_MODULE_ID = "virtual:@astroscope/wormhole/registry";
|
|
10
|
+
const CONFIG_VIRTUAL_MODULE_ID = "virtual:@astroscope/wormhole/config";
|
|
11
|
+
const RESOLVED_VIRTUAL_MODULE_ID = `\0${WORMHOLE_VIRTUAL_MODULE_ID}`;
|
|
12
|
+
const RESOLVED_REGISTRY_STUB_ID = `\0${REGISTRY_VIRTUAL_MODULE_ID}`;
|
|
13
|
+
const RESOLVED_CONFIG_VIRTUAL_MODULE_ID = `\0${CONFIG_VIRTUAL_MODULE_ID}`;
|
|
14
|
+
const MANIFEST_FILE_NAME = "wormhole-manifest.json";
|
|
15
|
+
const SCAN_EXTENSIONS = [
|
|
16
|
+
".ts",
|
|
17
|
+
".tsx",
|
|
18
|
+
".js",
|
|
19
|
+
".jsx"
|
|
20
|
+
];
|
|
21
|
+
const SERVER_SCAN_EXTENSIONS = [...SCAN_EXTENSIONS, ".astro"];
|
|
22
|
+
/** chunk file name → manifest key, e.g. `_astro/Counter.abc.js` → `Counter.abc` */
|
|
23
|
+
function chunkFileNameToName(fileName) {
|
|
24
|
+
return fileName.slice(fileName.lastIndexOf("/") + 1).replace(/\.js$/, "");
|
|
25
|
+
}
|
|
26
|
+
function wormholeVitePlugin(options) {
|
|
27
|
+
let isBuild = false;
|
|
28
|
+
const clientScans = /* @__PURE__ */ new Map();
|
|
29
|
+
const serverScans = /* @__PURE__ */ new Map();
|
|
30
|
+
const routeNames = /* @__PURE__ */ new Map();
|
|
31
|
+
const patternsByFile = /* @__PURE__ */ new Map();
|
|
12
32
|
return {
|
|
13
|
-
name,
|
|
14
|
-
|
|
15
|
-
|
|
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;
|
|
33
|
+
name: "@astroscope/wormhole/extract",
|
|
34
|
+
configResolved(config) {
|
|
35
|
+
isBuild = config.command === "build";
|
|
21
36
|
},
|
|
22
|
-
|
|
23
|
-
if (
|
|
24
|
-
|
|
25
|
-
|
|
37
|
+
resolveId(id) {
|
|
38
|
+
if (id === "virtual:@astroscope/wormhole/manifest") return RESOLVED_VIRTUAL_MODULE_ID;
|
|
39
|
+
if (id === "virtual:@astroscope/wormhole/config") return RESOLVED_CONFIG_VIRTUAL_MODULE_ID;
|
|
40
|
+
if (id === "virtual:@astroscope/wormhole/registry") return options.registryPath ?? RESOLVED_REGISTRY_STUB_ID;
|
|
26
41
|
},
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
return ()
|
|
30
|
-
|
|
42
|
+
load(id) {
|
|
43
|
+
if (id === RESOLVED_REGISTRY_STUB_ID) return "export const wormholes = {};";
|
|
44
|
+
if (id === RESOLVED_CONFIG_VIRTUAL_MODULE_ID) return `export const exclude = ${options.exclude ? serializeExcludePatterns(options.exclude) : "null"};`;
|
|
45
|
+
if (id === options.registryPath && this.environment?.name === "client") throw new Error(`[@astroscope/wormhole] ${path.relative(process.cwd(), id)} is server-only — client code reads wormholes through the \`wormholes\` proxy from '@astroscope/wormhole'`);
|
|
46
|
+
if (id !== RESOLVED_VIRTUAL_MODULE_ID) return;
|
|
47
|
+
if (!isBuild) return "export const manifest = null;";
|
|
48
|
+
return `
|
|
49
|
+
import { readFileSync } from 'node:fs';
|
|
50
|
+
import { fileURLToPath } from 'node:url';
|
|
51
|
+
import { dirname, join } from 'node:path';
|
|
52
|
+
|
|
53
|
+
const dir = dirname(fileURLToPath(import.meta.url));
|
|
54
|
+
|
|
55
|
+
let manifest = null;
|
|
56
|
+
|
|
57
|
+
for (const candidate of [
|
|
58
|
+
join(dir, '${MANIFEST_FILE_NAME}'),
|
|
59
|
+
join(dir, 'chunks', '${MANIFEST_FILE_NAME}'),
|
|
60
|
+
join(dir, '..', 'chunks', '${MANIFEST_FILE_NAME}'),
|
|
61
|
+
]) {
|
|
62
|
+
try {
|
|
63
|
+
manifest = JSON.parse(readFileSync(candidate, 'utf-8'));
|
|
64
|
+
break;
|
|
65
|
+
} catch {
|
|
66
|
+
manifest = null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export { manifest };
|
|
71
|
+
`;
|
|
72
|
+
},
|
|
73
|
+
async transform(code, id) {
|
|
74
|
+
if (!isBuild || !this.environment) return;
|
|
75
|
+
if (id.includes("node_modules")) return;
|
|
76
|
+
const client = this.environment.name === "client";
|
|
77
|
+
const extensions = client ? SCAN_EXTENSIONS : SERVER_SCAN_EXTENSIONS;
|
|
78
|
+
const file = extensions.some((ext) => id.endsWith(ext)) ? id : stripQuery(id);
|
|
79
|
+
if (!extensions.some((ext) => file.endsWith(ext))) return;
|
|
80
|
+
if (!code.includes("@astroscope/wormhole")) return;
|
|
81
|
+
const { scanWormholeAccess } = await import("./scan-CczEOWpD.js");
|
|
82
|
+
const scan = scanWormholeAccess(code, file);
|
|
83
|
+
if (scan) (client ? clientScans : serverScans).set(client ? id : stripQuery(id), scan);
|
|
84
|
+
},
|
|
85
|
+
async generateBundle() {
|
|
86
|
+
if (this.environment.name === "client") return;
|
|
87
|
+
const { routes, patternsByModule } = await collectRouteIslands(this, options.pages());
|
|
88
|
+
for (const pattern of routes.keys()) routeNames.set(pattern, routeNames.get(pattern) ?? /* @__PURE__ */ new Set());
|
|
89
|
+
for (const [id, patterns] of patternsByModule) {
|
|
90
|
+
const file = stripQuery(id);
|
|
91
|
+
let owners = patternsByFile.get(file);
|
|
92
|
+
if (!owners) {
|
|
93
|
+
owners = /* @__PURE__ */ new Set();
|
|
94
|
+
patternsByFile.set(file, owners);
|
|
95
|
+
}
|
|
96
|
+
patterns.forEach((pattern) => owners.add(pattern));
|
|
97
|
+
const scan = serverScans.get(file);
|
|
98
|
+
if (!scan) continue;
|
|
99
|
+
for (const pattern of patterns) {
|
|
100
|
+
const names = routeNames.get(pattern);
|
|
101
|
+
scan.names.forEach((name) => names.add(name));
|
|
102
|
+
if (scan.dynamic) names.add("*");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
writeBundle(outputOptions, bundle) {
|
|
107
|
+
if (this.environment.name !== "client" || !outputOptions.dir) return;
|
|
108
|
+
const chunks = {};
|
|
109
|
+
const chunkNamesByFileName = /* @__PURE__ */ new Map();
|
|
110
|
+
const chunkEdges = /* @__PURE__ */ new Map();
|
|
111
|
+
const scriptEntries = [];
|
|
112
|
+
for (const chunk of Object.values(bundle)) {
|
|
113
|
+
if (chunk.type !== "chunk") continue;
|
|
114
|
+
const names = /* @__PURE__ */ new Set();
|
|
115
|
+
let dynamic = false;
|
|
116
|
+
for (const moduleId of chunk.moduleIds) {
|
|
117
|
+
const scan = clientScans.get(moduleId);
|
|
118
|
+
scan?.names.forEach((name) => names.add(name));
|
|
119
|
+
dynamic ||= scan?.dynamic ?? false;
|
|
120
|
+
}
|
|
121
|
+
const chunkNames = [...dynamic ? ["*"] : [], ...names];
|
|
122
|
+
if (chunkNames.length > 0) chunks[chunkFileNameToName(chunk.fileName)] = chunkNames;
|
|
123
|
+
chunkNamesByFileName.set(chunk.fileName, chunkNames);
|
|
124
|
+
chunkEdges.set(chunk.fileName, [...chunk.imports, ...chunk.dynamicImports]);
|
|
125
|
+
if ((chunk.isEntry || chunk.isDynamicEntry) && chunk.facadeModuleId?.includes("astro&type=script")) scriptEntries.push(chunk.fileName);
|
|
126
|
+
}
|
|
127
|
+
const scriptNames = /* @__PURE__ */ new Set();
|
|
128
|
+
for (const entry of scriptEntries) {
|
|
129
|
+
const names = /* @__PURE__ */ new Set();
|
|
130
|
+
const visited = /* @__PURE__ */ new Set();
|
|
131
|
+
const walk = (fileName) => {
|
|
132
|
+
if (visited.has(fileName)) return;
|
|
133
|
+
visited.add(fileName);
|
|
134
|
+
chunkNamesByFileName.get(fileName)?.forEach((name) => names.add(name));
|
|
135
|
+
chunkEdges.get(fileName)?.forEach(walk);
|
|
136
|
+
};
|
|
137
|
+
walk(entry);
|
|
138
|
+
names.forEach((name) => scriptNames.add(name));
|
|
139
|
+
const owner = bundle[entry]?.type === "chunk" ? stripQuery(bundle[entry].facadeModuleId ?? "") : "";
|
|
140
|
+
const owners = patternsByFile.get(owner) ?? routeNames.keys();
|
|
141
|
+
for (const pattern of owners) {
|
|
142
|
+
const route = routeNames.get(pattern);
|
|
143
|
+
names.forEach((name) => route?.add(name));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const routes = {};
|
|
147
|
+
for (const [pattern, names] of routeNames) routes[pattern] = [...names];
|
|
148
|
+
const manifest = {
|
|
149
|
+
chunks,
|
|
150
|
+
scripts: [...scriptNames],
|
|
151
|
+
routes
|
|
31
152
|
};
|
|
153
|
+
const chunksDir = path.resolve(outputOptions.dir, "..", "server", "chunks");
|
|
154
|
+
if (fs.existsSync(chunksDir)) fs.writeFileSync(path.join(chunksDir, MANIFEST_FILE_NAME), JSON.stringify(manifest));
|
|
155
|
+
else console.error(`[@astroscope/wormhole] server chunks directory not found, cannot write manifest to ${chunksDir}`);
|
|
32
156
|
}
|
|
33
157
|
};
|
|
34
158
|
}
|
|
35
159
|
//#endregion
|
|
36
|
-
|
|
160
|
+
//#region src/integration.ts
|
|
161
|
+
/**
|
|
162
|
+
* Astro integration for wormholes with per-route loading and per-island delivery.
|
|
163
|
+
*
|
|
164
|
+
* Resolves the `src/wormholes.ts` registry and exposes it to the middleware as a
|
|
165
|
+
* virtual module, injects the middleware after the project's own (so handlers see
|
|
166
|
+
* everything earlier middleware put on `locals`), registers the vite plugin that
|
|
167
|
+
* scans server and client code for `wormholes.<name>` accesses and maps them onto
|
|
168
|
+
* routes and chunks, and generates the registry type stub that types the
|
|
169
|
+
* `wormholes` proxy. Delivery happens at runtime: the middleware runs the handlers
|
|
170
|
+
* the route needs, and the islands emitter (via `@astroscope/node`) writes each
|
|
171
|
+
* island's slice before its tag.
|
|
172
|
+
*/
|
|
173
|
+
function wormholeIntegration(options = {}) {
|
|
174
|
+
const resolveRegistryPath = (srcDir) => ["wormholes.ts", path.join("wormholes", "index.ts")].map((candidate) => path.join(fileURLToPath(srcDir), candidate)).find((candidate) => fs.existsSync(candidate)) ?? null;
|
|
175
|
+
let root = "";
|
|
176
|
+
let resolvedRoutes = [];
|
|
177
|
+
return {
|
|
178
|
+
name: "@astroscope/wormhole",
|
|
179
|
+
hooks: {
|
|
180
|
+
"astro:config:setup": ({ config, updateConfig, addMiddleware }) => {
|
|
181
|
+
root = fileURLToPath(config.root);
|
|
182
|
+
addMiddleware({
|
|
183
|
+
order: "post",
|
|
184
|
+
entrypoint: "@astroscope/wormhole/middleware"
|
|
185
|
+
});
|
|
186
|
+
updateConfig({ vite: { plugins: [wormholeVitePlugin({
|
|
187
|
+
registryPath: resolveRegistryPath(config.srcDir),
|
|
188
|
+
exclude: options.exclude ?? null,
|
|
189
|
+
pages: () => routeEntrypoints(root, resolvedRoutes)
|
|
190
|
+
})] } });
|
|
191
|
+
},
|
|
192
|
+
"astro:routes:resolved": ({ routes }) => {
|
|
193
|
+
resolvedRoutes = routes;
|
|
194
|
+
},
|
|
195
|
+
"astro:config:done": ({ config, injectTypes, logger }) => {
|
|
196
|
+
const registryPath = resolveRegistryPath(config.srcDir);
|
|
197
|
+
if (!registryPath) {
|
|
198
|
+
logger.warn("no src/wormholes.ts registry found — the wormholes proxy stays untyped");
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const stubFile = path.join(fileURLToPath(config.root), ".astro", "integrations", "_astroscope_wormhole", "registry.d.ts");
|
|
202
|
+
const specifier = path.relative(path.dirname(stubFile), registryPath.replace(/([/\\]index)?\.ts$/, "")).split(path.sep).join("/");
|
|
203
|
+
const written = injectTypes({
|
|
204
|
+
filename: "registry.d.ts",
|
|
205
|
+
content: `import type { UnwrapWormholes } from '@astroscope/wormhole';
|
|
206
|
+
|
|
207
|
+
declare module '@astroscope/wormhole' {
|
|
208
|
+
interface WormholeRegistry extends UnwrapWormholes<typeof import(${JSON.stringify(specifier)}).wormholes> {}\n}\n`
|
|
209
|
+
});
|
|
210
|
+
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`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
//#endregion
|
|
216
|
+
//#region src/index.ts
|
|
217
|
+
const stubs = /* @__PURE__ */ new Map();
|
|
218
|
+
function stub(name) {
|
|
219
|
+
let wormhole = stubs.get(name);
|
|
220
|
+
if (!wormhole) {
|
|
221
|
+
wormhole = createWormhole();
|
|
222
|
+
assignWormholeNames({ [name]: wormhole });
|
|
223
|
+
stubs.set(name, wormhole);
|
|
224
|
+
}
|
|
225
|
+
return wormhole;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Access wormholes by name — `wormholes.cart.get()`. This is the server side of the
|
|
229
|
+
* proxy (the browser build ships its own): values come from the request's
|
|
230
|
+
* wormhole middleware scope, so it works in frontmatter, endpoints, and
|
|
231
|
+
* server-rendered islands alike. Typed via the registry type stub the integration
|
|
232
|
+
* generates from `src/wormholes.ts`.
|
|
233
|
+
*/
|
|
234
|
+
const wormholes = new Proxy({}, { get(_target, prop) {
|
|
235
|
+
return typeof prop === "string" ? stub(prop) : void 0;
|
|
236
|
+
} });
|
|
237
|
+
//#endregion
|
|
238
|
+
export { assignWormholeNames, wormholeIntegration as default, defineWormhole, wormholes };
|
|
37
239
|
|
|
38
240
|
//# 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 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":";;;;;;;AAQA,MAAa,6BAA6B;AAC1C,MAAa,6BAA6B;AAC1C,MAAa,2BAA2B;AAExC,MAAM,6BAA6B,KAAK;AACxC,MAAM,4BAA4B,KAAK;AACvC,MAAM,oCAAoC,KAAK;AAE/C,MAAM,qBAAqB;AAE3B,MAAM,kBAAkB;CAAC;CAAO;CAAQ;CAAO;AAAM;AAErD,MAAM,yBAAyB,CAAC,GAAG,iBAAiB,QAAQ;;AAG5D,SAAS,oBAAoB,UAA0B;CACrD,OAAO,SAAS,MAAM,SAAS,YAAY,GAAG,IAAI,CAAC,CAAC,CAAC,QAAQ,SAAS,EAAE;AAC1E;AAoBA,SAAgB,mBAAmB,SAA4C;CAC7E,IAAI,UAAU;CACd,MAAM,8BAAc,IAAI,IAA0B;CAClD,MAAM,8BAAc,IAAI,IAA0B;CAIlD,MAAM,6BAAa,IAAI,IAAyB;CAChD,MAAM,iCAAiB,IAAI,IAAyB;CAEpD,OAAO;EACL,MAAM;EAEN,eAAe,QAAQ;GACrB,UAAU,OAAO,YAAY;EAC/B;EAEA,UAAU,IAAI;GACZ,IAAI,OAAA,yCAAmC,OAAO;GAC9C,IAAI,OAAA,uCAAiC,OAAO;GAK5C,IAAI,OAAA,yCAAmC,OAAO,QAAQ,gBAAgB;EACxE;EAEA,KAAK,IAAI;GACP,IAAI,OAAO,2BACT,OAAO;GAGT,IAAI,OAAO,mCAET,OAAO,0BAA0B,QAAQ,UAAU,yBAAyB,QAAQ,OAAO,IAAI,OAAO;GAIxG,IAAI,OAAO,QAAQ,gBAAgB,KAAK,aAAa,SAAS,UAC5D,MAAM,IAAI,MACR,0BAA0B,KAAK,SAAS,QAAQ,IAAI,GAAG,EAAE,EAAE,0GAC7D;GAGF,IAAI,OAAO,4BAA4B;GAEvC,IAAI,CAAC,SACH,OAAO;GAMT,OAAO;;;;;;;;;;eAUE,mBAAmB;yBACT,mBAAmB;+BACb,mBAAmB;;;;;;;;;;;;EAY9C;EAEA,MAAM,UAAU,MAAM,IAAI;GACxB,IAAI,CAAC,WAAW,CAAC,KAAK,aAAa;GACnC,IAAI,GAAG,SAAS,cAAc,GAAG;GAEjC,MAAM,SAAS,KAAK,YAAY,SAAS;GACzC,MAAM,aAAa,SAAS,kBAAkB;GAG9C,MAAM,OAAO,WAAW,MAAM,QAAQ,GAAG,SAAS,GAAG,CAAC,IAAI,KAAK,WAAW,EAAE;GAE5E,IAAI,CAAC,WAAW,MAAM,QAAQ,KAAK,SAAS,GAAG,CAAC,GAAG;GACnD,IAAI,CAAC,KAAK,SAAS,sBAAsB,GAAG;GAG5C,MAAM,EAAE,uBAAuB,MAAM,OAAO;GAC5C,MAAM,OAAO,mBAAmB,MAAM,IAAI;GAE1C,IAAI,MACF,CAAC,SAAS,cAAc,YAAA,CAAa,IAAI,SAAS,KAAK,WAAW,EAAE,GAAG,IAAI;EAE/E;EAEA,MAAM,iBAAiB;GACrB,IAAI,KAAK,YAAY,SAAS,UAAU;GAExC,MAAM,EAAE,QAAQ,qBAAqB,MAAM,oBAAoB,MAAM,QAAQ,MAAM,CAAC;GAEpF,KAAK,MAAM,WAAW,OAAO,KAAK,GAChC,WAAW,IAAI,SAAS,WAAW,IAAI,OAAO,qBAAK,IAAI,IAAI,CAAC;GAG9D,KAAK,MAAM,CAAC,IAAI,aAAa,kBAAkB;IAC7C,MAAM,OAAO,WAAW,EAAE;IAC1B,IAAI,SAAS,eAAe,IAAI,IAAI;IAEpC,IAAI,CAAC,QAAQ;KACX,yBAAS,IAAI,IAAI;KACjB,eAAe,IAAI,MAAM,MAAM;IACjC;IAEA,SAAS,SAAS,YAAY,OAAO,IAAI,OAAO,CAAC;IAEjD,MAAM,OAAO,YAAY,IAAI,IAAI;IAEjC,IAAI,CAAC,MAAM;IAEX,KAAK,MAAM,WAAW,UAAU;KAC9B,MAAM,QAAQ,WAAW,IAAI,OAAO;KAEpC,KAAK,MAAM,SAAS,SAAS,MAAM,IAAI,IAAI,CAAC;KAE5C,IAAI,KAAK,SACP,MAAM,IAAI,GAAG;IAEjB;GACF;EACF;EAEA,YAAY,eAAe,QAAQ;GACjC,IAAI,KAAK,YAAY,SAAS,YAAY,CAAC,cAAc,KAAK;GAE9D,MAAM,SAAqC,CAAC;GAC5C,MAAM,uCAAuB,IAAI,IAAsB;GACvD,MAAM,6BAAa,IAAI,IAAsB;GAC7C,MAAM,gBAA0B,CAAC;GAEjC,KAAK,MAAM,SAAS,OAAO,OAAO,MAAM,GAAG;IACzC,IAAI,MAAM,SAAS,SAAS;IAE5B,MAAM,wBAAQ,IAAI,IAAY;IAC9B,IAAI,UAAU;IAEd,KAAK,MAAM,YAAY,MAAM,WAAW;KACtC,MAAM,OAAO,YAAY,IAAI,QAAQ;KAErC,MAAM,MAAM,SAAS,SAAS,MAAM,IAAI,IAAI,CAAC;KAC7C,YAAY,MAAM,WAAW;IAC/B;IAEA,MAAM,aAAa,CAAC,GAAI,UAAU,CAAC,GAAG,IAAI,CAAC,GAAI,GAAG,KAAK;IAEvD,IAAI,WAAW,SAAS,GACtB,OAAO,oBAAoB,MAAM,QAAQ,KAAK;IAGhD,qBAAqB,IAAI,MAAM,UAAU,UAAU;IACnD,WAAW,IAAI,MAAM,UAAU,CAAC,GAAG,MAAM,SAAS,GAAG,MAAM,cAAc,CAAC;IAI1E,KAAK,MAAM,WAAW,MAAM,mBAAmB,MAAM,gBAAgB,SAAS,mBAAmB,GAC/F,cAAc,KAAK,MAAM,QAAQ;GAErC;GAEA,MAAM,8BAAc,IAAI,IAAY;GAEpC,KAAK,MAAM,SAAS,eAAe;IACjC,MAAM,wBAAQ,IAAI,IAAY;IAC9B,MAAM,0BAAU,IAAI,IAAY;IAEhC,MAAM,QAAQ,aAA2B;KACvC,IAAI,QAAQ,IAAI,QAAQ,GAAG;KAE3B,QAAQ,IAAI,QAAQ;KACpB,qBAAqB,IAAI,QAAQ,CAAC,EAAE,SAAS,SAAS,MAAM,IAAI,IAAI,CAAC;KACrE,WAAW,IAAI,QAAQ,CAAC,EAAE,QAAQ,IAAI;IACxC;IAEA,KAAK,KAAK;IACV,MAAM,SAAS,SAAS,YAAY,IAAI,IAAI,CAAC;IAI7C,MAAM,QAAQ,OAAO,MAAM,EAAE,SAAS,UAAU,WAAW,OAAO,MAAM,CAAC,kBAAkB,EAAE,IAAI;IACjG,MAAM,SAAS,eAAe,IAAI,KAAK,KAAK,WAAW,KAAK;IAE5D,KAAK,MAAM,WAAW,QAAQ;KAC5B,MAAM,QAAQ,WAAW,IAAI,OAAO;KAEpC,MAAM,SAAS,SAAS,OAAO,IAAI,IAAI,CAAC;IAC1C;GACF;GAEA,MAAM,SAAqC,CAAC;GAE5C,KAAK,MAAM,CAAC,SAAS,UAAU,YAC7B,OAAO,WAAW,CAAC,GAAG,KAAK;GAG7B,MAAM,WAA6B;IAAE;IAAQ,SAAS,CAAC,GAAG,WAAW;IAAG;GAAO;GAC/E,MAAM,YAAY,KAAK,QAAQ,cAAc,KAAK,MAAM,UAAU,QAAQ;GAE1E,IAAI,GAAG,WAAW,SAAS,GACzB,GAAG,cAAc,KAAK,KAAK,WAAW,kBAAkB,GAAG,KAAK,UAAU,QAAQ,CAAC;QAEnF,QAAQ,MACN,sFAAsF,WACxF;EAEJ;CACF;AACF;;;;;;;;;;;;;;;ACjPA,SAAwB,oBAAoB,UAAsC,CAAC,GAAqB;CACtG,MAAM,uBAAuB,WAC3B,CAAC,gBAAgB,KAAK,KAAK,aAAa,UAAU,CAAC,CAAC,CACjD,KAAK,cAAc,KAAK,KAAK,cAAc,MAAM,GAAG,SAAS,CAAC,CAAC,CAC/D,MAAM,cAAc,GAAG,WAAW,SAAS,CAAC,KAAK;CAEtD,IAAI,OAAO;CACX,IAAI,iBAA6C,CAAC;CAElD,OAAO;EACL,MAAM;EACN,OAAO;GACL,uBAAuB,EAAE,QAAQ,cAAc,oBAAoB;IACjE,OAAO,cAAc,OAAO,IAAI;IAEhC,cAAc;KAAE,OAAO;KAAQ,YAAY;IAAkC,CAAC;IAE9E,aAAa,EACX,MAAM,EACJ,SAAS,CACP,mBAAmB;KACjB,cAAc,oBAAoB,OAAO,MAAM;KAC/C,SAAS,QAAQ,WAAW;KAC5B,aAAa,iBAAiB,MAAM,cAAc;IACpD,CAAC,CACH,EACF,EACF,CAAC;GACH;GAEA,0BAA0B,EAAE,aAAa;IACvC,iBAAiB;GACnB;GAEA,sBAAsB,EAAE,QAAQ,aAAa,aAAa;IACxD,MAAM,eAAe,oBAAoB,OAAO,MAAM;IAEtD,IAAI,CAAC,cAAc;KACjB,OAAO,KAAK,wEAAwE;KAEpF;IACF;IAMA,MAAM,WAAW,KAAK,KACpB,cAAc,OAAO,IAAI,GACzB,UACA,gBACA,wBACA,eACF;IAEA,MAAM,YADW,KAAK,SAAS,KAAK,QAAQ,QAAQ,GAAG,aAAa,QAAQ,sBAAsB,EAAE,CAC3E,CAAC,CAAC,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,GAAG;IAEnD,MAAM,UAAU,YAAY;KAC1B,UAAU;KACV,SACE;;;qEAEsE,KAAK,UAAU,SAAS,EAAE;IAEpG,CAAC;IAED,IAAI,KAAK,QAAQ,cAAc,OAAO,CAAC,MAAM,KAAK,QAAQ,QAAQ,GAChE,OAAO,MACL,gCAAgC,cAAc,OAAO,EAAE,2BAA2B,SAAS,mHAE7F;GAEJ;EACF;CACF;AACF;;;ACrFA,MAAM,wBAAQ,IAAI,IAA+B;AAEjD,SAAS,KAAK,MAAiC;CAC7C,IAAI,WAAW,MAAM,IAAI,IAAI;CAE7B,IAAI,CAAC,UAAU;EACb,WAAW,eAAe;EAE1B,oBAAoB,GAAG,OAAO,SAAS,CAAC;EACxC,MAAM,IAAI,MAAM,QAAQ;CAC1B;CAEA,OAAO;AACT;;;;;;;;AASA,MAAa,YAAyB,IAAI,MAAM,CAAC,GAAkB,EACjE,IAAI,SAAS,MAAM;CACjB,OAAO,OAAO,SAAS,WAAW,KAAK,IAAI,IAAI,KAAA;AACjD,EACF,CAAC"}
|
|
@@ -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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware-entrypoint.d.ts","names":[],"sources":["../src/middleware-entrypoint.ts"],"mappings":";qBAGa,2BAAS"}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { t as als } from "./als-CntRqWdO.js";
|
|
2
|
+
import { i as getWormholeSource, t as assignWormholeNames } from "./define-1UkPnsXV.js";
|
|
3
|
+
import { getRouteIslands, registerDocumentEmitter, registerIslandEmitter } from "@astroscope/node/islands";
|
|
4
|
+
import { RECOMMENDED_EXCLUDES, shouldExclude } from "@astroscope/node/excludes";
|
|
5
|
+
import { exclude } from "virtual:@astroscope/wormhole/config";
|
|
6
|
+
import { maybeAll, maybeThen } from "@entwico/dash";
|
|
7
|
+
import { wormholes } from "virtual:@astroscope/wormhole/registry";
|
|
8
|
+
import { log } from "@astroscope/node/log";
|
|
9
|
+
import { manifest } from "virtual:@astroscope/wormhole/manifest";
|
|
10
|
+
import { DURATION_BUCKETS, createCounter, createHistogram, errorType, withSpan } from "@astroscope/node/telemetry";
|
|
11
|
+
//#region src/client-state.ts
|
|
12
|
+
/**
|
|
13
|
+
* Script generation for wormhole delivery, shipped through the `@astroscope/node`
|
|
14
|
+
* islands pipeline (see islands-emitter.ts for the placement policy).
|
|
15
|
+
*/
|
|
16
|
+
/** values may contain `<\/script>` — escape `<` so they cannot end the tag */
|
|
17
|
+
function jsonForScript(value) {
|
|
18
|
+
return JSON.stringify(value).replace(/</g, "\\u003c");
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Inline script writing entries onto `self.__wormholes__` and notifying listeners —
|
|
22
|
+
* the counterpart of the browser proxy's entry protocol, safe in either order.
|
|
23
|
+
*/
|
|
24
|
+
function createWormholeMergeScript(entries) {
|
|
25
|
+
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>`;
|
|
26
|
+
}
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region src/manifest.ts
|
|
29
|
+
/** null in dev and when the build produced no manifest (broken deploy → full delivery fallback) */
|
|
30
|
+
function getWormholeManifest() {
|
|
31
|
+
return manifest;
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
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` */
|
|
36
|
+
function chunkName(url) {
|
|
37
|
+
return url.slice(url.lastIndexOf("/") + 1).replace(/\.js$/, "");
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The wormhole names the chunks of a closure can reach; `null` when one of them
|
|
41
|
+
* reads dynamically (`*` — all open wormholes).
|
|
42
|
+
*/
|
|
43
|
+
function namesInClosure(closure, manifest) {
|
|
44
|
+
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
|
+
}
|
|
49
|
+
return [...names];
|
|
50
|
+
}
|
|
51
|
+
//#endregion
|
|
52
|
+
//#region src/request-store.ts
|
|
53
|
+
const requestWormholes = /* @__PURE__ */ new WeakMap();
|
|
54
|
+
function setRequestWormholes(request, wormholes) {
|
|
55
|
+
requestWormholes.set(request, wormholes);
|
|
56
|
+
}
|
|
57
|
+
function getRequestWormholes(request) {
|
|
58
|
+
return requestWormholes.get(request);
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
61
|
+
//#region src/islands-emitter.ts
|
|
62
|
+
/**
|
|
63
|
+
* Contributes each island's wormhole slice: an inline merge script with the values
|
|
64
|
+
* of every open wormhole the island's full chunk closure (dynamic imports included)
|
|
65
|
+
* can reach, emitted before the island tag — parsed strictly before the island
|
|
66
|
+
* connects, even mid-stream. Names already written earlier in the document are
|
|
67
|
+
* skipped; a `*` chunk (dynamic proxy access) degrades to all open wormholes.
|
|
68
|
+
*/
|
|
69
|
+
const IS_DEV = !!import.meta.env?.DEV;
|
|
70
|
+
let registered = false;
|
|
71
|
+
let warnedMissingManifest = false;
|
|
72
|
+
const reachableByManifest = /* @__PURE__ */ new WeakMap();
|
|
73
|
+
function reachableFor(island, manifest) {
|
|
74
|
+
let byIsland = reachableByManifest.get(manifest);
|
|
75
|
+
if (!byIsland) {
|
|
76
|
+
byIsland = /* @__PURE__ */ new WeakMap();
|
|
77
|
+
reachableByManifest.set(manifest, byIsland);
|
|
78
|
+
}
|
|
79
|
+
const cached = byIsland.get(island);
|
|
80
|
+
if (cached !== void 0) return cached;
|
|
81
|
+
const reachable = namesInClosure(island.fullClosure, manifest);
|
|
82
|
+
byIsland.set(island, reachable);
|
|
83
|
+
return reachable;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The document-level wormhole delivery, exported for tests: everything open as
|
|
87
|
+
* `head` content in dev (or without a manifest), script-entry names as `end`
|
|
88
|
+
* content in prod — running after the rewriter, names the islands emitter
|
|
89
|
+
* already wrote are skipped race-free.
|
|
90
|
+
*/
|
|
91
|
+
function createWormholeDocumentEmitter(dev = IS_DEV) {
|
|
92
|
+
return (context) => {
|
|
93
|
+
const request = getRequestWormholes(context.request);
|
|
94
|
+
if (!request || request.values.size === 0) return null;
|
|
95
|
+
if (dev) return { head: createWormholeMergeScript(Object.fromEntries(request.values)) };
|
|
96
|
+
const manifest = getWormholeManifest();
|
|
97
|
+
if (!manifest) {
|
|
98
|
+
if (!warnedMissingManifest) {
|
|
99
|
+
warnedMissingManifest = true;
|
|
100
|
+
log.error("wormhole manifest not found — delivering all open wormholes with every page (is the integration configured?)");
|
|
101
|
+
}
|
|
102
|
+
return { head: createWormholeMergeScript(Object.fromEntries(request.values)) };
|
|
103
|
+
}
|
|
104
|
+
const names = manifest.scripts.includes("*") ? [...request.values.keys()] : manifest.scripts.filter((name) => request.values.has(name));
|
|
105
|
+
if (names.length === 0) return null;
|
|
106
|
+
return { end: () => {
|
|
107
|
+
const fresh = names.filter((name) => !request.emitted.has(name));
|
|
108
|
+
if (fresh.length === 0) return null;
|
|
109
|
+
return createWormholeMergeScript(Object.fromEntries(fresh.map((name) => [name, request.values.get(name)])));
|
|
110
|
+
} };
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function registerWormholeEmitters() {
|
|
114
|
+
if (registered) return;
|
|
115
|
+
registered = true;
|
|
116
|
+
registerDocumentEmitter(createWormholeDocumentEmitter());
|
|
117
|
+
registerIslandEmitter((island, context) => {
|
|
118
|
+
if (!context) return null;
|
|
119
|
+
const request = getRequestWormholes(context.request);
|
|
120
|
+
const manifest = getWormholeManifest();
|
|
121
|
+
if (!request || request.values.size === 0 || !manifest) return null;
|
|
122
|
+
const reachable = reachableFor(island, manifest);
|
|
123
|
+
const entries = {};
|
|
124
|
+
let count = 0;
|
|
125
|
+
for (const name of reachable ?? request.values.keys()) {
|
|
126
|
+
if (!request.values.has(name) || request.emitted.has(name)) continue;
|
|
127
|
+
request.emitted.add(name);
|
|
128
|
+
entries[name] = request.values.get(name);
|
|
129
|
+
count++;
|
|
130
|
+
}
|
|
131
|
+
if (count === 0) return null;
|
|
132
|
+
return { html: createWormholeMergeScript(entries) };
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
//#endregion
|
|
136
|
+
//#region src/telemetry.ts
|
|
137
|
+
const SCOPE = "@astroscope/wormhole";
|
|
138
|
+
const handlerDuration = createHistogram("astro.wormhole.handler.duration", {
|
|
139
|
+
scope: SCOPE,
|
|
140
|
+
description: "Wormhole handler execution per request",
|
|
141
|
+
unit: "s",
|
|
142
|
+
buckets: DURATION_BUCKETS.io
|
|
143
|
+
});
|
|
144
|
+
const handlerFailures = createCounter("astro.wormhole.handler.failures", {
|
|
145
|
+
scope: SCOPE,
|
|
146
|
+
description: "Wormhole handlers that threw",
|
|
147
|
+
unit: "{handler}"
|
|
148
|
+
});
|
|
149
|
+
/**
|
|
150
|
+
* Runs one handler under its own span (child of the request span — the
|
|
151
|
+
* handlers run in parallel, so the trace shows which one holds the page's
|
|
152
|
+
* first byte), records its duration and counts and logs a failure. A
|
|
153
|
+
* synchronous handler stays synchronous.
|
|
154
|
+
*/
|
|
155
|
+
function measureHandler(name, run) {
|
|
156
|
+
const attributes = { "astro.wormhole.name": name };
|
|
157
|
+
const started = performance.now();
|
|
158
|
+
const fail = (error) => {
|
|
159
|
+
handlerFailures.add(1, {
|
|
160
|
+
...attributes,
|
|
161
|
+
"error.type": errorType(error)
|
|
162
|
+
});
|
|
163
|
+
log.error(error instanceof Error ? {
|
|
164
|
+
err: error,
|
|
165
|
+
wormhole: name
|
|
166
|
+
} : {
|
|
167
|
+
reason: error,
|
|
168
|
+
wormhole: name
|
|
169
|
+
}, "wormhole handler failed");
|
|
170
|
+
throw error;
|
|
171
|
+
};
|
|
172
|
+
return withSpan(`wormhole ${name}`, {
|
|
173
|
+
scope: SCOPE,
|
|
174
|
+
attributes
|
|
175
|
+
}, () => {
|
|
176
|
+
let result;
|
|
177
|
+
try {
|
|
178
|
+
result = run();
|
|
179
|
+
} catch (error) {
|
|
180
|
+
return fail(error);
|
|
181
|
+
}
|
|
182
|
+
return maybeThen(result, (value) => {
|
|
183
|
+
handlerDuration.record((performance.now() - started) / 1e3, attributes);
|
|
184
|
+
return value;
|
|
185
|
+
}, fail);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
//#endregion
|
|
189
|
+
//#region src/middleware.ts
|
|
190
|
+
/**
|
|
191
|
+
* The wormhole middleware, injected by the integration after the project's own
|
|
192
|
+
* middleware: runs the handlers of the wormholes the request's route can read —
|
|
193
|
+
* server and `<script>` reads from the manifest, island reads through the route's
|
|
194
|
+
* islands from `@astroscope/node`, and eager wormholes — makes the values
|
|
195
|
+
* readable on the server for the duration of the request, and hands them to the
|
|
196
|
+
* islands pipeline for delivery. Routes the build could not attribute (dev, no
|
|
197
|
+
* manifest, astro's own routes) load everything.
|
|
198
|
+
*/
|
|
199
|
+
function createWormholeMiddleware(options = {}) {
|
|
200
|
+
assignWormholeNames(wormholes);
|
|
201
|
+
registerWormholeEmitters();
|
|
202
|
+
const sources = /* @__PURE__ */ new Map();
|
|
203
|
+
for (const [name, wormhole] of Object.entries(wormholes)) {
|
|
204
|
+
const source = getWormholeSource(wormhole);
|
|
205
|
+
if (!source) throw new Error(`wormhole "${name}" has no handler — define it with defineWormhole({ handler: (ctx) => ... })`);
|
|
206
|
+
sources.set(name, source);
|
|
207
|
+
}
|
|
208
|
+
const everything = [...sources.keys()];
|
|
209
|
+
const eager = everything.filter((name) => sources.get(name).eager);
|
|
210
|
+
const entriesByRoute = /* @__PURE__ */ new Map();
|
|
211
|
+
const routeNames = (pattern) => {
|
|
212
|
+
const manifest = getWormholeManifest();
|
|
213
|
+
if (!manifest) return null;
|
|
214
|
+
const server = manifest.routes[pattern];
|
|
215
|
+
const islands = getRouteIslands(pattern);
|
|
216
|
+
if (!server || !islands || server.includes("*")) return null;
|
|
217
|
+
const names = /* @__PURE__ */ new Set([...eager, ...server]);
|
|
218
|
+
for (const island of islands) {
|
|
219
|
+
const reachable = namesInClosure(island.fullClosure, manifest);
|
|
220
|
+
if (!reachable) return null;
|
|
221
|
+
reachable.forEach((name) => names.add(name));
|
|
222
|
+
}
|
|
223
|
+
return [...names].filter((name) => sources.has(name));
|
|
224
|
+
};
|
|
225
|
+
const entriesFor = (pattern) => {
|
|
226
|
+
let entries = entriesByRoute.get(pattern);
|
|
227
|
+
if (!entries) {
|
|
228
|
+
entries = (routeNames(pattern) ?? everything).map((name) => ({
|
|
229
|
+
name,
|
|
230
|
+
key: wormholes[name].key,
|
|
231
|
+
handler: sources.get(name).handler
|
|
232
|
+
}));
|
|
233
|
+
entriesByRoute.set(pattern, entries);
|
|
234
|
+
}
|
|
235
|
+
return entries;
|
|
236
|
+
};
|
|
237
|
+
return (ctx, next) => {
|
|
238
|
+
if (shouldExclude(ctx, options.exclude ?? RECOMMENDED_EXCLUDES)) return next();
|
|
239
|
+
const entries = entriesFor(ctx.routePattern);
|
|
240
|
+
return maybeThen(maybeAll(entries.map((entry) => measureHandler(entry.name, () => entry.handler(ctx)))), (loaded) => {
|
|
241
|
+
const open = /* @__PURE__ */ new Map();
|
|
242
|
+
const scope = new Map(als.getStore());
|
|
243
|
+
for (let i = 0; i < entries.length; i++) {
|
|
244
|
+
const data = loaded[i];
|
|
245
|
+
if (data === void 0) continue;
|
|
246
|
+
open.set(entries[i].name, data);
|
|
247
|
+
scope.set(entries[i].key, data);
|
|
248
|
+
}
|
|
249
|
+
setRequestWormholes(ctx.request, {
|
|
250
|
+
values: open,
|
|
251
|
+
emitted: /* @__PURE__ */ new Set()
|
|
252
|
+
});
|
|
253
|
+
return als.run(scope, () => next());
|
|
254
|
+
});
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
//#endregion
|
|
258
|
+
//#region src/middleware-entrypoint.ts
|
|
259
|
+
const onRequest = createWormholeMiddleware({ exclude: exclude ?? void 0 });
|
|
260
|
+
//#endregion
|
|
261
|
+
export { onRequest };
|
|
262
|
+
|
|
263
|
+
//# sourceMappingURL=middleware-entrypoint.js.map
|