@analogjs/vite-plugin-nitro 3.0.0-alpha.64 → 3.0.0-alpha.65

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 (34) hide show
  1. package/package.json +6 -1
  2. package/src/index.d.ts +8 -0
  3. package/src/index.js +3 -1
  4. package/src/index.js.map +1 -1
  5. package/src/lib/build-sitemap.d.ts +1 -1
  6. package/src/lib/build-sitemap.js.map +1 -1
  7. package/src/lib/options.d.ts +4 -9
  8. package/src/lib/plugins/dev-server-plugin.js +1 -0
  9. package/src/lib/plugins/dev-server-plugin.js.map +1 -1
  10. package/src/lib/plugins/server-fn-id-plugin.d.ts +11 -0
  11. package/src/lib/plugins/server-fn-id-plugin.js +27 -0
  12. package/src/lib/plugins/server-fn-id-plugin.js.map +1 -0
  13. package/src/lib/utils/derive-server-fn-id.d.ts +25 -0
  14. package/src/lib/utils/derive-server-fn-id.js +37 -0
  15. package/src/lib/utils/derive-server-fn-id.js.map +1 -0
  16. package/src/lib/utils/get-page-handlers.d.ts +6 -9
  17. package/src/lib/utils/get-page-handlers.js +7 -10
  18. package/src/lib/utils/get-page-handlers.js.map +1 -1
  19. package/src/lib/utils/get-server-fn-handlers.d.ts +28 -0
  20. package/src/lib/utils/get-server-fn-handlers.js +45 -0
  21. package/src/lib/utils/get-server-fn-handlers.js.map +1 -0
  22. package/src/lib/utils/inject-server-fn-ids.d.ts +17 -0
  23. package/src/lib/utils/inject-server-fn-ids.js +94 -0
  24. package/src/lib/utils/inject-server-fn-ids.js.map +1 -0
  25. package/src/lib/utils/register-i18n-watcher.js +27 -0
  26. package/src/lib/utils/register-i18n-watcher.js.map +1 -0
  27. package/src/lib/utils/renderers.d.ts +0 -20
  28. package/src/lib/utils/renderers.js +1 -61
  29. package/src/lib/utils/renderers.js.map +1 -1
  30. package/src/lib/utils/server-fn-endpoints.d.ts +41 -0
  31. package/src/lib/utils/server-fn-endpoints.js +57 -0
  32. package/src/lib/utils/server-fn-endpoints.js.map +1 -0
  33. package/src/lib/vite-plugin-nitro.js +33 -31
  34. 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":"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"}
@@ -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"}
@@ -31,23 +31,3 @@ export declare function ssrRenderer(): string;
31
31
  * for every route, letting the client-side Angular router handle navigation.
32
32
  */
