feat(platform)!: migrate to nitro/vite and split analog() into analog() + angular() + nitro()#2343
Conversation
Relocate the Analog-specific Nitro helpers and types into packages/platform/src/lib/nitro/ so @analogjs/platform owns them directly: - renderers (SSR/client renderer virtuals, server-fetch snippet) - get-page-handlers (.server.ts discovery → Nitro event handlers) - page-endpoints-plugin (Rollup transform for load/action) - get-content-files (content frontmatter discovery) - build-sitemap (sitemap.xml + hreflang helpers) - post-rendering-hook (Nitro prerender:generate wiring) - i18n-prerender (locale expansion + HTML lang injection) - debug instances under analog:nitro:* - types (Sitemap*, Prerender*, I18nPrerenderOptions, etc.) platform/lib/options.ts and platform/lib/utils/debug.ts now import from the local nitro/ folder. Adds the runtime deps the helpers require (ofetch, oxc-parser, xmlbuilder2). Refs #2035 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds packages/platform/src/lib/nitro/angular-linker-plugin.ts: a Rolldown transform that runs the Angular Linker (@angular/compiler-cli/linker/babel, gated by needsLinking()) against partially-compiled Angular npm packages. The plugin is intended to be wired into ssr.optimizeDeps.rolldownOptions.plugins so the SSR / nitro environment's dep optimizer converts ɵɵngDeclare* partial declarations into fully-compiled definitions. Without this, the server bundle would require JIT (eval) at runtime — forbidden on workerd / edge runtimes and unnecessary anywhere else. Lazily loads @babel/core + @angular/compiler-cli only on the first matching file so apps that never trigger the SSR optimizer don't pay the cost. Refs #2035 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…per)
Adds packages/platform/src/lib/nitro/analog-nitro-plugin.ts: the Vite
plugin with a .nitro = { setup } property that Nitro v3's first-party
Vite plugin (nitro/vite) picks up as a NitroModule.
config() hook contributes:
- experimental.vite.services.ssr.entry → synthetic absolute path that
the plugin's own resolveId/load resolves to a generated wrapper
module.
- environments.ssr.optimizeDeps.include — the five @angular/* packages
— plus rolldownOptions.plugins wiring in the linker from
#2035.
The wrapper imports the user's main.server.ts default export
(unchanged Angular renderer contract) and inlines the client
index.html as TEMPLATE, exposing { fetch(req): Response } so
nitro/vite's default SSR renderer (fetchViteEnv) works uniformly in
dev and prod.
nitro.setup(nitro) registers page handlers, scanDirs for src/server,
the page-endpoints Rollup plugin via rollup:before, prerender route
expansion (content dirs + i18n), post-rendering hooks, sitemap on
prerender:done, and HTML lang injection for i18n.
Refs #2035
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
platform-plugin.ts now imports nitro from 'nitro/vite' and the
analogNitroPlugin from the local nitro/ folder, replacing the
viteNitroPlugin call. The plugin chain returns:
[...nitro(nitroOptions), analogNitroPlugin({ ...platformOptions, nitro: nitroOptions }), ...]
The routeRules x-analog-no-ssr header injection continues to run
against nitroOptions before forwarding, so the generated SSR service
wrapper's noSSR branch keeps working for ssr: false routes.
Spec updated to mock nitro/vite and the analog-nitro-plugin module
directly.
Refs #2035
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…analogjs/platform The legacy 1735-line vite-plugin-nitro.ts orchestrator and its helpers (build-server, build-ssr, dev-server-plugin, node-web-bridge, register-dev-middleware, register-i18n-watcher, renderers, page endpoints, content discovery, sitemap, etc.) are all deleted. Their behavior now lives in @analogjs/platform's nitro/ module set, driven by Nitro v3's first-party nitro/vite plugin. vite-plugin-nitro is kept as a thin published package so existing dependency declarations still install, but src/index.ts is now an empty export with a @deprecated JSDoc directing consumers to migrate to @analogjs/platform. The ./internal sub-export (used only by the debug instances) is removed; nitro debug instances live in @analogjs/platform's nitro/debug. Drop @analogjs/vite-plugin-nitro from @analogjs/platform's runtime dependencies and resync the lockfile. BREAKING CHANGE: @analogjs/vite-plugin-nitro no longer exports a Vite plugin. Direct importers must migrate to @analogjs/platform, whose analog() factory composes nitro() from 'nitro/vite' with Analog's internal analogNitroPlugin. The ./internal sub-export (debugInstances) is also removed. Type exports (SitemapConfig, PrerenderRouteConfig, PrerenderContentDir, etc.) are now re-exported by @analogjs/platform. Closes #2035 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…alogNitroPlugin.setup
The mapValues() pre-processing in platform-plugin.ts that stamped
'x-analog-no-ssr: true' onto routeRules with 'ssr: false' moves into
analogNitroPlugin's nitro.setup(nitro) hook, where it mutates
nitro.options.routeRules in place. This lets the injection survive
whether the Nitro config came in via analog({ nitro: ... }) or directly
through a user-invoked nitro() call.
Drops the unused mapValues import; restores the analogNitroPlugin
call in the platform plugin chain so the .nitro module actually runs.
Refs #2035
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…entry
Promotes the previously-internal discoverLibraryRoutes helper to a
named export so workspace apps can compute additional page / content /
API directories once and feed them to both analog() and angular()
when the plugins are invoked separately.
Adds a small pageGlobs(dirs) helper that turns directories into
'${dir}/**/*.page.ts' globs, ready for angular({ include }).
Refs #2035
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
analog() no longer internally invokes @analogjs/vite-plugin-angular.
Apps now call angular() themselves alongside analog():
plugins: [analog(), angular(), nitro()]
Removed from analog()'s Options (hard-cut, not deprecated):
- vite (the passthrough to vite-plugin-angular)
- jit, disableTypeChecking, liveReload, inlineStylesExtension
- fileReplacements, fastCompile, fastCompileMode, include
- tailwindCss
- experimental.useAngularCompilationAPI
- experimental.stylePipeline.angularPlugins
Each plugin owns its own options. Pass debug to angular() to enable
analog:angular:* scopes — analog({ debug }) now controls
analog:platform:* and analog:nitro:* only.
depsPlugin no longer reads the vite/useAngularCompilationAPI flags:
it unconditionally excludes .ts/.js from Vite's built-in oxc transform
so vite-plugin-angular (when present) owns Angular file compilation.
Apps using an alternative compiler or compilation-API mode can
override the exclude in their own Vite config.
BREAKING CHANGE: The pass-through Angular options on analog() are
removed. Move them to angular() directly. The vite: false escape
hatch is no longer needed — drop angular() from your plugins array
to opt out of vite-plugin-angular.
Refs #2035
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
analog() no longer internally invokes nitro() from 'nitro/vite'. Apps
now call nitro() themselves alongside analog():
plugins: [analog(), angular(), nitro()]
The analogNitroPlugin's .nitro property is still discovered by
nitro/vite via flattenPlugins(userConfig.plugins), so Analog's Nitro
module (SSR wrapper, page handlers, prerender, sitemap) plugs into
whatever Nitro config the user gave nitro() directly.
Drops the 'nitro' option from analog()'s Options (hard-cut) and the
NitroConfig import. The previous routeRules x-analog-no-ssr header
injection already lives inside analogNitroPlugin.setup(nitro), so it
keeps working against the user-supplied nitro() config.
Also removes the now-unused externalPlugins helper.
BREAKING CHANGE: analog({ nitro: ... }) no longer accepts a NitroConfig.
Pass Nitro config directly to nitro() from 'nitro/vite' in the Vite
plugin array.
Refs #2035
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…log-app
Reshapes apps/analog-app/vite.config.ts to match the public API
introduced by the prior commits:
plugins: [analog(), angular(), nitro(), ...]
- analog() keeps file-routing/content/prerender/i18n options. Reads
discovered workspace libs via discoverLibraryRoutes() (now exported
from @analogjs/platform).
- angular() receives Angular-specific knobs directly: include
(workspace lib *.page.ts globs via pageGlobs()), fileReplacements,
inlineStylesExtension, fastCompile, workspaceRoot.
- nitro() receives routeRules. analogNitroPlugin (returned by
analog()) is discovered via plugin.nitro and stamps
'x-analog-no-ssr' on rules with 'ssr: false'.
Adds nitro: 'catalog:' to analog-app's package.json since the app now
imports { nitro } from 'nitro/vite' directly.
Refs #2035
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…Options.input nitro/vite's setupNitroContext (node_modules/nitro/dist/vite.mjs:710-734) only reads ssr service entries from two sources: 1. pluginConfig.experimental.vite.services on the nitro() call 2. userConfig.environments.ssr.build.rollupOptions.input as a fallback When analog() and nitro() are invoked separately, source #1 is empty (the user's nitro() pluginConfig doesn't know about Analog's wrapper), so source #2 is the only way our generated SSR service entry gets registered. Without it, nitro/vite never wires the ssr service, the ssr environment defaults to building index.html as an SSR input, and Vite throws 'rollupOptions.input should not be an html file when building for SSR'. analogNitroPlugin now sets the input alongside the existing experimental.vite.services.ssr.entry so it works whether nitro/vite reads services from its own pluginConfig or falls through to the environments fallback. Refs #2035 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…orchestration
@nx/vite:build iterates builder.environments and calls
builder.build(env) per environment, but it does not call
builder.buildApp(). nitro/vite's prerender + final nitro env build
orchestration lives inside the buildApp hook
(nitro/dist/vite.mjs:600-606 → buildEnvironments → prerender() + the
explicit builder.build(builder.environments.nitro) tail). Iterating
envs individually skips both, so only client + the ssr/nitro env
builds produce output — no prerender, no sitemap.
Switch analog-app's build target to nx:run-commands invoking
'vite build -c apps/analog-app/vite.config.ts'. The Vite CLI's
build path goes through buildApp, which triggers the full pipeline
(client → prerender → nitro env → close → writeBuildInfo).
Also drops the stale top-level build.outDir from vite.config.ts:
under nitro/vite the client output is owned by the client
environment's outDir which Nitro relocates to .output/public, so the
legacy '../../dist/apps/analog-app/client' override no longer
matches the active output path. Updates project.json outputs to
{workspaceRoot}/apps/analog-app/.output to match.
Refs #2035
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-entry.mjs Vite 8's server.fs is strict by default, allowing fs fallback reads only inside config.root. nitro/vite's env-runner worker imports its own runtime entry from node_modules/.pnpm/nitro@<hash>/.../dev-entry.mjs, which resolves through pnpm's content-addressable store at the workspace root — one level above the app's config.root. Vite's loadAndTransform sees a non-allowed absolute path, skips fs.readFile, and throws 'Failed to load url ... Does the file exist?' even though the file is right there on disk. Setting server.fs.allow to the workspace root in analog-app's vite config lets Vite's load fallback succeed for any pnpm path the env-runner reaches for. Refs #2035 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
srvx's toNodeHandler checks res instanceof Promise to decide whether to await the fetch handler's result. nitro/h3's dev fetch chain returns a Promise<Promise<Response>> in some paths, so the outer instanceof check awaits once and leaks the inner Promise through as the response. srvx's sendNodeResponse then throws a TypeError when it tries to spread Promise.prototype.headers (undefined, hence the webRes.headers is not iterable error). Patches the two call sites in srvx's adapters/node.mjs to recursively await when the resolved value is itself thenable. Backport of the fix from benpsnyder's PR on the same upstream issue. Refs #2035 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-template Nitro's resolveRendererOptions (nitro/dist/_chunks/nitro.mjs:296-311) auto-detects index.html and installs renderer-template[.dev] as renderer.handler. nitro/vite's configResolved branch that would swap in its SSR-dispatch renderer only runs when both renderer.handler and renderer.template are empty (nitro/dist/vite.mjs:574), which never holds for a typical app with an index.html at root. Result: every HTML request returns the raw template instead of routing through the SSR service. analogNitroPlugin now registers an explicit #analog/ssr-renderer virtual in nitro.options.virtual and sets renderer.handler to it. The handler short-circuits to the raw template when x-analog-no-ssr is on the response (set by injectAnalogRouteRuleHeaders from routeRules with ssr: false) and otherwise dispatches through fetchViteEnv to the SSR service env. The SSR-wrapper service entry keeps its own response shape; the no-ssr request-header check stays in place as a defense-in-depth path. Refs #2035 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… of fetchViteEnv nitro/vite's prodSetup polyfill (which populates globalThis.__nitro_vite_envs__ so fetchViteEnv can dispatch to the SSR service) is registered as a Vite plugin virtual and added to nitro.options.unenv.polyfill. That works for the Vite-built main bundle, but Nitro's prerender forces builder: 'rolldown' on its own Nitro instance, and the Rolldown build doesn't run Vite plugins. The nitro-vite-setup polyfill stays unresolved, the env-services global is never set, fetchViteEnv throws HTTPError 404, and every SSR route fails the prerender silently (Nitro reports 0 routes prerendered). Switch the renderer to a Nitro virtual indirection. analogNitroPlugin registers 'analog-ssr' as a function-valued nitro.options.virtual that resolves bundler-agnostically: - Dev: emits a thin fetch adapter that delegates to fetchViteEnv, since the SSR service module isn't on disk yet and nitro/vite's env runner is the dispatch path. - Build / prerender: scans the SSR services build dir for the entry built by nitro/vite's services pipeline and re-exports it directly. By the time Nitro resolves the virtual for the main bundle (built last) or for the prerender bundle (built after the main build completes), the SSR service file exists. The renderer virtual now imports from analog-ssr and calls its fetch method. No Vite-specific runtime globals; works in any bundler Nitro picks. Prerendered 6/6 routes in the analog-app verification. Refs #2035 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…non-SSR and edge cases Three small additions to analogNitroPlugin found while migrating the other demo apps: 1. Gate the analog-owned SSR renderer registration behind options.ssr. When ssr is false, leave Nitro's auto-detected template-serving renderer in place — serving the raw index.html for every HTML request is exactly the desired behavior, and our renderer virtual would otherwise try to dispatch to an SSR service that was never built. Lets tailwind-debug-app (ssr: false) build cleanly. 2. Carry over Analog's Nitro externals list. rxjs's facade subpaths confuse Nitro/Rolldown's resolver, node-fetch-native's polyfill rewrites global fetch, and sharp ships platform-specific binaries under @img/sharp-* whose unused symlinks crash Nitro's externals plugin with ENOENT during realpath(). All three were externalized by the legacy @analogjs/vite-plugin-nitro orchestrator; they need to be restored under the new plugin chain or blog-app's nitro env build fails on the sharp symlink walk. 3. Carry over the Nitro/Rolldown bundler-config sanitizer. Nitro's Rollup config sets output.codeSplitting (rejected as unknown by Rolldown), output.manualChunks (crashes Nitro's prerender rebundle), and a chunkFileNames function that emits route-derived [token] patterns Rollup/Rolldown treats as placeholders. The sanitizer strips the first two and rewrites non-standard [token] patterns to _token_. All three apply in the rollup:before hook on the resolved Nitro bundler config so they survive into both the main bundle and the prerender bundle. Refs #2035 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…plugin shape
Migrates opt-catchall-app, tanstack-query-app, blog-app, and
tailwind-debug-app to the public-API shape introduced earlier on this
branch:
plugins: [analog(), angular(), nitro(), ...]
Per app:
- vite.config.ts:
- Add explicit imports for @analogjs/vite-plugin-angular and
nitro from 'nitro/vite'.
- Drop the top-level build.outDir override that Nitro relocates
anyway under nitro/vite (.output/public).
- Add server.fs.allow pointing at the workspace root so Vite's fs
fallback can read pnpm content-hash paths reached by nitro/vite's
env-runner (dev-entry.mjs).
- Pull angular-specific options out of analog(): liveReload,
inlineStylesExtension, useAngularCompilationAPI, fileReplacements,
tailwindCss, fastCompile.
- Pull nitro-specific options out of analog(): routeRules,
nitro.prerender.*, nitro.experimental.websocket.
- analog() keeps the file-routing/content/prerender/i18n surface.
- project.json: switch the build target from @nx/vite:build to
nx:run-commands invoking 'vite build -c ...', so Vite's CLI
buildApp pipeline runs (nitro/vite's prerender + final nitro env
build orchestration lives in the buildApp hook). Update outputs to
apps/<app>/.output.
- package.json: add 'nitro: catalog:' to devDependencies for the
direct 'nitro/vite' import; add @analogjs/vite-plugin-angular where
it wasn't yet declared.
Builds verified:
- opt-catchall-app: server bundle only (no prerender configured)
- tanstack-query-app: server bundle only
- blog-app: 8 prerendered routes + sitemap, ssr-rendered HTML
- tailwind-debug-app: ssr: false, server bundle only (Nitro
auto-template-handler serves raw HTML)
Refs #2035
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Documents the v3 breaking change introduced earlier on this branch: - analog() no longer internally invokes @analogjs/vite-plugin-angular or nitro/vite; users call each plugin explicitly and pass each option to the plugin that now owns it. - @analogjs/vite-plugin-nitro is deprecated; the orchestration moved into @analogjs/platform. - Lists every option that moved off analog() and where it lives now. - Documents the discoverLibraryRoutes + pageGlobs helpers exported from @analogjs/platform for workspace-library include patterns. - Explains the Nx build target switch (@nx/vite:build cannot trigger nitro/vite's buildApp hook; nx:run-commands invoking vite build is required for prerender + final Nitro env build). - Notes the server.fs.allow workaround for Vite 8 strict fs in pnpm monorepos. Adds the corresponding bullets under the automated-migration notes. Refs #2035 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The v3 separated-plugin shape imports nitro from 'nitro/vite' directly in vite.config.ts, so scaffolded apps need nitro declared as a direct dependency. Pin to 3.0.260415-beta to match the workspace catalog version currently in use. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ngular + nitro)
Updates the latest, minimal, and blog templates so scaffolded apps
match the v3 separated-plugin shape:
- import angular from '@analogjs/vite-plugin-angular'
- import { nitro } from 'nitro/vite'
- plugins: [analog(), angular(), nitro()] (latest, blog)
- plugins: [analog({ ssr: false, ... }), angular(), nitro({ static: true })] (minimal)
Tailwind placeholder tokens (__TAILWIND_IMPORT__ / __TAILWIND_PLUGIN__)
and the blog content highlighter token (__CONTENT_HIGHLIGHTER__) are
preserved so create-analog's scaffolding substitution keeps working.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ration
Adds a lightweight schematic at version 3.0.0-alpha.56 that runs
during `ng update @analogjs/platform@latest`:
- Visits every vite.config.{ts,mts,js,mjs} in the workspace and
detects the legacy single-call analog() shape (default import from
@analogjs/platform + an analog() call + no companion import from
@analogjs/vite-plugin-angular and nitro/vite).
- If any are found:
- Adds @analogjs/vite-plugin-angular to devDependencies, matching
the existing @analogjs/platform pin so the angular plugin stays
on the same release line.
- Adds 'nitro: 3.0.260415-beta' to devDependencies — the version
the workspace catalog currently pins for nitro/vite.
- Schedules a package install task.
- Logs each detected file and a link to the v2-to-v3 migration
guide for the option-relocation step (which is too workspace-
specific to safely automate).
Doesn't rewrite vite.config sources. The option moves between
analog(), angular(), and nitro() are case-by-case enough that
forcing an automatic rewrite would either be very large (full AST
transform) or break common variations; pointing users at the docs is
the honest middle ground.
Built on @angular-devkit/schematics (not @nx/devkit) to match the
existing platform migration setup. Tests cover detection, dep
add/skip paths, dependencies-vs-devDependencies version lookup, the
node_modules skip, .mts discovery, and the path-prefix check that
keeps non-vite-config files from triggering the migration.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…vite + analog.nitro
The schematic now attempts a real source rewrite for the two
unambiguous option moves:
- `analog({ vite: VALUE })` -> `angular(VALUE)`
- `analog({ nitro: VALUE })` -> `nitro(VALUE)`
Implementation parses the vite.config file with the TypeScript compiler
to locate the single `analog(...)` call. If its argument is an object
literal containing `vite` and/or `nitro` properties, those property
values are extracted verbatim from the source (preserving original
formatting, comments, trailing commas), and the call is replaced with
three calls in sequence: a slim `analog({...remaining})`, then
`angular(VALUE)` and `nitro(VALUE)`. The companion imports for
`@analogjs/vite-plugin-angular` and `nitro/vite` are inserted right
after the `@analogjs/platform` import.
The transform is intentionally narrow:
- only fires when the file has exactly one `analog(...)` call,
- only fires when its argument is an object literal,
- only moves the two named keys.
When the file doesn't fit (no argument, variable argument, multiple
`analog()` calls, parse failure), the schematic falls back to the
previous behavior: log the file path and the migration-guide URL so
the user can move the options by hand. Either way, deps get added and
a package install is scheduled.
Six new tests cover the rewrite paths:
- `vite` lifted into a companion `angular(...)`
- `nitro` lifted into a companion `nitro(...)`
- both lifted while other analog options stay on `analog(...)`
- `analog()` with no argument: no rewrite, instructions logged
- `analog(opts)` where `opts` is a variable: no rewrite, instructions
logged
- existing `angular` import isn't duplicated
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… + nitro()
The schematic now rewrites every shape it can statically reason about:
- analog() (no argument) -> analog(), angular(), nitro()
- analog({ apiPrefix: 'api' }) -> analog({ apiPrefix: 'api' }), angular(), nitro()
- analog({ vite: V }) -> analog(), angular(V), nitro()
- analog({ nitro: N }) -> analog(), angular(), nitro(N)
- analog({ vite: V, nitro: N, ...rest }) -> analog({ ...rest }), angular(V), nitro(N)
Only analog(variable) and analog(someCall()) still bail out to the
logging-only fallback, since we can't statically split a value we
don't have an object literal for.
Two tests updated/added to cover the argumentless and
no-vite-no-nitro paths; full migration spec now at 15 tests, all
passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ess.cwd() The plugin previously defaulted workspaceRoot to process.cwd(), which is correct when the user runs vite build from the workspace root. But when the build runs from an app's own directory (or any other non-workspace cwd) inside a monorepo, workspace-relative paths (e.g. fileReplacements entries written as apps/<app>/src/...) fail to resolve. Check NX_WORKSPACE_ROOT before process.cwd() so the standard Nx invocation already has the right default even when cwd happens to be the app dir. The legacy single-call analog() did the same thing implicitly; this restores that behavior for users who now invoke angular() directly. User-supplied workspaceRoot still takes precedence, so anyone needing a specific value can override it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@analogjs/vite-plugin-angular now defaults workspaceRoot to NX_WORKSPACE_ROOT (and then process.cwd()), which is the right value for every flow these apps actually use: - pnpm nx build <app>: Nx sets NX_WORKSPACE_ROOT and runs from the workspace root, so the default resolves correctly with or without cwd being the workspace. - standalone scaffolded apps (templates): process.cwd() is the project root which IS the workspace root for those users. The explicit workspaceRoot: resolve(__dirname, '../..') override the migration originally added was defensive; remove it from each migrated app config now that the plugin's own default covers the same ground. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The patch was added to work around srvx's toNodeHandler awaiting only one level of promise, while nitro/h3's fetch chain at the time was returning Promise<Promise<Response>> through the SSR handler path. Subsequent SSR-renderer refactors on this branch removed the second promise layer: - 7b938f7 fix(platform): install analog-owned SSR renderer to bypass Nitro auto-template - 16e73ab fix(platform): use Nitro virtual indirection for SSR dispatch instead of fetchViteEnv - d9d842a fix(platform): gate SSR renderer + add Nitro externals The renderer virtual now returns service.fetch(event.req) directly, which is a single-level Promise<Response>; combined with h3 v2's recursive toResponse() unwrap, srvx's vanilla single-level await is sufficient. Verified by rebuilding analog-app against vanilla srvx@0.11.15 and hitting the production server on SSR routes, ssr: false routes, API JSON, and SSR catch-all 404s; all returned 200 with the expected hydration tokens.
Publish ./builders/vite and ./builders/vite-dev-server in the package exports map so packages that ship Angular Devkit Architect builders (@analogjs/platform) can land on a real, resolvable file via a thin re-export shim instead of a cross-package string redirect in builders.json. This mirrors @storybook/angular's pattern of exporting ./builders/build-storybook and ./builders/start-storybook.
…vite Replace the cross-package string redirect vite: '@analogjs/vite-plugin-angular:vite' in @analogjs/platform's builders.json with thin local Architect builders that import + re-export the real implementations from @analogjs/vite-plugin-angular/builders/vite (and /builders/vite-dev-server). The string-redirect form had two problems when invoked through Nx: 1. Architect resolves the redirect via package.json/builders.json walks starting from the root node_modules. The root-level pnpm copy of @analogjs/platform shadows the per-app workspace symlink, so changes to the source package didn't take effect until a fresh pnpm install. 2. The Nx 'vite' executor entry pointed at @nx/vite's viteBuildExecutor which doesn't call createBuilder().buildApp() and therefore never triggers Nitro's prerender + server pipeline. The shim pattern (mirroring @analogjs/storybook-angular's re-export of @storybook/angular/builders/build-storybook) keeps the user-facing @analogjs/platform:vite identifier stable while landing Architect on a real local file. The actual builder implementation continues to live in @analogjs/vite-plugin-angular. Also drops the dead Nx vite executor source and its build entry, and removes the 'vite' entry from the executors block of executors.json so Nx falls through to the builders block (architect compat) for the shim.
Switches the five demo apps (analog-app, blog-app, opt-catchall-app, tailwind-debug-app, tanstack-query-app) from the nx:run-commands shim that invoked 'vite build -c <config>' directly to the supported @analogjs/platform:vite executor. The platform builder forwards through a local Architect shim to the implementation in @analogjs/vite-plugin-angular, which calls createBuilder(cfg).buildApp() and drives the full Vite environment pipeline (client + ssr + nitro) plus Nitro prerender and sitemap. This is the executor identifier that platform's app generators emit for newly scaffolded apps, so the demo apps now match the shape an end user gets from `nx g @analogjs/platform:app`.
getPageHandlers hard-coded '/api' as the prefix for discovered
server-side page routes, and the analog-nitro-plugin's hasAPIDir check
hard-coded 'src/server/routes/api'. A user setting
`analog({ apiPrefix: 'rpc' })` got page endpoints mounted under
'/api/_analog/pages/...' regardless and the hasAPIDir probe missed
the existing 'src/server/routes/rpc/' directory.
Pass apiPrefix through to getPageHandlers and use it as both the
filesystem probe segment and the route prefix. Strips any user-supplied
leading slashes before composing the path so both 'api' and '/api'
forms produce the same output.
The /\/\((.*?)\)$/ replace only matched a single group at the end of the route path. Pages organized into Angular Router groups beyond the last segment (e.g. `(auth)/login.server.ts` or `(group)/users/[id].server.ts`) kept the literal parens in their mounted route — Nitro then registers an invalid path the runtime can't reach. Switch to a global regex matching any `/(group)` segment and rewrite it to /-group- per the docstring's example.
event.path?.startsWith(apiPrefix) matched any path that had apiPrefix as a literal prefix string, so a request to /apiary with apiPrefix of /api would be incorrectly routed through the API middleware (and have the leading /api stripped to produce a nonsensical 'ry' reqUrl). Require either an exact match or a / boundary after the prefix before treating the request as an API call.
The handler registered on `prerender:generate` was synchronous and swallowed the returned promise from each hook. Async post-rendering hooks (i18n route expansion, content-derived output writes, etc.) could race past the next prerender step, and any thrown error was silently lost because Nitro never saw a rejected promise from the hook callback. Mark the wrapper async and await each hook so errors propagate and `prerender:generate` blocks on completion. Widens the hook signature to `Promise<void> | void` so sync callers stay supported.
The previous /\/([^/.]+)(\.([^/.]+))?$/ split content file paths on
the FIRST dot, so a file named `post.en.md` was parsed as name='post'
and extension='en' (losing both the locale suffix and the actual
extension). Authors relying on locale-tagged filenames had their
suffixes silently dropped from the PrerenderContentFile metadata.
Switch to a basename + lastIndexOf('.') split so the trailing segment
becomes the extension and everything before it (inner dots included)
stays in name. `post.en.md` now yields name='post.en' and
extension='md'.
The route-skip guard checked `route.startsWith('/api/')`, which
matches every nested API path but lets the bare `/api` route fall
through into locale expansion. expandRoutesWithLocales would then emit
phantom `/en/api`, `/fr/api`, etc., that the user never intended
to prerender.
Treat `route === '/api'` as a sibling exact match alongside the
existing prefix check.
XMLBuilder is only used as a type annotation in build-sitemap.ts. Importing it as a runtime symbol from xmlbuilder2/lib/interfaces risks pulling in (or breaking on) the internal subpath at runtime in environments where bundlers don't tree-shake unused values. Switch to `import type` so the symbol is erased after compilation.
The dev-server builder's `port` option accepted any JSON `number`, so fractional values like 43000.5 passed schema validation and only failed later inside Node's net stack. Switch to `integer` and bound the range to the valid TCP port window (1-65535) so misconfigurations surface at validation time.
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
packages/platform/src/lib/nitro/analog-nitro-plugin.ts (1)
649-677:⚠️ Potential issue | 🟠 Major | ⚡ Quick winRestore
staticDataendpoint prerendering forPrerenderContentDirroutes.
collectRoutes()now restoresoutputSourceFile, but it still skips thestaticDataexpansion in thecontentDirbranch. That drops/<apiPrefix>/_analog/pages/...prerenders for transformed content routes and breaks parity with prior behavior.Proposed fix
for (const file of files) { const route = dir.transform(file); if (route === false) continue; out.push(route); @@ if (dir.outputSourceFile) { const sourceContent = dir.outputSourceFile(file); if (typeof sourceContent === 'string') { routeSourceFiles[route] = sourceContent; } } + if (dir.staticData) { + const prefix = apiPrefix.startsWith('/') ? apiPrefix : `/${apiPrefix}`; + const normalizedRoute = route.startsWith('/') ? route : `/${route}`; + out.push(`${prefix}/_analog/pages${normalizedRoute}`); + } }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/platform/src/lib/nitro/analog-nitro-plugin.ts` around lines 649 - 677, In collectRoutes (analog-nitro-plugin.ts) the PrerenderContentDir branch processes transform(), sitemaps and outputSourceFile but does not add the corresponding staticData prerender routes (the /<apiPrefix>/_analog/pages/... endpoints), causing missing prerenders; update the 'contentDir' branch (PrerenderContentDir handling) to mirror the staticData expansion logic used elsewhere: after computing route and routeSourceFiles[route], also push the generated staticData route(s) into out (using the same route -> static data endpoint naming as the rest of collectRoutes) and include any associated routeSourceFiles/sitemaps entries the same way so transformed content routes get their /_analog/pages/* prerenders restored.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/platform/src/lib/nitro/i18n-prerender.ts`:
- Around line 24-28: The route-exclusion predicate in i18n-prerender.ts
currently hardcodes '/api'; update it to honor the configured apiPrefix by
changing the predicate to check route === apiPrefix ||
route.startsWith(apiPrefix + '/') and by accepting an apiPrefix parameter into
the function (or into wirePrerender's internal call) so the correct prefix is
used; update the caller (wirePrerender) to pass the configured apiPrefix through
to this check so custom API prefixes (e.g., '/rpc') are excluded from locale
expansion.
---
Duplicate comments:
In `@packages/platform/src/lib/nitro/analog-nitro-plugin.ts`:
- Around line 649-677: In collectRoutes (analog-nitro-plugin.ts) the
PrerenderContentDir branch processes transform(), sitemaps and outputSourceFile
but does not add the corresponding staticData prerender routes (the
/<apiPrefix>/_analog/pages/... endpoints), causing missing prerenders; update
the 'contentDir' branch (PrerenderContentDir handling) to mirror the staticData
expansion logic used elsewhere: after computing route and
routeSourceFiles[route], also push the generated staticData route(s) into out
(using the same route -> static data endpoint naming as the rest of
collectRoutes) and include any associated routeSourceFiles/sitemaps entries the
same way so transformed content routes get their /_analog/pages/* prerenders
restored.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 6c0ef65c-acd9-421c-a5dc-6d94f4901b81
📒 Files selected for processing (8)
packages/nx-plugin/src/builders/vite-dev-server/schema.jsonpackages/platform/src/lib/nitro/analog-nitro-plugin.tspackages/platform/src/lib/nitro/build-sitemap.tspackages/platform/src/lib/nitro/get-content-files.tspackages/platform/src/lib/nitro/get-page-handlers.tspackages/platform/src/lib/nitro/i18n-prerender.tspackages/platform/src/lib/nitro/post-rendering-hook.tspackages/platform/src/lib/nitro/renderers.ts
| if ( | ||
| route.includes('/_analog/') || | ||
| route === '/api' || | ||
| route.startsWith('/api/') | ||
| ) { |
There was a problem hiding this comment.
Make API-route exclusion honor configured apiPrefix, not hardcoded /api.
This predicate still hardcodes /api. For custom prefixes, API routes may be incorrectly locale-expanded (e.g., /rpc/... becoming /en/rpc/...).
Proposed direction
-export function expandRoutesWithLocales(
+export function expandRoutesWithLocales(
routes: string[],
i18n: I18nPrerenderOptions,
+ apiPrefix = 'api',
): string[] {
+ const normalizedApiPrefix = apiPrefix.startsWith('/')
+ ? apiPrefix
+ : `/${apiPrefix}`;
@@
if (
route.includes('/_analog/') ||
- route === '/api' ||
- route.startsWith('/api/')
+ route === normalizedApiPrefix ||
+ route.startsWith(`${normalizedApiPrefix}/`)
) {…and pass apiPrefix from the wirePrerender() caller.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/platform/src/lib/nitro/i18n-prerender.ts` around lines 24 - 28, The
route-exclusion predicate in i18n-prerender.ts currently hardcodes '/api';
update it to honor the configured apiPrefix by changing the predicate to check
route === apiPrefix || route.startsWith(apiPrefix + '/') and by accepting an
apiPrefix parameter into the function (or into wirePrerender's internal call) so
the correct prefix is used; update the caller (wirePrerender) to pass the
configured apiPrefix through to this check so custom API prefixes (e.g., '/rpc')
are excluded from locale expansion.
The migration to nitro/vite dropped two pieces of behavior that @analogjs/vite-plugin-nitro shipped: 1. Vercel CI auto-detection. The legacy plugin promoted process.env.VERCEL (set on every Vercel build) and the BUILD_PRESET env var into the Nitro preset. Nitro v3 only reads NITRO_PRESET / SERVER_PRESET, so users upgrading kept hitting the default node-server preset on Vercel deployments and Vercel CLI refused to deploy what looked like a non-Vercel build. 2. Vercel runtime defaults. entryFormat: 'node' and functions.runtime: 'nodejs24.x' were applied so the emitted Build Output API config targets the supported Node runtime tier. Bridge BUILD_PRESET -> NITRO_PRESET and VERCEL -> 'vercel' in the config() hook (early enough that Nitro's createNitro() picks the preset up), and apply the runtime defaults in setup() when Vercel is active. Skip the legacy dist/<rootDir>/analog/ output override for Vercel — Nitro's preset owns the Build Output API layout and overriding only output.dir left the functions/ subtree at Nitro's project-root default while static files moved, splitting the deploy across two trees and breaking Vercel's discovery. Default (non-Vercel) builds still use the legacy dist/<rootDir>/analog/ paths.
Commit 1a59b6a moved the vite executor from the dead Nx-executor forward at src/executors/vite/vite.impl.ts to the architect-builder shims under src/builders/vite/ and src/builders/vite-dev-server/, but the post-build artifact-verification list at tools/scripts/verify-package-artifacts.mts still required the legacy executors/vite/vite.impl.js path. CI's prepare step calls verify-package-artifacts.mts platform and fails with a missing- artifact error pointing at the deleted path. Replace the stale entry with the two shim outputs that actually ship in the dist tree.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/platform/src/lib/nitro/analog-nitro-plugin.ts`:
- Around line 238-240: The isVercel detection currently treats any VERCEL env
var as true even when an explicit Nitro preset is set; change the logic so an
explicit nitro.options.preset always wins: compute the preset
(nitro.options.preset) first and only consider process.env['VERCEL'] when the
preset is empty/undefined, i.e. make isVercel true if the preset contains
'vercel' OR if there is no preset and VERCEL is present (use the existing
identifiers nitro.options.preset, isVercel, and process.env['VERCEL']).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 3d80f0f2-1342-4278-bea6-dc78b8466827
📒 Files selected for processing (1)
packages/platform/src/lib/nitro/analog-nitro-plugin.ts
The prepare script chained: git config core.hookspath .githooks || true && node tools/scripts/build-release.mts cmd.exe doesn't have a true builtin. On Windows the chain evaluates as (git || true) && node, where the unknown true command returns 9009, the OR short-circuits to that exit, and the AND that follows never runs node. Result: the script prints Done in ~40 ms with no work performed, the platform dist never gets built during pnpm install, and subsequent pnpm build invocations fail with 'Cannot find module .../platform/dist/src/lib/nx-plugin/executors.json' when Nx tries to resolve @analogjs/platform:vite from project.json. Move the git-config call into build-release.mts inside a try/catch so it runs cross-platform and a missing-git environment is a soft skip. Simplify prepare to a single node invocation.
…atic oxlint's prefer-const rule flags the analogCalls binding in migrate-to-separated-plugins.ts: the array is mutated via push() but the binding itself is never reassigned. CI's Linux / Lint job fails with one extra error against this PR vs origin/alpha because of it. Switch to const so the lint count matches the baseline upstream.
…inal
The filter that strips analog plugins from the user's vite.config
called .includes() on every plugin's name unconditionally:
config.plugins.flat().filter(p => !p.name.includes('analogjs'))
A plugin with an undefined name (or any falsy entry slipping through
the user's plugins array) throws:
TypeError: Cannot read properties of undefined (reading 'includes')
which fails build-storybook with 'Broken build, fix the error above'.
Use optional chaining on both the plugin entry and its name so
unnamed/falsy plugins fall through untouched and only entries with a
name explicitly containing 'analogjs' are dropped.
This reverts commit 0e1c47d.
Analog v3 no longer installs the legacy /api/** -> /** proxy routeRule (the @analogjs/vite-plugin-nitro fallback for projects without a routes/api/ directory). blog-app's e2e expects /api/rss.xml to serve the RSS feed and /api/v1/* for OG image helpers, but its routes lived directly at routes/rss.xml.ts and routes/v1/, which Nitro mounted at /rss.xml and /v1/* under v3. Move both into routes/api/ so they land at the URL paths the app's own client + e2e tests reference.
Angular's HttpClient (via withFetch) and Analog's injectLoad() issue HTTP calls during SSR/prerender to addresses like http://localhost/api/_analog/pages/... When the SSR service wrapper didn't pass a fetch implementation to the renderer, those calls fell through to the default global fetch — which ECONNREFUSEd during prerender because no socket is listening, and on a live server hit the loopback instead of the in-process Nitro pipeline. The legacy @analogjs/vite-plugin-nitro renderer constructed a request-scoped fetch with h3's fetchWithEvent and passed it as `fetch` into Angular's renderApplication. The new two-env split (renderer virtual in Nitro env, SSR service in Vite env) lost that wiring because fetchWithEvent needs the live h3 event which can't cross env-runner boundaries. Use Nitro v3's nitro/app serverFetch instead — it does an in-process fetch through useNitroApp().fetch without needing the originating event. Build the fetch in the SSR service wrapper and pass it as `fetch` into the renderer call. Relative URLs get the http://localhost host prefix Nitro's Request constructor needs. Restores route-level SSR output: prerendered pages now carry the full Angular-rendered router-outlet content instead of just the shell with the top bar.
… fetch in ofetch
Two related bugs in the nitro/vite migration's preset handling and SSR
fetch wiring broke Netlify deployments:
1. The setup() output-path override unconditionally redirected every
non-Vercel preset to dist/<rootDir>/analog/, clobbering Netlify's
{{rootDir}}/.netlify/functions-internal/ path. Netlify deploys 404'd
on both functions and assets because the artifacts never landed where
the platform expected them.
2. The SSR entry wrapper passed a plain fetch as the renderer's fetch
option, which becomes INTERNAL_FETCH. The router's request-context
interceptor short-circuits SSR HttpClient calls through
serverFetch.raw(...) — an ofetch API. Plain fetch lacks .raw, so
every SSR data load threw TypeError during prerender.
Changes:
- Bridge NETLIFY and CF_PAGES env vars into NITRO_PRESET alongside the
existing VERCEL bridge.
- Make the output-path override preset-aware: only apply the legacy
dist/<rootDir>/analog/ paths for the default node-server preset.
Managed presets (Vercel, Netlify, Cloudflare, ...) use their own
layouts.
- For Netlify, hoist functions to <workspaceRoot>/.netlify/functions-internal/
so the deploy auto-discovers them; keep publicDir at
dist/<rootDir>/analog/public/ for netlify.toml publish wiring.
- Wrap nitroServerFetch in ofetch's createFetch in the SSR entry wrapper
and pass that as the renderer fetch, so INTERNAL_FETCH has .raw.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The /app subpath creates a fresh `useNitroApp()` instance scoped to the
importing bundle. In the SSR vite bundle that has no registered route
handlers, so every page-endpoint fetch from injectLoad() 404'd during
prerender (`FetchError: [GET] "/api/_analog/pages/-home-": 404").
The root `nitro` entry's serverFetch reads
`globalThis.__nitro__.{default,prerender}` instead, which the
surrounding Nitro server (prerender pass or production runtime) has
already populated with the real app + handlers. Page-endpoints now
resolve correctly during prerender.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e root
Nitro's Vercel and Cloudflare presets anchor their output paths at
`{{rootDir}}`, which for Nx monorepos lands deployment artifacts under
`apps/<name>/` where the deploy CLIs (`vercel build`, `wrangler pages
deploy`) can't auto-discover them. Same shape as the Netlify issue
already fixed; extend the hoist:
- Vercel: `<workspaceRoot>/.vercel/output/{functions/__server.func,static}/`
so `vercel build` at the repo root finds the Build Output API tree.
- Cloudflare Pages/Workers: `<workspaceRoot>/dist/<rootDir>/` with
`_worker.js/` alongside static assets, so `wrangler pages deploy
dist/<rootDir>` from the workspace root works.
Verified each preset emits to the expected workspace-root layout with
`BUILD_PRESET=<preset> nx build analog-app`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Nitro v3's prerender writer compares filePath.startsWith(publicDir), but filePath is built via node:path.join (platform-native separators on Windows) while publicDir comes from resolveNitroPath which always returns forward-slash paths via pathe. On Windows the two sides disagree and every prerendered route is marked (skipped) — the CI 'more dist\apps\blog-app\analog\public\index.html' verify step then fails because no HTML was written. Hook prerender:route to detect the skip-after-generate case (skip=true with non-empty data) and write the file ourselves under the override publicDir, then clear the skip flag so the prerender count is honest. Idempotent across the two-fire prerender:route hook contract. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ve SSR query string
Two distinct SSR regressions in this PR's nitro/vite migration:
1) /assets/<file> 404'd during SSR / fell through to the catch-all
SSR renderer, leaking HTML where consumers (router request-context
interceptor, page `HttpClient.get` calls) expected JSON or a real
asset. nitro/vite turns off Vite's `build.copyPublicDir`
(vite.mjs:248) and expects Nitro to manage public assets, but it
doesn't auto-bridge the user's `publicDir` setting. Capture
`userConfig.publicDir` in `config()` and register it as a Nitro
`publicAssets` entry in `setup()`. Also bridge `output.publicDir`
into the nested prerender Nitro via the `prerender:config` hook
so the prerender's asset manifest scan finds the same files.
2) URL query string was being dropped before the Angular renderer,
so `route.queryParams` was always empty and
`definePageLoad({ query })` saw nothing. Renderer was called with
bare `requestPath`; pass `requestUrl` (path+search) instead.
Verified: `Hello, ANALOG!` now renders for `/greet/analog?shout=true`
and `/assets/shipping.json` is no longer caught by the SSR fallback
during prerender. e2e count drops by 1 (16 → 15 failing); remaining
failures are client-side hydration/interactivity issues unrelated to
SSR routing or static assets.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…) + angular() + nitro() (#2343) Closes #2035 Closes #2188 BREAKING CHANGE: `@analogjs/vite-plugin-nitro` → `nitro/vite` `@analogjs/platform`'s `analog()` now bundles Nitro v3's first-party `nitro/vite` plugin instead of `@analogjs/vite-plugin-nitro`. Standalone users (those who used `@analogjs/vite-plugin-nitro` directly) must install `nitro` as a dev dep and add `nitro()` to their Vite plugin chain alongside `analog()`. BEFORE: ```ts // vite.config.ts import analog from '@analogjs/platform'; import { defineConfig } from 'vite'; export default defineConfig({ plugins: [analog({ /* ... */ })], }); ``` AFTER: ```ts // vite.config.ts import analog from '@analogjs/platform'; import angular from '@analogjs/vite-plugin-angular'; import { nitro } from 'nitro/vite'; import { defineConfig } from 'vite'; export default defineConfig({ plugins: [ analog({ /* ... */ }), angular({ /* ... vite object */ }), nitro({ /* ... nitro object*/ }) ], }); ```
PR Checklist
Migrates Analog to Nitro v3's first-party Vite plugin (
nitro/vite) and reshapes@analogjs/platform's plugin surface.analog()no longer composes@analogjs/vite-plugin-angularor Nitro internally — users wireanalog() + angular() + nitro()as three explicit plugins.Closes #2035
Affected scope
platformvite-plugin-angular,vite-plugin-nitro,create-analogRecommended merge strategy for maintainer [optional]
What is the new behavior?
Plugin chain is now explicit (breaking)
analog()used to invoke@analogjs/vite-plugin-angularand@analogjs/vite-plugin-nitrointernally. v3 splits the chain into three plugins users compose themselves:@analogjs/platformowns its own Nitro orchestration throughnitro/vitedirectly (via a newanalogNitroPluginregistered as a Nitro module).@analogjs/vite-plugin-nitrocontinues to ship as a standalone package (orchestration helpers were copied into platform — the two evolve independently).Architect builder shims for the platform vite executor
@analogjs/vite-plugin-angularpublishes./builders/viteand./builders/vite-dev-serversubpath exports.@analogjs/platformships local Architect-builder shims that import + re-export from those subpaths (storybook-angular pattern). Drops thevite: "@analogjs/vite-plugin-angular:vite"string redirect frombuilders.jsonso Architect lands on a real local file on first lookup.viteexecutor that forwarded to@nx/vite'sviteBuildExecutor(it never calledbuildApp()).Output layout preserved
Nitro's final output paths are pinned in
analogNitroPlugin.setup()to the legacydist/<rootDir>/analog/{server,public,nitro.json}layout for build only. Dev mode keeps Nitro defaults so the dev server'sreadAssetdoesn't crash on the unbuiltdist/.buildDirstays at Nitro's default so Rolldown's prerender rebundle finds workspace deps innode_modules/.Platform owns the
server.fs.allowworkaroundVite 8's strict fs would otherwise reject
nitro/vite's env-runner load of its owndev-entry.mjs(a pnpm content-hash path).analogNitroPlugin.config()now contributesserver.fs.allow: [workspaceRoot]so uservite.config.tsfiles don't carry the workaround.ng-update schematic for plugin separation
@analogjs/platform's ng-update migrationmigrate-to-separated-plugins:analog({ ... })shape via TypeScript AST traversal.analog({ vite: {...}, nitro: {...}, ... })intoanalog({...}), angular({...}), nitro({...}), liftingvite/nitrokeys into their owning plugin while preserving remaininganalogoptions.analog()intoanalog(), angular(), nitro().@analogjs/vite-plugin-angulartodevDependencies(version mirrors@analogjs/platform's pin).Templates updated
create-analog'slatest,blog, andminimaltemplates use the separated plugin shape.nitrois injected only for pnpm scaffolds (addPnpmDependencies) since npm/yarn auto-hoist it as a transitive of@analogjs/platform; pnpm's strict isolation needs the top-level declaration.Other notable changes
analog-app,blog-app,opt-catchall-app,tailwind-debug-app,tanstack-query-app) all on@analogjs/platform:vitebuilder.discoverLibraryRoutes+pageGlobshelpers re-exported from@analogjs/platform(single call, fed to bothanalog()andangular()).Options.useAPIMiddlewareandOptions.ssrBuildDirdropped — both were unused; the api-middleware fallback is being removed in v3 anyway.apps/docs-app/docs/guides/migrating-v2-to-v3.md.nitropin bumped to3.0.260522-beta(latest beta).server.fs.allowworkaround removed;experimental.websocketflag gated behindprocess.env.VITESTintailwind-debug-appso Vitest run doesn't crash.Test plan
nx format:checkpnpm build(22/22 projects)pnpm test(18/18 projects)pnpm nx build analog-appemitsdist/apps/analog-app/analog/server/index.mjs,nitro.json, and prerendered HTMLnode dist/apps/analog-app/analog/server/index.mjsserves SSR routes with hydration tokens, API routes with JSON, andssr: falseroutes with the raw templatepnpm nx serve analog-appboots cleanly without user-sideserver.fs.allowmigrate-to-separated-pluginsschematic: 15/15 unit tests passDoes this PR introduce a breaking change?
Breaking changes (all documented in
docs/guides/migrating-v2-to-v3.mdwith the schematic covering most cases):analog()no longer composesangular()ornitro()— users must call all three.@analogjs/platformv3 no longer composes@analogjs/vite-plugin-nitrointernally (vpn keeps shipping standalone).Options.useAPIMiddlewareandOptions.ssrBuildDirremoved.Other information
alpha.🤖 Generated with Claude Code