@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
@@ -1,24 +1,65 @@
1
1
  import { buildServer, isVercelPreset } from "./build-server.js";
2
2
  import { getBundleOptionsKey, isRolldown } from "./utils/rolldown.js";
3
- import { buildSSRApp } from "./build-ssr.js";
4
- import { apiMiddleware, clientRenderer, ssrRenderer } from "./utils/renderers.js";
3
+ import { buildClientApp, buildSSRApp } from "./build-ssr.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 { SERVER_FN_DISPATCH_VIRTUAL, 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";
10
13
  import { getMatchingContentFilesWithFrontMatter } from "./utils/get-content-files.js";
14
+ import { debugNitro, debugSsr } from "./utils/debug.js";
11
15
  import { build, createDevServer, createNitro } from "nitro/builder";
12
16
  import { mergeConfig, normalizePath } from "vite";
13
17
  import { relative, resolve } from "node:path";
14
18
  import { pathToFileURL } from "node:url";
15
- import { existsSync, readFileSync } from "node:fs";
19
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
16
20
  //#region packages/vite-plugin-nitro/src/lib/vite-plugin-nitro.ts
17
- function createNitroMiddlewareHandler(handler) {
21
+ function isObjectHook(value) {
22
+ return !!value && typeof value === "object" && "handler" in value;
23
+ }
24
+ function cloneObjectHook(hook) {
25
+ if (!isObjectHook(hook)) return hook;
18
26
  return {
19
- route: "/**",
20
- handler,
21
- middleware: true
27
+ ...hook,
28
+ handler: hook.handler
29
+ };
30
+ }
31
+ function cloneUserPlugin(plugin) {
32
+ if (!plugin || typeof plugin !== "object") return plugin;
33
+ const pluginRecord = plugin;
34
+ const clone = Object.assign(Object.create(Object.getPrototypeOf(plugin)), pluginRecord);
35
+ for (const key of Object.keys(pluginRecord)) clone[key] = cloneObjectHook(pluginRecord[key]);
36
+ return clone;
37
+ }
38
+ function cloneEnvironmentEntries(environments) {
39
+ if (!environments || typeof environments !== "object") return environments;
40
+ return Object.fromEntries(Object.entries(environments).map(([name, environment]) => {
41
+ if (!environment || typeof environment !== "object") return [name, environment];
42
+ const environmentRecord = environment;
43
+ return [name, {
44
+ ...environmentRecord,
45
+ build: environmentRecord["build"] && typeof environmentRecord["build"] === "object" ? { ...environmentRecord["build"] } : environmentRecord["build"]
46
+ }];
47
+ }));
48
+ }
49
+ function cloneUserConfig(userConfig) {
50
+ const { environments, resolve, build, server, plugins } = userConfig;
51
+ const test = userConfig.test;
52
+ return {
53
+ ...userConfig,
54
+ plugins: plugins?.map(cloneUserPlugin),
55
+ build: build && { ...build },
56
+ environments: cloneEnvironmentEntries(environments),
57
+ server: server && { ...server },
58
+ test: test && { ...test },
59
+ resolve: resolve && {
60
+ ...resolve,
61
+ alias: Array.isArray(resolve.alias) ? [...resolve.alias] : resolve.alias && typeof resolve.alias === "object" ? { ...resolve.alias } : resolve.alias
62
+ }
22
63
  };
23
64
  }
24
65
  /**
@@ -87,10 +128,137 @@ function sanitizeNitroBundlerConfig(_nitro, bundlerConfig) {
87
128
  };
88
129
  }
89
130
  }
90
- function resolveClientOutputPath(cachedPath, workspaceRoot, rootDir, configuredOutDir, ssrBuild) {
91
- if (cachedPath) return cachedPath;
92
- if (!ssrBuild) return resolve(workspaceRoot, rootDir, configuredOutDir || "dist/client");
93
- return resolve(workspaceRoot, "dist", rootDir, "client");
131
+ function resolveClientOutputPath(cachedPath, workspaceRoot, rootDir, configuredOutDir) {
132
+ if (cachedPath) {
133
+ debugNitro("resolveClientOutputPath using cached path", {
134
+ cachedPath,
135
+ workspaceRoot,
136
+ rootDir,
137
+ configuredOutDir
138
+ });
139
+ return cachedPath;
140
+ }
141
+ if (configuredOutDir) {
142
+ const resolvedPath = normalizePath(resolve(workspaceRoot, rootDir, configuredOutDir));
143
+ debugNitro("resolveClientOutputPath using configured build.outDir", {
144
+ workspaceRoot,
145
+ rootDir,
146
+ configuredOutDir,
147
+ resolvedPath
148
+ });
149
+ return resolvedPath;
150
+ }
151
+ const resolvedPath = normalizePath(resolve(workspaceRoot, "dist", rootDir, "client"));
152
+ debugNitro("resolveClientOutputPath using default dist client path", {
153
+ workspaceRoot,
154
+ rootDir,
155
+ configuredOutDir,
156
+ resolvedPath
157
+ });
158
+ return resolvedPath;
159
+ }
160
+ function getEnvironmentBuildOutDir(environment) {
161
+ if (!environment || typeof environment !== "object") return;
162
+ const environmentConfig = environment;
163
+ return environmentConfig.config?.build?.outDir ?? environmentConfig.build?.outDir;
164
+ }
165
+ function resolveBuiltClientOutputPath(cachedPath, workspaceRoot, rootDir, configuredOutDir, environment) {
166
+ const environmentOutDir = getEnvironmentBuildOutDir(environment);
167
+ if (environmentOutDir) {
168
+ const resolvedPath = normalizePath(resolve(workspaceRoot, rootDir, environmentOutDir));
169
+ debugNitro("resolveBuiltClientOutputPath using environment outDir", {
170
+ cachedPath,
171
+ workspaceRoot,
172
+ rootDir,
173
+ configuredOutDir,
174
+ environmentOutDir,
175
+ resolvedPath
176
+ });
177
+ return resolvedPath;
178
+ }
179
+ debugNitro("resolveBuiltClientOutputPath falling back to shared resolver", {
180
+ cachedPath,
181
+ workspaceRoot,
182
+ rootDir,
183
+ configuredOutDir,
184
+ environmentOutDir
185
+ });
186
+ return resolveClientOutputPath(cachedPath, workspaceRoot, rootDir, configuredOutDir);
187
+ }
188
+ function getNitroPublicOutputDir(nitroConfig) {
189
+ const publicDir = nitroConfig.output?.publicDir;
190
+ if (!publicDir) throw new Error("Nitro public output directory is required to build the sitemap.");
191
+ return publicDir;
192
+ }
193
+ function readDirectoryEntries(path) {
194
+ try {
195
+ return readdirSync(path).sort();
196
+ } catch (error) {
197
+ return [`<<unable to read directory: ${error instanceof Error ? error.message : String(error)}>>`];
198
+ }
199
+ }
200
+ function getPathDebugInfo(path) {
201
+ return {
202
+ rawPath: path,
203
+ normalizedPath: normalizePath(path),
204
+ exists: existsSync(path),
205
+ entries: existsSync(path) ? readDirectoryEntries(path) : []
206
+ };
207
+ }
208
+ function assetSourceToString(source) {
209
+ return typeof source === "string" ? source : Buffer.from(source).toString("utf8");
210
+ }
211
+ function captureClientIndexHtmlFromBundle(bundle, hook) {
212
+ const indexHtmlAsset = Object.values(bundle).find((chunk) => chunk.type === "asset" && chunk.fileName === "index.html" && typeof chunk.source !== "undefined");
213
+ if (!indexHtmlAsset?.source) {
214
+ debugNitro(`client bundle did not expose index.html during ${hook}`, {
215
+ hook,
216
+ bundleKeys: Object.keys(bundle).sort(),
217
+ assetFileNames: Object.values(bundle).filter((chunk) => chunk.type === "asset").map((chunk) => chunk.fileName).filter(Boolean)
218
+ });
219
+ return;
220
+ }
221
+ const indexHtml = assetSourceToString(indexHtmlAsset.source);
222
+ debugNitro(`captured client bundle index.html asset during ${hook}`, {
223
+ hook,
224
+ fileName: indexHtmlAsset.fileName,
225
+ htmlLength: indexHtml.length
226
+ });
227
+ return indexHtml;
228
+ }
229
+ function registerIndexHtmlVirtual(nitroConfig, clientOutputPath, inlineIndexHtml) {
230
+ const indexHtmlPath = resolve(clientOutputPath, "index.html");
231
+ debugNitro("registerIndexHtmlVirtual inspecting client output", {
232
+ platform: process.platform,
233
+ cwd: process.cwd(),
234
+ clientOutputPath,
235
+ clientOutputPathInfo: getPathDebugInfo(clientOutputPath),
236
+ indexHtmlPath,
237
+ indexHtmlExists: existsSync(indexHtmlPath),
238
+ hasInlineIndexHtml: typeof inlineIndexHtml === "string"
239
+ });
240
+ if (!existsSync(indexHtmlPath) && typeof inlineIndexHtml !== "string") {
241
+ debugNitro("registerIndexHtmlVirtual missing index.html", {
242
+ platform: process.platform,
243
+ cwd: process.cwd(),
244
+ clientOutputPath,
245
+ clientOutputPathInfo: getPathDebugInfo(clientOutputPath),
246
+ indexHtmlPath,
247
+ hasInlineIndexHtml: typeof inlineIndexHtml === "string",
248
+ nitroOutput: nitroConfig.output,
249
+ nitroPublicAssets: nitroConfig.publicAssets
250
+ });
251
+ throw new Error(`[analog] Client build output not found at ${indexHtmlPath}.\nEnsure the client environment build completed successfully before the server build.`);
252
+ }
253
+ const indexHtml = typeof inlineIndexHtml === "string" ? inlineIndexHtml : readFileSync(indexHtmlPath, "utf8");
254
+ debugNitro("registerIndexHtmlVirtual using HTML template source", {
255
+ source: typeof inlineIndexHtml === "string" ? "captured client bundle asset" : "client output index.html file",
256
+ indexHtmlPath
257
+ });
258
+ nitroConfig.virtual = {
259
+ ...nitroConfig.virtual,
260
+ "#analog/index": `export default ${JSON.stringify(indexHtml)};`
261
+ };
94
262
  }
95
263
  /**
96
264
  * Converts the built SSR entry path into a specifier that Nitro's bundler
@@ -147,7 +315,6 @@ function nitro(options, nitroOptions) {
147
315
  const baseURL = process.env["NITRO_APP_BASE_URL"] || "";
148
316
  const prefix = baseURL ? baseURL.substring(0, baseURL.length - 1) : "";
149
317
  const apiPrefix = `/${options?.apiPrefix || "api"}`;
150
- const useAPIMiddleware = typeof options?.useAPIMiddleware !== "undefined" ? options?.useAPIMiddleware : true;
151
318
  const viteRolldownOutput = options?.vite?.build?.rolldownOptions?.output;
152
319
  const viteRolldownOutputConfig = viteRolldownOutput && !Array.isArray(viteRolldownOutput) ? viteRolldownOutput : void 0;
153
320
  const codeSplitting = viteRolldownOutputConfig?.codeSplitting;
@@ -157,10 +324,12 @@ function nitro(options, nitroOptions) {
157
324
  let config;
158
325
  let nitroConfig;
159
326
  let environmentBuild = false;
160
- let hasAPIDir = false;
327
+ let hasServerFns = false;
161
328
  let clientOutputPath = "";
162
- let rendererIndexEntry = "";
329
+ let clientIndexHtml;
330
+ let legacyClientSubBuild = false;
163
331
  const rollupExternalEntries = [];
332
+ const sitemapRoutes = [];
164
333
  const routeSitemaps = {};
165
334
  const routeSourceFiles = {};
166
335
  let rootDir = workspaceRoot;
@@ -168,7 +337,8 @@ function nitro(options, nitroOptions) {
168
337
  options?.ssr ? devServerPlugin({
169
338
  entryServer: options?.entryServer,
170
339
  index: options?.index,
171
- routeRules: nitroOptions?.routeRules
340
+ routeRules: nitroOptions?.routeRules,
341
+ i18n: options?.i18n
172
342
  }) : false,
173
343
  {
174
344
  name: "@analogjs/vite-plugin-nitro",
@@ -176,20 +346,48 @@ function nitro(options, nitroOptions) {
176
346
  isServe = command === "serve";
177
347
  isBuild = command === "build";
178
348
  ssrBuild = userConfig.build?.ssr === true;
179
- config = userConfig;
349
+ config = cloneUserConfig(userConfig);
180
350
  isTest = isTest ? isTest : mode === "test";
181
- rootDir = relative(workspaceRoot, config.root || ".") || ".";
182
- hasAPIDir = existsSync(resolve(workspaceRoot, rootDir, `${sourceRoot}/server/routes/${options?.apiPrefix || "api"}`));
351
+ rollupExternalEntries.length = 0;
352
+ clientIndexHtml = void 0;
353
+ sitemapRoutes.length = 0;
354
+ for (const key of Object.keys(routeSitemaps)) delete routeSitemaps[key];
355
+ for (const key of Object.keys(routeSourceFiles)) delete routeSourceFiles[key];
356
+ const resolvedConfigRoot = config.root ? resolve(workspaceRoot, config.root) : workspaceRoot;
357
+ rootDir = relative(workspaceRoot, resolvedConfigRoot) || ".";
183
358
  const buildPreset = process.env["BUILD_PRESET"] ?? nitroOptions?.preset ?? (process.env["VERCEL"] ? "vercel" : void 0);
184
359
  const pageHandlers = getPageHandlers({
185
360
  workspaceRoot,
186
361
  sourceRoot,
187
362
  rootDir,
188
- additionalPagesDirs: options?.additionalPagesDirs,
189
- hasAPIDir
363
+ additionalPagesDirs: options?.additionalPagesDirs
190
364
  });
191
- const resolvedClientOutputPath = resolveClientOutputPath(clientOutputPath, workspaceRoot, rootDir, config.build?.outDir, ssrBuild);
192
- rendererIndexEntry = normalizePath(resolve(resolvedClientOutputPath, "index.html"));
365
+ const resolvedClientOutputPath = resolveClientOutputPath(clientOutputPath, workspaceRoot, rootDir, config.build?.outDir);
366
+ debugNitro("nitro config resolved client output path", {
367
+ platform: process.platform,
368
+ workspaceRoot,
369
+ configRoot: config.root,
370
+ resolvedConfigRoot,
371
+ rootDir,
372
+ buildOutDir: config.build?.outDir,
373
+ clientOutputPath,
374
+ resolvedClientOutputPath,
375
+ hasEnvironmentConfig: !!config.environments,
376
+ clientEnvironmentOutDir: config.environments?.["client"] && typeof config.environments["client"] === "object" && "build" in config.environments["client"] ? config.environments["client"].build?.outDir : void 0
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
+ const serverFnHandlers = hasServerFns ? [getServerFnDispatchHandler()] : [];
387
+ const serverFnVirtual = serverFnModules.length > 0 ? { [SERVER_FN_DISPATCH_VIRTUAL]: buildServerFnDispatchModule({
388
+ modules: serverFnModules,
389
+ appConfigModule: serverFnAppConfigModule
390
+ }) } : {};
193
391
  nitroConfig = {
194
392
  rootDir: normalizePath(rootDir),
195
393
  preset: buildPreset,
@@ -202,36 +400,43 @@ function nitro(options, nitroOptions) {
202
400
  publicDir: normalizePath(resolve(workspaceRoot, "dist", rootDir, "analog/public"))
203
401
  },
204
402
  buildDir: normalizePath(resolve(workspaceRoot, "dist", rootDir, ".nitro")),
205
- typescript: { generateTsConfig: false },
206
403
  runtimeConfig: {
207
404
  apiPrefix: apiPrefix.substring(1),
208
405
  prefix
209
406
  },
210
407
  renderer: false,
211
- imports: { autoImport: false },
212
408
  hooks: { "rollup:before": createRollupBeforeHook(rollupExternalEntries) },
213
409
  rollupConfig: {
214
410
  onwarn(warning) {
215
411
  if (warning.message.includes("empty chunk") && warning.message.endsWith(".server")) return;
216
412
  },
217
- plugins: [pageEndpointsPlugin()]
413
+ plugins: [pageEndpointsPlugin(), serverFnIdPlugin(normalizePath(resolve(workspaceRoot, rootDir)))]
218
414
  },
219
- handlers: [...hasAPIDir ? [] : useAPIMiddleware ? [createNitroMiddlewareHandler("#ANALOG_API_MIDDLEWARE")] : [], ...pageHandlers],
220
- routeRules: hasAPIDir ? void 0 : useAPIMiddleware ? void 0 : { [`${prefix}${apiPrefix}/**`]: { proxy: { to: "/**" } } },
415
+ ...hasServerFns ? { moduleSideEffects: ["@angular/compiler", ...serverFnModules.map((m) => normalizePath(m.file))] } : {},
416
+ handlers: [...pageHandlers, ...serverFnHandlers],
417
+ routeRules: void 0,
221
418
  virtual: {
222
- "#ANALOG_SSR_RENDERER": ssrRenderer(rendererIndexEntry),
223
- "#ANALOG_CLIENT_RENDERER": clientRenderer(rendererIndexEntry),
224
- ...hasAPIDir ? {} : { "#ANALOG_API_MIDDLEWARE": apiMiddleware }
419
+ "#ANALOG_SSR_RENDERER": ssrRenderer(),
420
+ "#ANALOG_CLIENT_RENDERER": clientRenderer(),
421
+ ...serverFnVirtual
225
422
  }
226
423
  };
227
424
  if (isVercelPreset(buildPreset)) nitroConfig = withVercelOutputAPI(nitroConfig, workspaceRoot);
228
425
  if (isCloudflarePreset(buildPreset)) nitroConfig = withCloudflareOutput(nitroConfig);
229
426
  if (isNetlifyPreset(buildPreset) && rootDir === "." && !existsSync(resolve(workspaceRoot, "netlify.toml"))) nitroConfig = withNetlifyOutputAPI(nitroConfig, workspaceRoot);
230
427
  if (isFirebaseAppHosting()) nitroConfig = withAppHostingOutput(nitroConfig);
231
- if (!ssrBuild && !isTest) clientOutputPath = resolvedClientOutputPath;
428
+ if (!ssrBuild && !isTest) {
429
+ clientOutputPath = resolvedClientOutputPath;
430
+ debugNitro("nitro config cached client output path for later SSR/Nitro build", {
431
+ ssrBuild,
432
+ isTest,
433
+ clientOutputPath
434
+ });
435
+ }
436
+ nitroConfig.alias = {};
232
437
  if (isBuild) {
233
438
  nitroConfig.publicAssets = [{
234
- dir: normalizePath(clientOutputPath),
439
+ dir: normalizePath(resolvedClientOutputPath),
235
440
  maxAge: 0
236
441
  }];
237
442
  const rendererHandler = options?.ssr ? "#ANALOG_SSR_RENDERER" : "#ANALOG_CLIENT_RENDERER";
@@ -249,12 +454,14 @@ function nitro(options, nitroOptions) {
249
454
  nitroConfig.prerender.crawlLinks = options?.prerender?.discover;
250
455
  let routes = [];
251
456
  const prerenderRoutes = options?.prerender?.routes;
457
+ const hasExplicitPrerenderRoutes = typeof prerenderRoutes === "function" || Array.isArray(prerenderRoutes);
252
458
  if (isArrayWithElements(prerenderRoutes)) routes = prerenderRoutes;
253
459
  else if (typeof prerenderRoutes === "function") routes = await prerenderRoutes();
254
- nitroConfig.prerender.routes = routes.reduce((prev, current) => {
460
+ const resolvedPrerenderRoutes = routes.reduce((prev, current) => {
255
461
  if (!current) return prev;
256
462
  if (typeof current === "string") {
257
463
  prev.push(current);
464
+ sitemapRoutes.push(current);
258
465
  return prev;
259
466
  }
260
467
  if ("route" in current) {
@@ -264,10 +471,11 @@ function nitro(options, nitroOptions) {
264
471
  routeSourceFiles[current.route] = readFileSync(sourcePath, "utf8");
265
472
  }
266
473
  prev.push(current.route);
474
+ sitemapRoutes.push(current.route);
267
475
  if ("staticData" in current) prev.push(`${apiPrefix}/_analog/pages/${current.route}`);
268
476
  return prev;
269
477
  }
270
- getMatchingContentFilesWithFrontMatter(workspaceRoot, rootDir, current.contentDir).forEach((f) => {
478
+ getMatchingContentFilesWithFrontMatter(workspaceRoot, rootDir, current.contentDir, current.recursive).forEach((f) => {
271
479
  const result = current.transform(f);
272
480
  if (result) {
273
481
  if (current.sitemap) routeSitemaps[result] = current.sitemap && typeof current.sitemap === "function" ? current.sitemap?.(f) : current.sitemap;
@@ -276,21 +484,23 @@ function nitro(options, nitroOptions) {
276
484
  if (sourceContent) routeSourceFiles[result] = sourceContent;
277
485
  }
278
486
  prev.push(result);
487
+ sitemapRoutes.push(result);
279
488
  if ("staticData" in current) prev.push(`${apiPrefix}/_analog/pages/${result}`);
280
489
  }
281
490
  });
282
491
  return prev;
283
492
  }, []);
493
+ nitroConfig.prerender.routes = hasExplicitPrerenderRoutes || resolvedPrerenderRoutes.length ? resolvedPrerenderRoutes : nitroConfig.prerender.routes ?? [];
284
494
  }
285
495
  if (ssrBuild || options?.ssr || nitroConfig.prerender?.routes?.length) {
496
+ nitroConfig.noExternals = appendNoExternals(nitroConfig.noExternals, "es-toolkit");
286
497
  if (process.platform === "win32") nitroConfig.noExternals = appendNoExternals(nitroConfig.noExternals, "std-env");
287
- rollupExternalEntries.push("rxjs", "node-fetch-native/dist/polyfill");
498
+ rollupExternalEntries.push("rxjs", "node-fetch-native/dist/polyfill", "sharp");
288
499
  nitroConfig = {
289
500
  ...nitroConfig,
290
- moduleSideEffects: ["zone.js/node", "zone.js/fesm2015/zone-node"],
291
501
  handlers: [
292
- ...hasAPIDir ? [] : useAPIMiddleware ? [createNitroMiddlewareHandler("#ANALOG_API_MIDDLEWARE")] : [],
293
502
  ...pageHandlers,
503
+ ...serverFnHandlers,
294
504
  {
295
505
  handler: rendererHandler,
296
506
  route: "/**",
@@ -301,10 +511,12 @@ function nitro(options, nitroOptions) {
301
511
  }
302
512
  }
303
513
  nitroConfig = mergeConfig(nitroConfig, nitroOptions);
514
+ if (environmentBuild || ssrBuild || isServe) return {};
304
515
  return {
305
516
  environments: {
306
517
  client: { build: {
307
518
  outDir: config?.build?.outDir || resolve(workspaceRoot, "dist", rootDir, "client"),
519
+ emptyOutDir: true,
308
520
  ...isRolldown() && codeSplitting !== void 0 ? { rolldownOptions: { output: {
309
521
  ...viteRolldownOutputConfig,
310
522
  codeSplitting
@@ -313,29 +525,81 @@ function nitro(options, nitroOptions) {
313
525
  ssr: { build: {
314
526
  ssr: true,
315
527
  [getBundleOptionsKey()]: { input: options?.entryServer || resolve(workspaceRoot, rootDir, `${sourceRoot}/main.server.ts`) },
316
- outDir: options?.ssrBuildDir || resolve(workspaceRoot, "dist", rootDir, "ssr")
528
+ outDir: options?.ssrBuildDir || resolve(workspaceRoot, "dist", rootDir, "ssr"),
529
+ emptyOutDir: false
317
530
  } }
318
531
  },
319
532
  builder: {
320
533
  sharedPlugins: true,
321
534
  buildApp: async (builder) => {
322
535
  environmentBuild = true;
323
- const builds = [builder.build(builder.environments["client"])];
324
- if (options?.ssr || nitroConfig.prerender?.routes?.length) builds.push(builder.build(builder.environments["ssr"]));
325
- await Promise.all(builds);
536
+ debugNitro("builder.buildApp starting", {
537
+ platform: process.platform,
538
+ workspaceRoot,
539
+ rootDir,
540
+ cachedClientOutputPath: clientOutputPath,
541
+ configuredBuildOutDir: config.build?.outDir,
542
+ clientEnvironmentOutDir: getEnvironmentBuildOutDir(builder.environments["client"]),
543
+ ssrEnvironmentOutDir: getEnvironmentBuildOutDir(builder.environments["ssr"])
544
+ });
545
+ await builder.build(builder.environments["client"]);
546
+ const postClientBuildOutputPath = resolveBuiltClientOutputPath(clientOutputPath, workspaceRoot, rootDir, config.build?.outDir, builder.environments["client"]);
547
+ registerIndexHtmlVirtual(nitroConfig, postClientBuildOutputPath, clientIndexHtml);
548
+ debugNitro("builder.buildApp completed client build", {
549
+ postClientBuildOutputPath,
550
+ postClientBuildOutputInfo: getPathDebugInfo(postClientBuildOutputPath),
551
+ postClientBuildIndexHtmlPath: resolve(postClientBuildOutputPath, "index.html"),
552
+ postClientBuildIndexHtmlExists: existsSync(resolve(postClientBuildOutputPath, "index.html"))
553
+ });
554
+ if (options?.ssr || nitroConfig.prerender?.routes?.length) {
555
+ debugSsr("builder.buildApp starting SSR build", {
556
+ ssrEnabled: options?.ssr,
557
+ prerenderRoutes: nitroConfig.prerender?.routes
558
+ });
559
+ /**
560
+ * This launches the SSR environment as a second build from the
561
+ * shared plugin graph above. When debugging an apparent
562
+ * duplicate `@analogjs/vite-plugin-angular` registration, this
563
+ * is the handoff to inspect: the SSR builder replays the shared
564
+ * plugins for the server pass and may therefore expose multiple
565
+ * Angular-plugin entries in the SSR resolved config.
566
+ *
567
+ * That is expected for this orchestration path and should not
568
+ * be treated the same as a duplicated client build, where two
569
+ * Angular plugin instances would maintain separate style maps.
570
+ */
571
+ await builder.build(builder.environments["ssr"]);
572
+ debugSsr("builder.buildApp completed SSR build", { ssrOutputPath: options?.ssrBuildDir || resolve(workspaceRoot, "dist", rootDir, "ssr") });
573
+ }
326
574
  applySsrEntryAlias(nitroConfig, options, workspaceRoot, rootDir);
