@analogjs/vite-plugin-nitro 2.4.0-alpha.1 → 2.4.0-alpha.3

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.
@@ -1,58 +1,130 @@
1
- export const ssrRenderer = `
2
- import { eventHandler, getResponseHeader } from 'h3';
1
+ /**
2
+ * SSR renderer virtual module content.
3
+ *
4
+ * This code runs inside Nitro's server runtime (Node.js context) where
5
+ * event.node is always populated. In h3 v2, event.node is typed as optional,
6
+ * so we use h3's first-class event properties (event.path, event.method) where
7
+ * possible and apply optional chaining when accessing the Node.js context for
8
+ * the Angular renderer which requires raw req/res objects.
9
+ *
10
+ * h3 v2 idiomatic APIs used:
11
+ * - defineHandler (replaces defineEventHandler / eventHandler)
12
+ * - event.path (replaces event.node.req.url)
13
+ * - getResponseHeader compat shim (still available in h3 v2)
14
+ */
15
+ export function ssrRenderer(templatePath) {
16
+ return `
17
+ import { readFileSync } from 'node:fs';
18
+ import { createFetch } from 'ofetch';
19
+ import { defineHandler, fetchWithEvent } from 'h3';
3
20
  // @ts-ignore
4
21
  import renderer from '#analog/ssr';
5
- // @ts-ignore
6
- import template from '#analog/index';
7
22
 
