@astroscope/node 1.4.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +32 -1
  2. package/dist/emitters-C20tjKLp.js +43 -0
  3. package/dist/emitters-C20tjKLp.js.map +1 -0
  4. package/dist/image-endpoint.d.ts +11 -0
  5. package/dist/image-endpoint.d.ts.map +1 -0
  6. package/dist/image-endpoint.js +11 -0
  7. package/dist/image-endpoint.js.map +1 -0
  8. package/dist/image-service.d.ts +15 -0
  9. package/dist/image-service.d.ts.map +1 -0
  10. package/dist/image-service.js +28 -0
  11. package/dist/image-service.js.map +1 -0
  12. package/dist/index.d.ts +24 -1
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +115 -7
  15. package/dist/index.js.map +1 -1
  16. package/dist/islands-middleware-entrypoint.d.ts +5 -0
  17. package/dist/islands-middleware-entrypoint.d.ts.map +1 -0
  18. package/dist/islands-middleware-entrypoint.js +58 -0
  19. package/dist/islands-middleware-entrypoint.js.map +1 -0
  20. package/dist/islands-runtime.d.ts +1 -0
  21. package/dist/islands-runtime.js +113 -0
  22. package/dist/islands-runtime.js.map +1 -0
  23. package/dist/islands.d.ts +2 -0
  24. package/dist/islands.js +3 -0
  25. package/dist/log-B69HEBvg.js.map +1 -1
  26. package/dist/native-mount-DjYEnO4X.js.map +1 -1
  27. package/dist/prepare-CXZsyAVk.js.map +1 -1
  28. package/dist/prerendered-CpEAJN_q.js +34 -0
  29. package/dist/prerendered-CpEAJN_q.js.map +1 -0
  30. package/dist/route-store-DdxGePj2.js +27 -0
  31. package/dist/route-store-DdxGePj2.js.map +1 -0
  32. package/dist/route-store-DtY3uvLf.d.ts +69 -0
  33. package/dist/route-store-DtY3uvLf.d.ts.map +1 -0
  34. package/dist/server.js +13 -11
  35. package/dist/server.js.map +1 -1
  36. package/dist/strip-build-paths-C6eb1QdG.js +63 -0
  37. package/dist/strip-build-paths-C6eb1QdG.js.map +1 -0
  38. package/dist/transform-D8dIBGEr.js +191 -0
  39. package/dist/transform-D8dIBGEr.js.map +1 -0
  40. package/package.json +29 -10
