@analogjs/vite-plugin-nitro 3.0.0-alpha.9 → 3.0.0-alpha.91

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/package.json +30 -6
  2. package/src/index.d.ts +19 -9
  3. package/src/index.js +4 -1
  4. package/src/index.js.map +1 -1
  5. package/src/lib/build-server.d.ts +2 -2
  6. package/src/lib/build-server.js +1 -62
  7. package/src/lib/build-server.js.map +1 -1
  8. package/src/lib/build-sitemap.d.ts +23 -9
  9. package/src/lib/build-sitemap.js +118 -37
  10. package/src/lib/build-sitemap.js.map +1 -1
  11. package/src/lib/build-ssr.d.ts +3 -2
  12. package/src/lib/build-ssr.js +12 -2
  13. package/src/lib/build-ssr.js.map +1 -1
  14. package/src/lib/hooks/post-rendering-hook.d.ts +1 -1
  15. package/src/lib/hooks/post-rendering-hook.js.map +1 -1
  16. package/src/lib/options.d.ts +148 -114
  17. package/src/lib/plugins/dev-server-plugin.d.ts +3 -3
  18. package/src/lib/plugins/dev-server-plugin.js +9 -1
  19. package/src/lib/plugins/dev-server-plugin.js.map +1 -1
  20. package/src/lib/plugins/page-endpoints.d.ts +5 -5
  21. package/src/lib/plugins/page-endpoints.js.map +1 -1
  22. package/src/lib/plugins/server-fn-id-plugin.d.ts +11 -0
  23. package/src/lib/plugins/server-fn-id-plugin.js +27 -0
  24. package/src/lib/plugins/server-fn-id-plugin.js.map +1 -0
  25. package/src/lib/utils/debug.d.ts +5 -0
  26. package/src/lib/utils/debug.js +15 -0
  27. package/src/lib/utils/debug.js.map +1 -0
  28. package/src/lib/utils/derive-server-fn-id.d.ts +25 -0
  29. package/src/lib/utils/derive-server-fn-id.js +37 -0
  30. package/src/lib/utils/derive-server-fn-id.js.map +1 -0
  31. package/src/lib/utils/get-content-files.d.ts +55 -55
  32. package/src/lib/utils/get-content-files.js +12 -5
  33. package/src/lib/utils/get-content-files.js.map +1 -1
  34. package/src/lib/utils/get-page-handlers.d.ts +55 -58
  35. package/src/lib/utils/get-page-handlers.js +7 -10
  36. package/src/lib/utils/get-page-handlers.js.map +1 -1
  37. package/src/lib/utils/get-server-fn-handlers.d.ts +29 -0
  38. package/src/lib/utils/get-server-fn-handlers.js +66 -0
  39. package/src/lib/utils/get-server-fn-handlers.js.map +1 -0
  40. package/src/lib/utils/i18n-prerender.d.ts +36 -0
  41. package/src/lib/utils/i18n-prerender.js +23 -0
  42. package/src/lib/utils/i18n-prerender.js.map +1 -0
  43. package/src/lib/utils/inject-server-fn-ids.d.ts +17 -0
  44. package/src/lib/utils/inject-server-fn-ids.js +94 -0
  45. package/src/lib/utils/inject-server-fn-ids.js.map +1 -0
  46. package/src/lib/utils/load-esm.d.ts +18 -18
  47. package/src/lib/utils/node-web-bridge.d.ts +1 -1
  48. package/src/lib/utils/node-web-bridge.js +18 -2
  49. package/src/lib/utils/node-web-bridge.js.map +1 -1
  50. package/src/lib/utils/register-dev-middleware.d.ts +12 -12
  51. package/src/lib/utils/register-dev-middleware.js.map +1 -1
  52. package/src/lib/utils/register-i18n-watcher.d.ts +15 -0
  53. package/src/lib/utils/register-i18n-watcher.js +27 -0
  54. package/src/lib/utils/register-i18n-watcher.js.map +1 -0
  55. package/src/lib/utils/renderers.d.ts +29 -49
  56. package/src/lib/utils/renderers.js +5 -52
  57. package/src/lib/utils/renderers.js.map +1 -1
  58. package/src/lib/utils/rolldown.d.ts +1 -1
  59. package/src/lib/utils/rolldown.js.map +1 -1
  60. package/src/lib/utils/server-fn-endpoints.d.ts +41 -0
  61. package/src/lib/utils/server-fn-endpoints.js +57 -0
  62. package/src/lib/utils/server-fn-endpoints.js.map +1 -0
  63. package/src/lib/vite-plugin-nitro.d.ts +3 -3
  64. package/src/lib/vite-plugin-nitro.js +406 -53
  65. package/src/lib/vite-plugin-nitro.js.map +1 -1