33
33
  export declare function clientRenderer(): 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 { createError, 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 const path = event.path || '';\n\n // only match the prefix on a path boundary, otherwise a URL such as\n // '/apihttp://internal-host' would pass startsWith() and be forwarded verbatim\n if (\n path === apiPrefix ||\n path.startsWith(`${apiPrefix}/`) ||\n path.startsWith(`${apiPrefix}?`)\n ) {\n let reqUrl = path.slice(apiPrefix.length);\n if (reqUrl === '' || reqUrl.startsWith('?')) {\n reqUrl = `/${reqUrl}`;\n }\n\n // reject absolute and protocol-relative targets to prevent SSRF\n if (!reqUrl.startsWith('/') || reqUrl.startsWith('//')) {\n throw createError({ statusCode: 400, statusMessage: 'Invalid API route' });\n }\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});";
@@ -93,67 +93,7 @@ export default defineHandler(async (event) => {
93
93
  });
94
94
  `;
95
95
  }
96
- /**
97
- * API middleware virtual module content.
98
- *
99
- * Intercepts requests matching the configured API prefix and either:
100
- * - Uses event-bound internal forwarding for GET requests (except .xml routes)
101
- * - Uses request proxying for all other methods to forward the full request
102
- *
103
- * h3 v2 idiomatic APIs used:
104
- * - defineHandler (replaces defineEventHandler / eventHandler)
105
- * - event.path (replaces event.node.req.url)
106
- * - event.method (replaces event.node.req.method)
107
- * - proxyRequest is retained internally because it preserves Nitro route
108
- * matching for event-bound server requests during SSR/prerender
109
- * - Object.fromEntries(event.req.headers.entries()) replaces direct event.node.req.headers access
110
- *
111
- * `fetchWithEvent` keeps the active event context while forwarding to a
112
- * rewritten path, which avoids falling through to the HTML renderer when
113
- * SSR code makes relative API requests.
114
- */
115
- var apiMiddleware = `
116
- import { createError, defineHandler, fetchWithEvent, proxyRequest } from 'nitro/h3';
117
- import { useRuntimeConfig } from 'nitro/runtime-config';
118
-
119
- export default defineHandler(async (event) => {
120
- const prefix = useRuntimeConfig().prefix;
121
- const apiPrefix = \`\${prefix}/\${useRuntimeConfig().apiPrefix}\`;
122
-
123
- const path = event.path || '';
124
-
125
- // only match the prefix on a path boundary, otherwise a URL such as
126
- // '/apihttp://internal-host' would pass startsWith() and be forwarded verbatim
127
- if (
128
- path === apiPrefix ||
129
- path.startsWith(\`\${apiPrefix}/\`) ||
130
- path.startsWith(\`\${apiPrefix}?\`)
131
- ) {
132
- let reqUrl = path.slice(apiPrefix.length);
133
- if (reqUrl === '' || reqUrl.startsWith('?')) {
134
- reqUrl = \`/\${reqUrl}\`;
135
- }
136
-
137
- // reject absolute and protocol-relative targets to prevent SSRF
138
- if (!reqUrl.startsWith('/') || reqUrl.startsWith('//')) {
139
- throw createError({ statusCode: 400, statusMessage: 'Invalid API route' });
140
- }
141
-
142
- if (
143
- event.method === 'GET' &&
144
- // in the case of XML routes, we want to proxy the request so that nitro gets the correct headers
145
- // and can render the XML correctly as a static asset
146
- !event.path?.endsWith('.xml')
147
- ) {
148
- return fetchWithEvent(event, reqUrl, {
149
- headers: Object.fromEntries(event.req.headers.entries()),
150
- });
151
- }
152
-
153
- return proxyRequest(event, reqUrl);
154
- }
155
- });`;
156
96
  //#endregion
157
- export { SERVER_FETCH_FACTORY_SNIPPET, apiMiddleware, clientRenderer, ssrRenderer };
97
+ export { SERVER_FETCH_FACTORY_SNIPPET, clientRenderer, ssrRenderer };
158
98
 
159
99
  //# sourceMappingURL=renderers.js.map
@@ -1 +1 @@
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\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 { createError, 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 const path = event.path || '';\n\n // only match the prefix on a path boundary, otherwise a URL such as\n // '/apihttp://internal-host' would pass startsWith() and be forwarded verbatim\n if (\n path === apiPrefix ||\n path.startsWith(\\`\\${apiPrefix}/\\`) ||\n path.startsWith(\\`\\${apiPrefix}?\\`)\n ) {\n let reqUrl = path.slice(apiPrefix.length);\n if (reqUrl === '' || reqUrl.startsWith('?')) {\n reqUrl = \\`/\\${reqUrl}\\`;\n }\n\n // reject absolute and protocol-relative targets to prevent SSRF\n if (!reqUrl.startsWith('/') || reqUrl.startsWith('//')) {\n throw createError({ statusCode: 400, statusMessage: 'Invalid API route' });\n }\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,cAAc;AAC5B,QAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAsCP,6BAA6B;;;;;;;;;;;;;AAc/B,SAAgB,iBAAiB;AAC/B,QAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BT,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"}
@@ -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 { buildServerFnDispatchModule, getServerFnDispatchHandler };
56
+
57
+ //# sourceMappingURL=server-fn-endpoints.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server-fn-endpoints.js","names":[],"sources":["../../../../src/lib/utils/server-fn-endpoints.ts"],"sourcesContent":["import type { NitroEventHandler } from 'nitropack';\nimport { normalizePath } from 'vite';\n\nimport type { ServerFnHandlerModule } from './get-server-fn-handlers';\n\n/**\n * Nitro virtual-module id for the generated server-function dispatch handler.\n * Referenced from `nitroConfig.handlers` and provided via `nitroConfig.virtual`.\n */\nexport const SERVER_FN_DISPATCH_VIRTUAL = '#ANALOG_SERVER_FN_DISPATCH';\n\n/** The single transport route all server functions dispatch through. */\nexport const SERVER_FN_DISPATCH_ROUTE = '/_analog/fn/:id';\n\n/** URL prefix of that route, for matching requests before the id is known. */\nexport const SERVER_FN_DISPATCH_PREFIX = '/_analog/fn/';\n\n/**\n * The Nitro handler registration for the server-function dispatch route.\n * Unlike page endpoints (one handler per file), every server function shares\n * this one `/_analog/fn/:id` route; the id selects the function at runtime.\n *\n * The route is fixed and NOT `/api`-prefixed: client proxies always call the\n * absolute `/_analog/fn/:id` URL, so an `/api` prefix (as page endpoints use)\n * would leave the handler unreachable in apps that have an API directory.\n */\nexport function getServerFnDispatchHandler(): NitroEventHandler {\n return {\n route: SERVER_FN_DISPATCH_ROUTE,\n handler: SERVER_FN_DISPATCH_VIRTUAL,\n lazy: true,\n };\n}\n\nexport type BuildServerFnDispatchModuleArgs = {\n /** Discovered `*.server.ts` modules, imported for registration side-effects. */\n modules: ServerFnHandlerModule[];\n /**\n * Absolute path to the app's server config module (`app.config.server.ts`),\n * which exports the `ApplicationConfig` (as `config`) that `main.server.ts`\n * renders with. Handlers bootstrap against it, so they see the same DI the\n * app configured. When absent, handlers run with only `providedIn: 'root'`.\n */\n appConfigModule?: string;\n};\n\n/**\n * Generates the source of the Nitro dispatch handler.\n *\n * The handler imports every discovered `*.server.ts` module so each\n * `serverFn(...)` registers itself, imports the app's server config, then on\n * each request looks up the function by id and runs it via `dispatchServerFn` —\n * the same call path the validation harnesses exercise.\n */\nexport function buildServerFnDispatchModule({\n modules,\n appConfigModule,\n}: BuildServerFnDispatchModuleArgs): string {\n const registrationImports = modules\n .map((m) => `import ${JSON.stringify(normalizePath(m.file))};`)\n .join('\\n');\n\n // Bootstrap against the app's own server config so a handler resolves exactly\n // the services, tokens, and interceptors the app configured — one config, no\n // second provider list. Fall back to an empty config when the app has none.\n const appConfig = appConfigModule\n ? `import { config as serverFnAppConfig } from ${JSON.stringify(\n normalizePath(appConfigModule),\n )};`\n : `const serverFnAppConfig = { providers: [] };`;\n\n // `@analogjs/router/server` is a partially-compiled Angular library, and this\n // module is bundled by Nitro rather than by the app's Angular pipeline, so the\n // linker never runs over it. Loading the compiler gives it the JIT fallback.\n // `zone.js` and the server platform init match the SSR entry, so bootstrapping\n // the app injector below runs in the same environment a render would.\n //\n // The transport itself — id decoding, the malformed-body contract, dispatch,\n // and response writing — lives in `createServerFnEventHandler`, so this module\n // is only wiring: bootstrap the app injector from the app's server config, and\n // hand it to that handler.\n return `import '@angular/compiler';\nimport 'zone.js/node';\nimport '@angular/platform-server/init';\nimport { createServerFnAppInjector, createServerFnEventHandler } from '@analogjs/router/server';\n\n// Discovered server-function modules (registration side-effects).\n${registrationImports}\n\n${appConfig}\n\n// Bootstrapped once from the app's own server config, so a handler resolves the\n// same DI as an SSR render (\\`providedIn: 'root'\\` and listed providers alike).\n// No component is bootstrapped, so nothing renders and the router never\n// navigates. Only REQUEST/RESPONSE are rebuilt per call.\nexport default createServerFnEventHandler(\n createServerFnAppInjector(serverFnAppConfig),\n);\n`;\n}\n"],"mappings":";;;;;;AASA,IAAa,6BAA6B;;AAG1C,IAAa,2BAA2B;;;;;;;;;;AAcxC,SAAgB,6BAAgD;AAC9D,QAAO;EACL,OAAO;EACP,SAAS;EACT,MAAM;EACP;;;;;;;;;;AAuBH,SAAgB,4BAA4B,EAC1C,SACA,mBAC0C;AAwB1C,QAAO;;;;;;EAvBqB,QACzB,KAAK,MAAM,UAAU,KAAK,UAAU,cAAc,EAAE,KAAK,CAAC,CAAC,GAAG,CAC9D,KAAK,KAAK,CA2BO;;EAtBF,kBACd,+CAA+C,KAAK,UAClD,cAAc,gBAAgB,CAC/B,CAAC,KACF,+CAoBM"}
@@ -1,9 +1,12 @@
1
1
  import { buildServer, isVercelPreset } from "./build-server.js";
2
2
  import { getBundleOptionsKey, isRolldown } from "./utils/rolldown.js";
3
3
  import { buildClientApp, buildSSRApp } from "./build-ssr.js";
4
- import { apiMiddleware, clientRenderer, ssrRenderer } from "./utils/renderers.js";
4
+ import { clientRenderer, ssrRenderer } from "./utils/renderers.js";
5
5
  import { pageEndpointsPlugin } from "./plugins/page-endpoints.js";
6
+ import { serverFnIdPlugin } from "./plugins/server-fn-id-plugin.js";
6
7
  import { getPageHandlers } from "./utils/get-page-handlers.js";
8
+ import { getServerFnHandlers } from "./utils/get-server-fn-handlers.js";
9
+ import { buildServerFnDispatchModule, getServerFnDispatchHandler } from "./utils/server-fn-endpoints.js";
7
10
  import { buildSitemap } from "./build-sitemap.js";
8
11
  import { toWebRequest, writeWebResponseToNode } from "./utils/node-web-bridge.js";
9
12
  import { devServerPlugin } from "./plugins/dev-server-plugin.js";
@@ -59,13 +62,6 @@ function cloneUserConfig(userConfig) {
59
62
  }
60
63
  };
61
64
  }
62
- function createNitroMiddlewareHandler(handler) {
63
- return {
64
- route: "/**",
65
- handler,
66
- middleware: true
67
- };
68
- }
69
65
  /**
70
66
  * Creates a `rollup:before` hook that marks specified packages as external
71
67
  * in Nitro's bundler config (applied to both the server build and the
@@ -319,7 +315,6 @@ function nitro(options, nitroOptions) {
319
315
  const baseURL = process.env["NITRO_APP_BASE_URL"] || "";
320
316
  const prefix = baseURL ? baseURL.substring(0, baseURL.length - 1) : "";
321
317
  const apiPrefix = `/${options?.apiPrefix || "api"}`;
322
- const useAPIMiddleware = typeof options?.useAPIMiddleware !== "undefined" ? options?.useAPIMiddleware : true;
323
318
  const viteRolldownOutput = options?.vite?.build?.rolldownOptions?.output;
324
319
  const viteRolldownOutputConfig = viteRolldownOutput && !Array.isArray(viteRolldownOutput) ? viteRolldownOutput : void 0;
325
320
  const codeSplitting = viteRolldownOutputConfig?.codeSplitting;
@@ -329,7 +324,7 @@ function nitro(options, nitroOptions) {
329
324
  let config;
330
325
  let nitroConfig;
331
326
  let environmentBuild = false;
332
- let hasAPIDir = false;
327
+ let hasServerFns = false;
333
328
  let clientOutputPath = "";
334
329
  let clientIndexHtml;
335
330
  let legacyClientSubBuild = false;
@@ -360,14 +355,12 @@ function nitro(options, nitroOptions) {
360
355
  for (const key of Object.keys(routeSourceFiles)) delete routeSourceFiles[key];
361
356
  const resolvedConfigRoot = config.root ? resolve(workspaceRoot, config.root) : workspaceRoot;
362
357
  rootDir = relative(workspaceRoot, resolvedConfigRoot) || ".";
363
- hasAPIDir = existsSync(resolve(workspaceRoot, rootDir, `${sourceRoot}/server/routes/${options?.apiPrefix || "api"}`));
364
358
  const buildPreset = process.env["BUILD_PRESET"] ?? nitroOptions?.preset ?? (process.env["VERCEL"] ? "vercel" : void 0);
365
359
  const pageHandlers = getPageHandlers({
366
360
  workspaceRoot,
367
361
  sourceRoot,
368
362
  rootDir,
369
- additionalPagesDirs: options?.additionalPagesDirs,
370
- hasAPIDir
363
+ additionalPagesDirs: options?.additionalPagesDirs
371
364
  });
372
365
  const resolvedClientOutputPath = resolveClientOutputPath(clientOutputPath, workspaceRoot, rootDir, config.build?.outDir);
373
366
  debugNitro("nitro config resolved client output path", {
@@ -382,6 +375,19 @@ function nitro(options, nitroOptions) {
382
375
  hasEnvironmentConfig: !!config.environments,
383
376
  clientEnvironmentOutDir: config.environments?.["client"] && typeof config.environments["client"] === "object" && "build" in config.environments["client"] ? config.environments["client"].build?.outDir : void 0
384
377
  });
378
+ const serverFnModules = getServerFnHandlers({
379
+ workspaceRoot,
380
+ sourceRoot,
381
+ rootDir,
382
+ additionalServerFnDirs: options?.additionalServerFnDirs
383
+ });
384
+ const serverFnAppConfigModule = resolveServerFnAppConfigModule(workspaceRoot, rootDir, sourceRoot);
385
+ hasServerFns = serverFnModules.length > 0;
386
+ hasServerFns && getServerFnDispatchHandler();
387
+ serverFnModules.length > 0 && buildServerFnDispatchModule({
388
+ modules: serverFnModules,
389
+ appConfigModule: serverFnAppConfigModule
390
+ });
385
391
  nitroConfig = {
386
392
  rootDir: normalizePath(rootDir),
387
393
  preset: buildPreset,
@@ -406,14 +412,13 @@ function nitro(options, nitroOptions) {
406
412
  onwarn(warning) {
407
413
  if (warning.message.includes("empty chunk") && warning.message.endsWith(".server")) return;
408
414
  },
409
- plugins: [pageEndpointsPlugin()]
415
+ plugins: [pageEndpointsPlugin(), serverFnIdPlugin(normalizePath(resolve(workspaceRoot, rootDir)))]
410
416
  },
411
- handlers: [...hasAPIDir ? [] : useAPIMiddleware ? [createNitroMiddlewareHandler("#ANALOG_API_MIDDLEWARE")] : [], ...pageHandlers],
412
- routeRules: hasAPIDir ? void 0 : useAPIMiddleware ? void 0 : { [`${prefix}${apiPrefix}/**`]: { proxy: { to: "/**" } } },
417
+ handlers: [...pageHandlers],
418
+ routeRules: void 0,
413
419
  virtual: {
414
420
  "#ANALOG_SSR_RENDERER": ssrRenderer(),
415
- "#ANALOG_CLIENT_RENDERER": clientRenderer(),
416
- ...hasAPIDir ? {} : { "#ANALOG_API_MIDDLEWARE": apiMiddleware }
421
+ "#ANALOG_CLIENT_RENDERER": clientRenderer()
417
422
  }
418
423
  };
419
424
  if (isVercelPreset(buildPreset)) nitroConfig = withVercelOutputAPI(nitroConfig, workspaceRoot);
@@ -493,15 +498,11 @@ function nitro(options, nitroOptions) {
493
498
  rollupExternalEntries.push("rxjs", "node-fetch-native/dist/polyfill", "sharp");
494
499
  nitroConfig = {
495
500
  ...nitroConfig,
496
- handlers: [
497
- ...hasAPIDir ? [] : useAPIMiddleware ? [createNitroMiddlewareHandler("#ANALOG_API_MIDDLEWARE")] : [],
498
- ...pageHandlers,
499
- {
500
- handler: rendererHandler,
501
- route: "/**",
502
- lazy: true
503
- }
504
- ]
501
+ handlers: [...pageHandlers, {
502
+ handler: rendererHandler,
503
+ route: "/**",
504
+ lazy: true
505
+ }]
505
506
  };
506
507
  }
507
508
  }
@@ -643,16 +644,13 @@ function nitro(options, nitroOptions) {
643
644
  const apiHandler = async (req, res) => {
644
645
  await writeWebResponseToNode(res, await server.fetch(toWebRequest(req)));
645
646
  };
646
- if (hasAPIDir) viteServer.middlewares.use((req, res, next) => {
647
+ viteServer.middlewares.use((req, res, next) => {
647
648
  if (req.url?.startsWith(`${prefix}${apiPrefix}`)) {
648
649
  apiHandler(req, res).catch((error) => next(error));
649
650
  return;
650
651
  }
651
652
  next();
652
653
  });
653
- else viteServer.middlewares.use(apiPrefix, (req, res, next) => {
654
- apiHandler(req, res).catch((error) => next(error));
655
- });
656
654
  viteServer.httpServer?.once("listening", () => {
657
655
  process.env["ANALOG_HOST"] = !viteServer.config.server.host ? "localhost" : viteServer.config.server.host;
658
656
  process.env["ANALOG_PORT"] = `${viteServer.config.server.port}`;
@@ -804,6 +802,10 @@ var withAppHostingOutput = (nitroConfig) => {
804
802
  }
805
803
  };
806
804
  };
805
+ function resolveServerFnAppConfigModule(workspaceRoot, rootDir, sourceRoot) {
806
+ const root = normalizePath(resolve(workspaceRoot, rootDir));
807
+ return [`${root}/${sourceRoot}/app/app.config.server.ts`, `${root}/${sourceRoot}/app.config.server.ts`].find((candidate) => existsSync(candidate));
808
+ }
807
809
  var isNetlifyPreset = (buildPreset) => process.env["NETLIFY"] || buildPreset && buildPreset.toLowerCase().includes("netlify");
808
810
  var withNetlifyOutputAPI = (nitroConfig, workspaceRoot) => ({
809
811
  ...nitroConfig,