@astroscope/node 2.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.
Files changed (70) hide show
  1. package/README.md +8 -5
  2. package/dist/boot.d.ts +2 -2
  3. package/dist/boot.d.ts.map +1 -1
  4. package/dist/csrf-middleware-entrypoint.d.ts +1 -2
  5. package/dist/csrf-middleware-entrypoint.d.ts.map +1 -1
  6. package/dist/csrf-middleware-entrypoint.js +1 -1
  7. package/dist/dev-middleware-entrypoint.d.ts +1 -2
  8. package/dist/dev-middleware-entrypoint.d.ts.map +1 -1
  9. package/dist/{prepare-CXZsyAVk.js → duplicate-slashes-C6hmOLl9.js} +94 -38
  10. package/dist/duplicate-slashes-C6hmOLl9.js.map +1 -0
  11. package/dist/{excludes-pE23EbmQ.js → excludes-10eDopVB.js} +18 -4
  12. package/dist/excludes-10eDopVB.js.map +1 -0
  13. package/dist/{excludes-BDiE3eyp.d.ts → excludes-rvkVGg6B.d.ts} +11 -3
  14. package/dist/excludes-rvkVGg6B.d.ts.map +1 -0
  15. package/dist/excludes.d.ts +2 -2
  16. package/dist/excludes.js +2 -2
  17. package/dist/graph-DdAxiaxa.js +35 -0
  18. package/dist/graph-DdAxiaxa.js.map +1 -0
  19. package/dist/health.d.ts +3 -4
  20. package/dist/health.d.ts.map +1 -1
  21. package/dist/image-endpoint.d.ts +1 -2
  22. package/dist/image-endpoint.d.ts.map +1 -1
  23. package/dist/index.d.ts +4 -4
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +38 -122
  26. package/dist/index.js.map +1 -1
  27. package/dist/islands-middleware-entrypoint.d.ts +1 -2
  28. package/dist/islands-middleware-entrypoint.d.ts.map +1 -1
  29. package/dist/islands-middleware-entrypoint.js +3 -1
  30. package/dist/islands-middleware-entrypoint.js.map +1 -1
  31. package/dist/islands-runtime.iife.js +1 -1
  32. package/dist/islands.d.ts +57 -2
  33. package/dist/islands.d.ts.map +1 -0
  34. package/dist/islands.js +3 -1
  35. package/dist/log/index.d.ts +5 -5
  36. package/dist/log/index.d.ts.map +1 -1
  37. package/dist/native.d.ts +5 -6
  38. package/dist/native.d.ts.map +1 -1
  39. package/dist/{prerendered-DVjzWNbW.js → prerendered-Z-Qi-FFK.js} +4 -4
  40. package/dist/prerendered-Z-Qi-FFK.js.map +1 -0
  41. package/dist/request-route-DcnZOOM4.js.map +1 -1
  42. package/dist/route-islands-B53rE5MH.js +73 -0
  43. package/dist/route-islands-B53rE5MH.js.map +1 -0
  44. package/dist/route-middleware-entrypoint.d.ts +1 -2
  45. package/dist/route-middleware-entrypoint.d.ts.map +1 -1
  46. package/dist/{route-store-CSYR4G5e.d.ts → route-store-neUA2nBb.d.ts} +10 -14
  47. package/dist/route-store-neUA2nBb.d.ts.map +1 -0
  48. package/dist/routes-D1o_WrJ5.js +39 -0
  49. package/dist/routes-D1o_WrJ5.js.map +1 -0
  50. package/dist/server.d.ts +2 -3
  51. package/dist/server.d.ts.map +1 -1
  52. package/dist/server.js +68 -45
  53. package/dist/server.js.map +1 -1
  54. package/dist/telemetry-B9aziFyQ.js +202 -0
  55. package/dist/telemetry-B9aziFyQ.js.map +1 -0
  56. package/dist/telemetry.d.ts +68 -0
  57. package/dist/telemetry.d.ts.map +1 -0
  58. package/dist/telemetry.js +2 -0
  59. package/dist/transform-Bz5z5w2Y.js +594 -0
  60. package/dist/transform-Bz5z5w2Y.js.map +1 -0
  61. package/package.json +13 -11
  62. package/dist/excludes-BDiE3eyp.d.ts.map +0 -1
  63. package/dist/excludes-pE23EbmQ.js.map +0 -1
  64. package/dist/prepare-CXZsyAVk.js.map +0 -1
  65. package/dist/prerendered-DVjzWNbW.js.map +0 -1
  66. package/dist/route-store-CSYR4G5e.d.ts.map +0 -1
  67. package/dist/strip-build-paths-C6eb1QdG.js +0 -63
  68. package/dist/strip-build-paths-C6eb1QdG.js.map +0 -1
  69. package/dist/transform-BqV8SOEm.js +0 -195
  70. package/dist/transform-BqV8SOEm.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,33 +1,33 @@