@@ -0,0 +1,94 @@
1
+ import { deriveServerFnId } from "./derive-server-fn-id.js";
2
+ import { parseSync } from "oxc-parser";
3
+ import MagicString from "magic-string";
4
+ //#region packages/vite-plugin-nitro/src/lib/utils/inject-server-fn-ids.ts
5
+ var SERVER_FN_SOURCE = "@analogjs/router/server";
6
+ /**
7
+ * Server/SSR-build transform: injects the derived `id` into each
8
+ * `export const NAME = serverFn(config, handler)` so the function registers
9
+ * under the same opaque id the client proxy dispatches to. Unlike the client
10
+ * scrub this keeps the handler and every other statement intact — it only edits
11
+ * the config object — so the server module still runs the real implementation.
12
+ *
13
+ * Returns `null` when the module defines no server function.
14
+ */
15
+ function injectServerFnIds(code, fileId) {
16
+ if (!code.includes("serverFn")) return null;
17
+ const { program } = parseSync(fileId, code);
18
+ const body = program.body;
19
+ const serverFnLocalNames = collectServerFnLocalNames(body);
20
+ const magic = new MagicString(code);
21
+ const ids = [];
22
+ for (const node of body) {
23
+ if (node.type !== "ExportNamedDeclaration" || node.declaration?.type !== "VariableDeclaration") continue;
24
+ for (const declarator of node.declaration.declarations) {
25
+ if (declarator.id?.type !== "Identifier" || declarator.init?.type !== "CallExpression" || declarator.init.callee?.type !== "Identifier" || !serverFnLocalNames.has(declarator.init.callee.name)) continue;
26
+ const name = declarator.id.name;
27
+ const id = deriveServerFnId(fileId, name);
28
+ ids.push({
29
+ name,
30
+ id
31
+ });
32
+ const args = declarator.init.arguments ?? [];
33
+ const arg0 = args[0];
34
+ const idProp = `id: ${JSON.stringify(id)}`;
35
+ if (arg0?.type === "ObjectExpression") {
36
+ assertMethodInputCompatible(arg0, name, fileId);
37
+ injectId(magic, arg0, id);
38
+ } else if (isFunctionNode(arg0)) magic.appendLeft(arg0.start, `{ ${idProp} }, `);
39
+ else if (arg0 && args.length >= 2) {
40
+ magic.appendLeft(arg0.start, `{ ${idProp}, input: `);
41
+ magic.appendRight(arg0.end, ` }`);
42
+ } else throw new Error(`[analog] serverFn "${name}" in ${fileId} must be called as serverFn(handler), serverFn(schema, handler), or serverFn(config, handler).`);
43
+ }
44
+ }
45
+ if (ids.length === 0) return null;
46
+ return {
47
+ code: magic.toString(),
48
+ ids
49
+ };
50
+ }
51
+ /**
52
+ * Insert (or replace) `id: "<derived>"` in the config object. Authors do not
53
+ * supply an id; a stray one is overwritten so the route is always the derived,
54
+ * non-enumerable digest — never an author-controlled value.
55
+ */
56
+ function injectId(magic, configArg, id) {
57
+ const existing = (configArg.properties ?? []).find((prop) => prop.type === "Property" && (prop.key?.type === "Identifier" ? prop.key.name : prop.key?.value) === "id");
58
+ if (existing) {
59
+ magic.overwrite(existing.value.start, existing.value.end, JSON.stringify(id));
60
+ return;
61
+ }
62
+ magic.appendRight(configArg.start + 1, ` id: ${JSON.stringify(id)},`);
63
+ }
64
+ /**
65
+ * Reject `method: 'GET'` alongside an `input` schema at build time: GET carries
66
+ * no body, so a GET function can never receive validated input.
67
+ */
68
+ function assertMethodInputCompatible(configArg, name, fileId) {
69
+ let method;
70
+ let hasInput = false;
71
+ for (const prop of configArg.properties ?? []) {
72
+ if (prop.type !== "Property") continue;
73
+ const key = prop.key?.type === "Identifier" ? prop.key.name : prop.key?.value;
74
+ if (key === "method" && prop.value?.type === "Literal") method = prop.value.value;
75
+ else if (key === "input") hasInput = true;
76
+ }
77
+ if (method === "GET" && hasInput) throw new Error(`[analog] serverFn "${name}" in ${fileId} declares method: 'GET' with an input schema; GET carries no body. Use POST (the default when input is present) or drop the input.`);
78
+ }
79
+ function isFunctionNode(node) {
80
+ return node?.type === "ArrowFunctionExpression" || node?.type === "FunctionExpression";
81
+ }
82
+ function collectServerFnLocalNames(body) {
83
+ const names = /* @__PURE__ */ new Set();
84
+ for (const node of body) {
85
+ if (node.type !== "ImportDeclaration" || node.source?.value !== SERVER_FN_SOURCE) continue;
86
+ for (const spec of node.specifiers ?? []) if (spec.type === "ImportSpecifier" && spec.imported?.name === "serverFn") names.add(spec.local.name);
87
+ }
88
+ if (names.size === 0) names.add("serverFn");
89
+ return names;
90
+ }
91
+ //#endregion
92
+ export { injectServerFnIds };
93
+
94
+ //# sourceMappingURL=inject-server-fn-ids.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inject-server-fn-ids.js","names":[],"sources":["../../../../src/lib/utils/inject-server-fn-ids.ts"],"sourcesContent":["import MagicString from 'magic-string';\nimport { parseSync } from 'oxc-parser';\n\nimport { deriveServerFnId } from './derive-server-fn-id.js';\n\nconst SERVER_FN_SOURCE = '@analogjs/router/server';\n\nexport interface InjectServerFnIdsResult {\n code: string;\n ids: { name: string; id: string }[];\n}\n\n/**\n * Server/SSR-build transform: injects the derived `id` into each\n * `export const NAME = serverFn(config, handler)` so the function registers\n * under the same opaque id the client proxy dispatches to. Unlike the client\n * scrub this keeps the handler and every other statement intact — it only edits\n * the config object — so the server module still runs the real implementation.\n *\n * Returns `null` when the module defines no server function.\n */\nexport function injectServerFnIds(\n code: string,\n fileId: string,\n): InjectServerFnIdsResult | null {\n if (!code.includes('serverFn')) {\n return null;\n }\n\n const { program } = parseSync(fileId, code);\n const body = (program as { body: any[] }).body;\n const serverFnLocalNames = collectServerFnLocalNames(body);\n\n const magic = new MagicString(code);\n const ids: { name: string; id: string }[] = [];\n\n for (const node of body) {\n if (\n node.type !== 'ExportNamedDeclaration' ||\n node.declaration?.type !== 'VariableDeclaration'\n ) {\n continue;\n }\n for (const declarator of node.declaration.declarations) {\n if (\n declarator.id?.type !== 'Identifier' ||\n declarator.init?.type !== 'CallExpression' ||\n declarator.init.callee?.type !== 'Identifier' ||\n !serverFnLocalNames.has(declarator.init.callee.name)\n ) {\n continue;\n }\n\n const name = declarator.id.name;\n const id = deriveServerFnId(fileId, name);\n ids.push({ name, id });\n\n const args = declarator.init.arguments ?? [];\n const arg0 = args[0];\n const idProp = `id: ${JSON.stringify(id)}`;\n\n if (arg0?.type === 'ObjectExpression') {\n // serverFn({ … }, handler) — inject the id into the config object.\n assertMethodInputCompatible(arg0, name, fileId);\n injectId(magic, arg0, id);\n } else if (isFunctionNode(arg0)) {\n // serverFn(handler) — synthesize a config: `{ id }, handler`.\n magic.appendLeft(arg0.start, `{ ${idProp} }, `);\n } else if (arg0 && args.length >= 2) {\n // serverFn(schema, handler) — wrap the schema into `{ id, input: <schema> }`.\n magic.appendLeft(arg0.start, `{ ${idProp}, input: `);\n magic.appendRight(arg0.end, ` }`);\n } else {\n throw new Error(\n `[analog] serverFn \"${name}\" in ${fileId} must be called as serverFn(handler), serverFn(schema, handler), or serverFn(config, handler).`,\n );\n }\n }\n }\n\n if (ids.length === 0) {\n return null;\n }\n\n return { code: magic.toString(), ids };\n}\n\n/**\n * Insert (or replace) `id: \"<derived>\"` in the config object. Authors do not\n * supply an id; a stray one is overwritten so the route is always the derived,\n * non-enumerable digest — never an author-controlled value.\n */\nfunction injectId(magic: MagicString, configArg: any, id: string): void {\n const existing = (configArg.properties ?? []).find(\n (prop: any) =>\n prop.type === 'Property' &&\n (prop.key?.type === 'Identifier' ? prop.key.name : prop.key?.value) ===\n 'id',\n );\n\n if (existing) {\n magic.overwrite(\n existing.value.start,\n existing.value.end,\n JSON.stringify(id),\n );\n return;\n }\n\n // Insert as the first property, right after the opening brace.\n magic.appendRight(configArg.start + 1, ` id: ${JSON.stringify(id)},`);\n}\n\n/**\n * Reject `method: 'GET'` alongside an `input` schema at build time: GET carries\n * no body, so a GET function can never receive validated input.\n */\nfunction assertMethodInputCompatible(\n configArg: any,\n name: string,\n fileId: string,\n): void {\n let method: string | undefined;\n let hasInput = false;\n for (const prop of configArg.properties ?? []) {\n if (prop.type !== 'Property') continue;\n const key =\n prop.key?.type === 'Identifier' ? prop.key.name : prop.key?.value;\n if (key === 'method' && prop.value?.type === 'Literal') {\n method = prop.value.value;\n } else if (key === 'input') {\n hasInput = true;\n }\n }\n if (method === 'GET' && hasInput) {\n throw new Error(\n `[analog] serverFn \"${name}\" in ${fileId} declares method: 'GET' with an input schema; GET carries no body. Use POST (the default when input is present) or drop the input.`,\n );\n }\n}\n\nfunction isFunctionNode(node: any): boolean {\n return (\n node?.type === 'ArrowFunctionExpression' ||\n node?.type === 'FunctionExpression'\n );\n}\n\nfunction collectServerFnLocalNames(body: any[]): Set<string> {\n const names = new Set<string>();\n for (const node of body) {\n if (\n node.type !== 'ImportDeclaration' ||\n node.source?.value !== SERVER_FN_SOURCE\n ) {\n continue;\n }\n for (const spec of node.specifiers ?? []) {\n if (\n spec.type === 'ImportSpecifier' &&\n spec.imported?.name === 'serverFn'\n ) {\n names.add(spec.local.name);\n }\n }\n }\n if (names.size === 0) {\n names.add('serverFn');\n }\n return names;\n}\n"],"mappings":";;;;AAKA,IAAM,mBAAmB;;;;;;;;;;AAgBzB,SAAgB,kBACd,MACA,QACgC;AAChC,KAAI,CAAC,KAAK,SAAS,WAAW,CAC5B,QAAO;CAGT,MAAM,EAAE,YAAY,UAAU,QAAQ,KAAK;CAC3C,MAAM,OAAQ,QAA4B;CAC1C,MAAM,qBAAqB,0BAA0B,KAAK;CAE1D,MAAM,QAAQ,IAAI,YAAY,KAAK;CACnC,MAAM,MAAsC,EAAE;AAE9C,MAAK,MAAM,QAAQ,MAAM;AACvB,MACE,KAAK,SAAS,4BACd,KAAK,aAAa,SAAS,sBAE3B;AAEF,OAAK,MAAM,cAAc,KAAK,YAAY,cAAc;AACtD,OACE,WAAW,IAAI,SAAS,gBACxB,WAAW,MAAM,SAAS,oBAC1B,WAAW,KAAK,QAAQ,SAAS,gBACjC,CAAC,mBAAmB,IAAI,WAAW,KAAK,OAAO,KAAK,CAEpD;GAGF,MAAM,OAAO,WAAW,GAAG;GAC3B,MAAM,KAAK,iBAAiB,QAAQ,KAAK;AACzC,OAAI,KAAK;IAAE;IAAM;IAAI,CAAC;GAEtB,MAAM,OAAO,WAAW,KAAK,aAAa,EAAE;GAC5C,MAAM,OAAO,KAAK;GAClB,MAAM,SAAS,OAAO,KAAK,UAAU,GAAG;AAExC,OAAI,MAAM,SAAS,oBAAoB;AAErC,gCAA4B,MAAM,MAAM,OAAO;AAC/C,aAAS,OAAO,MAAM,GAAG;cAChB,eAAe,KAAK,CAE7B,OAAM,WAAW,KAAK,OAAO,KAAK,OAAO,MAAM;YACtC,QAAQ,KAAK,UAAU,GAAG;AAEnC,UAAM,WAAW,KAAK,OAAO,KAAK,OAAO,WAAW;AACpD,UAAM,YAAY,KAAK,KAAK,KAAK;SAEjC,OAAM,IAAI,MACR,sBAAsB,KAAK,OAAO,OAAO,gGAC1C;;;AAKP,KAAI,IAAI,WAAW,EACjB,QAAO;AAGT,QAAO;EAAE,MAAM,MAAM,UAAU;EAAE;EAAK;;;;;;;AAQxC,SAAS,SAAS,OAAoB,WAAgB,IAAkB;CACtE,MAAM,YAAY,UAAU,cAAc,EAAE,EAAE,MAC3C,SACC,KAAK,SAAS,eACb,KAAK,KAAK,SAAS,eAAe,KAAK,IAAI,OAAO,KAAK,KAAK,WAC3D,KACL;AAED,KAAI,UAAU;AACZ,QAAM,UACJ,SAAS,MAAM,OACf,SAAS,MAAM,KACf,KAAK,UAAU,GAAG,CACnB;AACD;;AAIF,OAAM,YAAY,UAAU,QAAQ,GAAG,QAAQ,KAAK,UAAU,GAAG,CAAC,GAAG;;;;;;AAOvE,SAAS,4BACP,WACA,MACA,QACM;CACN,IAAI;CACJ,IAAI,WAAW;AACf,MAAK,MAAM,QAAQ,UAAU,cAAc,EAAE,EAAE;AAC7C,MAAI,KAAK,SAAS,WAAY;EAC9B,MAAM,MACJ,KAAK,KAAK,SAAS,eAAe,KAAK,IAAI,OAAO,KAAK,KAAK;AAC9D,MAAI,QAAQ,YAAY,KAAK,OAAO,SAAS,UAC3C,UAAS,KAAK,MAAM;WACX,QAAQ,QACjB,YAAW;;AAGf,KAAI,WAAW,SAAS,SACtB,OAAM,IAAI,MACR,sBAAsB,KAAK,OAAO,OAAO,oIAC1C;;AAIL,SAAS,eAAe,MAAoB;AAC1C,QACE,MAAM,SAAS,6BACf,MAAM,SAAS;;AAInB,SAAS,0BAA0B,MAA0B;CAC3D,MAAM,wBAAQ,IAAI,KAAa;AAC/B,MAAK,MAAM,QAAQ,MAAM;AACvB,MACE,KAAK,SAAS,uBACd,KAAK,QAAQ,UAAU,iBAEvB;AAEF,OAAK,MAAM,QAAQ,KAAK,cAAc,EAAE,CACtC,KACE,KAAK,SAAS,qBACd,KAAK,UAAU,SAAS,WAExB,OAAM,IAAI,KAAK,MAAM,KAAK;;AAIhC,KAAI,MAAM,SAAS,EACjB,OAAM,IAAI,WAAW;AAEvB,QAAO"}
@@ -1,21 +1,21 @@
1
1
  /**
2
- * @license
3
- * Copyright Google LLC All Rights Reserved.
4
- *
5
- * Use of this source code is governed by an MIT-style license that can be
6
- * found in the LICENSE file at https://angular.dev/license
7
- */
8
- import { URL } from 'node:url';
2
+ * @license
3
+ * Copyright Google LLC All Rights Reserved.
4
+ *
5
+ * Use of this source code is governed by an MIT-style license that can be
6
+ * found in the LICENSE file at https://angular.dev/license
7
+ */
8
+ import { URL } from "node:url";
9
9
  /**
10
- * This uses a dynamic import to load a module which may be ESM.
11
- * CommonJS code can load ESM code via a dynamic import. Unfortunately, TypeScript
12
- * will currently, unconditionally downlevel dynamic import into a require call.
13
- * require calls cannot load ESM code and will result in a runtime error. To workaround
14
- * this, a Function constructor is used to prevent TypeScript from changing the dynamic import.
15
- * Once TypeScript provides support for keeping the dynamic import this workaround can
16
- * be dropped.
17
- *
18
- * @param modulePath The path of the module to load.
19
- * @returns A Promise that resolves to the dynamically imported module.
20
- */
10
+ * This uses a dynamic import to load a module which may be ESM.
11
+ * CommonJS code can load ESM code via a dynamic import. Unfortunately, TypeScript
12
+ * will currently, unconditionally downlevel dynamic import into a require call.
13
+ * require calls cannot load ESM code and will result in a runtime error. To workaround
14
+ * this, a Function constructor is used to prevent TypeScript from changing the dynamic import.
15
+ * Once TypeScript provides support for keeping the dynamic import this workaround can
16
+ * be dropped.
17
+ *
18
+ * @param modulePath The path of the module to load.
19
+ * @returns A Promise that resolves to the dynamically imported module.
20
+ */
21
21
  export declare function loadEsmModule<T>(modulePath: string | URL): Promise<T>;
