@astroscope/node 2.1.1 → 3.1.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 +39 -118
  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 +5 -3
  30. package/dist/islands-middleware-entrypoint.js.map +1 -1
  31. package/dist/islands.d.ts +57 -2
  32. package/dist/islands.d.ts.map +1 -0
  33. package/dist/islands.js +4 -2
  34. package/dist/log/index.d.ts +5 -5
  35. package/dist/log/index.d.ts.map +1 -1
  36. package/dist/native.d.ts +5 -6
  37. package/dist/native.d.ts.map +1 -1
  38. package/dist/{prerendered-DVjzWNbW.js → prerendered-Z-Qi-FFK.js} +4 -4
  39. package/dist/prerendered-Z-Qi-FFK.js.map +1 -0
  40. package/dist/request-route-DcnZOOM4.js.map +1 -1
  41. package/dist/route-islands-B53rE5MH.js +73 -0
  42. package/dist/route-islands-B53rE5MH.js.map +1 -0
  43. package/dist/route-middleware-entrypoint.d.ts +1 -2
  44. package/dist/route-middleware-entrypoint.d.ts.map +1 -1
  45. package/dist/{route-store-neUA2nBb.d.ts → route-store-Cl3OT0-q.d.ts} +7 -3
  46. package/dist/route-store-Cl3OT0-q.d.ts.map +1 -0
  47. package/dist/{route-store-DdxGePj2.js → route-store-DVSmOa34.js} +14 -4
  48. package/dist/route-store-DVSmOa34.js.map +1 -0
  49. package/dist/routes-D1o_WrJ5.js +39 -0
  50. package/dist/routes-D1o_WrJ5.js.map +1 -0
  51. package/dist/server.d.ts +2 -3
  52. package/dist/server.d.ts.map +1 -1
  53. package/dist/server.js +73 -47
  54. package/dist/server.js.map +1 -1
  55. package/dist/telemetry-B9aziFyQ.js +202 -0
  56. package/dist/telemetry-B9aziFyQ.js.map +1 -0
  57. package/dist/telemetry.d.ts +68 -0
  58. package/dist/telemetry.d.ts.map +1 -0
  59. package/dist/telemetry.js +2 -0
  60. package/dist/transform-Bz5z5w2Y.js +594 -0
  61. package/dist/transform-Bz5z5w2Y.js.map +1 -0
  62. package/package.json +13 -11
  63. package/dist/excludes-BDiE3eyp.d.ts.map +0 -1
  64. package/dist/excludes-pE23EbmQ.js.map +0 -1
  65. package/dist/prepare-CXZsyAVk.js.map +0 -1
  66. package/dist/prerendered-DVjzWNbW.js.map +0 -1
  67. package/dist/route-store-DdxGePj2.js.map +0 -1
  68. package/dist/route-store-neUA2nBb.d.ts.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
- import { t as getRequestRouteData } from "./route-store-DdxGePj2.js";
8
+ import { t as getRequestRouteData } from "./route-store-DVSmOa34.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,23 +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 (inlined
585
- * into documents rather than shipped as an asset an external fetch would
586
- * install the gates a round-trip after astro's inline island machinery), and
587
- * drops the manifest next to the server chunks, mirroring the i18n manifest.
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.
588
581
  *
589
582
  * The client build runs after the server build, so the SSR bundle can only carry
590
583
  * code that reads the file lazily; in dev there is no manifest and the middleware
@@ -592,6 +585,7 @@ const MANIFEST_FILE_NAME = "islands-manifest.json";
592
585
  */