1
1
  import { n as setBootContext } from "./context-Bkg-FnnQ.js";
2
- import { a as runShutdown, i as createRequestInstrumentation, o as runStartup, t as preparePlatform } from "./prepare-CXZsyAVk.js";
2
+ import { a as createRequestInstrumentation, n as preparePlatform, o as runShutdown, s as runStartup, t as redirectDuplicateSlashes } from "./duplicate-slashes-C6hmOLl9.js";
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
- import { r as RECOMMENDED_EXCLUDES } from "./excludes-pE23EbmQ.js";
5
+ import { r as RECOMMENDED_EXCLUDES, s as serializeExcludePatterns } from "./excludes-10eDopVB.js";
6
+ import { n as routeEntrypoints, r as stripQuery, t as collectRouteIslands } from "./route-islands-B53rE5MH.js";
6
7
  import { i as registerIslandEmitter } from "./emitters-C20tjKLp.js";
7
8
  import { t as getRequestRouteData } from "./route-store-DdxGePj2.js";
8
9
  import fs, { readFileSync } from "node:fs";
9
10
  import path from "node:path";
10
11
  import { fileURLToPath } from "node:url";
12
+ import { defined } from "@entwico/dash";
11
13
  import { parse } from "@astrojs/compiler-rs";
12
- import { Parser } from "acorn";
13
- import MagicString from "magic-string";
14
14
  //#region src/dev-mode/island-warmup.ts
15
15
  function isAstNode(value) {
16
16
  return typeof value === "object" && value !== null && typeof value.type === "string";
17
17
  }