@@ -0,0 +1,63 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { pathToFileURL } from "node:url";
4
+ //#region src/tweaks/strip-build-paths.ts
5
+ /**
6
+ * Stable stand-in for the build machine's project root in emitted server code.
7
+ * Only relative relationships matter at runtime (see `resolveClientDir`), so the
8
+ * value is never dereferenced — it just has to be consistent across the bundle.
9
+ */
10
+ const ROOT_PLACEHOLDER = "/_astroscope";
11
+ const TEXT_EXTENSIONS = /* @__PURE__ */ new Set([
12
+ ".mjs",
13
+ ".js",
14
+ ".cjs",
15
+ ".json",
16
+ ".map"
17
+ ]);
18
+ const HOISTED_STORE_RE = /(file:\/\/)?\/[^"'`\s]*?\/node_modules\/([^"'`\s]*)/g;
19
+ const MACHINE_PATH_RE = /(file:\/\/)?\/(?:Users|home)\/[^"'`\s]+/g;
20
+ function collectFiles(dir, out) {
21
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
22
+ const abs = path.join(dir, entry.name);
23
+ if (entry.isDirectory()) collectFiles(abs, out);
24
+ else if (TEXT_EXTENSIONS.has(path.extname(entry.name))) out.push(abs);
25
+ }
26
+ }
27
+ /**
28
+ * Replace the build machine's project root in the server build output with a
29
+ * stable placeholder.
30
+ *
31
+ * Astro serializes absolute `file://` config URLs (root, outDir, srcDir, ...)
32
+ * and absolute component module ids into the SSR manifest, and the compiled
33
+ * chunks and sourcemaps carry the same absolute paths. Shipping them makes the
34
+ * artifact irreproducible and leaks the developer's filesystem layout. The
35
+ * replacement is applied uniformly, so manifest keys keep matching the module
36
+ * ids embedded in chunks, and the server→client relative resolution keeps
37
+ * working.
38
+ *
39
+ * Returns the number of files rewritten.
40
+ */
41
+ function stripBuildPaths(serverDir, root) {
42
+ const rootFsPosix = root.split(path.sep).join("/").replace(/\/+$/, "");
43
+ const rootUrl = pathToFileURL(rootFsPosix).href;
44
+ const files = [];
45
+ collectFiles(serverDir, files);
46
+ let rewritten = 0;
47
+ for (const file of files) {
48
+ const content = fs.readFileSync(file, "utf-8");
49
+ const replaced = content.replaceAll(rootUrl, `file://${ROOT_PLACEHOLDER}`).replaceAll(rootFsPosix, ROOT_PLACEHOLDER).replace(HOISTED_STORE_RE, (_, proto, suffix) => `${proto ?? ""}${ROOT_PLACEHOLDER}/node_modules/${suffix}`).replace(MACHINE_PATH_RE, (match, proto) => {
50
+ const rel = path.posix.relative(rootFsPosix, proto ? match.slice(proto.length) : match);
51
+ return `${proto ?? ""}${ROOT_PLACEHOLDER}/${rel}`;
52
+ });
53
+ if (replaced !== content) {
54
+ fs.writeFileSync(file, replaced);
55
+ rewritten++;
56
+ }
57
+ }
58
+ return rewritten;
59
+ }
60
+ //#endregion
61
+ export { stripBuildPaths };
62
+
63
+ //# sourceMappingURL=strip-build-paths-C6eb1QdG.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"strip-build-paths-C6eb1QdG.js","names":[],"sources":["../src/tweaks/strip-build-paths.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\nimport { pathToFileURL } from 'node:url';\n\n/**\n * Stable stand-in for the build machine's project root in emitted server code.\n * Only relative relationships matter at runtime (see `resolveClientDir`), so the\n * value is never dereferenced — it just has to be consistent across the bundle.\n */\nexport const ROOT_PLACEHOLDER = '/_astroscope';\n\nconst TEXT_EXTENSIONS = new Set(['.mjs', '.js', '.cjs', '.json', '.map']);\n\n// absolute module ids pointing into a dependency store hoisted above the project\n// root (pnpm workspaces); the prefix before the first node_modules is collapsed,\n// the store-relative suffix keeps ids unique and matching. the greedy suffix\n// consumes the whole path token so a nested node_modules is not re-matched\nconst HOISTED_STORE_RE = /(file:\\/\\/)?\\/[^\"'`\\s]*?\\/node_modules\\/([^\"'`\\s]*)/g;\n\n// remaining machine paths outside the root and outside any store (workspace\n// siblings, linked package sources): rewritten relative to the root, which keeps\n// the mapping unique without shipping the home directory or the layout above root\nconst MACHINE_PATH_RE = /(file:\\/\\/)?\\/(?:Users|home)\\/[^\"'`\\s]+/g;\n\nfunction collectFiles(dir: string, out: string[]): void {\n for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {\n const abs = path.join(dir, entry.name);\n\n if (entry.isDirectory()) {\n collectFiles(abs, out);\n } else if (TEXT_EXTENSIONS.has(path.extname(entry.name))) {\n out.push(abs);\n }\n }\n}\n\n/**\n * Replace the build machine's project root in the server build output with a\n * stable placeholder.\n *\n * Astro serializes absolute `file://` config URLs (root, outDir, srcDir, ...)\n * and absolute component module ids into the SSR manifest, and the compiled\n * chunks and sourcemaps carry the same absolute paths. Shipping them makes the\n * artifact irreproducible and leaks the developer's filesystem layout. The\n * replacement is applied uniformly, so manifest keys keep matching the module\n * ids embedded in chunks, and the server→client relative resolution keeps\n * working.\n *\n * Returns the number of files rewritten.\n */\nexport function stripBuildPaths(serverDir: string, root: string): number {\n const rootFsPosix = root.split(path.sep).join('/').replace(/\\/+$/, '');\n const rootUrl = pathToFileURL(rootFsPosix).href;\n\n const files: string[] = [];\n\n collectFiles(serverDir, files);\n\n let rewritten = 0;\n\n for (const file of files) {\n const content = fs.readFileSync(file, 'utf-8');\n const replaced = content\n .replaceAll(rootUrl, `file://${ROOT_PLACEHOLDER}`)\n .replaceAll(rootFsPosix, ROOT_PLACEHOLDER)\n .replace(\n HOISTED_STORE_RE,\n (_, proto: string | undefined, suffix: string) => `${proto ?? ''}${ROOT_PLACEHOLDER}/node_modules/${suffix}`,\n )\n .replace(MACHINE_PATH_RE, (match, proto: string | undefined) => {\n const rel = path.posix.relative(rootFsPosix, proto ? match.slice(proto.length) : match);\n\n return `${proto ?? ''}${ROOT_PLACEHOLDER}/${rel}`;\n });\n\n if (replaced !== content) {\n fs.writeFileSync(file, replaced);\n rewritten++;\n }\n }\n\n return rewritten;\n}\n"],"mappings":";;;;;;;;;AASA,MAAa,mBAAmB;AAEhC,MAAM,kCAAkB,IAAI,IAAI;CAAC;CAAQ;CAAO;CAAQ;CAAS;AAAM,CAAC;AAMxE,MAAM,mBAAmB;AAKzB,MAAM,kBAAkB;AAExB,SAAS,aAAa,KAAa,KAAqB;CACtD,KAAK,MAAM,SAAS,GAAG,YAAY,KAAK,EAAE,eAAe,KAAK,CAAC,GAAG;EAChE,MAAM,MAAM,KAAK,KAAK,KAAK,MAAM,IAAI;EAErC,IAAI,MAAM,YAAY,GACpB,aAAa,KAAK,GAAG;OAChB,IAAI,gBAAgB,IAAI,KAAK,QAAQ,MAAM,IAAI,CAAC,GACrD,IAAI,KAAK,GAAG;CAEhB;AACF;;;;;;;;;;;;;;;AAgBA,SAAgB,gBAAgB,WAAmB,MAAsB;CACvE,MAAM,cAAc,KAAK,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,QAAQ,QAAQ,EAAE;CACrE,MAAM,UAAU,cAAc,WAAW,CAAC,CAAC;CAE3C,MAAM,QAAkB,CAAC;CAEzB,aAAa,WAAW,KAAK;CAE7B,IAAI,YAAY;CAEhB,KAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,UAAU,GAAG,aAAa,MAAM,OAAO;EAC7C,MAAM,WAAW,QACd,WAAW,SAAS,UAAU,kBAAkB,CAAC,CACjD,WAAW,aAAa,gBAAgB,CAAC,CACzC,QACC,mBACC,GAAG,OAA2B,WAAmB,GAAG,SAAS,KAAK,iBAAiB,gBAAgB,QACtG,CAAC,CACA,QAAQ,kBAAkB,OAAO,UAA8B;GAC9D,MAAM,MAAM,KAAK,MAAM,SAAS,aAAa,QAAQ,MAAM,MAAM,MAAM,MAAM,IAAI,KAAK;GAEtF,OAAO,GAAG,SAAS,KAAK,iBAAiB,GAAG;EAC9C,CAAC;EAEH,IAAI,aAAa,SAAS;GACxB,GAAG,cAAc,MAAM,QAAQ;GAC/B;EACF;CACF;CAEA,OAAO;AACT"}
@@ -0,0 +1,191 @@
1
+ import { n as log } from "./log-B69HEBvg.js";
2
+ import { n as getIslandEmitters } from "./emitters-C20tjKLp.js";
3
+ import { RewritingStream } from "parse5-html-rewriting-stream";
4
+ //#region src/islands/graph.ts
5
+ function createClosure(chunks, edges) {
6
+ const cache = /* @__PURE__ */ new Map();
7
+ const walk = (fileName, result, visited) => {
8
+ if (visited.has(fileName)) return;
9
+ visited.add(fileName);
10
+ const entry = chunks[fileName];
11
+ if (!entry) return;
12
+ for (const dep of edges(entry)) {
13
+ result.add(dep);
14
+ walk(dep, result, visited);
15
+ }
16
+ };
17
+ return (fileName) => {
18
+ const cached = cache.get(fileName);
19
+ if (cached) return cached;
20
+ const result = /* @__PURE__ */ new Set();
21
+ walk(fileName, result, /* @__PURE__ */ new Set());
22
+ result.delete(fileName);
23
+ const list = [...result];
24
+ cache.set(fileName, list);
25
+ return list;
26
+ };
27
+ }
28
+ function createChunkGraph(manifest) {
29
+ return {
30
+ staticClosure: createClosure(manifest.chunks, (entry) => entry.i ?? []),
31
+ fullClosure: createClosure(manifest.chunks, (entry) => [...entry.i ?? [], ...entry.d ?? []]),
32
+ has: (fileName) => fileName in manifest.chunks
33
+ };
34
+ }
35
+ //#endregion
36
+ //#region src/islands/rewriter.ts
37
+ const ISLAND = "astro-island";
38
+ function encodeAttribute(value) {
39
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;");
40
+ }
41
+ function createIslandRewriter(onIsland) {
42
+ const stream = new RewritingStream();
43
+ const out = [];
44
+ stream.on("data", (chunk) => {
45
+ out.push(typeof chunk === "string" ? chunk : chunk.toString());
46
+ });
47
+ stream.on("startTag", (tag, rawHtml) => {
48
+ if (tag.tagName === ISLAND) {
49
+ const attrs = {};
50
+ for (const attr of tag.attrs) attrs[attr.name] = attr.value;
51
+ const rewrite = onIsland(attrs);
52
+ if (rewrite?.prepend) stream.emitRaw(rewrite.prepend);
53
+ }
54
+ stream.emitRaw(rawHtml);
55
+ });
56
+ const drain = () => out.splice(0).join("");
57
+ return {
58
+ write(chunk) {
59
+ stream.write(chunk);
60
+ return drain();
61
+ },
62
+ end() {
63
+ return new Promise((resolve, reject) => {
64
+ stream.once("error", reject);
65
+ stream.end(() => resolve(drain()));
66
+ });
67
+ }
68
+ };
69
+ }
70
+ const IMMEDIATE_DIRECTIVES = /* @__PURE__ */ new Set(["load", "only"]);
71
+ /** urls cannot contain `<\/script>`, but escape `<` anyway so no value ever can */
72
+ function jsonForScript(value) {
73
+ return JSON.stringify(value).replace(/</g, "\\u003c");
74
+ }
75
+ function createIslandsTransformer(manifest) {
76
+ const graph = createChunkGraph(manifest);
77
+ const fileNames = Object.keys(manifest.chunks);
78
+ const resolveCache = /* @__PURE__ */ new Map();
79
+ const resolve = (url) => {
80
+ const cached = resolveCache.get(url);
81
+ if (cached !== void 0) return cached;
82
+ let resolved = null;
83
+ for (const fileName of fileNames) if (url.endsWith(fileName)) {
84
+ const boundary = url.length - fileName.length;
85
+ if (boundary === 0 || url[boundary - 1] === "/") {
86
+ resolved = {
87
+ prefix: url.slice(0, boundary),
88
+ fileName
89
+ };
90
+ break;
91
+ }
92
+ }
93
+ resolveCache.set(url, resolved);
94
+ return resolved;
95
+ };
96
+ const createDocumentRewriter = (context) => {
97
+ const emittedLinks = /* @__PURE__ */ new Set();
98
+ const registeredComponents = /* @__PURE__ */ new Set();
99
+ let runtimeInjected = false;
100
+ return createIslandRewriter((attrs) => {
101
+ const componentUrl = attrs["component-url"];
102
+ if (!componentUrl) return null;
103
+ const component = resolve(componentUrl);
104
+ if (!component) return null;
105
+ const rendererUrl = attrs["renderer-url"] || null;
106
+ const renderer = rendererUrl ? resolve(rendererUrl) : null;
107
+ const closureUrls = (closure) => {
108
+ const urls = /* @__PURE__ */ new Set([componentUrl, ...closure(component.fileName).map((f) => component.prefix + f)]);
109
+ if (rendererUrl) {
110
+ urls.add(rendererUrl);
111
+ for (const f of renderer ? closure(renderer.fileName) : []) urls.add(renderer.prefix + f);
112
+ }
113
+ return [...urls];
114
+ };
115
+ const island = {
116
+ componentUrl,
117
+ rendererUrl,
118
+ client: attrs["client"] || null,
119
+ staticClosure: closureUrls(graph.staticClosure),
120
+ fullClosure: closureUrls(graph.fullClosure)
121
+ };
122
+ const emissions = [{ links: island.staticClosure }];
123
+ for (const emitter of getIslandEmitters()) try {
124
+ const emission = emitter(island, context);
125
+ if (emission) emissions.push(emission);
126
+ } catch (error) {
127
+ log.error({
128
+ err: error,
129
+ componentUrl
130
+ }, "island emitter failed");
131
+ }
132
+ const links = [...new Set(emissions.flatMap((e) => e.links ?? []))];
133
+ const html = emissions.map((e) => e.html ?? "").join("");
134
+ const directive = (island.client ?? "").replace(/-x$/, "");
135
+ if (IMMEDIATE_DIRECTIVES.has(directive)) {
136
+ const fresh = links.filter((url) => !emittedLinks.has(url));
137
+ for (const url of fresh) emittedLinks.add(url);
138
+ return { prepend: html + fresh.map((url) => `<link rel="modulepreload" fetchpriority="low" href="${encodeAttribute(url)}">`).join("") || void 0 };
139
+ }
140
+ if (registeredComponents.has(componentUrl)) return { prepend: html || void 0 };
141
+ registeredComponents.add(componentUrl);
142
+ const runtime = runtimeInjected ? "" : `<script type="module" src="${encodeAttribute(component.prefix + manifest.runtime)}"><\/script>`;
143
+ runtimeInjected = true;
144
+ return { prepend: runtime + `<script>(self.__islands__??={})[${jsonForScript(componentUrl)}]=${jsonForScript(links)};<\/script>` + html || void 0 };
145
+ });
146
+ };
147
+ return { createDocumentRewriter };
148
+ }
149
+ /**
150
+ * The single streaming pass over an html page response: chunks go through the
151
+ * islands rewriter (when present), head content is inserted right after the
152
+ * opening `<head>` tag (which requires buffering the whole document — head
153
+ * content is dev-style delivery, where throughput is irrelevant), and end
154
+ * factories run last, strictly after every island passed the rewriter.
155
+ */
156
+ function createPageTransformStream(options) {
157
+ const { rewriter, heads, ends } = options;
158
+ const decoder = new TextDecoder();
159
+ const encoder = new TextEncoder();
160
+ const buffered = heads.length > 0 ? [] : null;
161
+ const enqueue = (controller, text) => {
162
+ if (text) controller.enqueue(encoder.encode(text));
163
+ };
164
+ return new TransformStream({
165
+ transform(chunk, controller) {
166
+ let text = decoder.decode(chunk, { stream: true });
167
+ if (rewriter) text = rewriter.write(text);
168
+ if (buffered) buffered.push(text);
169
+ else enqueue(controller, text);
170
+ },
171
+ async flush(controller) {
172
+ let tail = decoder.decode();
173
+ if (rewriter) tail = (tail ? rewriter.write(tail) : "") + await rewriter.end();
174
+ if (buffered) {
175
+ const html = buffered.join("") + tail;
176
+ const head = /<head[^>]*>/i.exec(html);
177
+ const at = head ? head.index + head[0].length : 0;
178
+ enqueue(controller, html.slice(0, at) + heads.join("") + html.slice(at));
179
+ } else enqueue(controller, tail);
180
+ for (const end of ends) try {
181
+ enqueue(controller, end() ?? "");
182
+ } catch (error) {
183
+ log.error({ err: error }, "document emitter failed");
184
+ }
185
+ }
186
+ });
187
+ }
188
+ //#endregion
189
+ export { createPageTransformStream as n, createIslandsTransformer as t };
190
+
191
+ //# sourceMappingURL=transform-D8dIBGEr.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transform-D8dIBGEr.js","names":[],"sources":["../src/islands/graph.ts","../src/islands/rewriter.ts","../src/islands/transform.ts"],"sourcesContent":["import type { IslandsManifest } from './types.js';\n\n/**\n * Closure computation over the chunk import graph, memoized per manifest. The\n * manifest stores direct edges only — flattened closures repeat heavily across\n * entries and would bloat the JSON.\n */\nexport type ChunkGraph = {\n /** transitive static imports of a chunk, the chunk itself excluded */\n staticClosure(fileName: string): string[];\n /** transitive static + dynamic imports of a chunk, the chunk itself excluded */\n fullClosure(fileName: string): string[];\n has(fileName: string): boolean;\n};\n\nfunction createClosure(\n chunks: IslandsManifest['chunks'],\n edges: (entry: { i?: string[] | undefined; d?: string[] | undefined }) => string[],\n): (fileName: string) => string[] {\n const cache = new Map<string, string[]>();\n\n const walk = (fileName: string, result: Set<string>, visited: Set<string>): void => {\n if (visited.has(fileName)) {\n return;\n }\n\n visited.add(fileName);\n\n const entry = chunks[fileName];\n\n if (!entry) {\n return;\n }\n\n for (const dep of edges(entry)) {\n result.add(dep);\n walk(dep, result, visited);\n }\n };\n\n return (fileName) => {\n const cached = cache.get(fileName);\n\n if (cached) {\n return cached;\n }\n\n const result = new Set<string>();\n\n walk(fileName, result, new Set());\n result.delete(fileName);\n\n const list = [...result];\n\n cache.set(fileName, list);\n\n return list;\n };\n}\n\nexport function createChunkGraph(manifest: IslandsManifest): ChunkGraph {\n return {\n staticClosure: createClosure(manifest.chunks, (entry) => entry.i ?? []),\n fullClosure: createClosure(manifest.chunks, (entry) => [...(entry.i ?? []), ...(entry.d ?? [])]),\n has: (fileName) => fileName in manifest.chunks,\n };\n}\n","import { RewritingStream } from 'parse5-html-rewriting-stream';\n\n/**\n * Streaming HTML rewriter that finds `<astro-island …>` opening tags, hands their\n * attributes to a handler, and prepends the handler's html before the tag — the\n * tag itself is always re-emitted from its raw source, byte-verbatim.\n *\n * Tokenization is parse5's — the reference WHATWG implementation — so every\n * context where island markup is not actually an element to the browser\n * (comments, raw-text elements like `script`/`iframe`/`noscript`, doctypes,\n * quoted attribute values) is skipped by construction rather than by our own\n * spec-tracking; unmodified content passes through as raw source slices.\n * Attribute values arrive with the full entity set decoded.\n *\n * Closing tags are never touched: injection happens before the island's opening\n * tag precisely so the island's subtree — the hydration target — stays untouched.\n *\n * The test suite pins the behavior this package relies on, so a parse5 upgrade\n * that changes it fails here rather than degrading silently — same philosophy as\n * the astro compatibility surface.\n */\n\nexport type IslandTagRewrite = {\n /** html emitted immediately before the island opening tag */\n prepend?: string | undefined;\n};\n\nexport type IslandTagHandler = (attrs: Record<string, string>) => IslandTagRewrite | null;\n\nexport type IslandRewriter = {\n write(chunk: string): string;\n end(): Promise<string>;\n};\n\nconst ISLAND = 'astro-island';\n\nexport function encodeAttribute(value: string): string {\n return value.replace(/&/g, '&amp;').replace(/\"/g, '&quot;').replace(/</g, '&lt;');\n}\n\nexport function createIslandRewriter(onIsland: IslandTagHandler): IslandRewriter {\n const stream = new RewritingStream();\n const out: string[] = [];\n\n stream.on('data', (chunk: string | Buffer) => {\n out.push(typeof chunk === 'string' ? chunk : chunk.toString());\n });\n\n stream.on('startTag', (tag, rawHtml) => {\n if (tag.tagName === ISLAND) {\n const attrs: Record<string, string> = {};\n\n for (const attr of tag.attrs) {\n attrs[attr.name] = attr.value;\n }\n\n const rewrite = onIsland(attrs);\n\n if (rewrite?.prepend) {\n stream.emitRaw(rewrite.prepend);\n }\n }\n\n stream.emitRaw(rawHtml);\n });\n\n const drain = (): string => out.splice(0).join('');\n\n return {\n // a transform stream with an attached data listener processes writes\n // synchronously, so each write can hand back its output right away\n write(chunk) {\n stream.write(chunk);\n\n return drain();\n },\n end() {\n return new Promise((resolve, reject) => {\n stream.once('error', reject);\n stream.end(() => resolve(drain()));\n });\n },\n };\n}\n","import type { APIContext } from 'astro';\nimport { log } from '../observability/log/index.js';\nimport { getIslandEmitters } from './emitters.js';\nimport { createChunkGraph } from './graph.js';\nimport { type IslandRewriter, createIslandRewriter, encodeAttribute } from './rewriter.js';\nimport type { IslandEmission, IslandInfo, IslandsManifest } from './types.js';\n\n/**\n * Turns the manifest into per-document rewriters: resolves each island's chunk\n * closures, collects emissions (the built-in preloader plus registered emitters),\n * and applies the emission policy —\n *\n * - immediate directives (`load`, `only`) get `<link rel=\"modulepreload\">` tags\n * before the tag, deduplicated per document; the browser's speculative preload\n * scanner acts on them from raw bytes, ahead of any script.\n * - deferred directives get an inline script before the first island per\n * component that registers the links on the preload global, keyed by\n * `component-url`; the gate runtime fires them by the directive's own\n * scheduling semantics. A script rather than an attribute: it takes effect at\n * parse time and its effect survives the island element being removed — the\n * island tag itself stays untouched.\n *\n * The gate runtime script is injected once, before the first island that needs it,\n * so pages without deferred islands never load it.\n */\n\n/** global registry of preload urls per component-url, written by the transform, read by the gate runtime */\nexport const PRELOAD_GLOBAL = '__islands__';\n\nconst IMMEDIATE_DIRECTIVES = new Set(['load', 'only']);\n\n/** urls cannot contain `</script>`, but escape `<` anyway so no value ever can */\nfunction jsonForScript(value: unknown): string {\n return JSON.stringify(value).replace(/</g, '\\\\u003c');\n}\n\ntype ResolvedChunk = { prefix: string; fileName: string };\n\nexport type IslandsTransformer = {\n createDocumentRewriter(context?: APIContext | undefined): IslandRewriter;\n};\n\nexport function createIslandsTransformer(manifest: IslandsManifest): IslandsTransformer {\n const graph = createChunkGraph(manifest);\n const fileNames = Object.keys(manifest.chunks);\n const resolveCache = new Map<string, ResolvedChunk | null>();\n\n // islands carry public urls (base and asset prefix baked in), the manifest holds\n // dist-relative file names — match by suffix at a path boundary, so the observed\n // prefix maps closures and the runtime script back to public urls with no config\n const resolve = (url: string): ResolvedChunk | null => {\n const cached = resolveCache.get(url);\n\n if (cached !== undefined) {\n return cached;\n }\n\n let resolved: ResolvedChunk | null = null;\n\n for (const fileName of fileNames) {\n if (url.endsWith(fileName)) {\n const boundary = url.length - fileName.length;\n\n if (boundary === 0 || url[boundary - 1] === '/') {\n resolved = { prefix: url.slice(0, boundary), fileName };\n break;\n }\n }\n }\n\n resolveCache.set(url, resolved);\n\n return resolved;\n };\n\n const createDocumentRewriter = (context?: APIContext | undefined): IslandRewriter => {\n const emittedLinks = new Set<string>();\n const registeredComponents = new Set<string>();\n let runtimeInjected = false;\n\n return createIslandRewriter((attrs) => {\n const componentUrl = attrs['component-url'];\n\n if (!componentUrl) {\n return null;\n }\n\n const component = resolve(componentUrl);\n\n if (!component) {\n return null;\n }\n\n const rendererUrl = attrs['renderer-url'] || null;\n const renderer = rendererUrl ? resolve(rendererUrl) : null;\n\n const closureUrls = (closure: (fileName: string) => string[]): string[] => {\n const urls = new Set([componentUrl, ...closure(component.fileName).map((f) => component.prefix + f)]);\n\n if (rendererUrl) {\n urls.add(rendererUrl);\n\n for (const f of renderer ? closure(renderer.fileName) : []) {\n urls.add(renderer!.prefix + f);\n }\n }\n\n return [...urls];\n };\n\n const island: IslandInfo = {\n componentUrl,\n rendererUrl,\n client: attrs['client'] || null,\n staticClosure: closureUrls(graph.staticClosure),\n fullClosure: closureUrls(graph.fullClosure),\n };\n\n const emissions: IslandEmission[] = [{ links: island.staticClosure }];\n\n for (const emitter of getIslandEmitters()) {\n try {\n const emission = emitter(island, context);\n\n if (emission) {\n emissions.push(emission);\n }\n } catch (error) {\n // an emitter must never break the page — its contribution is dropped\n log.error({ err: error, componentUrl }, 'island emitter failed');\n }\n }\n\n const links = [...new Set(emissions.flatMap((e) => e.links ?? []))];\n const html = emissions.map((e) => e.html ?? '').join('');\n const directive = (island.client ?? '').replace(/-x$/, '');\n\n if (IMMEDIATE_DIRECTIVES.has(directive)) {\n const fresh = links.filter((url) => !emittedLinks.has(url));\n\n for (const url of fresh) {\n emittedLinks.add(url);\n }\n\n const prepend =\n html +\n fresh.map((url) => `<link rel=\"modulepreload\" fetchpriority=\"low\" href=\"${encodeAttribute(url)}\">`).join('');\n\n return { prepend: prepend || undefined };\n }\n\n if (registeredComponents.has(componentUrl)) {\n return { prepend: html || undefined };\n }\n\n registeredComponents.add(componentUrl);\n\n const runtime = runtimeInjected\n ? ''\n : `<script type=\"module\" src=\"${encodeAttribute(component.prefix + manifest.runtime)}\"></script>`;\n\n runtimeInjected = true;\n\n const register = `<script>(self.${PRELOAD_GLOBAL}??={})[${jsonForScript(componentUrl)}]=${jsonForScript(links)};</script>`;\n\n return { prepend: runtime + register + html || undefined };\n });\n };\n\n return { createDocumentRewriter };\n}\n\nexport type PageTransformOptions = {\n /** the islands rewriter for this document, when preloading is on (prod with a manifest) */\n rewriter?: IslandRewriter | undefined;\n /** scripts inserted right after the opening `<head>` tag — buffers the response */\n heads: string[];\n /** script factories invoked after the rewriter finished, results appended at stream end */\n ends: (() => string | null)[];\n};\n\n/**\n * The single streaming pass over an html page response: chunks go through the\n * islands rewriter (when present), head content is inserted right after the\n * opening `<head>` tag (which requires buffering the whole document — head\n * content is dev-style delivery, where throughput is irrelevant), and end\n * factories run last, strictly after every island passed the rewriter.\n */\nexport function createPageTransformStream(options: PageTransformOptions): TransformStream<Uint8Array, Uint8Array> {\n const { rewriter, heads, ends } = options;\n const decoder = new TextDecoder();\n const encoder = new TextEncoder();\n const buffered = heads.length > 0 ? ([] as string[]) : null;\n\n const enqueue = (controller: TransformStreamDefaultController<Uint8Array>, text: string): void => {\n if (text) {\n controller.enqueue(encoder.encode(text));\n }\n };\n\n return new TransformStream({\n transform(chunk, controller) {\n let text = decoder.decode(chunk, { stream: true });\n\n if (rewriter) {\n text = rewriter.write(text);\n }\n\n if (buffered) {\n buffered.push(text);\n } else {\n enqueue(controller, text);\n }\n },\n async flush(controller) {\n let tail = decoder.decode();\n\n if (rewriter) {\n tail = (tail ? rewriter.write(tail) : '') + (await rewriter.end());\n }\n\n if (buffered) {\n const html = buffered.join('') + tail;\n // nothing but the doctype, comments and <html> can precede <head>, so the\n // first match cannot sit inside script raw text\n const head = /<head[^>]*>/i.exec(html);\n const at = head ? head.index + head[0].length : 0;\n\n enqueue(controller, html.slice(0, at) + heads.join('') + html.slice(at));\n } else {\n enqueue(controller, tail);\n }\n\n for (const end of ends) {\n try {\n enqueue(controller, end() ?? '');\n } catch (error) {\n // an emitter must never break the page — its contribution is dropped\n log.error({ err: error }, 'document emitter failed');\n }\n }\n },\n });\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astroscope/node",
3
- "version": "1.4.0",
3
+ "version": "2.0.1",
4
4
  "description": "Opinionated, cloud-friendly Node adapter for Astro: boot lifecycle, health probes, request logging, telemetry, CSRF and static serving run as plain code around server.listen()",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -51,6 +51,23 @@
51
51
  "types": "./dist/dev-middleware-entrypoint.d.ts",
52
52
  "import": "./dist/dev-middleware-entrypoint.js"
53
53
  },