@@ -1,3 +1,3 @@
1
- import type { IncomingMessage, ServerResponse } from 'node:http';
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
2
  export declare function toWebRequest(req: IncomingMessage): Request;
3
3
  export declare function writeWebResponseToNode(res: ServerResponse, response: Response): Promise<void>;
@@ -3,7 +3,7 @@ import { pipeline } from "node:stream/promises";
3
3
  //#region packages/vite-plugin-nitro/src/lib/utils/node-web-bridge.ts
4
4
  function toWebHeaders(headers) {
5
5
  return Object.entries(headers).reduce((acc, [key, value]) => {
6
- if (value) acc.set(key, Array.isArray(value) ? value.join(", ") : value);
6
+ if (value && !key.startsWith(":")) acc.set(key, Array.isArray(value) ? value.join(", ") : value);
7
7
  return acc;
8
8
  }, new Headers());
9
9
  }
@@ -19,6 +19,17 @@ function toWebRequest(req) {
19
19
  duplex: body ? "half" : void 0
20
20
  });
21
21
  }
22
+ function isClientDisconnectError(error, res) {
23
+ if (!(error instanceof Error)) return false;
24
+ const hasDisconnectCode = "code" in error && typeof error.code === "string" && [
25
+ "ERR_STREAM_PREMATURE_CLOSE",
26
+ "ERR_INVALID_STATE",
27
+ "ECONNRESET",
28
+ "EPIPE"
29
+ ].includes(error.code);
30
+ const hasDisconnectMessage = /closed or destroyed stream/i.test(error.message);
31
+ return (res.destroyed || res.writableEnded) && (hasDisconnectCode || hasDisconnectMessage);
32
+ }
22
33
  async function writeWebResponseToNode(res, response) {
23
34
  res.statusCode = response.status;
24
35
  res.statusMessage = response.statusText;
@@ -31,7 +42,12 @@ async function writeWebResponseToNode(res, response) {
31
42
  res.end();
32
43
  return;
33
44
  }
34
- await pipeline(Readable.fromWeb(response.body), res);
45
+ try {
46
+ await pipeline(Readable.fromWeb(response.body), res);
47
+ } catch (error) {
48
+ if (isClientDisconnectError(error, res)) return;
49
+ throw error;
50
+ }
35
51
  }
36
52
  //#endregion
37
53
  export { toWebRequest, writeWebResponseToNode };