593
586
  function createIslandsManifestPlugin(options) {
594
587
  let isBuild = false;
588
+ const routeIslands = /* @__PURE__ */ new Map();
595
589
  return {
596
590
  name: "@astroscope/node/islands-manifest",
597
591
  configResolved(config) {
@@ -628,21 +622,31 @@ for (const candidate of [
628
622
  export { manifest };
629
623
  `;
630
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
+ },
631
630
  writeBundle(outputOptions, bundle) {
632
631
  if (!options.enabled || this.environment.name !== "client" || !outputOptions.dir) return;
633
632
  const chunks = {};
633
+ const entryByComponent = /* @__PURE__ */ new Map();
634
634
  for (const chunk of Object.values(bundle)) {
635
635
  if (chunk.type !== "chunk") continue;
636
636
  chunks[chunk.fileName] = {
637
637
  ...chunk.imports.length > 0 && { i: chunk.imports },
638
638
  ...chunk.dynamicImports.length > 0 && { d: chunk.dynamicImports }
639
639
  };
640
+ if (chunk.isEntry && chunk.facadeModuleId) entryByComponent.set(stripQuery(chunk.facadeModuleId), chunk.fileName);
640
641
  }
642
+ const routes = {};
643
+ for (const [pattern, islands] of routeIslands) routes[pattern] = [...islands].map((component) => entryByComponent.get(component)).filter(defined);
641
644
  const runtimeSource = fs.readFileSync(new URL("./islands-runtime.iife.js", import.meta.url), "utf-8").trim();
642
645
  if (runtimeSource.toLowerCase().includes("<\/script")) throw new Error("islands gate runtime must not contain \"<\/script\"");
643
646
  const manifest = {
644
647
  runtimeSource,
645
- chunks
648
+ chunks,
649
+ routes
646
650
  };
647
651
  const chunksDir = path.resolve(outputOptions.dir, "..", "server", "chunks");
648
652
  if (fs.existsSync(chunksDir)) fs.writeFileSync(path.join(chunksDir, MANIFEST_FILE_NAME), JSON.stringify(manifest));
@@ -671,94 +675,6 @@ function ssrSourcemapPlugin() {
671
675
  };
672
676
  }
673
677
  //#endregion
674
- //#region src/tweaks/strip-effects.ts
675
- const HOOK_NAMES = /* @__PURE__ */ new Set([
676
- "useEffect",
677
- "useLayoutEffect",
678
- "useInsertionEffect"
679
- ]);
680
- const REACT_SOURCE = /^react(\/.*)?$/;
681
- const TRANSFORMABLE = /\.(?:[mc]?[jt]sx?)$/;
682
- const EMPTY_FN = "(()=>{})";
683
- /**
684
- * in SSR builds, react effect hooks never execute. emptying their callbacks
685
- * lets rolldown drop dead branches — including dynamic imports of client-only
686
- * libs (maplibre-gl, hls.js, etc.) — from the server bundle, which in turn
687
- * stops nft from tracing them at docker-image time.
688
- *
689
- * scope is deliberately narrow: first-party code only (no node_modules), only
690
- * in the SSR pass, and binding-aware (the React import must resolve to the
691
- * real react package). raw chunks where bundling has erased the binding are
692
- * left alone — that's NFT's domain, not ours.
693
- */
694
- function stripSsrEffectsPlugin() {
695
- return {
696
- name: "@astroscope/node/tweaks/strip-effects",
697
- enforce: "post",
698
- transform(code, id, options) {
699
- if (!options?.ssr) return null;
700
- if (id.includes("/node_modules/")) return null;
701
- const cleanId = id.split("?")[0] ?? id;
702
- if (!TRANSFORMABLE.test(cleanId)) return null;
703
- if (!code.includes("useEffect") && !code.includes("useLayoutEffect") && !code.includes("useInsertionEffect")) return null;
704
- let ast;
705
- try {
706
- ast = Parser.parse(code, {
707
- ecmaVersion: "latest",
708
- sourceType: "module",
709
- allowReturnOutsideFunction: true,
710
- allowAwaitOutsideFunction: true,
711
- allowImportExportEverywhere: true,
712
- allowHashBang: true
713
- });
714
- } catch {
715
- return null;
716
- }
717
- const directHooks = /* @__PURE__ */ new Set();
718
- const namespaceHooks = /* @__PURE__ */ new Set();
719
- walk(ast, (node) => {
720
- if (node.type !== "ImportDeclaration") return;
721
- const src = node.source?.value;
722
- if (typeof src !== "string" || !REACT_SOURCE.test(src)) return;
723
- for (const spec of node.specifiers ?? []) if (spec.type === "ImportSpecifier") {
724
- const imported = spec.imported?.name ?? spec.imported?.value;
725
- if (HOOK_NAMES.has(imported)) directHooks.add(spec.local.name);
726
- } else if (spec.type === "ImportDefaultSpecifier" || spec.type === "ImportNamespaceSpecifier") namespaceHooks.add(spec.local.name);
727
- });
728
- if (directHooks.size === 0 && namespaceHooks.size === 0) return null;
729
- const replacements = [];
730
- walk(ast, (node) => {
731
- if (node.type !== "CallExpression" || !node.arguments?.length) return;
732
- const callee = node.callee;
733
- const isDirect = callee.type === "Identifier" && directHooks.has(callee.name);
734
- 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);
735
- if (!isDirect && !isMember) return;
736
- const arg = node.arguments[0];
737
- replacements.push({
738
- start: arg.start,
739
- end: arg.end
740
- });
741
- });
742
- if (replacements.length === 0) return null;
743
- const s = new MagicString(code);
744
- for (const { start, end } of replacements) s.overwrite(start, end, EMPTY_FN);
745
- return {
746
- code: s.toString(),
747
- map: s.generateMap({ hires: true }).toString()
748
- };
749
- }
750
- };
751
- }
752
- function walk(node, visit) {
753
- if (!node || typeof node !== "object") return;
754
- if (typeof node.type === "string") visit(node);
755
- for (const key of Object.keys(node)) {
756
- const v = node[key];
757
- if (Array.isArray(v)) for (const item of v) walk(item, visit);
758
- else if (v && typeof v === "object") walk(v, visit);
759
- }
760
- }
761
- //#endregion
762
678
  //#region src/integration/integration.ts
763
679
  const CONFIG_VIRTUAL_MODULE_ID = "virtual:@astroscope/node/config";
764
680
  const BOOT_VIRTUAL_MODULE_ID = "virtual:@astroscope/node/boot";
@@ -813,6 +729,7 @@ function node(options = {}) {
813
729
  const loggingExclude = loggingOptions ? loggingOptions.exclude ?? DEFAULT_REQUEST_EXCLUDES : [];
814
730
  const telemetryExclude = telemetryOptions ? telemetryOptions.exclude ?? DEFAULT_REQUEST_EXCLUDES : [];
815
731
  let astroConfig = null;
732
+ let resolvedRoutes = [];
816
733
  let bootEntry;
817
734
  let configSeam;
818
735
  let instrumentationSeam;
@@ -845,7 +762,7 @@ function node(options = {}) {
845
762
  const devMachinery = command === "dev" && bootOptions !== false ? createDevMachinery({
846
763
  entry: bootEntry ? path.relative(root, bootEntry) : void 0,
847
764
  watch,
848
- watchEntries: [relativeSeam(configSeam), relativeSeam(logSeam)].filter((entry) => !!entry),
765
+ watchEntries: [relativeSeam(configSeam), relativeSeam(logSeam)].filter(defined),
849
766
  prepare: (importModule) => preparePlatform({
850
767
  dev: true,
851
768
  telemetry: telemetryOptions && telemetryOptions.dev ? { prometheus: telemetryOptions.prometheus ?? {} } : false,
@@ -891,10 +808,10 @@ function node(options = {}) {
891
808
  ...islandWarmup,
892
809
  createIslandsManifestPlugin({
893
810
  logger,
894
- enabled: islandsEnabled
811
+ enabled: islandsEnabled,
812
+ pages: () => routeEntrypoints(root, resolvedRoutes)
895
813
  }),
896
814
  ssrSourcemapPlugin(),
897
- stripSsrEffectsPlugin(),
898
815
  {
899
816
  name: "@astroscope/node",
900
817
  configEnvironment(environmentName) {
@@ -951,6 +868,7 @@ function node(options = {}) {
951
868
  }) : void 0;
952
869
  server.middlewares.use((req, res, next) => {
953
870
  const inner = () => {
871
+ if (redirectDuplicateSlashes(req, res)) return;
954
872
  if (!dispatchNativeMount(req, res)) next();
955
873
  };
956
874
  if (instrument) instrument(req, res, inner);
@@ -978,14 +896,17 @@ function node(options = {}) {
978
896
  }
979
897
  });
980
898
  },
899
+ "astro:routes:resolved": ({ routes }) => {
900
+ resolvedRoutes = routes;
901
+ },
981
902
  "astro:build:done": async ({ logger }) => {
982
903
  if (!astroConfig) return;
983
904
  if (islandsEnabled) {
984
905
  const manifestPath = path.join(fileURLToPath(astroConfig.build.server), "chunks", "islands-manifest.json");
985
906
  if (fs.existsSync(manifestPath)) {
986
- const { transformPrerenderedHtml } = await import("./prerendered-DVjzWNbW.js");
907
+ const { transformPrerenderedHtml } = await import("./prerendered-Z-Qi-FFK.js");
987
908
  const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf-8"));
988
- const transformed = await transformPrerenderedHtml(fileURLToPath(astroConfig.build.client), manifest);
909
+ const transformed = transformPrerenderedHtml(fileURLToPath(astroConfig.build.client), manifest);
989
910
  if (transformed > 0) logger.info(`island preloading applied to ${transformed} prerendered page(s)`);
990
911
  }
991
912
  }