54
+ "./image-endpoint": {
55
+ "types": "./dist/image-endpoint.d.ts",
56
+ "import": "./dist/image-endpoint.js",
57
+ "default": "./dist/image-endpoint.js"
58
+ },
59
+ "./image-service": {
60
+ "types": "./dist/image-service.d.ts",
61
+ "import": "./dist/image-service.js"
62
+ },
63
+ "./islands": {
64
+ "types": "./dist/islands.d.ts",
65
+ "import": "./dist/islands.js"
66
+ },
67
+ "./islands-middleware": {
68
+ "types": "./dist/islands-middleware-entrypoint.d.ts",
69
+ "import": "./dist/islands-middleware-entrypoint.js"
70
+ },
54
71
  "./route-middleware": {
55
72
  "types": "./dist/route-middleware-entrypoint.d.ts",
56
73
  "import": "./dist/route-middleware-entrypoint.js"
@@ -82,24 +99,26 @@
82
99
  },
83
100
  "homepage": "https://github.com/entwico/astroscope/tree/main/packages/node#readme",
84
101
  "dependencies": {
85
- "@astrojs/compiler-rs": "^0.3.1",
102
+ "@astrojs/compiler-rs": "^0.4.0",
86
103
  "@entwico/dash": "^1.3.0",
87
104
  "@entwico/health-probes": "^1.1.2",
88
105
  "@opentelemetry/api": "^1.9.1",
89
- "@opentelemetry/exporter-prometheus": "^0.220.0",
106
+ "@opentelemetry/exporter-prometheus": "^0.221.0",
90
107
  "@opentelemetry/host-metrics": "^0.39.0",
91
- "@opentelemetry/instrumentation-runtime-node": "^0.33.0",
92
- "@opentelemetry/instrumentation-undici": "^0.30.0",
93
- "@opentelemetry/sdk-node": "^0.220.0",
94
- "acorn": "^8.17.0",
95
- "magic-string": "^0.30.21",
108
+ "@opentelemetry/instrumentation-runtime-node": "^0.34.0",
109
+ "@opentelemetry/instrumentation-undici": "^0.31.0",
110
+ "@opentelemetry/sdk-node": "^0.221.0",
111
+ "acorn": "^8.18.0",
112
+ "magic-string": "^1.2.2",
113
+ "parse5-html-rewriting-stream": "^8.0.1",
96
114
  "pino": "^10.3.1",
97
115
  "send": "^1.2.1"
98
116
  },
99
117
  "devDependencies": {
100
118
  "@types/send": "^1.2.1",
101
- "astro": "^7.0.9",
102
- "tsdown": "^0.22.8",
119
+ "astro": "^7.2.6",
120
+ "happy-dom": "^20.11.6",
121
+ "tsdown": "^0.22.14",
103
122
  "typescript": "^6.0.3"
104
123
  },
105
124
  "peerDependencies": {