@@ -1 +1 @@
1
- {"version":3,"file":"node-web-bridge.js","names":[],"sources":["../../../../../../packages/vite-plugin-nitro/src/lib/utils/node-web-bridge.ts"],"sourcesContent":["import type {\n IncomingHttpHeaders,\n IncomingMessage,\n ServerResponse,\n} from 'node:http';\nimport { Readable } from 'node:stream';\nimport { pipeline } from 'node:stream/promises';\n\nfunction toWebHeaders(headers: IncomingHttpHeaders) {\n return Object.entries(headers).reduce((acc, [key, value]) => {\n if (value) {\n acc.set(key, Array.isArray(value) ? value.join(', ') : value);\n }\n\n return acc;\n }, new Headers());\n}\n\nexport function toWebRequest(req: IncomingMessage): Request {\n const protocol = 'http';\n const host = req.headers.host || 'localhost';\n const url = new URL(req.url || '/', `${protocol}://${host}`);\n const body =\n req.method && !['GET', 'HEAD'].includes(req.method)\n ? (Readable.toWeb(req) as ReadableStream<Uint8Array>)\n : undefined;\n\n return new Request(url, {\n method: req.method,\n headers: toWebHeaders(req.headers),\n body,\n // @ts-expect-error duplex is required for streaming request bodies in Node.js\n duplex: body ? 'half' : undefined,\n });\n}\n\nexport async function writeWebResponseToNode(\n res: ServerResponse,\n response: Response,\n): Promise<void> {\n res.statusCode = response.status;\n res.statusMessage = response.statusText;\n\n const setCookies =\n 'getSetCookie' in response.headers &&\n typeof response.headers.getSetCookie === 'function'\n ? response.headers.getSetCookie()\n : [];\n\n if (setCookies.length > 0) {\n res.setHeader('set-cookie', setCookies);\n }\n\n response.headers.forEach((value, key) => {\n if (key !== 'set-cookie') {\n res.setHeader(key, value);\n }\n });\n\n if (!response.body) {\n res.end();\n return;\n }\n\n // The Web ReadableStream and Node.js stream/web ReadableStream types\n // are structurally identical at runtime but TypeScript treats them as\n // distinct nominal types. The double-cast bridges this gap safely.\n await pipeline(\n Readable.fromWeb(\n response.body as unknown as import('node:stream/web').ReadableStream,\n ),\n res,\n );\n}\n"],"mappings":";;;AAQA,SAAS,aAAa,SAA8B;AAClD,QAAO,OAAO,QAAQ,QAAQ,CAAC,QAAQ,KAAK,CAAC,KAAK,WAAW;AAC3D,MAAI,MACF,KAAI,IAAI,KAAK,MAAM,QAAQ,MAAM,GAAG,MAAM,KAAK,KAAK,GAAG,MAAM;AAG/D,SAAO;IACN,IAAI,SAAS,CAAC;;AAGnB,SAAgB,aAAa,KAA+B;CAC1D,MAAM,WAAW;CACjB,MAAM,OAAO,IAAI,QAAQ,QAAQ;CACjC,MAAM,MAAM,IAAI,IAAI,IAAI,OAAO,KAAK,GAAG,SAAS,KAAK,OAAO;CAC5D,MAAM,OACJ,IAAI,UAAU,CAAC,CAAC,OAAO,OAAO,CAAC,SAAS,IAAI,OAAO,GAC9C,SAAS,MAAM,IAAI,GACpB,KAAA;AAEN,QAAO,IAAI,QAAQ,KAAK;EACtB,QAAQ,IAAI;EACZ,SAAS,aAAa,IAAI,QAAQ;EAClC;EAEA,QAAQ,OAAO,SAAS,KAAA;EACzB,CAAC;;AAGJ,eAAsB,uBACpB,KACA,UACe;AACf,KAAI,aAAa,SAAS;AAC1B,KAAI,gBAAgB,SAAS;CAE7B,MAAM,aACJ,kBAAkB,SAAS,WAC3B,OAAO,SAAS,QAAQ,iBAAiB,aACrC,SAAS,QAAQ,cAAc,GAC/B,EAAE;AAER,KAAI,WAAW,SAAS,EACtB,KAAI,UAAU,cAAc,WAAW;AAGzC,UAAS,QAAQ,SAAS,OAAO,QAAQ;AACvC,MAAI,QAAQ,aACV,KAAI,UAAU,KAAK,MAAM;GAE3B;AAEF,KAAI,CAAC,SAAS,MAAM;AAClB,MAAI,KAAK;AACT;;AAMF,OAAM,SACJ,SAAS,QACP,SAAS,KACV,EACD,IACD"}
1
+ {"version":3,"file":"node-web-bridge.js","names":[],"sources":["../../../../src/lib/utils/node-web-bridge.ts"],"sourcesContent":["import type {\n IncomingHttpHeaders,\n IncomingMessage,\n ServerResponse,\n} from 'node:http';\nimport { Readable } from 'node:stream';\nimport { pipeline } from 'node:stream/promises';\n\nfunction toWebHeaders(headers: IncomingHttpHeaders) {\n return Object.entries(headers).reduce((acc, [key, value]) => {\n if (value && !key.startsWith(':')) {\n acc.set(key, Array.isArray(value) ? value.join(', ') : value);\n }\n\n return acc;\n }, new Headers());\n}\n\nexport function toWebRequest(req: IncomingMessage): Request {\n const protocol = 'http';\n const host = req.headers.host || 'localhost';\n const url = new URL(req.url || '/', `${protocol}://${host}`);\n const body =\n req.method && !['GET', 'HEAD'].includes(req.method)\n ? (Readable.toWeb(req) as ReadableStream<Uint8Array>)\n : undefined;\n\n return new Request(url, {\n method: req.method,\n headers: toWebHeaders(req.headers),\n body,\n // @ts-expect-error duplex is required for streaming request bodies in Node.js\n duplex: body ? 'half' : undefined,\n });\n}\n\nfunction isClientDisconnectError(error: unknown, res: ServerResponse): boolean {\n if (!(error instanceof Error)) {\n return false;\n }\n\n const hasDisconnectCode =\n 'code' in error &&\n typeof error.code === 'string' &&\n [\n 'ERR_STREAM_PREMATURE_CLOSE',\n 'ERR_INVALID_STATE',\n 'ECONNRESET',\n 'EPIPE',\n ].includes(error.code);\n\n const hasDisconnectMessage = /closed or destroyed stream/i.test(\n error.message,\n );\n\n return (\n (res.destroyed || res.writableEnded) &&\n (hasDisconnectCode || hasDisconnectMessage)\n );\n}\n\nexport async function writeWebResponseToNode(\n res: ServerResponse,\n response: Response,\n): Promise<void> {\n res.statusCode = response.status;\n res.statusMessage = response.statusText;\n\n const setCookies =\n 'getSetCookie' in response.headers &&\n typeof response.headers.getSetCookie === 'function'\n ? response.headers.getSetCookie()\n : [];\n\n if (setCookies.length > 0) {\n res.setHeader('set-cookie', setCookies);\n }\n\n response.headers.forEach((value, key) => {\n if (key !== 'set-cookie') {\n res.setHeader(key, value);\n }\n });\n\n if (!response.body) {\n res.end();\n return;\n }\n\n // The Web ReadableStream and Node.js stream/web ReadableStream types\n // are structurally identical at runtime but TypeScript treats them as\n // distinct nominal types. The double-cast bridges this gap safely.\n try {\n await pipeline(\n Readable.fromWeb(\n response.body as unknown as import('node:stream/web').ReadableStream,\n ),\n res,\n );\n } catch (error) {\n // Long-lived dev responses such as SSE can be interrupted by a browser\n // refresh or HMR-triggered reconnect. Those closed-stream cases are\n // expected and should not surface as noisy server errors.\n if (isClientDisconnectError(error, res)) {\n return;\n }\n\n throw error;\n }\n}\n"],"mappings":";;;AAQA,SAAS,aAAa,SAA8B;AAClD,QAAO,OAAO,QAAQ,QAAQ,CAAC,QAAQ,KAAK,CAAC,KAAK,WAAW;AAC3D,MAAI,SAAS,CAAC,IAAI,WAAW,IAAI,CAC/B,KAAI,IAAI,KAAK,MAAM,QAAQ,MAAM,GAAG,MAAM,KAAK,KAAK,GAAG,MAAM;AAG/D,SAAO;IACN,IAAI,SAAS,CAAC;;AAGnB,SAAgB,aAAa,KAA+B;CAC1D,MAAM,WAAW;CACjB,MAAM,OAAO,IAAI,QAAQ,QAAQ;CACjC,MAAM,MAAM,IAAI,IAAI,IAAI,OAAO,KAAK,GAAG,SAAS,KAAK,OAAO;CAC5D,MAAM,OACJ,IAAI,UAAU,CAAC,CAAC,OAAO,OAAO,CAAC,SAAS,IAAI,OAAO,GAC9C,SAAS,MAAM,IAAI,GACpB,KAAA;AAEN,QAAO,IAAI,QAAQ,KAAK;EACtB,QAAQ,IAAI;EACZ,SAAS,aAAa,IAAI,QAAQ;EAClC;EAEA,QAAQ,OAAO,SAAS,KAAA;EACzB,CAAC;;AAGJ,SAAS,wBAAwB,OAAgB,KAA8B;AAC7E,KAAI,EAAE,iBAAiB,OACrB,QAAO;CAGT,MAAM,oBACJ,UAAU,SACV,OAAO,MAAM,SAAS,YACtB;EACE;EACA;EACA;EACA;EACD,CAAC,SAAS,MAAM,KAAK;CAExB,MAAM,uBAAuB,8BAA8B,KACzD,MAAM,QACP;AAED,SACG,IAAI,aAAa,IAAI,mBACrB,qBAAqB;;AAI1B,eAAsB,uBACpB,KACA,UACe;AACf,KAAI,aAAa,SAAS;AAC1B,KAAI,gBAAgB,SAAS;CAE7B,MAAM,aACJ,kBAAkB,SAAS,WAC3B,OAAO,SAAS,QAAQ,iBAAiB,aACrC,SAAS,QAAQ,cAAc,GAC/B,EAAE;AAER,KAAI,WAAW,SAAS,EACtB,KAAI,UAAU,cAAc,WAAW;AAGzC,UAAS,QAAQ,SAAS,OAAO,QAAQ;AACvC,MAAI,QAAQ,aACV,KAAI,UAAU,KAAK,MAAM;GAE3B;AAEF,KAAI,CAAC,SAAS,MAAM;AAClB,MAAI,KAAK;AACT;;AAMF,KAAI;AACF,QAAM,SACJ,SAAS,QACP,SAAS,KACV,EACD,IACD;UACM,OAAO;AAId,MAAI,wBAAwB,OAAO,IAAI,CACrC;AAGF,QAAM"}
@@ -1,14 +1,14 @@
1
- import { ViteDevServer } from 'vite';
1
+ import { ViteDevServer } from "vite";
2
2
  /**
3
- * Registers development server middleware by discovering and loading middleware files.
4
- *
5
- * Each discovered h3 middleware module is loaded through Vite's SSR loader,
6
- * wrapped in a temporary H3 app, then bridged back into Vite's Connect stack.
7
- * If the middleware does not write a response, control falls through to the
8
- * next Vite middleware.
9
- *
10
- * @param root The project root directory path
11
- * @param sourceRoot The source directory path (e.g., 'src')
12
- * @param viteServer The Vite development server instance
13
- */
3
+ * Registers development server middleware by discovering and loading middleware files.
4
+ *
5
+ * Each discovered h3 middleware module is loaded through Vite's SSR loader,
6
+ * wrapped in a temporary H3 app, then bridged back into Vite's Connect stack.
7
+ * If the middleware does not write a response, control falls through to the
8
+ * next Vite middleware.
9
+ *
10
+ * @param root The project root directory path
11
+ * @param sourceRoot The source directory path (e.g., 'src')
12
+ * @param viteServer The Vite development server instance
13
+ */
14
14
  export declare function registerDevServerMiddleware(root: string, sourceRoot: string, viteServer: ViteDevServer): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"register-dev-middleware.js","names":[],"sources":["../../../../../../packages/vite-plugin-nitro/src/lib/utils/register-dev-middleware.ts"],"sourcesContent":["import { ViteDevServer } from 'vite';\nimport { EventHandler, H3 } from 'nitro/h3';\nimport { globSync } from 'tinyglobby';\n\nimport { toWebRequest, writeWebResponseToNode } from './node-web-bridge.js';\n\nconst PASSTHROUGH_HEADER = 'x-analog-passthrough';\n\n/**\n * Registers development server middleware by discovering and loading middleware files.\n *\n * Each discovered h3 middleware module is loaded through Vite's SSR loader,\n * wrapped in a temporary H3 app, then bridged back into Vite's Connect stack.\n * If the middleware does not write a response, control falls through to the\n * next Vite middleware.\n *\n * @param root The project root directory path\n * @param sourceRoot The source directory path (e.g., 'src')\n * @param viteServer The Vite development server instance\n */\nexport async function registerDevServerMiddleware(\n root: string,\n sourceRoot: string,\n viteServer: ViteDevServer,\n): Promise<void> {\n const middlewareFiles = globSync(\n [`${root}/${sourceRoot}/server/middleware/**/*.ts`],\n {\n dot: true,\n absolute: true,\n },\n );\n\n middlewareFiles.forEach((file) => {\n // Create the H3 app once per middleware file (not per request).\n // The dynamic handler inside still loads the module fresh each request\n // via ssrLoadModule, preserving HMR.\n const app = new H3();\n app.use(async (event) => {\n const handler: EventHandler = await viteServer\n .ssrLoadModule(file)\n .then((m: unknown) => (m as { default: EventHandler }).default);\n return handler(event);\n });\n // Sentinel catch-all: when the middleware returns undefined (does not\n // handle the request), h3 does not emit its default 404 — instead we\n // detect the passthrough header and let the Connect stack continue.\n app.use(\n () =>\n new Response(null, {\n status: 204,\n headers: { [PASSTHROUGH_HEADER]: '1' },\n }),\n );\n\n viteServer.middlewares.use(async (req, res, next) => {\n const response = await app.fetch(toWebRequest(req));\n\n if (response.headers.get(PASSTHROUGH_HEADER) === '1') {\n next();\n return;\n }\n\n await writeWebResponseToNode(res, response);\n });\n });\n}\n"],"mappings":";;;;AAMA,IAAM,qBAAqB;;;;;;;;;;;;;AAc3B,eAAsB,4BACpB,MACA,YACA,YACe;AACS,UACtB,CAAC,GAAG,KAAK,GAAG,WAAW,4BAA4B,EACnD;EACE,KAAK;EACL,UAAU;EACX,CACF,CAEe,SAAS,SAAS;EAIhC,MAAM,MAAM,IAAI,IAAI;AACpB,MAAI,IAAI,OAAO,UAAU;AAIvB,WAH8B,MAAM,WACjC,cAAc,KAAK,CACnB,MAAM,MAAgB,EAAgC,QAAQ,EAClD,MAAM;IACrB;AAIF,MAAI,UAEA,IAAI,SAAS,MAAM;GACjB,QAAQ;GACR,SAAS,GAAG,qBAAqB,KAAK;GACvC,CAAC,CACL;AAED,aAAW,YAAY,IAAI,OAAO,KAAK,KAAK,SAAS;GACnD,MAAM,WAAW,MAAM,IAAI,MAAM,aAAa,IAAI,CAAC;AAEnD,OAAI,SAAS,QAAQ,IAAI,mBAAmB,KAAK,KAAK;AACpD,UAAM;AACN;;AAGF,SAAM,uBAAuB,KAAK,SAAS;IAC3C;GACF"}