575
+ const resolvedClientOutputPath = resolveBuiltClientOutputPath(clientOutputPath, workspaceRoot, rootDir, config.build?.outDir, builder.environments["client"]);
576
+ nitroConfig.publicAssets = [{
577
+ dir: normalizePath(resolvedClientOutputPath),
578
+ maxAge: 0
579
+ }];
580
+ debugNitro("builder.buildApp resolved final client output path before Nitro build", {
581
+ resolvedClientOutputPath,
582
+ resolvedClientOutputInfo: getPathDebugInfo(resolvedClientOutputPath),
583
+ nitroPublicAssets: nitroConfig.publicAssets
584
+ });
327
585
  await buildServer(options, nitroConfig, routeSourceFiles);
328
586
  if (nitroConfig.prerender?.routes?.length && options?.prerender?.sitemap) {
329
- const publicDir = nitroConfig.output?.publicDir;
330
- if (!publicDir) throw new Error("Nitro public output directory is required to build the sitemap.");
331
587
  console.log("Building Sitemap...");
332
- await buildSitemap(config, options.prerender.sitemap, nitroConfig.prerender.routes, publicDir, routeSitemaps);
588
+ await buildSitemap(config, options.prerender.sitemap, sitemapRoutes.length ? sitemapRoutes : nitroConfig.prerender.routes, getNitroPublicOutputDir(nitroConfig), routeSitemaps, { apiPrefix: options?.apiPrefix || "api" });
333
589
  }