18
- function walk$1(node, visit) {
18
+ function walk(node, visit) {
19
19
  if (Array.isArray(node)) {
20
- for (const item of node) walk$1(item, visit);
20
+ for (const item of node) walk(item, visit);
21
21
  return;
22
22
  }
23
23
  if (typeof node !== "object" || node === null) return;
24
24
  if (isAstNode(node)) visit(node);
25
- for (const value of Object.values(node)) walk$1(value, visit);
25
+ for (const value of Object.values(node)) walk(value, visit);
26
26
  }
27
27
  /** local binding name → import specifier, from the frontmatter program */
28
28
  function collectImports(program) {
29
29
  const imports = /* @__PURE__ */ new Map();
30
- walk$1(program, (node) => {
30
+ walk(program, (node) => {
31
31
  if (node.type !== "ImportDeclaration") return;
32
32
  const decl = node;
33
33
  if (decl.importKind === "type") return;
@@ -68,7 +68,7 @@ function scanAstroSource(source) {
68
68
  const imports = collectImports(root.frontmatter?.program);
69
69
  if (imports.size === 0) return [];
70
70
  const specifiers = /* @__PURE__ */ new Set();
71
- walk$1(root.body, (node) => {
71
+ walk(root.body, (node) => {
72
72
  if (node.type !== "JSXOpeningElement") return;
73
73
  const element = node;
74
74
  if (!hasClientDirective(element.attributes)) return;
@@ -568,25 +568,16 @@ function createDevMachinery(options) {
568
568
  }
569
569
  }];
570
570
  }
571
- //#endregion
572
- //#region src/excludes/serialize.ts
573
- /**
574
- * Serialize exclude patterns to JavaScript code for use in virtual modules.
575
- * Handles RegExp objects which JSON.stringify cannot serialize.
576
- */
577
- function serializeExcludePatterns(patterns) {
578
- return `[${patterns.map((p) => "pattern" in p ? `{ pattern: ${p.pattern.toString()} }` : JSON.stringify(p)).join(", ")}]`;
579
- }
580
571
  const RESOLVED_ISLANDS_VIRTUAL_MODULE_ID = `\0virtual:@astroscope/node/islands-manifest`;
581
572
  const MANIFEST_FILE_NAME = "islands-manifest.json";
582
573
  /**
583
- * Client-build side of island preloading: records the chunk import graph (direct
584
- * static and dynamic edges per chunk), embeds the gate runtime source, and drops
585
- * the manifest next to the server chunks where the virtual module reads it back
586
- * at runtime, mirroring the i18n manifest. The runtime is inlined into documents
587
- * rather than shipped as an asset: an external module fetch would install the
588
- * gates a round-trip after astro's own inline island machinery, losing the race
589
- * it exists to win.
574
+ * Build side of island preloading: the server build attributes islands to routes
575
+ * (its module graph is the only place a page's components are visible together),
576
+ * the client build records the chunk import graph (direct static and dynamic edges
577
+ * per chunk), embeds the gate runtime source (inlined into documents rather than
578
+ * shipped as an asset an external fetch would install the gates a round-trip
579
+ * after astro's inline island machinery), and drops the manifest next to the
580
+ * server chunks, mirroring the i18n manifest.
590
581
  *
591
582
  * The client build runs after the server build, so the SSR bundle can only carry
592
583
  * code that reads the file lazily; in dev there is no manifest and the middleware
@@ -594,6 +585,7 @@ const MANIFEST_FILE_NAME = "islands-manifest.json";
594
585
  */
595
586
  function createIslandsManifestPlugin(options) {
596
587
  let isBuild = false;
588
+ const routeIslands = /* @__PURE__ */ new Map();
597
589
  return {
598
590
  name: "@astroscope/node/islands-manifest",
599
591
  configResolved(config) {
@@ -630,21 +622,31 @@ for (const candidate of [
630
622
  export { manifest };
631
623
  `;
632
624
  },
625
+ async generateBundle() {
626
+ if (!options.enabled || this.environment.name === "client") return;
627
+ const { routes } = await collectRouteIslands(this, options.pages());
628
+ for (const [pattern, islands] of routes) routeIslands.set(pattern, islands);
629
+ },
633
630
  writeBundle(outputOptions, bundle) {
634
631
  if (!options.enabled || this.environment.name !== "client" || !outputOptions.dir) return;
635
632
  const chunks = {};
633
+ const entryByComponent = /* @__PURE__ */ new Map();
636
634
  for (const chunk of Object.values(bundle)) {
637
635
  if (chunk.type !== "chunk") continue;
638
636
  chunks[chunk.fileName] = {
639
637
  ...chunk.imports.length > 0 && { i: chunk.imports },
640
638
  ...chunk.dynamicImports.length > 0 && { d: chunk.dynamicImports }
641
639
  };
640
+ if (chunk.isEntry && chunk.facadeModuleId) entryByComponent.set(stripQuery(chunk.facadeModuleId), chunk.fileName);
642
641
  }
642
+ const routes = {};
643
+ for (const [pattern, islands] of routeIslands) routes[pattern] = [...islands].map((component) => entryByComponent.get(component)).filter(defined);
643
644
  const runtimeSource = fs.readFileSync(new URL("./islands-runtime.iife.js", import.meta.url), "utf-8").trim();
644
645
  if (runtimeSource.toLowerCase().includes("<\/script")) throw new Error("islands gate runtime must not contain \"<\/script\"");
645
646
  const manifest = {
646
647
  runtimeSource,
647
- chunks
648
+ chunks,
649
+ routes
648
650
  };
649
651
  const chunksDir = path.resolve(outputOptions.dir, "..", "server", "chunks");
650
652
  if (fs.existsSync(chunksDir)) fs.writeFileSync(path.join(chunksDir, MANIFEST_FILE_NAME), JSON.stringify(manifest));
@@ -673,94 +675,6 @@ function ssrSourcemapPlugin() {
673
675
  };
674
676
  }
675
677
  //#endregion
676
- //#region src/tweaks/strip-effects.ts
677
- const HOOK_NAMES = /* @__PURE__ */ new Set([
678
- "useEffect",
679
- "useLayoutEffect",
680
- "useInsertionEffect"
681
- ]);
682
- const REACT_SOURCE = /^react(\/.*)?$/;
683
- const TRANSFORMABLE = /\.(?:[mc]?[jt]sx?)$/;
684
- const EMPTY_FN = "(()=>{})";
685
- /**
686
- * in SSR builds, react effect hooks never execute. emptying their callbacks
687
- * lets rolldown drop dead branches — including dynamic imports of client-only
688
- * libs (maplibre-gl, hls.js, etc.) — from the server bundle, which in turn
689
- * stops nft from tracing them at docker-image time.
690
- *
691
- * scope is deliberately narrow: first-party code only (no node_modules), only
692
- * in the SSR pass, and binding-aware (the React import must resolve to the
693
- * real react package). raw chunks where bundling has erased the binding are
694
- * left alone — that's NFT's domain, not ours.
695
- */
696
- function stripSsrEffectsPlugin() {
697
- return {
698
- name: "@astroscope/node/tweaks/strip-effects",
699
- enforce: "post",
700
- transform(code, id, options) {
701
- if (!options?.ssr) return null;
702
- if (id.includes("/node_modules/")) return null;
703
- const cleanId = id.split("?")[0] ?? id;
704
- if (!TRANSFORMABLE.test(cleanId)) return null;
705
- if (!code.includes("useEffect") && !code.includes("useLayoutEffect") && !code.includes("useInsertionEffect")) return null;
706
- let ast;
707
- try {
708
- ast = Parser.parse(code, {
709
- ecmaVersion: "latest",
710
- sourceType: "module",
711
- allowReturnOutsideFunction: true,
712
- allowAwaitOutsideFunction: true,
713
- allowImportExportEverywhere: true,
714
- allowHashBang: true
715
- });
716
- } catch {
717
- return null;
718
- }
719
- const directHooks = /* @__PURE__ */ new Set();
720
- const namespaceHooks = /* @__PURE__ */ new Set();
721
- walk(ast, (node) => {
722
- if (node.type !== "ImportDeclaration") return;
723
- const src = node.source?.value;
724
- if (typeof src !== "string" || !REACT_SOURCE.test(src)) return;
725
- for (const spec of node.specifiers ?? []) if (spec.type === "ImportSpecifier") {
726
- const imported = spec.imported?.name ?? spec.imported?.value;
727
- if (HOOK_NAMES.has(imported)) directHooks.add(spec.local.name);
728
- } else if (spec.type === "ImportDefaultSpecifier" || spec.type === "ImportNamespaceSpecifier") namespaceHooks.add(spec.local.name);
729
- });
730
- if (directHooks.size === 0 && namespaceHooks.size === 0) return null;
731
- const replacements = [];
732
- walk(ast, (node) => {
733
- if (node.type !== "CallExpression" || !node.arguments?.length) return;
734
- const callee = node.callee;
735
- const isDirect = callee.type === "Identifier" && directHooks.has(callee.name);
736
- const isMember = callee.type === "MemberExpression" && !callee.computed && callee.object?.type === "Identifier" && namespaceHooks.has(callee.object.name) && callee.property?.type === "Identifier" && HOOK_NAMES.has(callee.property.name);
737
- if (!isDirect && !isMember) return;
738
- const arg = node.arguments[0];
739
- replacements.push({
740
- start: arg.start,
741
- end: arg.end
742
- });
743
- });
744
- if (replacements.length === 0) return null;
745
- const s = new MagicString(code);
746
- for (const { start, end } of replacements) s.overwrite(start, end, EMPTY_FN);
747
- return {
748
- code: s.toString(),
749
- map: s.generateMap({ hires: true }).toString()
750
- };
751
- }
752
- };
753
- }
754
- function walk(node, visit) {
755
- if (!node || typeof node !== "object") return;
756
- if (typeof node.type === "string") visit(node);
757
- for (const key of Object.keys(node)) {
758
- const v = node[key];
759
- if (Array.isArray(v)) for (const item of v) walk(item, visit);
760
- else if (v && typeof v === "object") walk(v, visit);
761
- }
762
- }
763
- //#endregion
764
678
  //#region src/integration/integration.ts
765
679
  const CONFIG_VIRTUAL_MODULE_ID = "virtual:@astroscope/node/config";
766
680
  const BOOT_VIRTUAL_MODULE_ID = "virtual:@astroscope/node/boot";
@@ -815,6 +729,7 @@ function node(options = {}) {
815
729
  const loggingExclude = loggingOptions ? loggingOptions.exclude ?? DEFAULT_REQUEST_EXCLUDES : [];
816
730
  const telemetryExclude = telemetryOptions ? telemetryOptions.exclude ?? DEFAULT_REQUEST_EXCLUDES : [];
817
731
  let astroConfig = null;
732
+ let resolvedRoutes = [];
818
733
  let bootEntry;
819
734
  let configSeam;
820
735
  let instrumentationSeam;
@@ -847,7 +762,7 @@ function node(options = {}) {
847
762
  const devMachinery = command === "dev" && bootOptions !== false ? createDevMachinery({
848
763
  entry: bootEntry ? path.relative(root, bootEntry) : void 0,
849
764
  watch,
850
- watchEntries: [relativeSeam(configSeam), relativeSeam(logSeam)].filter((entry) => !!entry),
765
+ watchEntries: [relativeSeam(configSeam), relativeSeam(logSeam)].filter(defined),
851
766
  prepare: (importModule) => preparePlatform({
852
767
  dev: true,
853
768
  telemetry: telemetryOptions && telemetryOptions.dev ? { prometheus: telemetryOptions.prometheus ?? {} } : false,
@@ -893,10 +808,10 @@ function node(options = {}) {
893
808
  ...islandWarmup,
894
809
  createIslandsManifestPlugin({
895
810
  logger,
896
- enabled: islandsEnabled
811
+ enabled: islandsEnabled,
812
+ pages: () => routeEntrypoints(root, resolvedRoutes)
897
813
  }),
898
814
  ssrSourcemapPlugin(),
899
- stripSsrEffectsPlugin(),
900
815
  {
901
816
  name: "@astroscope/node",
902
817
  configEnvironment(environmentName) {
@@ -953,6 +868,7 @@ function node(options = {}) {
953
868
  }) : void 0;
954
869
  server.middlewares.use((req, res, next) => {
955
870
  const inner = () => {
871
+ if (redirectDuplicateSlashes(req, res)) return;
956
872
  if (!dispatchNativeMount(req, res)) next();
957
873
  };
958
874
  if (instrument) instrument(req, res, inner);
@@ -980,20 +896,20 @@ function node(options = {}) {
980
896
  }
981
897
  });
982
898
  },
899
+ "astro:routes:resolved": ({ routes }) => {
900
+ resolvedRoutes = routes;
901
+ },
983
902
  "astro:build:done": async ({ logger }) => {
984
903
  if (!astroConfig) return;
985
904
  if (islandsEnabled) {
986
905
  const manifestPath = path.join(fileURLToPath(astroConfig.build.server), "chunks", "islands-manifest.json");
987
906
  if (fs.existsSync(manifestPath)) {
988
- const { transformPrerenderedHtml } = await import("./prerendered-DVjzWNbW.js");
907
+ const { transformPrerenderedHtml } = await import("./prerendered-Z-Qi-FFK.js");
989
908
  const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
990
- const transformed = await transformPrerenderedHtml(fileURLToPath(astroConfig.build.client), manifest);
909
+ const transformed = transformPrerenderedHtml(fileURLToPath(astroConfig.build.client), manifest);
991
910
  if (transformed > 0) logger.info(`island preloading applied to ${transformed} prerendered page(s)`);
992
911
  }
993
912
  }
994
- const { stripBuildPaths } = await import("./strip-build-paths-C6eb1QdG.js");
995
- const stripped = stripBuildPaths(fileURLToPath(astroConfig.build.server), fileURLToPath(astroConfig.root));
996
- if (stripped > 0) logger.info(`build machine paths stripped from ${stripped} server file(s)`);
997
913
  const { compressClientDir } = await import("./compress-B11aeRHQ.js");
998
914
  await compressClientDir(fileURLToPath(astroConfig.build.client), logger);
999
915
  }