1
+ {"version":3,"file":"register-dev-middleware.js","names":[],"sources":["../../../../src/lib/utils/register-dev-middleware.ts"],"sourcesContent":["import { ViteDevServer } from 'vite';\nimport { EventHandler, H3 } from 'nitro/h3';\nimport { globSync } from 'tinyglobby';\n\nimport { toWebRequest, writeWebResponseToNode } from './node-web-bridge.js';\n\nconst PASSTHROUGH_HEADER = 'x-analog-passthrough';\n\n/**\n * Registers development server middleware by discovering and loading middleware files.\n *\n * Each discovered h3 middleware module is loaded through Vite's SSR loader,\n * wrapped in a temporary H3 app, then bridged back into Vite's Connect stack.\n * If the middleware does not write a response, control falls through to the\n * next Vite middleware.\n *\n * @param root The project root directory path\n * @param sourceRoot The source directory path (e.g., 'src')\n * @param viteServer The Vite development server instance\n */\nexport async function registerDevServerMiddleware(\n root: string,\n sourceRoot: string,\n viteServer: ViteDevServer,\n): Promise<void> {\n const middlewareFiles = globSync(\n [`${root}/${sourceRoot}/server/middleware/**/*.ts`],\n {\n dot: true,\n absolute: true,\n },\n );\n\n middlewareFiles.forEach((file) => {\n // Create the H3 app once per middleware file (not per request).\n // The dynamic handler inside still loads the module fresh each request\n // via ssrLoadModule, preserving HMR.\n const app = new H3();\n app.use(async (event) => {\n const handler: EventHandler = await viteServer\n .ssrLoadModule(file)\n .then((m: unknown) => (m as { default: EventHandler }).default);\n return handler(event);\n });\n // Sentinel catch-all: when the middleware returns undefined (does not\n // handle the request), h3 does not emit its default 404 — instead we\n // detect the passthrough header and let the Connect stack continue.\n app.use(\n () =>\n new Response(null, {\n status: 204,\n headers: { [PASSTHROUGH_HEADER]: '1' },\n }),\n );\n\n viteServer.middlewares.use(async (req, res, next) => {\n const response = await app.fetch(toWebRequest(req));\n\n if (response.headers.get(PASSTHROUGH_HEADER) === '1') {\n next();\n return;\n }\n\n await writeWebResponseToNode(res, response);\n });\n });\n}\n"],"mappings":";;;;AAMA,IAAM,qBAAqB;;;;;;;;;;;;;AAc3B,eAAsB,4BACpB,MACA,YACA,YACe;AACS,UACtB,CAAC,GAAG,KAAK,GAAG,WAAW,4BAA4B,EACnD;EACE,KAAK;EACL,UAAU;EACX,CACF,CAEe,SAAS,SAAS;EAIhC,MAAM,MAAM,IAAI,IAAI;AACpB,MAAI,IAAI,OAAO,UAAU;AAIvB,WAH8B,MAAM,WACjC,cAAc,KAAK,CACnB,MAAM,MAAgB,EAAgC,QAAQ,EAClD,MAAM;IACrB;AAIF,MAAI,UAEA,IAAI,SAAS,MAAM;GACjB,QAAQ;GACR,SAAS,GAAG,qBAAqB,KAAK;GACvC,CAAC,CACL;AAED,aAAW,YAAY,IAAI,OAAO,KAAK,KAAK,SAAS;GACnD,MAAM,WAAW,MAAM,IAAI,MAAM,aAAa,IAAI,CAAC;AAEnD,OAAI,SAAS,QAAQ,IAAI,mBAAmB,KAAK,KAAK;AACpD,UAAM;AACN;;AAGF,SAAM,uBAAuB,KAAK,SAAS;IAC3C;GACF"}
@@ -0,0 +1,15 @@
1
+ import { ViteDevServer } from "vite";
2
+ /**
3
+ * Registers a file watcher that triggers a full page reload
4
+ * when translation files are added or modified.
5
+ *
6
+ * Matches files in i18n directories with .json, .xlf, .xmb, or .arb extensions.
7
+ *
8
+ * @param viteServer The Vite development server instance
9
+ */
10
+ export declare function registerI18nWatcher(viteServer: ViteDevServer): void;
11
+ /**
12
+ * Checks whether a file path looks like a translation file
13
+ * based on its location in an i18n directory and its extension.
14
+ */
15
+ export declare function isTranslationFile(path: string): boolean;
@@ -0,0 +1,27 @@
1
+ //#region packages/vite-plugin-nitro/src/lib/utils/register-i18n-watcher.ts
2
+ /**
3
+ * Registers a file watcher that triggers a full page reload
4
+ * when translation files are added or modified.
5
+ *
6
+ * Matches files in i18n directories with .json, .xlf, .xmb, or .arb extensions.
7
+ *
8
+ * @param viteServer The Vite development server instance
9
+ */
10
+ function registerI18nWatcher(viteServer) {
11
+ const triggerReload = (path) => {
12
+ if (isTranslationFile(path)) viteServer.ws.send({ type: "full-reload" });
13
+ };
14
+ viteServer.watcher.on("change", triggerReload);
15
+ viteServer.watcher.on("add", triggerReload);
16
+ }
17
+ /**
18
+ * Checks whether a file path looks like a translation file
19
+ * based on its location in an i18n directory and its extension.
20
+ */
21
+ function isTranslationFile(path) {
22
+ return /i18n.*\.(json|xlf|xmb|arb)$/.test(path);
23
+ }
24
+ //#endregion
25
+ export { registerI18nWatcher };
26
+
27
+ //# sourceMappingURL=register-i18n-watcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"register-i18n-watcher.js","names":[],"sources":["../../../../src/lib/utils/register-i18n-watcher.ts"],"sourcesContent":["import { ViteDevServer } from 'vite';\n\n/**\n * Registers a file watcher that triggers a full page reload\n * when translation files are added or modified.\n *\n * Matches files in i18n directories with .json, .xlf, .xmb, or .arb extensions.\n *\n * @param viteServer The Vite development server instance\n */\nexport function registerI18nWatcher(viteServer: ViteDevServer): void {\n const triggerReload = (path: string) => {\n if (isTranslationFile(path)) {\n viteServer.ws.send({ type: 'full-reload' });\n }\n };\n\n viteServer.watcher.on('change', triggerReload);\n viteServer.watcher.on('add', triggerReload);\n}\n\n/**\n * Checks whether a file path looks like a translation file\n * based on its location in an i18n directory and its extension.\n */\nexport function isTranslationFile(path: string): boolean {\n return /i18n.*\\.(json|xlf|xmb|arb)$/.test(path);\n}\n"],"mappings":";;;;;;;;;AAUA,SAAgB,oBAAoB,YAAiC;CACnE,MAAM,iBAAiB,SAAiB;AACtC,MAAI,kBAAkB,KAAK,CACzB,YAAW,GAAG,KAAK,EAAE,MAAM,eAAe,CAAC;;AAI/C,YAAW,QAAQ,GAAG,UAAU,cAAc;AAC9C,YAAW,QAAQ,GAAG,OAAO,cAAc;;;;;;AAO7C,SAAgB,kBAAkB,MAAuB;AACvD,QAAO,8BAA8B,KAAK,KAAK"}
@@ -1,53 +1,33 @@
1
1
  /**
2
- * Code snippet emitted into virtual modules to create a request-scoped
3
- * fetch using ofetch's `createFetch` + h3's `fetchWithEvent`.
4
- *
5
- * Shared between the SSR renderer and page-endpoint virtual modules so
6
- * the fetch-wiring logic stays in sync.
7
- *
8
- * The emitted variable is named `serverFetch` — callers should reference it
9
- * by that name.
10
- */
2
+ * Code snippet emitted into virtual modules to create a request-scoped
3
+ * fetch using ofetch's `createFetch` + h3's `fetchWithEvent`.
4
+ *
5
+ * Shared between the SSR renderer and page-endpoint virtual modules so
6
+ * the fetch-wiring logic stays in sync.
7
+ *
8
+ * The emitted variable is named `serverFetch` — callers should reference it
9
+ * by that name.
10
+ */
11
11
  export declare const SERVER_FETCH_FACTORY_SNIPPET = "\n const serverFetch = createFetch({\n fetch: (resource, init) => {\n const url = resource instanceof Request ? resource.url : resource.toString();\n return fetchWithEvent(event, url, init);\n }\n });";
