@astroscope/node 1.3.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.
Files changed (65) hide show
  1. package/README.md +33 -1
  2. package/dist/boot.d.ts +0 -1
  3. package/dist/boot.d.ts.map +1 -1
  4. package/dist/{construct-DgB-jR0a.d.ts → construct-BGlPfWMF.d.ts} +1 -2
  5. package/dist/construct-BGlPfWMF.d.ts.map +1 -0
  6. package/dist/csrf-middleware-entrypoint.d.ts.map +1 -1
  7. package/dist/dev-middleware-entrypoint.d.ts +0 -1
  8. package/dist/dev-middleware-entrypoint.d.ts.map +1 -1
  9. package/dist/emitters-C20tjKLp.js +43 -0
  10. package/dist/emitters-C20tjKLp.js.map +1 -0
  11. package/dist/{events-CUzQ2_cp.d.ts → events-u7J3ezJR.d.ts} +1 -2
  12. package/dist/events-u7J3ezJR.d.ts.map +1 -0
  13. package/dist/{excludes-DLF3A_Cf.d.ts → excludes-BDiE3eyp.d.ts} +1 -2
  14. package/dist/excludes-BDiE3eyp.d.ts.map +1 -0
  15. package/dist/excludes.d.ts +1 -1
  16. package/dist/health.d.ts.map +1 -1
  17. package/dist/image-endpoint.d.ts +11 -0
  18. package/dist/image-endpoint.d.ts.map +1 -0
  19. package/dist/image-endpoint.js +11 -0
  20. package/dist/image-endpoint.js.map +1 -0
  21. package/dist/image-service.d.ts +15 -0
  22. package/dist/image-service.d.ts.map +1 -0
  23. package/dist/image-service.js +28 -0
  24. package/dist/image-service.js.map +1 -0
  25. package/dist/index.d.ts +27 -4
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +280 -7
  28. package/dist/index.js.map +1 -1
  29. package/dist/islands-middleware-entrypoint.d.ts +5 -0
  30. package/dist/islands-middleware-entrypoint.d.ts.map +1 -0
  31. package/dist/islands-middleware-entrypoint.js +58 -0
  32. package/dist/islands-middleware-entrypoint.js.map +1 -0
  33. package/dist/islands-runtime.d.ts +1 -0
  34. package/dist/islands-runtime.js +113 -0
  35. package/dist/islands-runtime.js.map +1 -0
  36. package/dist/islands.d.ts +2 -0
  37. package/dist/islands.js +3 -0
  38. package/dist/lifecycle/events.d.ts +1 -1
  39. package/dist/log/index.d.ts +1 -2
  40. package/dist/log/index.d.ts.map +1 -1
  41. package/dist/log-B69HEBvg.js.map +1 -1
  42. package/dist/native-mount-DjYEnO4X.js.map +1 -1
  43. package/dist/native.d.ts +0 -1
  44. package/dist/native.d.ts.map +1 -1
  45. package/dist/prepare-CXZsyAVk.js.map +1 -1
  46. package/dist/prerendered-CpEAJN_q.js +34 -0
  47. package/dist/prerendered-CpEAJN_q.js.map +1 -0
  48. package/dist/preview.d.ts +0 -1
  49. package/dist/preview.d.ts.map +1 -1
  50. package/dist/route-middleware-entrypoint.d.ts +0 -1
  51. package/dist/route-middleware-entrypoint.d.ts.map +1 -1
  52. package/dist/route-store-DdxGePj2.js +27 -0
  53. package/dist/route-store-DdxGePj2.js.map +1 -0
  54. package/dist/route-store-DtY3uvLf.d.ts +69 -0
  55. package/dist/route-store-DtY3uvLf.d.ts.map +1 -0
  56. package/dist/server.d.ts.map +1 -1
  57. package/dist/server.js +32 -15
  58. package/dist/server.js.map +1 -1
  59. package/dist/transform-D8dIBGEr.js +191 -0
  60. package/dist/transform-D8dIBGEr.js.map +1 -0
  61. package/dist/types-D0uMBi2M.d.ts.map +1 -1
  62. package/package.json +31 -11
  63. package/dist/construct-DgB-jR0a.d.ts.map +0 -1
  64. package/dist/events-CUzQ2_cp.d.ts.map +0 -1
  65. package/dist/excludes-DLF3A_Cf.d.ts.map +0 -1