8
- export default eventHandler(async (event) => {
9
- const noSSR = getResponseHeader(event, 'x-analog-no-ssr');
23
+ const template = readFileSync(${JSON.stringify(templatePath)}, 'utf8');
24
+ const normalizeHtmlRequestUrl = (url) =>
25
+ url.replace(/\\/index\\.html(?=$|[?#])/, '/');
26
+
27
+ export default defineHandler(async (event) => {
28
+ event.res.headers.set('content-type', 'text/html; charset=utf-8');
29
+ const noSSR = event.res.headers.get('x-analog-no-ssr');
30
+ const requestPath = normalizeHtmlRequestUrl(event.path);
10
31
 
11
32
  if (noSSR === 'true') {
12
33
  return template;
13
34
  }
14
35
 
15
- const html = await renderer(event.node.req.url, template, {
16
- req: event.node.req,
17
- res: event.node.res,
36
+ // event.path is the canonical h3 v2 way to access the request URL.
37
+ // event.node?.req and event.node?.res are needed by the Angular SSR renderer
38
+ // which operates on raw Node.js request/response objects.
39
+ // During prerendering (Nitro v3 fetch-based pipeline), event.node is undefined.
40
+ // The Angular renderer requires a req object with at least { headers, url },
41
+ // so we provide a minimal stub to avoid runtime errors in prerender context.
42
+ const req = event.node?.req
43
+ ? {
44
+ ...event.node.req,
45
+ url: requestPath,
46
+ originalUrl: requestPath,
47
+ }
48
+ : {
49
+ headers: { host: 'localhost' },
50
+ url: requestPath,
51
+ originalUrl: requestPath,
52
+ connection: {},
53
+ };
54
+ const res = event.node?.res;
55
+ const fetch = createFetch({
56
+ fetch: (resource, init) => {
57
+ const url = resource instanceof Request ? resource.url : resource.toString();
58
+ return fetchWithEvent(event, url, init);
59
+ }
18
60
  });
19
61
 
62
+ const html = await renderer(requestPath, template, { req, res, fetch });
63
+
20
64
  return html;
21
65
  });`;
22
- export const clientRenderer = `
23
- import { eventHandler } from 'h3';
66
+ }
67
+ /**
68
+ * Client-only renderer virtual module content.
69
+ *
70
+ * Used when SSR is disabled — simply serves the static index.html template
71
+ * for every route, letting the client-side Angular router handle navigation.
72
+ */
73
+ export function clientRenderer(templatePath) {
74
+ return `
75
+ import { readFileSync } from 'node:fs';
76
+ import { defineHandler } from 'h3';
24
77
 
25
- // @ts-ignore
26
- import template from '#analog/index';
78
+ const template = readFileSync(${JSON.stringify(templatePath)}, 'utf8');
27
79
 
28
- export default eventHandler(async () => {
80
+ export default defineHandler(async (event) => {
81
+ event.res.headers.set('content-type', 'text/html; charset=utf-8');
29
82
  return template;
30
83
  });
31
84
  `;
85
+ }
86
+ /**
87
+ * API middleware virtual module content.
88
+ *
89
+ * Intercepts requests matching the configured API prefix and either:
90
+ * - Uses event-bound internal forwarding for GET requests (except .xml routes)
91
+ * - Uses request proxying for all other methods to forward the full request
92
+ *
93
+ * h3 v2 idiomatic APIs used:
94
+ * - defineHandler (replaces defineEventHandler / eventHandler)
95
+ * - event.path (replaces event.node.req.url)
96
+ * - event.method (replaces event.node.req.method)
97
+ * - proxyRequest is retained internally because it preserves Nitro route
98
+ * matching for event-bound server requests during SSR/prerender
99
+ * - Object.fromEntries(event.req.headers.entries()) replaces direct event.node.req.headers access
100
+ *
101
+ * `fetchWithEvent` keeps the active event context while forwarding to a
102
+ * rewritten path, which avoids falling through to the HTML renderer when
103
+ * SSR code makes relative API requests.
104
+ */
32
105
  export const apiMiddleware = `
33
- import { eventHandler, proxyRequest } from 'h3';
34
- import { useRuntimeConfig } from '#imports';
106
+ import { defineHandler, fetchWithEvent, proxyRequest } from 'h3';
107
+ import { useRuntimeConfig } from 'nitro/runtime-config';
35
108
 
36
- export default eventHandler(async (event) => {
109
+ export default defineHandler(async (event) => {
37
110
  const prefix = useRuntimeConfig().prefix;
38
111
  const apiPrefix = \`\${prefix}/\${useRuntimeConfig().apiPrefix}\`;
39
112
 
40
- if (event.node.req.url?.startsWith(apiPrefix)) {
41
- const reqUrl = event.node.req.url?.replace(apiPrefix, '');
113
+ if (event.path?.startsWith(apiPrefix)) {
114
+ const reqUrl = event.path?.replace(apiPrefix, '');
42
115
 
43
116
  if (
44
- event.node.req.method === 'GET' &&
117
+ event.method === 'GET' &&
45
118
  // in the case of XML routes, we want to proxy the request so that nitro gets the correct headers
46
119
  // and can render the XML correctly as a static asset
47
- !event.node.req.url?.endsWith('.xml')
120
+ !event.path?.endsWith('.xml')
48
121
  ) {
49
- return $fetch(reqUrl, { headers: event.node.req.headers });
122
+ return fetchWithEvent(event, reqUrl, {
123
+ headers: Object.fromEntries(event.req.headers.entries()),
124
+ });
50
125
  }
51
126
 
52
- return proxyRequest(event, reqUrl, {
53
- // @ts-ignore
54
- fetch: $fetch.native,
55
- });
127
+ return proxyRequest(event, reqUrl);
56
128
  }
57
129
  });`;
58
130
  //# sourceMappingURL=renderers.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"renderers.js","sourceRoot":"","sources":["../../../../../../packages/vite-plugin-nitro/src/lib/utils/renderers.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,WAAW,GAAG;;;;;;;;;;;;;;;;;;;;IAoBvB,CAAC;AAEL,MAAM,CAAC,MAAM,cAAc,GAAG;;;;;;;;;CAS7B,CAAC;AAEF,MAAM,CAAC,MAAM,aAAa,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;IAyBzB,CAAC"}
1
+ {"version":3,"file":"renderers.js","sourceRoot":"","sources":["../../../../../../packages/vite-plugin-nitro/src/lib/utils/renderers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,WAAW,CAAC,YAAoB;IAC9C,OAAO;;;;;;;gCAOuB,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IA0CxD,CAAC;AACL,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,YAAoB;IACjD,OAAO;;;;gCAIuB,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC;;;;;;CAM3D,CAAC;AACF,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG;;;;;;;;;;;;;;;;;;;;;;;;IAwBzB,CAAC"}
@@ -1,4 +1,4 @@
1
- import { NitroConfig } from 'nitropack';
1
+ import type { NitroConfig } from 'nitro/types';
2
2
  import type { Plugin } from 'vite';
3
3
  import { Options } from './options.js';
4
4
  export declare function nitro(options?: Options, nitroOptions?: NitroConfig): Plugin[];
@@ -1,9 +1,8 @@
1
- import { build, createDevServer, createNitro } from 'nitropack';
2
- import { toNodeListener } from 'h3';
1
+ import { build, createDevServer, createNitro } from 'nitro/builder';
2
+ import * as vite from 'vite';
3
3
  import { mergeConfig, normalizePath } from 'vite';
4
- import { dirname, relative, resolve } from 'node:path';
5
- import { platform } from 'node:os';
6
- import { fileURLToPath, pathToFileURL } from 'node:url';
4
+ import { relative, resolve } from 'node:path';
5
+ import { pathToFileURL } from 'node:url';
7
6
  import { existsSync, readFileSync } from 'node:fs';
8
7
  import { buildServer } from './build-server.js';
9
8
  import { buildSSRApp } from './build-ssr.js';
@@ -11,13 +10,115 @@ import { pageEndpointsPlugin } from './plugins/page-endpoints.js';
11
10
  import { getPageHandlers } from './utils/get-page-handlers.js';
12
11
  import { buildSitemap } from './build-sitemap.js';
13
12
  import { devServerPlugin } from './plugins/dev-server-plugin.js';
13
+ import { toWebRequest, writeWebResponseToNode, } from './utils/node-web-bridge.js';
14
14
  import { getMatchingContentFilesWithFrontMatter } from './utils/get-content-files.js';
15
15
  import { ssrRenderer, clientRenderer, apiMiddleware, } from './utils/renderers.js';
16
- const isWindows = platform() === 'win32';
17
- const filePrefix = isWindows ? 'file:///' : '';
18
16
  let clientOutputPath = '';
19
- const __filename = fileURLToPath(import.meta.url);
20
- const __dirname = dirname(__filename);
17
+ let rendererIndexEntry = '';
18
+ function createNitroMiddlewareHandler(handler) {
19
+ return {
20
+ route: '/**',
21
+ handler,
22
+ middleware: true,
23
+ };
24
+ }
25
+ function createRollupBeforeHook(externalEntries) {
26
+ return (_nitro, bundlerConfig) => {
27
+ removeInvalidRollupCodeSplitting(_nitro, bundlerConfig);
28
+ if (externalEntries.length === 0) {
29
+ return;
30
+ }
31
+ const existing = bundlerConfig.external;
32
+ if (!existing) {
33
+ bundlerConfig.external = externalEntries;
34
+ }
35
+ else if (typeof existing === 'function') {
36
+ bundlerConfig.external = (source, importer, isResolved) => existing(source, importer, isResolved) ||
37
+ externalEntries.includes(source);
38
+ }
39
+ else if (Array.isArray(existing)) {
40
+ bundlerConfig.external = [...existing, ...externalEntries];
41
+ }
42
+ else {
43
+ bundlerConfig.external = [existing, ...externalEntries];
44
+ }
45
+ };
46
+ }
47
+ function appendNoExternals(noExternals, ...entries) {
48
+ if (!noExternals) {
49
+ return entries;
50
+ }
51
+ return Array.isArray(noExternals)
52
+ ? [...noExternals, ...entries]
53
+ : noExternals;
54
+ }
55
+ function removeInvalidRollupCodeSplitting(_nitro, bundlerConfig) {
56
+ // Workaround for a Nitro v3 alpha bundler bug:
57
+ //
58
+ // Analog does not add `output.codeSplitting` to Nitro's Rollup config, but
59
+ // Nitro 3.0.1-alpha.2 builds an internal server bundler config that can
60
+ // still contain that key while running under Vite 8 / Rolldown. At runtime
61
+ // this surfaces as:
62
+ //
63
+ // Warning: Invalid output options (1 issue found)
64
+ // - For the "codeSplitting". Invalid key: Expected never but received "codeSplitting".
65
+ //
66
+ // That warning comes from Nitro's own bundler handoff, not from user config
67
+ // in Analog apps. We remove only the invalid `output.codeSplitting` field
68
+ // right before Nitro starts prerender/server builds.
69
+ //
70
+ // Why this is safe:
71
+ // - Analog is not relying on Nitro-side `output.codeSplitting`.
72
+ // - The warning path only rejects the option; removing it restores the
73
+ // default Nitro/Rollup behavior instead of changing any Analog semantics.
74
+ // - The hook is narrowly scoped to the final Nitro bundler config, so it
75
+ // does not affect the normal Vite client/SSR environment build config.
76
+ const output = bundlerConfig['output'];
77
+ if (!output || Array.isArray(output) || typeof output !== 'object') {
78
+ return;
79
+ }
80
+ if ('codeSplitting' in output) {
81
+ delete output['codeSplitting'];
82
+ }
83
+ // Nitro's default server bundler config currently enables manual chunking for
84
+ // node_modules. Under Nitro v3 alpha + Rollup 4.59 this can crash during the
85
+ // prerender rebundle with "Cannot read properties of undefined (reading
86
+ // 'included')" while generating chunks. A single server bundle is acceptable
87
+ // here, so strip manualChunks until the upstream bug is fixed.
88
+ if ('manualChunks' in output) {
89
+ delete output['manualChunks'];
90
+ }
91
+ }
92
+ function resolveClientOutputPath(workspaceRoot, rootDir, configuredOutDir, ssrBuild) {
93
+ if (clientOutputPath) {
94
+ return clientOutputPath;
95
+ }
96
+ if (!ssrBuild) {
97
+ return resolve(workspaceRoot, rootDir, configuredOutDir || 'dist/client');
98
+ }
99
+ // SSR builds write server assets to dist/<app>/ssr, but the renderer template
100
+ // still needs the client index.html emitted to dist/<app>/client.
101
+ return resolve(workspaceRoot, 'dist', rootDir, 'client');
102
+ }
103
+ function toNitroSsrEntrypointSpecifier(ssrEntryPath) {
104
+ // Nitro rebundles the generated SSR entry. On Windows, a file URL preserves
105
+ // the importer location so relative "./assets/*" imports resolve correctly.
106
+ return process.platform === 'win32'
107
+ ? pathToFileURL(ssrEntryPath).href
108
+ : normalizePath(ssrEntryPath);
109
+ }
110
+ function resolveBuiltSsrEntryPath(ssrOutDir) {
111
+ const candidatePaths = [
112
+ resolve(ssrOutDir, 'main.server.mjs'),
113
+ resolve(ssrOutDir, 'main.server.js'),
114
+ resolve(ssrOutDir, 'main.server'),
115
+ ];
116
+ const ssrEntryPath = candidatePaths.find((candidatePath) => existsSync(candidatePath));
117
+ if (!ssrEntryPath) {
118
+ throw new Error(`Unable to locate the built SSR entry in "${ssrOutDir}". Expected one of: ${candidatePaths.join(', ')}`);
119
+ }
120
+ return ssrEntryPath;
121
+ }
21
122
  export function nitro(options, nitroOptions) {
22
123
  const workspaceRoot = options?.workspaceRoot ?? process.cwd();
23
124
  const sourceRoot = options?.sourceRoot ?? 'src';
@@ -35,6 +136,7 @@ export function nitro(options, nitroOptions) {
35
136
  let nitroConfig;
36
137
  let environmentBuild = false;
37
138
  let hasAPIDir = false;
139
+ const rollupExternalEntries = [];
38
140
  const routeSitemaps = {};
39
141
  const routeSourceFiles = {};
40
142
  let rootDir = workspaceRoot;
@@ -57,7 +159,8 @@ export function nitro(options, nitroOptions) {
57
159
  rootDir = relative(workspaceRoot, config.root || '.') || '.';
58
160
  hasAPIDir = existsSync(resolve(workspaceRoot, rootDir, `${sourceRoot}/server/routes/${options?.apiPrefix || 'api'}`));
59
161
  const buildPreset = process.env['BUILD_PRESET'] ??
60
- nitroOptions?.preset;
162
+ nitroOptions?.preset ??
163
+ (process.env['VERCEL'] ? 'vercel' : undefined);
61
164
  const pageHandlers = getPageHandlers({
62
165
  workspaceRoot,
63
166
  sourceRoot,
@@ -65,12 +168,14 @@ export function nitro(options, nitroOptions) {
65
168
  additionalPagesDirs: options?.additionalPagesDirs,
66
169
  hasAPIDir,
67
170
  });
171
+ const resolvedClientOutputPath = resolveClientOutputPath(workspaceRoot, rootDir, config.build?.outDir, ssrBuild);
172
+ rendererIndexEntry = normalizePath(resolve(resolvedClientOutputPath, 'index.html'));
68
173
  nitroConfig = {
69
- rootDir,
174
+ rootDir: normalizePath(rootDir),
70
175
  preset: buildPreset,
71
176
  compatibilityDate: '2024-11-19',
72
177
  logLevel: nitroOptions?.logLevel || 0,
73
- srcDir: normalizePath(`${sourceRoot}/server`),
178
+ serverDir: normalizePath(`${sourceRoot}/server`),
74
179
  scanDirs: [
75
180
  normalizePath(`${rootDir}/${sourceRoot}/server`),
76
181
  ...(options?.additionalAPIDirs || []).map((dir) => normalizePath(`${workspaceRoot}${dir}`)),
@@ -87,10 +192,15 @@ export function nitro(options, nitroOptions) {
87
192
  apiPrefix: apiPrefix.substring(1),
88
193
  prefix,
89
194
  },
90
- // Fixes support for Rolldown
195
+ // Analog provides its own renderer handler; prevent Nitro v3 from
196
+ // auto-detecting index.html in rootDir and adding a conflicting one.
197
+ renderer: false,
91
198
  imports: {
92
199
  autoImport: false,
93
200
  },
201
+ hooks: {
202
+ 'rollup:before': createRollupBeforeHook(rollupExternalEntries),
203
+ },
94
204
  rollupConfig: {
95
205
  onwarn(warning) {
96
206
  if (warning.message.includes('empty chunk') &&
@@ -104,12 +214,7 @@ export function nitro(options, nitroOptions) {
104
214
  ...(hasAPIDir
105
215
  ? []
106
216
  : useAPIMiddleware
107
- ? [
108
- {
109
- handler: '#ANALOG_API_MIDDLEWARE',
110
- middleware: true,
111
- },
112
- ]
217
+ ? [createNitroMiddlewareHandler('#ANALOG_API_MIDDLEWARE')]
113
218
  : []),
114
219
  ...pageHandlers,
115
220
  ],
@@ -123,13 +228,13 @@ export function nitro(options, nitroOptions) {
123
228
  },
124
229
  },
125
230
  virtual: {
126
- '#ANALOG_SSR_RENDERER': ssrRenderer,
127
- '#ANALOG_CLIENT_RENDERER': clientRenderer,
231
+ '#ANALOG_SSR_RENDERER': ssrRenderer(rendererIndexEntry),
232
+ '#ANALOG_CLIENT_RENDERER': clientRenderer(rendererIndexEntry),
128
233
  ...(hasAPIDir ? {} : { '#ANALOG_API_MIDDLEWARE': apiMiddleware }),
129
234
  },
130
235
  };
131
236
  if (isVercelPreset(buildPreset)) {
132
- nitroConfig = withVercelOutputAPI(nitroConfig, workspaceRoot);
237
+ nitroConfig = withVercelOutputAPI(nitroConfig, workspaceRoot, buildPreset);
133
238
  }
134
239
  if (isCloudflarePreset(buildPreset)) {
135
240
  nitroConfig = withCloudflareOutput(nitroConfig);
@@ -144,17 +249,30 @@ export function nitro(options, nitroOptions) {
144
249
  }
145
250
  if (!ssrBuild && !isTest) {
146
251
  // store the client output path for the SSR build config
147
- clientOutputPath = resolve(workspaceRoot, rootDir, config.build?.outDir || 'dist/client');
252
+ clientOutputPath = resolvedClientOutputPath;
148
253
  }
149
- const indexEntry = normalizePath(resolve(clientOutputPath, 'index.html'));
150
- nitroConfig.alias = {
151
- '#analog/index': indexEntry,
152
- };
153
254
  if (isBuild) {
154
- nitroConfig.publicAssets = [{ dir: clientOutputPath }];
155
- nitroConfig.renderer = options?.ssr
255
+ nitroConfig.publicAssets = [
256
+ { dir: normalizePath(clientOutputPath), maxAge: 0 },
257
+ ];
258
+ // In Nitro v3, renderer.entry is resolved via resolveModulePath()
259
+ // during options normalization, which requires a real filesystem path.
260
+ // Virtual modules (prefixed with #) can't survive this resolution.
261
+ // Instead, we add the renderer as a catch-all handler directly —
262
+ // this is functionally equivalent to what Nitro does internally
263
+ // (it converts renderer.entry into a { route: '/**', lazy: true }
264
+ // handler), but avoids the filesystem resolution step.
265
+ const rendererHandler = options?.ssr
156
266
  ? '#ANALOG_SSR_RENDERER'
157
267
  : '#ANALOG_CLIENT_RENDERER';
268
+ nitroConfig.handlers = [
269
+ ...(nitroConfig.handlers || []),
270
+ {
271
+ handler: rendererHandler,
272
+ route: '/**',
273
+ lazy: true,
274
+ },
275
+ ];
158
276
  if (isEmptyPrerenderRoutes(options)) {
159
277
  nitroConfig.prerender = {};
160
278
  nitroConfig.prerender.routes = ['/'];
@@ -220,30 +338,26 @@ export function nitro(options, nitroOptions) {
220
338
  }, []);
221
339
  }
222
340
  if (ssrBuild) {
223
- if (isWindows) {
224
- nitroConfig.externals = {
225
- inline: ['std-env'],
226
- };
341
+ if (process.platform === 'win32') {
342
+ nitroConfig.noExternals = appendNoExternals(nitroConfig.noExternals, 'std-env');
227
343
  }
344
+ rollupExternalEntries.push('rxjs', 'node-fetch-native/dist/polyfill');
228
345
  nitroConfig = {
229
346
  ...nitroConfig,
230
- externals: {
231
- ...nitroConfig.externals,
232
- external: ['rxjs', 'node-fetch-native/dist/polyfill'],
233
- },
234
347
  moduleSideEffects: ['zone.js/node', 'zone.js/fesm2015/zone-node'],
235
348
  handlers: [
236
349
  ...(hasAPIDir
237
350
  ? []
238
351
  : useAPIMiddleware
239
- ? [
240
- {
241
- handler: '#ANALOG_API_MIDDLEWARE',
242
- middleware: true,
243
- },
244
- ]
352
+ ? [createNitroMiddlewareHandler('#ANALOG_API_MIDDLEWARE')]
245
353
  : []),
246
354
  ...pageHandlers,
355
+ // Preserve the renderer catch-all handler added above
356
+ {
357
+ handler: rendererHandler,
358
+ route: '/**',
359
+ lazy: true,
360
+ },
247
361
  ],
248
362
  };
249
363
  }
@@ -260,7 +374,7 @@ export function nitro(options, nitroOptions) {
260
374
  ssr: {
261
375
  build: {
262
376
  ssr: true,
263
- rollupOptions: {
377
+ [vite.rolldownVersion ? 'rolldownOptions' : 'rollupOptions']: {
264
378
  input: options?.entryServer ||
265
379
  resolve(workspaceRoot, rootDir, `${sourceRoot}/main.server.ts`),
266
380
  },
@@ -278,17 +392,16 @@ export function nitro(options, nitroOptions) {
278
392
  builds.push(builder.build(builder.environments['ssr']));
279
393
  }
280
394
  await Promise.all(builds);
281
- let ssrEntryPath = resolve(options?.ssrBuildDir ||
282
- resolve(workspaceRoot, 'dist', rootDir, `ssr`), `main.server${filePrefix ? '.js' : ''}`);
283
- // add check for main.server.mjs fallback on Windows
284
- if (isWindows && !existsSync(ssrEntryPath)) {
285
- ssrEntryPath = ssrEntryPath.replace('.js', '.mjs');
395
+ const ssrOutDir = options?.ssrBuildDir ||
396
+ resolve(workspaceRoot, 'dist', rootDir, `ssr`);
397
+ if (options?.ssr || nitroConfig.prerender?.routes?.length) {
398
+ const ssrEntryPath = resolveBuiltSsrEntryPath(ssrOutDir);
399
+ const ssrEntry = toNitroSsrEntrypointSpecifier(ssrEntryPath);
400
+ nitroConfig.alias = {
401
+ ...nitroConfig.alias,
402
+ '#analog/ssr': ssrEntry,
403
+ };
286
404
  }
287
- const ssrEntry = normalizePath(filePrefix + ssrEntryPath);
288
- nitroConfig.alias = {
289
- ...nitroConfig.alias,
290
- '#analog/ssr': ssrEntry,
291
- };
292
405
  await buildServer(options, nitroConfig, routeSourceFiles);
293
406
  if (nitroConfig.prerender?.routes?.length &&
294
407
  options?.prerender?.sitemap) {
@@ -309,22 +422,33 @@ export function nitro(options, nitroOptions) {
309
422
  if (isServe && !isTest) {
310
423
  const nitro = await createNitro({
311
424
  dev: true,
425
+ // Nitro's Vite builder now rejects `build()` in dev mode, but Analog's
426
+ // dev integration still relies on the builder-driven reload hooks.
427
+ // Force the server worker onto Rollup for this dev-only path.
428
+ builder: 'rollup',
312
429
  ...nitroConfig,
313
430
  });
314
431
  const server = createDevServer(nitro);
315
432
  await build(nitro);
316
- const apiHandler = toNodeListener(server.app);
433
+ const apiHandler = async (req, res) => {
434
+ // Nitro v3's dev server is fetch-first, so adapt Vite's Node
435
+ // request once and let Nitro respond with a standard Web Response.
436
+ const response = await server.fetch(toWebRequest(req));
437
+ await writeWebResponseToNode(res, response);
438
+ };
317
439
  if (hasAPIDir) {
318
440
  viteServer.middlewares.use((req, res, next) => {
319
441
  if (req.url?.startsWith(`${prefix}${apiPrefix}`)) {
320
- apiHandler(req, res);
442
+ void apiHandler(req, res).catch((error) => next(error));
321
443
  return;
322
444
  }
323
445
  next();
324
446
  });
325
447
  }
326
448
  else {
327
- viteServer.middlewares.use(apiPrefix, apiHandler);
449
+ viteServer.middlewares.use(apiPrefix, (req, res, next) => {
450
+ void apiHandler(req, res).catch((error) => next(error));
451
+ });
328
452
  }
329
453
  viteServer.httpServer?.once('listening', () => {
330
454
  process.env['ANALOG_HOST'] = !viteServer.config.server.host
@@ -358,17 +482,16 @@ export function nitro(options, nitroOptions) {
358
482
  // sitemap needs to be built after all directories are built
359
483
  await buildSitemap(config, options.prerender.sitemap, nitroConfig.prerender.routes, clientOutputPath, routeSitemaps);
360
484
  }
361
- let ssrEntryPath = resolve(options?.ssrBuildDir ||
362
- resolve(workspaceRoot, 'dist', rootDir, `ssr`), `main.server${filePrefix ? '.js' : ''}`);
363
- // add check for main.server.mjs fallback on Windows
364
- if (isWindows && !existsSync(ssrEntryPath)) {
365
- ssrEntryPath = ssrEntryPath.replace('.js', '.mjs');
485
+ const closeBundleSsrOutDir = options?.ssrBuildDir ||
486
+ resolve(workspaceRoot, 'dist', rootDir, `ssr`);
487
+ if (options?.ssr || nitroConfig.prerender?.routes?.length) {
488
+ const ssrEntryPath = resolveBuiltSsrEntryPath(closeBundleSsrOutDir);
489
+ const ssrEntry = toNitroSsrEntrypointSpecifier(ssrEntryPath);
490
+ nitroConfig.alias = {
491
+ ...nitroConfig.alias,
492
+ '#analog/ssr': ssrEntry,
493
+ };
366
494
  }
367
- const ssrEntry = normalizePath(filePrefix + ssrEntryPath);
368
- nitroConfig.alias = {
369
- ...nitroConfig.alias,
370
- '#analog/ssr': ssrEntry,
371
- };
372
495
  await buildServer(options, nitroConfig, routeSourceFiles);
373
496
  console.log(`\n\nThe '@analogjs/platform' server has been successfully built.`);
374
497
  }
@@ -397,8 +520,24 @@ function isArrayWithElements(arr) {
397
520
  }
398
521
  const isVercelPreset = (buildPreset) => process.env['VERCEL'] ||
399
522
  (buildPreset && buildPreset.toLowerCase().includes('vercel'));
400
- const withVercelOutputAPI = (nitroConfig, workspaceRoot) => ({
523
+ const withVercelOutputAPI = (nitroConfig, workspaceRoot, buildPreset) => ({
401
524
  ...nitroConfig,
525
+ preset: nitroConfig?.preset ??
526
+ (buildPreset?.toLowerCase().includes('vercel-edge')
527
+ ? 'vercel-edge'
528
+ : 'vercel'),
529
+ vercel: {
530
+ ...nitroConfig?.vercel,
531
+ ...(buildPreset?.toLowerCase().includes('vercel-edge')
532
+ ? {}
533
+ : {
534
+ entryFormat: nitroConfig?.vercel?.entryFormat ?? 'node',
535
+ functions: {
536
+ runtime: nitroConfig?.vercel?.functions?.runtime ?? 'nodejs24.x',
537
+ ...nitroConfig?.vercel?.functions,
538
+ },
539
+ }),
540
+ },
402
541
  output: {
403
542
  ...nitroConfig?.output,
404
543
  dir: normalizePath(resolve(workspaceRoot, '.vercel', 'output')),