12
12
  /**
13
- * SSR renderer virtual module content.
14
- *
15
- * This code runs inside Nitro's server runtime (Node.js context) where
16
- * event.node is always populated. In h3 v2, event.node is typed as optional,
17
- * so we use h3's first-class event properties (event.path, event.method) where
18
- * possible and apply optional chaining when accessing the Node.js context for
19
- * the Angular renderer which requires raw req/res objects.
20
- *
21
- * h3 v2 idiomatic APIs used:
22
- * - defineHandler (replaces defineEventHandler / eventHandler)
23
- * - event.path (replaces event.node.req.url)
24
- * - getResponseHeader compat shim (still available in h3 v2)
25
- */
26
- export declare function ssrRenderer(templatePath: string): string;
13
+ * SSR renderer virtual module content.
14
+ *
15
+ * This code runs inside Nitro's server runtime (Node.js context) where
16
+ * event.node is always populated. In h3 v2, event.node is typed as optional,
17
+ * so we use h3's first-class event properties (event.path, event.method) where
18
+ * possible and apply optional chaining when accessing the Node.js context for
19
+ * the Angular renderer which requires raw req/res objects.
20
+ *
21
+ * h3 v2 idiomatic APIs used:
22
+ * - defineHandler (replaces defineEventHandler / eventHandler)
23
+ * - event.path (replaces event.node.req.url)
24
+ * - getResponseHeader compat shim (still available in h3 v2)
25
+ */
26
+ export declare function ssrRenderer(): string;
27
27
  /**
28
- * Client-only renderer virtual module content.
29
- *
30
- * Used when SSR is disabled — simply serves the static index.html template
31
- * for every route, letting the client-side Angular router handle navigation.
32
- */
33
- export declare function clientRenderer(templatePath: string): string;
34
- /**
35
- * API middleware virtual module content.
36
- *
37
- * Intercepts requests matching the configured API prefix and either:
38
- * - Uses event-bound internal forwarding for GET requests (except .xml routes)
39
- * - Uses request proxying for all other methods to forward the full request
40
- *
41
- * h3 v2 idiomatic APIs used:
42
- * - defineHandler (replaces defineEventHandler / eventHandler)
43
- * - event.path (replaces event.node.req.url)
44
- * - event.method (replaces event.node.req.method)
45
- * - proxyRequest is retained internally because it preserves Nitro route
46
- * matching for event-bound server requests during SSR/prerender
47
- * - Object.fromEntries(event.req.headers.entries()) replaces direct event.node.req.headers access
48
- *
49
- * `fetchWithEvent` keeps the active event context while forwarding to a
50
- * rewritten path, which avoids falling through to the HTML renderer when
51
- * SSR code makes relative API requests.
52
- */
53
- export declare const apiMiddleware = "\nimport { defineHandler, fetchWithEvent, proxyRequest } from 'nitro/h3';\nimport { useRuntimeConfig } from 'nitro/runtime-config';\n\nexport default defineHandler(async (event) => {\n const prefix = useRuntimeConfig().prefix;\n const apiPrefix = `${prefix}/${useRuntimeConfig().apiPrefix}`;\n\n if (event.path?.startsWith(apiPrefix)) {\n const reqUrl = event.path?.replace(apiPrefix, '');\n\n if (\n event.method === 'GET' &&\n // in the case of XML routes, we want to proxy the request so that nitro gets the correct headers\n // and can render the XML correctly as a static asset\n !event.path?.endsWith('.xml')\n ) {\n return fetchWithEvent(event, reqUrl, {\n headers: Object.fromEntries(event.req.headers.entries()),\n });\n }\n\n return proxyRequest(event, reqUrl);\n }\n});";
28
+ * Client-only renderer virtual module content.
29
+ *
30
+ * Used when SSR is disabled — simply serves the static index.html template
31
+ * for every route, letting the client-side Angular router handle navigation.
32
+ */
33
+ export declare function clientRenderer(): string;
@@ -30,15 +30,14 @@ var SERVER_FETCH_FACTORY_SNIPPET = `
30
30
  * - event.path (replaces event.node.req.url)
31
31
  * - getResponseHeader compat shim (still available in h3 v2)
32
32
  */
33
- function ssrRenderer(templatePath) {
33
+ function ssrRenderer() {
34
34
  return `
35
- import { readFileSync } from 'node:fs';
36
35
  import { createFetch } from 'ofetch';
37
36
  import { defineHandler, fetchWithEvent } from 'nitro/h3';
38
37
  // @ts-ignore
39
38
  import renderer from '#analog/ssr';
39
+ import template from '#analog/index';
40
40
 
41
- const template = readFileSync(${JSON.stringify(templatePath)}, 'utf8');
42
41
  const normalizeHtmlRequestUrl = (url) =>