package/dist/index.js CHANGED
@@ -3,11 +3,176 @@ import { a as runShutdown, i as createRequestInstrumentation, o as runStartup, t
3
3
  import { n as dispatchNativeMount, t as clearNativeMounts } from "./native-mount-DjYEnO4X.js";
4
4
  import { n as getCurrentGeneration, r as incrementGeneration, t as GEN_HEADER } from "./generation-Bp2IA0jf.js";
5
5
  import { r as RECOMMENDED_EXCLUDES } from "./excludes-pE23EbmQ.js";
6
+ import { i as registerIslandEmitter } from "./emitters-C20tjKLp.js";
7
+ import { t as getRequestRouteData } from "./route-store-DdxGePj2.js";
6
8
  import fs, { readFileSync } from "node:fs";
7
9
  import path from "node:path";
8
10
  import { fileURLToPath } from "node:url";
11
+ import { parse } from "@astrojs/compiler-rs";
12
+ import { createHash } from "node:crypto";
9
13
  import { Parser } from "acorn";
10
14
  import MagicString from "magic-string";
15
+ //#region src/dev-mode/island-warmup.ts
16
+ function isAstNode(value) {
17
+ return typeof value === "object" && value !== null && typeof value.type === "string";
18
+ }
19
+ function walk$1(node, visit) {
20
+ if (Array.isArray(node)) {
21
+ for (const item of node) walk$1(item, visit);
22
+ return;
23
+ }
24
+ if (typeof node !== "object" || node === null) return;
25
+ if (isAstNode(node)) visit(node);
26
+ for (const value of Object.values(node)) walk$1(value, visit);
27
+ }
28
+ /** local binding name → import specifier, from the frontmatter program */
29
+ function collectImports(program) {
30
+ const imports = /* @__PURE__ */ new Map();
31
+ walk$1(program, (node) => {
32
+ if (node.type !== "ImportDeclaration") return;
33
+ const decl = node;
34
+ if (decl.importKind === "type") return;
35
+ const specifier = typeof decl.source?.value === "string" ? decl.source.value : void 0;
36
+ if (!specifier) return;
37
+ for (const spec of decl.specifiers ?? []) {
38
+ if (spec.importKind === "type") continue;
39
+ if (typeof spec.local?.name === "string") imports.set(spec.local.name, specifier);
40
+ }
41
+ });
42
+ return imports;
43
+ }
44
+ /** the root identifier of a JSX tag: `Foo` → Foo, `Ns.Chart` → Ns */
45
+ function tagRootIdentifier(name) {
46
+ let current = name;
47
+ while (isAstNode(current) && current.type === "JSXMemberExpression") current = current.object;
48
+ if (isAstNode(current) && current.type === "JSXIdentifier") {
49
+ const identifier = current;
50
+ return typeof identifier.name === "string" ? identifier.name : void 0;
51
+ }
52
+ }
53
+ function hasClientDirective(attributes) {
54
+ if (!Array.isArray(attributes)) return false;
55
+ return attributes.some((attr) => {
56
+ if (!isAstNode(attr) || attr.type !== "JSXAttribute") return false;
57
+ const name = attr.name;
58
+ return typeof name?.name === "string" && name.name.startsWith("client:");
59
+ });
60
+ }
61
+ /**
62
+ * Extract the import specifiers of all hydrated (`client:*`) components from
63
+ * raw `.astro` source. Astro components can't hydrate, so `.astro` specifiers
64
+ * are skipped; dynamic tags without a frontmatter import are invisible here.
65
+ */
66
+ function scanAstroSource(source) {
67
+ const { ast } = parse(source);
68
+ const root = ast;
69
+ const imports = collectImports(root.frontmatter?.program);
70
+ if (imports.size === 0) return [];
71
+ const specifiers = /* @__PURE__ */ new Set();
72
+ walk$1(root.body, (node) => {
73
+ if (node.type !== "JSXOpeningElement") return;
74
+ const element = node;
75
+ if (!hasClientDirective(element.attributes)) return;
76
+ const rootName = tagRootIdentifier(element.name);
77
+ if (!rootName || /^[a-z]/.test(rootName)) return;
78
+ const specifier = imports.get(rootName);
79
+ if (specifier && !specifier.endsWith(".astro")) specifiers.add(specifier);
80
+ });
81
+ return [...specifiers];
82
+ }
83
+ /** scan all `.astro` files under `srcDir` for hydrated components */
84
+ async function scanProjectIslands(srcDir, logger) {
85
+ const islands = [];
86
+ let entries;
87
+ try {
88
+ entries = await fs.promises.readdir(srcDir, {
89
+ recursive: true,
90
+ withFileTypes: true
91
+ });
92
+ } catch {
93
+ return islands;
94
+ }
95
+ const files = entries.filter((entry) => entry.isFile() && entry.name.endsWith(".astro")).map((entry) => path.join(entry.parentPath, entry.name));
96
+ await Promise.all(files.map(async (file) => {
97
+ try {
98
+ const source = await fs.promises.readFile(file, "utf8");
99
+ for (const specifier of scanAstroSource(source)) islands.push({
100
+ importer: file,
101
+ specifier
102
+ });
103
+ } catch (error) {
104
+ logger.debug(`island scan skipped ${file}: ${error instanceof Error ? error.message : String(error)}`);
105
+ }
106
+ }));
107
+ return islands;
108
+ }
109
+ function packageNameOf(specifier) {
110
+ const segments = specifier.split("/");
111
+ return specifier.startsWith("@") ? segments.slice(0, 2).join("/") : segments[0] ?? specifier;
112
+ }
113
+ /**
114
+ * Bare package specifiers resolvable from the project's `node_modules` — these
115
+ * go into `optimizeDeps.include`. Anything else (relative paths, tsconfig
116
+ * aliases, hoisted workspace deps) is resolved through vite at server start;
117
+ * misclassification is harmless, just slightly later discovery.
118
+ */
119
+ function selectBareSpecifiers(islands, root) {
120
+ const bare = /* @__PURE__ */ new Set();
121
+ for (const { specifier } of islands) {
122
+ if (specifier.startsWith(".") || specifier.startsWith("/") || specifier.startsWith("#")) continue;
123
+ if (fs.existsSync(path.join(root, "node_modules", packageNameOf(specifier)))) bare.add(specifier);
124
+ }
125
+ return [...bare];
126
+ }
127
+ function toRequestUrl(id, root) {
128
+ const normalized = id.split(path.sep).join("/");
129
+ const normalizedRoot = root.split(path.sep).join("/");
130
+ return normalized.startsWith(`${normalizedRoot}/`) ? normalized.slice(normalizedRoot.length) : `/@fs/${normalized}`;
131
+ }
132
+ function createIslandWarmup(options) {
133
+ const { root, srcDir, logger } = options;
134
+ let islands = [];
135
+ const warmIslands = async (server, batch, warmed) => {
136
+ const env = server.environments.client;
137
+ await Promise.all(batch.map(async ({ importer, specifier }) => {
138
+ try {
139
+ const resolved = await env.pluginContainer.resolveId(specifier, importer);
140
+ if (!resolved || resolved.external) return;
141
+ if (resolved.id.startsWith("\0") || resolved.id.includes("node_modules")) return;
142
+ if (warmed.has(resolved.id)) return;
143
+ warmed.add(resolved.id);
144
+ await env.warmupRequest(toRequestUrl(resolved.id, root));
145
+ } catch {}
146
+ }));
147
+ };
148
+ return {
149
+ name: "@astroscope/node/island-warmup",
150
+ async config() {
151
+ islands = await scanProjectIslands(srcDir, logger);
152
+ const include = selectBareSpecifiers(islands, root);
153
+ if (islands.length > 0) logger.info(`warming ${islands.length} island(s), pre-optimizing ${include.length} dependenc(ies)`);
154
+ return include.length > 0 ? { optimizeDeps: { include } } : void 0;
155
+ },
156
+ configureServer(server) {
157
+ const warmed = /* @__PURE__ */ new Set();
158
+ if (server.httpServer) server.httpServer.once("listening", () => void warmIslands(server, islands, warmed));
159
+ else warmIslands(server, islands, warmed);
160
+ const onWatcherEvent = (file) => {
161
+ if (!file.endsWith(".astro") || !file.startsWith(srcDir)) return;
162
+ fs.promises.readFile(file, "utf8").then((source) => {
163
+ const batch = scanAstroSource(source).map((specifier) => ({
164
+ importer: file,
165
+ specifier
166
+ }));
167
+ return warmIslands(server, batch, warmed);
168
+ }).catch(() => {});
169
+ };
170
+ server.watcher.on("add", onWatcherEvent);
171
+ server.watcher.on("change", onWatcherEvent);
172
+ }
173
+ };
174
+ }
175
+ //#endregion
11
176
  //#region src/dev-mode/serialize-error.ts
12
177
  function serializeError(error) {
13
178
  if (error instanceof Error) return error.stack ?? error.message;
@@ -413,6 +578,81 @@ function createDevMachinery(options) {
413
578
  function serializeExcludePatterns(patterns) {
414
579
  return `[${patterns.map((p) => "pattern" in p ? `{ pattern: ${p.pattern.toString()} }` : JSON.stringify(p)).join(", ")}]`;
415
580
  }
581
+ const RESOLVED_ISLANDS_VIRTUAL_MODULE_ID = `\0virtual:@astroscope/node/islands-manifest`;
582
+ const MANIFEST_FILE_NAME = "islands-manifest.json";
583
+ /**
584
+ * Client-build side of island preloading: records the chunk import graph (direct
585
+ * static and dynamic edges per chunk), writes the content-hashed gate runtime into
586
+ * the client assets dir, and drops the manifest next to the server chunks — where
587
+ * the virtual module reads it back at runtime, mirroring the i18n manifest.
588
+ *
589
+ * The client build runs after the server build, so the SSR bundle can only carry
590
+ * code that reads the file lazily; in dev there is no manifest and the middleware
591
+ * stays inert.
592
+ */
593
+ function createIslandsManifestPlugin(options) {
594
+ let isBuild = false;
595
+ return {
596
+ name: "@astroscope/node/islands-manifest",
597
+ configResolved(config) {
598
+ isBuild = config.command === "build";
599
+ },
600
+ resolveId(id) {
601
+ if (id === "virtual:@astroscope/node/islands-manifest") return RESOLVED_ISLANDS_VIRTUAL_MODULE_ID;
602
+ },
603
+ load(id) {
604
+ if (id !== RESOLVED_ISLANDS_VIRTUAL_MODULE_ID) return;
605
+ if (!isBuild || !options.enabled) return "export const manifest = null;";
606
+ return `
607
+ import { readFileSync } from 'node:fs';
608
+ import { fileURLToPath } from 'node:url';
609
+ import { dirname, join } from 'node:path';
610
+
611
+ const dir = dirname(fileURLToPath(import.meta.url));
612
+
613
+ let manifest = null;
614
+
615
+ for (const candidate of [
616
+ join(dir, '${MANIFEST_FILE_NAME}'),
617
+ join(dir, 'chunks', '${MANIFEST_FILE_NAME}'),
618
+ join(dir, '..', 'chunks', '${MANIFEST_FILE_NAME}'),
619
+ ]) {
620
+ try {
621
+ manifest = JSON.parse(readFileSync(candidate, 'utf-8'));
622
+ break;
623
+ } catch {
624
+ manifest = null;
625
+ }
626
+ }
627
+
628
+ export { manifest };
629
+ `;
630
+ },
631
+ writeBundle(outputOptions, bundle) {
632
+ if (!options.enabled || this.environment.name !== "client" || !outputOptions.dir) return;
633
+ const chunks = {};
634
+ for (const chunk of Object.values(bundle)) {
635
+ if (chunk.type !== "chunk") continue;
636
+ chunks[chunk.fileName] = {
637
+ ...chunk.imports.length > 0 && { i: chunk.imports },
638
+ ...chunk.dynamicImports.length > 0 && { d: chunk.dynamicImports }
639
+ };
640
+ }
641
+ const runtimeSource = fs.readFileSync(new URL("./islands-runtime.js", import.meta.url), "utf-8").replace(/^\/\/# sourceMappingURL=.*$/m, "").trimEnd();
642
+ const hash = createHash("sha256").update(runtimeSource).digest("hex").slice(0, 8);
643
+ const runtime = `${options.assetsDir}/islands-runtime.${hash}.js`;
644
+ fs.mkdirSync(path.join(outputOptions.dir, options.assetsDir), { recursive: true });
645
+ fs.writeFileSync(path.join(outputOptions.dir, options.assetsDir, `islands-runtime.${hash}.js`), runtimeSource);
646
+ const manifest = {
647
+ runtime,
648
+ chunks
649
+ };
650
+ const chunksDir = path.resolve(outputOptions.dir, "..", "server", "chunks");
651
+ if (fs.existsSync(chunksDir)) fs.writeFileSync(path.join(chunksDir, MANIFEST_FILE_NAME), JSON.stringify(manifest));
652
+ else options.logger.warn(`server chunks directory not found, cannot write manifest to ${chunksDir}`);
653
+ }
654
+ };
655
+ }
416
656
  //#endregion
417
657
  //#region src/tweaks/sourcemap.ts
418
658
  /**
@@ -507,7 +747,7 @@ function stripSsrEffectsPlugin() {
507
747
  for (const { start, end } of replacements) s.overwrite(start, end, EMPTY_FN);
508
748
  return {
509
749
  code: s.toString(),
510
- map: s.generateMap({ hires: true })
750
+ map: s.generateMap({ hires: true }).toString()
511
751
  };
512
752
  }
513
753
  };
@@ -570,6 +810,7 @@ function node(options = {}) {
570
810
  const bootOptions = options.boot ?? {};
571
811
  const healthOptions = options.health ?? {};
572
812
  const csrfOptions = options.csrf ?? {};
813
+ const islandsEnabled = options.islands !== false;
573
814
  const loggingOptions = options.logging ?? {};
574
815
  const telemetryOptions = options.telemetry ?? {};
575
816
  const loggingExclude = loggingOptions ? loggingOptions.exclude ?? DEFAULT_REQUEST_EXCLUDES : [];
@@ -593,6 +834,10 @@ function node(options = {}) {
593
834
  order: "pre",
594
835
  entrypoint: "@astroscope/node/csrf-middleware"
595
836
  });
837
+ addMiddleware({
838
+ order: "pre",
839
+ entrypoint: "@astroscope/node/islands-middleware"
840
+ });
596
841
  const root = fileURLToPath(config.root);
597
842
  const watch = bootOptions === false ? false : bootOptions.watch ?? true;
598
843
  bootEntry = bootOptions === false ? void 0 : resolveBootEntry(root, bootOptions.entry);
@@ -620,6 +865,13 @@ function node(options = {}) {
620
865
  entrypoint: "@astroscope/node/dev-middleware",
621
866
  order: "pre"
622
867
  });
868
+ const islandWarmup = command === "dev" ? [createIslandWarmup({
869
+ root,
870
+ srcDir: fileURLToPath(config.srcDir),
871
+ logger
872
+ })] : [];
873
+ const imageService = options.imageService ?? "auto";
874
+ const imageOff = imageService === "off" || imageService === "auto" && config.image.service.entrypoint === "astro/assets/services/sharp";
623
875
  updateConfig({
624
876
  build: { redirects: false },
625
877
  ...config.trailingSlash === "ignore" && { trailingSlash: "never" },
@@ -627,12 +879,24 @@ function node(options = {}) {
627
879
  ...!config.security.allowedDomains?.length && { allowedDomains: [{}] },
628
880
  ...csrfOptions && { checkOrigin: false }
629
881
  },
630
- image: { endpoint: {
631
- route: config.image.endpoint.route ?? "_image",
632
- entrypoint: config.image.endpoint.entrypoint ?? (command === "dev" ? "astro/assets/endpoint/dev" : "astro/assets/endpoint/node")
633
- } },
882
+ image: {
883
+ ...imageOff && { service: {
884
+ entrypoint: "@astroscope/node/image-service",
885
+ config: {}
886
+ } },
887
+ endpoint: {
888
+ route: config.image.endpoint.route ?? "_image",
889
+ entrypoint: imageOff ? "@astroscope/node/image-endpoint" : config.image.endpoint.entrypoint ?? (command === "dev" ? "astro/assets/endpoint/dev" : "astro/assets/endpoint/node")
890
+ }
891
+ },
634
892
  vite: { plugins: [
635
893
  ...devMachinery,
894
+ ...islandWarmup,
895
+ createIslandsManifestPlugin({
896
+ assetsDir: config.build.assets,
897
+ logger,
898
+ enabled: islandsEnabled
899
+ }),
636
900
  ssrSourcemapPlugin(),
637
901
  stripSsrEffectsPlugin(),
638
902
  {
@@ -656,7 +920,7 @@ function node(options = {}) {
656
920
  port: astroConfig.server.port ?? 4321,
657
921
  client: astroConfig.build.client.toString(),
658
922
  server: astroConfig.build.server.toString(),
659
- bodySizeLimit: options.bodySizeLimit ?? 1024 * 1024 * 1024,
923
+ bodySizeLimit: options.bodySizeLimit ?? 1073741824,
660
924
  shutdownTimeout: options.shutdownTimeout ?? 1e4,
661
925
  health: healthOptions ? {
662
926
  ...healthOptions.host !== void 0 && { host: healthOptions.host },
@@ -720,6 +984,15 @@ function node(options = {}) {
720
984
  },
721
985
  "astro:build:done": async ({ logger }) => {
722
986
  if (!astroConfig) return;
987
+ if (islandsEnabled) {
988
+ const manifestPath = path.join(fileURLToPath(astroConfig.build.server), "chunks", "islands-manifest.json");
989
+ if (fs.existsSync(manifestPath)) {
990
+ const { transformPrerenderedHtml } = await import("./prerendered-CpEAJN_q.js");
991
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
992
+ const transformed = await transformPrerenderedHtml(fileURLToPath(astroConfig.build.client), manifest);
993
+ if (transformed > 0) logger.info(`island preloading applied to ${transformed} prerendered page(s)`);
994
+ }
995
+ }
723
996
  const { compressClientDir } = await import("./compress-B11aeRHQ.js");
724
997
  await compressClientDir(fileURLToPath(astroConfig.build.client), logger);
725
998
  }
@@ -727,6 +1000,6 @@ function node(options = {}) {
727
1000
  };
728
1001
  }
729
1002
  //#endregion
730
- export { node as default };
1003
+ export { node as default, getRequestRouteData, registerIslandEmitter };
731
1004
 
732
1005
  //# sourceMappingURL=index.js.map