334
590
  console.log(`\n\nThe '@analogjs/platform' server has been successfully built.`);
335
591
  }
336
592
  }
337
593
  };
338
594
  },
595
+ generateBundle(_options, bundle) {
596
+ if (!isBuild || ssrBuild) return;
597
+ clientIndexHtml = captureClientIndexHtmlFromBundle(bundle, "generateBundle") ?? clientIndexHtml;
598
+ },
599
+ writeBundle(_options, bundle) {
600
+ if (!isBuild || ssrBuild) return;
601
+ clientIndexHtml = captureClientIndexHtmlFromBundle(bundle, "writeBundle") ?? clientIndexHtml;
602
+ },
339
603
  async configureServer(viteServer) {
340
604
  if (isServe && !isTest) {
341
605
  const nitro = await createNitro({
@@ -345,41 +609,120 @@ function nitro(options, nitroOptions) {
345
609
  });
346
610
  const server = createDevServer(nitro);
347
611
  await build(nitro);
612
+ const nitroSourceRoots = [normalizePath(resolve(workspaceRoot, rootDir, `${sourceRoot}/server`)), ...(options?.additionalAPIDirs || []).map((dir) => normalizePath(`${workspaceRoot}${dir}`))];
613
+ const isNitroSourceFile = (path) => {
614
+ const normalizedPath = normalizePath(path);
615
+ return nitroSourceRoots.some((root) => normalizedPath === root || normalizedPath.startsWith(`${root}/`));
616
+ };
617
+ let nitroRebuildPromise;
618
+ let nitroRebuildPending = false;
619
+ const rebuildNitroServer = () => {
620
+ if (nitroRebuildPromise) {
621
+ nitroRebuildPending = true;
622
+ return nitroRebuildPromise;
623
+ }
624
+ nitroRebuildPromise = (async () => {
625
+ do {
626
+ nitroRebuildPending = false;
627
+ await build(nitro);
628
+ } while (nitroRebuildPending);
629
+ viteServer.ws.send("analog:debug-full-reload", {
630
+ plugin: "vite-plugin-nitro",
631
+ reason: "nitro-server-rebuilt"
632
+ });
633
+ viteServer.ws.send({ type: "full-reload" });
634
+ })().catch((error) => {
635
+ viteServer.config.logger.error(`[analog] Failed to rebuild Nitro dev server.\n${error instanceof Error ? error.stack || error.message : String(error)}`);
636
+ }).finally(() => {
637
+ nitroRebuildPromise = void 0;
638
+ });
639
+ return nitroRebuildPromise;
640
+ };
641
+ const onNitroSourceChange = (path) => {
642
+ if (!isNitroSourceFile(path)) return;
643
+ rebuildNitroServer();
644
+ };
645
+ viteServer.watcher.on("add", onNitroSourceChange);
646
+ viteServer.watcher.on("change", onNitroSourceChange);
647
+ viteServer.watcher.on("unlink", onNitroSourceChange);
348
648
  const apiHandler = async (req, res) => {
349
649
  await writeWebResponseToNode(res, await server.fetch(toWebRequest(req)));
350
650
  };
351
- if (hasAPIDir) viteServer.middlewares.use((req, res, next) => {
651
+ viteServer.middlewares.use((req, res, next) => {
352
652
  if (req.url?.startsWith(`${prefix}${apiPrefix}`)) {
353
653
  apiHandler(req, res).catch((error) => next(error));
354
654
  return;
355
655
  }
356
656
  next();
357
657
  });
358
- else viteServer.middlewares.use(apiPrefix, (req, res, next) => {
359
- apiHandler(req, res).catch((error) => next(error));
360
- });
361
658
  viteServer.httpServer?.once("listening", () => {
362
659
  process.env["ANALOG_HOST"] = !viteServer.config.server.host ? "localhost" : viteServer.config.server.host;
363
660
  process.env["ANALOG_PORT"] = `${viteServer.config.server.port}`;
364
661
  });
365
- if (nitroOptions?.experimental?.websocket) viteServer.httpServer?.on("upgrade", server.upgrade);
662
+ if (nitroOptions?.experimental?.websocket) {
663
+ debugNitro("experimental websocket upgrade handler enabled");
664
+ viteServer.httpServer?.on("upgrade", server.upgrade);
665
+ }
366
666
  console.log(`\n\nThe server endpoints are accessible under the "${prefix}${apiPrefix}" path.`);
367
667
  }
368
668
  },
369
669
  async closeBundle() {
670
+ if (legacyClientSubBuild) return;
370
671
  if (environmentBuild) return;
371
672
  if (ssrBuild) return;
372
673
  if (isBuild) {
674
+ const resolvedClientOutputPath = resolveClientOutputPath(clientOutputPath, workspaceRoot, rootDir, config.build?.outDir);
675
+ debugNitro("closeBundle resolved client output path before legacy SSR build", {
676
+ platform: process.platform,
677
+ workspaceRoot,
678
+ rootDir,
679
+ cachedClientOutputPath: clientOutputPath,
680
+ configuredBuildOutDir: config.build?.outDir,
681
+ resolvedClientOutputPath,
682
+ resolvedClientOutputInfo: getPathDebugInfo(resolvedClientOutputPath)
683
+ });
684
+ const indexHtmlPath = resolve(resolvedClientOutputPath, "index.html");
685
+ if (!existsSync(indexHtmlPath) && typeof clientIndexHtml !== "string") {
686
+ debugNitro("closeBundle rebuilding missing client output before SSR/Nitro", {
687
+ platform: process.platform,
688
+ workspaceRoot,
689
+ rootDir,
690
+ configuredBuildOutDir: config.build?.outDir,
691
+ resolvedClientOutputPath,
692
+ indexHtmlPath
693
+ });
694
+ legacyClientSubBuild = true;
695
+ try {
696
+ await buildClientApp(config, options);
697
+ } finally {
698
+ legacyClientSubBuild = false;
699
+ }
700
+ }
701
+ registerIndexHtmlVirtual(nitroConfig, resolvedClientOutputPath, clientIndexHtml);
373
702
  if (options?.ssr) {
374
703
  console.log("Building SSR application...");
375
704
  await buildSSRApp(config, options);
705
+ debugSsr("closeBundle completed standalone SSR build", {
706
+ ssrBuildDir: options?.ssrBuildDir || resolve(workspaceRoot, "dist", rootDir, "ssr"),
707
+ clientOutputPathInfo: clientOutputPath ? getPathDebugInfo(clientOutputPath) : null
708
+ });
376
709
  }
710
+ applySsrEntryAlias(nitroConfig, options, workspaceRoot, rootDir);
711
+ debugNitro("closeBundle resolved client output path before Nitro build", {
712
+ platform: process.platform,
713
+ workspaceRoot,
714
+ rootDir,
715
+ cachedClientOutputPath: clientOutputPath,
716
+ configuredBuildOutDir: config.build?.outDir,
717
+ resolvedClientOutputPath,
718
+ resolvedClientOutputInfo: getPathDebugInfo(resolvedClientOutputPath)
719
+ });
720
+ registerIndexHtmlVirtual(nitroConfig, resolvedClientOutputPath, clientIndexHtml);
721
+ await buildServer(options, nitroConfig, routeSourceFiles);
377
722
  if (nitroConfig.prerender?.routes?.length && options?.prerender?.sitemap) {
378
723
  console.log("Building Sitemap...");
379
- await buildSitemap(config, options.prerender.sitemap, nitroConfig.prerender.routes, clientOutputPath, routeSitemaps);
724
+ await buildSitemap(config, options.prerender.sitemap, sitemapRoutes.length ? sitemapRoutes : nitroConfig.prerender.routes, getNitroPublicOutputDir(nitroConfig), routeSitemaps, { apiPrefix: options?.apiPrefix || "api" });
380
725
  }
381
- applySsrEntryAlias(nitroConfig, options, workspaceRoot, rootDir);
382
- await buildServer(options, nitroConfig, routeSourceFiles);
383
726
  console.log(`\n\nThe '@analogjs/platform' server has been successfully built.`);
384
727
  }
385
728
  }
@@ -387,7 +730,13 @@ function nitro(options, nitroOptions) {
387
730
  {
388
731
  name: "@analogjs/vite-plugin-nitro-api-prefix",
389
732
  config() {
390
- return { define: { ANALOG_API_PREFIX: `"${baseURL.substring(1)}${apiPrefix.substring(1)}"` } };
733
+ return { define: {
734
+ ANALOG_API_PREFIX: `"${baseURL.substring(1)}${apiPrefix.substring(1)}"`,
735
+ ...options?.i18n ? {
736
+ ANALOG_I18N_DEFAULT_LOCALE: JSON.stringify(options.i18n.defaultLocale),
737
+ ANALOG_I18N_LOCALES: JSON.stringify(options.i18n.locales)
738
+ } : {}
739
+ } };
391
740
  }
392
741
  }
393
742
  ];
@@ -457,6 +806,10 @@ var withAppHostingOutput = (nitroConfig) => {
457
806
  }
458
807
  };
459
808
  };
809
+ function resolveServerFnAppConfigModule(workspaceRoot, rootDir, sourceRoot) {
810
+ const root = normalizePath(resolve(workspaceRoot, rootDir));
811
+ return [`${root}/${sourceRoot}/app/app.config.server.ts`, `${root}/${sourceRoot}/app.config.server.ts`].find((candidate) => existsSync(candidate));
812
+ }
460
813
  var isNetlifyPreset = (buildPreset) => process.env["NETLIFY"] || buildPreset && buildPreset.toLowerCase().includes("netlify");
461
814
  var withNetlifyOutputAPI = (nitroConfig, workspaceRoot) => ({
462
815
  ...nitroConfig,