43
42
  url.replace(/\\/index\\.html(?=$|[?#])/, '/');
44
43
 
@@ -83,12 +82,10 @@ ${SERVER_FETCH_FACTORY_SNIPPET}
83
82
  * Used when SSR is disabled — simply serves the static index.html template
84
83
  * for every route, letting the client-side Angular router handle navigation.
85
84
  */
86
- function clientRenderer(templatePath) {
85
+ function clientRenderer() {
87
86
  return `
88
- import { readFileSync } from 'node:fs';
89
87
  import { defineHandler } from 'nitro/h3';
90
-
91
- const template = readFileSync(${JSON.stringify(templatePath)}, 'utf8');
88
+ import template from '#analog/index';
92
89
 
93
90
  export default defineHandler(async (event) => {
94
91
  event.res.headers.set('content-type', 'text/html; charset=utf-8');
@@ -96,51 +93,7 @@ export default defineHandler(async (event) => {
96
93
  });
97
94
  `;
98
95
  }
99
- /**
100
- * API middleware virtual module content.
101
- *
102
- * Intercepts requests matching the configured API prefix and either:
103
- * - Uses event-bound internal forwarding for GET requests (except .xml routes)
104
- * - Uses request proxying for all other methods to forward the full request
105
- *
106
- * h3 v2 idiomatic APIs used:
107
- * - defineHandler (replaces defineEventHandler / eventHandler)
108
- * - event.path (replaces event.node.req.url)
109
- * - event.method (replaces event.node.req.method)
110
- * - proxyRequest is retained internally because it preserves Nitro route
111
- * matching for event-bound server requests during SSR/prerender
112
- * - Object.fromEntries(event.req.headers.entries()) replaces direct event.node.req.headers access
113
- *
114
- * `fetchWithEvent` keeps the active event context while forwarding to a
115
- * rewritten path, which avoids falling through to the HTML renderer when
116
- * SSR code makes relative API requests.
117
- */
118
- var apiMiddleware = `
119
- import { defineHandler, fetchWithEvent, proxyRequest } from 'nitro/h3';
120
- import { useRuntimeConfig } from 'nitro/runtime-config';
121
-
122
- export default defineHandler(async (event) => {
123
- const prefix = useRuntimeConfig().prefix;
124
- const apiPrefix = \`\${prefix}/\${useRuntimeConfig().apiPrefix}\`;
125
-
126
- if (event.path?.startsWith(apiPrefix)) {
127
- const reqUrl = event.path?.replace(apiPrefix, '');
128
-
129
- if (
130
- event.method === 'GET' &&
131
- // in the case of XML routes, we want to proxy the request so that nitro gets the correct headers
132
- // and can render the XML correctly as a static asset
133
- !event.path?.endsWith('.xml')
134
- ) {
135
- return fetchWithEvent(event, reqUrl, {
136
- headers: Object.fromEntries(event.req.headers.entries()),
137
- });
138
- }
139
-
140
- return proxyRequest(event, reqUrl);
141
- }
142
- });`;
143
96
  //#endregion
144
- export { SERVER_FETCH_FACTORY_SNIPPET, apiMiddleware, clientRenderer, ssrRenderer };
97
+ export { SERVER_FETCH_FACTORY_SNIPPET, clientRenderer, ssrRenderer };
145
98
 
146
99
  //# sourceMappingURL=renderers.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"renderers.js","names":[],"sources":["../../../../../../packages/vite-plugin-nitro/src/lib/utils/renderers.ts"],"sourcesContent":["/**\n * Code snippet emitted into virtual modules to create a request-scoped\n * fetch using ofetch's `createFetch` + h3's `fetchWithEvent`.\n *\n * Shared between the SSR renderer and page-endpoint virtual modules so\n * the fetch-wiring logic stays in sync.\n *\n * The emitted variable is named `serverFetch` — callers should reference it\n * by that name.\n */\nexport const SERVER_FETCH_FACTORY_SNIPPET = `\n const serverFetch = createFetch({\n fetch: (resource, init) => {\n const url = resource instanceof Request ? resource.url : resource.toString();\n return fetchWithEvent(event, url, init);\n }\n });`;\n\n/**\n * SSR renderer virtual module content.\n *\n * This code runs inside Nitro's server runtime (Node.js context) where\n * event.node is always populated. In h3 v2, event.node is typed as optional,\n * so we use h3's first-class event properties (event.path, event.method) where\n * possible and apply optional chaining when accessing the Node.js context for\n * the Angular renderer which requires raw req/res objects.\n *\n * h3 v2 idiomatic APIs used:\n * - defineHandler (replaces defineEventHandler / eventHandler)\n * - event.path (replaces event.node.req.url)\n * - getResponseHeader compat shim (still available in h3 v2)\n */\nexport function ssrRenderer(templatePath: string) {\n return `\nimport { readFileSync } from 'node:fs';\nimport { createFetch } from 'ofetch';\nimport { defineHandler, fetchWithEvent } from 'nitro/h3';\n// @ts-ignore\nimport renderer from '#analog/ssr';\n\nconst template = readFileSync(${JSON.stringify(templatePath)}, 'utf8');\nconst normalizeHtmlRequestUrl = (url) =>\n url.replace(/\\\\/index\\\\.html(?=$|[?#])/, '/');\n\nexport default defineHandler(async (event) => {\n event.res.headers.set('content-type', 'text/html; charset=utf-8');\n const noSSR = event.res.headers.get('x-analog-no-ssr');\n const requestPath = normalizeHtmlRequestUrl(event.path);\n\n if (noSSR === 'true') {\n return template;\n }\n\n // event.path is the canonical h3 v2 way to access the request URL.\n // event.node?.req and event.node?.res are needed by the Angular SSR renderer\n // which operates on raw Node.js request/response objects.\n // During prerendering (Nitro v3 fetch-based pipeline), event.node is undefined.\n // The Angular renderer requires a req object with at least { headers, url },\n // so we provide a minimal stub to avoid runtime errors in prerender context.\n const req = event.node?.req\n ? {\n ...event.node.req,\n url: requestPath,\n originalUrl: requestPath,\n }\n : {\n headers: { host: 'localhost' },\n url: requestPath,\n originalUrl: requestPath,\n connection: {},\n };\n const res = event.node?.res;\n${SERVER_FETCH_FACTORY_SNIPPET}\n\n const html = await renderer(requestPath, template, { req, res, fetch: serverFetch });\n\n return html;\n});`;\n}\n\n/**\n * Client-only renderer virtual module content.\n *\n * Used when SSR is disabled — simply serves the static index.html template\n * for every route, letting the client-side Angular router handle navigation.\n */\nexport function clientRenderer(templatePath: string) {\n return `\nimport { readFileSync } from 'node:fs';\nimport { defineHandler } from 'nitro/h3';\n\nconst template = readFileSync(${JSON.stringify(templatePath)}, 'utf8');\n\nexport default defineHandler(async (event) => {\n event.res.headers.set('content-type', 'text/html; charset=utf-8');\n return template;\n});\n`;\n}\n\n/**\n * API middleware virtual module content.\n *\n * Intercepts requests matching the configured API prefix and either:\n * - Uses event-bound internal forwarding for GET requests (except .xml routes)\n * - Uses request proxying for all other methods to forward the full request\n *\n * h3 v2 idiomatic APIs used:\n * - defineHandler (replaces defineEventHandler / eventHandler)\n * - event.path (replaces event.node.req.url)\n * - event.method (replaces event.node.req.method)\n * - proxyRequest is retained internally because it preserves Nitro route\n * matching for event-bound server requests during SSR/prerender\n * - Object.fromEntries(event.req.headers.entries()) replaces direct event.node.req.headers access\n *\n * `fetchWithEvent` keeps the active event context while forwarding to a\n * rewritten path, which avoids falling through to the HTML renderer when\n * SSR code makes relative API requests.\n */\nexport const apiMiddleware = `\nimport { defineHandler, fetchWithEvent, proxyRequest } from 'nitro/h3';\nimport { useRuntimeConfig } from 'nitro/runtime-config';\n\nexport default defineHandler(async (event) => {\n const prefix = useRuntimeConfig().prefix;\n const apiPrefix = \\`\\${prefix}/\\${useRuntimeConfig().apiPrefix}\\`;\n\n if (event.path?.startsWith(apiPrefix)) {\n const reqUrl = event.path?.replace(apiPrefix, '');\n\n if (\n event.method === 'GET' &&\n // in the case of XML routes, we want to proxy the request so that nitro gets the correct headers\n // and can render the XML correctly as a static asset\n !event.path?.endsWith('.xml')\n ) {\n return fetchWithEvent(event, reqUrl, {\n headers: Object.fromEntries(event.req.headers.entries()),\n });\n }\n\n return proxyRequest(event, reqUrl);\n }\n});`;\n"],"mappings":";;;;;;;;;;;AAUA,IAAa,+BAA+B;;;;;;;;;;;;;;;;;;;;;AAsB5C,SAAgB,YAAY,cAAsB;AAChD,QAAO;;;;;;;gCAOuB,KAAK,UAAU,aAAa,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAgC3D,6BAA6B;;;;;;;;;;;;;AAc/B,SAAgB,eAAe,cAAsB;AACnD,QAAO;;;;gCAIuB,KAAK,UAAU,aAAa,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4B7D,IAAa,gBAAgB"}
1
+ {"version":3,"file":"renderers.js","names":[],"sources":["../../../../src/lib/utils/renderers.ts"],"sourcesContent":["/**\n * Code snippet emitted into virtual modules to create a request-scoped\n * fetch using ofetch's `createFetch` + h3's `fetchWithEvent`.\n *\n * Shared between the SSR renderer and page-endpoint virtual modules so\n * the fetch-wiring logic stays in sync.\n *\n * The emitted variable is named `serverFetch` — callers should reference it\n * by that name.\n */\nexport const SERVER_FETCH_FACTORY_SNIPPET = `\n const serverFetch = createFetch({\n fetch: (resource, init) => {\n const url = resource instanceof Request ? resource.url : resource.toString();\n return fetchWithEvent(event, url, init);\n }\n });`;\n\n/**\n * SSR renderer virtual module content.\n *\n * This code runs inside Nitro's server runtime (Node.js context) where\n * event.node is always populated. In h3 v2, event.node is typed as optional,\n * so we use h3's first-class event properties (event.path, event.method) where\n * possible and apply optional chaining when accessing the Node.js context for\n * the Angular renderer which requires raw req/res objects.\n *\n * h3 v2 idiomatic APIs used:\n * - defineHandler (replaces defineEventHandler / eventHandler)\n * - event.path (replaces event.node.req.url)\n * - getResponseHeader compat shim (still available in h3 v2)\n */\nexport function ssrRenderer() {\n return `\nimport { createFetch } from 'ofetch';\nimport { defineHandler, fetchWithEvent } from 'nitro/h3';\n// @ts-ignore\nimport renderer from '#analog/ssr';\nimport template from '#analog/index';\n\nconst normalizeHtmlRequestUrl = (url) =>\n url.replace(/\\\\/index\\\\.html(?=$|[?#])/, '/');\n\nexport default defineHandler(async (event) => {\n event.res.headers.set('content-type', 'text/html; charset=utf-8');\n const noSSR = event.res.headers.get('x-analog-no-ssr');\n const requestPath = normalizeHtmlRequestUrl(event.path);\n\n if (noSSR === 'true') {\n return template;\n }\n\n // event.path is the canonical h3 v2 way to access the request URL.\n // event.node?.req and event.node?.res are needed by the Angular SSR renderer\n // which operates on raw Node.js request/response objects.\n // During prerendering (Nitro v3 fetch-based pipeline), event.node is undefined.\n // The Angular renderer requires a req object with at least { headers, url },\n // so we provide a minimal stub to avoid runtime errors in prerender context.\n const req = event.node?.req\n ? {\n ...event.node.req,\n url: requestPath,\n originalUrl: requestPath,\n }\n : {\n headers: { host: 'localhost' },\n url: requestPath,\n originalUrl: requestPath,\n connection: {},\n };\n const res = event.node?.res;\n${SERVER_FETCH_FACTORY_SNIPPET}\n\n const html = await renderer(requestPath, template, { req, res, fetch: serverFetch });\n\n return html;\n});`;\n}\n\n/**\n * Client-only renderer virtual module content.\n *\n * Used when SSR is disabled — simply serves the static index.html template\n * for every route, letting the client-side Angular router handle navigation.\n */\nexport function clientRenderer() {\n return `\nimport { defineHandler } from 'nitro/h3';\nimport template from '#analog/index';\n\nexport default defineHandler(async (event) => {\n event.res.headers.set('content-type', 'text/html; charset=utf-8');\n return template;\n});\n`;\n}\n"],"mappings":";;;;;;;;;;;AAUA,IAAa,+BAA+B;;;;;;;;;;;;;;;;;;;;;AAsB5C,SAAgB,cAAc;AAC5B,QAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAsCP,6BAA6B;;;;;;;;;;;;;AAc/B,SAAgB,iBAAiB;AAC/B,QAAO"}
@@ -1,2 +1,2 @@
1
1
  export declare function isRolldown(): boolean;
2
- export declare function getBundleOptionsKey(): 'rolldownOptions' | 'rollupOptions';
2
+ export declare function getBundleOptionsKey(): "rolldownOptions" | "rollupOptions";
@@ -1 +1 @@
1
- {"version":3,"file":"rolldown.js","names":[],"sources":["../../../../../../packages/vite-plugin-nitro/src/lib/utils/rolldown.ts"],"sourcesContent":["import * as vite from 'vite';\n\nexport function isRolldown(): boolean {\n return !!vite.rolldownVersion;\n}\n\nexport function getBundleOptionsKey(): 'rolldownOptions' | 'rollupOptions' {\n return isRolldown() ? 'rolldownOptions' : 'rollupOptions';\n}\n"],"mappings":";;AAEA,SAAgB,aAAsB;AACpC,QAAO,CAAC,CAAC,KAAK;;AAGhB,SAAgB,sBAA2D;AACzE,QAAO,YAAY,GAAG,oBAAoB"}
1
+ {"version":3,"file":"rolldown.js","names":[],"sources":["../../../../src/lib/utils/rolldown.ts"],"sourcesContent":["import * as vite from 'vite';\n\nexport function isRolldown(): boolean {\n return !!vite.rolldownVersion;\n}\n\nexport function getBundleOptionsKey(): 'rolldownOptions' | 'rollupOptions' {\n return isRolldown() ? 'rolldownOptions' : 'rollupOptions';\n}\n"],"mappings":";;AAEA,SAAgB,aAAsB;AACpC,QAAO,CAAC,CAAC,KAAK;;AAGhB,SAAgB,sBAA2D;AACzE,QAAO,YAAY,GAAG,oBAAoB"}
@@ -0,0 +1,41 @@
1
+ import type { NitroEventHandler } from "nitropack";
2
+ import type { ServerFnHandlerModule } from "./get-server-fn-handlers";
3
+ /**
4
+ * Nitro virtual-module id for the generated server-function dispatch handler.
5
+ * Referenced from `nitroConfig.handlers` and provided via `nitroConfig.virtual`.
6
+ */
7
+ export declare const SERVER_FN_DISPATCH_VIRTUAL = "#ANALOG_SERVER_FN_DISPATCH";
8
+ /** The single transport route all server functions dispatch through. */
9
+ export declare const SERVER_FN_DISPATCH_ROUTE = "/_analog/fn/:id";
10
+ /** URL prefix of that route, for matching requests before the id is known. */
11
+ export declare const SERVER_FN_DISPATCH_PREFIX = "/_analog/fn/";
12
+ /**
13
+ * The Nitro handler registration for the server-function dispatch route.
14
+ * Unlike page endpoints (one handler per file), every server function shares
15
+ * this one `/_analog/fn/:id` route; the id selects the function at runtime.
16
+ *
17
+ * The route is fixed and NOT `/api`-prefixed: client proxies always call the
18
+ * absolute `/_analog/fn/:id` URL, so an `/api` prefix (as page endpoints use)
19
+ * would leave the handler unreachable in apps that have an API directory.
20
+ */
21
+ export declare function getServerFnDispatchHandler(): NitroEventHandler;
22
+ export type BuildServerFnDispatchModuleArgs = {
23
+ /** Discovered `*.server.ts` modules, imported for registration side-effects. */
24
+ modules: ServerFnHandlerModule[];
25
+ /**
26
+ * Absolute path to the app's server config module (`app.config.server.ts`),
27
+ * which exports the `ApplicationConfig` (as `config`) that `main.server.ts`
28
+ * renders with. Handlers bootstrap against it, so they see the same DI the
29
+ * app configured. When absent, handlers run with only `providedIn: 'root'`.
30
+ */
31
+ appConfigModule?: string;
32
+ };
33
+ /**
34
+ * Generates the source of the Nitro dispatch handler.
35
+ *
36
+ * The handler imports every discovered `*.server.ts` module so each
37
+ * `serverFn(...)` registers itself, imports the app's server config, then on
38
+ * each request looks up the function by id and runs it via `dispatchServerFn` —
39
+ * the same call path the validation harnesses exercise.
40
+ */
41
+ export declare function buildServerFnDispatchModule({ modules, appConfigModule }: BuildServerFnDispatchModuleArgs): string;
@@ -0,0 +1,57 @@
1
+ import { normalizePath } from "vite";
2
+ //#region packages/vite-plugin-nitro/src/lib/utils/server-fn-endpoints.ts
3
+ /**
4
+ * Nitro virtual-module id for the generated server-function dispatch handler.
5
+ * Referenced from `nitroConfig.handlers` and provided via `nitroConfig.virtual`.
6
+ */
7
+ var SERVER_FN_DISPATCH_VIRTUAL = "#ANALOG_SERVER_FN_DISPATCH";
8
+ /** The single transport route all server functions dispatch through. */
9
+ var SERVER_FN_DISPATCH_ROUTE = "/_analog/fn/:id";
10
+ /**
11
+ * The Nitro handler registration for the server-function dispatch route.
12
+ * Unlike page endpoints (one handler per file), every server function shares
13
+ * this one `/_analog/fn/:id` route; the id selects the function at runtime.
14
+ *
15
+ * The route is fixed and NOT `/api`-prefixed: client proxies always call the
16
+ * absolute `/_analog/fn/:id` URL, so an `/api` prefix (as page endpoints use)
17
+ * would leave the handler unreachable in apps that have an API directory.
18
+ */
19
+ function getServerFnDispatchHandler() {
20
+ return {
21
+ route: SERVER_FN_DISPATCH_ROUTE,
22
+ handler: SERVER_FN_DISPATCH_VIRTUAL,
23
+ lazy: true
24
+ };
25
+ }
26
+ /**
27
+ * Generates the source of the Nitro dispatch handler.
28
+ *
29
+ * The handler imports every discovered `*.server.ts` module so each
30
+ * `serverFn(...)` registers itself, imports the app's server config, then on
31
+ * each request looks up the function by id and runs it via `dispatchServerFn` —
32
+ * the same call path the validation harnesses exercise.
33
+ */
34
+ function buildServerFnDispatchModule({ modules, appConfigModule }) {
35
+ return `import '@angular/compiler';
36
+ import 'zone.js/node';
37
+ import '@angular/platform-server/init';
38
+ import { createServerFnAppInjector, createServerFnEventHandler } from '@analogjs/router/server';
39
+
40
+ // Discovered server-function modules (registration side-effects).
41
+ ${modules.map((m) => `import ${JSON.stringify(normalizePath(m.file))};`).join("\n")}
42
+
43
+ ${appConfigModule ? `import { config as serverFnAppConfig } from ${JSON.stringify(normalizePath(appConfigModule))};` : `const serverFnAppConfig = { providers: [] };`}
44
+
45
+ // Bootstrapped once from the app's own server config, so a handler resolves the
46
+ // same DI as an SSR render (\`providedIn: 'root'\` and listed providers alike).
47
+ // No component is bootstrapped, so nothing renders and the router never
48
+ // navigates. Only REQUEST/RESPONSE are rebuilt per call.
49
+ export default createServerFnEventHandler(
50
+ createServerFnAppInjector(serverFnAppConfig),
51
+ );
52
+ `;
53
+ }
54
+ //#endregion
55
+ export { SERVER_FN_DISPATCH_VIRTUAL, buildServerFnDispatchModule, getServerFnDispatchHandler };
56
+
57
+ //# sourceMappingURL=server-fn-endpoints.js.map