From 9baaf790539f8d020901dfefcc658587d3a1bdf1 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Fri, 22 May 2026 21:54:04 -0500 Subject: [PATCH 01/65] feat(platform): move Nitro orchestration helpers from vite-plugin-nitro MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 analogjs/analog#2035 Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/platform/package.json | 5 +- .../src/lib/nitro/build-sitemap.spec.ts | 222 +++++++++++ .../platform/src/lib/nitro/build-sitemap.ts | 372 ++++++++++++++++++ packages/platform/src/lib/nitro/debug.ts | 8 + .../src/lib/nitro/get-content-files.spec.ts | 77 ++++ .../src/lib/nitro/get-content-files.ts | 74 ++++ .../src/lib/nitro/get-page-handlers.ts | 66 ++++ .../src/lib/nitro/i18n-prerender.spec.ts | 186 +++++++++ .../platform/src/lib/nitro/i18n-prerender.ts | 93 +++++ .../lib/nitro/page-endpoints-plugin.spec.ts | 82 ++++ .../src/lib/nitro/page-endpoints-plugin.ts | 98 +++++ .../src/lib/nitro/post-rendering-hook.spec.ts | 33 ++ .../src/lib/nitro/post-rendering-hook.ts | 12 + .../platform/src/lib/nitro/renderers.spec.ts | 41 ++ packages/platform/src/lib/nitro/renderers.ts | 141 +++++++ packages/platform/src/lib/nitro/types.ts | 165 ++++++++ packages/platform/src/lib/options.ts | 2 +- packages/platform/src/lib/utils/debug.spec.ts | 4 +- packages/platform/src/lib/utils/debug.ts | 2 +- 19 files changed, 1678 insertions(+), 5 deletions(-) create mode 100644 packages/platform/src/lib/nitro/build-sitemap.spec.ts create mode 100644 packages/platform/src/lib/nitro/build-sitemap.ts create mode 100644 packages/platform/src/lib/nitro/debug.ts create mode 100644 packages/platform/src/lib/nitro/get-content-files.spec.ts create mode 100644 packages/platform/src/lib/nitro/get-content-files.ts create mode 100644 packages/platform/src/lib/nitro/get-page-handlers.ts create mode 100644 packages/platform/src/lib/nitro/i18n-prerender.spec.ts create mode 100644 packages/platform/src/lib/nitro/i18n-prerender.ts create mode 100644 packages/platform/src/lib/nitro/page-endpoints-plugin.spec.ts create mode 100644 packages/platform/src/lib/nitro/page-endpoints-plugin.ts create mode 100644 packages/platform/src/lib/nitro/post-rendering-hook.spec.ts create mode 100644 packages/platform/src/lib/nitro/post-rendering-hook.ts create mode 100644 packages/platform/src/lib/nitro/renderers.spec.ts create mode 100644 packages/platform/src/lib/nitro/renderers.ts create mode 100644 packages/platform/src/lib/nitro/types.ts diff --git a/packages/platform/package.json b/packages/platform/package.json index b7e436cba..b8749e549 100644 --- a/packages/platform/package.json +++ b/packages/platform/package.json @@ -47,9 +47,12 @@ "nitro": "catalog:", "@analogjs/vite-plugin-angular": "workspace:*", "@analogjs/vite-plugin-nitro": "workspace:*", + "ofetch": "catalog:", + "oxc-parser": "catalog:", "rolldown": "catalog:", "obug": "catalog:", - "vitefu": "catalog:" + "vitefu": "catalog:", + "xmlbuilder2": "catalog:" }, "peerDependencies": { "@nx/angular": "catalog:peerCompat", diff --git a/packages/platform/src/lib/nitro/build-sitemap.spec.ts b/packages/platform/src/lib/nitro/build-sitemap.spec.ts new file mode 100644 index 000000000..5d16b7477 --- /dev/null +++ b/packages/platform/src/lib/nitro/build-sitemap.spec.ts @@ -0,0 +1,222 @@ +import { existsSync, mkdirSync, writeFileSync } from 'node:fs'; +import { buildSitemap } from './build-sitemap'; + +vi.mock('node:fs', () => ({ + existsSync: vi.fn(() => true), + mkdirSync: vi.fn(), + writeFileSync: vi.fn(), +})); + +describe('build sitemap', () => { + const config = { root: 'root' }; + const existsSyncMock = vi.mocked(existsSync); + const mkdirSyncMock = vi.mocked(mkdirSync); + const writeFileSyncMock = vi.mocked(writeFileSync); + + afterEach(() => { + vi.restoreAllMocks(); + existsSyncMock.mockReturnValue(true); + mkdirSyncMock.mockReset(); + writeFileSyncMock.mockReset(); + }); + + it('should not perform functionality if no predefined routes are present', async () => { + await buildSitemap(config, { host: 'https://host.com' }, [], '', {}); + + expect(writeFileSyncMock).not.toHaveBeenCalled(); + }); + + it('should preserve route sitemap metadata when the host has a trailing slash', async () => { + existsSyncMock.mockReturnValue(true); + + await buildSitemap( + config, + { host: 'https://host.com/' }, + ['/blog'], + '/tmp/analog/public', + { + '/blog': { + lastmod: '2024-01-15', + changefreq: 'weekly', + priority: 0.8, + }, + }, + ); + + expect(writeFileSyncMock).toHaveBeenCalledWith( + expect.stringContaining('/tmp/analog/public/sitemap.xml'), + expect.stringContaining('https://host.com/blog'), + ); + expect(writeFileSyncMock.mock.calls[0]?.[1]).toContain( + '2024-01-15', + ); + expect(writeFileSyncMock.mock.calls[0]?.[1]).toContain( + 'weekly', + ); + expect(writeFileSyncMock.mock.calls[0]?.[1]).toContain( + '0.8', + ); + }); + + it('should apply include defaults transform exclude and internal route filtering', async () => { + existsSyncMock.mockReturnValue(true); + + await buildSitemap( + config, + { + host: 'https://host.com', + defaults: { + changefreq: 'monthly', + priority: 0.4, + }, + include: async () => [ + '/extra', + { + route: '/docs/hello world', + lastmod: '2024-01-01', + }, + ], + exclude: ['/drafts/**', /^\/admin/], + transform: (entry) => + entry.route === '/extra' + ? { + route: '/extra-updated', + priority: 0.9, + } + : { + route: entry.route, + }, + }, + [ + '/products', + '/products', + '/drafts/preview', + '/api/_analog/pages/products', + ], + '/tmp/analog/public', + {}, + ); + + const xml = writeFileSyncMock.mock.calls[0]?.[1] ?? ''; + expect(xml).toContain('https://host.com/products'); + expect(xml).toContain('monthly'); + expect(xml).toContain('0.4'); + expect(xml).toContain('https://host.com/extra-updated'); + expect(xml).toContain('0.9'); + expect(xml).toContain('https://host.com/docs/hello%20world'); + expect(xml).not.toContain('/drafts/preview'); + expect(xml).not.toContain('/api/_analog/pages/products'); + }); + + it('should support predicate exclude rules and transform returning false', async () => { + existsSyncMock.mockReturnValue(true); + + await buildSitemap( + config, + { + host: 'https://host.com', + exclude: [async (entry) => entry.route === '/private'], + transform: (entry) => + entry.route === '/skip-me' + ? false + : { + route: entry.route, + }, + }, + ['/public', '/private', '/skip-me'], + '/tmp/analog/public', + {}, + ); + + const xml = writeFileSyncMock.mock.calls[0]?.[1] ?? ''; + expect(xml).toContain('https://host.com/public'); + expect(xml).not.toContain('/private'); + expect(xml).not.toContain('/skip-me'); + expect(xml).not.toContain(''); + }); + + it('should resolve callable per-route sitemap metadata', async () => { + existsSyncMock.mockReturnValue(true); + + await buildSitemap( + config, + { host: 'https://host.com/' }, + ['/blog'], + '/tmp/analog/public', + { + '/blog': () => ({ + lastmod: '2024-04-01', + changefreq: 'daily', + }), + }, + ); + + const xml = writeFileSyncMock.mock.calls[0]?.[1] ?? ''; + expect(xml).toContain('https://host.com/blog'); + expect(xml).toContain('2024-04-01'); + expect(xml).toContain('daily'); + }); + + it('should filter internal routes when a custom api prefix is configured', async () => { + existsSyncMock.mockReturnValue(true); + + await buildSitemap( + config, + { host: 'https://host.com' }, + ['/shop', '/functions/_analog/pages/shop'], + '/tmp/analog/public', + {}, + { apiPrefix: 'functions' }, + ); + + const xml = writeFileSyncMock.mock.calls[0]?.[1] ?? ''; + expect(xml).toContain('https://host.com/shop'); + expect(xml).not.toContain('/functions/_analog/pages/shop'); + }); + + it('should create the output directory when it does not exist', async () => { + existsSyncMock.mockReturnValue(false); + + await buildSitemap( + config, + { host: 'https://host.com' }, + ['/'], + '/tmp/generated/public', + {}, + ); + + expect(mkdirSyncMock).toHaveBeenCalledWith('/tmp/generated/public', { + recursive: true, + }); + expect(writeFileSyncMock).toHaveBeenCalled(); + }); + + it('should refuse to write to the current working directory', async () => { + existsSyncMock.mockReturnValue(true); + const errorSpy = vi + .spyOn(console, 'error') + .mockImplementation(() => undefined); + + await buildSitemap(config, { host: 'https://host.com' }, ['/'], '', {}); + + expect(writeFileSyncMock).not.toHaveBeenCalled(); + expect(errorSpy).toHaveBeenCalledWith( + expect.stringContaining('Unable to write file at'), + expect.any(Error), + ); + }); + + it('should reject invalid sitemap hosts before writing output', async () => { + await expect( + buildSitemap( + config, + { host: 'not-a-valid-url' }, + ['/'], + '/tmp/analog/public', + {}, + ), + ).rejects.toThrow(); + + expect(writeFileSyncMock).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/platform/src/lib/nitro/build-sitemap.ts b/packages/platform/src/lib/nitro/build-sitemap.ts new file mode 100644 index 000000000..9c8282d8b --- /dev/null +++ b/packages/platform/src/lib/nitro/build-sitemap.ts @@ -0,0 +1,372 @@ +import { existsSync, mkdirSync, writeFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { create } from 'xmlbuilder2'; +import { XMLBuilder } from 'xmlbuilder2/lib/interfaces'; +import { UserConfig } from 'vite'; +import type { + I18nPrerenderOptions, + PrerenderSitemapConfig, + SitemapConfig, + SitemapEntry, + SitemapExcludeRule, + SitemapRouteDefinition, + SitemapRouteInput, + SitemapRouteSource, +} from './types.js'; + +type RouteSitemapConfig = + | PrerenderSitemapConfig + | (() => PrerenderSitemapConfig) + | undefined; + +export type PagesJson = SitemapEntry; + +export interface BuildSitemapOptions { + apiPrefix?: string; +} + +export async function buildSitemap( + _config: UserConfig, + sitemapConfig: SitemapConfig, + routes: (string | undefined)[] | (() => Promise<(string | undefined)[]>), + outputDir: string, + routeSitemaps: Record, + buildOptions: BuildSitemapOptions = {}, +): Promise { + const host = normalizeSitemapHost(sitemapConfig.host); + const routeList = await collectSitemapRoutes(routes, sitemapConfig.include); + const sitemapData = await resolveSitemapEntries( + routeList, + host, + routeSitemaps, + sitemapConfig, + buildOptions, + ); + + if (!sitemapData.length) { + return; + } + + const sitemap = createXml('urlset'); + + for (const item of sitemapData) { + const page = sitemap.ele('url'); + page.ele('loc').txt(item.loc); + + if (item.lastmod) { + page.ele('lastmod').txt(item.lastmod); + } + + if (item.changefreq) { + page.ele('changefreq').txt(item.changefreq); + } + + if (item.priority !== undefined) { + page.ele('priority').txt(String(item.priority)); + } + } + + const resolvedOutputDir = resolve(outputDir); + const mapPath = resolve(resolvedOutputDir, 'sitemap.xml'); + try { + if (!resolvedOutputDir || resolvedOutputDir === resolve()) { + throw new Error( + 'Refusing to write the sitemap to the current working directory. Expected the Nitro public output directory instead.', + ); + } + + if (!existsSync(resolvedOutputDir)) { + mkdirSync(resolvedOutputDir, { recursive: true }); + } + console.log(`Writing sitemap at ${mapPath}`); + writeFileSync(mapPath, sitemap.end({ prettyPrint: true })); + } catch (e) { + console.error(`Unable to write file at ${mapPath}`, e); + } +} + +async function resolveSitemapEntries( + routes: SitemapRouteInput[], + host: string, + routeSitemaps: Record, + sitemapConfig: SitemapConfig, + buildOptions: BuildSitemapOptions, +): Promise { + const defaults = sitemapConfig.defaults ?? {}; + const seen = new Set(); + const entries: SitemapEntry[] = []; + + for (const route of routes) { + const entry = await toSitemapEntry( + route, + host, + routeSitemaps, + defaults, + sitemapConfig.transform, + ); + + if (!entry) { + continue; + } + + if ( + isInternalSitemapRoute(entry.route, buildOptions.apiPrefix) || + (await isExcludedSitemapRoute(entry, sitemapConfig.exclude)) + ) { + continue; + } + + if (seen.has(entry.loc)) { + continue; + } + + seen.add(entry.loc); + entries.push(entry); + } + + return entries; +} + +async function toSitemapEntry( + route: SitemapRouteInput, + host: string, + routeSitemaps: Record, + defaults: PrerenderSitemapConfig, + transform: SitemapConfig['transform'], +): Promise { + const normalizedRoute = normalizeSitemapRoute( + typeof route === 'string' ? route : route?.route, + ); + if (!normalizedRoute) { + return undefined; + } + + const baseEntry = createSitemapEntry( + { + ...defaults, + ...resolveRouteSitemapConfig(routeSitemaps[normalizedRoute]), + ...(typeof route === 'object' ? route : {}), + route: normalizedRoute, + }, + host, + ); + + if (!transform) { + return baseEntry; + } + + const transformed = await transform(baseEntry); + if (!transformed) { + return undefined; + } + + return createSitemapEntry( + { + ...baseEntry, + ...transformed, + }, + host, + ); +} + +function createSitemapEntry( + routeDefinition: SitemapRouteDefinition, + host: string, +): SitemapEntry { + const route = normalizeSitemapRoute(routeDefinition.route) ?? '/'; + + return { + route, + loc: new URL(route, ensureTrailingSlash(host)).toString(), + lastmod: routeDefinition.lastmod, + changefreq: routeDefinition.changefreq, + priority: routeDefinition.priority, + }; +} + +function resolveRouteSitemapConfig( + config: RouteSitemapConfig, +): PrerenderSitemapConfig { + if (!config) { + return {}; + } + + return typeof config === 'function' ? config() : config; +} + +function normalizeSitemapHost(host: string): string { + const resolvedHost = new URL(host); + resolvedHost.hash = ''; + return resolvedHost.toString(); +} + +function ensureTrailingSlash(host: string): string { + return host.endsWith('/') ? host : `${host}/`; +} + +function normalizeSitemapRoute(route: string | undefined): string | undefined { + if (!route) { + return undefined; + } + + const trimmedRoute = route.trim(); + if (!trimmedRoute) { + return undefined; + } + + const pathWithQuery = trimmedRoute.split('#', 1)[0] ?? ''; + const [pathname, search] = pathWithQuery.split('?', 2); + const normalizedPathname = pathname + ? `/${pathname.replace(/^\/+/, '').replace(/\/{2,}/g, '/')}` + : '/'; + + return search ? `${normalizedPathname}?${search}` : normalizedPathname; +} + +function isInternalSitemapRoute(route: string, apiPrefix = 'api'): boolean { + const normalizedApiPrefix = normalizeSitemapRoute(`/${apiPrefix}`) ?? '/api'; + return ( + route === `${normalizedApiPrefix}/_analog/pages` || + route.startsWith(`${normalizedApiPrefix}/_analog/pages/`) + ); +} + +async function isExcludedSitemapRoute( + entry: SitemapEntry, + excludeRules: SitemapExcludeRule[] | undefined, +): Promise { + if (!excludeRules?.length) { + return false; + } + + for (const rule of excludeRules) { + if (typeof rule === 'function') { + if (await rule(entry)) { + return true; + } + continue; + } + + if (rule instanceof RegExp) { + if (rule.test(entry.route)) { + return true; + } + continue; + } + + if (toGlobRegExp(rule).test(entry.route)) { + return true; + } + } + + return false; +} + +function toGlobRegExp(pattern: string): RegExp { + const doubleStarToken = '__ANALOG_DOUBLE_STAR__'; + const singleStarToken = '__ANALOG_SINGLE_STAR__'; + const escapedPattern = pattern + .replace(/\*\*/g, doubleStarToken) + .replace(/\*/g, singleStarToken) + .replace(/[.+^${}()|[\]\\]/g, '\\$&'); + const regexPattern = escapedPattern + .replace(new RegExp(doubleStarToken, 'g'), '.*') + .replace(new RegExp(singleStarToken, 'g'), '[^/]*'); + return new RegExp(`^${regexPattern}$`); +} + +async function collectSitemapRoutes( + routes: (string | undefined)[] | (() => Promise<(string | undefined)[]>), + include?: SitemapRouteSource, +): Promise { + const routeList = await resolveRouteInputs(routes); + const includedRoutes = include ? await resolveRouteInputs(include) : []; + return [...routeList, ...includedRoutes]; +} + +async function resolveRouteInputs( + routes: + | SitemapRouteSource + | (string | undefined)[] + | (() => Promise<(string | undefined)[]>), +): Promise { + let routeList: SitemapRouteInput[]; + + if (typeof routes === 'function') { + routeList = await routes(); + } else if (Array.isArray(routes)) { + routeList = routes; + } else { + routeList = []; + } + + return routeList.filter(Boolean); +} + +/** + * Generates hreflang alternate URLs for a given page URL. + * For a URL like `https://example.com/fr/about`, it produces alternates + * for all configured locales. + */ +export function getHreflangAlternates( + pageUrl: string, + host: string, + i18n: I18nPrerenderOptions, +): { locale: string; href: string }[] { + const alternates: { locale: string; href: string }[] = []; + const normalizedHost = host.replace(/\/+$/, ''); + + const path = pageUrl.replace(normalizedHost, ''); + const basePath = stripLocalePrefix(path, i18n.locales); + + for (const locale of i18n.locales) { + const localizedPath = + basePath === '/' || basePath === '' + ? `/${locale}` + : `/${locale}${basePath}`; + alternates.push({ + locale, + href: `${normalizedHost}${localizedPath}`, + }); + } + + const defaultPath = + basePath === '/' || basePath === '' + ? `/${i18n.defaultLocale}` + : `/${i18n.defaultLocale}${basePath}`; + alternates.push({ + locale: 'x-default', + href: `${normalizedHost}${defaultPath}`, + }); + + return alternates; +} + +/** + * Strips a locale prefix from a URL path. + * E.g., '/fr/about' -> '/about', '/en' -> '/' + */ +export function stripLocalePrefix(path: string, locales: string[]): string { + const segments = path.split('/').filter(Boolean); + if (segments.length > 0 && locales.includes(segments[0])) { + const rest = segments.slice(1).join('/'); + return rest ? `/${rest}` : '/'; + } + return path || '/'; +} + +function createXml( + elementName: 'urlset' | 'sitemapindex', + includeXhtml = false, +): XMLBuilder { + const attrs: Record = { + xmlns: 'https://www.sitemaps.org/schemas/sitemap/0.9', + }; + if (includeXhtml) { + attrs['xmlns:xhtml'] = 'https://www.w3.org/1999/xhtml'; + } + + return create({ version: '1.0', encoding: 'UTF-8' }) + .ele(elementName, attrs) + .com(`This file was automatically generated by Analog.`); +} diff --git a/packages/platform/src/lib/nitro/debug.ts b/packages/platform/src/lib/nitro/debug.ts new file mode 100644 index 000000000..29643cb3b --- /dev/null +++ b/packages/platform/src/lib/nitro/debug.ts @@ -0,0 +1,8 @@ +import { createDebug } from 'obug'; + +export const debugNitro = createDebug('analog:nitro'); +export const debugSsr = createDebug('analog:nitro:ssr'); +export const debugPrerender = createDebug('analog:nitro:prerender'); + +/** All Nitro-related debug instances, for external wrapping (e.g. file logging). */ +export const nitroDebugInstances = [debugNitro, debugSsr, debugPrerender]; diff --git a/packages/platform/src/lib/nitro/get-content-files.spec.ts b/packages/platform/src/lib/nitro/get-content-files.spec.ts new file mode 100644 index 000000000..473cbe0d9 --- /dev/null +++ b/packages/platform/src/lib/nitro/get-content-files.spec.ts @@ -0,0 +1,77 @@ +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +import { getMatchingContentFilesWithFrontMatter } from './get-content-files'; + +describe('getMatchingContentFilesWithFrontMatter', () => { + let workspaceRoot: string; + const rootDir = '.'; + const contentDir = '/src/content/docs'; + + beforeEach(() => { + workspaceRoot = mkdtempSync(join(tmpdir(), 'analog-content-')); + mkdirSync(join(workspaceRoot, 'src/content/docs/erste-schritte'), { + recursive: true, + }); + mkdirSync(join(workspaceRoot, 'src/content/docs/assets'), { + recursive: true, + }); + writeFileSync( + join(workspaceRoot, 'src/content/docs/intro.md'), + '---\ntitle: Intro\n---\n# Intro', + ); + writeFileSync( + join(workspaceRoot, 'src/content/docs/erste-schritte/willkommen.md'), + '---\ntitle: Willkommen\n---\n# Willkommen', + ); + writeFileSync( + join(workspaceRoot, 'src/content/docs/assets/hochladen.md'), + '---\ntitle: Hochladen\n---\n# Hochladen', + ); + }); + + afterEach(() => { + rmSync(workspaceRoot, { recursive: true, force: true }); + }); + + it('returns only top-level files by default', () => { + const files = getMatchingContentFilesWithFrontMatter( + workspaceRoot, + rootDir, + contentDir, + ); + + expect(files.map((f) => f.name).sort()).toEqual(['intro']); + }); + + it('returns nested files when recursive is enabled', () => { + const files = getMatchingContentFilesWithFrontMatter( + workspaceRoot, + rootDir, + contentDir, + true, + ); + + expect(files.map((f) => f.name).sort()).toEqual([ + 'hochladen', + 'intro', + 'willkommen', + ]); + }); + + it('exposes the directory relative to contentDir as relativePath', () => { + const files = getMatchingContentFilesWithFrontMatter( + workspaceRoot, + rootDir, + contentDir, + true, + ); + + const byName = Object.fromEntries(files.map((f) => [f.name, f])); + expect(byName['intro'].relativePath).toBe(''); + expect(byName['willkommen'].relativePath).toBe('erste-schritte'); + expect(byName['hochladen'].relativePath).toBe('assets'); + }); +}); diff --git a/packages/platform/src/lib/nitro/get-content-files.ts b/packages/platform/src/lib/nitro/get-content-files.ts new file mode 100644 index 000000000..2e8101c7a --- /dev/null +++ b/packages/platform/src/lib/nitro/get-content-files.ts @@ -0,0 +1,74 @@ +import { readFileSync } from 'node:fs'; +import { join, relative, resolve } from 'node:path'; +import { normalizePath } from 'vite'; +import { createRequire } from 'node:module'; +import { globSync } from 'tinyglobby'; + +import type { PrerenderContentFile } from './types.js'; + +const require = createRequire(import.meta.url); + +/** + * Discovers content files with front matter and extracts metadata for prerendering. + * + * Globs the resolved content directory, reads each match, parses YAML/TOML + * front matter via `front-matter`, and returns `PrerenderContentFile` + * entries used by `PrerenderContentDir.transform()` to map files to routes. + * + * When `recursive` is enabled, `relativePath` on each result captures the + * file's directory relative to `contentDir` so transforms can disambiguate + * identically-named files across subdirectories. + */ +export function getMatchingContentFilesWithFrontMatter( + workspaceRoot: string, + rootDir: string, + glob: string, + recursive = false, +): PrerenderContentFile[] { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const fm = require('front-matter'); + + const root = normalizePath(resolve(workspaceRoot, rootDir)); + const resolvedDir = normalizePath(relative(root, join(root, glob))); + + const pattern = recursive + ? `${root}/${resolvedDir}/**/*` + : `${root}/${resolvedDir}/*`; + const contentFiles: string[] = globSync([pattern], { + dot: true, + absolute: true, + onlyFiles: true, + }); + + const dirPrefix = `${root}/${resolvedDir}`; + + const mappedFilesWithFm: PrerenderContentFile[] = contentFiles.map((f) => { + const fileContents = readFileSync(f, 'utf8'); + const raw = fm(fileContents); + + const filepath = normalizePath(f).replace(root, ''); + const match = filepath.match(/\/([^/.]+)(\.([^/.]+))?$/); + let name = ''; + let extension = ''; + if (match) { + name = match[1]; + extension = match[3] || ''; + } + + const relativeDir = normalizePath(relative(dirPrefix, f)); + const lastSlash = relativeDir.lastIndexOf('/'); + const relativePath = + lastSlash === -1 ? '' : relativeDir.slice(0, lastSlash); + + return { + name, + extension, + path: resolvedDir, + attributes: raw.attributes as { attributes: Record }, + content: fileContents, + relativePath, + }; + }); + + return mappedFilesWithFm; +} diff --git a/packages/platform/src/lib/nitro/get-page-handlers.ts b/packages/platform/src/lib/nitro/get-page-handlers.ts new file mode 100644 index 000000000..2b8461af3 --- /dev/null +++ b/packages/platform/src/lib/nitro/get-page-handlers.ts @@ -0,0 +1,66 @@ +import { resolve } from 'node:path'; +import { globSync } from 'tinyglobby'; + +import type { NitroEventHandler } from 'nitro/types'; +import { normalizePath } from 'vite'; + +type GetHandlersArgs = { + workspaceRoot: string; + sourceRoot: string; + rootDir: string; + additionalPagesDirs?: string[]; + hasAPIDir?: boolean; +}; + +/** + * Discovers and generates Nitro event handlers for server-side page routes. + * + * Discovers all `.server.ts` files under `app/pages/**` and any additional + * pages directories, then maps each file to a Nitro route pattern under + * `/_analog/pages/...` (prefixed with `/api` when the project has an API dir). + * + * Route transformation examples: + * - index.server.ts → /_analog/pages/index + * - users/[id].server.ts → /_analog/pages/users/:id + * - products/[...slug].server.ts → /_analog/pages/products/**:slug + * - (auth)/login.server.ts → /_analog/pages/-auth-/login + */ +export function getPageHandlers({ + workspaceRoot, + sourceRoot, + rootDir, + additionalPagesDirs, + hasAPIDir, +}: GetHandlersArgs): NitroEventHandler[] { + const root = normalizePath(resolve(workspaceRoot, rootDir)); + + const endpointFiles: string[] = globSync( + [ + `${root}/${sourceRoot}/app/pages/**/*.server.ts`, + ...(additionalPagesDirs || []).map( + (dir) => `${workspaceRoot}${dir}/**/*.server.ts`, + ), + ], + { dot: true, absolute: true }, + ); + + const handlers: NitroEventHandler[] = endpointFiles.map((endpointFile) => { + const normalized = normalizePath(endpointFile); + const route = normalized + .replace(/^(.*?)\/pages/, '/pages') + .replace(/\.server\.ts$/, '') + .replace(/\[\.{3}(.+)\]/g, '**:$1') + .replace(/\[\.{3}(\w+)\]/g, '**:$1') + .replace(/\/\((.*?)\)$/, '/-$1-') + .replace(/\[(\w+)\]/g, ':$1') + .replace(/\./g, '/'); + + return { + handler: endpointFile, + route: `${hasAPIDir ? '/api' : ''}/_analog${route}`, + lazy: true, + }; + }); + + return handlers; +} diff --git a/packages/platform/src/lib/nitro/i18n-prerender.spec.ts b/packages/platform/src/lib/nitro/i18n-prerender.spec.ts new file mode 100644 index 000000000..b1ad7f33f --- /dev/null +++ b/packages/platform/src/lib/nitro/i18n-prerender.spec.ts @@ -0,0 +1,186 @@ +import { describe, expect, it } from 'vitest'; +import { + expandRoutesWithLocales, + detectLocaleFromRoute, + setHtmlLang, +} from './i18n-prerender'; +import { getHreflangAlternates, stripLocalePrefix } from './build-sitemap'; +import { I18nPrerenderOptions } from './types'; + +const i18n: I18nPrerenderOptions = { + defaultLocale: 'en', + locales: ['en', 'fr', 'de'], +}; + +describe('expandRoutesWithLocales', () => { + it('should expand a single route to all locales', () => { + const result = expandRoutesWithLocales(['/about'], i18n); + + expect(result).toContain('/en/about'); + expect(result).toContain('/fr/about'); + expect(result).toContain('/de/about'); + }); + + it('should handle the root route', () => { + const result = expandRoutesWithLocales(['/'], i18n); + + expect(result).toContain('/en'); + expect(result).toContain('/fr'); + expect(result).toContain('/de'); + }); + + it('should keep the unprefixed root route for the default locale', () => { + const result = expandRoutesWithLocales(['/'], i18n); + + expect(result).toContain('/'); + }); + + it('should not prefix API routes', () => { + const result = expandRoutesWithLocales( + ['/about', '/api/v1/users', '/api/_analog/pages/about'], + i18n, + ); + + expect(result).toContain('/api/v1/users'); + expect(result).toContain('/api/_analog/pages/about'); + expect(result).not.toContain('/en/api/v1/users'); + }); + + it('should expand multiple routes', () => { + const result = expandRoutesWithLocales(['/about', '/contact'], i18n); + + expect(result).toContain('/en/about'); + expect(result).toContain('/fr/about'); + expect(result).toContain('/de/about'); + expect(result).toContain('/en/contact'); + expect(result).toContain('/fr/contact'); + expect(result).toContain('/de/contact'); + }); + + it('should not duplicate routes', () => { + const result = expandRoutesWithLocales(['/about'], i18n); + const aboutRoutes = result.filter((r) => r === '/about'); + + expect(aboutRoutes.length).toBeLessThanOrEqual(1); + }); +}); + +describe('detectLocaleFromRoute', () => { + it('should detect locale from route prefix', () => { + expect(detectLocaleFromRoute('/fr/about', i18n)).toBe('fr'); + expect(detectLocaleFromRoute('/de/contact', i18n)).toBe('de'); + expect(detectLocaleFromRoute('/en', i18n)).toBe('en'); + }); + + it('should return defaultLocale for routes without locale prefix', () => { + expect(detectLocaleFromRoute('/about', i18n)).toBe('en'); + expect(detectLocaleFromRoute('/', i18n)).toBe('en'); + }); + + it('should not match non-configured locales', () => { + expect(detectLocaleFromRoute('/es/about', i18n)).toBe('en'); + }); +}); + +describe('setHtmlLang', () => { + it('should add lang attribute to html tag', () => { + const html = ''; + const result = setHtmlLang(html, 'fr'); + + expect(result).toBe(''); + }); + + it('should replace existing lang attribute', () => { + const html = ''; + const result = setHtmlLang(html, 'de'); + + expect(result).toBe(''); + }); + + it('should preserve other attributes on html tag', () => { + const html = ''; + const result = setHtmlLang(html, 'fr'); + + expect(result).toContain('lang="fr"'); + expect(result).toContain('class="dark"'); + expect(result).toContain('dir="ltr"'); + }); +}); + +describe('getHreflangAlternates', () => { + it('should generate alternates for all locales plus x-default', () => { + const alternates = getHreflangAlternates( + 'https://example.com/fr/about', + 'https://example.com', + i18n, + ); + + expect(alternates).toContainEqual({ + locale: 'en', + href: 'https://example.com/en/about', + }); + expect(alternates).toContainEqual({ + locale: 'fr', + href: 'https://example.com/fr/about', + }); + expect(alternates).toContainEqual({ + locale: 'de', + href: 'https://example.com/de/about', + }); + expect(alternates).toContainEqual({ + locale: 'x-default', + href: 'https://example.com/en/about', + }); + }); + + it('should handle root locale paths', () => { + const alternates = getHreflangAlternates( + 'https://example.com/fr', + 'https://example.com', + i18n, + ); + + expect(alternates).toContainEqual({ + locale: 'en', + href: 'https://example.com/en', + }); + expect(alternates).toContainEqual({ + locale: 'fr', + href: 'https://example.com/fr', + }); + }); + + it('should handle host with trailing slash', () => { + const alternates = getHreflangAlternates( + 'https://example.com/en/about', + 'https://example.com/', + i18n, + ); + + expect(alternates).toContainEqual({ + locale: 'en', + href: 'https://example.com/en/about', + }); + }); +}); + +describe('stripLocalePrefix', () => { + it('should strip locale from path', () => { + expect(stripLocalePrefix('/fr/about', ['en', 'fr'])).toBe('/about'); + expect(stripLocalePrefix('/en/products/123', ['en', 'fr'])).toBe( + '/products/123', + ); + }); + + it('should return root for locale-only path', () => { + expect(stripLocalePrefix('/fr', ['en', 'fr'])).toBe('/'); + }); + + it('should return path unchanged if no locale prefix', () => { + expect(stripLocalePrefix('/about', ['en', 'fr'])).toBe('/about'); + }); + + it('should return root for empty path', () => { + expect(stripLocalePrefix('', ['en', 'fr'])).toBe('/'); + }); +}); diff --git a/packages/platform/src/lib/nitro/i18n-prerender.ts b/packages/platform/src/lib/nitro/i18n-prerender.ts new file mode 100644 index 000000000..1d2802d46 --- /dev/null +++ b/packages/platform/src/lib/nitro/i18n-prerender.ts @@ -0,0 +1,93 @@ +import type { PrerenderRoute } from 'nitro/types'; +import type { I18nPrerenderOptions } from './types.js'; + +/** + * Expands a list of routes to include locale-prefixed variants. + * + * For each route and each locale, generates a prefixed route: + * '/' + locale + route + * + * The default locale's routes are included both with and without the prefix + * so that `/about` and `/en/about` both render. + */ +export function expandRoutesWithLocales( + routes: string[], + i18n: I18nPrerenderOptions, +): string[] { + const expanded: string[] = []; + + for (const route of routes) { + if (route.includes('/_analog/') || route.startsWith('/api/')) { + expanded.push(route); + continue; + } + + for (const locale of i18n.locales) { + const prefix = `/${locale}`; + const localizedRoute = route === '/' ? prefix : `${prefix}${route}`; + expanded.push(localizedRoute); + } + + if (!expanded.includes(route)) { + expanded.push(route); + } + } + + return expanded; +} + +/** + * Creates a post-rendering hook that injects the `lang` attribute + * into the `` tag of prerendered pages based on the route's + * locale prefix. + */ +export function createI18nPostRenderingHook( + i18n: I18nPrerenderOptions, +): (route: PrerenderRoute) => Promise { + return async (route: PrerenderRoute) => { + if (!route.contents || typeof route.contents !== 'string') { + return; + } + + const locale = detectLocaleFromRoute(route.route, i18n); + if (!locale) { + return; + } + + route.contents = setHtmlLang(route.contents, locale); + }; +} + +/** + * Detects the locale from a prerendered route path by checking + * the first path segment against the configured locales. + */ +export function detectLocaleFromRoute( + route: string, + i18n: I18nPrerenderOptions, +): string { + const segments = route.split('/').filter(Boolean); + const firstSegment = segments[0]; + + if (firstSegment && i18n.locales.includes(firstSegment)) { + return firstSegment; + } + + return i18n.defaultLocale; +} + +/** + * Sets the `lang` attribute on the `` tag in an HTML string. + * If a `lang` attribute already exists, it is replaced. + * If no `lang` attribute exists, it is added. + */ +export function setHtmlLang(html: string, locale: string): string { + if (/]*\slang\s*=\s*["'][^"']*["']/i.test(html)) { + return html.replace( + /(]*\s)lang\s*=\s*["'][^"']*["']/i, + `$1lang="${locale}"`, + ); + } + + return html.replace(/ { + const plugin = pageEndpointsPlugin(); + + it('uses Nitro runtime $fetch instead of a private nitro import', async () => { + const result = await plugin.transform?.( + `export const load = () => ({ ok: true });`, + '/src/app/pages/index.server.ts', + ); + + expect(result).toBeDefined(); + expect(result?.code).toContain( + 'export default defineHandler(async(event) => {', + ); + expect(result?.code).toContain(`import { createFetch } from 'ofetch';`); + expect(result?.code).toContain('fetchWithEvent'); + expect(result?.code).toContain('const serverFetch = createFetch'); + expect(result?.code).toContain('fetch: serverFetch'); + expect(result?.code).not.toContain(`nitro/deps/ofetch`); + }); + + it('generates a default load when only action is exported', async () => { + const result = await plugin.transform?.( + `export const action = () => ({ saved: true });`, + '/src/app/pages/index.server.ts', + ); + + expect(result).toBeDefined(); + expect(result?.code).toContain('export const load = () =>'); + expect(result?.code).toContain( + 'export const action = () => ({ saved: true })', + ); + }); + + it('uses both exports when load and action are provided', async () => { + const result = await plugin.transform?.( + `export const load = () => ({ ok: true });\nexport const action = () => ({ saved: true });`, + '/src/app/pages/index.server.ts', + ); + + expect(result).toBeDefined(); + expect(result?.code).toContain('export const load = () => ({ ok: true })'); + expect(result?.code).toContain( + 'export const action = () => ({ saved: true })', + ); + expect(result?.code).not.toContain('return {};'); + }); + + it('generates default load and action when neither is exported', async () => { + const result = await plugin.transform?.( + `export const helper = () => 42;`, + '/src/app/pages/index.server.ts', + ); + + expect(result).toBeDefined(); + expect(result?.code).toContain('export const load = () =>'); + expect(result?.code).toContain('export const action = () =>'); + const stubs = (result?.code.match(/return \{\};/g) || []).length; + expect(stubs).toBe(2); + }); + + it('skips files that are not .server.ts', async () => { + const result = await plugin.transform?.( + `export const load = () => ({ ok: true });`, + '/src/app/pages/index.ts', + ); + + expect(result).toBeUndefined(); + }); + + it('skips .server.ts files outside /pages/', async () => { + const result = await plugin.transform?.( + `export const load = () => ({ ok: true });`, + '/src/app/services/auth.server.ts', + ); + + expect(result).toBeUndefined(); + }); +}); diff --git a/packages/platform/src/lib/nitro/page-endpoints-plugin.ts b/packages/platform/src/lib/nitro/page-endpoints-plugin.ts new file mode 100644 index 000000000..3cf2e3267 --- /dev/null +++ b/packages/platform/src/lib/nitro/page-endpoints-plugin.ts @@ -0,0 +1,98 @@ +import { parseSync } from 'oxc-parser'; +import { normalizePath } from 'vite'; +import { SERVER_FETCH_FACTORY_SNIPPET } from './renderers.js'; + +export function pageEndpointsPlugin() { + return { + name: 'analogjs-platform-rollup-page-endpoint', + async transform( + _code: string, + id: string, + ): Promise<{ code: string; map: null } | undefined> { + if (normalizePath(id).includes('/pages/') && id.endsWith('.server.ts')) { + const result = parseSync(id, _code, { + sourceType: 'module', + lang: 'ts', + }); + + const fileExports: string[] = result.module.staticExports.flatMap((e) => + e.entries + .filter((entry) => entry.exportName.name !== null) + .map((entry) => entry.exportName.name as string), + ); + + // In h3 v2 / Nitro v3, event.node is undefined during prerendering + // (which uses the fetch-based pipeline, not Node.js http). We use + // optional chaining so that page endpoints work in both Node.js + // server and fetch-based prerender contexts. + // + // Page loaders expect Nitro-style `$fetch` semantics (parsed data plus + // internal relative-route support), so we construct a request-local + // fetch using `createFetch` from ofetch + `fetchWithEvent` from h3. + const code = ` + import { defineHandler, fetchWithEvent } from 'nitro/h3'; + import { createFetch } from 'ofetch'; + + ${ + fileExports.includes('load') + ? _code + : ` + ${_code} + export const load = () => { + return {}; + }` + } + + ${ + fileExports.includes('action') + ? '' + : ` + export const action = () => { + return {}; + } + ` + } + + export default defineHandler(async(event) => { + ${SERVER_FETCH_FACTORY_SNIPPET} + + if (event.method === 'GET') { + try { + return await load({ + params: event.context.params, + req: event.node?.req, + res: event.node?.res, + fetch: serverFetch, + event + }); + } catch(e) { + console.error(\` An error occurred: \${e}\`) + throw e; + } + } else { + try { + return await action({ + params: event.context.params, + req: event.node?.req, + res: event.node?.res, + fetch: serverFetch, + event + }); + } catch(e) { + console.error(\` An error occurred: \${e}\`) + throw e; + } + } + }); + `; + + return { + code, + map: null, + }; + } + + return; + }, + }; +} diff --git a/packages/platform/src/lib/nitro/post-rendering-hook.spec.ts b/packages/platform/src/lib/nitro/post-rendering-hook.spec.ts new file mode 100644 index 000000000..a9e4439d5 --- /dev/null +++ b/packages/platform/src/lib/nitro/post-rendering-hook.spec.ts @@ -0,0 +1,33 @@ +import type { Nitro } from 'nitro/types'; +import { vi } from 'vitest'; + +import { addPostRenderingHooks } from './post-rendering-hook'; + +describe('postRenderingHook', () => { + const genRoute = { + route: 'test/testRoute', + contents: 'This is a test.', + }; + + const nitroMock = { + hooks: { + hook: vi.fn((name: string, callback: (route: any) => void) => + callback(genRoute), + ), + }, + } as unknown as Nitro; + + const mockFunc1 = vi.fn(); + const mockFunc2 = vi.fn(); + + it('should not attempt to call nitro mocks if no callbacks provided', () => { + addPostRenderingHooks(nitroMock, []); + expect(nitroMock.hooks.hook).not.toHaveBeenCalled(); + }); + + it('should call provided hooks', () => { + addPostRenderingHooks(nitroMock, [mockFunc1, mockFunc2]); + expect(mockFunc1).toHaveBeenCalledWith(genRoute); + expect(mockFunc2).toHaveBeenCalled(); + }); +}); diff --git a/packages/platform/src/lib/nitro/post-rendering-hook.ts b/packages/platform/src/lib/nitro/post-rendering-hook.ts new file mode 100644 index 000000000..9e6dc379c --- /dev/null +++ b/packages/platform/src/lib/nitro/post-rendering-hook.ts @@ -0,0 +1,12 @@ +import type { Nitro, PrerenderRoute } from 'nitro/types'; + +export function addPostRenderingHooks( + nitro: Nitro, + hooks: ((pr: PrerenderRoute) => Promise)[], +): void { + hooks.forEach((hook: (preRoute: PrerenderRoute) => void) => { + nitro.hooks.hook('prerender:generate', (route: PrerenderRoute) => { + hook(route); + }); + }); +} diff --git a/packages/platform/src/lib/nitro/renderers.spec.ts b/packages/platform/src/lib/nitro/renderers.spec.ts new file mode 100644 index 000000000..b21878a5f --- /dev/null +++ b/packages/platform/src/lib/nitro/renderers.spec.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from 'vitest'; + +import { apiMiddleware, clientRenderer, ssrRenderer } from './renderers'; + +describe('renderers virtual modules', () => { + it('emits an SSR renderer that serves HTML responses', () => { + const moduleSource = ssrRenderer(); + + expect(moduleSource).toContain("import template from '#analog/index';"); + expect(moduleSource).not.toContain('readFileSync('); + expect(moduleSource).toContain( + "event.res.headers.set('content-type', 'text/html; charset=utf-8');", + ); + expect(moduleSource).toContain( + 'const requestPath = normalizeHtmlRequestUrl(event.path);', + ); + expect(moduleSource).toContain('const req = event.node?.req'); + expect(moduleSource).toContain( + 'const html = await renderer(requestPath, template, { req, res, fetch: serverFetch });', + ); + expect(moduleSource).toContain("import renderer from '#analog/ssr';"); + }); + + it('emits a client renderer that serves HTML responses', () => { + const moduleSource = clientRenderer(); + + expect(moduleSource).toContain("import template from '#analog/index';"); + expect(moduleSource).not.toContain('readFileSync('); + expect(moduleSource).toContain( + "event.res.headers.set('content-type', 'text/html; charset=utf-8');", + ); + }); + + it('uses event-bound forwarding for API middleware', () => { + expect(apiMiddleware).toContain( + "import { defineHandler, fetchWithEvent, proxyRequest } from 'nitro/h3';", + ); + expect(apiMiddleware).toContain('return fetchWithEvent(event, reqUrl'); + expect(apiMiddleware).toContain('return proxyRequest(event, reqUrl);'); + }); +}); diff --git a/packages/platform/src/lib/nitro/renderers.ts b/packages/platform/src/lib/nitro/renderers.ts new file mode 100644 index 000000000..19fd29c10 --- /dev/null +++ b/packages/platform/src/lib/nitro/renderers.ts @@ -0,0 +1,141 @@ +/** + * Code snippet emitted into virtual modules to create a request-scoped + * fetch using ofetch's `createFetch` + h3's `fetchWithEvent`. + * + * Shared between the SSR renderer and page-endpoint virtual modules so + * the fetch-wiring logic stays in sync. + * + * The emitted variable is named `serverFetch` — callers should reference it + * by that name. + */ +export const SERVER_FETCH_FACTORY_SNIPPET = ` + const serverFetch = createFetch({ + fetch: (resource, init) => { + const url = resource instanceof Request ? resource.url : resource.toString(); + return fetchWithEvent(event, url, init); + } + });`; + +/** + * SSR renderer virtual module content. + * + * This code runs inside Nitro's server runtime (Node.js context) where + * event.node is always populated. In h3 v2, event.node is typed as optional, + * so we use h3's first-class event properties (event.path, event.method) where + * possible and apply optional chaining when accessing the Node.js context for + * the Angular renderer which requires raw req/res objects. + * + * h3 v2 idiomatic APIs used: + * - defineHandler (replaces defineEventHandler / eventHandler) + * - event.path (replaces event.node.req.url) + * - getResponseHeader compat shim (still available in h3 v2) + */ +export function ssrRenderer() { + return ` +import { createFetch } from 'ofetch'; +import { defineHandler, fetchWithEvent } from 'nitro/h3'; +// @ts-ignore +import renderer from '#analog/ssr'; +import template from '#analog/index'; + +const normalizeHtmlRequestUrl = (url) => + url.replace(/\\/index\\.html(?=$|[?#])/, '/'); + +export default defineHandler(async (event) => { + event.res.headers.set('content-type', 'text/html; charset=utf-8'); + const noSSR = event.res.headers.get('x-analog-no-ssr'); + const requestPath = normalizeHtmlRequestUrl(event.path); + + if (noSSR === 'true') { + return template; + } + + // event.path is the canonical h3 v2 way to access the request URL. + // event.node?.req and event.node?.res are needed by the Angular SSR renderer + // which operates on raw Node.js request/response objects. + // During prerendering (Nitro v3 fetch-based pipeline), event.node is undefined. + // The Angular renderer requires a req object with at least { headers, url }, + // so we provide a minimal stub to avoid runtime errors in prerender context. + const req = event.node?.req + ? { + ...event.node.req, + url: requestPath, + originalUrl: requestPath, + } + : { + headers: { host: 'localhost' }, + url: requestPath, + originalUrl: requestPath, + connection: {}, + }; + const res = event.node?.res; +${SERVER_FETCH_FACTORY_SNIPPET} + + const html = await renderer(requestPath, template, { req, res, fetch: serverFetch }); + + return html; +});`; +} + +/** + * Client-only renderer virtual module content. + * + * Used when SSR is disabled — simply serves the static index.html template + * for every route, letting the client-side Angular router handle navigation. + */ +export function clientRenderer() { + return ` +import { defineHandler } from 'nitro/h3'; +import template from '#analog/index'; + +export default defineHandler(async (event) => { + event.res.headers.set('content-type', 'text/html; charset=utf-8'); + return template; +}); +`; +} + +/** + * API middleware virtual module content. + * + * Intercepts requests matching the configured API prefix and either: + * - Uses event-bound internal forwarding for GET requests (except .xml routes) + * - Uses request proxying for all other methods to forward the full request + * + * h3 v2 idiomatic APIs used: + * - defineHandler (replaces defineEventHandler / eventHandler) + * - event.path (replaces event.node.req.url) + * - event.method (replaces event.node.req.method) + * - proxyRequest is retained internally because it preserves Nitro route + * matching for event-bound server requests during SSR/prerender + * - Object.fromEntries(event.req.headers.entries()) replaces direct event.node.req.headers access + * + * `fetchWithEvent` keeps the active event context while forwarding to a + * rewritten path, which avoids falling through to the HTML renderer when + * SSR code makes relative API requests. + */ +export const apiMiddleware = ` +import { defineHandler, fetchWithEvent, proxyRequest } from 'nitro/h3'; +import { useRuntimeConfig } from 'nitro/runtime-config'; + +export default defineHandler(async (event) => { + const prefix = useRuntimeConfig().prefix; + const apiPrefix = \`\${prefix}/\${useRuntimeConfig().apiPrefix}\`; + + if (event.path?.startsWith(apiPrefix)) { + const reqUrl = event.path?.replace(apiPrefix, ''); + + if ( + event.method === 'GET' && + // in the case of XML routes, we want to proxy the request so that nitro gets the correct headers + // and can render the XML correctly as a static asset + !event.path?.endsWith('.xml') + ) { + return fetchWithEvent(event, reqUrl, { + headers: Object.fromEntries(event.req.headers.entries()), + }); + } + + return proxyRequest(event, reqUrl); + } +});`; diff --git a/packages/platform/src/lib/nitro/types.ts b/packages/platform/src/lib/nitro/types.ts new file mode 100644 index 000000000..706105539 --- /dev/null +++ b/packages/platform/src/lib/nitro/types.ts @@ -0,0 +1,165 @@ +import type { PrerenderRoute } from 'nitro/types'; + +export interface I18nPrerenderOptions { + /** + * The default/source locale for the application. + */ + defaultLocale: string; + + /** + * List of supported locale identifiers. + * Each route will be prerendered once per locale with a locale prefix. + */ + locales: string[]; +} + +export interface PrerenderOptions { + /** + * Add additional routes to prerender through crawling page links. + */ + discover?: boolean; + + /** + * List of routes to prerender resolved statically or dynamically. + */ + routes?: + | (string | PrerenderContentDir | PrerenderRouteConfig)[] + | (() => Promise< + (string | PrerenderContentDir | PrerenderRouteConfig | undefined)[] + >); + sitemap?: SitemapConfig; + /** List of functions that run for each route after pre-rendering is complete. */ + postRenderingHooks?: ((routes: PrerenderRoute) => Promise)[]; +} + +export type SitemapPriority = number | `${number}`; + +export interface SitemapRouteDefinition { + route: string; + lastmod?: string; + changefreq?: + | 'always' + | 'hourly' + | 'daily' + | 'weekly' + | 'monthly' + | 'yearly' + | 'never'; + priority?: SitemapPriority; +} + +export interface SitemapEntry extends SitemapRouteDefinition { + loc: string; +} + +export type SitemapRouteInput = string | SitemapRouteDefinition | undefined; +export type SitemapRouteSource = + | SitemapRouteInput[] + | (() => Promise); +export type SitemapExcludeRule = + | string + | RegExp + | ((entry: SitemapEntry) => boolean | Promise); +export type SitemapTransform = ( + entry: SitemapEntry, +) => SitemapRouteDefinition | false | Promise; + +export interface SitemapConfig { + host: string; + include?: SitemapRouteSource; + exclude?: SitemapExcludeRule[]; + defaults?: PrerenderSitemapConfig; + transform?: SitemapTransform; +} + +export interface PrerenderContentDir { + /** + * The directory where files should be grabbed from. + * @example `/src/contents/blog` + */ + contentDir: string; + /** + * Transform the matching content files path into a route. + * The function is called for each matching content file within the specified contentDir. + * @param file information of the matching file (`path`, `name`, `extension`, `attributes`, `content`) + * @returns a string with the route should be returned (e. g. `/blog/`) or the value `false`, when the route should not be prerendered. + */ + transform: (file: PrerenderContentFile) => string | false; + + /** + * Customize the sitemap definition for the prerendered route + * + * https://www.sitemaps.org/protocol.html#xmlTagDefinitions + */ + sitemap?: + | PrerenderSitemapConfig + | ((file: PrerenderContentFile) => PrerenderSitemapConfig); + + /** + * Output the source markdown content alongside the prerendered route. + * The source file will be accessible at the route path with a .md extension. + * @param file information of the matching file including its content + * @returns the markdown content string to output, or `false` to skip outputting for this file + */ + outputSourceFile?: (file: PrerenderContentFile) => string | false; + + /** + * Recurse into subdirectories of `contentDir` when discovering files. + * When enabled, the matching file's directory relative to `contentDir` + * is exposed via `PrerenderContentFile.relativePath` so transforms can + * disambiguate identically-named files across subdirectories. + * @default false + */ + recursive?: boolean; +} + +/** + * @param path the path to the content file + * @param name the basename of the matching content file without the file extension + * @param extension the file extension + * @param attributes the frontmatter attributes extracted from the frontmatter section of the file + * @param content the raw file content including frontmatter + * @param relativePath when `recursive` is enabled, the directory of the file relative to `contentDir` (empty string for files at the top level) + * @returns a string with the route should be returned (e. g. `/blog/`) or the value `false`, when the route should not be prerendered. + */ +export interface PrerenderContentFile { + path: string; + attributes: Record; + name: string; + extension: string; + content: string; + relativePath?: string; +} + +export interface PrerenderSitemapConfig { + lastmod?: string; + changefreq?: + | 'always' + | 'hourly' + | 'daily' + | 'weekly' + | 'monthly' + | 'yearly' + | 'never'; + priority?: SitemapPriority; +} + +export interface PrerenderRouteConfig { + route: string; + /** + * Customize the sitemap definition for the prerendered route + * + * https://www.sitemaps.org/protocol.html#xmlTagDefinitions + */ + sitemap?: PrerenderSitemapConfig | (() => PrerenderSitemapConfig); + /** + * Prerender static data for the prerendered route + */ + staticData?: boolean; + /** + * Path to the source markdown file to output alongside the prerendered route. + * The source file will be accessible at the route path with a .md extension. + * @example 'src/content/overview.md' + */ + outputSourceFile?: string; +} diff --git a/packages/platform/src/lib/options.ts b/packages/platform/src/lib/options.ts index a54b80d72..1beb34733 100644 --- a/packages/platform/src/lib/options.ts +++ b/packages/platform/src/lib/options.ts @@ -13,7 +13,7 @@ import type { PrerenderContentFile, PrerenderSitemapConfig, PrerenderRouteConfig, -} from '@analogjs/vite-plugin-nitro'; +} from './nitro/types.js'; import type { ContentPluginOptions } from './content-plugin.js'; import type { DebugOption } from './utils/debug.js'; diff --git a/packages/platform/src/lib/utils/debug.spec.ts b/packages/platform/src/lib/utils/debug.spec.ts index ca811e14c..846ad8f49 100644 --- a/packages/platform/src/lib/utils/debug.spec.ts +++ b/packages/platform/src/lib/utils/debug.spec.ts @@ -10,8 +10,8 @@ vi.mock('obug', () => ({ enable: vi.fn(), })); -vi.mock('@analogjs/vite-plugin-nitro/internal', () => ({ - debugInstances: [], +vi.mock('../nitro/debug.js', () => ({ + nitroDebugInstances: [], })); vi.mock('./debug-log-file.js', () => ({ diff --git a/packages/platform/src/lib/utils/debug.ts b/packages/platform/src/lib/utils/debug.ts index 416e8f9b5..2b5dae72c 100644 --- a/packages/platform/src/lib/utils/debug.ts +++ b/packages/platform/src/lib/utils/debug.ts @@ -1,5 +1,5 @@ import { createDebug } from 'obug'; -import { debugInstances as nitroDebugInstances } from '@analogjs/vite-plugin-nitro/internal'; +import { nitroDebugInstances } from '../nitro/debug.js'; import { createDebugHarness } from './debug-harness.js'; export const debugPlatform = createDebug('analog:platform'); From de768eb8f1aea5a2566fac8836ed2b37f915ae40 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Fri, 22 May 2026 21:54:19 -0500 Subject: [PATCH 02/65] feat(platform): add Angular linker Rolldown plugin for SSR optimizeDeps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 analogjs/analog#2035 Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/platform/package.json | 1 + .../lib/nitro/angular-linker-plugin.spec.ts | 25 +++++++ .../src/lib/nitro/angular-linker-plugin.ts | 65 +++++++++++++++++++ pnpm-workspace.yaml | 1 + 4 files changed, 92 insertions(+) create mode 100644 packages/platform/src/lib/nitro/angular-linker-plugin.spec.ts create mode 100644 packages/platform/src/lib/nitro/angular-linker-plugin.ts diff --git a/packages/platform/package.json b/packages/platform/package.json index b8749e549..6dcca0f8b 100644 --- a/packages/platform/package.json +++ b/packages/platform/package.json @@ -47,6 +47,7 @@ "nitro": "catalog:", "@analogjs/vite-plugin-angular": "workspace:*", "@analogjs/vite-plugin-nitro": "workspace:*", + "@babel/core": "catalog:", "ofetch": "catalog:", "oxc-parser": "catalog:", "rolldown": "catalog:", diff --git a/packages/platform/src/lib/nitro/angular-linker-plugin.spec.ts b/packages/platform/src/lib/nitro/angular-linker-plugin.spec.ts new file mode 100644 index 000000000..ea29e1d1b --- /dev/null +++ b/packages/platform/src/lib/nitro/angular-linker-plugin.spec.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from 'vitest'; + +import { angularLinkerPlugin } from './angular-linker-plugin'; + +describe('angularLinkerPlugin', () => { + const plugin = angularLinkerPlugin(); + + it('exposes a Rolldown plugin shape', () => { + expect(plugin.name).toBe('analogjs-platform-angular-linker'); + expect(typeof plugin.transform).toBe('function'); + }); + + it('skips non-JS files', async () => { + const result = await plugin.transform('export const x = 1;', '/foo.ts'); + expect(result).toBeUndefined(); + }); + + it('skips JS files that do not contain partial Angular declarations', async () => { + const result = await plugin.transform( + 'export const greeting = "hello";', + '/foo.mjs', + ); + expect(result).toBeUndefined(); + }); +}); diff --git a/packages/platform/src/lib/nitro/angular-linker-plugin.ts b/packages/platform/src/lib/nitro/angular-linker-plugin.ts new file mode 100644 index 000000000..c56f78eb0 --- /dev/null +++ b/packages/platform/src/lib/nitro/angular-linker-plugin.ts @@ -0,0 +1,65 @@ +/** + * Rolldown plugin that runs the Angular Linker against partially-compiled + * Angular npm packages. + * + * Wired into `ssr.optimizeDeps.rolldownOptions.plugins` so the SSR / + * `nitro` environment's dep optimizer turns `ɵɵngDeclare*` partial + * declarations into fully-compiled definitions. Without this, the + * server bundle would need JIT (eval) at runtime — forbidden on + * `workerd` / edge runtimes and unnecessary anywhere else. + * + * Loaded lazily so apps that never trigger the SSR optimizer don't + * incur the babel + compiler-cli/linker cost. + */ +export function angularLinkerPlugin() { + let linkerBabelPlugin: unknown; + let needsLinkingFn: ((id: string, code: string) => boolean) | undefined; + let transformAsyncFn: + | (( + code: string, + options: Record, + ) => Promise<{ code?: string; map?: unknown } | null>) + | undefined; + + async function ensureLoaded() { + if (linkerBabelPlugin && needsLinkingFn && transformAsyncFn) return; + + const linker = await import('@angular/compiler-cli/linker'); + needsLinkingFn = linker.needsLinking; + + const linkerBabel = await import('@angular/compiler-cli/linker/babel'); + linkerBabelPlugin = + (linkerBabel as { default?: unknown }).default ?? linkerBabel; + + // @ts-expect-error — @babel/core ships without bundled type declarations + const babel = await import('@babel/core'); + transformAsyncFn = babel.transformAsync; + } + + return { + name: 'analogjs-platform-angular-linker', + async transform(code: string, id: string) { + if (!id.endsWith('.mjs') && !id.endsWith('.js')) return; + + // Cheap pre-check before pulling babel/compiler-cli into memory. + if (!code.includes('ɵɵngDeclare')) return; + + await ensureLoaded(); + if (!needsLinkingFn!(id, code)) return; + + const result = await transformAsyncFn!(code, { + filename: id, + plugins: [linkerBabelPlugin], + sourceMaps: true, + compact: false, + configFile: false, + babelrc: false, + }); + + if (result?.code) { + return { code: result.code, map: result.map ?? null }; + } + return; + }, + }; +} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 70ac3ef82..a4593bf65 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -80,6 +80,7 @@ catalog: '@angular/cli': 21.2.7 '@angular/compiler-cli': 21.2.8 '@angular/language-service': 21.2.8 + '@babel/core': ^7.28.6 '@commitlint/cli': ^20.5.0 '@commitlint/config-conventional': ^20.5.0 '@compodoc/compodoc': ^1.2.1 From 853cecc923067e8f7a45472b24a204510ac69afc Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Fri, 22 May 2026 21:54:58 -0500 Subject: [PATCH 03/65] feat(platform): add analogNitroPlugin (NitroModule + SSR service wrapper) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 analogjs/analog#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 analogjs/analog#2035 Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/lib/nitro/analog-nitro-plugin.spec.ts | 140 +++++++ .../src/lib/nitro/analog-nitro-plugin.ts | 391 ++++++++++++++++++ 2 files changed, 531 insertions(+) create mode 100644 packages/platform/src/lib/nitro/analog-nitro-plugin.spec.ts create mode 100644 packages/platform/src/lib/nitro/analog-nitro-plugin.ts diff --git a/packages/platform/src/lib/nitro/analog-nitro-plugin.spec.ts b/packages/platform/src/lib/nitro/analog-nitro-plugin.spec.ts new file mode 100644 index 000000000..55177de38 --- /dev/null +++ b/packages/platform/src/lib/nitro/analog-nitro-plugin.spec.ts @@ -0,0 +1,140 @@ +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; + +import { analogNitroPlugin } from './analog-nitro-plugin'; + +function callConfig(plugin: any, root: string) { + const hook = plugin.config; + return typeof hook === 'function' + ? hook({ root }, { command: 'build', mode: 'production' }) + : hook?.handler({ root }, { command: 'build', mode: 'production' }); +} + +function callResolveId(plugin: any, id: string) { + const hook = plugin.resolveId; + if (typeof hook === 'function') { + return hook.call({} as any, id, undefined, {} as any); + } + return hook?.handler.call({} as any, id, undefined, {} as any); +} + +function callLoad(plugin: any, id: string) { + const hook = plugin.load; + if (typeof hook === 'function') { + return hook.call({} as any, id); + } + return hook?.handler.call({} as any, id); +} + +describe('analogNitroPlugin', () => { + let workspaceRoot: string; + let projectRoot: string; + + beforeEach(() => { + workspaceRoot = mkdtempSync(join(tmpdir(), 'analog-nitro-plugin-')); + projectRoot = workspaceRoot; + mkdirSync(join(workspaceRoot, 'src'), { recursive: true }); + writeFileSync( + join(workspaceRoot, 'src/main.server.ts'), + 'export default () => "";', + ); + writeFileSync( + join(workspaceRoot, 'index.html'), + '
', + ); + }); + + afterEach(() => { + rmSync(workspaceRoot, { recursive: true, force: true }); + }); + + it('exposes the expected plugin shape', () => { + const plugin = analogNitroPlugin({ workspaceRoot }); + expect(plugin.name).toBe('@analogjs/nitro'); + expect(plugin.enforce).toBe('pre'); + expect(typeof plugin.config).toBe('function'); + expect(typeof plugin.resolveId).toBe('function'); + expect(typeof plugin.load).toBe('function'); + expect(typeof (plugin as any).nitro.setup).toBe('function'); + }); + + it('registers the SSR service entry and linker optimizeDeps when ssr=true', () => { + const plugin = analogNitroPlugin({ workspaceRoot, ssr: true }); + const overrides: any = callConfig(plugin, projectRoot); + + expect(overrides.experimental.vite.services.ssr.entry).toMatch( + /\.analog\/__ssr-entry\.mjs$/, + ); + expect(overrides.environments.ssr.optimizeDeps.include).toContain( + '@angular/core', + ); + expect(overrides.environments.ssr.optimizeDeps.include).toContain( + '@angular/platform-server', + ); + expect( + overrides.environments.ssr.optimizeDeps.rolldownOptions.plugins, + ).toHaveLength(1); + }); + + it('does not configure SSR overrides when ssr=false', () => { + const plugin = analogNitroPlugin({ workspaceRoot, ssr: false }); + const overrides: any = callConfig(plugin, projectRoot); + + expect(overrides.experimental).toBeUndefined(); + expect(overrides.environments).toBeUndefined(); + }); + + it('resolves the SSR entry marker path to the virtual id', () => { + const plugin = analogNitroPlugin({ workspaceRoot }); + callConfig(plugin, projectRoot); + + const markerPath = join(workspaceRoot, '.analog/__ssr-entry.mjs'); + expect(callResolveId(plugin, markerPath)).toBe( + '\0virtual:@analogjs/nitro/ssr-entry', + ); + expect(callResolveId(plugin, '/some/other/path.ts')).toBeNull(); + }); + + it('emits a wrapper that imports the user main.server.ts and inlines the template', () => { + const plugin = analogNitroPlugin({ workspaceRoot }); + callConfig(plugin, projectRoot); + + const code = callLoad(plugin, '\0virtual:@analogjs/nitro/ssr-entry'); + expect(typeof code).toBe('string'); + expect(code).toContain('main.server.ts'); + expect(code).toContain('export default {'); + expect(code).toContain('fetch(req)'); + // Template is JSON.stringified into the wrapper, so quotes are escaped. + expect(code).toContain('id=\\"app\\"'); + expect(code).toContain("'x-analog-no-ssr'"); + }); + + it('registers page handlers and the page-endpoints rollup plugin in nitro setup', async () => { + mkdirSync(join(workspaceRoot, 'src/app/pages'), { recursive: true }); + writeFileSync( + join(workspaceRoot, 'src/app/pages/index.server.ts'), + 'export const load = () => ({});', + ); + + const plugin = analogNitroPlugin({ workspaceRoot }); + callConfig(plugin, projectRoot); + + const hookFn = vi.fn(); + const nitroMock: any = { + options: { + rootDir: projectRoot, + handlers: [], + scanDirs: [], + }, + hooks: { hook: hookFn }, + }; + + await (plugin as any).nitro.setup(nitroMock); + + expect(nitroMock.options.handlers).toHaveLength(1); + expect(nitroMock.options.handlers[0].route).toContain('/_analog/pages'); + expect(hookFn).toHaveBeenCalledWith('rollup:before', expect.any(Function)); + }); +}); diff --git a/packages/platform/src/lib/nitro/analog-nitro-plugin.ts b/packages/platform/src/lib/nitro/analog-nitro-plugin.ts new file mode 100644 index 000000000..48de19fb1 --- /dev/null +++ b/packages/platform/src/lib/nitro/analog-nitro-plugin.ts @@ -0,0 +1,391 @@ +import { existsSync, readFileSync } from 'node:fs'; +import { relative, resolve } from 'node:path'; +import type { Nitro, NitroEventHandler, PrerenderRoute } from 'nitro/types'; +import type { Plugin, UserConfig } from 'vite'; + +import type { Options } from '../options.js'; +import type { + PrerenderContentDir, + PrerenderContentFile, + PrerenderRouteConfig, + PrerenderSitemapConfig, +} from './types.js'; +import { getPageHandlers } from './get-page-handlers.js'; +import { pageEndpointsPlugin } from './page-endpoints-plugin.js'; +import { getMatchingContentFilesWithFrontMatter } from './get-content-files.js'; +import { buildSitemap } from './build-sitemap.js'; +import { addPostRenderingHooks } from './post-rendering-hook.js'; +import { + expandRoutesWithLocales, + createI18nPostRenderingHook, +} from './i18n-prerender.js'; +import { angularLinkerPlugin } from './angular-linker-plugin.js'; + +const SSR_ENTRY_VIRTUAL_ID = '\0virtual:@analogjs/nitro/ssr-entry'; + +/** + * Angular packages that ship in partial-compilation form and must pass + * through the linker before the SSR / nitro bundle can execute without + * JIT. Wired into `ssr.optimizeDeps.rolldownOptions.plugins`. + */ +const ANGULAR_SSR_DEPS = [ + '@angular/compiler', + '@angular/core', + '@angular/common', + '@angular/platform-browser', + '@angular/platform-server', +]; + +interface NitroPluginContext { + workspaceRoot: string; + rootDir: string; + sourceRoot: string; +} + +type RouteSitemap = + | PrerenderSitemapConfig + | (() => PrerenderSitemapConfig) + | undefined; + +/** + * Analog's NitroModule. Plug it into the Vite plugin chain alongside + * `nitro()` from `nitro/vite`; nitro/vite picks up the `.nitro` property + * and runs `setup(nitro)` once the Nitro instance is ready. + */ +export function analogNitroPlugin(options: Options = {}): Plugin { + const workspaceRoot = + options.workspaceRoot ?? process.env['NX_WORKSPACE_ROOT'] ?? process.cwd(); + const sourceRoot = 'src'; + const ssr = options.ssr ?? true; + const apiPrefix = options.apiPrefix ?? 'api'; + + let context: NitroPluginContext = { + workspaceRoot, + rootDir: '.', + sourceRoot, + }; + let ssrEntryMarkerPath = ''; + + function refreshContext(viteRoot: string | undefined) { + const root = viteRoot ?? process.cwd(); + context = { + workspaceRoot, + rootDir: relative(workspaceRoot, root) || '.', + sourceRoot, + }; + ssrEntryMarkerPath = resolve( + context.workspaceRoot, + context.rootDir, + '.analog/__ssr-entry.mjs', + ); + } + + function readIndexHtml(): string { + const indexFile = options.index ?? 'index.html'; + const candidates = [ + resolve(context.workspaceRoot, context.rootDir, indexFile), + resolve( + context.workspaceRoot, + 'dist', + context.rootDir, + 'client', + indexFile, + ), + ]; + for (const candidate of candidates) { + if (existsSync(candidate)) { + return readFileSync(candidate, 'utf-8'); + } + } + return '
'; + } + + function resolveEntryServer(): string { + return ( + options.entryServer ?? + resolve( + context.workspaceRoot, + context.rootDir, + `${sourceRoot}/main.server.ts`, + ) + ); + } + + const plugin: Plugin & { + nitro: { setup: (nitro: Nitro) => void | Promise }; + } = { + name: '@analogjs/nitro', + enforce: 'pre', + + config(userConfig) { + refreshContext(userConfig.root); + + const overrides: UserConfig = {}; + + if (ssr) { + overrides.experimental = { + vite: { + services: { + ssr: { entry: ssrEntryMarkerPath }, + }, + }, + }; + overrides.environments = { + ssr: { + optimizeDeps: { + include: ANGULAR_SSR_DEPS, + rolldownOptions: { + plugins: [angularLinkerPlugin()], + }, + }, + }, + } as UserConfig['environments']; + } + + return overrides; + }, + + resolveId(id) { + if (id === ssrEntryMarkerPath || id === SSR_ENTRY_VIRTUAL_ID) { + return SSR_ENTRY_VIRTUAL_ID; + } + return null; + }, + + load(id) { + if (id !== SSR_ENTRY_VIRTUAL_ID) return null; + return generateSsrEntryWrapper(resolveEntryServer(), readIndexHtml()); + }, + + nitro: { + async setup(nitro) { + // refreshContext may not have run yet if nitro/vite resolved the + // plugin before `config()`; fall back to nitro's own root. + if (!context || context.rootDir === '.') { + refreshContext(nitro.options.rootDir); + } + + const hasAPIDir = existsSync( + resolve( + context.workspaceRoot, + context.rootDir, + `${context.sourceRoot}/server/routes/api`, + ), + ); + + const pageHandlers: NitroEventHandler[] = getPageHandlers({ + workspaceRoot: context.workspaceRoot, + sourceRoot: context.sourceRoot, + rootDir: context.rootDir, + additionalPagesDirs: options.additionalPagesDirs, + hasAPIDir, + }); + nitro.options.handlers.push(...pageHandlers); + + const serverDir = resolve( + context.workspaceRoot, + context.rootDir, + `${context.sourceRoot}/server`, + ); + if ( + existsSync(serverDir) && + !nitro.options.scanDirs.includes(serverDir) + ) { + nitro.options.scanDirs.push(serverDir); + } + + nitro.hooks.hook('rollup:before', (_n, rollupConfig: any) => { + if (Array.isArray(rollupConfig.plugins)) { + rollupConfig.plugins.push(pageEndpointsPlugin()); + } + }); + + await wirePrerender(nitro, options, context, apiPrefix); + + if (options.i18n) { + addPostRenderingHooks(nitro, [ + createI18nPostRenderingHook({ + defaultLocale: options.i18n.defaultLocale, + locales: options.i18n.locales, + }), + ]); + } + }, + }, + }; + + return plugin; +} + +/** + * Builds the SSR service entry source. The wrapper imports the user's + * `main.server.ts` Angular renderer and adapts it to the `{ fetch(req) }` + * shape that nitro/vite's service mechanism expects. + */ +function generateSsrEntryWrapper( + entryServer: string, + template: string, +): string { + return ` +import renderer from ${JSON.stringify(entryServer)}; + +const TEMPLATE = ${JSON.stringify(template)}; + +const normalizeRequestPath = (url) => + url.replace(/\\/index\\.html(?=$|[?#])/, '/'); + +export default { + async fetch(req) { + const url = new URL(req.url); + const requestPath = normalizeRequestPath(url.pathname); + + if (req.headers.get('x-analog-no-ssr') === 'true') { + return new Response(TEMPLATE, { + status: 200, + headers: { 'content-type': 'text/html; charset=utf-8' }, + }); + } + + const reqShim = { + headers: Object.fromEntries(req.headers.entries()), + url: requestPath, + originalUrl: requestPath, + connection: {}, + }; + + try { + const html = await renderer(requestPath, TEMPLATE, { req: reqShim }); + return new Response(html, { + status: 200, + headers: { 'content-type': 'text/html; charset=utf-8' }, + }); + } catch (err) { + console.error('[analog ssr]', err); + return new Response(TEMPLATE, { + status: 500, + headers: { 'content-type': 'text/html; charset=utf-8' }, + }); + } + }, +}; +`; +} + +async function wirePrerender( + nitro: Nitro, + options: Options, + context: NitroPluginContext, + apiPrefix: string, +): Promise { + const prerender = options.prerender; + if (!prerender && !options.i18n) return; + + const { routes: collected, sitemaps: routeSitemaps } = await collectRoutes( + prerender?.routes, + context, + ); + + const expanded = options.i18n + ? expandRoutesWithLocales(collected, { + defaultLocale: options.i18n.defaultLocale, + locales: options.i18n.locales, + }) + : collected; + + const nitroPrerender = (nitro.options.prerender ??= {}) as Record< + string, + any + >; + nitroPrerender.routes ??= []; + nitroPrerender.routes.push(...expanded); + if (prerender?.discover ?? false) { + nitroPrerender.crawlLinks = true; + } + + if (prerender?.postRenderingHooks?.length) { + addPostRenderingHooks(nitro, prerender.postRenderingHooks); + } + + const sitemapConfig = prerender?.sitemap; + if (sitemapConfig) { + nitro.hooks.hook('prerender:done', async (result) => { + const prerenderedRoutes = (result?.prerenderedRoutes ?? []).map( + (r: PrerenderRoute) => r.route, + ); + const publicDir = resolve(nitro.options.output.publicDir); + await buildSitemap( + {} as any, + sitemapConfig, + prerenderedRoutes, + publicDir, + routeSitemaps, + { apiPrefix }, + ); + }); + } +} + +async function collectRoutes( + routesInput: Options['prerender'] extends infer P + ? P extends { routes?: infer R } + ? R + : never + : never, + context: NitroPluginContext, +): Promise<{ routes: string[]; sitemaps: Record }> { + const out: string[] = []; + const sitemaps: Record = {}; + + if (!routesInput) return { routes: out, sitemaps }; + + const inputs = Array.isArray(routesInput) + ? routesInput + : typeof routesInput === 'function' + ? await routesInput() + : []; + + for (const entry of inputs) { + if (!entry) continue; + + if (typeof entry === 'string') { + out.push(entry); + continue; + } + + if ('contentDir' in entry) { + const dir = entry as PrerenderContentDir; + const files = getMatchingContentFilesWithFrontMatter( + context.workspaceRoot, + context.rootDir, + dir.contentDir, + !!dir.recursive, + ); + for (const file of files) { + const route = dir.transform(file); + if (route === false) continue; + out.push(route); + if (dir.sitemap) { + sitemaps[route] = + typeof dir.sitemap === 'function' + ? ( + dir.sitemap as ( + f: PrerenderContentFile, + ) => PrerenderSitemapConfig + )(file) + : dir.sitemap; + } + } + continue; + } + + if ('route' in entry) { + const cfg = entry as PrerenderRouteConfig; + out.push(cfg.route); + if (cfg.sitemap) { + sitemaps[cfg.route] = cfg.sitemap; + } + } + } + + return { routes: out, sitemaps }; +} From beda21b93b583655c3f9237c76c98b460f8803b4 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Fri, 22 May 2026 21:55:06 -0500 Subject: [PATCH 04/65] feat(platform): wire analog() to nitro/vite + analogNitroPlugin 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 analogjs/analog#2035 Co-Authored-By: Claude Opus 4.7 (1M context) --- .../platform/src/lib/platform-plugin.spec.ts | 34 +++++++++++-------- packages/platform/src/lib/platform-plugin.ts | 9 +++-- 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/packages/platform/src/lib/platform-plugin.spec.ts b/packages/platform/src/lib/platform-plugin.spec.ts index 6791294ee..b51fa3159 100644 --- a/packages/platform/src/lib/platform-plugin.spec.ts +++ b/packages/platform/src/lib/platform-plugin.spec.ts @@ -1,7 +1,8 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; const { - viteNitroPluginSpy, + nitroFromViteSpy, + analogNitroPluginSpy, angularSpy, ssrBuildPluginSpy, injectHTMLPluginSpy, @@ -16,7 +17,8 @@ const { stylePipelineFactorySpy, stylePipelinePluginSpy, } = vi.hoisted(() => ({ - viteNitroPluginSpy: vi.fn(() => []), + nitroFromViteSpy: vi.fn(() => []), + analogNitroPluginSpy: vi.fn(() => ({ name: '@analogjs/nitro' })), angularSpy: vi.fn(() => []), ssrBuildPluginSpy: vi.fn(() => []), injectHTMLPluginSpy: vi.fn(() => []), @@ -36,12 +38,11 @@ const { stylePipelinePluginSpy: { name: 'community-style-pipeline' }, })); -vi.mock('@analogjs/vite-plugin-nitro', () => ({ - nitro: viteNitroPluginSpy, - default: viteNitroPluginSpy, +vi.mock('nitro/vite', () => ({ + nitro: nitroFromViteSpy, })); -vi.mock('@analogjs/vite-plugin-nitro/internal', () => ({ - debugInstances: [], +vi.mock('./nitro/analog-nitro-plugin.js', () => ({ + analogNitroPlugin: analogNitroPluginSpy, })); vi.mock('@analogjs/vite-plugin-angular', () => ({ angular: angularSpy, @@ -94,7 +95,8 @@ import { platformPlugin } from './platform-plugin.js'; describe('platformPlugin', () => { beforeEach(() => { vi.clearAllMocks(); - viteNitroPluginSpy.mockReturnValue([]); + nitroFromViteSpy.mockReturnValue([]); + analogNitroPluginSpy.mockReturnValue({ name: '@analogjs/nitro' }); angularSpy.mockReturnValue([]); ssrBuildPluginSpy.mockReturnValue([]); injectHTMLPluginSpy.mockReturnValue([]); @@ -112,7 +114,10 @@ describe('platformPlugin', () => { it('defaults ssr to true and passes that value to the composed plugins', () => { platformPlugin(); - expect(viteNitroPluginSpy).toHaveBeenCalledWith({ ssr: true }, undefined); + expect(nitroFromViteSpy).toHaveBeenCalledWith({}); + expect(analogNitroPluginSpy).toHaveBeenCalledWith( + expect.objectContaining({ ssr: true }), + ); expect(ssrBuildPluginSpy).toHaveBeenCalled(); expect(injectHTMLPluginSpy).toHaveBeenCalled(); }); @@ -120,7 +125,9 @@ describe('platformPlugin', () => { it('passes through ssr false without wiring SSR-only plugins', () => { platformPlugin({ ssr: false }); - expect(viteNitroPluginSpy).toHaveBeenCalledWith({ ssr: false }, undefined); + expect(analogNitroPluginSpy).toHaveBeenCalledWith( + expect.objectContaining({ ssr: false }), + ); expect(ssrBuildPluginSpy).not.toHaveBeenCalled(); expect(injectHTMLPluginSpy).not.toHaveBeenCalled(); }); @@ -160,9 +167,9 @@ describe('platformPlugin', () => { it('still includes non-Angular plugins when vite is set to false', () => { const plugins = platformPlugin({ vite: false }); - expect(viteNitroPluginSpy).toHaveBeenCalledWith( + expect(nitroFromViteSpy).toHaveBeenCalled(); + expect(analogNitroPluginSpy).toHaveBeenCalledWith( expect.objectContaining({ ssr: true }), - undefined, ); expect(routeGenerationPluginSpy).toHaveBeenCalled(); expect(serverModePluginSpy).toHaveBeenCalled(); @@ -197,11 +204,10 @@ describe('platformPlugin', () => { additionalPagesDirs: ['/libs/shared/feature'], }), ); - expect(viteNitroPluginSpy).toHaveBeenCalledWith( + expect(analogNitroPluginSpy).toHaveBeenCalledWith( expect.objectContaining({ additionalAPIDirs: ['/libs/shared/feature/src/api'], }), - undefined, ); expect(contentPluginSpy).toHaveBeenCalledWith( undefined, diff --git a/packages/platform/src/lib/platform-plugin.ts b/packages/platform/src/lib/platform-plugin.ts index e456c2bbc..21089cd17 100644 --- a/packages/platform/src/lib/platform-plugin.ts +++ b/packages/platform/src/lib/platform-plugin.ts @@ -1,5 +1,5 @@ import { Plugin } from 'vite'; -import viteNitroPlugin from '@analogjs/vite-plugin-nitro'; +import { nitro } from 'nitro/vite'; import angular from '@analogjs/vite-plugin-angular'; import { mapValues, union } from 'es-toolkit'; @@ -20,6 +20,7 @@ import { serverModePlugin } from '../server-mode-plugin.js'; import { routeGenerationPlugin } from './route-generation-plugin.js'; import { resolveStylePipelinePlugins } from './style-pipeline.js'; import { i18nComponentRegistryPlugin } from './i18n-component-registry-plugin.js'; +import { analogNitroPlugin } from './nitro/analog-nitro-plugin.js'; // Bridge Plugin types from external @analogjs packages that resolve a different vite instance function externalPlugins(plugins: unknown): Plugin[] { @@ -90,7 +91,11 @@ export function platformPlugin(opts: Options = {}): Plugin[] { activateDeferredDebug(command); }, }, - ...externalPlugins(viteNitroPlugin(platformOptions as any, nitroOptions)), + ...externalPlugins(nitro(nitroOptions ?? {})), + analogNitroPlugin({ + ...platformOptions, + nitro: nitroOptions, + }), ...(platformOptions.ssr ? [...ssrBuildPlugin(), ...injectHTMLPlugin()] : []), From 88919d3e515500ce42b65f691fe684bf4898fc36 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Fri, 22 May 2026 21:55:39 -0500 Subject: [PATCH 05/65] feat(vite-plugin-nitro)!: deprecate package; orchestration moved to @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 analogjs/analog#2035 Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/platform/package.json | 1 - packages/vite-plugin-nitro/package.json | 29 +- packages/vite-plugin-nitro/src/index.spec.ts | 9 - packages/vite-plugin-nitro/src/index.ts | 47 +- .../src/lib/build-server.spec.ts | 73 - .../vite-plugin-nitro/src/lib/build-server.ts | 106 - .../src/lib/build-sitemap.spec.ts | 222 --- .../src/lib/build-sitemap.ts | 375 ---- .../src/lib/build-ssr.spec.ts | 65 - .../vite-plugin-nitro/src/lib/build-ssr.ts | 73 - .../src/lib/hooks/post-rendering-hook.ts | 12 - .../lib/hooks/post-rendering-hooks.spec.ts | 33 - packages/vite-plugin-nitro/src/lib/options.ts | 213 -- .../src/lib/page-endpoints.spec.ts | 84 - .../src/lib/plugins/dev-server-plugin.ts | 176 -- .../src/lib/plugins/page-endpoints.ts | 105 - .../vite-plugin-nitro/src/lib/utils/debug.ts | 8 - .../src/lib/utils/get-content-files.spec.ts | 77 - .../src/lib/utils/get-content-files.ts | 133 -- .../src/lib/utils/get-page-handlers.ts | 112 -- .../src/lib/utils/i18n-prerender.spec.ts | 186 -- .../src/lib/utils/i18n-prerender.ts | 105 - .../src/lib/utils/load-esm.ts | 27 - .../src/lib/utils/node-web-bridge.spec.ts | 31 - .../src/lib/utils/node-web-bridge.ts | 110 -- .../src/lib/utils/register-dev-middleware.ts | 67 - .../lib/utils/register-i18n-watcher.spec.ts | 84 - .../src/lib/utils/register-i18n-watcher.ts | 28 - .../src/lib/utils/renderers.spec.ts | 41 - .../src/lib/utils/renderers.ts | 141 -- .../src/lib/utils/rolldown.spec.ts | 33 - .../src/lib/utils/rolldown.ts | 9 - .../src/lib/vite-nitro-plugin.spec.data.ts | 73 - .../src/lib/vite-plugin-nitro.spec.ts | 951 --------- .../src/lib/vite-plugin-nitro.ts | 1735 ----------------- .../test-data/content/01-first.md | 7 - .../test-data/content/02-second.md | 6 - .../test-data/content/03-third.md | 7 - packages/vite-plugin-nitro/vite.config.lib.ts | 1 - pnpm-lock.yaml | 410 +++- 40 files changed, 360 insertions(+), 5645 deletions(-) delete mode 100644 packages/vite-plugin-nitro/src/index.spec.ts delete mode 100644 packages/vite-plugin-nitro/src/lib/build-server.spec.ts delete mode 100644 packages/vite-plugin-nitro/src/lib/build-server.ts delete mode 100644 packages/vite-plugin-nitro/src/lib/build-sitemap.spec.ts delete mode 100644 packages/vite-plugin-nitro/src/lib/build-sitemap.ts delete mode 100644 packages/vite-plugin-nitro/src/lib/build-ssr.spec.ts delete mode 100644 packages/vite-plugin-nitro/src/lib/build-ssr.ts delete mode 100644 packages/vite-plugin-nitro/src/lib/hooks/post-rendering-hook.ts delete mode 100644 packages/vite-plugin-nitro/src/lib/hooks/post-rendering-hooks.spec.ts delete mode 100644 packages/vite-plugin-nitro/src/lib/options.ts delete mode 100644 packages/vite-plugin-nitro/src/lib/page-endpoints.spec.ts delete mode 100644 packages/vite-plugin-nitro/src/lib/plugins/dev-server-plugin.ts delete mode 100644 packages/vite-plugin-nitro/src/lib/plugins/page-endpoints.ts delete mode 100644 packages/vite-plugin-nitro/src/lib/utils/debug.ts delete mode 100644 packages/vite-plugin-nitro/src/lib/utils/get-content-files.spec.ts delete mode 100644 packages/vite-plugin-nitro/src/lib/utils/get-content-files.ts delete mode 100644 packages/vite-plugin-nitro/src/lib/utils/get-page-handlers.ts delete mode 100644 packages/vite-plugin-nitro/src/lib/utils/i18n-prerender.spec.ts delete mode 100644 packages/vite-plugin-nitro/src/lib/utils/i18n-prerender.ts delete mode 100644 packages/vite-plugin-nitro/src/lib/utils/load-esm.ts delete mode 100644 packages/vite-plugin-nitro/src/lib/utils/node-web-bridge.spec.ts delete mode 100644 packages/vite-plugin-nitro/src/lib/utils/node-web-bridge.ts delete mode 100644 packages/vite-plugin-nitro/src/lib/utils/register-dev-middleware.ts delete mode 100644 packages/vite-plugin-nitro/src/lib/utils/register-i18n-watcher.spec.ts delete mode 100644 packages/vite-plugin-nitro/src/lib/utils/register-i18n-watcher.ts delete mode 100644 packages/vite-plugin-nitro/src/lib/utils/renderers.spec.ts delete mode 100644 packages/vite-plugin-nitro/src/lib/utils/renderers.ts delete mode 100644 packages/vite-plugin-nitro/src/lib/utils/rolldown.spec.ts delete mode 100644 packages/vite-plugin-nitro/src/lib/utils/rolldown.ts delete mode 100644 packages/vite-plugin-nitro/src/lib/vite-nitro-plugin.spec.data.ts delete mode 100644 packages/vite-plugin-nitro/src/lib/vite-plugin-nitro.spec.ts delete mode 100644 packages/vite-plugin-nitro/src/lib/vite-plugin-nitro.ts delete mode 100644 packages/vite-plugin-nitro/test-data/content/01-first.md delete mode 100644 packages/vite-plugin-nitro/test-data/content/02-second.md delete mode 100644 packages/vite-plugin-nitro/test-data/content/03-third.md diff --git a/packages/platform/package.json b/packages/platform/package.json index 6dcca0f8b..68cc2db56 100644 --- a/packages/platform/package.json +++ b/packages/platform/package.json @@ -46,7 +46,6 @@ "tinyglobby": "catalog:", "nitro": "catalog:", "@analogjs/vite-plugin-angular": "workspace:*", - "@analogjs/vite-plugin-nitro": "workspace:*", "@babel/core": "catalog:", "ofetch": "catalog:", "oxc-parser": "catalog:", diff --git a/packages/vite-plugin-nitro/package.json b/packages/vite-plugin-nitro/package.json index c5a8411e6..249c7a3bb 100644 --- a/packages/vite-plugin-nitro/package.json +++ b/packages/vite-plugin-nitro/package.json @@ -1,7 +1,7 @@ { "name": "@analogjs/vite-plugin-nitro", "version": "3.0.0-alpha.55", - "description": "A Vite plugin for adding a nitro API server", + "description": "Deprecated — Nitro orchestration moved to @analogjs/platform. This package no longer exposes a plugin.", "type": "module", "author": "Brandon Roberts ", "exports": { @@ -10,11 +10,6 @@ "import": "./dist/src/index.js", "default": "./dist/src/index.js" }, - "./internal": { - "types": "./dist/src/lib/utils/debug.d.ts", - "import": "./dist/src/lib/utils/debug.js", - "default": "./dist/src/lib/utils/debug.js" - }, "./package.json": "./package.json" }, "keywords": [ @@ -38,23 +33,7 @@ "type": "github", "url": "https://github.com/sponsors/brandonroberts" }, - "peerDependencies": { - "sharp": ">=0.32.0" - }, - "peerDependenciesMeta": { - "sharp": { - "optional": true - } - }, - "dependencies": { - "defu": "catalog:", - "nitro": "catalog:", - "obug": "catalog:", - "ofetch": "catalog:", - "oxc-parser": "catalog:", - "radix3": "catalog:", - "xmlbuilder2": "catalog:" - }, + "dependencies": {}, "ng-update": { "packageGroup": [ "@analogjs/platform", @@ -67,10 +46,6 @@ ], "migrations": "./migrations/migration.json" }, - "imports": { - "#analog/ssr": "./dist/src/index.js", - "#analog/index": "./dist/src/index.js" - }, "publishConfig": { "access": "public", "provenance": true diff --git a/packages/vite-plugin-nitro/src/index.spec.ts b/packages/vite-plugin-nitro/src/index.spec.ts deleted file mode 100644 index 82bc0e57e..000000000 --- a/packages/vite-plugin-nitro/src/index.spec.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import nitroDefault, { nitro } from './index.js'; - -describe('vite-plugin-nitro entrypoint', () => { - it('exports the nitro plugin as both named and default exports', () => { - expect(nitroDefault).toBe(nitro); - }); -}); diff --git a/packages/vite-plugin-nitro/src/index.ts b/packages/vite-plugin-nitro/src/index.ts index 9fc349045..c9c7d8145 100644 --- a/packages/vite-plugin-nitro/src/index.ts +++ b/packages/vite-plugin-nitro/src/index.ts @@ -1,31 +1,16 @@ -import { nitro } from './lib/vite-plugin-nitro.js'; -export { debugInstances } from './lib/utils/debug.js'; -export { nitro } from './lib/vite-plugin-nitro.js'; -export type { - Options, - SitemapConfig, - SitemapEntry, - SitemapExcludeRule, - SitemapPriority, - SitemapRouteDefinition, - SitemapRouteInput, - SitemapRouteSource, - SitemapTransform, - PrerenderSitemapConfig, - PrerenderRouteConfig, - PrerenderContentDir, - PrerenderContentFile, - I18nPrerenderOptions, -} from './lib/options.js'; - -declare module 'nitro/types' { - interface NitroRouteConfig { - ssr?: boolean; - } - - interface NitroRouteRules { - ssr?: boolean; - } -} - -export default nitro; +/** + * @deprecated `@analogjs/vite-plugin-nitro` has been deprecated. The Nitro + * orchestration lives in `@analogjs/platform`, which composes Nitro's + * first-party Vite plugin (`nitro/vite`) with Analog's `analogNitroPlugin`. + * + * Migration: + * - Replace `import { nitro } from '@analogjs/vite-plugin-nitro'` with + * `import { analog } from '@analogjs/platform'` and use `analog()` in + * your Vite plugin chain. + * - Type exports (`SitemapConfig`, `PrerenderRouteConfig`, + * `PrerenderContentDir`, etc.) are re-exported from `@analogjs/platform`. + * + * This package is kept as a placeholder so existing dependency declarations + * don't fail to install, but it no longer exposes a Vite plugin. + */ +export {}; diff --git a/packages/vite-plugin-nitro/src/lib/build-server.spec.ts b/packages/vite-plugin-nitro/src/lib/build-server.spec.ts deleted file mode 100644 index 22818eb41..000000000 --- a/packages/vite-plugin-nitro/src/lib/build-server.spec.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { afterEach, describe, expect, it, vi } from 'vitest'; -import { mkdtempSync, mkdirSync, rmSync } from 'node:fs'; -import { tmpdir } from 'node:os'; -import { join, resolve } from 'node:path'; - -vi.mock('nitro/builder', () => ({ - build: vi.fn(), - copyPublicAssets: vi.fn(), - createNitro: vi.fn(), - prepare: vi.fn(), - prerender: vi.fn(), -})); - -import { - build, - copyPublicAssets, - createNitro, - prepare, - prerender, -} from 'nitro/builder'; - -import { buildServer } from './build-server'; - -describe('buildServer', () => { - afterEach(() => { - vi.restoreAllMocks(); - }); - - it('forces rollup bundler and builds successfully', async () => { - const workspaceRoot = mkdtempSync(join(tmpdir(), 'analog-build-server-')); - const outputDir = resolve(workspaceRoot, '.output'); - const serverDir = resolve(outputDir, 'server'); - const publicDir = resolve(outputDir, 'public'); - - mkdirSync(serverDir, { recursive: true }); - mkdirSync(publicDir, { recursive: true }); - - vi.mocked(createNitro).mockResolvedValue({ - options: { - framework: { - name: 'nitro', - version: '3.0.0', - }, - output: { - dir: outputDir, - publicDir, - serverDir, - }, - preset: 'node-server', - routeRules: {}, - static: false, - }, - close: vi.fn().mockResolvedValue(undefined), - } as never); - vi.mocked(prepare).mockResolvedValue(undefined as never); - vi.mocked(copyPublicAssets).mockResolvedValue(undefined as never); - vi.mocked(prerender).mockResolvedValue(undefined as never); - vi.mocked(build).mockResolvedValue(undefined as never); - - try { - await buildServer({}, { output: { publicDir } }); - - expect(createNitro).toHaveBeenCalledWith( - expect.objectContaining({ - builder: 'rollup', - }), - ); - expect(build).toHaveBeenCalled(); - } finally { - rmSync(workspaceRoot, { recursive: true, force: true }); - } - }); -}); diff --git a/packages/vite-plugin-nitro/src/lib/build-server.ts b/packages/vite-plugin-nitro/src/lib/build-server.ts deleted file mode 100644 index 046a639dd..000000000 --- a/packages/vite-plugin-nitro/src/lib/build-server.ts +++ /dev/null @@ -1,106 +0,0 @@ -import type { NitroConfig } from 'nitro/types'; -import { - build, - copyPublicAssets, - createNitro, - prepare, - prerender, -} from 'nitro/builder'; -import { mkdirSync, rmSync, writeFileSync } from 'node:fs'; -import { dirname, join } from 'node:path'; - -import { Options } from './options.js'; -import { addPostRenderingHooks } from './hooks/post-rendering-hook.js'; - -export function isVercelPreset(preset: string | undefined): boolean { - return !!preset?.toLowerCase().includes('vercel'); -} - -export async function buildServer( - options?: Options, - nitroConfig?: NitroConfig, - routeSourceFiles?: Record, -): Promise { - // ── Force Rollup as the server bundler ──────────────────────────── - // - // Nitro v3 defaults to Rolldown when available. Rolldown is faster, - // but its module resolver cannot resolve relative chunk imports - // (e.g. `./assets/core-DTazUigR.js`) from a rebundled SSR entry on - // Windows. The prerender build fails with: - // - // [RESOLVE_ERROR] Could not resolve './assets/core-DTazUigR.js' - // in ../../dist/apps/blog-app/ssr/main.server.js - // - // This is a known Rolldown limitation with cross-directory relative - // paths on Windows (backslash vs forward-slash normalisation). - // Rollup handles these paths correctly on all platforms. - // - // The dev server already uses `builder: 'rollup'` for the same - // reason. Default to Rollup here too until Rolldown's resolver - // matures. The caller can still opt in to Rolldown explicitly via - // nitroConfig.builder if their platform supports it. - const nitro = await createNitro({ - dev: false, - preset: process.env['BUILD_PRESET'], - ...nitroConfig, - builder: nitroConfig?.builder ?? 'rollup', - }); - - if (options?.prerender?.postRenderingHooks) { - addPostRenderingHooks(nitro, options.prerender.postRenderingHooks); - } - - await prepare(nitro); - await copyPublicAssets(nitro); - - if ( - options?.ssr && - nitroConfig?.prerender?.routes && - (nitroConfig?.prerender?.routes.find((route) => route === '/') || - nitroConfig?.prerender?.routes?.length === 0) - ) { - const indexFileExts = ['', '.br', '.gz']; - - indexFileExts.forEach((fileExt) => { - // Remove the root index.html(.br|.gz) files - const indexFilePath = join( - nitroConfig?.output?.publicDir ?? '', - `index.html${fileExt}`, - ); - - rmSync(indexFilePath, { force: true }); - }); - } - - if ( - nitroConfig?.prerender?.routes && - nitroConfig?.prerender?.routes?.length > 0 - ) { - console.log(`Prerendering static pages...`); - await prerender(nitro); - } - - if (routeSourceFiles && Object.keys(routeSourceFiles).length > 0) { - const publicDir = nitroConfig?.output?.publicDir; - if (!publicDir) { - throw new Error( - 'Nitro public output directory is required to write route source files.', - ); - } - - for (const [route, content] of Object.entries(routeSourceFiles)) { - const outputPath = join(publicDir, `${route}.md`); - const outputDir = dirname(outputPath); - mkdirSync(outputDir, { recursive: true }); - - writeFileSync(outputPath, content, 'utf8'); - } - } - - if (!options?.static) { - console.log('Building Server...'); - await build(nitro); - } - - await nitro.close(); -} diff --git a/packages/vite-plugin-nitro/src/lib/build-sitemap.spec.ts b/packages/vite-plugin-nitro/src/lib/build-sitemap.spec.ts deleted file mode 100644 index 5d16b7477..000000000 --- a/packages/vite-plugin-nitro/src/lib/build-sitemap.spec.ts +++ /dev/null @@ -1,222 +0,0 @@ -import { existsSync, mkdirSync, writeFileSync } from 'node:fs'; -import { buildSitemap } from './build-sitemap'; - -vi.mock('node:fs', () => ({ - existsSync: vi.fn(() => true), - mkdirSync: vi.fn(), - writeFileSync: vi.fn(), -})); - -describe('build sitemap', () => { - const config = { root: 'root' }; - const existsSyncMock = vi.mocked(existsSync); - const mkdirSyncMock = vi.mocked(mkdirSync); - const writeFileSyncMock = vi.mocked(writeFileSync); - - afterEach(() => { - vi.restoreAllMocks(); - existsSyncMock.mockReturnValue(true); - mkdirSyncMock.mockReset(); - writeFileSyncMock.mockReset(); - }); - - it('should not perform functionality if no predefined routes are present', async () => { - await buildSitemap(config, { host: 'https://host.com' }, [], '', {}); - - expect(writeFileSyncMock).not.toHaveBeenCalled(); - }); - - it('should preserve route sitemap metadata when the host has a trailing slash', async () => { - existsSyncMock.mockReturnValue(true); - - await buildSitemap( - config, - { host: 'https://host.com/' }, - ['/blog'], - '/tmp/analog/public', - { - '/blog': { - lastmod: '2024-01-15', - changefreq: 'weekly', - priority: 0.8, - }, - }, - ); - - expect(writeFileSyncMock).toHaveBeenCalledWith( - expect.stringContaining('/tmp/analog/public/sitemap.xml'), - expect.stringContaining('https://host.com/blog'), - ); - expect(writeFileSyncMock.mock.calls[0]?.[1]).toContain( - '2024-01-15', - ); - expect(writeFileSyncMock.mock.calls[0]?.[1]).toContain( - 'weekly', - ); - expect(writeFileSyncMock.mock.calls[0]?.[1]).toContain( - '0.8', - ); - }); - - it('should apply include defaults transform exclude and internal route filtering', async () => { - existsSyncMock.mockReturnValue(true); - - await buildSitemap( - config, - { - host: 'https://host.com', - defaults: { - changefreq: 'monthly', - priority: 0.4, - }, - include: async () => [ - '/extra', - { - route: '/docs/hello world', - lastmod: '2024-01-01', - }, - ], - exclude: ['/drafts/**', /^\/admin/], - transform: (entry) => - entry.route === '/extra' - ? { - route: '/extra-updated', - priority: 0.9, - } - : { - route: entry.route, - }, - }, - [ - '/products', - '/products', - '/drafts/preview', - '/api/_analog/pages/products', - ], - '/tmp/analog/public', - {}, - ); - - const xml = writeFileSyncMock.mock.calls[0]?.[1] ?? ''; - expect(xml).toContain('https://host.com/products'); - expect(xml).toContain('monthly'); - expect(xml).toContain('0.4'); - expect(xml).toContain('https://host.com/extra-updated'); - expect(xml).toContain('0.9'); - expect(xml).toContain('https://host.com/docs/hello%20world'); - expect(xml).not.toContain('/drafts/preview'); - expect(xml).not.toContain('/api/_analog/pages/products'); - }); - - it('should support predicate exclude rules and transform returning false', async () => { - existsSyncMock.mockReturnValue(true); - - await buildSitemap( - config, - { - host: 'https://host.com', - exclude: [async (entry) => entry.route === '/private'], - transform: (entry) => - entry.route === '/skip-me' - ? false - : { - route: entry.route, - }, - }, - ['/public', '/private', '/skip-me'], - '/tmp/analog/public', - {}, - ); - - const xml = writeFileSyncMock.mock.calls[0]?.[1] ?? ''; - expect(xml).toContain('https://host.com/public'); - expect(xml).not.toContain('/private'); - expect(xml).not.toContain('/skip-me'); - expect(xml).not.toContain(''); - }); - - it('should resolve callable per-route sitemap metadata', async () => { - existsSyncMock.mockReturnValue(true); - - await buildSitemap( - config, - { host: 'https://host.com/' }, - ['/blog'], - '/tmp/analog/public', - { - '/blog': () => ({ - lastmod: '2024-04-01', - changefreq: 'daily', - }), - }, - ); - - const xml = writeFileSyncMock.mock.calls[0]?.[1] ?? ''; - expect(xml).toContain('https://host.com/blog'); - expect(xml).toContain('2024-04-01'); - expect(xml).toContain('daily'); - }); - - it('should filter internal routes when a custom api prefix is configured', async () => { - existsSyncMock.mockReturnValue(true); - - await buildSitemap( - config, - { host: 'https://host.com' }, - ['/shop', '/functions/_analog/pages/shop'], - '/tmp/analog/public', - {}, - { apiPrefix: 'functions' }, - ); - - const xml = writeFileSyncMock.mock.calls[0]?.[1] ?? ''; - expect(xml).toContain('https://host.com/shop'); - expect(xml).not.toContain('/functions/_analog/pages/shop'); - }); - - it('should create the output directory when it does not exist', async () => { - existsSyncMock.mockReturnValue(false); - - await buildSitemap( - config, - { host: 'https://host.com' }, - ['/'], - '/tmp/generated/public', - {}, - ); - - expect(mkdirSyncMock).toHaveBeenCalledWith('/tmp/generated/public', { - recursive: true, - }); - expect(writeFileSyncMock).toHaveBeenCalled(); - }); - - it('should refuse to write to the current working directory', async () => { - existsSyncMock.mockReturnValue(true); - const errorSpy = vi - .spyOn(console, 'error') - .mockImplementation(() => undefined); - - await buildSitemap(config, { host: 'https://host.com' }, ['/'], '', {}); - - expect(writeFileSyncMock).not.toHaveBeenCalled(); - expect(errorSpy).toHaveBeenCalledWith( - expect.stringContaining('Unable to write file at'), - expect.any(Error), - ); - }); - - it('should reject invalid sitemap hosts before writing output', async () => { - await expect( - buildSitemap( - config, - { host: 'not-a-valid-url' }, - ['/'], - '/tmp/analog/public', - {}, - ), - ).rejects.toThrow(); - - expect(writeFileSyncMock).not.toHaveBeenCalled(); - }); -}); diff --git a/packages/vite-plugin-nitro/src/lib/build-sitemap.ts b/packages/vite-plugin-nitro/src/lib/build-sitemap.ts deleted file mode 100644 index 4f896d830..000000000 --- a/packages/vite-plugin-nitro/src/lib/build-sitemap.ts +++ /dev/null @@ -1,375 +0,0 @@ -import { existsSync, mkdirSync, writeFileSync } from 'node:fs'; -import { resolve } from 'node:path'; -import { create } from 'xmlbuilder2'; -import { XMLBuilder } from 'xmlbuilder2/lib/interfaces'; -import { UserConfig } from 'vite'; -import { - PrerenderSitemapConfig, - SitemapConfig, - SitemapEntry, - SitemapExcludeRule, - SitemapRouteDefinition, - SitemapRouteInput, - SitemapRouteSource, -} from './options'; - -type RouteSitemapConfig = - | PrerenderSitemapConfig - | (() => PrerenderSitemapConfig) - | undefined; - -export type PagesJson = SitemapEntry; - -export interface BuildSitemapOptions { - apiPrefix?: string; -} - -export async function buildSitemap( - _config: UserConfig, - sitemapConfig: SitemapConfig, - routes: (string | undefined)[] | (() => Promise<(string | undefined)[]>), - outputDir: string, - routeSitemaps: Record, - buildOptions: BuildSitemapOptions = {}, -): Promise { - const host = normalizeSitemapHost(sitemapConfig.host); - const routeList = await collectSitemapRoutes(routes, sitemapConfig.include); - const sitemapData = await resolveSitemapEntries( - routeList, - host, - routeSitemaps, - sitemapConfig, - buildOptions, - ); - - if (!sitemapData.length) { - return; - } - - const sitemap = createXml('urlset'); - - for (const item of sitemapData) { - const page = sitemap.ele('url'); - page.ele('loc').txt(item.loc); - - if (item.lastmod) { - page.ele('lastmod').txt(item.lastmod); - } - - if (item.changefreq) { - page.ele('changefreq').txt(item.changefreq); - } - - if (item.priority !== undefined) { - page.ele('priority').txt(String(item.priority)); - } - } - - const resolvedOutputDir = resolve(outputDir); - const mapPath = resolve(resolvedOutputDir, 'sitemap.xml'); - try { - if (!resolvedOutputDir || resolvedOutputDir === resolve()) { - throw new Error( - 'Refusing to write the sitemap to the current working directory. Expected the Nitro public output directory instead.', - ); - } - - if (!existsSync(resolvedOutputDir)) { - mkdirSync(resolvedOutputDir, { recursive: true }); - } - console.log(`Writing sitemap at ${mapPath}`); - writeFileSync(mapPath, sitemap.end({ prettyPrint: true })); - } catch (e) { - console.error(`Unable to write file at ${mapPath}`, e); - } -} - -async function resolveSitemapEntries( - routes: SitemapRouteInput[], - host: string, - routeSitemaps: Record, - sitemapConfig: SitemapConfig, - buildOptions: BuildSitemapOptions, -): Promise { - const defaults = sitemapConfig.defaults ?? {}; - const seen = new Set(); - const entries: SitemapEntry[] = []; - - for (const route of routes) { - const entry = await toSitemapEntry( - route, - host, - routeSitemaps, - defaults, - sitemapConfig.transform, - ); - - if (!entry) { - continue; - } - - if ( - isInternalSitemapRoute(entry.route, buildOptions.apiPrefix) || - (await isExcludedSitemapRoute(entry, sitemapConfig.exclude)) - ) { - continue; - } - - if (seen.has(entry.loc)) { - continue; - } - - seen.add(entry.loc); - entries.push(entry); - } - - return entries; -} - -async function toSitemapEntry( - route: SitemapRouteInput, - host: string, - routeSitemaps: Record, - defaults: PrerenderSitemapConfig, - transform: SitemapConfig['transform'], -): Promise { - const normalizedRoute = normalizeSitemapRoute( - typeof route === 'string' ? route : route?.route, - ); - if (!normalizedRoute) { - return undefined; - } - - const baseEntry = createSitemapEntry( - { - ...defaults, - ...resolveRouteSitemapConfig(routeSitemaps[normalizedRoute]), - ...(typeof route === 'object' ? route : {}), - route: normalizedRoute, - }, - host, - ); - - if (!transform) { - return baseEntry; - } - - const transformed = await transform(baseEntry); - if (!transformed) { - return undefined; - } - - return createSitemapEntry( - { - ...baseEntry, - ...transformed, - }, - host, - ); -} - -function createSitemapEntry( - routeDefinition: SitemapRouteDefinition, - host: string, -): SitemapEntry { - const route = normalizeSitemapRoute(routeDefinition.route) ?? '/'; - - return { - route, - loc: new URL(route, ensureTrailingSlash(host)).toString(), - lastmod: routeDefinition.lastmod, - changefreq: routeDefinition.changefreq, - priority: routeDefinition.priority, - }; -} - -function resolveRouteSitemapConfig( - config: RouteSitemapConfig, -): PrerenderSitemapConfig { - if (!config) { - return {}; - } - - return typeof config === 'function' ? config() : config; -} - -function normalizeSitemapHost(host: string): string { - const resolvedHost = new URL(host); - resolvedHost.hash = ''; - return resolvedHost.toString(); -} - -function ensureTrailingSlash(host: string): string { - return host.endsWith('/') ? host : `${host}/`; -} - -function normalizeSitemapRoute(route: string | undefined): string | undefined { - if (!route) { - return undefined; - } - - const trimmedRoute = route.trim(); - if (!trimmedRoute) { - return undefined; - } - - const pathWithQuery = trimmedRoute.split('#', 1)[0] ?? ''; - const [pathname, search] = pathWithQuery.split('?', 2); - const normalizedPathname = pathname - ? `/${pathname.replace(/^\/+/, '').replace(/\/{2,}/g, '/')}` - : '/'; - - return search ? `${normalizedPathname}?${search}` : normalizedPathname; -} - -function isInternalSitemapRoute(route: string, apiPrefix = 'api'): boolean { - const normalizedApiPrefix = normalizeSitemapRoute(`/${apiPrefix}`) ?? '/api'; - return ( - route === `${normalizedApiPrefix}/_analog/pages` || - route.startsWith(`${normalizedApiPrefix}/_analog/pages/`) - ); -} - -async function isExcludedSitemapRoute( - entry: SitemapEntry, - excludeRules: SitemapExcludeRule[] | undefined, -): Promise { - if (!excludeRules?.length) { - return false; - } - - for (const rule of excludeRules) { - if (typeof rule === 'function') { - if (await rule(entry)) { - return true; - } - continue; - } - - if (rule instanceof RegExp) { - if (rule.test(entry.route)) { - return true; - } - continue; - } - - if (toGlobRegExp(rule).test(entry.route)) { - return true; - } - } - - return false; -} - -function toGlobRegExp(pattern: string): RegExp { - const doubleStarToken = '__ANALOG_DOUBLE_STAR__'; - const singleStarToken = '__ANALOG_SINGLE_STAR__'; - const escapedPattern = pattern - .replace(/\*\*/g, doubleStarToken) - .replace(/\*/g, singleStarToken) - .replace(/[.+^${}()|[\]\\]/g, '\\$&'); - const regexPattern = escapedPattern - .replace(new RegExp(doubleStarToken, 'g'), '.*') - .replace(new RegExp(singleStarToken, 'g'), '[^/]*'); - return new RegExp(`^${regexPattern}$`); -} - -async function collectSitemapRoutes( - routes: (string | undefined)[] | (() => Promise<(string | undefined)[]>), - include?: SitemapRouteSource, -): Promise { - const routeList = await resolveRouteInputs(routes); - const includedRoutes = include ? await resolveRouteInputs(include) : []; - return [...routeList, ...includedRoutes]; -} - -async function resolveRouteInputs( - routes: - | SitemapRouteSource - | (string | undefined)[] - | (() => Promise<(string | undefined)[]>), -): Promise { - let routeList: SitemapRouteInput[]; - - if (typeof routes === 'function') { - routeList = await routes(); - } else if (Array.isArray(routes)) { - routeList = routes; - } else { - routeList = []; - } - - return routeList.filter(Boolean); -} - -/** - * Generates hreflang alternate URLs for a given page URL. - * For a URL like `https://example.com/fr/about`, it produces alternates - * for all configured locales. - */ -export function getHreflangAlternates( - pageUrl: string, - host: string, - i18n: I18nPrerenderOptions, -): { locale: string; href: string }[] { - const alternates: { locale: string; href: string }[] = []; - const normalizedHost = host.replace(/\/+$/, ''); - - // Extract the path portion after the host - const path = pageUrl.replace(normalizedHost, ''); - - // Strip locale prefix to get the base path - const basePath = stripLocalePrefix(path, i18n.locales); - - for (const locale of i18n.locales) { - const localizedPath = - basePath === '/' || basePath === '' - ? `/${locale}` - : `/${locale}${basePath}`; - alternates.push({ - locale, - href: `${normalizedHost}${localizedPath}`, - }); - } - - // Add x-default pointing to the default locale variant - const defaultPath = - basePath === '/' || basePath === '' - ? `/${i18n.defaultLocale}` - : `/${i18n.defaultLocale}${basePath}`; - alternates.push({ - locale: 'x-default', - href: `${normalizedHost}${defaultPath}`, - }); - - return alternates; -} - -/** - * Strips a locale prefix from a URL path. - * E.g., '/fr/about' -> '/about', '/en' -> '/' - */ -export function stripLocalePrefix(path: string, locales: string[]): string { - const segments = path.split('/').filter(Boolean); - if (segments.length > 0 && locales.includes(segments[0])) { - const rest = segments.slice(1).join('/'); - return rest ? `/${rest}` : '/'; - } - return path || '/'; -} - -function createXml( - elementName: 'urlset' | 'sitemapindex', - includeXhtml = false, -): XMLBuilder { - const attrs: Record = { - xmlns: 'https://www.sitemaps.org/schemas/sitemap/0.9', - }; - if (includeXhtml) { - attrs['xmlns:xhtml'] = 'https://www.w3.org/1999/xhtml'; - } - - return create({ version: '1.0', encoding: 'UTF-8' }) - .ele(elementName, attrs) - .com(`This file was automatically generated by Analog.`); -} diff --git a/packages/vite-plugin-nitro/src/lib/build-ssr.spec.ts b/packages/vite-plugin-nitro/src/lib/build-ssr.spec.ts deleted file mode 100644 index fce106e13..000000000 --- a/packages/vite-plugin-nitro/src/lib/build-ssr.spec.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { describe, expect, it, vi } from 'vitest'; - -vi.mock('vite', async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - build: vi.fn(), - }; -}); - -import { build } from 'vite'; - -import { buildClientApp, buildSSRApp } from './build-ssr'; - -describe('build helpers', () => { - it('uses the client output directory for the explicit legacy rebuild', async () => { - const workspaceRoot = '/workspace'; - - await buildClientApp( - { - root: '/workspace/apps/my-app', - }, - { - workspaceRoot, - }, - ); - - expect(build).toHaveBeenCalledWith( - expect.objectContaining({ - build: expect.objectContaining({ - ssr: false, - outDir: '/workspace/dist/apps/my-app/client', - emptyOutDir: true, - }), - }), - ); - }); - - it('preserves client output when starting the SSR sub-build', async () => { - const workspaceRoot = '/workspace'; - - await buildSSRApp( - { - root: '/workspace/apps/my-app', - build: { - outDir: '../../dist/apps/my-app/client', - emptyOutDir: true, - }, - }, - { - workspaceRoot, - }, - ); - - expect(build).toHaveBeenCalledWith( - expect.objectContaining({ - build: expect.objectContaining({ - ssr: true, - outDir: '/workspace/dist/apps/my-app/ssr', - emptyOutDir: false, - }), - }), - ); - }); -}); diff --git a/packages/vite-plugin-nitro/src/lib/build-ssr.ts b/packages/vite-plugin-nitro/src/lib/build-ssr.ts deleted file mode 100644 index adc7fdee4..000000000 --- a/packages/vite-plugin-nitro/src/lib/build-ssr.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { build, mergeConfig, UserConfig } from 'vite'; -import { relative, resolve } from 'node:path'; - -import { Options } from './options.js'; -import { getBundleOptionsKey } from './utils/rolldown.js'; - -export async function buildClientApp( - config: UserConfig, - options?: Options, -): Promise { - const workspaceRoot = options?.workspaceRoot ?? process.cwd(); - const rootDir = relative(workspaceRoot, config.root || '.') || '.'; - const clientBuildConfig = mergeConfig(config, { - build: { - ssr: false, - outDir: - config.build?.outDir || - resolve(workspaceRoot, 'dist', rootDir, 'client'), - emptyOutDir: true, - }, - }); - - await build(clientBuildConfig); -} - -export async function buildSSRApp( - config: UserConfig, - options?: Options, -): Promise { - const workspaceRoot = options?.workspaceRoot ?? process.cwd(); - const sourceRoot = options?.sourceRoot ?? 'src'; - const rootDir = relative(workspaceRoot, config.root || '.') || '.'; - const bundleOptionsKey = getBundleOptionsKey(); - - /** - * SSR is built as a second pass from the already prepared Analog/Vite config. - * - * That means we intentionally start from the same base config used for the - * client build and then merge only the SSR-specific overrides (entry, outDir, - * `build.ssr`, etc). - * - * A side effect of this design is that the resolved SSR config can expose the - * same high-level Analog plugin chain more than once when Vite/Nitro replays - * shared plugins for the server environment. In particular, - * `@analogjs/vite-plugin-angular` may appear twice in `config.plugins` during - * SSR resolution: - * - once from the normal Analog platform plugin expansion - * - once from the reused/shared plugin graph for the SSR pass - * - * That does NOT imply the client build has two competing style registries. - * The client-side duplicate-registration guard in `vite-plugin-angular` - * therefore explicitly ignores `build.ssr === true` to avoid treating this - * valid SSR orchestration detail as a style-map bug. - */ - const ssrBuildConfig = mergeConfig(config, { - build: { - ssr: true, - [bundleOptionsKey]: { - input: - options?.entryServer || - resolve(workspaceRoot, rootDir, `${sourceRoot}/main.server.ts`), - }, - outDir: - options?.ssrBuildDir || resolve(workspaceRoot, 'dist', rootDir, 'ssr'), - // Preserve the client build output. The client pass already handled its - // own cleanup, and on Windows this nested SSR build can otherwise remove - // sibling artifacts that Nitro needs to read immediately afterward. - emptyOutDir: false, - }, - }); - - await build(ssrBuildConfig); -} diff --git a/packages/vite-plugin-nitro/src/lib/hooks/post-rendering-hook.ts b/packages/vite-plugin-nitro/src/lib/hooks/post-rendering-hook.ts deleted file mode 100644 index 9e6dc379c..000000000 --- a/packages/vite-plugin-nitro/src/lib/hooks/post-rendering-hook.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { Nitro, PrerenderRoute } from 'nitro/types'; - -export function addPostRenderingHooks( - nitro: Nitro, - hooks: ((pr: PrerenderRoute) => Promise)[], -): void { - hooks.forEach((hook: (preRoute: PrerenderRoute) => void) => { - nitro.hooks.hook('prerender:generate', (route: PrerenderRoute) => { - hook(route); - }); - }); -} diff --git a/packages/vite-plugin-nitro/src/lib/hooks/post-rendering-hooks.spec.ts b/packages/vite-plugin-nitro/src/lib/hooks/post-rendering-hooks.spec.ts deleted file mode 100644 index a9e4439d5..000000000 --- a/packages/vite-plugin-nitro/src/lib/hooks/post-rendering-hooks.spec.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { Nitro } from 'nitro/types'; -import { vi } from 'vitest'; - -import { addPostRenderingHooks } from './post-rendering-hook'; - -describe('postRenderingHook', () => { - const genRoute = { - route: 'test/testRoute', - contents: 'This is a test.', - }; - - const nitroMock = { - hooks: { - hook: vi.fn((name: string, callback: (route: any) => void) => - callback(genRoute), - ), - }, - } as unknown as Nitro; - - const mockFunc1 = vi.fn(); - const mockFunc2 = vi.fn(); - - it('should not attempt to call nitro mocks if no callbacks provided', () => { - addPostRenderingHooks(nitroMock, []); - expect(nitroMock.hooks.hook).not.toHaveBeenCalled(); - }); - - it('should call provided hooks', () => { - addPostRenderingHooks(nitroMock, [mockFunc1, mockFunc2]); - expect(mockFunc1).toHaveBeenCalledWith(genRoute); - expect(mockFunc2).toHaveBeenCalled(); - }); -}); diff --git a/packages/vite-plugin-nitro/src/lib/options.ts b/packages/vite-plugin-nitro/src/lib/options.ts deleted file mode 100644 index f710e2261..000000000 --- a/packages/vite-plugin-nitro/src/lib/options.ts +++ /dev/null @@ -1,213 +0,0 @@ -import type { PrerenderRoute } from 'nitro/types'; -import type { UserConfig } from 'vite'; - -export interface I18nPrerenderOptions { - /** - * The default/source locale for the application. - */ - defaultLocale: string; - - /** - * List of supported locale identifiers. - * Each route will be prerendered once per locale with a locale prefix. - */ - locales: string[]; -} - -export interface Options { - ssr?: boolean; - ssrBuildDir?: string; - /** - * Prerender the static pages without producing the server output. - */ - static?: boolean; - prerender?: PrerenderOptions; - entryServer?: string; - index?: string; - /** - * Relative path to source files. Default is 'src'. - */ - sourceRoot?: string; - /** - * Absolute path to workspace root. Default is 'process.cwd()' - */ - workspaceRoot?: string; - /** - * Additional page paths to include - */ - additionalPagesDirs?: string[]; - /** - * Additional API paths to include - */ - additionalAPIDirs?: string[]; - apiPrefix?: string; - - /** - * Toggles internal API middleware. - * If disabled, a proxy request is used to route /api - * requests to / in the production server build. - * - * @deprecated - * Use the src/server/routes/api folder - * for API routes. - */ - useAPIMiddleware?: boolean; - /** - * Vite-native build passthrough. Rolldown-only options such as - * `build.rolldownOptions.output.codeSplitting` are forwarded when present. - */ - vite?: { - build?: UserConfig['build']; - }; -} - -export interface PrerenderOptions { - /** - * Add additional routes to prerender through crawling page links. - */ - discover?: boolean; - - /** - * List of routes to prerender resolved statically or dynamically. - */ - routes?: - | (string | PrerenderContentDir | PrerenderRouteConfig)[] - | (() => Promise< - (string | PrerenderContentDir | PrerenderRouteConfig | undefined)[] - >); - sitemap?: SitemapConfig; - /** List of functions that run for each route after pre-rendering is complete. */ - postRenderingHooks?: ((routes: PrerenderRoute) => Promise)[]; -} - -export type SitemapPriority = number | `${number}`; - -export interface SitemapRouteDefinition { - route: string; - lastmod?: string; - changefreq?: - | 'always' - | 'hourly' - | 'daily' - | 'weekly' - | 'monthly' - | 'yearly' - | 'never'; - priority?: SitemapPriority; -} - -export interface SitemapEntry extends SitemapRouteDefinition { - loc: string; -} - -export type SitemapRouteInput = string | SitemapRouteDefinition | undefined; -export type SitemapRouteSource = - | SitemapRouteInput[] - | (() => Promise); -export type SitemapExcludeRule = - | string - | RegExp - | ((entry: SitemapEntry) => boolean | Promise); -export type SitemapTransform = ( - entry: SitemapEntry, -) => SitemapRouteDefinition | false | Promise; - -export interface SitemapConfig { - host: string; - include?: SitemapRouteSource; - exclude?: SitemapExcludeRule[]; - defaults?: PrerenderSitemapConfig; - transform?: SitemapTransform; -} - -export interface PrerenderContentDir { - /** - * The directory where files should be grabbed from. - * @example `/src/contents/blog` - */ - contentDir: string; - /** - * Transform the matching content files path into a route. - * The function is called for each matching content file within the specified contentDir. - * @param file information of the matching file (`path`, `name`, `extension`, `attributes`, `content`) - * @returns a string with the route should be returned (e. g. `/blog/`) or the value `false`, when the route should not be prerendered. - */ - transform: (file: PrerenderContentFile) => string | false; - - /** - * Customize the sitemap definition for the prerendered route - * - * https://www.sitemaps.org/protocol.html#xmlTagDefinitions - */ - sitemap?: - | PrerenderSitemapConfig - | ((file: PrerenderContentFile) => PrerenderSitemapConfig); - - /** - * Output the source markdown content alongside the prerendered route. - * The source file will be accessible at the route path with a .md extension. - * @param file information of the matching file including its content - * @returns the markdown content string to output, or `false` to skip outputting for this file - */ - outputSourceFile?: (file: PrerenderContentFile) => string | false; - - /** - * Recurse into subdirectories of `contentDir` when discovering files. - * When enabled, the matching file's directory relative to `contentDir` - * is exposed via `PrerenderContentFile.relativePath` so transforms can - * disambiguate identically-named files across subdirectories. - * @default false - */ - recursive?: boolean; -} - -/** - * @param path the path to the content file - * @param name the basename of the matching content file without the file extension - * @param extension the file extension - * @param attributes the frontmatter attributes extracted from the frontmatter section of the file - * @param content the raw file content including frontmatter - * @param relativePath when `recursive` is enabled, the directory of the file relative to `contentDir` (empty string for files at the top level) - * @returns a string with the route should be returned (e. g. `/blog/`) or the value `false`, when the route should not be prerendered. - */ -export interface PrerenderContentFile { - path: string; - attributes: Record; - name: string; - extension: string; - content: string; - relativePath?: string; -} - -export interface PrerenderSitemapConfig { - lastmod?: string; - changefreq?: - | 'always' - | 'hourly' - | 'daily' - | 'weekly' - | 'monthly' - | 'yearly' - | 'never'; - priority?: SitemapPriority; -} - -export interface PrerenderRouteConfig { - route: string; - /** - * Customize the sitemap definition for the prerendered route - * - * https://www.sitemaps.org/protocol.html#xmlTagDefinitions - */ - sitemap?: PrerenderSitemapConfig | (() => PrerenderSitemapConfig); - /** - * Prerender static data for the prerendered route - */ - staticData?: boolean; - /** - * Path to the source markdown file to output alongside the prerendered route. - * The source file will be accessible at the route path with a .md extension. - * @example 'src/content/overview.md' - */ - outputSourceFile?: string; -} diff --git a/packages/vite-plugin-nitro/src/lib/page-endpoints.spec.ts b/packages/vite-plugin-nitro/src/lib/page-endpoints.spec.ts deleted file mode 100644 index 90fe8ccd3..000000000 --- a/packages/vite-plugin-nitro/src/lib/page-endpoints.spec.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { pageEndpointsPlugin } from './plugins/page-endpoints'; - -describe('pageEndpointsPlugin', () => { - const plugin = pageEndpointsPlugin(); - - it('uses Nitro runtime $fetch instead of a private nitro import', async () => { - const result = await plugin.transform?.( - `export const load = () => ({ ok: true });`, - '/src/app/pages/index.server.ts', - ); - - expect(result).toBeDefined(); - expect(result?.code).toContain( - 'export default defineHandler(async(event) => {', - ); - expect(result?.code).toContain(`import { createFetch } from 'ofetch';`); - expect(result?.code).toContain('fetchWithEvent'); - expect(result?.code).toContain('const serverFetch = createFetch'); - expect(result?.code).toContain('fetch: serverFetch'); - expect(result?.code).not.toContain(`nitro/deps/ofetch`); - }); - - it('generates a default load when only action is exported', async () => { - const result = await plugin.transform?.( - `export const action = () => ({ saved: true });`, - '/src/app/pages/index.server.ts', - ); - - expect(result).toBeDefined(); - expect(result?.code).toContain('export const load = () =>'); - expect(result?.code).toContain( - 'export const action = () => ({ saved: true })', - ); - }); - - it('uses both exports when load and action are provided', async () => { - const result = await plugin.transform?.( - `export const load = () => ({ ok: true });\nexport const action = () => ({ saved: true });`, - '/src/app/pages/index.server.ts', - ); - - expect(result).toBeDefined(); - expect(result?.code).toContain('export const load = () => ({ ok: true })'); - expect(result?.code).toContain( - 'export const action = () => ({ saved: true })', - ); - // should not generate default stubs - expect(result?.code).not.toContain('return {};'); - }); - - it('generates default load and action when neither is exported', async () => { - const result = await plugin.transform?.( - `export const helper = () => 42;`, - '/src/app/pages/index.server.ts', - ); - - expect(result).toBeDefined(); - expect(result?.code).toContain('export const load = () =>'); - expect(result?.code).toContain('export const action = () =>'); - // both stubs return empty objects - const stubs = (result?.code.match(/return \{\};/g) || []).length; - expect(stubs).toBe(2); - }); - - it('skips files that are not .server.ts', async () => { - const result = await plugin.transform?.( - `export const load = () => ({ ok: true });`, - '/src/app/pages/index.ts', - ); - - expect(result).toBeUndefined(); - }); - - it('skips .server.ts files outside /pages/', async () => { - const result = await plugin.transform?.( - `export const load = () => ({ ok: true });`, - '/src/app/services/auth.server.ts', - ); - - expect(result).toBeUndefined(); - }); -}); diff --git a/packages/vite-plugin-nitro/src/lib/plugins/dev-server-plugin.ts b/packages/vite-plugin-nitro/src/lib/plugins/dev-server-plugin.ts deleted file mode 100644 index da4441617..000000000 --- a/packages/vite-plugin-nitro/src/lib/plugins/dev-server-plugin.ts +++ /dev/null @@ -1,176 +0,0 @@ -// SSR dev server, middleware and error page source modified from -// https://github.com/solidjs/solid-start/blob/main/packages/start/dev/server.js - -import { - Connect, - Plugin, - UserConfig, - ViteDevServer, - normalizePath, -} from 'vite'; -import { resolve } from 'node:path'; -import { readFileSync } from 'node:fs'; -import { createRouter as createRadixRouter, toRouteMatcher } from 'radix3'; -import { defu } from 'defu'; -import type { NitroRouteRules } from 'nitro/types'; - -import { registerDevServerMiddleware } from '../utils/register-dev-middleware.js'; -import { writeWebResponseToNode } from '../utils/node-web-bridge.js'; -import { Options } from '../options.js'; -import { detectLocaleFromRoute, setHtmlLang } from '../utils/i18n-prerender.js'; - -type ServerOptions = Options & { routeRules?: Record | undefined }; - -export function devServerPlugin(options: ServerOptions): Plugin { - const workspaceRoot = options?.workspaceRoot || process.cwd(); - const sourceRoot = options?.sourceRoot ?? 'src'; - const index = options.index || 'index.html'; - let config: UserConfig; - let root: string; - let isTest = false; - - return { - name: 'analogjs-dev-ssr-plugin', - config(userConfig, { mode }) { - config = userConfig; - root = normalizePath(resolve(workspaceRoot, config.root || '.') || '.'); - isTest = isTest ? isTest : mode === 'test'; - return { - appType: 'custom', - resolve: { - alias: { - '~analog/entry-server': - options.entryServer || `${root}/${sourceRoot}/main.server.ts`, - }, - }, - }; - }, - configureServer(viteServer) { - if (isTest) { - return; - } - - return async () => { - remove_html_middlewares(viteServer.middlewares); - registerDevServerMiddleware(root, sourceRoot, viteServer); - - if (options.i18n) { - registerI18nWatcher(viteServer); - } - - viteServer.middlewares.use(async (req, res) => { - let template = readFileSync( - resolve(viteServer.config.root, index), - 'utf-8', - ); - - template = await viteServer.transformIndexHtml( - req.originalUrl as string, - template, - ); - - const _routeRulesMatcher = toRouteMatcher( - createRadixRouter({ routes: options.routeRules }), - ); - const _getRouteRules = (path: string) => - defu( - {}, - ..._routeRulesMatcher.matchAll(path).reverse(), - ) as NitroRouteRules; - - try { - let result: string | Response; - // Check for route rules explicitly disabling SSR - if (_getRouteRules(req.originalUrl as string).ssr === false) { - result = template; - } else { - const entryServer = ( - await viteServer.ssrLoadModule('~analog/entry-server') - )['default']; - result = await entryServer(req.originalUrl, template, { - req, - res, - }); - } - - if (result instanceof Response) { - await writeWebResponseToNode(res, result); - return; - } - - // Inject lang attribute when i18n is configured - let html = typeof result === 'string' ? result : template; - if (options.i18n) { - const locale = detectLocaleFromRoute( - req.originalUrl as string, - options.i18n, - ); - html = setHtmlLang(html, locale); - } - - res.setHeader('Content-Type', 'text/html'); - res.end(html); - } catch (e) { - viteServer.ssrFixStacktrace(e as Error); - res.statusCode = 500; - res.end(` - - - - - Error - - - - - - `); - } - }); - }; - }, - }; -} - -/** - * Removes Vite internal middleware - * - * @param server - */ -function remove_html_middlewares(server: ViteDevServer['middlewares']) { - const html_middlewares = [ - 'viteIndexHtmlMiddleware', - 'vite404Middleware', - 'viteSpaFallbackMiddleware', - 'viteHtmlFallbackMiddleware', - ]; - for (let i = server.stack.length - 1; i > 0; i--) { - const handler = server.stack[i]?.handle; - const handlerName = - typeof handler === 'function' ? handler.name : undefined; - if (handlerName && html_middlewares.includes(handlerName)) { - server.stack.splice(i, 1); - } - } -} - -/** - * Formats error for SSR message in error overlay - * @param req - * @param error - * @returns - */ -function prepareError(req: Connect.IncomingMessage, error: unknown) { - const e = error as Error; - return { - message: `An error occured while server rendering ${req.url}:\n\n\t${ - typeof e === 'string' ? e : e.message - } `, - stack: typeof e === 'string' ? '' : e.stack, - }; -} diff --git a/packages/vite-plugin-nitro/src/lib/plugins/page-endpoints.ts b/packages/vite-plugin-nitro/src/lib/plugins/page-endpoints.ts deleted file mode 100644 index bd1f18bf3..000000000 --- a/packages/vite-plugin-nitro/src/lib/plugins/page-endpoints.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { parseSync } from 'oxc-parser'; -import { normalizePath } from 'vite'; -import { SERVER_FETCH_FACTORY_SNIPPET } from '../utils/renderers.js'; - -export function pageEndpointsPlugin() { - return { - name: 'analogjs-vite-plugin-nitro-rollup-page-endpoint', - async transform( - _code: string, - id: string, - ): Promise<{ code: string; map: null } | undefined> { - if (normalizePath(id).includes('/pages/') && id.endsWith('.server.ts')) { - const result = parseSync(id, _code, { - sourceType: 'module', - lang: 'ts', - }); - - const fileExports: string[] = result.module.staticExports.flatMap((e) => - e.entries - .filter((entry) => entry.exportName.name !== null) - .map((entry) => entry.exportName.name as string), - ); - - // In h3 v2 / Nitro v3, event.node is undefined during prerendering - // (which uses the fetch-based pipeline, not Node.js http). We use - // optional chaining so that page endpoints work in both Node.js - // server and fetch-based prerender contexts. - // Nitro v3 no longer guarantees the private `nitro/deps/ofetch` - // subpath that older codegen relied on. - // - // Page loaders expect Nitro-style `$fetch` semantics (parsed data plus - // internal relative-route support), so construct a request-local fetch - // using public APIs: - // - `createFetch` from `ofetch` for `$fetch` behavior - // - `fetchWithEvent` from `h3` for internal Nitro request routing - // - // This avoids both unstable private Nitro imports and assumptions about - // a global runtime `$fetch` being available during prerender. - const code = ` - import { defineHandler, fetchWithEvent } from 'nitro/h3'; - import { createFetch } from 'ofetch'; - - ${ - fileExports.includes('load') - ? _code - : ` - ${_code} - export const load = () => { - return {}; - }` - } - - ${ - fileExports.includes('action') - ? '' - : ` - export const action = () => { - return {}; - } - ` - } - - export default defineHandler(async(event) => { - ${SERVER_FETCH_FACTORY_SNIPPET} - - if (event.method === 'GET') { - try { - return await load({ - params: event.context.params, - req: event.node?.req, - res: event.node?.res, - fetch: serverFetch, - event - }); - } catch(e) { - console.error(\` An error occurred: \${e}\`) - throw e; - } - } else { - try { - return await action({ - params: event.context.params, - req: event.node?.req, - res: event.node?.res, - fetch: serverFetch, - event - }); - } catch(e) { - console.error(\` An error occurred: \${e}\`) - throw e; - } - } - }); - `; - - return { - code, - map: null, - }; - } - - return; - }, - }; -} diff --git a/packages/vite-plugin-nitro/src/lib/utils/debug.ts b/packages/vite-plugin-nitro/src/lib/utils/debug.ts deleted file mode 100644 index 14878f366..000000000 --- a/packages/vite-plugin-nitro/src/lib/utils/debug.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { createDebug } from 'obug'; - -export const debugNitro = createDebug('analog:nitro'); -export const debugSsr = createDebug('analog:nitro:ssr'); -export const debugPrerender = createDebug('analog:nitro:prerender'); - -/** All debug instances in this package, for external wrapping (e.g. file logging). */ -export const debugInstances = [debugNitro, debugSsr, debugPrerender]; diff --git a/packages/vite-plugin-nitro/src/lib/utils/get-content-files.spec.ts b/packages/vite-plugin-nitro/src/lib/utils/get-content-files.spec.ts deleted file mode 100644 index 473cbe0d9..000000000 --- a/packages/vite-plugin-nitro/src/lib/utils/get-content-files.spec.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; -import { tmpdir } from 'node:os'; -import { join } from 'node:path'; -import { afterEach, beforeEach, describe, expect, it } from 'vitest'; - -import { getMatchingContentFilesWithFrontMatter } from './get-content-files'; - -describe('getMatchingContentFilesWithFrontMatter', () => { - let workspaceRoot: string; - const rootDir = '.'; - const contentDir = '/src/content/docs'; - - beforeEach(() => { - workspaceRoot = mkdtempSync(join(tmpdir(), 'analog-content-')); - mkdirSync(join(workspaceRoot, 'src/content/docs/erste-schritte'), { - recursive: true, - }); - mkdirSync(join(workspaceRoot, 'src/content/docs/assets'), { - recursive: true, - }); - writeFileSync( - join(workspaceRoot, 'src/content/docs/intro.md'), - '---\ntitle: Intro\n---\n# Intro', - ); - writeFileSync( - join(workspaceRoot, 'src/content/docs/erste-schritte/willkommen.md'), - '---\ntitle: Willkommen\n---\n# Willkommen', - ); - writeFileSync( - join(workspaceRoot, 'src/content/docs/assets/hochladen.md'), - '---\ntitle: Hochladen\n---\n# Hochladen', - ); - }); - - afterEach(() => { - rmSync(workspaceRoot, { recursive: true, force: true }); - }); - - it('returns only top-level files by default', () => { - const files = getMatchingContentFilesWithFrontMatter( - workspaceRoot, - rootDir, - contentDir, - ); - - expect(files.map((f) => f.name).sort()).toEqual(['intro']); - }); - - it('returns nested files when recursive is enabled', () => { - const files = getMatchingContentFilesWithFrontMatter( - workspaceRoot, - rootDir, - contentDir, - true, - ); - - expect(files.map((f) => f.name).sort()).toEqual([ - 'hochladen', - 'intro', - 'willkommen', - ]); - }); - - it('exposes the directory relative to contentDir as relativePath', () => { - const files = getMatchingContentFilesWithFrontMatter( - workspaceRoot, - rootDir, - contentDir, - true, - ); - - const byName = Object.fromEntries(files.map((f) => [f.name, f])); - expect(byName['intro'].relativePath).toBe(''); - expect(byName['willkommen'].relativePath).toBe('erste-schritte'); - expect(byName['hochladen'].relativePath).toBe('assets'); - }); -}); diff --git a/packages/vite-plugin-nitro/src/lib/utils/get-content-files.ts b/packages/vite-plugin-nitro/src/lib/utils/get-content-files.ts deleted file mode 100644 index 0c2030fa8..000000000 --- a/packages/vite-plugin-nitro/src/lib/utils/get-content-files.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { readFileSync } from 'node:fs'; -import { join, relative, resolve } from 'node:path'; -import { normalizePath } from 'vite'; -import { createRequire } from 'node:module'; -import { globSync } from 'tinyglobby'; - -import { PrerenderContentFile } from '../options'; - -const require = createRequire(import.meta.url); - -/** - * Discovers content files with front matter and extracts metadata for prerendering. - * - * This function: - * 1. Discovers all content files matching the specified glob pattern - * 2. Reads each file and parses front matter metadata - * 3. Extracts file name, extension, and path information - * 4. Returns structured data for prerendering content pages - * - * @param workspaceRoot The workspace root directory path - * @param rootDir The project root directory relative to workspace - * @param glob The glob pattern to match content files (e.g., 'content/blog') - * @returns Array of PrerenderContentFile objects with metadata and front matter - * - * Example usage: - * const contentFiles = getMatchingContentFilesWithFrontMatter( - * '/workspace', - * 'apps/my-app', - * 'content/blog' - * ); - * - * Sample discovered file paths: - * - /workspace/apps/my-app/content/blog/first-post.md - * - /workspace/apps/my-app/content/blog/2024/01/hello-world.md - * - /workspace/apps/my-app/content/blog/tech/angular-v17.mdx - * - /workspace/apps/my-app/content/blog/about/index.md - * - * Sample output structure: - * { - * name: 'first-post', - * extension: 'md', - * path: 'content/blog', - * attributes: { title: 'My First Post', date: '2024-01-01', tags: ['intro'] } - * } - * - * tinyglobby vs fast-glob comparison: - * - Both support the same glob patterns for file discovery - * - Both are efficient for finding content files - * - tinyglobby is now used instead of fast-glob - * - tinyglobby provides similar functionality with smaller bundle size - * - tinyglobby's globSync returns absolute paths when absolute: true is set - * - * Front matter parsing: - * - Uses front-matter library to parse YAML/TOML front matter - * - Extracts metadata like title, date, tags, author, etc. - * - Supports both YAML (---) and TOML (+++) delimiters - * - Returns structured attributes for prerendering - * - * File path processing: - * - Normalizes paths for cross-platform compatibility - * - Extracts file name without extension - * - Determines file extension for content type handling - * - Maintains relative path structure for routing - */ -export function getMatchingContentFilesWithFrontMatter( - workspaceRoot: string, - rootDir: string, - glob: string, - recursive = false, -): PrerenderContentFile[] { - // Dynamically require front-matter library - // eslint-disable-next-line @typescript-eslint/no-var-requires - const fm = require('front-matter'); - - // Normalize the project root path for consistent path handling - const root = normalizePath(resolve(workspaceRoot, rootDir)); - - // Resolve the content directory path relative to the project root - const resolvedDir = normalizePath(relative(root, join(root, glob))); - - // Discover all content files in the specified directory. - // Default pattern matches only top-level files; recursive opt-in walks subdirectories. - const pattern = recursive - ? `${root}/${resolvedDir}/**/*` - : `${root}/${resolvedDir}/*`; - const contentFiles: string[] = globSync([pattern], { - dot: true, - absolute: true, - onlyFiles: true, - }); - - const dirPrefix = `${root}/${resolvedDir}`; - - // Process each discovered content file to extract metadata and front matter - const mappedFilesWithFm: PrerenderContentFile[] = contentFiles.map((f) => { - // Read the file contents as UTF-8 text - const fileContents = readFileSync(f, 'utf8'); - - // Parse front matter from the file content - const raw = fm(fileContents); - - const filepath = normalizePath(f).replace(root, ''); - - const match = filepath.match(/\/([^/.]+)(\.([^/.]+))?$/); - let name = ''; - let extension = ''; - if (match) { - name = match[1]; // File name without extension - extension = match[3] || ''; // File extension or empty string if no extension - } - - // Path of the file's directory relative to the configured contentDir. - // For top-level files this is an empty string; for nested files it - // gives transforms enough context to disambiguate identically-named - // files (e.g. docs/a/post.md vs docs/b/post.md). - const relativeDir = normalizePath(relative(dirPrefix, f)); - const lastSlash = relativeDir.lastIndexOf('/'); - const relativePath = - lastSlash === -1 ? '' : relativeDir.slice(0, lastSlash); - - // Return structured content file data for prerendering - return { - name, - extension, - path: resolvedDir, - attributes: raw.attributes as { attributes: Record }, - content: fileContents, - relativePath, - }; - }); - - return mappedFilesWithFm; -} diff --git a/packages/vite-plugin-nitro/src/lib/utils/get-page-handlers.ts b/packages/vite-plugin-nitro/src/lib/utils/get-page-handlers.ts deleted file mode 100644 index c4d965be0..000000000 --- a/packages/vite-plugin-nitro/src/lib/utils/get-page-handlers.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { resolve, relative } from 'node:path'; -import { globSync } from 'tinyglobby'; - -import type { NitroEventHandler } from 'nitro/types'; -import { normalizePath } from 'vite'; - -type GetHandlersArgs = { - workspaceRoot: string; - sourceRoot: string; - rootDir: string; - additionalPagesDirs?: string[]; - hasAPIDir?: boolean; -}; - -/** - * Discovers and generates Nitro event handlers for server-side page routes. - * - * This function: - * 1. Discovers all .server.ts files in the app/pages directory and additional pages directories - * 2. Converts file paths to route patterns using Angular-style route syntax - * 3. Generates Nitro event handlers with proper route mapping and lazy loading - * 4. Handles dynamic route parameters and catch-all routes - * - * @param workspaceRoot The workspace root directory path - * @param sourceRoot The source directory path (e.g., 'src') - * @param rootDir The project root directory relative to workspace - * @param additionalPagesDirs Optional array of additional pages directories to scan - * @param hasAPIDir Whether the project has an API directory (affects route prefixing) - * @returns Array of NitroEventHandler objects with handler paths and route patterns - * - * Example usage: - * const handlers = getPageHandlers({ - * workspaceRoot: '/workspace', - * sourceRoot: 'src', - * rootDir: 'apps/my-app', - * additionalPagesDirs: ['/libs/shared/pages'], - * hasAPIDir: true - * }); - * - * Sample discovered file paths: - * - /workspace/apps/my-app/src/app/pages/index.server.ts - * - /workspace/apps/my-app/src/app/pages/users/[id].server.ts - * - /workspace/apps/my-app/src/app/pages/products/[...slug].server.ts - * - /workspace/apps/my-app/src/app/pages/(auth)/login.server.ts - * - * Route transformation examples: - * - index.server.ts → /_analog/pages/index - * - users/[id].server.ts → /_analog/pages/users/:id - * - products/[...slug].server.ts → /_analog/pages/products/**:slug - * - (auth)/login.server.ts → /_analog/pages/-auth-/login - * - * tinyglobby vs fast-glob comparison: - * - Both support the same glob patterns for file discovery - * - Both are efficient for finding server-side page files - * - tinyglobby is now used instead of fast-glob - * - tinyglobby provides similar functionality with smaller bundle size - * - tinyglobby's globSync returns absolute paths when absolute: true is set - * - * Route transformation rules: - * 1. Removes .server.ts extension - * 2. Converts [param] to :param for dynamic routes - * 3. Converts [...param] to **:param for catch-all routes - * 4. Converts (group) to -group- for route groups - * 5. Converts dots to forward slashes - * 6. Prefixes with /_analog/pages and optionally /api - */ -export function getPageHandlers({ - workspaceRoot, - sourceRoot, - rootDir, - additionalPagesDirs, - hasAPIDir, -}: GetHandlersArgs): NitroEventHandler[] { - // Normalize the project root path for consistent path handling - const root = normalizePath(resolve(workspaceRoot, rootDir)); - - // Discover all .server.ts files in the app/pages directory and additional pages directories - // Pattern: looks for any .server.ts files in app/pages/**/*.server.ts and additional directories - const endpointFiles: string[] = globSync( - [ - `${root}/${sourceRoot}/app/pages/**/*.server.ts`, - ...(additionalPagesDirs || []).map( - (dir) => `${workspaceRoot}${dir}/**/*.server.ts`, - ), - ], - { dot: true, absolute: true }, - ); - - // Transform each discovered file into a Nitro event handler - const handlers: NitroEventHandler[] = endpointFiles.map((endpointFile) => { - // Normalize the endpoint file path for consistent path handling - const normalized = normalizePath(endpointFile); - // Transform the normalized path into a route pattern - const route = normalized - .replace(/^(.*?)\/pages/, '/pages') - .replace(/\.server\.ts$/, '') // Remove .server.ts extension - .replace(/\[\.{3}(.+)\]/g, '**:$1') // Convert [...param] to **:param (catch-all routes) - .replace(/\[\.{3}(\w+)\]/g, '**:$1') // Alternative catch-all pattern - .replace(/\/\((.*?)\)$/, '/-$1-') // Convert (group) to -group- (route groups) - .replace(/\[(\w+)\]/g, ':$1') // Convert [param] to :param (dynamic routes) - .replace(/\./g, '/'); // Convert dots to forward slashes - - // Return Nitro event handler with absolute handler path and transformed route - return { - handler: endpointFile, - route: `${hasAPIDir ? '/api' : ''}/_analog${route}`, - lazy: true, - }; - }); - - return handlers; -} diff --git a/packages/vite-plugin-nitro/src/lib/utils/i18n-prerender.spec.ts b/packages/vite-plugin-nitro/src/lib/utils/i18n-prerender.spec.ts deleted file mode 100644 index 825b83141..000000000 --- a/packages/vite-plugin-nitro/src/lib/utils/i18n-prerender.spec.ts +++ /dev/null @@ -1,186 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { - expandRoutesWithLocales, - detectLocaleFromRoute, - setHtmlLang, -} from './i18n-prerender'; -import { getHreflangAlternates, stripLocalePrefix } from '../build-sitemap'; -import { I18nPrerenderOptions } from '../options'; - -const i18n: I18nPrerenderOptions = { - defaultLocale: 'en', - locales: ['en', 'fr', 'de'], -}; - -describe('expandRoutesWithLocales', () => { - it('should expand a single route to all locales', () => { - const result = expandRoutesWithLocales(['/about'], i18n); - - expect(result).toContain('/en/about'); - expect(result).toContain('/fr/about'); - expect(result).toContain('/de/about'); - }); - - it('should handle the root route', () => { - const result = expandRoutesWithLocales(['/'], i18n); - - expect(result).toContain('/en'); - expect(result).toContain('/fr'); - expect(result).toContain('/de'); - }); - - it('should keep the unprefixed root route for the default locale', () => { - const result = expandRoutesWithLocales(['/'], i18n); - - expect(result).toContain('/'); - }); - - it('should not prefix API routes', () => { - const result = expandRoutesWithLocales( - ['/about', '/api/v1/users', '/api/_analog/pages/about'], - i18n, - ); - - expect(result).toContain('/api/v1/users'); - expect(result).toContain('/api/_analog/pages/about'); - expect(result).not.toContain('/en/api/v1/users'); - }); - - it('should expand multiple routes', () => { - const result = expandRoutesWithLocales(['/about', '/contact'], i18n); - - expect(result).toContain('/en/about'); - expect(result).toContain('/fr/about'); - expect(result).toContain('/de/about'); - expect(result).toContain('/en/contact'); - expect(result).toContain('/fr/contact'); - expect(result).toContain('/de/contact'); - }); - - it('should not duplicate routes', () => { - const result = expandRoutesWithLocales(['/about'], i18n); - const aboutRoutes = result.filter((r) => r === '/about'); - - expect(aboutRoutes.length).toBeLessThanOrEqual(1); - }); -}); - -describe('detectLocaleFromRoute', () => { - it('should detect locale from route prefix', () => { - expect(detectLocaleFromRoute('/fr/about', i18n)).toBe('fr'); - expect(detectLocaleFromRoute('/de/contact', i18n)).toBe('de'); - expect(detectLocaleFromRoute('/en', i18n)).toBe('en'); - }); - - it('should return defaultLocale for routes without locale prefix', () => { - expect(detectLocaleFromRoute('/about', i18n)).toBe('en'); - expect(detectLocaleFromRoute('/', i18n)).toBe('en'); - }); - - it('should not match non-configured locales', () => { - expect(detectLocaleFromRoute('/es/about', i18n)).toBe('en'); - }); -}); - -describe('setHtmlLang', () => { - it('should add lang attribute to html tag', () => { - const html = ''; - const result = setHtmlLang(html, 'fr'); - - expect(result).toBe(''); - }); - - it('should replace existing lang attribute', () => { - const html = ''; - const result = setHtmlLang(html, 'de'); - - expect(result).toBe(''); - }); - - it('should preserve other attributes on html tag', () => { - const html = ''; - const result = setHtmlLang(html, 'fr'); - - expect(result).toContain('lang="fr"'); - expect(result).toContain('class="dark"'); - expect(result).toContain('dir="ltr"'); - }); -}); - -describe('getHreflangAlternates', () => { - it('should generate alternates for all locales plus x-default', () => { - const alternates = getHreflangAlternates( - 'https://example.com/fr/about', - 'https://example.com', - i18n, - ); - - expect(alternates).toContainEqual({ - locale: 'en', - href: 'https://example.com/en/about', - }); - expect(alternates).toContainEqual({ - locale: 'fr', - href: 'https://example.com/fr/about', - }); - expect(alternates).toContainEqual({ - locale: 'de', - href: 'https://example.com/de/about', - }); - expect(alternates).toContainEqual({ - locale: 'x-default', - href: 'https://example.com/en/about', - }); - }); - - it('should handle root locale paths', () => { - const alternates = getHreflangAlternates( - 'https://example.com/fr', - 'https://example.com', - i18n, - ); - - expect(alternates).toContainEqual({ - locale: 'en', - href: 'https://example.com/en', - }); - expect(alternates).toContainEqual({ - locale: 'fr', - href: 'https://example.com/fr', - }); - }); - - it('should handle host with trailing slash', () => { - const alternates = getHreflangAlternates( - 'https://example.com/en/about', - 'https://example.com/', - i18n, - ); - - expect(alternates).toContainEqual({ - locale: 'en', - href: 'https://example.com/en/about', - }); - }); -}); - -describe('stripLocalePrefix', () => { - it('should strip locale from path', () => { - expect(stripLocalePrefix('/fr/about', ['en', 'fr'])).toBe('/about'); - expect(stripLocalePrefix('/en/products/123', ['en', 'fr'])).toBe( - '/products/123', - ); - }); - - it('should return root for locale-only path', () => { - expect(stripLocalePrefix('/fr', ['en', 'fr'])).toBe('/'); - }); - - it('should return path unchanged if no locale prefix', () => { - expect(stripLocalePrefix('/about', ['en', 'fr'])).toBe('/about'); - }); - - it('should return root for empty path', () => { - expect(stripLocalePrefix('', ['en', 'fr'])).toBe('/'); - }); -}); diff --git a/packages/vite-plugin-nitro/src/lib/utils/i18n-prerender.ts b/packages/vite-plugin-nitro/src/lib/utils/i18n-prerender.ts deleted file mode 100644 index 632c37348..000000000 --- a/packages/vite-plugin-nitro/src/lib/utils/i18n-prerender.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { PrerenderRoute } from 'nitropack'; -import { I18nPrerenderOptions } from '../options.js'; - -/** - * Expands a list of routes to include locale-prefixed variants. - * - * For each route and each locale, generates a prefixed route: - * '/' + locale + route - * - * The default locale's routes are included both with and without the prefix - * so that `/about` and `/en/about` both render. - * - * @param routes - The original routes to expand - * @param i18n - The i18n prerender configuration - * @returns Expanded routes with locale prefixes - */ -export function expandRoutesWithLocales( - routes: string[], - i18n: I18nPrerenderOptions, -): string[] { - const expanded: string[] = []; - - for (const route of routes) { - // Skip API routes — they don't need locale prefixes - if (route.includes('/_analog/') || route.startsWith('/api/')) { - expanded.push(route); - continue; - } - - for (const locale of i18n.locales) { - const prefix = `/${locale}`; - const localizedRoute = route === '/' ? prefix : `${prefix}${route}`; - expanded.push(localizedRoute); - } - - // Keep the unprefixed route for the default locale - if (!expanded.includes(route)) { - expanded.push(route); - } - } - - return expanded; -} - -/** - * Creates a post-rendering hook that injects the `lang` attribute - * into the `` tag of prerendered pages based on the route's - * locale prefix. - * - * @param i18n - The i18n prerender configuration - * @returns A post-rendering hook function - */ -export function createI18nPostRenderingHook( - i18n: I18nPrerenderOptions, -): (route: PrerenderRoute) => Promise { - return async (route: PrerenderRoute) => { - if (!route.contents || typeof route.contents !== 'string') { - return; - } - - const locale = detectLocaleFromRoute(route.route, i18n); - if (!locale) { - return; - } - - // Inject or replace the lang attribute on - route.contents = setHtmlLang(route.contents, locale); - }; -} - -/** - * Detects the locale from a prerendered route path by checking - * the first path segment against the configured locales. - */ -export function detectLocaleFromRoute( - route: string, - i18n: I18nPrerenderOptions, -): string { - const segments = route.split('/').filter(Boolean); - const firstSegment = segments[0]; - - if (firstSegment && i18n.locales.includes(firstSegment)) { - return firstSegment; - } - - return i18n.defaultLocale; -} - -/** - * Sets the `lang` attribute on the `` tag in an HTML string. - * If a `lang` attribute already exists, it is replaced. - * If no `lang` attribute exists, it is added. - */ -export function setHtmlLang(html: string, locale: string): string { - // Replace existing lang attribute - if (/]*\slang\s*=\s*["'][^"']*["']/i.test(html)) { - return html.replace( - /(]*\s)lang\s*=\s*["'][^"']*["']/i, - `$1lang="${locale}"`, - ); - } - - // Add lang attribute to tag - return html.replace(/(modulePath: string | URL): Promise { - return new Function('modulePath', `return import(modulePath);`)( - modulePath, - ) as Promise; -} diff --git a/packages/vite-plugin-nitro/src/lib/utils/node-web-bridge.spec.ts b/packages/vite-plugin-nitro/src/lib/utils/node-web-bridge.spec.ts deleted file mode 100644 index ec8344a3c..000000000 --- a/packages/vite-plugin-nitro/src/lib/utils/node-web-bridge.spec.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { IncomingMessage } from 'node:http'; -import { describe, expect, it } from 'vitest'; - -import { toWebRequest } from './node-web-bridge'; - -describe('toWebRequest', () => { - it('ignores HTTP/2 pseudo-headers when building web headers', () => { - const req = { - headers: { - ':authority': 'example.com', - ':method': 'GET', - ':path': '/blog', - accept: 'text/html', - host: 'example.com', - 'x-forwarded-proto': 'https', - }, - method: 'GET', - url: '/blog', - } as IncomingMessage; - - const request = toWebRequest(req); - const headerKeys = Array.from(request.headers.keys()); - - expect(request.url).toBe('http://example.com/blog'); - expect(request.headers.get('accept')).toBe('text/html'); - expect(request.headers.get('host')).toBe('example.com'); - expect(headerKeys).not.toContain(':authority'); - expect(headerKeys).not.toContain(':method'); - expect(headerKeys).not.toContain(':path'); - }); -}); diff --git a/packages/vite-plugin-nitro/src/lib/utils/node-web-bridge.ts b/packages/vite-plugin-nitro/src/lib/utils/node-web-bridge.ts deleted file mode 100644 index fb97d424a..000000000 --- a/packages/vite-plugin-nitro/src/lib/utils/node-web-bridge.ts +++ /dev/null @@ -1,110 +0,0 @@ -import type { - IncomingHttpHeaders, - IncomingMessage, - ServerResponse, -} from 'node:http'; -import { Readable } from 'node:stream'; -import { pipeline } from 'node:stream/promises'; - -function toWebHeaders(headers: IncomingHttpHeaders) { - return Object.entries(headers).reduce((acc, [key, value]) => { - if (value && !key.startsWith(':')) { - acc.set(key, Array.isArray(value) ? value.join(', ') : value); - } - - return acc; - }, new Headers()); -} - -export function toWebRequest(req: IncomingMessage): Request { - const protocol = 'http'; - const host = req.headers.host || 'localhost'; - const url = new URL(req.url || '/', `${protocol}://${host}`); - const body = - req.method && !['GET', 'HEAD'].includes(req.method) - ? (Readable.toWeb(req) as ReadableStream) - : undefined; - - return new Request(url, { - method: req.method, - headers: toWebHeaders(req.headers), - body, - // @ts-expect-error duplex is required for streaming request bodies in Node.js - duplex: body ? 'half' : undefined, - }); -} - -function isClientDisconnectError(error: unknown, res: ServerResponse): boolean { - if (!(error instanceof Error)) { - return false; - } - - const hasDisconnectCode = - 'code' in error && - typeof error.code === 'string' && - [ - 'ERR_STREAM_PREMATURE_CLOSE', - 'ERR_INVALID_STATE', - 'ECONNRESET', - 'EPIPE', - ].includes(error.code); - - const hasDisconnectMessage = /closed or destroyed stream/i.test( - error.message, - ); - - return ( - (res.destroyed || res.writableEnded) && - (hasDisconnectCode || hasDisconnectMessage) - ); -} - -export async function writeWebResponseToNode( - res: ServerResponse, - response: Response, -): Promise { - res.statusCode = response.status; - res.statusMessage = response.statusText; - - const setCookies = - 'getSetCookie' in response.headers && - typeof response.headers.getSetCookie === 'function' - ? response.headers.getSetCookie() - : []; - - if (setCookies.length > 0) { - res.setHeader('set-cookie', setCookies); - } - - response.headers.forEach((value, key) => { - if (key !== 'set-cookie') { - res.setHeader(key, value); - } - }); - - if (!response.body) { - res.end(); - return; - } - - // The Web ReadableStream and Node.js stream/web ReadableStream types - // are structurally identical at runtime but TypeScript treats them as - // distinct nominal types. The double-cast bridges this gap safely. - try { - await pipeline( - Readable.fromWeb( - response.body as unknown as import('node:stream/web').ReadableStream, - ), - res, - ); - } catch (error) { - // Long-lived dev responses such as SSE can be interrupted by a browser - // refresh or HMR-triggered reconnect. Those closed-stream cases are - // expected and should not surface as noisy server errors. - if (isClientDisconnectError(error, res)) { - return; - } - - throw error; - } -} diff --git a/packages/vite-plugin-nitro/src/lib/utils/register-dev-middleware.ts b/packages/vite-plugin-nitro/src/lib/utils/register-dev-middleware.ts deleted file mode 100644 index 4c3d5648a..000000000 --- a/packages/vite-plugin-nitro/src/lib/utils/register-dev-middleware.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { ViteDevServer } from 'vite'; -import { EventHandler, H3 } from 'nitro/h3'; -import { globSync } from 'tinyglobby'; - -import { toWebRequest, writeWebResponseToNode } from './node-web-bridge.js'; - -const PASSTHROUGH_HEADER = 'x-analog-passthrough'; - -/** - * Registers development server middleware by discovering and loading middleware files. - * - * Each discovered h3 middleware module is loaded through Vite's SSR loader, - * wrapped in a temporary H3 app, then bridged back into Vite's Connect stack. - * If the middleware does not write a response, control falls through to the - * next Vite middleware. - * - * @param root The project root directory path - * @param sourceRoot The source directory path (e.g., 'src') - * @param viteServer The Vite development server instance - */ -export async function registerDevServerMiddleware( - root: string, - sourceRoot: string, - viteServer: ViteDevServer, -): Promise { - const middlewareFiles = globSync( - [`${root}/${sourceRoot}/server/middleware/**/*.ts`], - { - dot: true, - absolute: true, - }, - ); - - middlewareFiles.forEach((file) => { - // Create the H3 app once per middleware file (not per request). - // The dynamic handler inside still loads the module fresh each request - // via ssrLoadModule, preserving HMR. - const app = new H3(); - app.use(async (event) => { - const handler: EventHandler = await viteServer - .ssrLoadModule(file) - .then((m: unknown) => (m as { default: EventHandler }).default); - return handler(event); - }); - // Sentinel catch-all: when the middleware returns undefined (does not - // handle the request), h3 does not emit its default 404 — instead we - // detect the passthrough header and let the Connect stack continue. - app.use( - () => - new Response(null, { - status: 204, - headers: { [PASSTHROUGH_HEADER]: '1' }, - }), - ); - - viteServer.middlewares.use(async (req, res, next) => { - const response = await app.fetch(toWebRequest(req)); - - if (response.headers.get(PASSTHROUGH_HEADER) === '1') { - next(); - return; - } - - await writeWebResponseToNode(res, response); - }); - }); -} diff --git a/packages/vite-plugin-nitro/src/lib/utils/register-i18n-watcher.spec.ts b/packages/vite-plugin-nitro/src/lib/utils/register-i18n-watcher.spec.ts deleted file mode 100644 index a69195c25..000000000 --- a/packages/vite-plugin-nitro/src/lib/utils/register-i18n-watcher.spec.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { describe, expect, it, vi } from 'vitest'; -import { - isTranslationFile, - registerI18nWatcher, -} from './register-i18n-watcher'; - -describe('isTranslationFile', () => { - it('should match JSON files in i18n directories', () => { - expect(isTranslationFile('src/i18n/en.json')).toBe(true); - expect(isTranslationFile('src/i18n/fr.json')).toBe(true); - expect(isTranslationFile('/abs/path/src/i18n/messages.json')).toBe(true); - }); - - it('should match XLIFF files in i18n directories', () => { - expect(isTranslationFile('src/i18n/messages.xlf')).toBe(true); - }); - - it('should match XMB files in i18n directories', () => { - expect(isTranslationFile('src/i18n/messages.xmb')).toBe(true); - }); - - it('should match ARB files in i18n directories', () => { - expect(isTranslationFile('src/i18n/intl_fr.arb')).toBe(true); - }); - - it('should not match non-translation files', () => { - expect(isTranslationFile('src/app/component.ts')).toBe(false); - expect(isTranslationFile('src/app/data.json')).toBe(false); - expect(isTranslationFile('package.json')).toBe(false); - }); - - it('should not match translation-like files outside i18n directories', () => { - expect(isTranslationFile('src/assets/config.json')).toBe(false); - }); -}); - -describe('registerI18nWatcher', () => { - it('should register change and add listeners on the watcher', () => { - const on = vi.fn(); - const viteServer = { - watcher: { on }, - ws: { send: vi.fn() }, - } as any; - - registerI18nWatcher(viteServer); - - expect(on).toHaveBeenCalledWith('change', expect.any(Function)); - expect(on).toHaveBeenCalledWith('add', expect.any(Function)); - }); - - it('should trigger full-reload when a translation file changes', () => { - const listeners: Record void> = {}; - const on = vi.fn((event: string, fn: (...args: string[]) => void) => { - listeners[event] = fn; - }); - const send = vi.fn(); - const viteServer = { - watcher: { on }, - ws: { send }, - } as any; - - registerI18nWatcher(viteServer); - listeners['change']('src/i18n/fr.json'); - - expect(send).toHaveBeenCalledWith({ type: 'full-reload' }); - }); - - it('should not trigger reload for non-translation files', () => { - const listeners: Record void> = {}; - const on = vi.fn((event: string, fn: (...args: string[]) => void) => { - listeners[event] = fn; - }); - const send = vi.fn(); - const viteServer = { - watcher: { on }, - ws: { send }, - } as any; - - registerI18nWatcher(viteServer); - listeners['change']('src/app/component.ts'); - - expect(send).not.toHaveBeenCalled(); - }); -}); diff --git a/packages/vite-plugin-nitro/src/lib/utils/register-i18n-watcher.ts b/packages/vite-plugin-nitro/src/lib/utils/register-i18n-watcher.ts deleted file mode 100644 index 156a811c2..000000000 --- a/packages/vite-plugin-nitro/src/lib/utils/register-i18n-watcher.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { ViteDevServer } from 'vite'; - -/** - * Registers a file watcher that triggers a full page reload - * when translation files are added or modified. - * - * Matches files in i18n directories with .json, .xlf, .xmb, or .arb extensions. - * - * @param viteServer The Vite development server instance - */ -export function registerI18nWatcher(viteServer: ViteDevServer): void { - const triggerReload = (path: string) => { - if (isTranslationFile(path)) { - viteServer.ws.send({ type: 'full-reload' }); - } - }; - - viteServer.watcher.on('change', triggerReload); - viteServer.watcher.on('add', triggerReload); -} - -/** - * Checks whether a file path looks like a translation file - * based on its location in an i18n directory and its extension. - */ -export function isTranslationFile(path: string): boolean { - return /i18n.*\.(json|xlf|xmb|arb)$/.test(path); -} diff --git a/packages/vite-plugin-nitro/src/lib/utils/renderers.spec.ts b/packages/vite-plugin-nitro/src/lib/utils/renderers.spec.ts deleted file mode 100644 index b21878a5f..000000000 --- a/packages/vite-plugin-nitro/src/lib/utils/renderers.spec.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { apiMiddleware, clientRenderer, ssrRenderer } from './renderers'; - -describe('renderers virtual modules', () => { - it('emits an SSR renderer that serves HTML responses', () => { - const moduleSource = ssrRenderer(); - - expect(moduleSource).toContain("import template from '#analog/index';"); - expect(moduleSource).not.toContain('readFileSync('); - expect(moduleSource).toContain( - "event.res.headers.set('content-type', 'text/html; charset=utf-8');", - ); - expect(moduleSource).toContain( - 'const requestPath = normalizeHtmlRequestUrl(event.path);', - ); - expect(moduleSource).toContain('const req = event.node?.req'); - expect(moduleSource).toContain( - 'const html = await renderer(requestPath, template, { req, res, fetch: serverFetch });', - ); - expect(moduleSource).toContain("import renderer from '#analog/ssr';"); - }); - - it('emits a client renderer that serves HTML responses', () => { - const moduleSource = clientRenderer(); - - expect(moduleSource).toContain("import template from '#analog/index';"); - expect(moduleSource).not.toContain('readFileSync('); - expect(moduleSource).toContain( - "event.res.headers.set('content-type', 'text/html; charset=utf-8');", - ); - }); - - it('uses event-bound forwarding for API middleware', () => { - expect(apiMiddleware).toContain( - "import { defineHandler, fetchWithEvent, proxyRequest } from 'nitro/h3';", - ); - expect(apiMiddleware).toContain('return fetchWithEvent(event, reqUrl'); - expect(apiMiddleware).toContain('return proxyRequest(event, reqUrl);'); - }); -}); diff --git a/packages/vite-plugin-nitro/src/lib/utils/renderers.ts b/packages/vite-plugin-nitro/src/lib/utils/renderers.ts deleted file mode 100644 index 19fd29c10..000000000 --- a/packages/vite-plugin-nitro/src/lib/utils/renderers.ts +++ /dev/null @@ -1,141 +0,0 @@ -/** - * Code snippet emitted into virtual modules to create a request-scoped - * fetch using ofetch's `createFetch` + h3's `fetchWithEvent`. - * - * Shared between the SSR renderer and page-endpoint virtual modules so - * the fetch-wiring logic stays in sync. - * - * The emitted variable is named `serverFetch` — callers should reference it - * by that name. - */ -export const SERVER_FETCH_FACTORY_SNIPPET = ` - const serverFetch = createFetch({ - fetch: (resource, init) => { - const url = resource instanceof Request ? resource.url : resource.toString(); - return fetchWithEvent(event, url, init); - } - });`; - -/** - * SSR renderer virtual module content. - * - * This code runs inside Nitro's server runtime (Node.js context) where - * event.node is always populated. In h3 v2, event.node is typed as optional, - * so we use h3's first-class event properties (event.path, event.method) where - * possible and apply optional chaining when accessing the Node.js context for - * the Angular renderer which requires raw req/res objects. - * - * h3 v2 idiomatic APIs used: - * - defineHandler (replaces defineEventHandler / eventHandler) - * - event.path (replaces event.node.req.url) - * - getResponseHeader compat shim (still available in h3 v2) - */ -export function ssrRenderer() { - return ` -import { createFetch } from 'ofetch'; -import { defineHandler, fetchWithEvent } from 'nitro/h3'; -// @ts-ignore -import renderer from '#analog/ssr'; -import template from '#analog/index'; - -const normalizeHtmlRequestUrl = (url) => - url.replace(/\\/index\\.html(?=$|[?#])/, '/'); - -export default defineHandler(async (event) => { - event.res.headers.set('content-type', 'text/html; charset=utf-8'); - const noSSR = event.res.headers.get('x-analog-no-ssr'); - const requestPath = normalizeHtmlRequestUrl(event.path); - - if (noSSR === 'true') { - return template; - } - - // event.path is the canonical h3 v2 way to access the request URL. - // event.node?.req and event.node?.res are needed by the Angular SSR renderer - // which operates on raw Node.js request/response objects. - // During prerendering (Nitro v3 fetch-based pipeline), event.node is undefined. - // The Angular renderer requires a req object with at least { headers, url }, - // so we provide a minimal stub to avoid runtime errors in prerender context. - const req = event.node?.req - ? { - ...event.node.req, - url: requestPath, - originalUrl: requestPath, - } - : { - headers: { host: 'localhost' }, - url: requestPath, - originalUrl: requestPath, - connection: {}, - }; - const res = event.node?.res; -${SERVER_FETCH_FACTORY_SNIPPET} - - const html = await renderer(requestPath, template, { req, res, fetch: serverFetch }); - - return html; -});`; -} - -/** - * Client-only renderer virtual module content. - * - * Used when SSR is disabled — simply serves the static index.html template - * for every route, letting the client-side Angular router handle navigation. - */ -export function clientRenderer() { - return ` -import { defineHandler } from 'nitro/h3'; -import template from '#analog/index'; - -export default defineHandler(async (event) => { - event.res.headers.set('content-type', 'text/html; charset=utf-8'); - return template; -}); -`; -} - -/** - * API middleware virtual module content. - * - * Intercepts requests matching the configured API prefix and either: - * - Uses event-bound internal forwarding for GET requests (except .xml routes) - * - Uses request proxying for all other methods to forward the full request - * - * h3 v2 idiomatic APIs used: - * - defineHandler (replaces defineEventHandler / eventHandler) - * - event.path (replaces event.node.req.url) - * - event.method (replaces event.node.req.method) - * - proxyRequest is retained internally because it preserves Nitro route - * matching for event-bound server requests during SSR/prerender - * - Object.fromEntries(event.req.headers.entries()) replaces direct event.node.req.headers access - * - * `fetchWithEvent` keeps the active event context while forwarding to a - * rewritten path, which avoids falling through to the HTML renderer when - * SSR code makes relative API requests. - */ -export const apiMiddleware = ` -import { defineHandler, fetchWithEvent, proxyRequest } from 'nitro/h3'; -import { useRuntimeConfig } from 'nitro/runtime-config'; - -export default defineHandler(async (event) => { - const prefix = useRuntimeConfig().prefix; - const apiPrefix = \`\${prefix}/\${useRuntimeConfig().apiPrefix}\`; - - if (event.path?.startsWith(apiPrefix)) { - const reqUrl = event.path?.replace(apiPrefix, ''); - - if ( - event.method === 'GET' && - // in the case of XML routes, we want to proxy the request so that nitro gets the correct headers - // and can render the XML correctly as a static asset - !event.path?.endsWith('.xml') - ) { - return fetchWithEvent(event, reqUrl, { - headers: Object.fromEntries(event.req.headers.entries()), - }); - } - - return proxyRequest(event, reqUrl); - } -});`; diff --git a/packages/vite-plugin-nitro/src/lib/utils/rolldown.spec.ts b/packages/vite-plugin-nitro/src/lib/utils/rolldown.spec.ts deleted file mode 100644 index 031bbab0b..000000000 --- a/packages/vite-plugin-nitro/src/lib/utils/rolldown.spec.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; - -let mockRolldownVersion: string | undefined; - -vi.mock('vite', async () => { - const actual = await vi.importActual('vite'); - return { - ...actual, - get rolldownVersion() { - return mockRolldownVersion; - }, - }; -}); - -import { getBundleOptionsKey, isRolldown } from './rolldown.js'; - -describe('rolldown utils', () => { - beforeEach(() => { - mockRolldownVersion = undefined; - }); - - it('returns rolldown bundle config when rolldown is enabled', () => { - mockRolldownVersion = '1.0.0'; - - expect(isRolldown()).toBe(true); - expect(getBundleOptionsKey()).toBe('rolldownOptions'); - }); - - it('returns rollup bundle config when rolldown is unavailable', () => { - expect(isRolldown()).toBe(false); - expect(getBundleOptionsKey()).toBe('rollupOptions'); - }); -}); diff --git a/packages/vite-plugin-nitro/src/lib/utils/rolldown.ts b/packages/vite-plugin-nitro/src/lib/utils/rolldown.ts deleted file mode 100644 index fd6825e4f..000000000 --- a/packages/vite-plugin-nitro/src/lib/utils/rolldown.ts +++ /dev/null @@ -1,9 +0,0 @@ -import * as vite from 'vite'; - -export function isRolldown(): boolean { - return !!vite.rolldownVersion; -} - -export function getBundleOptionsKey(): 'rolldownOptions' | 'rollupOptions' { - return isRolldown() ? 'rolldownOptions' : 'rollupOptions'; -} diff --git a/packages/vite-plugin-nitro/src/lib/vite-nitro-plugin.spec.data.ts b/packages/vite-plugin-nitro/src/lib/vite-nitro-plugin.spec.data.ts deleted file mode 100644 index ca4a2639c..000000000 --- a/packages/vite-plugin-nitro/src/lib/vite-nitro-plugin.spec.data.ts +++ /dev/null @@ -1,73 +0,0 @@ -import type { NitroConfig } from 'nitro/types'; -import { ConfigEnv, UserConfig, Plugin } from 'vite'; -import { vi } from 'vitest'; -import { resolve } from 'node:path'; - -export const mockViteDevServer = { - middlewares: { - // eslint-disable-next-line @typescript-eslint/no-empty-function - use: () => {}, - }, -}; - -export const mockNitroConfig: NitroConfig = { - buildDir: resolve('./dist/.nitro'), - preset: undefined, - compatibilityDate: '2025-11-19', - handlers: [], - logLevel: 0, - output: { - dir: resolve('dist/analog'), - publicDir: resolve('dist/analog/public'), - }, - rootDir: '.', - scanDirs: ['src/server'], - serverDir: 'src/server', - prerender: { - crawlLinks: undefined, - }, - typescript: { - generateTsConfig: false, - }, - imports: { - autoImport: false, - }, - rollupConfig: { - plugins: [ - { - name: 'analogjs-vite-plugin-nitro-rollup-page-endpoint', - transform() { - return undefined; - }, - }, - ], - }, - routeRules: undefined, - runtimeConfig: { - apiPrefix: 'api', - }, - virtual: { - '#ANALOG_API_MIDDLEWARE': expect.anything(), - }, -}; - -export async function mockBuildFunctions() { - const buildServerImport = await import('./build-server'); - const buildServerImportSpy = vi.fn(); - buildServerImport.buildServer = buildServerImportSpy; - - const buildSitemapImport = await import('./build-sitemap'); - const buildSitemapImportSpy = vi.fn(); - buildSitemapImport.buildSitemap = buildSitemapImportSpy; - - return { buildServerImportSpy, buildSitemapImportSpy }; -} - -export async function runConfigAndCloseBundle(plugin: Plugin[]): Promise { - await ( - plugin[1].config as ( - config: UserConfig, - env: ConfigEnv, - ) => Promise - )({}, { command: 'build' } as ConfigEnv); -} diff --git a/packages/vite-plugin-nitro/src/lib/vite-plugin-nitro.spec.ts b/packages/vite-plugin-nitro/src/lib/vite-plugin-nitro.spec.ts deleted file mode 100644 index e49bbe12e..000000000 --- a/packages/vite-plugin-nitro/src/lib/vite-plugin-nitro.spec.ts +++ /dev/null @@ -1,951 +0,0 @@ -import { describe, expect, it, vi } from 'vitest'; -import * as vite from 'vite'; -import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'; -import { join, resolve } from 'node:path'; -import { tmpdir } from 'node:os'; - -vi.mock('nitro/builder', () => ({ - build: vi.fn(), - createDevServer: vi.fn(), - createNitro: vi.fn(), -})); - -vi.mock('./build-server'); -vi.mock('./build-sitemap'); - -vi.mock('./build-ssr', async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - buildClientApp: vi.fn(), - buildSSRApp: vi.fn(), - }; -}); - -import { build, createDevServer, createNitro } from 'nitro/builder'; -import { buildClientApp } from './build-ssr'; -import { - mockBuildFunctions, - mockNitroConfig, - mockViteDevServer, - runConfigAndCloseBundle, -} from './vite-nitro-plugin.spec.data'; -import { nitro } from './vite-plugin-nitro'; - -function writeBuiltClientIndexHtml( - workspaceRoot: string, - html = '', - clientBuildDir = resolve(workspaceRoot, 'dist', 'client'), -) { - mkdirSync(clientBuildDir, { recursive: true }); - writeFileSync(resolve(clientBuildDir, 'index.html'), html); -} - -describe('nitro', () => { - afterEach(() => { - vi.restoreAllMocks(); - vi.unstubAllEnvs(); - }); - - it('should work', () => { - expect(nitro({})[1].name).toEqual('@analogjs/vite-plugin-nitro'); - }); - - it('should snapshot the incoming Vite config before later mutations', async () => { - vi.stubEnv('VITEST', ''); - vi.stubEnv('NODE_ENV', 'production'); - - const workspaceRoot = mkdtempSync(join(tmpdir(), 'analog-nitro-config-')); - const originalBuildOutDir = 'custom-client'; - const originalAlias = { '@app/root': '/virtual/original-entry.ts' }; - const originalClientEnvironmentOutDir = 'env-client-output'; - const pluginPrototype = { marker: 'user-plugin-prototype' }; - const originalHook = { handler: vi.fn(), order: 'pre' }; - const userPlugin = Object.assign(Object.create(pluginPrototype), { - name: 'user-plugin', - configResolved: originalHook, - }) as vite.Plugin; - const userConfig: vite.UserConfig = { - root: workspaceRoot, - build: { outDir: originalBuildOutDir }, - environments: { - client: { - build: { - outDir: originalClientEnvironmentOutDir, - }, - }, - } as vite.UserConfig['environments'], - plugins: [userPlugin], - resolve: { - alias: originalAlias, - }, - }; - const ssrBuildDir = resolve(workspaceRoot, 'dist', 'ssr'); - - const { buildServerImportSpy } = await mockBuildFunctions(); - vi.mocked(buildClientApp).mockImplementation(async () => { - writeBuiltClientIndexHtml( - workspaceRoot, - 'snapshot', - resolve(workspaceRoot, originalBuildOutDir), - ); - }); - - try { - mkdirSync(ssrBuildDir, { recursive: true }); - writeFileSync( - resolve(ssrBuildDir, 'main.server.js'), - 'export default async function renderer() {}', - ); - - const plugin = nitro({ workspaceRoot }); - - await (plugin[1].config as any)(userConfig, { - command: 'build', - mode: 'production', - }); - - const mutatedHook = { handler: vi.fn(), order: 'post' }; - // Simulate later config hooks mutating the user-owned object after - // Nitro's `config()` hook. The assertions below prove Nitro keeps - // building from the captured snapshot instead of drifting with those - // later edits. - userConfig.build!.outDir = 'mutated-client'; - userConfig.resolve!.alias = { - '@app/root': '/virtual/mutated-entry.ts', - }; - ( - userConfig.environments!['client'] as { build?: { outDir?: string } } - ).build = { - outDir: 'mutated-env-client-output', - }; - (userConfig.plugins![0] as Record)['configResolved'] = - mutatedHook; - userConfig.plugins!.push({ - name: 'mutated-plugin', - } as vite.Plugin); - - await (plugin[1].closeBundle as any)(); - - expect(buildClientApp).toHaveBeenCalledOnce(); - expect(buildServerImportSpy).toHaveBeenCalledOnce(); - - const capturedConfig = vi.mocked(buildClientApp).mock.calls[0]?.[0] as - | vite.UserConfig - | undefined; - const capturedPlugin = capturedConfig?.plugins?.[0] as - | Record - | undefined; - const capturedClientEnvironment = capturedConfig?.environments?.[ - 'client' - ] as { build?: { outDir?: string } } | undefined; - - expect(capturedConfig?.build?.outDir).toBe(originalBuildOutDir); - expect(capturedConfig?.resolve?.alias).toEqual(originalAlias); - expect(capturedClientEnvironment?.build?.outDir).toBe( - originalClientEnvironmentOutDir, - ); - expect(capturedConfig?.plugins).toHaveLength(1); - expect(capturedPlugin).toBeDefined(); - expect(Object.getPrototypeOf(capturedPlugin!)).toBe(pluginPrototype); - expect(capturedPlugin?.['configResolved']).toEqual(originalHook); - expect(capturedPlugin?.['configResolved']).not.toBe(originalHook); - expect(capturedPlugin?.['configResolved']).not.toBe(mutatedHook); - } finally { - rmSync(workspaceRoot, { recursive: true, force: true }); - } - }); - - it('should not call the route middleware in test mode', async () => { - // Arrange - const spy = vi.spyOn(mockViteDevServer.middlewares, 'use'); - - // Act - await (nitro({})[1].configureServer as any)(mockViteDevServer); - - // Assert - expect(spy).toHaveBeenCalledTimes(0); - expect(spy).not.toHaveBeenCalledWith('/api', expect.anything()); - }); - - it('should initialize Nitro dev mode with renderer virtual modules', async () => { - const nitroInstance = {} as never; - const devServer = { - fetch: vi.fn(), - upgrade: vi.fn(), - } as never; - const use = vi.fn(); - const once = vi.fn(); - const on = vi.fn(); - - vi.mocked(createNitro).mockResolvedValue(nitroInstance); - vi.mocked(createDevServer).mockReturnValue(devServer); - vi.mocked(build).mockResolvedValue(undefined as never); - vi.stubEnv('VITEST', ''); - vi.stubEnv('NODE_ENV', 'development'); - - const plugin = nitro({ ssr: true }); - await (plugin[1].config as any)( - {}, - { command: 'serve', mode: 'development' }, - ); - - const configureNitro = await (plugin[1].configureServer as any)({ - config: { - root: '/workspace/app', - server: { - host: '127.0.0.1', - port: 4300, - }, - }, - httpServer: { - once, - on, - }, - middlewares: { - stack: [], - use, - }, - watcher: { - on: vi.fn(), - }, - }); - - await configureNitro?.(); - - expect(createNitro).toHaveBeenCalledWith( - expect.objectContaining({ - builder: 'rollup', - dev: true, - virtual: expect.objectContaining({ - '#ANALOG_SSR_RENDERER': expect.stringContaining( - "import template from '#analog/index';", - ), - '#ANALOG_CLIENT_RENDERER': expect.stringContaining( - "import template from '#analog/index';", - ), - }), - }), - ); - expect(createDevServer).toHaveBeenCalledWith(nitroInstance); - expect(build).toHaveBeenCalledWith(nitroInstance); - expect(use).toHaveBeenCalled(); - expect(once).toHaveBeenCalledWith('listening', expect.any(Function)); - expect(on).not.toHaveBeenCalled(); - }); - - it('should use the active Vite SSR bundler config key', async () => { - vi.stubEnv('VITEST', ''); - vi.stubEnv('NODE_ENV', 'production'); - const plugin = nitro({}); - const result = await (plugin[1].config as any)( - {}, - { command: 'build', mode: 'production' }, - ); - const ssrBuild = result.environments.ssr.build; - const activeKey = vite.rolldownVersion - ? 'rolldownOptions' - : 'rollupOptions'; - const inactiveKey = vite.rolldownVersion - ? 'rollupOptions' - : 'rolldownOptions'; - - expect(ssrBuild).toHaveProperty(activeKey); - expect(ssrBuild[activeKey]).toEqual( - expect.objectContaining({ - input: expect.stringMatching(/src[\\/]+main\.server\.ts$/), - }), - ); - expect(ssrBuild.emptyOutDir).toBe(false); - expect(ssrBuild).not.toHaveProperty(inactiveKey); - }); - - it.runIf(vite.rolldownVersion)( - 'should forward nested vite rolldown codeSplitting config to the client build (Rolldown)', - async () => { - vi.stubEnv('VITEST', ''); - vi.stubEnv('NODE_ENV', 'production'); - const codeSplitting = { - groups: [{ test: /node_modules/, name: 'vendor' }], - }; - const plugin = nitro({ - vite: { - build: { - rolldownOptions: { - output: { - codeSplitting, - entryFileNames: 'assets/[name].js', - } as any, - }, - }, - }, - }); - const result = await (plugin[1].config as any)( - {}, - { command: 'build', mode: 'production' }, - ); - const clientBuild = result.environments.client.build; - - expect(clientBuild.rolldownOptions.output).toEqual( - expect.objectContaining({ - codeSplitting, - entryFileNames: 'assets/[name].js', - }), - ); - }, - ); - - it.runIf(!vite.rolldownVersion)( - 'should not have rolldownOptions when not using Rolldown', - async () => { - vi.stubEnv('VITEST', ''); - vi.stubEnv('NODE_ENV', 'production'); - const codeSplitting = { - groups: [{ test: /node_modules/, name: 'vendor' }], - }; - const plugin = nitro({ - vite: { - build: { - rolldownOptions: { - output: { - codeSplitting, - entryFileNames: 'assets/[name].js', - } as any, - }, - }, - }, - }); - const result = await (plugin[1].config as any)( - {}, - { command: 'build', mode: 'production' }, - ); - const clientBuild = result.environments.client.build; - - expect(clientBuild).not.toHaveProperty('rolldownOptions'); - }, - ); - - it.runIf(vite.rolldownVersion)( - 'should ignore codeSplitting forwarding when rolldown output is an array', - async () => { - vi.stubEnv('VITEST', ''); - vi.stubEnv('NODE_ENV', 'production'); - const plugin = nitro({ - vite: { - build: { - rolldownOptions: { - output: [{ entryFileNames: 'assets/[name].js' }] as any, - }, - }, - }, - }); - const result = await (plugin[1].config as any)( - {}, - { command: 'build', mode: 'production' }, - ); - const clientBuild = result.environments.client.build; - - expect(clientBuild.rolldownOptions).toBeUndefined(); - }, - ); - - it('should strip Rolldown-only codeSplitting from Nitro rollup builds', async () => { - vi.stubEnv('VITEST', ''); - vi.stubEnv('NODE_ENV', 'production'); - const { buildServerImportSpy } = await mockBuildFunctions(); - const workspaceRoot = mkdtempSync(join(tmpdir(), 'analog-nitro-')); - - try { - const ssrBuildDir = resolve(workspaceRoot, 'dist', 'ssr'); - const builtSsrEntry = resolve(ssrBuildDir, 'main.server.js'); - mkdirSync(ssrBuildDir, { recursive: true }); - writeFileSync( - builtSsrEntry, - 'export default async function renderer() {}', - ); - writeBuiltClientIndexHtml(workspaceRoot, 'rollup build'); - - const plugin = nitro({ - workspaceRoot, - ssrBuildDir, - }); - const result = await (plugin[1].config as any)( - {}, - { command: 'build', mode: 'production' }, - ); - await result.builder.buildApp({ - build: vi.fn().mockResolvedValue(undefined), - environments: { - client: {}, - ssr: {}, - }, - }); - - const nitroConfig = buildServerImportSpy.mock.calls[0][1]; - const bundlerConfig = { - output: { - codeSplitting: { groups: [{ test: /node_modules/, name: 'vendor' }] }, - entryFileNames: 'index.mjs', - }, - }; - - await nitroConfig.hooks['rollup:before']({}, bundlerConfig); - - expect(bundlerConfig.output).toEqual({ - entryFileNames: 'index.mjs', - }); - expect(nitroConfig.virtual?.['#analog/index']).toBe( - 'export default "rollup build";', - ); - } finally { - rmSync(workspaceRoot, { recursive: true, force: true }); - } - }); - - it('should alias the built SSR entry for Nitro server builds', async () => { - vi.stubEnv('VITEST', ''); - vi.stubEnv('NODE_ENV', 'production'); - const { buildServerImportSpy } = await mockBuildFunctions(); - const workspaceRoot = mkdtempSync(join(tmpdir(), 'analog-nitro-')); - - try { - const ssrBuildDir = resolve(workspaceRoot, 'dist', 'demo', 'ssr'); - const builtSsrEntry = resolve(ssrBuildDir, 'main.server.js'); - mkdirSync(ssrBuildDir, { recursive: true }); - writeFileSync( - builtSsrEntry, - 'export default async function renderer() {}', - ); - writeBuiltClientIndexHtml(workspaceRoot, 'ssr alias'); - - const plugin = nitro({ - ssr: true, - workspaceRoot, - ssrBuildDir, - }); - const result = await (plugin[1].config as any)( - {}, - { command: 'build', mode: 'production' }, - ); - - await result.builder.buildApp({ - build: vi.fn().mockResolvedValue(undefined), - environments: { - client: {}, - ssr: {}, - }, - }); - - const nitroConfig = buildServerImportSpy.mock.calls[0][1]; - const expectedAlias = vite.normalizePath(builtSsrEntry); - - expect(nitroConfig.alias).toEqual( - expect.objectContaining({ - '#analog/ssr': expectedAlias, - }), - ); - expect(nitroConfig.virtual?.['#ANALOG_SSR_RENDERER']).toContain( - "import renderer from '#analog/ssr';", - ); - expect(nitroConfig.virtual?.['#analog/index']).toBe( - 'export default "ssr alias";', - ); - } finally { - rmSync(workspaceRoot, { recursive: true, force: true }); - } - }); - - it('passes only canonical page routes to sitemap generation in builder.buildApp', async () => { - vi.stubEnv('VITEST', ''); - vi.stubEnv('NODE_ENV', 'production'); - const { buildSitemapImportSpy } = await mockBuildFunctions(); - const workspaceRoot = mkdtempSync(join(tmpdir(), 'analog-nitro-')); - - try { - const ssrBuildDir = resolve(workspaceRoot, 'dist', 'ssr'); - mkdirSync(ssrBuildDir, { recursive: true }); - writeFileSync( - resolve(ssrBuildDir, 'main.server.js'), - 'export default async function renderer() {}', - ); - writeBuiltClientIndexHtml(workspaceRoot, 'sitemap buildApp'); - - const plugin = nitro({ - workspaceRoot, - prerender: { - sitemap: { host: 'https://example.com' }, - routes: [ - '/about', - { - route: '/blog', - staticData: true, - sitemap: { - lastmod: '2024-02-10', - }, - }, - ], - }, - }); - const result = await (plugin[1].config as any)( - {}, - { command: 'build', mode: 'production' }, - ); - - await result.builder.buildApp({ - build: vi.fn().mockResolvedValue(undefined), - environments: { - client: {}, - ssr: {}, - }, - }); - - expect(buildSitemapImportSpy).toHaveBeenCalledWith( - {}, - { host: 'https://example.com' }, - ['/about', '/blog'], - resolve(workspaceRoot, 'dist', 'analog', 'public'), - { - '/blog': { lastmod: '2024-02-10' }, - }, - { apiPrefix: 'api' }, - ); - } finally { - rmSync(workspaceRoot, { recursive: true, force: true }); - } - }); - - it('does not require an SSR entry for client-only apps with explicit empty prerender routes', async () => { - vi.stubEnv('VITEST', ''); - vi.stubEnv('NODE_ENV', 'production'); - const { buildServerImportSpy } = await mockBuildFunctions(); - const workspaceRoot = mkdtempSync(join(tmpdir(), 'analog-nitro-')); - - try { - writeBuiltClientIndexHtml( - workspaceRoot, - 'client only explicit prerender opt-out', - ); - - const plugin = nitro({ - workspaceRoot, - ssr: false, - prerender: { - routes: [], - }, - }); - const result = await (plugin[1].config as any)( - {}, - { command: 'build', mode: 'production' }, - ); - - const builderBuild = vi.fn().mockResolvedValue(undefined); - await result.builder.buildApp({ - build: builderBuild, - environments: { - client: {}, - ssr: {}, - }, - }); - - expect(builderBuild).toHaveBeenCalledTimes(1); - expect(builderBuild).not.toHaveBeenCalledWith( - expect.objectContaining({ - config: expect.objectContaining({ - build: expect.objectContaining({ - ssr: true, - }), - }), - }), - ); - - const nitroConfig = buildServerImportSpy.mock.calls[0][1]; - expect(nitroConfig.alias?.['#analog/ssr']).toBeUndefined(); - expect(nitroConfig.virtual?.['#ANALOG_CLIENT_RENDERER']).toContain( - "import template from '#analog/index';", - ); - expect(nitroConfig.virtual?.['#analog/index']).toBe( - 'export default "client only explicit prerender opt-out";', - ); - } finally { - rmSync(workspaceRoot, { recursive: true, force: true }); - } - }); - - it('should resolve client output path correctly for nested roots without explicit build.outDir', async () => { - vi.stubEnv('VITEST', ''); - vi.stubEnv('NODE_ENV', 'production'); - const { buildServerImportSpy } = await mockBuildFunctions(); - const workspaceRoot = mkdtempSync(join(tmpdir(), 'analog-nitro-')); - - try { - const nestedRoot = 'apps/my-app'; - const ssrBuildDir = resolve(workspaceRoot, 'dist', nestedRoot, 'ssr'); - const builtSsrEntry = resolve(ssrBuildDir, 'main.server.js'); - mkdirSync(ssrBuildDir, { recursive: true }); - writeFileSync( - builtSsrEntry, - 'export default async function renderer() {}', - ); - - // The client build emits to /dist//client when no - // explicit build.outDir is set — write index.html there. - const clientBuildDir = resolve( - workspaceRoot, - 'dist', - nestedRoot, - 'client', - ); - mkdirSync(clientBuildDir, { recursive: true }); - writeFileSync( - resolve(clientBuildDir, 'index.html'), - 'nested root', - ); - - // Create the nested app source directory so the plugin can resolve it. - mkdirSync(resolve(workspaceRoot, nestedRoot, 'src/server'), { - recursive: true, - }); - - const plugin = nitro({ - workspaceRoot, - ssrBuildDir, - }); - const result = await (plugin[1].config as any)( - { root: nestedRoot }, - { command: 'build', mode: 'production' }, - ); - await result.builder.buildApp({ - build: vi.fn().mockResolvedValue(undefined), - environments: { - client: {}, - ssr: {}, - }, - }); - - const nitroConfig = buildServerImportSpy.mock.calls[0][1]; - - // registerIndexHtmlVirtual must read index.html from - // /dist//client — not //dist/client. - expect(nitroConfig.virtual?.['#analog/index']).toBe( - 'export default "nested root";', - ); - } finally { - rmSync(workspaceRoot, { recursive: true, force: true }); - } - }); - - it('uses the finalized client environment outDir during builder.buildApp', async () => { - vi.stubEnv('VITEST', ''); - vi.stubEnv('NODE_ENV', 'production'); - const { buildServerImportSpy } = await mockBuildFunctions(); - const workspaceRoot = mkdtempSync(join(tmpdir(), 'analog-nitro-')); - - try { - const nestedRoot = 'apps/my-app'; - const ssrBuildDir = resolve(workspaceRoot, 'dist', nestedRoot, 'ssr'); - const builtSsrEntry = resolve(ssrBuildDir, 'main.server.js'); - const staleClientDir = resolve( - workspaceRoot, - 'dist', - nestedRoot, - 'client', - ); - const finalClientDir = resolve( - workspaceRoot, - 'dist', - nestedRoot, - 'client-final', - ); - - mkdirSync(ssrBuildDir, { recursive: true }); - writeFileSync( - builtSsrEntry, - 'export default async function renderer() {}', - ); - writeBuiltClientIndexHtml( - workspaceRoot, - 'finalized client env', - finalClientDir, - ); - mkdirSync(resolve(workspaceRoot, nestedRoot, 'src/server'), { - recursive: true, - }); - - const plugin = nitro({ - workspaceRoot, - ssrBuildDir, - }); - const result = await (plugin[1].config as any)( - { - root: nestedRoot, - build: { - outDir: '../../dist/apps/my-app/client', - }, - }, - { command: 'build', mode: 'production' }, - ); - - await result.builder.buildApp({ - build: vi.fn().mockResolvedValue(undefined), - environments: { - client: { - config: { - build: { - outDir: '../../dist/apps/my-app/client-final', - }, - }, - }, - ssr: {}, - }, - }); - - const nitroConfig = buildServerImportSpy.mock.calls[0][1]; - - expect(nitroConfig.virtual?.['#analog/index']).toBe( - 'export default "finalized client env";', - ); - expect(nitroConfig.publicAssets).toEqual([ - { - dir: vite.normalizePath(finalClientDir), - maxAge: 0, - }, - ]); - expect(nitroConfig.publicAssets).not.toEqual([ - { - dir: vite.normalizePath(staleClientDir), - maxAge: 0, - }, - ]); - } finally { - rmSync(workspaceRoot, { recursive: true, force: true }); - } - }); - - it('falls back to the captured client index asset during closeBundle', async () => { - const { buildServerImportSpy } = await mockBuildFunctions(); - const workspaceRoot = mkdtempSync(join(tmpdir(), 'analog-nitro-')); - - try { - const ssrBuildDir = resolve(workspaceRoot, 'dist', 'ssr'); - mkdirSync(ssrBuildDir, { recursive: true }); - writeFileSync( - resolve(ssrBuildDir, 'main.server.js'), - 'export default async function renderer() {}', - ); - - const plugin = nitro({ - workspaceRoot, - ssrBuildDir, - }); - - await (plugin[1].config as any)( - {}, - { command: 'build', mode: 'production' }, - ); - - await (plugin[1].generateBundle as any)( - {}, - { - 'index.html': { - type: 'asset', - fileName: 'index.html', - source: 'captured bundle asset', - }, - }, - ); - - await (plugin[1].closeBundle as any)(); - - const nitroConfig = buildServerImportSpy.mock.calls[0][1]; - expect(nitroConfig.virtual?.['#analog/index']).toBe( - 'export default "captured bundle asset";', - ); - } finally { - rmSync(workspaceRoot, { recursive: true, force: true }); - } - }); - - it('rebuilds the client output during closeBundle when index.html is missing', async () => { - const { buildServerImportSpy } = await mockBuildFunctions(); - const workspaceRoot = mkdtempSync(join(tmpdir(), 'analog-nitro-')); - - try { - const ssrBuildDir = resolve(workspaceRoot, 'dist', 'ssr'); - mkdirSync(ssrBuildDir, { recursive: true }); - writeFileSync( - resolve(ssrBuildDir, 'main.server.js'), - 'export default async function renderer() {}', - ); - - vi.mocked(buildClientApp).mockImplementation(async () => { - writeBuiltClientIndexHtml( - workspaceRoot, - 'rebuilt client output', - ); - }); - - const plugin = nitro({ - workspaceRoot, - }); - - await (plugin[1].config as any)( - { - root: '.', - build: { - outDir: 'dist/client', - }, - }, - { command: 'build', mode: 'production' }, - ); - - await (plugin[1].closeBundle as any)(); - - expect(buildClientApp).toHaveBeenCalledWith( - expect.objectContaining({ - build: expect.objectContaining({ - outDir: 'dist/client', - }), - }), - expect.objectContaining({ - workspaceRoot, - }), - ); - - const nitroConfig = buildServerImportSpy.mock.calls[0][1]; - expect(nitroConfig.virtual?.['#analog/index']).toBe( - 'export default "rebuilt client output";', - ); - } finally { - rmSync(workspaceRoot, { recursive: true, force: true }); - } - }); - - describe.skip('preset output', () => { - it('should use the analog output paths when preset is not vercel', async () => { - // Arrange - vi.spyOn(process, 'cwd').mockReturnValue('/custom-root-directory'); - const { buildServerImportSpy } = await mockBuildFunctions(); - - const plugin = nitro({}, {}); - - // Act - await runConfigAndCloseBundle(plugin); - - // Assert - expect(buildServerImportSpy).toHaveBeenCalledWith( - {}, - expect.objectContaining({ - output: { - dir: '/custom-root-directory/dist/analog', - publicDir: '/custom-root-directory/dist/analog/public', - }, - }), - ); - }); - - it('should use the workspace root option when it is set', async () => { - // Arrange - vi.spyOn(process, 'cwd').mockReturnValue('/some-other-root-directory'); - const { buildServerImportSpy } = await mockBuildFunctions(); - - const plugin = nitro({ workspaceRoot: '/custom-root-directory' }, {}); - - // Act - await runConfigAndCloseBundle(plugin); - - // Assert - expect(buildServerImportSpy).toHaveBeenCalledWith( - { workspaceRoot: '/custom-root-directory' }, - expect.objectContaining({ - output: { - dir: '/custom-root-directory/some-other-root-directory/analog', - publicDir: - '/custom-root-directory/some-other-root-directory/analog/public', - }, - }), - ); - }); - - it('should use the .vercel output paths when preset is vercel', async () => { - // Arrange - vi.spyOn(process, 'cwd').mockReturnValue('/custom-root-directory'); - const { buildServerImportSpy } = await mockBuildFunctions(); - - const plugin = nitro({}, { preset: 'vercel' }); - - // Act - await runConfigAndCloseBundle(plugin); - - // Assert - expect(buildServerImportSpy).toHaveBeenCalledWith( - {}, - expect.objectContaining({ - preset: 'vercel', - output: { - dir: '/custom-root-directory/.vercel/output', - publicDir: '/custom-root-directory/.vercel/output/static', - }, - vercel: expect.objectContaining({ - entryFormat: 'node', - functions: expect.objectContaining({ - runtime: 'nodejs24.x', - }), - }), - }), - ); - }); - - it('should use the .vercel output paths without runtime config when preset is vercel', async () => { - // Arrange - vi.spyOn(process, 'cwd').mockReturnValue('/custom-root-directory'); - const { buildServerImportSpy } = await mockBuildFunctions(); - - const plugin = nitro({}, { preset: 'vercel' }); - - // Act - await runConfigAndCloseBundle(plugin); - - // Assert - expect(buildServerImportSpy).toHaveBeenCalledWith( - {}, - expect.objectContaining({ - preset: 'vercel', - output: { - dir: '/custom-root-directory/.vercel/output', - publicDir: '/custom-root-directory/.vercel/output/static', - }, - }), - ); - }); - - it('should use the .vercel output paths when preset is VERCEL environment variable is set', async () => { - // Arrange - vi.stubEnv('VERCEL', '1'); - vi.spyOn(process, 'cwd').mockReturnValue('/custom-root-directory'); - const { buildServerImportSpy } = await mockBuildFunctions(); - - const plugin = nitro({}, {}); - - // Act - await runConfigAndCloseBundle(plugin); - - // Assert - expect(buildServerImportSpy).toHaveBeenCalledWith( - {}, - expect.objectContaining({ - preset: 'vercel', - output: { - dir: '/custom-root-directory/.vercel/output', - publicDir: '/custom-root-directory/.vercel/output/static', - }, - vercel: expect.objectContaining({ - entryFormat: 'node', - functions: expect.objectContaining({ - runtime: 'nodejs24.x', - }), - }), - }), - ); - }); - }); -}); diff --git a/packages/vite-plugin-nitro/src/lib/vite-plugin-nitro.ts b/packages/vite-plugin-nitro/src/lib/vite-plugin-nitro.ts deleted file mode 100644 index 400038166..000000000 --- a/packages/vite-plugin-nitro/src/lib/vite-plugin-nitro.ts +++ /dev/null @@ -1,1735 +0,0 @@ -import type { NitroConfig, NitroEventHandler, RollupConfig } from 'nitro/types'; -import { build, createDevServer, createNitro } from 'nitro/builder'; -import * as vite from 'vite'; -import type { Plugin, UserConfig, ViteDevServer } from 'vite'; -import { mergeConfig, normalizePath } from 'vite'; -import { relative, resolve } from 'node:path'; -import { pathToFileURL } from 'node:url'; -import { existsSync, readFileSync, readdirSync } from 'node:fs'; -import type { IncomingMessage, ServerResponse } from 'node:http'; - -import { buildServer, isVercelPreset } from './build-server.js'; -import { buildClientApp, buildSSRApp } from './build-ssr.js'; -import { - Options, - PrerenderContentDir, - PrerenderContentFile, - PrerenderRouteConfig, - PrerenderSitemapConfig, -} from './options.js'; -import { pageEndpointsPlugin } from './plugins/page-endpoints.js'; -import { getPageHandlers } from './utils/get-page-handlers.js'; -import { buildSitemap } from './build-sitemap.js'; -import { devServerPlugin } from './plugins/dev-server-plugin.js'; -import { - toWebRequest, - writeWebResponseToNode, -} from './utils/node-web-bridge.js'; -import { getMatchingContentFilesWithFrontMatter } from './utils/get-content-files.js'; -import { - ssrRenderer, - clientRenderer, - apiMiddleware, -} from './utils/renderers.js'; -import { getBundleOptionsKey, isRolldown } from './utils/rolldown.js'; -import { debugNitro, debugSsr } from './utils/debug.js'; - -// Snapshot the caller-owned Vite config once so the client build, SSR handoff, -// and closeBundle all read the same view of the app. -// -// Guards against: -// - `build.outDir` can change after capture, sending the client sub-build to a -// different directory than the one Nitro later probes for `index.html`. -// - a plugin can replace `{ handler, order }` with a new wrapper object, which -// changes hook ordering for Nitro even though Nitro already "captured" config. -// - `resolve.alias` can be rewritten between the client and SSR passes, causing -// the two environments to build against different module graphs. -type ObjectHook = { handler: T; [key: string]: unknown }; - -function isObjectHook(value: unknown): value is ObjectHook { - return !!value && typeof value === 'object' && 'handler' in value; -} - -// Freeze hook-wrapper metadata without changing the executable handler. -// Value: Nitro keeps the same hook ordering and flags it captured at config() -// time, while still invoking the original plugin behavior. -// -// Guards against: a caller swapping `{ handler, order: 'pre' }` for a fresh -// `{ handler, order: 'post' }` object after Nitro's `config()` hook ran. Nitro -// should keep the captured ordering metadata instead of silently retargeting -// when the outer wrapper object changes. -function cloneObjectHook(hook: T): T { - if (!isObjectHook(hook)) { - return hook; - } - - return { - ...hook, - handler: hook.handler, - } as T; -} - -function cloneUserPlugin(plugin: T): T { - if (!plugin || typeof plugin !== 'object') return plugin; - const pluginRecord = plugin as Record; - // Preserve the original prototype because some plugins hang metadata or - // behavior off the instance instead of plain object fields. - const clone = Object.assign( - Object.create(Object.getPrototypeOf(plugin)), - pluginRecord, - ) as Record; - - for (const key of Object.keys(pluginRecord)) { - clone[key] = cloneObjectHook(pluginRecord[key]); - } - - return clone as T; -} - -function cloneEnvironmentEntries( - environments: UserConfig['environments'], -): UserConfig['environments'] { - if (!environments || typeof environments !== 'object') { - return environments; - } - - // Snapshot the per-environment overrides Nitro reads later. - // Value: environment-specific output paths and diagnostics stay aligned with - // the build Nitro already started coordinating. - // - // Guards against: a late write like - // `environments.client.build.outDir = ...` does not redirect follow-up - // diagnostics or asset lookups away from the client build Nitro already - // started coordinating. - return Object.fromEntries( - Object.entries(environments).map(([name, environment]) => { - if (!environment || typeof environment !== 'object') { - return [name, environment]; - } - - const environmentRecord = environment as Record; - return [ - name, - { - ...environmentRecord, - build: - environmentRecord['build'] && - typeof environmentRecord['build'] === 'object' - ? { ...(environmentRecord['build'] as Record) } - : environmentRecord['build'], - }, - ]; - }), - ) as UserConfig['environments']; -} - -// Take a selective snapshot of the mutable config branches Nitro re-reads -// after `config()` returns: plugin entries, build/server/test options, resolve -// aliases, and environment overrides. -// -// Value: Nitro can coordinate multiple build phases from one stable config -// view without breaking plugin identity or executable behavior. -// -// Guards against: -// - a later write to `config.build.outDir` can desynchronize where the client -// build writes files vs where Nitro tries to read them back. -// - plugin array edits after capture can add/remove behavior from one sub-build -// but not the other, which makes client and SSR resolution diverge. -// - alias rewrites after capture can make the SSR environment import different -// files than the client environment even though Nitro is orchestrating one app. -// - environment-specific build overrides can drift after capture, which makes -// diagnostics and any environment-aware follow-up logic observe the wrong -// client/SSR shape. -// -// We do not deep-clone functions or plugin instances because Nitro still needs -// the original executable behavior and plugin shape. The problem here is -// mutable container objects, not function identity. -function cloneUserConfig(userConfig: UserConfig): UserConfig { - const { environments, resolve, build, server, plugins } = userConfig; - const test = (userConfig as UserConfig & { test?: Record }) - .test; - return { - ...userConfig, - plugins: plugins?.map(cloneUserPlugin), - build: build && { ...build }, - environments: cloneEnvironmentEntries(environments), - server: server && { ...server }, - test: test && { ...test }, - resolve: resolve && { - ...resolve, - alias: Array.isArray(resolve.alias) - ? [...resolve.alias] - : resolve.alias && typeof resolve.alias === 'object' - ? { ...resolve.alias } - : resolve.alias, - }, - } as UserConfig; -} - -function createNitroMiddlewareHandler(handler: string): NitroEventHandler { - return { - route: '/**', - handler, - middleware: true, - }; -} - -/** - * Creates a `rollup:before` hook that marks specified packages as external - * in Nitro's bundler config (applied to both the server build and the - * prerender build). - * - * ## Subpath matching (Rolldown compatibility) - * - * When `bundlerConfig.external` is an **array**, Rollup automatically - * prefix-matches entries — `'rxjs'` in the array will also externalise - * `'rxjs/operators'`, `'rxjs/internal/Observable'`, etc. - * - * Rolldown (the default bundler in Nitro v3) does **not** do this. It - * treats array entries as exact strings. To keep behaviour consistent - * across both bundlers, the **function** branch already needed explicit - * subpath matching. We now use the same `isExternal` helper for all - * branches so that `'rxjs'` reliably matches `'rxjs/operators'` - * regardless of whether the existing `external` value is a function, - * array, or absent. - * - * Without this, the Nitro prerender build fails on Windows CI with: - * - * [RESOLVE_ERROR] Could not resolve 'rxjs/operators' - */ -function createRollupBeforeHook(externalEntries: string[]) { - const isExternal = (source: string) => - externalEntries.some( - (entry) => source === entry || source.startsWith(entry + '/'), - ); - - return (_nitro: unknown, bundlerConfig: RollupConfig) => { - sanitizeNitroBundlerConfig(_nitro, bundlerConfig); - - if (externalEntries.length === 0) { - return; - } - - const existing = bundlerConfig.external; - if (!existing) { - bundlerConfig.external = externalEntries; - } else if (typeof existing === 'function') { - bundlerConfig.external = ( - source: string, - importer: string | undefined, - isResolved: boolean, - ) => existing(source, importer, isResolved) || isExternal(source); - } else if (Array.isArray(existing)) { - bundlerConfig.external = [...existing, ...externalEntries]; - } else { - bundlerConfig.external = [existing as string, ...externalEntries]; - } - }; -} - -function appendNoExternals( - noExternals: NitroConfig['noExternals'], - ...entries: string[] -): NitroConfig['noExternals'] { - if (!noExternals) { - return entries; - } - - return Array.isArray(noExternals) - ? [...noExternals, ...entries] - : noExternals; -} - -/** - * Patches Nitro's internal Rollup/Rolldown bundler config to work around - * incompatibilities in the Nitro v3 alpha series. - * - * Called from the `rollup:before` hook, this function runs against the *final* - * bundler config that Nitro assembles for its server/prerender builds — it - * does NOT touch the normal Vite client or SSR environment configs. - * - * Each workaround is narrowly scoped and safe to remove once the corresponding - * upstream Nitro issue is resolved. - */ -function sanitizeNitroBundlerConfig( - _nitro: unknown, - bundlerConfig: RollupConfig, -) { - const output = bundlerConfig['output']; - if (!output || Array.isArray(output) || typeof output !== 'object') { - return; - } - - // ── 1. Remove invalid `output.codeSplitting` ──────────────────────── - // - // Nitro 3.0.1-alpha.2 adds `output.codeSplitting` to its internal bundler - // config, but Rolldown rejects it as an unknown key: - // - // Warning: Invalid output options (1 issue found) - // - For the "codeSplitting". Invalid key: Expected never but received "codeSplitting". - // - // Analog never sets this option. Removing it restores default bundler - // behavior without changing any Analog semantics. - if ('codeSplitting' in output) { - delete (output as Record)['codeSplitting']; - } - - // ── 2. Remove invalid `output.manualChunks` ───────────────────────── - // - // Nitro's default config enables manual chunking for node_modules. Under - // Nitro v3 alpha + Rollup 4.59 this crashes during the prerender rebundle: - // - // Cannot read properties of undefined (reading 'included') - // - // A single server bundle is acceptable for Analog's use case, so we strip - // `manualChunks` until the upstream bug is fixed. - if ('manualChunks' in output) { - delete (output as Record)['manualChunks']; - } - - // ── 3. Escape route params in `output.chunkFileNames` ─────────────── - // - // Nitro's `getChunkName()` derives chunk filenames from route patterns, - // using its internal `routeToFsPath()` helper to convert route params - // (`:productId` → `[productId]`) and catch-alls (`**` → `[...]`). - // - // Rollup/Rolldown interprets *any* `[token]` in the string returned by a - // `chunkFileNames` function as a placeholder. Only a handful are valid — - // `[name]`, `[hash]`, `[format]`, `[ext]` — so route-derived tokens like - // `[productId]` or `[...]` trigger a build error: - // - // "[productId]" is not a valid placeholder in the "output.chunkFileNames" pattern. - // - // We wrap the original function to replace non-standard `[token]` patterns - // with `_token_`, preserving the intended filename while avoiding the - // placeholder validation error. - // - // Example: `_routes/products/[productId].mjs` → `_routes/products/_productId_.mjs` - const VALID_ROLLUP_PLACEHOLDER = /^\[(?:name|hash|format|ext)\]$/; - const chunkFileNames = (output as Record)['chunkFileNames']; - if (typeof chunkFileNames === 'function') { - const originalFn = chunkFileNames as (...args: unknown[]) => unknown; - (output as Record)['chunkFileNames'] = ( - ...args: unknown[] - ) => { - const result = originalFn(...args); - if (typeof result !== 'string') return result; - return result.replace(/\[[^\]]+\]/g, (match: string) => - VALID_ROLLUP_PLACEHOLDER.test(match) - ? match - : `_${match.slice(1, -1)}_`, - ); - }; - } -} - -function resolveClientOutputPath( - cachedPath: string, - workspaceRoot: string, - rootDir: string, - configuredOutDir: string | undefined, -) { - if (cachedPath) { - debugNitro('resolveClientOutputPath using cached path', { - cachedPath, - workspaceRoot, - rootDir, - configuredOutDir, - }); - return cachedPath; - } - - if (configuredOutDir) { - const resolvedPath = normalizePath( - resolve(workspaceRoot, rootDir, configuredOutDir), - ); - debugNitro('resolveClientOutputPath using configured build.outDir', { - workspaceRoot, - rootDir, - configuredOutDir, - resolvedPath, - }); - return resolvedPath; - } - - // When no explicit build.outDir is set, the environment build config defaults - // to `/dist//client` for the client build. The non-SSR - // (client) and SSR paths must agree on this so that registerIndexHtmlVirtual() - // and publicAssets read from the directory the client build actually wrote to. - const resolvedPath = normalizePath( - resolve(workspaceRoot, 'dist', rootDir, 'client'), - ); - debugNitro('resolveClientOutputPath using default dist client path', { - workspaceRoot, - rootDir, - configuredOutDir, - resolvedPath, - }); - return resolvedPath; -} - -function getEnvironmentBuildOutDir(environment: unknown): string | undefined { - if (!environment || typeof environment !== 'object') { - return undefined; - } - - const environmentConfig = environment as { - config?: { - build?: { - outDir?: string; - }; - }; - build?: { - outDir?: string; - }; - }; - - return ( - environmentConfig.config?.build?.outDir ?? environmentConfig.build?.outDir - ); -} - -function resolveBuiltClientOutputPath( - cachedPath: string, - workspaceRoot: string, - rootDir: string, - configuredOutDir: string | undefined, - environment?: unknown, -) { - const environmentOutDir = getEnvironmentBuildOutDir(environment); - if (environmentOutDir) { - const resolvedPath = normalizePath( - resolve(workspaceRoot, rootDir, environmentOutDir), - ); - debugNitro('resolveBuiltClientOutputPath using environment outDir', { - cachedPath, - workspaceRoot, - rootDir, - configuredOutDir, - environmentOutDir, - resolvedPath, - }); - return resolvedPath; - } - - debugNitro('resolveBuiltClientOutputPath falling back to shared resolver', { - cachedPath, - workspaceRoot, - rootDir, - configuredOutDir, - environmentOutDir, - }); - return resolveClientOutputPath( - cachedPath, - workspaceRoot, - rootDir, - configuredOutDir, - ); -} - -function getNitroPublicOutputDir(nitroConfig: NitroConfig): string { - const publicDir = nitroConfig.output?.publicDir; - if (!publicDir) { - throw new Error( - 'Nitro public output directory is required to build the sitemap.', - ); - } - - return publicDir; -} - -function readDirectoryEntries(path: string): string[] { - try { - return readdirSync(path).sort(); - } catch (error) { - return [ - `<>`, - ]; - } -} - -function getPathDebugInfo(path: string) { - return { - rawPath: path, - normalizedPath: normalizePath(path), - exists: existsSync(path), - entries: existsSync(path) ? readDirectoryEntries(path) : [], - }; -} - -function assetSourceToString(source: string | Uint8Array) { - return typeof source === 'string' - ? source - : Buffer.from(source).toString('utf8'); -} - -function captureClientIndexHtmlFromBundle( - bundle: Record< - string, - { - type?: string; - fileName?: string; - source?: string | Uint8Array; - } - >, - hook: 'generateBundle' | 'writeBundle', -) { - const indexHtmlAsset = Object.values(bundle).find( - (chunk) => - chunk.type === 'asset' && - chunk.fileName === 'index.html' && - typeof chunk.source !== 'undefined', - ); - - if (!indexHtmlAsset?.source) { - debugNitro(`client bundle did not expose index.html during ${hook}`, { - hook, - bundleKeys: Object.keys(bundle).sort(), - assetFileNames: Object.values(bundle) - .filter((chunk) => chunk.type === 'asset') - .map((chunk) => chunk.fileName) - .filter(Boolean), - }); - return undefined; - } - - const indexHtml = assetSourceToString(indexHtmlAsset.source); - debugNitro(`captured client bundle index.html asset during ${hook}`, { - hook, - fileName: indexHtmlAsset.fileName, - htmlLength: indexHtml.length, - }); - return indexHtml; -} - -// Nitro only needs the HTML template string. Prefer the on-disk file when it -// exists, but allow the captured client asset to cover build flows where the -// client output directory disappears before Nitro assembles its virtual modules. -function registerIndexHtmlVirtual( - nitroConfig: NitroConfig, - clientOutputPath: string, - inlineIndexHtml?: string, -) { - const indexHtmlPath = resolve(clientOutputPath, 'index.html'); - debugNitro('registerIndexHtmlVirtual inspecting client output', { - platform: process.platform, - cwd: process.cwd(), - clientOutputPath, - clientOutputPathInfo: getPathDebugInfo(clientOutputPath), - indexHtmlPath, - indexHtmlExists: existsSync(indexHtmlPath), - hasInlineIndexHtml: typeof inlineIndexHtml === 'string', - }); - if (!existsSync(indexHtmlPath) && typeof inlineIndexHtml !== 'string') { - debugNitro('registerIndexHtmlVirtual missing index.html', { - platform: process.platform, - cwd: process.cwd(), - clientOutputPath, - clientOutputPathInfo: getPathDebugInfo(clientOutputPath), - indexHtmlPath, - hasInlineIndexHtml: typeof inlineIndexHtml === 'string', - nitroOutput: nitroConfig.output, - nitroPublicAssets: nitroConfig.publicAssets, - }); - throw new Error( - `[analog] Client build output not found at ${indexHtmlPath}.\n` + - `Ensure the client environment build completed successfully before the server build.`, - ); - } - const indexHtml = - typeof inlineIndexHtml === 'string' - ? inlineIndexHtml - : readFileSync(indexHtmlPath, 'utf8'); - debugNitro('registerIndexHtmlVirtual using HTML template source', { - source: - typeof inlineIndexHtml === 'string' - ? 'captured client bundle asset' - : 'client output index.html file', - indexHtmlPath, - }); - nitroConfig.virtual = { - ...nitroConfig.virtual, - '#analog/index': `export default ${JSON.stringify(indexHtml)};`, - }; -} - -/** - * Converts the built SSR entry path into a specifier that Nitro's bundler - * can resolve, including all relative `./assets/*` chunk imports inside - * the entry. - * - * The returned path **must** be an absolute filesystem path with forward - * slashes (e.g. `D:/a/analog/dist/apps/blog-app/ssr/main.server.js`). - * This lets Rollup/Rolldown determine the entry's directory and resolve - * sibling chunk imports like `./assets/core-DTazUigR.js` correctly. - * - * ## Why not pathToFileURL() on Windows? - * - * Earlier versions converted the path to a `file:///D:/a/...` URL on - * Windows, which worked with Nitro v2 + Rollup. Nitro v3 switched its - * default bundler to Rolldown, and Rolldown does **not** extract the - * importer directory from `file://` URLs. This caused every relative - * import inside the SSR entry to fail during the prerender build: - * - * [RESOLVE_ERROR] Could not resolve './assets/core-DTazUigR.js' - * in ../../dist/apps/blog-app/ssr/main.server.js - * - * `normalizePath()` (from Vite) simply converts backslashes to forward - * slashes, which both Rollup and Rolldown handle correctly on all - * platforms. - */ -function toNitroSsrEntrypointSpecifier(ssrEntryPath: string) { - return normalizePath(ssrEntryPath); -} - -function applySsrEntryAlias( - nitroConfig: NitroConfig, - options: Options | undefined, - workspaceRoot: string, - rootDir: string, -): void { - const ssrOutDir = - options?.ssrBuildDir || resolve(workspaceRoot, 'dist', rootDir, 'ssr'); - if (options?.ssr || nitroConfig.prerender?.routes?.length) { - const ssrEntryPath = resolveBuiltSsrEntryPath(ssrOutDir); - const ssrEntry = toNitroSsrEntrypointSpecifier(ssrEntryPath); - nitroConfig.alias = { - ...nitroConfig.alias, - '#analog/ssr': ssrEntry, - }; - } -} - -function resolveBuiltSsrEntryPath(ssrOutDir: string) { - const candidatePaths = [ - resolve(ssrOutDir, 'main.server.mjs'), - resolve(ssrOutDir, 'main.server.js'), - resolve(ssrOutDir, 'main.server'), - ]; - - const ssrEntryPath = candidatePaths.find((candidatePath) => - existsSync(candidatePath), - ); - - if (!ssrEntryPath) { - throw new Error( - `Unable to locate the built SSR entry in "${ssrOutDir}". Expected one of: ${candidatePaths.join( - ', ', - )}`, - ); - } - - return ssrEntryPath; -} - -export function nitro(options?: Options, nitroOptions?: NitroConfig): Plugin[] { - const workspaceRoot = options?.workspaceRoot ?? process.cwd(); - const sourceRoot = options?.sourceRoot ?? 'src'; - let isTest = process.env['NODE_ENV'] === 'test' || !!process.env['VITEST']; - const baseURL = process.env['NITRO_APP_BASE_URL'] || ''; - const prefix = baseURL ? baseURL.substring(0, baseURL.length - 1) : ''; - const apiPrefix = `/${options?.apiPrefix || 'api'}`; - const useAPIMiddleware = - typeof options?.useAPIMiddleware !== 'undefined' - ? options?.useAPIMiddleware - : true; - const viteRolldownOutput = options?.vite?.build?.rolldownOptions?.output; - // Vite's native build typing allows `output` to be either a single object or - // an array. Analog only forwards `codeSplitting` into the client environment - // when there is a single output object to merge into. - const viteRolldownOutputConfig = - viteRolldownOutput && !Array.isArray(viteRolldownOutput) - ? viteRolldownOutput - : undefined; - const codeSplitting = viteRolldownOutputConfig?.codeSplitting; - - let isBuild = false; - let isServe = false; - let ssrBuild = false; - let config: UserConfig; - let nitroConfig: NitroConfig; - let environmentBuild = false; - let hasAPIDir = false; - let clientOutputPath = ''; - let clientIndexHtml: string | undefined; - let legacyClientSubBuild = false; - const rollupExternalEntries: string[] = []; - const sitemapRoutes: string[] = []; - const routeSitemaps: Record< - string, - PrerenderSitemapConfig | (() => PrerenderSitemapConfig) - > = {}; - const routeSourceFiles: Record = {}; - let rootDir = workspaceRoot; - - return [ - (options?.ssr - ? devServerPlugin({ - entryServer: options?.entryServer, - index: options?.index, - routeRules: nitroOptions?.routeRules, - i18n: options?.i18n, - }) - : false) as Plugin, - { - name: '@analogjs/vite-plugin-nitro', - async config(userConfig, { mode, command }) { - isServe = command === 'serve'; - isBuild = command === 'build'; - ssrBuild = userConfig.build?.ssr === true; - // Capture the incoming config at the `config()` boundary so every later - // Nitro phase reads the same settings the build started with. - // - // Guards against: Nitro capturing `userConfig`, then another - // hook rewrites `build.outDir` or replaces a plugin hook wrapper. If - // we keep the live object, `closeBundle()` and the SSR handoff can end - // up reading a different config than the one the client pass started - // with. - config = cloneUserConfig(userConfig); - isTest = isTest ? isTest : mode === 'test'; - rollupExternalEntries.length = 0; - clientIndexHtml = undefined; - sitemapRoutes.length = 0; - for (const key of Object.keys(routeSitemaps)) { - delete routeSitemaps[key]; - } - for (const key of Object.keys(routeSourceFiles)) { - delete routeSourceFiles[key]; - } - - const resolvedConfigRoot = config.root - ? resolve(workspaceRoot, config.root) - : workspaceRoot; - rootDir = relative(workspaceRoot, resolvedConfigRoot) || '.'; - hasAPIDir = existsSync( - resolve( - workspaceRoot, - rootDir, - `${sourceRoot}/server/routes/${options?.apiPrefix || 'api'}`, - ), - ); - const buildPreset = - process.env['BUILD_PRESET'] ?? - (nitroOptions?.preset as string | undefined) ?? - (process.env['VERCEL'] ? 'vercel' : undefined); - - const pageHandlers = getPageHandlers({ - workspaceRoot, - sourceRoot, - rootDir, - additionalPagesDirs: options?.additionalPagesDirs, - hasAPIDir, - }); - const resolvedClientOutputPath = resolveClientOutputPath( - clientOutputPath, - workspaceRoot, - rootDir, - config.build?.outDir, - ); - debugNitro('nitro config resolved client output path', { - platform: process.platform, - workspaceRoot, - configRoot: config.root, - resolvedConfigRoot, - rootDir, - buildOutDir: config.build?.outDir, - clientOutputPath, - resolvedClientOutputPath, - hasEnvironmentConfig: !!config.environments, - clientEnvironmentOutDir: - config.environments?.['client'] && - typeof config.environments['client'] === 'object' && - 'build' in config.environments['client'] - ? ( - config.environments['client'] as { - build?: { outDir?: string }; - } - ).build?.outDir - : undefined, - }); - - nitroConfig = { - rootDir: normalizePath(rootDir), - preset: buildPreset, - compatibilityDate: '2025-11-19', - logLevel: nitroOptions?.logLevel || 0, - serverDir: normalizePath(`${sourceRoot}/server`), - scanDirs: [ - normalizePath(`${rootDir}/${sourceRoot}/server`), - ...(options?.additionalAPIDirs || []).map((dir) => - normalizePath(`${workspaceRoot}${dir}`), - ), - ], - output: { - dir: normalizePath( - resolve(workspaceRoot, 'dist', rootDir, 'analog'), - ), - publicDir: normalizePath( - resolve(workspaceRoot, 'dist', rootDir, 'analog/public'), - ), - }, - buildDir: normalizePath( - resolve(workspaceRoot, 'dist', rootDir, '.nitro'), - ), - typescript: { - generateTsConfig: false, - }, - runtimeConfig: { - apiPrefix: apiPrefix.substring(1), - prefix, - }, - // Analog provides its own renderer handler; prevent Nitro v3 from - // auto-detecting index.html in rootDir and adding a conflicting one. - renderer: false, - imports: { - autoImport: false, - }, - hooks: { - 'rollup:before': createRollupBeforeHook(rollupExternalEntries), - }, - rollupConfig: { - onwarn(warning) { - if ( - warning.message.includes('empty chunk') && - warning.message.endsWith('.server') - ) { - return; - } - }, - plugins: [pageEndpointsPlugin()], - }, - handlers: [ - ...(hasAPIDir - ? [] - : useAPIMiddleware - ? [createNitroMiddlewareHandler('#ANALOG_API_MIDDLEWARE')] - : []), - ...pageHandlers, - ], - routeRules: hasAPIDir - ? undefined - : useAPIMiddleware - ? undefined - : { - [`${prefix}${apiPrefix}/**`]: { - proxy: { to: '/**' }, - }, - }, - virtual: { - '#ANALOG_SSR_RENDERER': ssrRenderer(), - '#ANALOG_CLIENT_RENDERER': clientRenderer(), - ...(hasAPIDir ? {} : { '#ANALOG_API_MIDDLEWARE': apiMiddleware }), - }, - }; - - if (isVercelPreset(buildPreset)) { - nitroConfig = withVercelOutputAPI(nitroConfig, workspaceRoot); - } - - if (isCloudflarePreset(buildPreset)) { - nitroConfig = withCloudflareOutput(nitroConfig); - } - - if ( - isNetlifyPreset(buildPreset) && - rootDir === '.' && - !existsSync(resolve(workspaceRoot, 'netlify.toml')) - ) { - nitroConfig = withNetlifyOutputAPI(nitroConfig, workspaceRoot); - } - - if (isFirebaseAppHosting()) { - nitroConfig = withAppHostingOutput(nitroConfig); - } - - if (!ssrBuild && !isTest) { - // store the client output path for the SSR build config - clientOutputPath = resolvedClientOutputPath; - debugNitro( - 'nitro config cached client output path for later SSR/Nitro build', - { - ssrBuild, - isTest, - clientOutputPath, - }, - ); - } - - // Start with a clean alias map. #analog/index is registered as a Nitro - // virtual module after the client build, inlining the HTML template so - // the server bundle imports it instead of using readFileSync with an - // absolute path. - nitroConfig.alias = {}; - - if (isBuild) { - nitroConfig.publicAssets = [ - { dir: normalizePath(resolvedClientOutputPath), maxAge: 0 }, - ]; - - // In Nitro v3, renderer.entry is resolved via resolveModulePath() - // during options normalization, which requires a real filesystem path. - // Virtual modules (prefixed with #) can't survive this resolution. - // Instead, we add the renderer as a catch-all handler directly — - // this is functionally equivalent to what Nitro does internally - // (it converts renderer.entry into a { route: '/**', lazy: true } - // handler), but avoids the filesystem resolution step. - const rendererHandler = options?.ssr - ? '#ANALOG_SSR_RENDERER' - : '#ANALOG_CLIENT_RENDERER'; - nitroConfig.handlers = [ - ...(nitroConfig.handlers || []), - { - handler: rendererHandler, - route: '/**', - lazy: true, - }, - ]; - - if (isEmptyPrerenderRoutes(options)) { - nitroConfig.prerender = {}; - nitroConfig.prerender.routes = ['/']; - } - - if (options?.prerender) { - nitroConfig.prerender = nitroConfig.prerender ?? {}; - nitroConfig.prerender.crawlLinks = options?.prerender?.discover; - - let routes: ( - | string - | PrerenderContentDir - | PrerenderRouteConfig - | undefined - )[] = []; - - const prerenderRoutes = options?.prerender?.routes; - const hasExplicitPrerenderRoutes = - typeof prerenderRoutes === 'function' || - Array.isArray(prerenderRoutes); - if ( - isArrayWithElements(prerenderRoutes) - ) { - routes = prerenderRoutes; - } else if (typeof prerenderRoutes === 'function') { - routes = await prerenderRoutes(); - } - - const resolvedPrerenderRoutes = routes.reduce( - (prev, current) => { - if (!current) { - return prev; - } - if (typeof current === 'string') { - prev.push(current); - sitemapRoutes.push(current); - return prev; - } - - if ('route' in current) { - if (current.sitemap) { - routeSitemaps[current.route] = current.sitemap; - } - - if (current.outputSourceFile) { - const sourcePath = resolve( - workspaceRoot, - rootDir, - current.outputSourceFile, - ); - routeSourceFiles[current.route] = readFileSync( - sourcePath, - 'utf8', - ); - } - - prev.push(current.route); - sitemapRoutes.push(current.route); - - // Add the server-side data fetching endpoint URL - if ('staticData' in current) { - prev.push(`${apiPrefix}/_analog/pages/${current.route}`); - } - - return prev; - } - - const affectedFiles: PrerenderContentFile[] = - getMatchingContentFilesWithFrontMatter( - workspaceRoot, - rootDir, - current.contentDir, - current.recursive, - ); - - affectedFiles.forEach((f) => { - const result = current.transform(f); - - if (result) { - if (current.sitemap) { - routeSitemaps[result] = - current.sitemap && typeof current.sitemap === 'function' - ? current.sitemap?.(f) - : current.sitemap; - } - - if (current.outputSourceFile) { - const sourceContent = current.outputSourceFile(f); - if (sourceContent) { - routeSourceFiles[result] = sourceContent; - } - } - - prev.push(result); - sitemapRoutes.push(result); - - // Add the server-side data fetching endpoint URL - if ('staticData' in current) { - prev.push(`${apiPrefix}/_analog/pages/${result}`); - } - } - }); - - return prev; - }, - [], - ); - - nitroConfig.prerender.routes = - hasExplicitPrerenderRoutes || resolvedPrerenderRoutes.length - ? resolvedPrerenderRoutes - : (nitroConfig.prerender.routes ?? []); - } - - // ── SSR / prerender Nitro config ───────────────────────────── - // - // This block configures Nitro for builds that rebundle the SSR - // entry (main.server.{js,mjs}). That happens in two cases: - // - // 1. Full SSR apps — `options.ssr === true` - // 2. Prerender-only — no runtime SSR, but the prerender build - // still imports the SSR entry to render static pages. - // - // The original gate was `if (ssrBuild)`, which checks the Vite - // top-level `build.ssr` flag. That works for SSR-only builds but - // misses two Vite 6+ paths: - // - // a. **Vite Environment API (Vite 6+)** — SSR config lives in - // `environments.ssr.build.ssr`, not `build.ssr`, so - // `ssrBuild` is always `false`. - // b. **Prerender-only apps** (e.g. blog-app) — `options.ssr` - // is `false`, but prerender routes exist and the prerender - // build still processes the SSR entry. - // - // Without this block: - // - `rxjs` is never externalised → RESOLVE_ERROR in the - // Nitro prerender build (especially on Windows CI). - // - `moduleSideEffects` for zone.js is never set → zone.js - // side-effects may be tree-shaken. - // - The handlers list is not reassembled with page endpoints - // + the renderer catch-all. - // - // The widened condition covers all supported build paths: - // - `ssrBuild` → SSR-only build - // - `options?.ssr` → Environment API SSR - // - `nitroConfig.prerender?.routes?.length` → prerender-only - if ( - ssrBuild || - options?.ssr || - nitroConfig.prerender?.routes?.length - ) { - nitroConfig.noExternals = appendNoExternals( - nitroConfig.noExternals, - 'es-toolkit', - ); - - if (process.platform === 'win32') { - nitroConfig.noExternals = appendNoExternals( - nitroConfig.noExternals, - 'std-env', - ); - } - - rollupExternalEntries.push( - 'rxjs', - 'node-fetch-native/dist/polyfill', - // sharp is a native module with platform-specific binaries - // (e.g. @img/sharp-darwin-arm64). pnpm creates symlinks for - // ALL optional platform deps but only installs the matching - // one — leaving broken symlinks that crash Nitro's bundler - // with ENOENT during realpath(). Externalizing sharp avoids - // bundling it entirely; it resolves from node_modules at - // runtime instead. - 'sharp', - ); - - nitroConfig = { - ...nitroConfig, - handlers: [ - ...(hasAPIDir - ? [] - : useAPIMiddleware - ? [createNitroMiddlewareHandler('#ANALOG_API_MIDDLEWARE')] - : []), - ...pageHandlers, - // Preserve the renderer catch-all handler added above - { - handler: rendererHandler, - route: '/**', - lazy: true, - }, - ], - }; - } - } - - nitroConfig = mergeConfig( - nitroConfig, - nitroOptions as Record, - ); - - // Only configure Vite 8 environments + builder on the top-level - // build invocation. When buildApp's builder.build() calls re-enter - // the config hook, returning environments/builder again would create - // recursive buildApp invocations — each nesting another client build - // that re-triggers config, producing an infinite loop of - // "building client environment... ✓ 1 modules transformed". - // - // environmentBuild — already inside a buildApp call (recursion guard) - // ssrBuild — legacy SSR-only sub-build - // isServe — dev server / Vitest test runner (command: 'serve') - if (environmentBuild || ssrBuild || isServe) { - return {}; - } - - return { - environments: { - client: { - build: { - outDir: - config?.build?.outDir || - resolve(workspaceRoot, 'dist', rootDir, 'client'), - emptyOutDir: true, - // Forward code-splitting config to Rolldown when running - // under Vite 8+. `false` disables splitting (inlines all - // dynamic imports); an object configures chunk groups. - // The `!== undefined` check ensures `codeSplitting: false` - // is forwarded correctly (a truthy check would swallow it). - ...(isRolldown() && codeSplitting !== undefined - ? { - rolldownOptions: { - output: { - // Preserve any sibling Rolldown output options while - // overriding just `codeSplitting` for the client build. - ...viteRolldownOutputConfig, - codeSplitting, - }, - }, - } - : {}), - }, - }, - ssr: { - build: { - ssr: true, - [getBundleOptionsKey()]: { - input: - options?.entryServer || - resolve( - workspaceRoot, - rootDir, - `${sourceRoot}/main.server.ts`, - ), - }, - outDir: - options?.ssrBuildDir || - resolve(workspaceRoot, 'dist', rootDir, 'ssr'), - // Preserve the client build output. The client environment is - // built first and Nitro reads its index.html after SSR finishes. - emptyOutDir: false, - }, - }, - }, - builder: { - /** - * Reuse the already resolved Analog/Vite plugin graph across the - * client and SSR environments. - * - * This keeps both builds behaviorally aligned: route generation, - * content discovery, Angular transforms, Tailwind integration, and - * other Analog-specific config stay consistent between the browser - * bundle and the server bundle. - * - * The tradeoff is that the SSR environment can observe repeated - * plugin entries when Vite materializes the environment-specific - * build. Most notably, `@analogjs/vite-plugin-angular` can appear - * twice in the SSR resolved plugin list even though the app only - * configured `analog(...)` once. - * - * That duplicated name during SSR is an artifact of the shared - * plugin graph, not evidence of a broken client build. The - * duplicate-registration check in `vite-plugin-angular` therefore - * throws only for non-SSR builds, where duplicate Angular plugin - * instances would actually split the component style registries. - */ - sharedPlugins: true, - buildApp: async (builder) => { - environmentBuild = true; - debugNitro('builder.buildApp starting', { - platform: process.platform, - workspaceRoot, - rootDir, - cachedClientOutputPath: clientOutputPath, - configuredBuildOutDir: config.build?.outDir, - clientEnvironmentOutDir: getEnvironmentBuildOutDir( - builder.environments['client'], - ), - ssrEnvironmentOutDir: getEnvironmentBuildOutDir( - builder.environments['ssr'], - ), - }); - - // Client must complete before SSR — the server build reads the - // client's index.html via registerIndexHtmlVirtual(). Running - // them in parallel caused a race on Windows where emptyOutDir - // could delete client output before the server read it. - await builder.build(builder.environments['client']); - const postClientBuildOutputPath = resolveBuiltClientOutputPath( - clientOutputPath, - workspaceRoot, - rootDir, - config.build?.outDir, - builder.environments['client'], - ); - // Capture the client template before any SSR/prerender work runs. - // On Windows, later phases can leave the client output directory - // unavailable even though the client build itself succeeded. - registerIndexHtmlVirtual( - nitroConfig, - postClientBuildOutputPath, - clientIndexHtml, - ); - debugNitro('builder.buildApp completed client build', { - postClientBuildOutputPath, - postClientBuildOutputInfo: getPathDebugInfo( - postClientBuildOutputPath, - ), - postClientBuildIndexHtmlPath: resolve( - postClientBuildOutputPath, - 'index.html', - ), - postClientBuildIndexHtmlExists: existsSync( - resolve(postClientBuildOutputPath, 'index.html'), - ), - }); - - if (options?.ssr || nitroConfig.prerender?.routes?.length) { - debugSsr('builder.buildApp starting SSR build', { - ssrEnabled: options?.ssr, - prerenderRoutes: nitroConfig.prerender?.routes, - }); - - /** - * This launches the SSR environment as a second build from the - * shared plugin graph above. When debugging an apparent - * duplicate `@analogjs/vite-plugin-angular` registration, this - * is the handoff to inspect: the SSR builder replays the shared - * plugins for the server pass and may therefore expose multiple - * Angular-plugin entries in the SSR resolved config. - * - * That is expected for this orchestration path and should not - * be treated the same as a duplicated client build, where two - * Angular plugin instances would maintain separate style maps. - */ - await builder.build(builder.environments['ssr']); - debugSsr('builder.buildApp completed SSR build', { - ssrOutputPath: - options?.ssrBuildDir || - resolve(workspaceRoot, 'dist', rootDir, 'ssr'), - }); - } - - applySsrEntryAlias(nitroConfig, options, workspaceRoot, rootDir); - - const resolvedClientOutputPath = resolveBuiltClientOutputPath( - clientOutputPath, - workspaceRoot, - rootDir, - config.build?.outDir, - builder.environments['client'], - ); - - nitroConfig.publicAssets = [ - { dir: normalizePath(resolvedClientOutputPath), maxAge: 0 }, - ]; - debugNitro( - 'builder.buildApp resolved final client output path before Nitro build', - { - resolvedClientOutputPath, - resolvedClientOutputInfo: getPathDebugInfo( - resolvedClientOutputPath, - ), - nitroPublicAssets: nitroConfig.publicAssets, - }, - ); - - await buildServer(options, nitroConfig, routeSourceFiles); - - if ( - nitroConfig.prerender?.routes?.length && - options?.prerender?.sitemap - ) { - console.log('Building Sitemap...'); - // sitemap needs to be built after all directories are built - await buildSitemap( - config, - options.prerender.sitemap, - sitemapRoutes.length - ? sitemapRoutes - : nitroConfig.prerender.routes, - getNitroPublicOutputDir(nitroConfig), - routeSitemaps, - { apiPrefix: options?.apiPrefix || 'api' }, - ); - } - - console.log( - `\n\nThe '@analogjs/platform' server has been successfully built.`, - ); - }, - }, - }; - }, - generateBundle( - _options, - bundle: Record< - string, - { - type?: string; - fileName?: string; - source?: string | Uint8Array; - } - >, - ) { - if (!isBuild || ssrBuild) { - return; - } - - clientIndexHtml = - captureClientIndexHtmlFromBundle(bundle, 'generateBundle') ?? - clientIndexHtml; - }, - writeBundle( - _options, - bundle: Record< - string, - { - type?: string; - fileName?: string; - source?: string | Uint8Array; - } - >, - ) { - if (!isBuild || ssrBuild) { - return; - } - - clientIndexHtml = - captureClientIndexHtmlFromBundle(bundle, 'writeBundle') ?? - clientIndexHtml; - }, - async configureServer(viteServer: ViteDevServer) { - if (isServe && !isTest) { - const nitro = await createNitro({ - dev: true, - // Nitro's Vite builder now rejects `build()` in dev mode, but Analog's - // dev integration still relies on the builder-driven reload hooks. - // Force the server worker onto Rollup for this dev-only path. - builder: 'rollup', - ...nitroConfig, - }); - const server = createDevServer(nitro); - await build(nitro); - const nitroSourceRoots = [ - normalizePath( - resolve(workspaceRoot, rootDir, `${sourceRoot}/server`), - ), - ...(options?.additionalAPIDirs || []).map((dir) => - normalizePath(`${workspaceRoot}${dir}`), - ), - ]; - const isNitroSourceFile = (path: string) => { - const normalizedPath = normalizePath(path); - return nitroSourceRoots.some( - (root) => - normalizedPath === root || - normalizedPath.startsWith(`${root}/`), - ); - }; - let nitroRebuildPromise: Promise | undefined; - let nitroRebuildPending = false; - const rebuildNitroServer = () => { - if (nitroRebuildPromise) { - // Coalesce rapid file events so a save that touches multiple server - // route files results in one follow-up rebuild instead of many. - nitroRebuildPending = true; - return nitroRebuildPromise; - } - - nitroRebuildPromise = (async () => { - do { - nitroRebuildPending = false; - // Nitro API routes are not part of Vite's normal client HMR graph, - // so rebuild the Nitro dev server to pick up handler edits. - await build(nitro); - } while (nitroRebuildPending); - - // Reload the page after the server rebuild completes so the next - // request observes the updated API route implementation. - viteServer.ws.send('analog:debug-full-reload', { - plugin: 'vite-plugin-nitro', - reason: 'nitro-server-rebuilt', - }); - viteServer.ws.send({ type: 'full-reload' }); - })() - .catch((error: unknown) => { - viteServer.config.logger.error( - `[analog] Failed to rebuild Nitro dev server.\n${error instanceof Error ? error.stack || error.message : String(error)}`, - ); - }) - .finally(() => { - nitroRebuildPromise = undefined; - }); - - return nitroRebuildPromise; - }; - const onNitroSourceChange = (path: string) => { - if (!isNitroSourceFile(path)) { - return; - } - - void rebuildNitroServer(); - }; - - // Watch the full Nitro source roots instead of only the API route - // directory. API handlers often read helper modules, shared data, or - // middleware from elsewhere under `src/server`, and those edits should - // still rebuild the Nitro dev server and refresh connected browsers. - viteServer.watcher.on('add', onNitroSourceChange); - viteServer.watcher.on('change', onNitroSourceChange); - viteServer.watcher.on('unlink', onNitroSourceChange); - - const apiHandler = async ( - req: IncomingMessage, - res: ServerResponse, - ) => { - // Nitro v3's dev server is fetch-first, so adapt Vite's Node - // request once and let Nitro respond with a standard Web Response. - const response = await server.fetch(toWebRequest(req)); - await writeWebResponseToNode(res, response); - }; - - if (hasAPIDir) { - viteServer.middlewares.use( - ( - req: IncomingMessage, - res: ServerResponse, - next: (error?: unknown) => void, - ) => { - if (req.url?.startsWith(`${prefix}${apiPrefix}`)) { - void apiHandler(req, res).catch((error) => next(error)); - return; - } - - next(); - }, - ); - } else { - viteServer.middlewares.use( - apiPrefix, - ( - req: IncomingMessage, - res: ServerResponse, - next: (error?: unknown) => void, - ) => { - void apiHandler(req, res).catch((error) => next(error)); - }, - ); - } - - viteServer.httpServer?.once('listening', () => { - process.env['ANALOG_HOST'] = !viteServer.config.server.host - ? 'localhost' - : (viteServer.config.server.host as string); - process.env['ANALOG_PORT'] = `${viteServer.config.server.port}`; - }); - - // handle upgrades if websockets are enabled - if (nitroOptions?.experimental?.websocket) { - debugNitro('experimental websocket upgrade handler enabled'); - viteServer.httpServer?.on('upgrade', server.upgrade); - } - - console.log( - `\n\nThe server endpoints are accessible under the "${prefix}${apiPrefix}" path.`, - ); - } - }, - - async closeBundle() { - if (legacyClientSubBuild) { - return; - } - - // When builder.buildApp ran, it already handled the full - // client → SSR → Nitro pipeline. Skip to avoid double work. - if (environmentBuild) { - return; - } - - // SSR sub-build — Vite re-enters the plugin with build.ssr; - // Nitro server assembly happens only after the client pass. - if (ssrBuild) { - return; - } - - // Nx executors (and any caller that runs `vite build` without - // the Environment API) never trigger builder.buildApp, so - // closeBundle is the only place to drive the SSR + Nitro build. - if (isBuild) { - const resolvedClientOutputPath = resolveClientOutputPath( - clientOutputPath, - workspaceRoot, - rootDir, - config.build?.outDir, - ); - debugNitro( - 'closeBundle resolved client output path before legacy SSR build', - { - platform: process.platform, - workspaceRoot, - rootDir, - cachedClientOutputPath: clientOutputPath, - configuredBuildOutDir: config.build?.outDir, - resolvedClientOutputPath, - resolvedClientOutputInfo: getPathDebugInfo( - resolvedClientOutputPath, - ), - }, - ); - const indexHtmlPath = resolve(resolvedClientOutputPath, 'index.html'); - if ( - !existsSync(indexHtmlPath) && - typeof clientIndexHtml !== 'string' - ) { - debugNitro( - 'closeBundle rebuilding missing client output before SSR/Nitro', - { - platform: process.platform, - workspaceRoot, - rootDir, - configuredBuildOutDir: config.build?.outDir, - resolvedClientOutputPath, - indexHtmlPath, - }, - ); - legacyClientSubBuild = true; - try { - await buildClientApp(config, options); - } finally { - legacyClientSubBuild = false; - } - } - // Capture the client HTML before kicking off the standalone SSR build. - // This mirrors the successful sequencing from before the closeBundle - // refactor and avoids depending on the client directory surviving the - // nested SSR build on Windows. - registerIndexHtmlVirtual( - nitroConfig, - resolvedClientOutputPath, - clientIndexHtml, - ); - - if (options?.ssr) { - console.log('Building SSR application...'); - await buildSSRApp(config, options); - debugSsr('closeBundle completed standalone SSR build', { - ssrBuildDir: - options?.ssrBuildDir || - resolve(workspaceRoot, 'dist', rootDir, 'ssr'), - clientOutputPathInfo: clientOutputPath - ? getPathDebugInfo(clientOutputPath) - : null, - }); - } - - applySsrEntryAlias(nitroConfig, options, workspaceRoot, rootDir); - debugNitro( - 'closeBundle resolved client output path before Nitro build', - { - platform: process.platform, - workspaceRoot, - rootDir, - cachedClientOutputPath: clientOutputPath, - configuredBuildOutDir: config.build?.outDir, - resolvedClientOutputPath, - resolvedClientOutputInfo: getPathDebugInfo( - resolvedClientOutputPath, - ), - }, - ); - registerIndexHtmlVirtual( - nitroConfig, - resolvedClientOutputPath, - clientIndexHtml, - ); - - await buildServer(options, nitroConfig, routeSourceFiles); - - if ( - nitroConfig.prerender?.routes?.length && - options?.prerender?.sitemap - ) { - console.log('Building Sitemap...'); - await buildSitemap( - config, - options.prerender.sitemap, - sitemapRoutes.length - ? sitemapRoutes - : nitroConfig.prerender.routes, - getNitroPublicOutputDir(nitroConfig), - routeSitemaps, - { apiPrefix: options?.apiPrefix || 'api' }, - ); - } - - console.log( - `\n\nThe '@analogjs/platform' server has been successfully built.`, - ); - } - }, - }, - { - name: '@analogjs/vite-plugin-nitro-api-prefix', - config() { - return { - define: { - ANALOG_API_PREFIX: `"${baseURL.substring(1)}${apiPrefix.substring(1)}"`, - ...(options?.i18n - ? { - ANALOG_I18N_DEFAULT_LOCALE: JSON.stringify( - options.i18n.defaultLocale, - ), - ANALOG_I18N_LOCALES: JSON.stringify(options.i18n.locales), - } - : {}), - }, - }; - }, - }, - ]; -} - -function isEmptyPrerenderRoutes(options?: Options): boolean { - if (!options || isArrayWithElements(options?.prerender?.routes)) { - return false; - } - return !options.prerender?.routes; -} - -function isArrayWithElements(arr: unknown): arr is [T, ...T[]] { - return !!(Array.isArray(arr) && arr.length); -} - -const VERCEL_PRESET = 'vercel'; -// Nitro v3 consolidates the old `vercel-edge` preset into `vercel` with -// fluid compute enabled by default, so a single preset covers both -// serverless and edge deployments. -const withVercelOutputAPI = ( - nitroConfig: NitroConfig | undefined, - workspaceRoot: string, -) => ({ - ...nitroConfig, - preset: nitroConfig?.preset ?? 'vercel', - vercel: { - ...nitroConfig?.vercel, - entryFormat: nitroConfig?.vercel?.entryFormat ?? 'node', - functions: { - runtime: nitroConfig?.vercel?.functions?.runtime ?? 'nodejs24.x', - ...nitroConfig?.vercel?.functions, - }, - }, - output: { - ...nitroConfig?.output, - dir: normalizePath(resolve(workspaceRoot, '.vercel', 'output')), - publicDir: normalizePath( - resolve(workspaceRoot, '.vercel', 'output/static'), - ), - }, -}); - -// Nitro v3 uses underscore-separated preset names (e.g. `cloudflare_pages`), -// but we accept both hyphen and underscore forms for backwards compatibility. -const isCloudflarePreset = (buildPreset: string | undefined) => - process.env['CF_PAGES'] || - (buildPreset && - (buildPreset.toLowerCase().includes('cloudflare-pages') || - buildPreset.toLowerCase().includes('cloudflare_pages'))); - -const withCloudflareOutput = (nitroConfig: NitroConfig | undefined) => ({ - ...nitroConfig, - output: { - ...nitroConfig?.output, - serverDir: '{{ output.publicDir }}/_worker.js', - }, -}); - -const isFirebaseAppHosting = () => !!process.env['NG_BUILD_LOGS_JSON']; -const withAppHostingOutput = (nitroConfig: NitroConfig) => { - let hasOutput = false; - - return { - ...nitroConfig, - serveStatic: true, - rollupConfig: { - ...nitroConfig.rollupConfig, - output: { - ...nitroConfig.rollupConfig?.output, - entryFileNames: 'server.mjs', - }, - }, - hooks: { - ...nitroConfig.hooks, - compiled: () => { - if (!hasOutput) { - const buildOutput = { - errors: [], - warnings: [], - outputPaths: { - root: pathToFileURL(`${nitroConfig.output?.dir}`), - browser: pathToFileURL(`${nitroConfig.output?.publicDir}`), - server: pathToFileURL(`${nitroConfig.output?.dir}/server`), - }, - }; - - // Log the build output for Firebase App Hosting to pick up - console.log(JSON.stringify(buildOutput, null, 2)); - hasOutput = true; - } - }, - }, - }; -}; - -const isNetlifyPreset = (buildPreset: string | undefined) => - process.env['NETLIFY'] || - (buildPreset && buildPreset.toLowerCase().includes('netlify')); - -const withNetlifyOutputAPI = ( - nitroConfig: NitroConfig | undefined, - workspaceRoot: string, -) => ({ - ...nitroConfig, - output: { - ...nitroConfig?.output, - dir: normalizePath(resolve(workspaceRoot, 'netlify/functions')), - }, -}); diff --git a/packages/vite-plugin-nitro/test-data/content/01-first.md b/packages/vite-plugin-nitro/test-data/content/01-first.md deleted file mode 100644 index ad9a3d21e..000000000 --- a/packages/vite-plugin-nitro/test-data/content/01-first.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: First -slug: first -description: My First Post ---- - -First Content File diff --git a/packages/vite-plugin-nitro/test-data/content/02-second.md b/packages/vite-plugin-nitro/test-data/content/02-second.md deleted file mode 100644 index 68492a999..000000000 --- a/packages/vite-plugin-nitro/test-data/content/02-second.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: Second -description: My Second Post ---- - -Second Content File diff --git a/packages/vite-plugin-nitro/test-data/content/03-third.md b/packages/vite-plugin-nitro/test-data/content/03-third.md deleted file mode 100644 index 427717985..000000000 --- a/packages/vite-plugin-nitro/test-data/content/03-third.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: Third (Draft) -description: My Third Post (Draft) -draft: true ---- - -Third Content File (Draft) diff --git a/packages/vite-plugin-nitro/vite.config.lib.ts b/packages/vite-plugin-nitro/vite.config.lib.ts index 3a0d746d9..42dab1f81 100644 --- a/packages/vite-plugin-nitro/vite.config.lib.ts +++ b/packages/vite-plugin-nitro/vite.config.lib.ts @@ -40,7 +40,6 @@ export default defineConfig({ lib: { entry: { 'src/index': resolve(pkgDir, 'src/index.ts'), - 'src/lib/utils/debug': resolve(pkgDir, 'src/lib/utils/debug.ts'), }, formats: ['es' as const], }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6854a17bc..bbc921f18 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -54,6 +54,9 @@ catalogs: '@astrojs/react': specifier: ^5.0.2 version: 5.0.3 + '@babel/core': + specifier: ^7.28.6 + version: 7.29.0 '@commitlint/cli': specifier: ^20.5.0 version: 20.5.0 @@ -1479,9 +1482,9 @@ importers: '@analogjs/vite-plugin-angular': specifier: workspace:* version: link:../vite-plugin-angular - '@analogjs/vite-plugin-nitro': - specifier: workspace:* - version: link:../vite-plugin-nitro + '@babel/core': + specifier: 'catalog:' + version: 7.29.0 '@nx/angular': specifier: catalog:peerCompat version: 22.6.5(41fc5ff660d836e7ccf859f85688b300) @@ -1518,6 +1521,12 @@ importers: obug: specifier: 'catalog:' version: 2.1.1 + ofetch: + specifier: 'catalog:' + version: 2.0.0-alpha.3 + oxc-parser: + specifier: 'catalog:' + version: 0.124.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) rolldown: specifier: 'catalog:' version: 1.0.0-rc.15 @@ -1533,6 +1542,9 @@ importers: vitefu: specifier: 'catalog:' version: 1.1.3(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) + xmlbuilder2: + specifier: 'catalog:' + version: 4.0.3 packages/router: dependencies: @@ -1596,10 +1608,10 @@ importers: dependencies: '@angular-devkit/build-angular': specifier: catalog:peerAngularBuilders - version: 21.2.7(0d8d439723faf789318b03d836c4657d) + version: 21.2.7(9d7c85fc87440b54c3f18e9dffd46aaa) '@angular/build': specifier: catalog:peerAngularBuilders - version: 21.2.7(6919e34d293f380cbe3efb02122eac30) + version: 21.2.7(d9d75657d4631d31c9fdc49d18abebe5) es-toolkit: specifier: 'catalog:' version: 1.45.1 @@ -1624,32 +1636,7 @@ importers: packages/vite-plugin-angular-tools: {} - packages/vite-plugin-nitro: - dependencies: - defu: - specifier: 'catalog:' - version: 6.1.7 - nitro: - specifier: 'catalog:' - version: 3.0.260415-beta(chokidar@5.0.0)(dotenv@16.4.7)(jiti@2.6.1)(lru-cache@11.3.5)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) - obug: - specifier: 'catalog:' - version: 2.1.1 - ofetch: - specifier: 'catalog:' - version: 2.0.0-alpha.3 - oxc-parser: - specifier: 'catalog:' - version: 0.124.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) - radix3: - specifier: 'catalog:' - version: 1.1.2 - sharp: - specifier: '>=0.32.0' - version: 0.34.5 - xmlbuilder2: - specifier: 'catalog:' - version: 4.0.3 + packages/vite-plugin-nitro: {} packages/vitest-angular: dependencies: @@ -16812,7 +16799,7 @@ snapshots: dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.2102.7(chokidar@5.0.0) - '@angular-devkit/build-webpack': 0.2102.7(chokidar@5.0.0)(webpack-dev-server@5.2.3(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) + '@angular-devkit/build-webpack': 0.2102.7(chokidar@5.0.0)(webpack-dev-server@5.2.3(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) '@angular-devkit/core': 21.2.7(chokidar@5.0.0) '@angular/build': 21.2.7(be30e769aef7c15f3ae11d69182ce8ae) '@angular/compiler-cli': 21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2) @@ -16826,35 +16813,35 @@ snapshots: '@babel/preset-env': 7.29.0(@babel/core@7.29.0) '@babel/runtime': 7.28.6 '@discoveryjs/json-ext': 0.6.3 - '@ngtools/webpack': 21.2.7(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2))(typescript@6.0.2)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) + '@ngtools/webpack': 21.2.7(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2))(typescript@6.0.2)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) ansi-colors: 4.1.3 autoprefixer: 10.4.27(postcss@8.5.6) - babel-loader: 10.0.0(@babel/core@7.29.0)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) + babel-loader: 10.0.0(@babel/core@7.29.0)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) browserslist: 4.28.2 - copy-webpack-plugin: 14.0.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) - css-loader: 7.1.3(@rspack/core@1.7.11(@swc/helpers@0.5.21))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) + copy-webpack-plugin: 14.0.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + css-loader: 7.1.3(@rspack/core@1.7.11(@swc/helpers@0.5.21))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) esbuild-wasm: 0.27.3 http-proxy-middleware: 3.0.5 istanbul-lib-instrument: 6.0.3 jsonc-parser: 3.3.1 karma-source-map-support: 1.4.0 less: 4.4.2 - less-loader: 12.3.1(@rspack/core@1.7.11(@swc/helpers@0.5.21))(less@4.4.2)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) - license-webpack-plugin: 4.0.2(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) + less-loader: 12.3.1(@rspack/core@1.7.11(@swc/helpers@0.5.21))(less@4.4.2)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + license-webpack-plugin: 4.0.2(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) loader-utils: 3.3.1 - mini-css-extract-plugin: 2.10.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) + mini-css-extract-plugin: 2.10.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) open: 11.0.0 ora: 9.3.0 picomatch: 4.0.4 piscina: 5.1.4 postcss: 8.5.6 - postcss-loader: 8.2.0(@rspack/core@1.7.11(@swc/helpers@0.5.21))(postcss@8.5.6)(typescript@6.0.2)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) + postcss-loader: 8.2.0(@rspack/core@1.7.11(@swc/helpers@0.5.21))(postcss@8.5.6)(typescript@6.0.2)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) resolve-url-loader: 5.0.0 rxjs: 7.8.2 sass: 1.97.3 - sass-loader: 16.0.7(@rspack/core@1.7.11(@swc/helpers@0.5.21))(sass-embedded@1.99.0)(sass@1.97.3)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) + sass-loader: 16.0.7(@rspack/core@1.7.11(@swc/helpers@0.5.21))(sass-embedded@1.99.0)(sass@1.97.3)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) semver: 7.7.4 - source-map-loader: 5.0.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) + source-map-loader: 5.0.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) source-map-support: 0.5.21 terser: 5.46.0 tinyglobby: 0.2.15 @@ -16862,8 +16849,8 @@ snapshots: tslib: 2.8.1 typescript: 6.0.2 webpack: 5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3) - webpack-dev-middleware: 7.4.5(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) - webpack-dev-server: 5.2.3(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) + webpack-dev-middleware: 7.4.5(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + webpack-dev-server: 5.2.3(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) webpack-merge: 6.0.1 webpack-subresource-integrity: 5.1.0(html-webpack-plugin@5.6.6(@rspack/core@1.7.11(@swc/helpers@0.5.21))(webpack@5.106.1(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) optionalDependencies: @@ -16904,7 +16891,7 @@ snapshots: dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.2102.7(chokidar@5.0.0) - '@angular-devkit/build-webpack': 0.2102.7(chokidar@5.0.0)(webpack-dev-server@5.2.3(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) + '@angular-devkit/build-webpack': 0.2102.7(chokidar@5.0.0)(webpack-dev-server@5.2.3(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) '@angular-devkit/core': 21.2.7(chokidar@5.0.0) '@angular/build': 21.2.7(be30e769aef7c15f3ae11d69182ce8ae) '@angular/compiler-cli': 21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2) @@ -16918,12 +16905,12 @@ snapshots: '@babel/preset-env': 7.29.0(@babel/core@7.29.0) '@babel/runtime': 7.28.6 '@discoveryjs/json-ext': 0.6.3 - '@ngtools/webpack': 21.2.7(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2))(typescript@6.0.2)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) + '@ngtools/webpack': 21.2.7(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2))(typescript@6.0.2)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) ansi-colors: 4.1.3 autoprefixer: 10.4.27(postcss@8.5.6) - babel-loader: 10.0.0(@babel/core@7.29.0)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) + babel-loader: 10.0.0(@babel/core@7.29.0)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) browserslist: 4.28.2 - copy-webpack-plugin: 14.0.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) + copy-webpack-plugin: 14.0.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) css-loader: 7.1.3(@rspack/core@1.6.8(@swc/helpers@0.5.21))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) esbuild-wasm: 0.27.3 http-proxy-middleware: 3.0.5 @@ -16932,9 +16919,9 @@ snapshots: karma-source-map-support: 1.4.0 less: 4.4.2 less-loader: 12.3.1(@rspack/core@1.6.8(@swc/helpers@0.5.21))(less@4.4.2)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) - license-webpack-plugin: 4.0.2(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) + license-webpack-plugin: 4.0.2(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) loader-utils: 3.3.1 - mini-css-extract-plugin: 2.10.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) + mini-css-extract-plugin: 2.10.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) open: 11.0.0 ora: 9.3.0 picomatch: 4.0.4 @@ -16946,7 +16933,7 @@ snapshots: sass: 1.97.3 sass-loader: 16.0.7(@rspack/core@1.6.8(@swc/helpers@0.5.21))(sass-embedded@1.99.0)(sass@1.97.3)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) semver: 7.7.4 - source-map-loader: 5.0.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) + source-map-loader: 5.0.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) source-map-support: 0.5.21 terser: 5.46.0 tinyglobby: 0.2.15 @@ -16954,8 +16941,8 @@ snapshots: tslib: 2.8.1 typescript: 6.0.2 webpack: 5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3) - webpack-dev-middleware: 7.4.5(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) - webpack-dev-server: 5.2.3(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) + webpack-dev-middleware: 7.4.5(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + webpack-dev-server: 5.2.3(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) webpack-merge: 6.0.1 webpack-subresource-integrity: 5.1.0(html-webpack-plugin@5.6.6(@rspack/core@1.6.8(@swc/helpers@0.5.21))(webpack@5.106.1(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) optionalDependencies: @@ -16993,12 +16980,104 @@ snapshots: - yaml optional: true - '@angular-devkit/build-webpack@0.2102.7(chokidar@5.0.0)(webpack-dev-server@5.2.3(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3))': + '@angular-devkit/build-angular@21.2.7(9d7c85fc87440b54c3f18e9dffd46aaa)': dependencies: + '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.2102.7(chokidar@5.0.0) + '@angular-devkit/build-webpack': 0.2102.7(chokidar@5.0.0)(webpack-dev-server@5.2.3(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + '@angular-devkit/core': 21.2.7(chokidar@5.0.0) + '@angular/build': 21.2.7(be30e769aef7c15f3ae11d69182ce8ae) + '@angular/compiler-cli': 21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2) + '@babel/core': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-split-export-declaration': 7.24.7 + '@babel/plugin-transform-async-generator-functions': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-transform-async-to-generator': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-runtime': 7.29.0(@babel/core@7.29.0) + '@babel/preset-env': 7.29.0(@babel/core@7.29.0) + '@babel/runtime': 7.28.6 + '@discoveryjs/json-ext': 0.6.3 + '@ngtools/webpack': 21.2.7(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2))(typescript@6.0.2)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + ansi-colors: 4.1.3 + autoprefixer: 10.4.27(postcss@8.5.6) + babel-loader: 10.0.0(@babel/core@7.29.0)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + browserslist: 4.28.2 + copy-webpack-plugin: 14.0.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + css-loader: 7.1.3(@rspack/core@1.7.11(@swc/helpers@0.5.21))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + esbuild-wasm: 0.27.3 + http-proxy-middleware: 3.0.5 + istanbul-lib-instrument: 6.0.3 + jsonc-parser: 3.3.1 + karma-source-map-support: 1.4.0 + less: 4.4.2 + less-loader: 12.3.1(@rspack/core@1.7.11(@swc/helpers@0.5.21))(less@4.4.2)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + license-webpack-plugin: 4.0.2(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + loader-utils: 3.3.1 + mini-css-extract-plugin: 2.10.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + open: 11.0.0 + ora: 9.3.0 + picomatch: 4.0.4 + piscina: 5.1.4 + postcss: 8.5.6 + postcss-loader: 8.2.0(@rspack/core@1.7.11(@swc/helpers@0.5.21))(postcss@8.5.6)(typescript@6.0.2)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + resolve-url-loader: 5.0.0 rxjs: 7.8.2 + sass: 1.97.3 + sass-loader: 16.0.7(@rspack/core@1.7.11(@swc/helpers@0.5.21))(sass-embedded@1.99.0)(sass@1.97.3)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + semver: 7.7.4 + source-map-loader: 5.0.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + source-map-support: 0.5.21 + terser: 5.46.0 + tinyglobby: 0.2.15 + tree-kill: 1.2.2 + tslib: 2.8.1 + typescript: 6.0.2 webpack: 5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3) - webpack-dev-server: 5.2.3(tslib@2.8.1)(webpack@5.106.1(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + webpack-dev-middleware: 7.4.5(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + webpack-dev-server: 5.2.3(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + webpack-merge: 6.0.1 + webpack-subresource-integrity: 5.1.0(html-webpack-plugin@5.6.6(@rspack/core@1.7.11(@swc/helpers@0.5.21))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + optionalDependencies: + '@angular/core': 21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1) + '@angular/localize': 21.2.8(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2))(@angular/compiler@21.2.8) + '@angular/platform-browser': 21.2.8(@angular/animations@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)) + '@angular/platform-server': 21.2.8(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/compiler@21.2.8)(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.8(@angular/animations@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2) + '@angular/ssr': 21.2.7(9c898ff27dabd87c2e39c7d23b6394cf) + esbuild: 0.27.3 + ng-packagr: 21.2.2(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2))(tailwindcss@4.2.2)(tslib@2.8.1)(typescript@6.0.2) + tailwindcss: 4.2.2 + transitivePeerDependencies: + - '@angular/compiler' + - '@emnapi/core' + - '@emnapi/runtime' + - '@rspack/core' + - '@swc/core' + - '@types/node' + - bufferutil + - chokidar + - debug + - html-webpack-plugin + - jiti + - lightningcss + - node-sass + - sass-embedded + - stylus + - sugarss + - supports-color + - tsx + - uglify-js + - utf-8-validate + - vitest + - webpack-cli + - yaml + + '@angular-devkit/build-webpack@0.2102.7(chokidar@5.0.0)(webpack-dev-server@5.2.3(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7))': + dependencies: + '@angular-devkit/architect': 0.2102.7(chokidar@5.0.0) + rxjs: 7.8.2 + webpack: 5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3) + webpack-dev-server: 5.2.3(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) transitivePeerDependencies: - chokidar @@ -17234,6 +17313,66 @@ snapshots: - tsx - yaml + '@angular/build@21.2.7(d9d75657d4631d31c9fdc49d18abebe5)': + dependencies: + '@ampproject/remapping': 2.3.0 + '@angular-devkit/architect': 0.2102.7(chokidar@5.0.0) + '@angular/compiler': 21.2.8 + '@angular/compiler-cli': 21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2) + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-split-export-declaration': 7.24.7 + '@inquirer/confirm': 5.1.21(@types/node@25.6.0) + '@vitejs/plugin-basic-ssl': 2.1.4(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.32.0)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3)) + beasties: 0.4.1 + browserslist: 4.28.2 + esbuild: 0.27.3 + https-proxy-agent: 7.0.6 + istanbul-lib-instrument: 6.0.3 + jsonc-parser: 3.3.1 + listr2: 9.0.5 + magic-string: 0.30.21 + mrmime: 2.0.1 + parse5-html-rewriting-stream: 8.0.0 + picomatch: 4.0.4 + piscina: 5.1.4 + rolldown: 1.0.0-rc.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + sass: 1.97.3 + semver: 7.7.4 + source-map-support: 0.5.21 + tinyglobby: 0.2.15 + tslib: 2.8.1 + typescript: 6.0.2 + undici: 7.24.4 + vite: 7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.32.0)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3) + watchpack: 2.5.1 + optionalDependencies: + '@angular/core': 21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1) + '@angular/localize': 21.2.8(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2))(@angular/compiler@21.2.8) + '@angular/platform-browser': 21.2.8(@angular/animations@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)) + '@angular/platform-server': 21.2.8(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/compiler@21.2.8)(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.8(@angular/animations@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2) + '@angular/ssr': 21.2.7(9c898ff27dabd87c2e39c7d23b6394cf) + less: 4.4.2 + lmdb: 3.5.1 + ng-packagr: 21.2.2(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2))(tailwindcss@4.2.2)(tslib@2.8.1)(typescript@6.0.2) + postcss: 8.5.6 + tailwindcss: 4.2.2 + vitest: 4.1.4(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(@vitest/ui@4.1.4)(happy-dom@20.8.9)(jsdom@29.0.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3)) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + - '@types/node' + - chokidar + - jiti + - lightningcss + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + '@angular/cdk@21.2.6(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.8(@angular/animations@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2)': dependencies: '@angular/common': 21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2) @@ -21748,7 +21887,7 @@ snapshots: '@netlify/types@2.6.0': {} - '@ngtools/webpack@21.2.7(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2))(typescript@6.0.2)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3))': + '@ngtools/webpack@21.2.7(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2))(typescript@6.0.2)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7))': dependencies: '@angular/compiler-cli': 21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2) typescript: 6.0.2 @@ -25143,6 +25282,10 @@ snapshots: dependencies: vite: 7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.32.0)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.3) + '@vitejs/plugin-basic-ssl@2.1.4(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.32.0)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3))': + dependencies: + vite: 7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.32.0)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3) + '@vitejs/plugin-basic-ssl@2.1.4(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3))': dependencies: vite: 7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3) @@ -25159,6 +25302,20 @@ snapshots: transitivePeerDependencies: - supports-color + '@vitest/browser-playwright@4.1.4(playwright@1.59.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.4)': + dependencies: + '@vitest/browser': 4.1.4(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.4) + '@vitest/mocker': 4.1.4(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3)) + playwright: 1.59.1 + tinyrainbow: 3.1.0 + vitest: 4.1.4(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(@vitest/ui@4.1.4)(happy-dom@20.8.9)(jsdom@29.0.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3)) + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + optional: true + '@vitest/browser-playwright@4.1.4(playwright@1.59.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.4)': dependencies: '@vitest/browser': 4.1.4(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.4) @@ -25186,6 +25343,24 @@ snapshots: - utf-8-validate - vite + '@vitest/browser@4.1.4(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.4)': + dependencies: + '@blazediff/core': 1.9.1 + '@vitest/mocker': 4.1.4(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3)) + '@vitest/utils': 4.1.4 + magic-string: 0.30.21 + pngjs: 7.0.0 + sirv: 3.0.2 + tinyrainbow: 3.1.0 + vitest: 4.1.4(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(@vitest/ui@4.1.4)(happy-dom@20.8.9)(jsdom@29.0.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3)) + ws: 8.20.0 + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + optional: true + '@vitest/browser@4.1.4(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.4)': dependencies: '@blazediff/core': 1.9.1 @@ -25254,6 +25429,15 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 + '@vitest/mocker@4.1.4(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3))': + dependencies: + '@vitest/spy': 4.1.4 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3) + optional: true + '@vitest/mocker@4.1.4(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3))': dependencies: '@vitest/spy': 4.1.4 @@ -25904,7 +26088,7 @@ snapshots: transitivePeerDependencies: - supports-color - babel-loader@10.0.0(@babel/core@7.29.0)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)): + babel-loader@10.0.0(@babel/core@7.29.0)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)): dependencies: '@babel/core': 7.29.0 find-up: 5.0.0 @@ -26693,7 +26877,7 @@ snapshots: serialize-javascript: 6.0.2 webpack: 5.106.1(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7) - copy-webpack-plugin@14.0.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)): + copy-webpack-plugin@14.0.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)): dependencies: glob-parent: 6.0.2 normalize-path: 3.0.0 @@ -26871,7 +27055,7 @@ snapshots: webpack: 5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3) optional: true - css-loader@7.1.3(@rspack/core@1.7.11(@swc/helpers@0.5.21))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)): + css-loader@7.1.3(@rspack/core@1.7.11(@swc/helpers@0.5.21))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)): dependencies: icss-utils: 5.1.0(postcss@8.5.9) postcss: 8.5.9 @@ -28834,6 +29018,18 @@ snapshots: webpack: 5.106.1(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7) optional: true + html-webpack-plugin@5.6.6(@rspack/core@1.7.11(@swc/helpers@0.5.21))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)): + dependencies: + '@types/html-minifier-terser': 6.1.0 + html-minifier-terser: 6.1.0 + lodash: 4.18.1 + pretty-error: 4.0.0 + tapable: 2.3.2 + optionalDependencies: + '@rspack/core': 1.7.11(@swc/helpers@0.5.21) + webpack: 5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3) + optional: true + html-webpack-plugin@5.6.6(@rspack/core@1.7.11(@swc/helpers@0.5.21))(webpack@5.106.1(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)): dependencies: '@types/html-minifier-terser': 6.1.0 @@ -29758,7 +29954,7 @@ snapshots: webpack: 5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3) optional: true - less-loader@12.3.1(@rspack/core@1.7.11(@swc/helpers@0.5.21))(less@4.4.2)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)): + less-loader@12.3.1(@rspack/core@1.7.11(@swc/helpers@0.5.21))(less@4.4.2)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)): dependencies: less: 4.4.2 optionalDependencies: @@ -29834,7 +30030,7 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 - license-webpack-plugin@4.0.2(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)): + license-webpack-plugin@4.0.2(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)): dependencies: webpack-sources: 3.3.4 optionalDependencies: @@ -30765,7 +30961,7 @@ snapshots: min-indent@1.0.1: {} - mini-css-extract-plugin@2.10.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)): + mini-css-extract-plugin@2.10.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)): dependencies: schema-utils: 4.3.3 tapable: 2.3.2 @@ -32190,7 +32386,7 @@ snapshots: - typescript optional: true - postcss-loader@8.2.0(@rspack/core@1.7.11(@swc/helpers@0.5.21))(postcss@8.5.6)(typescript@6.0.2)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)): + postcss-loader@8.2.0(@rspack/core@1.7.11(@swc/helpers@0.5.21))(postcss@8.5.6)(typescript@6.0.2)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)): dependencies: cosmiconfig: 9.0.1(typescript@6.0.2) jiti: 2.6.1 @@ -33481,7 +33677,7 @@ snapshots: sass-embedded: 1.99.0 webpack: 5.106.1(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7) - sass-loader@16.0.7(@rspack/core@1.7.11(@swc/helpers@0.5.21))(sass-embedded@1.99.0)(sass@1.97.3)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)): + sass-loader@16.0.7(@rspack/core@1.7.11(@swc/helpers@0.5.21))(sass-embedded@1.99.0)(sass@1.97.3)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)): dependencies: neo-async: 2.6.2 optionalDependencies: @@ -33938,7 +34134,7 @@ snapshots: source-map-js@1.2.1: {} - source-map-loader@5.0.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)): + source-map-loader@5.0.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)): dependencies: iconv-lite: 0.6.3 source-map-js: 1.2.1 @@ -34985,6 +35181,25 @@ snapshots: terser: 5.46.0 yaml: 2.8.3 + vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.32.0)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3): + dependencies: + esbuild: 0.27.7 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.9 + rollup: 4.60.1 + tinyglobby: 0.2.16 + optionalDependencies: + '@types/node': 25.6.0 + fsevents: 2.3.3 + jiti: 2.6.1 + less: 4.4.2 + lightningcss: 1.32.0 + sass: 1.97.3 + sass-embedded: 1.99.0 + terser: 5.46.1 + yaml: 2.8.3 + vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3): dependencies: esbuild: 0.27.7 @@ -35023,6 +35238,25 @@ snapshots: terser: 5.46.1 yaml: 2.8.3 + vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.9 + rolldown: 1.0.0-rc.15 + tinyglobby: 0.2.16 + optionalDependencies: + '@types/node': 25.6.0 + esbuild: 0.27.7 + fsevents: 2.3.3 + jiti: 2.6.1 + less: 4.4.2 + sass: 1.97.3 + sass-embedded: 1.99.0 + terser: 5.46.1 + yaml: 2.8.3 + optional: true + vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3): dependencies: lightningcss: 1.32.0 @@ -35067,6 +35301,39 @@ snapshots: optionalDependencies: vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3) + vitest@4.1.4(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(@vitest/ui@4.1.4)(happy-dom@20.8.9)(jsdom@29.0.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3)): + dependencies: + '@vitest/expect': 4.1.4 + '@vitest/mocker': 4.1.4(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3)) + '@vitest/pretty-format': 4.1.4 + '@vitest/runner': 4.1.4 + '@vitest/snapshot': 4.1.4 + '@vitest/spy': 4.1.4 + '@vitest/utils': 4.1.4 + es-module-lexer: 2.0.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.0.0 + tinybench: 2.9.0 + tinyexec: 1.1.1 + tinyglobby: 0.2.16 + tinyrainbow: 3.1.0 + vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 25.6.0 + '@vitest/browser-playwright': 4.1.4(playwright@1.59.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.4) + '@vitest/coverage-v8': 4.1.4(@vitest/browser@4.1.4)(vitest@4.1.4) + '@vitest/ui': 4.1.4(vitest@4.1.4) + happy-dom: 20.8.9 + jsdom: 29.0.2 + transitivePeerDependencies: + - msw + optional: true + vitest@4.1.4(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(@vitest/ui@4.1.4)(happy-dom@20.8.9)(jsdom@29.0.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3)): dependencies: '@vitest/expect': 4.1.4 @@ -35220,7 +35487,7 @@ snapshots: optionalDependencies: webpack: 5.106.1(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7) - webpack-dev-middleware@7.4.5(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)): + webpack-dev-middleware@7.4.5(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)): dependencies: colorette: 2.0.20 memfs: 4.57.1(tslib@2.8.1) @@ -35246,7 +35513,7 @@ snapshots: transitivePeerDependencies: - tslib - webpack-dev-server@5.2.3(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)): + webpack-dev-server@5.2.3(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)): dependencies: '@types/bonjour': 3.5.13 '@types/connect-history-api-fallback': 1.5.4 @@ -35274,7 +35541,7 @@ snapshots: serve-index: 1.9.2 sockjs: 0.3.24 spdy: 4.0.2 - webpack-dev-middleware: 7.4.5(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) + webpack-dev-middleware: 7.4.5(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) ws: 8.20.0 optionalDependencies: webpack: 5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3) @@ -35361,6 +35628,13 @@ snapshots: optionalDependencies: html-webpack-plugin: 5.6.6(@rspack/core@1.6.8(@swc/helpers@0.5.21))(webpack@5.106.1(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + webpack-subresource-integrity@5.1.0(html-webpack-plugin@5.6.6(@rspack/core@1.7.11(@swc/helpers@0.5.21))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)): + dependencies: + typed-assert: 1.0.9 + webpack: 5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3) + optionalDependencies: + html-webpack-plugin: 5.6.6(@rspack/core@1.7.11(@swc/helpers@0.5.21))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + webpack-subresource-integrity@5.1.0(html-webpack-plugin@5.6.6(@rspack/core@1.7.11(@swc/helpers@0.5.21))(webpack@5.106.1(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)): dependencies: typed-assert: 1.0.9 From 278ed5ff57e880d5716e411d18081c58d265f17f Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Sun, 24 May 2026 15:54:17 -0500 Subject: [PATCH 06/65] refactor(platform): move routeRules x-analog-no-ssr injection into analogNitroPlugin.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 analogjs/analog#2035 Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/lib/nitro/analog-nitro-plugin.ts | 20 +++++++++++++++++++ packages/platform/src/lib/platform-plugin.ts | 17 ++-------------- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/packages/platform/src/lib/nitro/analog-nitro-plugin.ts b/packages/platform/src/lib/nitro/analog-nitro-plugin.ts index 48de19fb1..f904fd74d 100644 --- a/packages/platform/src/lib/nitro/analog-nitro-plugin.ts +++ b/packages/platform/src/lib/nitro/analog-nitro-plugin.ts @@ -200,6 +200,8 @@ export function analogNitroPlugin(options: Options = {}): Plugin { } }); + injectAnalogRouteRuleHeaders(nitro); + await wirePrerender(nitro, options, context, apiPrefix); if (options.i18n) { @@ -217,6 +219,24 @@ export function analogNitroPlugin(options: Options = {}): Plugin { return plugin; } +/** + * Walks Nitro's resolved routeRules and stamps `x-analog-no-ssr: true` onto + * any rule with `ssr: false`. Analog's SSR service wrapper reads this header + * to short-circuit the renderer and return the raw template. + */ +function injectAnalogRouteRuleHeaders(nitro: Nitro): void { + const routeRules = nitro.options.routeRules as + | Record }> + | undefined; + if (!routeRules) return; + + for (const rule of Object.values(routeRules)) { + if (rule?.ssr === false) { + rule.headers = { ...rule.headers, 'x-analog-no-ssr': 'true' }; + } + } +} + /** * Builds the SSR service entry source. The wrapper imports the user's * `main.server.ts` Angular renderer and adapts it to the `{ fetch(req) }` diff --git a/packages/platform/src/lib/platform-plugin.ts b/packages/platform/src/lib/platform-plugin.ts index 21089cd17..70d952180 100644 --- a/packages/platform/src/lib/platform-plugin.ts +++ b/packages/platform/src/lib/platform-plugin.ts @@ -1,7 +1,7 @@ import { Plugin } from 'vite'; import { nitro } from 'nitro/vite'; import angular from '@analogjs/vite-plugin-angular'; -import { mapValues, union } from 'es-toolkit'; +import { union } from 'es-toolkit'; import { Options } from './options.js'; import { @@ -69,20 +69,7 @@ export function platformPlugin(opts: Options = {}): Plugin[] { typedRouter: platformOptions.experimental?.typedRouter, stylePipeline: !!platformOptions.experimental?.stylePipeline, }); - let nitroOptions = platformOptions?.nitro; - - if (nitroOptions?.routeRules) { - nitroOptions = { - ...nitroOptions, - routeRules: mapValues(nitroOptions.routeRules, (rule) => ({ - ...rule, - headers: { - ...rule.headers, - 'x-analog-no-ssr': rule?.ssr === false ? 'true' : undefined, - } as any, - })), - }; - } + const nitroOptions = platformOptions?.nitro; return [ { From 20e8e33d280e707fe6fc2619881745b56af76d8d Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Sun, 24 May 2026 15:55:05 -0500 Subject: [PATCH 07/65] feat(platform): export discoverLibraryRoutes and pageGlobs from main 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 analogjs/analog#2035 Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/platform/src/index.ts | 5 +++++ .../src/lib/discover-library-routes.spec.ts | 15 ++++++++++++++- .../platform/src/lib/discover-library-routes.ts | 12 ++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/packages/platform/src/index.ts b/packages/platform/src/index.ts index 5bcfebc28..b535d40a8 100644 --- a/packages/platform/src/index.ts +++ b/packages/platform/src/index.ts @@ -14,6 +14,11 @@ export type { SitemapRouteSource, SitemapTransform, } from './lib/options.js'; +export { + discoverLibraryRoutes, + pageGlobs, +} from './lib/discover-library-routes.js'; +export type { DiscoveredLibraryRoutes } from './lib/discover-library-routes.js'; export { routeGenerationPlugin } from './lib/route-generation-plugin.js'; export { tailwindPreprocessor } from './lib/tailwind-preprocessor.js'; export type { diff --git a/packages/platform/src/lib/discover-library-routes.spec.ts b/packages/platform/src/lib/discover-library-routes.spec.ts index 680608b7a..61488fa46 100644 --- a/packages/platform/src/lib/discover-library-routes.spec.ts +++ b/packages/platform/src/lib/discover-library-routes.spec.ts @@ -5,7 +5,7 @@ vi.mock('tinyglobby', () => ({ })); import { globSync } from 'tinyglobby'; -import { discoverLibraryRoutes } from './discover-library-routes.js'; +import { discoverLibraryRoutes, pageGlobs } from './discover-library-routes.js'; const mockGlobSync = vi.mocked(globSync); @@ -99,3 +99,16 @@ describe('discoverLibraryRoutes', () => { ]); }); }); + +describe('pageGlobs', () => { + it('maps directories to *.page.ts globs', () => { + expect(pageGlobs(['/libs/foo', '/libs/bar'])).toEqual([ + '/libs/foo/**/*.page.ts', + '/libs/bar/**/*.page.ts', + ]); + }); + + it('returns an empty array for no directories', () => { + expect(pageGlobs([])).toEqual([]); + }); +}); diff --git a/packages/platform/src/lib/discover-library-routes.ts b/packages/platform/src/lib/discover-library-routes.ts index 8e1b4ef17..e9c534314 100644 --- a/packages/platform/src/lib/discover-library-routes.ts +++ b/packages/platform/src/lib/discover-library-routes.ts @@ -109,3 +109,15 @@ export function discoverLibraryRoutes( return result; } + +/** + * Builds `${dir}/**\/*.page.ts` globs for each provided directory. + * + * Pass the result to `@analogjs/vite-plugin-angular`'s `include` option so + * workspace libraries discovered by `discoverLibraryRoutes` (or supplied + * via `analog({ additionalPagesDirs })`) participate in Angular + * compilation. + */ +export function pageGlobs(dirs: string[]): string[] { + return dirs.map((dir) => `${dir}/**/*.page.ts`); +} From a6d164367ec4ceacd2e8336c5cfacb290718a605 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Sun, 24 May 2026 15:59:04 -0500 Subject: [PATCH 08/65] feat(platform)!: drop angular() from analog()'s plugin chain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 analogjs/analog#2035 Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/platform/src/lib/deps-plugin.spec.ts | 27 +--- packages/platform/src/lib/deps-plugin.ts | 17 +- packages/platform/src/lib/options.ts | 145 +----------------- .../platform/src/lib/platform-plugin.spec.ts | 86 ----------- packages/platform/src/lib/platform-plugin.ts | 47 ------ .../platform/src/lib/style-pipeline.spec.ts | 2 - packages/platform/src/lib/style-pipeline.ts | 1 - 7 files changed, 14 insertions(+), 311 deletions(-) diff --git a/packages/platform/src/lib/deps-plugin.spec.ts b/packages/platform/src/lib/deps-plugin.spec.ts index e9b088343..07f92beea 100644 --- a/packages/platform/src/lib/deps-plugin.spec.ts +++ b/packages/platform/src/lib/deps-plugin.spec.ts @@ -11,35 +11,10 @@ describe('depsPlugin oxc config', () => { expect(result).not.toHaveProperty('esbuild'); }); - it('excludes ts/js files by default', () => { + it('excludes ts/js files so vite-plugin-angular owns Angular file compilation', () => { const plugins = depsPlugin(); const result = (plugins[0].config as any)(); expect(result.oxc).toEqual({ exclude: ['**/*.ts', '**/*.js'] }); }); - - it('uses empty oxc config when vite option is false', () => { - const plugins = depsPlugin({ vite: false } as any); - const result = (plugins[0].config as any)(); - - expect(result.oxc).toEqual({}); - }); - - it('uses empty config when useAngularCompilationAPI is enabled', () => { - const plugins = depsPlugin({ - vite: { experimental: { useAngularCompilationAPI: true } }, - } as any); - const result = (plugins[0].config as any)(); - - expect(result.oxc).toEqual({}); - }); - - it('uses empty config when top-level experimental useAngularCompilationAPI is enabled', () => { - const plugins = depsPlugin({ - experimental: { useAngularCompilationAPI: true }, - } as any); - const result = (plugins[0].config as any)(); - - expect(result.oxc).toEqual({}); - }); }); diff --git a/packages/platform/src/lib/deps-plugin.ts b/packages/platform/src/lib/deps-plugin.ts index 03db5e5e8..1c279b223 100644 --- a/packages/platform/src/lib/deps-plugin.ts +++ b/packages/platform/src/lib/deps-plugin.ts @@ -9,24 +9,19 @@ import { getJsTransformConfigKey } from './utils/rolldown.js'; export function depsPlugin(options?: Options): Plugin[] { const workspaceRoot = options?.workspaceRoot ?? process.env['NX_WORKSPACE_ROOT'] ?? process.cwd(); - const viteOptions = options?.vite === false ? undefined : options?.vite; return [ { name: 'analogjs-deps-plugin', config() { - const useAngularCompilationAPI = - options?.experimental?.useAngularCompilationAPI ?? - viteOptions?.experimental?.useAngularCompilationAPI; - - const transformConfig = - options?.vite === false || useAngularCompilationAPI - ? {} - : { exclude: ['**/*.ts', '**/*.js'] }; + // Skip Vite's built-in ts/js transform so `@analogjs/vite-plugin-angular` + // (when the user includes it) owns Angular file compilation. Users who + // run an alternative compiler or compile through Angular's own + // compilation API can override this in their own Vite config. + const transformConfig = { exclude: ['**/*.ts', '**/*.js'] }; debugPlatform('deps transform config', { - useAngularCompilationAPI: !!useAngularCompilationAPI, jsTransformKey: getJsTransformConfigKey(), - transformExcluded: 'exclude' in transformConfig, + transformExcluded: true, }); return { diff --git a/packages/platform/src/lib/options.ts b/packages/platform/src/lib/options.ts index 1beb34733..57b4a90c3 100644 --- a/packages/platform/src/lib/options.ts +++ b/packages/platform/src/lib/options.ts @@ -1,4 +1,3 @@ -import type { PluginOptions } from '@analogjs/vite-plugin-angular'; import type { NitroConfig, PrerenderRoute } from 'nitro/types'; import type { SitemapConfig, @@ -87,51 +86,24 @@ export interface Options { static?: boolean; prerender?: PrerenderOptions; entryServer?: string; - /** - * Pass configuration options to the internal `@analogjs/vite-plugin-angular` - * plugin. Set to `false` to disable the internal vite plugin (e.g. when - * using an alternative compiler like `@oxc-angular/vite`). - * - * `vite.build` uses Vite's native config shape and is forwarded to the - * internal Nitro/Vite build pipeline, while the remaining fields are passed - * to `@analogjs/vite-plugin-angular`. - * - * When `false`, the following top-level options are ignored because they - * are only forwarded to the internal Angular plugin: `jit`, - * `disableTypeChecking`, `liveReload`, `inlineStylesExtension`, - * `fileReplacements`, and `include`. - * - * Use this to configure the embedded Angular integration itself, not as the - * primary home for Analog-owned experimental features. - */ - vite?: PluginOptions | false; nitro?: NitroConfig; apiPrefix?: string; - jit?: boolean; index?: string; workspaceRoot?: string; content?: ContentPluginOptions; /** - * Extension applied for inline styles - */ - inlineStylesExtension?: string; - /** - * Enables Analog's Angular live-reload/HMR pipeline during development/watch mode. - * - * This is separate from Vite's `server.hmr` option, which configures the - * HMR client transport. - * - * Defaults to `true` for watch mode. - */ - liveReload?: boolean; - /** - * Enable debug logging for specific scopes. + * Enable debug logging for the `analog:platform:*` and `analog:nitro:*` + * scopes. * - * - `true` → enables all `analog:*` scopes (platform + angular + nitro) + * - `true` → enables all platform + nitro scopes * - `string[]` → enables listed namespaces * - `{ scopes?, mode? }` → object form with optional `mode: 'build' | 'dev'` * to restrict output to a specific Vite command (omit for both) * + * Angular scopes (`analog:angular:*`) are owned by + * `@analogjs/vite-plugin-angular` — pass `debug` to `angular()` directly + * to enable them. + * * Also responds to the `DEBUG` env var (Node.js) or `localStorage.debug` * (browser), using the `obug` convention. */ @@ -160,41 +132,18 @@ export interface Options { * @default false */ discoverRoutes?: boolean; - /** - * Additional files to include in compilation - */ - include?: string[]; /** * Toggles internal API middleware. * If disabled, a proxy request is used to route /api * requests to / in the production server build. */ useAPIMiddleware?: boolean; - /** - * Disable type checking diagnostics by the Angular compiler - */ - disableTypeChecking?: boolean; /** * Configuration for runtime i18n support. * When set, enables locale detection on SSR and provides * the LOCALE injection token. */ i18n?: I18nOptions; - /** - * Opt into the fast compile path. Skips Angular's template type-checking - * and routes compilation through an internal single-pass transform. - */ - fastCompile?: boolean; - /** - * Compilation output mode used when `fastCompile` is enabled. - * - `'full'` (default): Emit final Ivy definitions for application builds. - * - `'partial'`: Emit partial declarations for library publishing. - */ - fastCompileMode?: 'full' | 'partial'; - /** - * File replacements - */ - fileReplacements?: PluginOptions['fileReplacements']; /** * Experimental features. These APIs are subject to change. * @@ -204,19 +153,6 @@ export interface Options { * a single Analog-first authoring surface. */ experimental?: { - /** - * Use Angular's experimental compilation API. - * - * This is forwarded to `@analogjs/vite-plugin-angular`'s - * `experimental.useAngularCompilationAPI`. - * - * Also accepted at `vite.experimental.useAngularCompilationAPI` - * for backwards compatibility. - * - * Has no effect when `vite` is set to `false`. - */ - useAngularCompilationAPI?: boolean; - /** * Enable typed route table generation for type-safe navigation. * @@ -249,73 +185,6 @@ export interface Options { */ stylePipeline?: StylePipelineOptions | false; }; - - /** - * First-class Tailwind CSS v4 integration for Angular component styles. - * - * Angular's compiler processes component CSS through Vite's `preprocessCSS()`, - * which runs `@tailwindcss/vite` — but each component stylesheet is processed - * in isolation without access to the root Tailwind configuration (prefix, @theme, - * @custom-variant, @plugin definitions). This causes errors like: - * - * "Cannot apply utility class `sa:grid` because the `sa` variant does not exist" - * - * The `tailwindCss` option solves this by auto-injecting a `@reference` directive - * into every component CSS file that uses Tailwind utilities, pointing it to the - * root Tailwind stylesheet so `@tailwindcss/vite` can resolve the full configuration. - * - * @example Basic usage — reference a root Tailwind CSS file: - * ```ts - * import { resolve } from 'node:path'; - * - * angular({ - * tailwindCss: { - * rootStylesheet: resolve(__dirname, 'src/styles/tailwind.css'), - * }, - * }) - * ``` - * - * @example With prefix detection — only inject for files using specific prefixes: - * ```ts - * angular({ - * tailwindCss: { - * rootStylesheet: resolve(__dirname, 'src/styles/tailwind.css'), - * // Only inject @reference into files that use these prefixed classes - * prefixes: ['sa:', 'tw:'], - * }, - * }) - * ``` - * - * @example AnalogJS platform — passed through the `vite` option: - * ```ts - * analog({ - * vite: { - * tailwindCss: { - * rootStylesheet: resolve(__dirname, '../../../libs/meritos/tailwind.config.css'), - * }, - * }, - * }) - * ``` - */ - tailwindCss?: { - /** - * Absolute path to the root Tailwind CSS file that contains `@import "tailwindcss"`, - * `@theme`, `@custom-variant`, and `@plugin` definitions. - * - * A `@reference` directive pointing to this file will be auto-injected into - * component CSS files that use Tailwind utilities. - */ - rootStylesheet: string; - /** - * Optional list of class prefixes to detect (e.g. `['sa:', 'tw:']`). - * When provided, `@reference` is only injected into component CSS files that - * contain at least one of these prefixes. When omitted, `@reference` is injected - * into all component CSS files that contain `@apply` or `@` directives. - * - * @default undefined — inject into all component CSS files with `@apply` - */ - prefixes?: string[]; - }; } export interface TypedRouterOptions { diff --git a/packages/platform/src/lib/platform-plugin.spec.ts b/packages/platform/src/lib/platform-plugin.spec.ts index b51fa3159..5d892468b 100644 --- a/packages/platform/src/lib/platform-plugin.spec.ts +++ b/packages/platform/src/lib/platform-plugin.spec.ts @@ -3,7 +3,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; const { nitroFromViteSpy, analogNitroPluginSpy, - angularSpy, ssrBuildPluginSpy, injectHTMLPluginSpy, depsPluginSpy, @@ -19,7 +18,6 @@ const { } = vi.hoisted(() => ({ nitroFromViteSpy: vi.fn(() => []), analogNitroPluginSpy: vi.fn(() => ({ name: '@analogjs/nitro' })), - angularSpy: vi.fn(() => []), ssrBuildPluginSpy: vi.fn(() => []), injectHTMLPluginSpy: vi.fn(() => []), depsPluginSpy: vi.fn(() => []), @@ -44,10 +42,6 @@ vi.mock('nitro/vite', () => ({ vi.mock('./nitro/analog-nitro-plugin.js', () => ({ analogNitroPlugin: analogNitroPluginSpy, })); -vi.mock('@analogjs/vite-plugin-angular', () => ({ - angular: angularSpy, - default: angularSpy, -})); vi.mock('./ssr/ssr-build-plugin.js', () => ({ ssrBuildPlugin: ssrBuildPluginSpy, })); @@ -97,7 +91,6 @@ describe('platformPlugin', () => { vi.clearAllMocks(); nitroFromViteSpy.mockReturnValue([]); analogNitroPluginSpy.mockReturnValue({ name: '@analogjs/nitro' }); - angularSpy.mockReturnValue([]); ssrBuildPluginSpy.mockReturnValue([]); injectHTMLPluginSpy.mockReturnValue([]); depsPluginSpy.mockReturnValue([]); @@ -132,63 +125,6 @@ describe('platformPlugin', () => { expect(injectHTMLPluginSpy).not.toHaveBeenCalled(); }); - it('forwards experimental.useAngularCompilationAPI to the Angular vite plugin', () => { - platformPlugin({ - experimental: { - useAngularCompilationAPI: true, - }, - }); - - expect(angularSpy).toHaveBeenCalledWith( - expect.objectContaining({ - experimental: { - useAngularCompilationAPI: true, - }, - }), - ); - }); - - it('does not force semantic type checking onto the dev hot path by default', () => { - platformPlugin(); - - expect(angularSpy).toHaveBeenCalledWith( - expect.objectContaining({ - disableTypeChecking: undefined, - }), - ); - }); - - it('does not call the Angular vite plugin when vite is set to false', () => { - platformPlugin({ vite: false }); - - expect(angularSpy).not.toHaveBeenCalled(); - }); - - it('still includes non-Angular plugins when vite is set to false', () => { - const plugins = platformPlugin({ vite: false }); - - expect(nitroFromViteSpy).toHaveBeenCalled(); - expect(analogNitroPluginSpy).toHaveBeenCalledWith( - expect.objectContaining({ ssr: true }), - ); - expect(routeGenerationPluginSpy).toHaveBeenCalled(); - expect(serverModePluginSpy).toHaveBeenCalled(); - expect(clearClientPageEndpointsPluginSpy).toHaveBeenCalled(); - expect(plugins.length).toBeGreaterThan(0); - }); - - it('does not crash when vite is false and useAngularCompilationAPI is true', () => { - const plugins = platformPlugin({ - vite: false, - experimental: { - useAngularCompilationAPI: true, - }, - }); - - expect(angularSpy).not.toHaveBeenCalled(); - expect(plugins.length).toBeGreaterThan(0); - }); - it('merges discovered library routes when discoverRoutes is true', () => { discoverLibraryRoutesSpy.mockReturnValue({ additionalPagesDirs: ['/libs/shared/feature'], @@ -260,26 +196,4 @@ describe('platformPlugin', () => { ); expect(stylePipelineFactorySpy).not.toHaveBeenCalled(); }); - - it('forwards angular style-pipeline plugins to the Angular vite plugin', () => { - const angularStylePipelinePlugin = { - name: 'community-angular-style-pipeline', - }; - - platformPlugin({ - experimental: { - stylePipeline: { - angularPlugins: [angularStylePipelinePlugin], - }, - }, - }); - - expect(angularSpy).toHaveBeenCalledWith( - expect.objectContaining({ - stylePipeline: { - plugins: [angularStylePipelinePlugin], - }, - }), - ); - }); }); diff --git a/packages/platform/src/lib/platform-plugin.ts b/packages/platform/src/lib/platform-plugin.ts index 70d952180..ea78b8d51 100644 --- a/packages/platform/src/lib/platform-plugin.ts +++ b/packages/platform/src/lib/platform-plugin.ts @@ -1,6 +1,5 @@ import { Plugin } from 'vite'; import { nitro } from 'nitro/vite'; -import angular from '@analogjs/vite-plugin-angular'; import { union } from 'es-toolkit'; import { Options } from './options.js'; @@ -31,12 +30,6 @@ export function platformPlugin(opts: Options = {}): Plugin[] { applyDebugOption(opts.debug, opts.workspaceRoot); const isTest = process.env['NODE_ENV'] === 'test' || !!process.env['VITEST']; - const viteOptions = opts?.vite === false ? undefined : opts?.vite; - const { - experimental: viteExperimental, - hmr: _removedViteHmrOption, - ...forwardedViteOptions - } = viteOptions ?? {}; const { ...platformOptions } = { ssr: true, ...opts, @@ -61,11 +54,7 @@ export function platformPlugin(opts: Options = {}): Plugin[] { ); } - const useAngularCompilationAPI = - platformOptions.experimental?.useAngularCompilationAPI ?? - viteExperimental?.useAngularCompilationAPI; debugPlatform('experimental options resolved', { - useAngularCompilationAPI: !!useAngularCompilationAPI, typedRouter: platformOptions.experimental?.typedRouter, stylePipeline: !!platformOptions.experimental?.stylePipeline, }); @@ -94,42 +83,6 @@ export function platformPlugin(opts: Options = {}): Plugin[] { ...routerPlugin(platformOptions), routeGenerationPlugin(platformOptions), ...contentPlugin(platformOptions?.content, platformOptions), - ...(opts?.vite === false - ? [] - : externalPlugins( - angular({ - jit: platformOptions.jit, - workspaceRoot: platformOptions.workspaceRoot, - // Let the Angular plugin keep its own dev-friendly default unless the - // app explicitly opts into stricter serve-time diagnostics. - disableTypeChecking: platformOptions.disableTypeChecking, - include: [ - ...(platformOptions.include ?? []), - ...(platformOptions.additionalPagesDirs ?? []).map( - (pageDir) => `${pageDir}/**/*.page.ts`, - ), - ], - additionalContentDirs: platformOptions.additionalContentDirs, - liveReload: platformOptions.liveReload, - inlineStylesExtension: platformOptions.inlineStylesExtension, - fileReplacements: platformOptions.fileReplacements, - fastCompile: platformOptions.fastCompile, - fastCompileMode: platformOptions.fastCompileMode, - debug: platformOptions.debug, - stylePipeline: platformOptions.experimental?.stylePipeline - ?.angularPlugins?.length - ? { - plugins: - platformOptions.experimental.stylePipeline.angularPlugins, - } - : undefined, - ...forwardedViteOptions, - experimental: { - ...(viteExperimental ?? {}), - useAngularCompilationAPI, - }, - }), - )), ...(platformOptions.i18n ? [i18nComponentRegistryPlugin()] : []), ...serverModePlugin(), ...clearClientPageEndpointsPlugin(), diff --git a/packages/platform/src/lib/style-pipeline.spec.ts b/packages/platform/src/lib/style-pipeline.spec.ts index 2df6d5f39..9a85685c9 100644 --- a/packages/platform/src/lib/style-pipeline.spec.ts +++ b/packages/platform/src/lib/style-pipeline.spec.ts @@ -13,11 +13,9 @@ describe('style-pipeline', () => { expect( defineStylePipeline({ plugins: [plugin], - angularPlugins: [], }), ).toEqual({ plugins: [plugin], - angularPlugins: [], }); }); diff --git a/packages/platform/src/lib/style-pipeline.ts b/packages/platform/src/lib/style-pipeline.ts index 93ff133bd..2219c3788 100644 --- a/packages/platform/src/lib/style-pipeline.ts +++ b/packages/platform/src/lib/style-pipeline.ts @@ -64,7 +64,6 @@ export type StylePipelinePluginEntry = export interface StylePipelineOptions { plugins?: StylePipelinePluginEntry[]; - angularPlugins?: AngularStylePipelinePlugin[]; } export function defineStylePipeline( From 0dd20accd32e1c3dc233fcff93e045ebaeae10fe Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Sun, 24 May 2026 16:00:11 -0500 Subject: [PATCH 09/65] feat(platform)!: drop nitro() from analog()'s plugin chain 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 analogjs/analog#2035 Co-Authored-By: Claude Opus 4.7 (1M context) --- packages/platform/src/lib/options.ts | 3 +-- packages/platform/src/lib/platform-plugin.spec.ts | 7 ------- packages/platform/src/lib/platform-plugin.ts | 13 +------------ 3 files changed, 2 insertions(+), 21 deletions(-) diff --git a/packages/platform/src/lib/options.ts b/packages/platform/src/lib/options.ts index 57b4a90c3..322230126 100644 --- a/packages/platform/src/lib/options.ts +++ b/packages/platform/src/lib/options.ts @@ -1,4 +1,4 @@ -import type { NitroConfig, PrerenderRoute } from 'nitro/types'; +import type { PrerenderRoute } from 'nitro/types'; import type { SitemapConfig, SitemapEntry, @@ -86,7 +86,6 @@ export interface Options { static?: boolean; prerender?: PrerenderOptions; entryServer?: string; - nitro?: NitroConfig; apiPrefix?: string; index?: string; workspaceRoot?: string; diff --git a/packages/platform/src/lib/platform-plugin.spec.ts b/packages/platform/src/lib/platform-plugin.spec.ts index 5d892468b..079a0ba5c 100644 --- a/packages/platform/src/lib/platform-plugin.spec.ts +++ b/packages/platform/src/lib/platform-plugin.spec.ts @@ -1,7 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; const { - nitroFromViteSpy, analogNitroPluginSpy, ssrBuildPluginSpy, injectHTMLPluginSpy, @@ -16,7 +15,6 @@ const { stylePipelineFactorySpy, stylePipelinePluginSpy, } = vi.hoisted(() => ({ - nitroFromViteSpy: vi.fn(() => []), analogNitroPluginSpy: vi.fn(() => ({ name: '@analogjs/nitro' })), ssrBuildPluginSpy: vi.fn(() => []), injectHTMLPluginSpy: vi.fn(() => []), @@ -36,9 +34,6 @@ const { stylePipelinePluginSpy: { name: 'community-style-pipeline' }, })); -vi.mock('nitro/vite', () => ({ - nitro: nitroFromViteSpy, -})); vi.mock('./nitro/analog-nitro-plugin.js', () => ({ analogNitroPlugin: analogNitroPluginSpy, })); @@ -89,7 +84,6 @@ import { platformPlugin } from './platform-plugin.js'; describe('platformPlugin', () => { beforeEach(() => { vi.clearAllMocks(); - nitroFromViteSpy.mockReturnValue([]); analogNitroPluginSpy.mockReturnValue({ name: '@analogjs/nitro' }); ssrBuildPluginSpy.mockReturnValue([]); injectHTMLPluginSpy.mockReturnValue([]); @@ -107,7 +101,6 @@ describe('platformPlugin', () => { it('defaults ssr to true and passes that value to the composed plugins', () => { platformPlugin(); - expect(nitroFromViteSpy).toHaveBeenCalledWith({}); expect(analogNitroPluginSpy).toHaveBeenCalledWith( expect.objectContaining({ ssr: true }), ); diff --git a/packages/platform/src/lib/platform-plugin.ts b/packages/platform/src/lib/platform-plugin.ts index ea78b8d51..97c367f97 100644 --- a/packages/platform/src/lib/platform-plugin.ts +++ b/packages/platform/src/lib/platform-plugin.ts @@ -1,5 +1,4 @@ import { Plugin } from 'vite'; -import { nitro } from 'nitro/vite'; import { union } from 'es-toolkit'; import { Options } from './options.js'; @@ -21,11 +20,6 @@ import { resolveStylePipelinePlugins } from './style-pipeline.js'; import { i18nComponentRegistryPlugin } from './i18n-component-registry-plugin.js'; import { analogNitroPlugin } from './nitro/analog-nitro-plugin.js'; -// Bridge Plugin types from external @analogjs packages that resolve a different vite instance -function externalPlugins(plugins: unknown): Plugin[] { - return plugins as Plugin[]; -} - export function platformPlugin(opts: Options = {}): Plugin[] { applyDebugOption(opts.debug, opts.workspaceRoot); @@ -58,7 +52,6 @@ export function platformPlugin(opts: Options = {}): Plugin[] { typedRouter: platformOptions.experimental?.typedRouter, stylePipeline: !!platformOptions.experimental?.stylePipeline, }); - const nitroOptions = platformOptions?.nitro; return [ { @@ -67,11 +60,7 @@ export function platformPlugin(opts: Options = {}): Plugin[] { activateDeferredDebug(command); }, }, - ...externalPlugins(nitro(nitroOptions ?? {})), - analogNitroPlugin({ - ...platformOptions, - nitro: nitroOptions, - }), + analogNitroPlugin(platformOptions), ...(platformOptions.ssr ? [...ssrBuildPlugin(), ...injectHTMLPlugin()] : []), From 05e7fa0022d30c0463e5255577d8e511cb1b946e Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Sun, 24 May 2026 16:09:34 -0500 Subject: [PATCH 10/65] chore: adopt separated analog()/angular()/nitro() plugin shape in analog-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 analogjs/analog#2035 Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/analog-app/package.json | 3 ++- apps/analog-app/vite.config.ts | 48 ++++++++++++++++++++-------------- pnpm-lock.yaml | 3 +++ 3 files changed, 33 insertions(+), 21 deletions(-) diff --git a/apps/analog-app/package.json b/apps/analog-app/package.json index a1702bd87..a6b1397b6 100644 --- a/apps/analog-app/package.json +++ b/apps/analog-app/package.json @@ -13,6 +13,7 @@ "@analogjs/platform": "workspace:*", "@analogjs/storybook-angular": "workspace:*", "@analogjs/vite-plugin-angular": "workspace:*", - "@analogjs/vitest-angular": "workspace:*" + "@analogjs/vitest-angular": "workspace:*", + "nitro": "catalog:" } } diff --git a/apps/analog-app/vite.config.ts b/apps/analog-app/vite.config.ts index feb168cab..42fccb75f 100644 --- a/apps/analog-app/vite.config.ts +++ b/apps/analog-app/vite.config.ts @@ -1,6 +1,8 @@ /// -import analog from '@analogjs/platform'; +import analog, { discoverLibraryRoutes, pageGlobs } from '@analogjs/platform'; +import angular from '@analogjs/vite-plugin-angular'; +import { nitro } from 'nitro/vite'; import { resolve } from 'node:path'; import { defineConfig, PluginOption } from 'vite'; import { getWorkspaceDependencyExcludes } from '../../tools/vite/get-workspace-dependency-excludes.js'; @@ -37,6 +39,11 @@ export default defineConfig(async ({ mode, command }) => { }, ]; + const discoveredLibs = discoverLibraryRoutes(resolve(__dirname, '../..')); + const explicitLibPages = useBuiltWorkspaceLibs + ? [] + : ['/libs/my-package/src/**/*.ts', '/libs/top-bar/src/**/*.ts']; + return { root: __dirname, publicDir: 'src/public', @@ -58,11 +65,9 @@ export default defineConfig(async ({ mode, command }) => { content: { highlighter: 'prism', }, - include: useBuiltWorkspaceLibs - ? [] - : ['/libs/my-package/src/**/*.ts', '/libs/top-bar/src/**/*.ts'], - discoverRoutes: true, - fileReplacements, + additionalPagesDirs: discoveredLibs.additionalPagesDirs, + additionalContentDirs: discoveredLibs.additionalContentDirs, + additionalAPIDirs: discoveredLibs.additionalAPIDirs, prerender: { routes: [ '/', @@ -79,23 +84,26 @@ export default defineConfig(async ({ mode, command }) => { host: base, }, }, - inlineStylesExtension: 'scss', - fastCompile: true, experimental: { typedRouter: true, }, - nitro: { - routeRules: { - '/client': { - ssr: false, - }, - '/cart/**': { - ssr: false, - }, - '/404.html': { - ssr: false, - }, - }, + }), + angular({ + workspaceRoot: resolve(__dirname, '../..'), + include: [ + ...explicitLibPages, + ...pageGlobs(discoveredLibs.additionalPagesDirs), + ], + additionalContentDirs: discoveredLibs.additionalContentDirs, + inlineStylesExtension: 'scss', + fileReplacements, + fastCompile: true, + }), + nitro({ + routeRules: { + '/client': { ssr: false }, + '/cart/**': { ssr: false }, + '/404.html': { ssr: false }, }, }), { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bbc921f18..b89aaa621 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1155,6 +1155,9 @@ importers: '@analogjs/vitest-angular': specifier: workspace:* version: link:../../packages/vitest-angular + nitro: + specifier: 'catalog:' + version: 3.0.260415-beta(chokidar@5.0.0)(dotenv@16.4.7)(jiti@2.6.1)(lru-cache@11.3.5)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) apps/analog-app-e2e: {} From 4400ebe159322eee983ea9d716416603762473a1 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Sun, 24 May 2026 16:24:42 -0500 Subject: [PATCH 11/65] fix(platform): register SSR wrapper via environments.ssr.build.rollupOptions.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 analogjs/analog#2035 Co-Authored-By: Claude Opus 4.7 (1M context) --- .../platform/src/lib/nitro/analog-nitro-plugin.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/platform/src/lib/nitro/analog-nitro-plugin.ts b/packages/platform/src/lib/nitro/analog-nitro-plugin.ts index f904fd74d..736c778ea 100644 --- a/packages/platform/src/lib/nitro/analog-nitro-plugin.ts +++ b/packages/platform/src/lib/nitro/analog-nitro-plugin.ts @@ -123,6 +123,13 @@ export function analogNitroPlugin(options: Options = {}): Plugin { const overrides: UserConfig = {}; if (ssr) { + // Two-pronged registration: `experimental.vite.services.ssr.entry` + // is the documented hook, but nitro/vite's setupNitroContext also + // accepts an `environments.ssr.build.rollupOptions.input` entry + // (see node_modules/nitro/dist/vite.mjs:710-734). When `analog()` + // and `nitro()` are invoked separately, the `services` slot on + // `nitro()`'s pluginConfig is empty, so the rollupOptions.input + // path is how we get our wrapper entry recognized. overrides.experimental = { vite: { services: { @@ -132,6 +139,11 @@ export function analogNitroPlugin(options: Options = {}): Plugin { }; overrides.environments = { ssr: { + build: { + rollupOptions: { + input: { index: ssrEntryMarkerPath }, + }, + }, optimizeDeps: { include: ANGULAR_SSR_DEPS, rolldownOptions: { From 8a2f6ccff2f357dfce6aaebf262626875e24214f Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Sun, 24 May 2026 16:25:04 -0500 Subject: [PATCH 12/65] chore: switch analog-app build target to vite build CLI for buildApp orchestration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @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 analogjs/analog#2035 Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/analog-app/project.json | 17 +++++------------ apps/analog-app/vite.config.ts | 2 -- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/apps/analog-app/project.json b/apps/analog-app/project.json index 5217ef58e..34a7bcba3 100644 --- a/apps/analog-app/project.json +++ b/apps/analog-app/project.json @@ -7,31 +7,24 @@ "tags": [], "targets": { "build": { - "executor": "@nx/vite:build", + "executor": "nx:run-commands", "dependsOn": [ "platform:build", "router:build", "my-package:build", "top-bar:build" ], - "outputs": [ - "{options.outputPath}", - "{workspaceRoot}/dist/apps/analog-app/.nitro", - "{workspaceRoot}/dist/apps/analog-app/ssr", - "{workspaceRoot}/dist/apps/analog-app/analog" - ], + "outputs": ["{workspaceRoot}/apps/analog-app/.output"], "options": { - "configFile": "apps/analog-app/vite.config.ts", - "outputPath": "dist/apps/analog-app/client" + "command": "vite build -c apps/analog-app/vite.config.ts" }, "defaultConfiguration": "production", "configurations": { "development": { - "mode": "development" + "command": "vite build -c apps/analog-app/vite.config.ts --mode development" }, "production": { - "sourcemap": false, - "mode": "production" + "command": "vite build -c apps/analog-app/vite.config.ts --mode production" } } }, diff --git a/apps/analog-app/vite.config.ts b/apps/analog-app/vite.config.ts index 42fccb75f..8b542faf6 100644 --- a/apps/analog-app/vite.config.ts +++ b/apps/analog-app/vite.config.ts @@ -48,8 +48,6 @@ export default defineConfig(async ({ mode, command }) => { root: __dirname, publicDir: 'src/public', build: { - outDir: '../../dist/apps/analog-app/client', - emptyOutDir: true, reportCompressedSize: true, target: ['es2020'], }, From d0c06163cdc1152398986ef6109b3f375218d357 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Sun, 24 May 2026 16:48:17 -0500 Subject: [PATCH 13/65] fix: allow workspace fs reads so nitro/vite's env-runner can load dev-entry.mjs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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@/.../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 analogjs/analog#2035 Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/analog-app/vite.config.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/apps/analog-app/vite.config.ts b/apps/analog-app/vite.config.ts index 8b542faf6..6d395b0be 100644 --- a/apps/analog-app/vite.config.ts +++ b/apps/analog-app/vite.config.ts @@ -51,6 +51,15 @@ export default defineConfig(async ({ mode, command }) => { reportCompressedSize: true, target: ['es2020'], }, + server: { + fs: { + // Allow Vite's dev fs fallback to read pnpm content-hash paths under + // the workspace root (e.g. node_modules/.pnpm/nitro@.../...). Without + // this, nitro/vite's env-runner cannot resolve its own dev-entry.mjs + // through Vite's ModuleRunner. + allow: [resolve(__dirname, '../..')], + }, + }, optimizeDeps: { include: ['@angular/forms'], // Keep workspace Angular libraries on the source-transform path so Analog From 4b7761cf6e6bfb9927b52e988fb450bf65982b74 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Sun, 24 May 2026 16:49:31 -0500 Subject: [PATCH 14/65] fix: patch srvx@0.11.15 to await double-nested promises in toNodeHandler 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> 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 analogjs/analog#2035 Co-Authored-By: Claude Opus 4.7 (1M context) --- patches/srvx@0.11.15.patch | 22 ++++++++++++++++++++++ pnpm-lock.yaml | 27 ++++++++++++++++----------- pnpm-workspace.yaml | 8 ++++++++ 3 files changed, 46 insertions(+), 11 deletions(-) create mode 100644 patches/srvx@0.11.15.patch diff --git a/patches/srvx@0.11.15.patch b/patches/srvx@0.11.15.patch new file mode 100644 index 000000000..92ce31104 --- /dev/null +++ b/patches/srvx@0.11.15.patch @@ -0,0 +1,22 @@ +diff --git a/dist/adapters/node.mjs b/dist/adapters/node.mjs +index bf860db..845af0b 100644 +--- a/dist/adapters/node.mjs ++++ b/dist/adapters/node.mjs +@@ -717,7 +717,7 @@ function toNodeHandler(handler) { + req: nodeReq, + res: nodeRes + })); +- return res instanceof Promise ? res.then((resolvedRes) => sendNodeResponse(nodeRes, resolvedRes)) : sendNodeResponse(nodeRes, res); ++ return (res && typeof res.then === 'function') ? res.then((resolvedRes) => (resolvedRes && typeof resolvedRes.then === 'function') ? resolvedRes.then((r) => sendNodeResponse(nodeRes, r)) : sendNodeResponse(nodeRes, resolvedRes)) : sendNodeResponse(nodeRes, res); + } + convertedNodeHandler.__fetchHandler = handler; + assignFnName(convertedNodeHandler, handler, " (converted to Node handler)"); +@@ -772,7 +772,7 @@ var NodeServer = class { + }); + request.waitUntil = this.#wait?.waitUntil; + const res = fetchHandler(request); +- return res instanceof Promise ? res.then((resolvedRes) => sendNodeResponse(nodeRes, resolvedRes)) : sendNodeResponse(nodeRes, res); ++ return (res && typeof res.then === 'function') ? res.then((resolvedRes) => (resolvedRes && typeof resolvedRes.then === 'function') ? resolvedRes.then((r) => sendNodeResponse(nodeRes, r)) : sendNodeResponse(nodeRes, resolvedRes)) : sendNodeResponse(nodeRes, res); + }; + this.node = { + handler, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b89aaa621..f6ff8e2ab 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -597,6 +597,11 @@ overrides: '@angular/compiler-cli': 21.2.8 '@angular/language-service': 21.2.8 +patchedDependencies: + srvx@0.11.15: + hash: 12566e1018bec67b6844444c1857232a989b8470cc6b12872dc3ce2900863591 + path: patches/srvx@0.11.15.patch + importers: .: @@ -26983,9 +26988,9 @@ snapshots: dependencies: uncrypto: 0.1.3 - crossws@0.4.5(srvx@0.11.15): + crossws@0.4.5(srvx@0.11.15(patch_hash=12566e1018bec67b6844444c1857232a989b8470cc6b12872dc3ce2900863591)): optionalDependencies: - srvx: 0.11.15 + srvx: 0.11.15(patch_hash=12566e1018bec67b6844444c1857232a989b8470cc6b12872dc3ce2900863591) crypto-random-string@4.0.0: dependencies: @@ -27763,10 +27768,10 @@ snapshots: env-runner@0.1.7: dependencies: - crossws: 0.4.5(srvx@0.11.15) + crossws: 0.4.5(srvx@0.11.15(patch_hash=12566e1018bec67b6844444c1857232a989b8470cc6b12872dc3ce2900863591)) exsolve: 1.0.8 httpxy: 0.5.0 - srvx: 0.11.15 + srvx: 0.11.15(patch_hash=12566e1018bec67b6844444c1857232a989b8470cc6b12872dc3ce2900863591) environment@1.1.0: {} @@ -28739,12 +28744,12 @@ snapshots: ufo: 1.6.3 uncrypto: 0.1.3 - h3@2.0.1-rc.20(crossws@0.4.5(srvx@0.11.15)): + h3@2.0.1-rc.20(crossws@0.4.5(srvx@0.11.15(patch_hash=12566e1018bec67b6844444c1857232a989b8470cc6b12872dc3ce2900863591))): dependencies: rou3: 0.8.1 - srvx: 0.11.15 + srvx: 0.11.15(patch_hash=12566e1018bec67b6844444c1857232a989b8470cc6b12872dc3ce2900863591) optionalDependencies: - crossws: 0.4.5(srvx@0.11.15) + crossws: 0.4.5(srvx@0.11.15(patch_hash=12566e1018bec67b6844444c1857232a989b8470cc6b12872dc3ce2900863591)) hachure-fill@0.5.2: {} @@ -31156,17 +31161,17 @@ snapshots: nitro@3.0.260415-beta(chokidar@5.0.0)(dotenv@16.4.7)(jiti@2.6.1)(lru-cache@11.3.5)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)): dependencies: consola: 3.4.2 - crossws: 0.4.5(srvx@0.11.15) + crossws: 0.4.5(srvx@0.11.15(patch_hash=12566e1018bec67b6844444c1857232a989b8470cc6b12872dc3ce2900863591)) db0: 0.3.4 env-runner: 0.1.7 - h3: 2.0.1-rc.20(crossws@0.4.5(srvx@0.11.15)) + h3: 2.0.1-rc.20(crossws@0.4.5(srvx@0.11.15(patch_hash=12566e1018bec67b6844444c1857232a989b8470cc6b12872dc3ce2900863591))) hookable: 6.1.1 nf3: 0.3.16 ocache: 0.1.4 ofetch: 2.0.0-alpha.3 ohash: 2.0.11 rolldown: 1.0.0-rc.15 - srvx: 0.11.15 + srvx: 0.11.15(patch_hash=12566e1018bec67b6844444c1857232a989b8470cc6b12872dc3ce2900863591) unenv: 2.0.0-rc.24 unstorage: 2.0.0-alpha.7(chokidar@5.0.0)(db0@0.3.4)(lru-cache@11.3.5)(ofetch@2.0.0-alpha.3) optionalDependencies: @@ -34224,7 +34229,7 @@ snapshots: srcset@4.0.0: {} - srvx@0.11.15: {} + srvx@0.11.15(patch_hash=12566e1018bec67b6844444c1857232a989b8470cc6b12872dc3ce2900863591): {} ssri@13.0.1: dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index a4593bf65..b678269aa 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -18,6 +18,14 @@ packages: # Integration / E2E test projects that need their own workspace deps. - 'tests/*' +patchedDependencies: + # srvx's toNodeHandler awaits only one level of promise; nitro/h3's fetch + # chain returns Promise>, so without this patch the + # outer promise leaks through as the "response" and srvx's + # sendNodeResponse throws `webRes.headers is not iterable`. Backport of + # the fix from analogjs/analog#2188. + srvx@0.11.15: patches/srvx@0.11.15.patch + catalog: # Keep the Angular runtime/compiler patch set aligned so compiler-cli, # language-service, and the framework runtime all resolve the same release. From 7b938f70588ab3f385b325be3e3755d95b00356f Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Sun, 24 May 2026 17:17:56 -0500 Subject: [PATCH 15/65] fix(platform): install analog-owned SSR renderer to bypass Nitro auto-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 analogjs/analog#2035 Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/lib/nitro/analog-nitro-plugin.ts | 48 ++++++++++++++++++- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/packages/platform/src/lib/nitro/analog-nitro-plugin.ts b/packages/platform/src/lib/nitro/analog-nitro-plugin.ts index 736c778ea..58f2a58d9 100644 --- a/packages/platform/src/lib/nitro/analog-nitro-plugin.ts +++ b/packages/platform/src/lib/nitro/analog-nitro-plugin.ts @@ -212,6 +212,21 @@ export function analogNitroPlugin(options: Options = {}): Plugin { } }); + // Override Nitro's auto-detected template-serving renderer with one + // that routes HTML requests to our SSR service. Nitro's + // `resolveRendererOptions` finds `index.html` at the project root + // and installs `internal/routes/renderer-template[.dev]`, which + // just serves the raw template. nitro/vite's own SSR-routing + // renderer only auto-installs when both `renderer.handler` and + // `renderer.template` are empty (vite.mjs:574), which never holds + // for a typical app — so we install our own renderer virtual + // explicitly here. + nitro.options.virtual['#analog/ssr-renderer'] = + generateSsrRendererVirtual(readIndexHtml()); + nitro.options.renderer ??= {}; + nitro.options.renderer.handler = '#analog/ssr-renderer'; + delete nitro.options.renderer.template; + injectAnalogRouteRuleHeaders(nitro); await wirePrerender(nitro, options, context, apiPrefix); @@ -231,10 +246,39 @@ export function analogNitroPlugin(options: Options = {}): Plugin { return plugin; } +/** + * Builds the h3 handler installed as Nitro's `renderer.handler`. Short-circuits + * `ssr: false` routes to the raw client template; otherwise dispatches the + * request to the SSR service env (`fetchViteEnv("ssr", req)` works in dev via + * the env-runner and in prod via the `__nitro_vite_envs__` global set up by + * nitro/vite's `prodSetup`). + */ +function generateSsrRendererVirtual(template: string): string { + return ` +import { defineHandler } from 'nitro/h3'; +import { fetchViteEnv } from 'nitro/vite/runtime'; + +const TEMPLATE = ${JSON.stringify(template)}; + +export default defineHandler(async (event) => { + event.res.headers.set('content-type', 'text/html; charset=utf-8'); + // 'x-analog-no-ssr' is stamped on response headers by + // injectAnalogRouteRuleHeaders for routeRules with \`ssr: false\`. Nitro + // applies routeRule headers to the response before the renderer fires, + // so we can short-circuit by reading them here. + if (event.res.headers.get('x-analog-no-ssr') === 'true') { + return TEMPLATE; + } + return fetchViteEnv('ssr', event.req); +}); +`; +} + /** * Walks Nitro's resolved routeRules and stamps `x-analog-no-ssr: true` onto - * any rule with `ssr: false`. Analog's SSR service wrapper reads this header - * to short-circuit the renderer and return the raw template. + * any rule with `ssr: false`. Kept as a response-header hint for downstream + * consumers (CDN, edge logic); the actual SSR short-circuit happens inside + * the SSR renderer virtual above. */ function injectAnalogRouteRuleHeaders(nitro: Nitro): void { const routeRules = nitro.options.routeRules as From 16e73ab93bbf0247648b919c873515bff35c626d Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Sun, 24 May 2026 18:54:40 -0500 Subject: [PATCH 16/65] fix(platform): use Nitro virtual indirection for SSR dispatch instead 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 analogjs/analog#2035 Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/lib/nitro/analog-nitro-plugin.spec.ts | 4 ++ .../src/lib/nitro/analog-nitro-plugin.ts | 53 +++++++++++++++++-- 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/packages/platform/src/lib/nitro/analog-nitro-plugin.spec.ts b/packages/platform/src/lib/nitro/analog-nitro-plugin.spec.ts index 55177de38..5d9ea4689 100644 --- a/packages/platform/src/lib/nitro/analog-nitro-plugin.spec.ts +++ b/packages/platform/src/lib/nitro/analog-nitro-plugin.spec.ts @@ -125,8 +125,12 @@ describe('analogNitroPlugin', () => { const nitroMock: any = { options: { rootDir: projectRoot, + buildDir: join(projectRoot, '.nitro'), handlers: [], scanDirs: [], + virtual: {}, + renderer: {}, + dev: true, }, hooks: { hook: hookFn }, }; diff --git a/packages/platform/src/lib/nitro/analog-nitro-plugin.ts b/packages/platform/src/lib/nitro/analog-nitro-plugin.ts index 58f2a58d9..9da3b0b19 100644 --- a/packages/platform/src/lib/nitro/analog-nitro-plugin.ts +++ b/packages/platform/src/lib/nitro/analog-nitro-plugin.ts @@ -1,4 +1,4 @@ -import { existsSync, readFileSync } from 'node:fs'; +import { existsSync, readFileSync, readdirSync } from 'node:fs'; import { relative, resolve } from 'node:path'; import type { Nitro, NitroEventHandler, PrerenderRoute } from 'nitro/types'; import type { Plugin, UserConfig } from 'vite'; @@ -221,6 +221,15 @@ export function analogNitroPlugin(options: Options = {}): Plugin { // `renderer.template` are empty (vite.mjs:574), which never holds // for a typical app — so we install our own renderer virtual // explicitly here. + // + // `#analog/ssr` is a Nitro virtual (not a Vite virtual) so it + // resolves under both Vite-built bundles (main) and Rolldown-built + // bundles (Nitro's prerender, which forces builder: 'rolldown' — + // see nitro/dist/_chunks/nitro.mjs:769). That sidesteps nitro/vite's + // prodSetup polyfill, which is Vite-only and leaves `__nitro_vite_envs__` + // unset in the prerender bundle. + nitro.options.virtual['#analog/ssr'] = () => + generateSsrServiceVirtual(nitro); nitro.options.virtual['#analog/ssr-renderer'] = generateSsrRendererVirtual(readIndexHtml()); nitro.options.renderer ??= {}; @@ -256,7 +265,7 @@ export function analogNitroPlugin(options: Options = {}): Plugin { function generateSsrRendererVirtual(template: string): string { return ` import { defineHandler } from 'nitro/h3'; -import { fetchViteEnv } from 'nitro/vite/runtime'; +import ssr from '#analog/ssr'; const TEMPLATE = ${JSON.stringify(template)}; @@ -269,11 +278,49 @@ export default defineHandler(async (event) => { if (event.res.headers.get('x-analog-no-ssr') === 'true') { return TEMPLATE; } - return fetchViteEnv('ssr', event.req); + const service = ssr.default ?? ssr; + return service.fetch(event.req); }); `; } +/** + * Resolves \`#analog/ssr\` to the SSR fetch handler. The shape returned by + * this function is bundler-agnostic: works in Vite-built main bundles and + * Rolldown-built prerender bundles alike. + * + * - Dev: dispatch through nitro/vite's env runner (\`fetchViteEnv\`); the SSR + * service module isn't on disk yet, so we delegate to the runner. + * - Build / prerender: re-export the built SSR entry directly from the + * filesystem. By the time Nitro's bundlers ask for \`#analog/ssr\`, Vite has + * already produced \`/vite/services/ssr/.mjs\`. + */ +function generateSsrServiceVirtual(nitro: Nitro): string { + if (nitro.options.dev) { + return ` +import { fetchViteEnv } from 'nitro/vite/runtime'; +export default { + async fetch(req) { + return fetchViteEnv('ssr', req); + }, +}; +`; + } + + const ssrDir = resolve(nitro.options.buildDir, 'vite/services/ssr'); + if (!existsSync(ssrDir)) { + return `export default { async fetch() { throw new Error('Analog SSR service directory missing: ${ssrDir}'); } };`; + } + const entries = readdirSync(ssrDir).filter((f) => f.endsWith('.mjs')); + if (entries.length === 0) { + return `export default { async fetch() { throw new Error('No Analog SSR entry file built in: ${ssrDir}'); } };`; + } + // Prefer 'main.server.mjs' if present; otherwise take the only entry. + const entry = entries.find((f) => f === 'main.server.mjs') ?? entries[0]; + const entryPath = resolve(ssrDir, entry); + return `export { default } from ${JSON.stringify(entryPath)};`; +} + /** * Walks Nitro's resolved routeRules and stamps `x-analog-no-ssr: true` onto * any rule with `ssr: false`. Kept as a response-header hint for downstream From d9d842ae22afccd73fc131898a849d42f0954f1b Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Sun, 24 May 2026 19:17:22 -0500 Subject: [PATCH 17/65] fix(platform): gate SSR renderer + add Nitro externals/sanitizer for non-SSR and edge cases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 analogjs/analog#2035 Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/lib/nitro/analog-nitro-plugin.ts | 137 +++++++++++++++--- 1 file changed, 114 insertions(+), 23 deletions(-) diff --git a/packages/platform/src/lib/nitro/analog-nitro-plugin.ts b/packages/platform/src/lib/nitro/analog-nitro-plugin.ts index 9da3b0b19..023cff7f9 100644 --- a/packages/platform/src/lib/nitro/analog-nitro-plugin.ts +++ b/packages/platform/src/lib/nitro/analog-nitro-plugin.ts @@ -210,31 +210,38 @@ export function analogNitroPlugin(options: Options = {}): Plugin { if (Array.isArray(rollupConfig.plugins)) { rollupConfig.plugins.push(pageEndpointsPlugin()); } + applyAnalogNitroExternals(rollupConfig); + sanitizeNitroBundlerConfig(rollupConfig); }); - // Override Nitro's auto-detected template-serving renderer with one - // that routes HTML requests to our SSR service. Nitro's - // `resolveRendererOptions` finds `index.html` at the project root - // and installs `internal/routes/renderer-template[.dev]`, which - // just serves the raw template. nitro/vite's own SSR-routing - // renderer only auto-installs when both `renderer.handler` and - // `renderer.template` are empty (vite.mjs:574), which never holds - // for a typical app — so we install our own renderer virtual - // explicitly here. - // - // `#analog/ssr` is a Nitro virtual (not a Vite virtual) so it - // resolves under both Vite-built bundles (main) and Rolldown-built - // bundles (Nitro's prerender, which forces builder: 'rolldown' — - // see nitro/dist/_chunks/nitro.mjs:769). That sidesteps nitro/vite's - // prodSetup polyfill, which is Vite-only and leaves `__nitro_vite_envs__` - // unset in the prerender bundle. - nitro.options.virtual['#analog/ssr'] = () => - generateSsrServiceVirtual(nitro); - nitro.options.virtual['#analog/ssr-renderer'] = - generateSsrRendererVirtual(readIndexHtml()); - nitro.options.renderer ??= {}; - nitro.options.renderer.handler = '#analog/ssr-renderer'; - delete nitro.options.renderer.template; + if (ssr) { + // Override Nitro's auto-detected template-serving renderer with one + // that routes HTML requests to our SSR service. Nitro's + // `resolveRendererOptions` finds `index.html` at the project root + // and installs `internal/routes/renderer-template[.dev]`, which + // just serves the raw template. nitro/vite's own SSR-routing + // renderer only auto-installs when both `renderer.handler` and + // `renderer.template` are empty (vite.mjs:574), which never holds + // for a typical app — so we install our own renderer virtual + // explicitly here. + // + // `#analog/ssr` is a Nitro virtual (not a Vite virtual) so it + // resolves under both Vite-built bundles (main) and Rolldown-built + // bundles (Nitro's prerender, which forces builder: 'rolldown' — + // see nitro/dist/_chunks/nitro.mjs:769). That sidesteps nitro/vite's + // prodSetup polyfill, which is Vite-only and leaves + // `__nitro_vite_envs__` unset in the prerender bundle. + nitro.options.virtual['#analog/ssr'] = () => + generateSsrServiceVirtual(nitro); + nitro.options.virtual['#analog/ssr-renderer'] = + generateSsrRendererVirtual(readIndexHtml()); + nitro.options.renderer ??= {}; + nitro.options.renderer.handler = '#analog/ssr-renderer'; + delete nitro.options.renderer.template; + } + // When ssr === false, Nitro's auto-detected template-serving + // renderer is exactly what we want (serve the raw index.html for + // every HTML request) — leave it in place. injectAnalogRouteRuleHeaders(nitro); @@ -321,6 +328,90 @@ export default { return `export { default } from ${JSON.stringify(entryPath)};`; } +/** + * Packages Analog forces external in the Nitro server bundle. Each entry is + * here for a specific reason — see comments. + */ +const ANALOG_NITRO_EXTERNALS = [ + // rxjs ships per-entry CJS/ESM facades that confuse the Nitro/Rolldown + // resolver during bundling. + 'rxjs', + // node-fetch-native's polyfill subpath rewrites global fetch and isn't + // safe to inline into the Nitro bundle. + 'node-fetch-native/dist/polyfill', + // sharp ships platform-specific native binaries under @img/sharp-*. pnpm + // creates symlinks for ALL optional platform deps but only installs the + // matching one, leaving broken symlinks that crash Nitro's externals + // plugin with ENOENT during realpath(). Externalizing sharp avoids + // bundling it; the user's app resolves it from node_modules at runtime. + 'sharp', +]; + +function applyAnalogNitroExternals(rollupConfig: { external?: unknown }): void { + // Rolldown's `external` only accepts `Array`; promote + // whatever shape Nitro gave us (regex, single string, undefined) to an + // array and append Analog's entries as regex patterns that also match + // sub-paths (e.g. `sharp` matches `sharp/lib/foo`). + const prev = rollupConfig.external; + const existing: Array = + prev === undefined + ? [] + : Array.isArray(prev) + ? (prev as Array) + : prev instanceof RegExp + ? [prev] + : typeof prev === 'string' + ? [prev] + : []; + + const escapeRegExp = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + + for (const entry of ANALOG_NITRO_EXTERNALS) { + const pattern = new RegExp(`^${escapeRegExp(entry)}(?:/|$)`); + if (!existing.some((p) => String(p) === String(pattern))) { + existing.push(pattern); + } + } + + rollupConfig.external = existing; +} + +/** + * Workarounds for Nitro v3 + Rolldown bundler interaction quirks. Each + * is narrowly scoped and can be removed once the upstream bug is fixed: + * + * 1. `output.codeSplitting` — Nitro 3.0.x sets this; Rolldown rejects it + * as an unknown key. + * 2. `output.manualChunks` — Nitro's default manual chunking crashes + * Nitro's prerender rebundle. + * 3. `output.chunkFileNames` — Nitro's chunk-name function produces + * route-derived `[token]` patterns which Rollup/Rolldown interprets as + * placeholders; we rewrite non-standard tokens to `_token_`. + */ +function sanitizeNitroBundlerConfig(rollupConfig: { output?: unknown }): void { + const output = rollupConfig.output; + if (!output || Array.isArray(output) || typeof output !== 'object') return; + const out = output as Record; + + if ('codeSplitting' in out) delete out['codeSplitting']; + if ('manualChunks' in out) delete out['manualChunks']; + + const VALID_ROLLUP_PLACEHOLDER = /^\[(?:name|hash|format|ext)\]$/; + const chunkFileNames = out['chunkFileNames']; + if (typeof chunkFileNames === 'function') { + const originalFn = chunkFileNames as (...args: unknown[]) => unknown; + out['chunkFileNames'] = (...args: unknown[]) => { + const result = originalFn(...args); + if (typeof result !== 'string') return result; + return result.replace(/\[[^\]]+\]/g, (match: string) => + VALID_ROLLUP_PLACEHOLDER.test(match) + ? match + : `_${match.slice(1, -1)}_`, + ); + }; + } +} + /** * Walks Nitro's resolved routeRules and stamps `x-analog-no-ssr: true` onto * any rule with `ssr: false`. Kept as a response-header hint for downstream From 7be302a153c790b365f62d914548a3ad623b1771 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Sun, 24 May 2026 19:18:26 -0500 Subject: [PATCH 18/65] chore: migrate remaining demo apps to separated analog/angular/nitro 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//.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 analogjs/analog#2035 Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 3 ++- apps/blog-app/package.json | 4 ++- apps/blog-app/project.json | 17 ++++-------- apps/blog-app/vite.config.ts | 25 +++++++++++------ apps/opt-catchall-app/package.json | 3 ++- apps/opt-catchall-app/project.json | 17 ++++-------- apps/opt-catchall-app/vite.config.ts | 22 +++++++++++---- apps/tailwind-debug-app/package.json | 3 ++- apps/tailwind-debug-app/project.json | 17 ++++-------- apps/tailwind-debug-app/vite.config.ts | 37 +++++++++++++++----------- apps/tanstack-query-app/package.json | 2 ++ apps/tanstack-query-app/project.json | 17 ++++-------- apps/tanstack-query-app/vite.config.ts | 13 ++++++++- pnpm-lock.yaml | 18 +++++++++++++ 14 files changed, 116 insertions(+), 82 deletions(-) diff --git a/.gitignore b/.gitignore index 8de9b0e66..b7e68531f 100644 --- a/.gitignore +++ b/.gitignore @@ -62,6 +62,7 @@ stats.html # Nitro .nitro +.output /migrations.json /.env @@ -85,4 +86,4 @@ gradle.properties .cursor .claude gradle.properties -*.tsbuildinfo \ No newline at end of file +*.tsbuildinfo diff --git a/apps/blog-app/package.json b/apps/blog-app/package.json index d1f5633cc..48cc4eca8 100644 --- a/apps/blog-app/package.json +++ b/apps/blog-app/package.json @@ -7,6 +7,8 @@ "@analogjs/router": "workspace:*" }, "devDependencies": { - "@analogjs/platform": "workspace:*" + "@analogjs/platform": "workspace:*", + "@analogjs/vite-plugin-angular": "workspace:*", + "nitro": "catalog:" } } diff --git a/apps/blog-app/project.json b/apps/blog-app/project.json index 5d78732c4..6781e2541 100644 --- a/apps/blog-app/project.json +++ b/apps/blog-app/project.json @@ -7,26 +7,19 @@ "tags": [], "targets": { "build": { - "executor": "@nx/vite:build", + "executor": "nx:run-commands", "dependsOn": ["platform:build", "router:build", "content:build"], - "outputs": [ - "{options.outputPath}", - "{workspaceRoot}/dist/apps/blog-app/.nitro", - "{workspaceRoot}/dist/apps/blog-app/ssr", - "{workspaceRoot}/dist/apps/blog-app/analog" - ], + "outputs": ["{workspaceRoot}/apps/blog-app/.output"], "options": { - "configFile": "apps/blog-app/vite.config.ts", - "outputPath": "dist/apps/blog-app/client" + "command": "vite build -c apps/blog-app/vite.config.ts" }, "defaultConfiguration": "production", "configurations": { "development": { - "mode": "development" + "command": "vite build -c apps/blog-app/vite.config.ts --mode development" }, "production": { - "sourcemap": false, - "mode": "production" + "command": "vite build -c apps/blog-app/vite.config.ts --mode production" } } }, diff --git a/apps/blog-app/vite.config.ts b/apps/blog-app/vite.config.ts index 6f14b2adb..766c56021 100644 --- a/apps/blog-app/vite.config.ts +++ b/apps/blog-app/vite.config.ts @@ -1,6 +1,9 @@ /// import analog, { type PrerenderContentFile } from '@analogjs/platform'; +import angular from '@analogjs/vite-plugin-angular'; +import { nitro } from 'nitro/vite'; +import { resolve } from 'node:path'; import { defineConfig } from 'vite'; import { getWorkspaceDependencyExcludes } from '../../tools/vite/get-workspace-dependency-excludes.js'; @@ -26,14 +29,16 @@ export default defineConfig(() => { exclude: getWorkspaceDependencyExcludes(__dirname), }, build: { - outDir: '../../dist/apps/blog-app/client', - emptyOutDir: true, reportCompressedSize: true, target: ['es2020'], }, + server: { + fs: { + allow: [resolve(__dirname, '../..')], + }, + }, plugins: [ analog({ - liveReload: true, content: { highlighter: 'shiki', shikiOptions: { @@ -84,11 +89,15 @@ export default defineConfig(() => { host: 'https://analog-blog.netlify.app', }, }, - nitro: { - prerender: { - autoSubfolderIndex: false, - failOnError: true, - }, + }), + angular({ + workspaceRoot: resolve(__dirname, '../..'), + liveReload: true, + }), + nitro({ + prerender: { + autoSubfolderIndex: false, + failOnError: true, }, }), ], diff --git a/apps/opt-catchall-app/package.json b/apps/opt-catchall-app/package.json index ca967937d..957553fdd 100644 --- a/apps/opt-catchall-app/package.json +++ b/apps/opt-catchall-app/package.json @@ -8,6 +8,7 @@ }, "devDependencies": { "@analogjs/platform": "workspace:*", - "@analogjs/vite-plugin-angular": "workspace:*" + "@analogjs/vite-plugin-angular": "workspace:*", + "nitro": "catalog:" } } diff --git a/apps/opt-catchall-app/project.json b/apps/opt-catchall-app/project.json index 638e54f21..8c79fe493 100644 --- a/apps/opt-catchall-app/project.json +++ b/apps/opt-catchall-app/project.json @@ -7,26 +7,19 @@ "tags": [], "targets": { "build": { - "executor": "@nx/vite:build", + "executor": "nx:run-commands", "dependsOn": ["platform:build", "router:build", "content:build"], - "outputs": [ - "{options.outputPath}", - "{workspaceRoot}/dist/apps/opt-catchall-app/.nitro", - "{workspaceRoot}/dist/apps/opt-catchall-app/ssr", - "{workspaceRoot}/dist/apps/opt-catchall-app/analog" - ], + "outputs": ["{workspaceRoot}/apps/opt-catchall-app/.output"], "options": { - "configFile": "apps/opt-catchall-app/vite.config.ts", - "outputPath": "dist/apps/opt-catchall-app/client" + "command": "vite build -c apps/opt-catchall-app/vite.config.ts" }, "defaultConfiguration": "production", "configurations": { "development": { - "mode": "development" + "command": "vite build -c apps/opt-catchall-app/vite.config.ts --mode development" }, "production": { - "sourcemap": false, - "mode": "production" + "command": "vite build -c apps/opt-catchall-app/vite.config.ts --mode production" } } }, diff --git a/apps/opt-catchall-app/vite.config.ts b/apps/opt-catchall-app/vite.config.ts index 4d1108cfb..adccfbdf8 100644 --- a/apps/opt-catchall-app/vite.config.ts +++ b/apps/opt-catchall-app/vite.config.ts @@ -1,6 +1,9 @@ /// import analog from '@analogjs/platform'; +import angular from '@analogjs/vite-plugin-angular'; +import { nitro } from 'nitro/vite'; +import { resolve } from 'node:path'; import { defineConfig } from 'vite'; import { getWorkspaceDependencyExcludes } from '../../tools/vite/get-workspace-dependency-excludes.js'; @@ -15,21 +18,30 @@ export default defineConfig(() => { exclude: getWorkspaceDependencyExcludes(__dirname), }, build: { - outDir: '../../dist/apps/opt-catchall-app/client', - emptyOutDir: true, reportCompressedSize: true, target: ['es2020'], }, + server: { + fs: { + // Allow Vite's fs fallback to read pnpm content-hash paths so + // nitro/vite's env-runner can load its own dev runtime entry. + allow: [resolve(__dirname, '../..')], + }, + }, plugins: [ analog({ + content: { + highlighter: 'shiki', + }, + }), + angular({ + workspaceRoot: resolve(__dirname, '../..'), liveReload: true, experimental: { useAngularCompilationAPI: true, }, - content: { - highlighter: 'shiki', - }, }), + nitro({}), ], }; }); diff --git a/apps/tailwind-debug-app/package.json b/apps/tailwind-debug-app/package.json index 7d8a9aef0..097fe45c5 100644 --- a/apps/tailwind-debug-app/package.json +++ b/apps/tailwind-debug-app/package.json @@ -8,6 +8,7 @@ "devDependencies": { "@analogjs/platform": "workspace:*", "@analogjs/vite-plugin-angular": "workspace:*", - "@analogjs/vitest-angular": "workspace:*" + "@analogjs/vitest-angular": "workspace:*", + "nitro": "catalog:" } } diff --git a/apps/tailwind-debug-app/project.json b/apps/tailwind-debug-app/project.json index 0eeee883d..3a42cede6 100644 --- a/apps/tailwind-debug-app/project.json +++ b/apps/tailwind-debug-app/project.json @@ -7,26 +7,19 @@ "tags": [], "targets": { "build": { - "executor": "@nx/vite:build", + "executor": "nx:run-commands", "dependsOn": ["platform:build", "router:build"], - "outputs": [ - "{options.outputPath}", - "{workspaceRoot}/dist/apps/tailwind-debug-app/.nitro", - "{workspaceRoot}/dist/apps/tailwind-debug-app/ssr", - "{workspaceRoot}/dist/apps/tailwind-debug-app/analog" - ], + "outputs": ["{workspaceRoot}/apps/tailwind-debug-app/.output"], "options": { - "configFile": "apps/tailwind-debug-app/vite.config.ts", - "outputPath": "dist/apps/tailwind-debug-app/client" + "command": "vite build -c apps/tailwind-debug-app/vite.config.ts" }, "defaultConfiguration": "production", "configurations": { "development": { - "mode": "development" + "command": "vite build -c apps/tailwind-debug-app/vite.config.ts --mode development" }, "production": { - "sourcemap": false, - "mode": "production" + "command": "vite build -c apps/tailwind-debug-app/vite.config.ts --mode production" } } }, diff --git a/apps/tailwind-debug-app/vite.config.ts b/apps/tailwind-debug-app/vite.config.ts index dd25855ec..545c3b628 100644 --- a/apps/tailwind-debug-app/vite.config.ts +++ b/apps/tailwind-debug-app/vite.config.ts @@ -1,9 +1,12 @@ /// import analog from '@analogjs/platform'; +import angular from '@analogjs/vite-plugin-angular'; +import { nitro } from 'nitro/vite'; import tailwindcss from '@tailwindcss/vite'; import fs from 'node:fs'; import path from 'node:path'; +import { resolve } from 'node:path'; import { createLogger, defineConfig, type Plugin } from 'vite'; import { getWorkspaceDependencyExcludes } from '../../tools/vite/get-workspace-dependency-excludes.js'; @@ -88,7 +91,6 @@ export default defineConfig(({ mode }) => ({ exclude: getWorkspaceDependencyExcludes(__dirname), }, build: { - outDir: '../../dist/apps/tailwind-debug-app/client', reportCompressedSize: true, target: ['es2020'], }, @@ -106,31 +108,34 @@ export default defineConfig(({ mode }) => ({ plugins: [ analog({ apiPrefix: 'api', + prerender: { + routes: [], + }, + ssr: false, + }), + angular({ + workspaceRoot: resolve(__dirname, '../..'), experimental: { // Required to reproduce #2293: @apply inside :host with Tailwind // prefix configuration requires the Angular Compilation API path // for style externalization. useAngularCompilationAPI: true, }, - prerender: { - routes: [], - }, - ssr: false, - nitro: { - routeRules: { - '/probe': { - ssr: false, - }, - }, - experimental: { - websocket: true, - }, - }, tailwindCss: { prefixes: ['tdbg:'], rootStylesheet: 'apps/tailwind-debug-app/src/styles.css', }, }), + nitro({ + routeRules: { + '/probe': { + ssr: false, + }, + }, + experimental: { + websocket: true, + }, + }), tailwindcss(), hmrWiretapPlugin(), ], @@ -151,7 +156,7 @@ export default defineConfig(({ mode }) => ({ server: { port: 43040, fs: { - allow: ['.'], + allow: [resolve(__dirname, '../..')], }, hmr: { clientPort: 4201, diff --git a/apps/tanstack-query-app/package.json b/apps/tanstack-query-app/package.json index 8b6998990..92ed2a369 100644 --- a/apps/tanstack-query-app/package.json +++ b/apps/tanstack-query-app/package.json @@ -7,9 +7,11 @@ }, "devDependencies": { "@analogjs/platform": "workspace:*", + "@analogjs/vite-plugin-angular": "workspace:*", "@analogjs/vitest-angular": "workspace:*", "@tailwindcss/vite": "catalog:", "daisyui": "^5.5.19", + "nitro": "catalog:", "tailwindcss": "catalog:" } } diff --git a/apps/tanstack-query-app/project.json b/apps/tanstack-query-app/project.json index 21bde6f84..be16066db 100644 --- a/apps/tanstack-query-app/project.json +++ b/apps/tanstack-query-app/project.json @@ -7,26 +7,19 @@ "tags": [], "targets": { "build": { - "executor": "@nx/vite:build", + "executor": "nx:run-commands", "dependsOn": ["platform:build", "router:build"], - "outputs": [ - "{options.outputPath}", - "{workspaceRoot}/dist/apps/tanstack-query-app/.nitro", - "{workspaceRoot}/dist/apps/tanstack-query-app/ssr", - "{workspaceRoot}/dist/apps/tanstack-query-app/analog" - ], + "outputs": ["{workspaceRoot}/apps/tanstack-query-app/.output"], "options": { - "configFile": "apps/tanstack-query-app/vite.config.ts", - "outputPath": "dist/apps/tanstack-query-app/client" + "command": "vite build -c apps/tanstack-query-app/vite.config.ts" }, "defaultConfiguration": "production", "configurations": { "development": { - "mode": "development" + "command": "vite build -c apps/tanstack-query-app/vite.config.ts --mode development" }, "production": { - "sourcemap": false, - "mode": "production" + "command": "vite build -c apps/tanstack-query-app/vite.config.ts --mode production" } } }, diff --git a/apps/tanstack-query-app/vite.config.ts b/apps/tanstack-query-app/vite.config.ts index 6c8bebd24..3ad16de5f 100644 --- a/apps/tanstack-query-app/vite.config.ts +++ b/apps/tanstack-query-app/vite.config.ts @@ -1,6 +1,9 @@ /// import analog from '@analogjs/platform'; +import angular from '@analogjs/vite-plugin-angular'; +import { nitro } from 'nitro/vite'; +import { resolve } from 'node:path'; import tailwindcss from '@tailwindcss/vite'; import { defineConfig } from 'vite'; import { getWorkspaceDependencyExcludes } from '../../tools/vite/get-workspace-dependency-excludes.js'; @@ -11,7 +14,6 @@ export default defineConfig(({ mode }) => { root: __dirname, publicDir: 'src/public', build: { - outDir: '../../dist/apps/tanstack-query-app/client', reportCompressedSize: true, target: ['es2020'], }, @@ -21,11 +23,20 @@ export default defineConfig(({ mode }) => { // can compile external templates/styles instead of Vite prebundling them. exclude: getWorkspaceDependencyExcludes(__dirname), }, + server: { + fs: { + allow: [resolve(__dirname, '../..')], + }, + }, plugins: [ tailwindcss(), analog({ apiPrefix: 'api', }), + angular({ + workspaceRoot: resolve(__dirname, '../..'), + }), + nitro({}), ], test: { reporters: ['default'], diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f6ff8e2ab..04fc32882 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1195,6 +1195,12 @@ importers: '@analogjs/platform': specifier: workspace:* version: link:../../packages/platform + '@analogjs/vite-plugin-angular': + specifier: workspace:* + version: link:../../packages/vite-plugin-angular + nitro: + specifier: 'catalog:' + version: 3.0.260415-beta(chokidar@5.0.0)(dotenv@16.4.7)(jiti@2.6.1)(lru-cache@11.3.5)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) apps/blog-app-e2e: {} @@ -1259,6 +1265,9 @@ importers: '@analogjs/vite-plugin-angular': specifier: workspace:* version: link:../../packages/vite-plugin-angular + nitro: + specifier: 'catalog:' + version: 3.0.260415-beta(chokidar@5.0.0)(dotenv@16.4.7)(jiti@2.6.1)(lru-cache@11.3.5)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) apps/tailwind-debug-app: dependencies: @@ -1275,6 +1284,9 @@ importers: '@analogjs/vitest-angular': specifier: workspace:* version: link:../../packages/vitest-angular + nitro: + specifier: 'catalog:' + version: 3.0.260415-beta(chokidar@5.0.0)(dotenv@16.4.7)(jiti@2.6.1)(lru-cache@11.3.5)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) apps/tailwind-debug-app-e2e: {} @@ -1287,6 +1299,9 @@ importers: '@analogjs/platform': specifier: workspace:* version: link:../../packages/platform + '@analogjs/vite-plugin-angular': + specifier: workspace:* + version: link:../../packages/vite-plugin-angular '@analogjs/vitest-angular': specifier: workspace:* version: link:../../packages/vitest-angular @@ -1296,6 +1311,9 @@ importers: daisyui: specifier: ^5.5.19 version: 5.5.19 + nitro: + specifier: 'catalog:' + version: 3.0.260415-beta(chokidar@5.0.0)(dotenv@16.4.7)(jiti@2.6.1)(lru-cache@11.3.5)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) tailwindcss: specifier: 'catalog:' version: 4.2.2 From 03004b5b55bf4371f15d33a79cb18c9d09b5e8f9 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Sun, 24 May 2026 19:36:19 -0500 Subject: [PATCH 19/65] docs: add plugin-separation guidance to the v2 to v3 migration guide 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 analogjs/analog#2035 Co-Authored-By: Claude Opus 4.7 (1M context) --- .../docs/guides/migrating-v2-to-v3.md | 171 ++++++++++++++++++ 1 file changed, 171 insertions(+) diff --git a/apps/docs-app/docs/guides/migrating-v2-to-v3.md b/apps/docs-app/docs/guides/migrating-v2-to-v3.md index 88b0c92e6..6c4a89144 100644 --- a/apps/docs-app/docs/guides/migrating-v2-to-v3.md +++ b/apps/docs-app/docs/guides/migrating-v2-to-v3.md @@ -37,6 +37,172 @@ Analog v3 no longer supports Angular v16. Upgrade the workspace to Angular v17 o Analog SFC support was removed and `.agx` files are no longer supported. Replace any remaining SFC usage with standard Angular components, markdown content files, or route/page files that use the current Analog conventions. +### `analog()`, `angular()`, and `nitro()` are now separate plugins + +Analog v3 splits the Vite plugin chain into three explicit calls. `analog()` no longer internally invokes `@analogjs/vite-plugin-angular` or `nitro/vite` — you call them yourself. Pass each plugin only the options it owns. + +`@analogjs/vite-plugin-nitro` is deprecated; the Nitro orchestration moved into `@analogjs/platform`. Direct importers of `@analogjs/vite-plugin-nitro` must migrate to the separated shape below. + +Before: + +```ts +import { defineConfig } from 'vite'; +import analog from '@analogjs/platform'; + +export default defineConfig(() => ({ + plugins: [ + analog({ + ssr: true, + apiPrefix: 'api', + vite: { + inlineStylesExtension: 'scss', + fastCompile: true, + }, + fileReplacements: [ + { replace: 'src/environment.ts', with: 'src/environment.prod.ts' }, + ], + nitro: { + routeRules: { '/admin/**': { ssr: false } }, + }, + prerender: { + routes: ['/', '/about'], + }, + }), + ], +})); +``` + +After: + +```ts +import { resolve } from 'node:path'; +import { defineConfig } from 'vite'; +import analog from '@analogjs/platform'; +import angular from '@analogjs/vite-plugin-angular'; +import { nitro } from 'nitro/vite'; + +export default defineConfig(() => ({ + server: { + // Vite 8's strict fs only allows reads under config.root; nitro/vite's + // env-runner reaches pnpm content-hash paths at the workspace root + // when loading its own dev runtime. + fs: { allow: [resolve(__dirname, '../..')] }, + }, + plugins: [ + analog({ + ssr: true, + apiPrefix: 'api', + prerender: { + routes: ['/', '/about'], + }, + }), + angular({ + workspaceRoot: resolve(__dirname, '../..'), + inlineStylesExtension: 'scss', + fastCompile: true, + fileReplacements: [ + { replace: 'src/environment.ts', with: 'src/environment.prod.ts' }, + ], + }), + nitro({ + routeRules: { '/admin/**': { ssr: false } }, + }), + ], +})); +``` + +Add `@analogjs/vite-plugin-angular` and `nitro` to the app's `devDependencies`: + +```json +{ + "devDependencies": { + "@analogjs/platform": "...", + "@analogjs/vite-plugin-angular": "...", + "nitro": "..." + } +} +``` + +#### Options that moved off `analog()` + +These options used to live on `analog()`. Pass them to `angular()` or `nitro()` directly: + +| v2 location | v3 location | +| ------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------- | +| `analog({ vite: {...} })` | spread directly into `angular({...})` | +| `analog({ jit })`, `disableTypeChecking`, `liveReload`, `inlineStylesExtension`, `fileReplacements`, `fastCompile`, `fastCompileMode`, `include` | `angular({...})` | +| `analog({ tailwindCss: {...} })` | `angular({ tailwindCss: {...} })` | +| `analog({ experimental: { useAngularCompilationAPI: true } })` | `angular({ experimental: { useAngularCompilationAPI: true } })` | +| `analog({ experimental: { stylePipeline: { angularPlugins: [...] } } })` | `angular({ stylePipeline: { plugins: [...] } })` | +| `analog({ nitro: {...} })` | `nitro({...})` (first arg) | +| `analog({ vite: false })` | drop `angular()` from the plugins array | + +`analog()` retains `ssr`, `apiPrefix`, `entryServer`, `content`, `prerender`, `i18n`, `discoverRoutes`, `additionalPagesDirs`/`additionalContentDirs`/`additionalAPIDirs`, `debug`, and `experimental.typedRouter`/`experimental.stylePipeline`. + +#### Workspace library globs + +If your v2 config used `discoverRoutes: true` to compile workspace library pages, the same helper is now exported from `@analogjs/platform`. Call it once and feed the result to both `analog()` and `angular()`: + +```ts +import analog, { discoverLibraryRoutes, pageGlobs } from '@analogjs/platform'; +import angular from '@analogjs/vite-plugin-angular'; + +const libs = discoverLibraryRoutes(resolve(__dirname, '../..')); + +plugins: [ + analog({ + additionalPagesDirs: libs.additionalPagesDirs, + additionalContentDirs: libs.additionalContentDirs, + additionalAPIDirs: libs.additionalAPIDirs, + }), + angular({ + include: pageGlobs(libs.additionalPagesDirs), + additionalContentDirs: libs.additionalContentDirs, + }), + nitro({}), +]; +``` + +#### Nx build target + +If your app uses `@nx/vite:build`, switch it to `nx:run-commands` invoking `vite build`. `@nx/vite:build` iterates `builder.environments` but doesn't call `builder.buildApp()` — and `nitro/vite`'s prerender + final Nitro env build orchestration lives in the `buildApp` hook. Without the CLI's `buildApp` invocation, no prerender runs and the SSR/Nitro env outputs are skipped. + +Before (`apps//project.json`): + +```json +{ + "build": { + "executor": "@nx/vite:build", + "outputs": [ + "{options.outputPath}", + "{workspaceRoot}/dist/apps//.nitro", + "{workspaceRoot}/dist/apps//ssr", + "{workspaceRoot}/dist/apps//analog" + ], + "options": { + "configFile": "apps//vite.config.ts", + "outputPath": "dist/apps//client" + } + } +} +``` + +After: + +```json +{ + "build": { + "executor": "nx:run-commands", + "outputs": ["{workspaceRoot}/apps//.output"], + "options": { + "command": "vite build -c apps//vite.config.ts" + } + } +} +``` + +Also drop the top-level `build.outDir` override in `vite.config.ts`. Under `nitro/vite`, the client environment's output is relocated to `/.output/public` by Nitro; the legacy `dist/apps//client` override no longer matches the active output path. + ### Content rendering now requires an explicit highlighter If your app renders markdown content, configure the content highlighter through the `analog()` plugin in `vite.config.ts`. New blog templates already do this, but older full-stack apps often do not. @@ -152,6 +318,11 @@ Keep automated migration tooling focused on the breaking changes above: - require Angular v17 or newer before applying v3 changes - replace deep or internal imports with public package entrypoints +- split `analog()` into `analog() + angular() + nitro()`, moving each option to the plugin that now owns it (see [plugin separation](#analog-angular-and-nitro-are-now-separate-plugins)) +- flag `@analogjs/vite-plugin-nitro` as deprecated; direct importers must migrate to `@analogjs/platform` + `nitro/vite` +- add `@analogjs/vite-plugin-angular` and `nitro` to app `devDependencies` (the separated shape imports them directly) +- replace `@nx/vite:build` with `nx:run-commands` invoking `vite build -c apps//vite.config.ts`; drop the legacy `build.outDir` override and update `outputs` to `apps//.output` +- add `server.fs.allow` pointing at the workspace root in `vite.config.ts` so Vite 8's strict fs allows nitro/vite's env runner to load its own dev runtime through pnpm content-hash paths - add explicit `analog({ content: { highlighter: 'shiki' } })` config when the app renders markdown content - add `withContentRoutes()` from `@analogjs/router/content` when the app uses markdown page routes - flag `analog({ i18n: ... })`, `provideI18n()`, `injectSwitchLocale()`, `loadTranslationsRuntime()`, or content locale helpers as removed v3 APIs From b15b796f8fa06a0eef64d1a8efed65842d810f97 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Sun, 24 May 2026 19:59:21 -0500 Subject: [PATCH 20/65] chore(create-analog): add nitro to template devDependencies 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) --- packages/create-analog/template-blog/package.json | 1 + packages/create-analog/template-latest/package.json | 1 + packages/create-analog/template-minimal/package.json | 1 + 3 files changed, 3 insertions(+) diff --git a/packages/create-analog/template-blog/package.json b/packages/create-analog/template-blog/package.json index 4a8e56709..6bc602262 100644 --- a/packages/create-analog/template-blog/package.json +++ b/packages/create-analog/template-blog/package.json @@ -42,6 +42,7 @@ "@angular/cli": "^21.0.0", "@angular/compiler-cli": "^21.0.0", "jsdom": "^22.0.0", + "nitro": "3.0.260415-beta", "rollup": "^4.40.0", "typescript": "~5.9.0", "vite": "^8.0.0", diff --git a/packages/create-analog/template-latest/package.json b/packages/create-analog/template-latest/package.json index 6d82cf5b7..3f3b3f65a 100644 --- a/packages/create-analog/template-latest/package.json +++ b/packages/create-analog/template-latest/package.json @@ -43,6 +43,7 @@ "@angular/cli": "^21.0.0", "@angular/compiler-cli": "^21.0.0", "jsdom": "^22.0.0", + "nitro": "3.0.260415-beta", "rollup": "^4.40.0", "typescript": "~5.9.0", "vite": "^8.0.0", diff --git a/packages/create-analog/template-minimal/package.json b/packages/create-analog/template-minimal/package.json index 6d82cf5b7..3f3b3f65a 100644 --- a/packages/create-analog/template-minimal/package.json +++ b/packages/create-analog/template-minimal/package.json @@ -43,6 +43,7 @@ "@angular/cli": "^21.0.0", "@angular/compiler-cli": "^21.0.0", "jsdom": "^22.0.0", + "nitro": "3.0.260415-beta", "rollup": "^4.40.0", "typescript": "~5.9.0", "vite": "^8.0.0", From 7e94cd70e04d3e3a0f12ec00e9d72731a076ac18 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Sun, 24 May 2026 23:10:24 -0500 Subject: [PATCH 21/65] chore(create-analog): wire templates to separated plugins (analog + angular + 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) --- packages/create-analog/template-blog/vite.config.ts | 4 ++++ packages/create-analog/template-latest/vite.config.ts | 4 ++++ packages/create-analog/template-minimal/vite.config.ts | 5 ++++- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/create-analog/template-blog/vite.config.ts b/packages/create-analog/template-blog/vite.config.ts index d6427eab1..7564e5a84 100644 --- a/packages/create-analog/template-blog/vite.config.ts +++ b/packages/create-analog/template-blog/vite.config.ts @@ -2,6 +2,8 @@ import { defineConfig } from 'vite'; __TAILWIND_IMPORT__import analog from '@analogjs/platform'; +import angular from '@analogjs/vite-plugin-angular'; +import { nitro } from 'nitro/vite'; // https://vitejs.dev/config/ export default defineConfig(({ mode }) => ({ @@ -20,6 +22,8 @@ __TAILWIND_PLUGIN__ analog({ routes: ['/blog', '/blog/2022-12-27-my-first-post'], }, }), + angular(), + nitro(), ], test: { globals: true, diff --git a/packages/create-analog/template-latest/vite.config.ts b/packages/create-analog/template-latest/vite.config.ts index 8737bf35c..6da8e9cf5 100644 --- a/packages/create-analog/template-latest/vite.config.ts +++ b/packages/create-analog/template-latest/vite.config.ts @@ -2,6 +2,8 @@ import { defineConfig } from 'vite'; __TAILWIND_IMPORT__import analog from '@analogjs/platform'; +import angular from '@analogjs/vite-plugin-angular'; +import { nitro } from 'nitro/vite'; // https://vitejs.dev/config/ export default defineConfig(({ mode }) => ({ @@ -13,6 +15,8 @@ export default defineConfig(({ mode }) => ({ }, plugins: [ analog(), + angular(), + nitro(), __TAILWIND_PLUGIN__ ], test: { globals: true, diff --git a/packages/create-analog/template-minimal/vite.config.ts b/packages/create-analog/template-minimal/vite.config.ts index bc4c98d2e..c340a496e 100644 --- a/packages/create-analog/template-minimal/vite.config.ts +++ b/packages/create-analog/template-minimal/vite.config.ts @@ -2,6 +2,8 @@ import { defineConfig } from 'vite'; __TAILWIND_IMPORT__import analog from '@analogjs/platform'; +import angular from '@analogjs/vite-plugin-angular'; +import { nitro } from 'nitro/vite'; // https://vitejs.dev/config/ export default defineConfig(({ mode }) => ({ @@ -14,10 +16,11 @@ export default defineConfig(({ mode }) => ({ plugins: [ __TAILWIND_PLUGIN__ analog({ ssr: false, - static: true, prerender: { routes: [], }, }), + angular(), + nitro({ static: true }), ], })); From 0580db20ab4a23666d3895ad25e862adb0247b17 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Sun, 24 May 2026 23:17:25 -0500 Subject: [PATCH 22/65] feat(platform): add ng-update schematic for the separated-plugins migration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../migrate-to-separated-plugins.spec.ts | 203 ++++++++++++++++++ .../migrate-to-separated-plugins.ts | 135 ++++++++++++ packages/platform/migrations/migration.json | 8 +- 3 files changed, 345 insertions(+), 1 deletion(-) create mode 100644 packages/platform/migrations/migrate-to-separated-plugins/migrate-to-separated-plugins.spec.ts create mode 100644 packages/platform/migrations/migrate-to-separated-plugins/migrate-to-separated-plugins.ts diff --git a/packages/platform/migrations/migrate-to-separated-plugins/migrate-to-separated-plugins.spec.ts b/packages/platform/migrations/migrate-to-separated-plugins/migrate-to-separated-plugins.spec.ts new file mode 100644 index 000000000..07286d9c6 --- /dev/null +++ b/packages/platform/migrations/migrate-to-separated-plugins/migrate-to-separated-plugins.spec.ts @@ -0,0 +1,203 @@ +import { describe, expect, it, beforeEach, vi } from 'vitest'; +import { UnitTestTree } from '@angular-devkit/schematics/testing'; +import { Tree, SchematicContext } from '@angular-devkit/schematics'; + +import migrateToSeparatedPlugins from './migrate-to-separated-plugins'; + +const LEGACY_CONFIG = ` +import analog from '@analogjs/platform'; +import { defineConfig } from 'vite'; + +export default defineConfig(() => ({ + plugins: [ + analog({ apiPrefix: 'api' }), + ], +})); +`; + +const SEPARATED_CONFIG = ` +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(), nitro()], +})); +`; + +function createContext() { + const infoLogs: string[] = []; + return { + infoLogs, + context: { + logger: { + info: (msg: string) => infoLogs.push(msg), + }, + addTask: vi.fn(), + } as unknown as SchematicContext, + }; +} + +describe('migrate-to-separated-plugins', () => { + let tree: UnitTestTree; + + beforeEach(() => { + tree = new UnitTestTree(Tree.empty()); + }); + + it('logs the migration notice and adds deps when a legacy vite.config.ts is detected', () => { + tree.create('/vite.config.ts', LEGACY_CONFIG); + tree.create( + '/package.json', + JSON.stringify({ + devDependencies: { + '@analogjs/platform': '^3.0.0-alpha.55', + }, + }), + ); + + const { context, infoLogs } = createContext(); + migrateToSeparatedPlugins()(tree, context); + + const pkg = JSON.parse(tree.readContent('/package.json')); + expect(pkg.devDependencies['@analogjs/vite-plugin-angular']).toBe( + '^3.0.0-alpha.55', + ); + expect(pkg.devDependencies['nitro']).toBe('3.0.260415-beta'); + expect(infoLogs.join('\n')).toContain('/vite.config.ts'); + expect(infoLogs.join('\n')).toContain('migrating-v2-to-v3'); + }); + + it('is a no-op when the vite.config is already on the separated shape', () => { + tree.create('/vite.config.ts', SEPARATED_CONFIG); + tree.create( + '/package.json', + JSON.stringify({ + devDependencies: { + '@analogjs/platform': '^3.0.0-alpha.55', + }, + }), + ); + + const { context, infoLogs } = createContext(); + migrateToSeparatedPlugins()(tree, context); + + const pkg = JSON.parse(tree.readContent('/package.json')); + expect( + pkg.devDependencies['@analogjs/vite-plugin-angular'], + ).toBeUndefined(); + expect(pkg.devDependencies['nitro']).toBeUndefined(); + expect(infoLogs).toEqual([]); + }); + + it('does not duplicate deps that are already declared', () => { + tree.create('/vite.config.ts', LEGACY_CONFIG); + tree.create( + '/package.json', + JSON.stringify({ + devDependencies: { + '@analogjs/platform': '^3.0.0-alpha.55', + '@analogjs/vite-plugin-angular': '^2.5.0', + nitro: '3.0.250101-beta', + }, + }), + ); + + const { context } = createContext(); + migrateToSeparatedPlugins()(tree, context); + + const pkg = JSON.parse(tree.readContent('/package.json')); + expect(pkg.devDependencies['@analogjs/vite-plugin-angular']).toBe('^2.5.0'); + expect(pkg.devDependencies['nitro']).toBe('3.0.250101-beta'); + }); + + it('reads version from `dependencies` if `devDependencies` is missing platform', () => { + tree.create('/vite.config.ts', LEGACY_CONFIG); + tree.create( + '/package.json', + JSON.stringify({ + dependencies: { + '@analogjs/platform': '~3.0.0-alpha.55', + }, + }), + ); + + const { context } = createContext(); + migrateToSeparatedPlugins()(tree, context); + + const pkg = JSON.parse(tree.readContent('/package.json')); + expect(pkg.devDependencies['@analogjs/vite-plugin-angular']).toBe( + '~3.0.0-alpha.55', + ); + }); + + it('skips files inside node_modules', () => { + tree.create('/node_modules/some-pkg/vite.config.ts', LEGACY_CONFIG); + tree.create( + '/package.json', + JSON.stringify({ + devDependencies: { '@analogjs/platform': '^3.0.0-alpha.55' }, + }), + ); + + const { context } = createContext(); + migrateToSeparatedPlugins()(tree, context); + + const pkg = JSON.parse(tree.readContent('/package.json')); + expect(pkg.devDependencies['nitro']).toBeUndefined(); + }); + + it('only matches vite.config files (vite.config.ts, .mts, .js, .mjs)', () => { + // Looks like a legacy analog() call but isn't a vite config. + tree.create('/src/example.ts', LEGACY_CONFIG); + tree.create( + '/package.json', + JSON.stringify({ + devDependencies: { '@analogjs/platform': '^3.0.0-alpha.55' }, + }), + ); + + const { context } = createContext(); + migrateToSeparatedPlugins()(tree, context); + + const pkg = JSON.parse(tree.readContent('/package.json')); + expect(pkg.devDependencies['nitro']).toBeUndefined(); + }); + + it('detects a vite.config.mts file', () => { + tree.create('/apps/example/vite.config.mts', LEGACY_CONFIG); + tree.create( + '/package.json', + JSON.stringify({ + devDependencies: { '@analogjs/platform': '^3.0.0-alpha.55' }, + }), + ); + + const { context } = createContext(); + migrateToSeparatedPlugins()(tree, context); + + const pkg = JSON.parse(tree.readContent('/package.json')); + expect(pkg.devDependencies['nitro']).toBe('3.0.260415-beta'); + }); + + it('does not treat a config that already imports the new plugins as legacy', () => { + // analog() still appears but both new imports are present — already migrated. + tree.create('/vite.config.ts', SEPARATED_CONFIG); + tree.create( + '/package.json', + JSON.stringify({ + devDependencies: { + '@analogjs/platform': '^3.0.0-alpha.55', + '@analogjs/vite-plugin-angular': '^3.0.0-alpha.55', + nitro: '3.0.260415-beta', + }, + }), + ); + + const { context, infoLogs } = createContext(); + migrateToSeparatedPlugins()(tree, context); + + expect(infoLogs).toEqual([]); + }); +}); diff --git a/packages/platform/migrations/migrate-to-separated-plugins/migrate-to-separated-plugins.ts b/packages/platform/migrations/migrate-to-separated-plugins/migrate-to-separated-plugins.ts new file mode 100644 index 000000000..a24ccf7ab --- /dev/null +++ b/packages/platform/migrations/migrate-to-separated-plugins/migrate-to-separated-plugins.ts @@ -0,0 +1,135 @@ +import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics'; +import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks'; + +const ANALOG_PLATFORM_IMPORT = `from '@analogjs/platform'`; +const ANGULAR_PLUGIN_IMPORT = `from '@analogjs/vite-plugin-angular'`; +const NITRO_VITE_IMPORT = `from 'nitro/vite'`; +const ANGULAR_PLUGIN_PKG = '@analogjs/vite-plugin-angular'; +const NITRO_PKG = 'nitro'; +const NITRO_VERSION = '3.0.260415-beta'; +const MIGRATION_DOC_URL = + 'https://analogjs.org/docs/guides/migrating-v2-to-v3#analog-angular-and-nitro-are-now-separate-plugins'; + +const VITE_CONFIG_EXTENSIONS = ['.ts', '.mts', '.js', '.mjs']; + +function isViteConfig(filePath: string): boolean { + const file = filePath.slice(filePath.lastIndexOf('/') + 1); + if (!file.startsWith('vite.config.')) { + return false; + } + return VITE_CONFIG_EXTENSIONS.some((ext) => file.endsWith(ext)); +} + +function usesLegacyPluginShape(source: string): boolean { + if (!source.includes(ANALOG_PLATFORM_IMPORT)) { + return false; + } + if ( + source.includes(ANGULAR_PLUGIN_IMPORT) && + source.includes(NITRO_VITE_IMPORT) + ) { + return false; + } + return /\banalog\s*\(/.test(source); +} + +function readPackageJson( + tree: Tree, +): { raw: string; pkg: Record } | null { + const content = tree.read('/package.json'); + if (!content) return null; + const raw = content.toString('utf-8'); + try { + return { raw, pkg: JSON.parse(raw) }; + } catch { + return null; + } +} + +function getDepVersion( + pkg: Record, + name: string, +): string | undefined { + const dev = + (pkg['devDependencies'] as Record | undefined) ?? {}; + const reg = (pkg['dependencies'] as Record | undefined) ?? {}; + return dev[name] ?? reg[name]; +} + +function addDependencies(tree: Tree, context: SchematicContext): boolean { + const info = readPackageJson(tree); + if (!info) return false; + + const { pkg, raw } = info; + const devDeps = { + ...((pkg['devDependencies'] as Record | undefined) ?? {}), + }; + let changed = false; + + if (!getDepVersion(pkg, ANGULAR_PLUGIN_PKG)) { + // Match the @analogjs/platform pin so the angular plugin stays aligned + // with the rest of the Analog packages already in this workspace. + const platformVersion = getDepVersion(pkg, '@analogjs/platform') ?? '*'; + devDeps[ANGULAR_PLUGIN_PKG] = platformVersion; + changed = true; + context.logger.info( + `Added '${ANGULAR_PLUGIN_PKG}': '${platformVersion}' to devDependencies.`, + ); + } + + if (!getDepVersion(pkg, NITRO_PKG)) { + devDeps[NITRO_PKG] = NITRO_VERSION; + changed = true; + context.logger.info( + `Added '${NITRO_PKG}': '${NITRO_VERSION}' to devDependencies.`, + ); + } + + if (!changed) return false; + + pkg['devDependencies'] = devDeps; + const trailingNewline = raw.endsWith('\n') ? '\n' : ''; + tree.overwrite( + '/package.json', + JSON.stringify(pkg, null, 2) + trailingNewline, + ); + context.addTask(new NodePackageInstallTask()); + return true; +} + +export default function migrateToSeparatedPlugins(): Rule { + return (tree: Tree, context: SchematicContext) => { + const filesUsingLegacyShape: string[] = []; + + tree.visit((filePath) => { + if (filePath.includes('/node_modules/')) return; + if (!isViteConfig(filePath)) return; + + const content = tree.read(filePath); + if (!content) return; + + const source = content.toString('utf-8'); + if (usesLegacyPluginShape(source)) { + filesUsingLegacyShape.push(filePath); + } + }); + + if (filesUsingLegacyShape.length === 0) { + return tree; + } + + context.logger.info( + `Detected ${filesUsingLegacyShape.length} vite.config file(s) using the legacy single-call \`analog()\` plugin shape:`, + ); + for (const file of filesUsingLegacyShape) { + context.logger.info(` - ${file}`); + } + context.logger.info( + `Split \`analog()\` into \`analog() + angular() + nitro()\` and move each option to its owning plugin.\nSee ${MIGRATION_DOC_URL}`, + ); + + addDependencies(tree, context); + + return tree; + }; +} diff --git a/packages/platform/migrations/migration.json b/packages/platform/migrations/migration.json index 00b46c7ac..7bbffd6cb 100644 --- a/packages/platform/migrations/migration.json +++ b/packages/platform/migrations/migration.json @@ -1,4 +1,10 @@ { "$schema": "../../../node_modules/@angular-devkit/schematics/collection-schema.json", - "schematics": {} + "schematics": { + "migrate-to-separated-plugins": { + "version": "3.0.0-alpha.56", + "description": "Migrate Analog vite.config from a single analog() call to the separated analog() + angular() + nitro() plugin shape", + "factory": "./migrate-to-separated-plugins/migrate-to-separated-plugins" + } + } } From 99a009c99e744c7fa714802605ac52efe6afe7fd Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Sun, 24 May 2026 23:23:23 -0500 Subject: [PATCH 23/65] feat(platform): teach the separated-plugins schematic to move analog.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) --- .../migrate-to-separated-plugins.spec.ts | 186 ++++++++++++++ .../migrate-to-separated-plugins.ts | 226 +++++++++++++++++- 2 files changed, 402 insertions(+), 10 deletions(-) diff --git a/packages/platform/migrations/migrate-to-separated-plugins/migrate-to-separated-plugins.spec.ts b/packages/platform/migrations/migrate-to-separated-plugins/migrate-to-separated-plugins.spec.ts index 07286d9c6..bdb81f6ad 100644 --- a/packages/platform/migrations/migrate-to-separated-plugins/migrate-to-separated-plugins.spec.ts +++ b/packages/platform/migrations/migrate-to-separated-plugins/migrate-to-separated-plugins.spec.ts @@ -200,4 +200,190 @@ describe('migrate-to-separated-plugins', () => { expect(infoLogs).toEqual([]); }); + + describe('option transform', () => { + const PKG = JSON.stringify({ + devDependencies: { '@analogjs/platform': '^3.0.0-alpha.55' }, + }); + + it('lifts `vite: {...}` into a companion angular() call', () => { + tree.create( + '/vite.config.ts', + [ + `import analog from '@analogjs/platform';`, + `import { defineConfig } from 'vite';`, + ``, + `export default defineConfig(() => ({`, + ` plugins: [`, + ` analog({`, + ` apiPrefix: 'api',`, + ` vite: { fastCompile: true },`, + ` }),`, + ` ],`, + `}));`, + ].join('\n'), + ); + tree.create('/package.json', PKG); + + const { context } = createContext(); + migrateToSeparatedPlugins()(tree, context); + + const result = tree.readContent('/vite.config.ts'); + expect(result).toContain( + `import angular from '@analogjs/vite-plugin-angular';`, + ); + expect(result).toContain(`import { nitro } from 'nitro/vite';`); + expect(result).toContain(`angular({ fastCompile: true })`); + expect(result).toContain(`nitro()`); + expect(result).not.toContain(`vite: { fastCompile: true }`); + expect(result).toContain(`apiPrefix: 'api'`); + }); + + it('lifts `nitro: {...}` into a companion nitro() call', () => { + tree.create( + '/vite.config.ts', + [ + `import analog from '@analogjs/platform';`, + ``, + `export default {`, + ` plugins: [`, + ` analog({`, + ` apiPrefix: 'api',`, + ` nitro: { preset: 'node-server' },`, + ` }),`, + ` ],`, + `};`, + ].join('\n'), + ); + tree.create('/package.json', PKG); + + const { context } = createContext(); + migrateToSeparatedPlugins()(tree, context); + + const result = tree.readContent('/vite.config.ts'); + expect(result).toContain(`nitro({ preset: 'node-server' })`); + expect(result).toContain(`angular()`); + expect(result).not.toContain(`nitro: { preset: 'node-server' }`); + expect(result).toContain(`apiPrefix: 'api'`); + }); + + it('lifts both `vite` and `nitro` and keeps the remaining analog options', () => { + tree.create( + '/vite.config.ts', + [ + `import analog from '@analogjs/platform';`, + ``, + `export default {`, + ` plugins: [`, + ` analog({`, + ` apiPrefix: 'api',`, + ` ssr: true,`, + ` vite: { fastCompile: true, liveReload: true },`, + ` nitro: { routeRules: { '/admin/**': { ssr: false } } },`, + ` prerender: { routes: ['/'] },`, + ` }),`, + ` ],`, + `};`, + ].join('\n'), + ); + tree.create('/package.json', PKG); + + const { context } = createContext(); + migrateToSeparatedPlugins()(tree, context); + + const result = tree.readContent('/vite.config.ts'); + expect(result).toContain( + `angular({ fastCompile: true, liveReload: true })`, + ); + expect(result).toContain( + `nitro({ routeRules: { '/admin/**': { ssr: false } } })`, + ); + // Remaining analog options stay on analog(). + expect(result).toMatch(/analog\(\{[\s\S]*apiPrefix: 'api'/); + expect(result).toMatch(/analog\(\{[\s\S]*ssr: true/); + expect(result).toMatch( + /analog\(\{[\s\S]*prerender: \{ routes: \['\/'\] \}/, + ); + // The `vite` and `nitro` keys are gone from the analog literal. + expect(result).not.toMatch(/vite:\s*\{/); + // The `nitro: {` form is gone; only `nitro({` remains. + expect(result).not.toMatch(/nitro:\s*\{/); + }); + + it('falls back to logging instructions when analog() has no argument', () => { + tree.create( + '/vite.config.ts', + [ + `import analog from '@analogjs/platform';`, + ``, + `export default {`, + ` plugins: [analog()],`, + `};`, + ].join('\n'), + ); + tree.create('/package.json', PKG); + + const { context, infoLogs } = createContext(); + migrateToSeparatedPlugins()(tree, context); + + const result = tree.readContent('/vite.config.ts'); + // Source untouched. + expect(result).toContain(`plugins: [analog()]`); + // But user gets pointed at the doc. + expect(infoLogs.join('\n')).toContain(MIGRATION_DOC_PHRASE); + }); + + it('does not rewrite when the analog() call argument is not an object literal', () => { + tree.create( + '/vite.config.ts', + [ + `import analog from '@analogjs/platform';`, + ``, + `const opts = { apiPrefix: 'api' };`, + `export default { plugins: [analog(opts)] };`, + ].join('\n'), + ); + tree.create('/package.json', PKG); + + const { context, infoLogs } = createContext(); + migrateToSeparatedPlugins()(tree, context); + + const result = tree.readContent('/vite.config.ts'); + // Source untouched because we can't safely split a variable reference. + expect(result).toContain(`plugins: [analog(opts)]`); + // Doc link is logged. + expect(infoLogs.join('\n')).toContain(MIGRATION_DOC_PHRASE); + }); + + it('does not duplicate the angular/nitro imports when they are already present', () => { + tree.create( + '/vite.config.ts', + [ + `import analog from '@analogjs/platform';`, + `import angular from '@analogjs/vite-plugin-angular';`, + ``, + `export default {`, + ` plugins: [`, + ` analog({ apiPrefix: 'api', vite: { fastCompile: true } }),`, + ` angular({ liveReload: true }),`, + ` ],`, + `};`, + ].join('\n'), + ); + tree.create('/package.json', PKG); + + const { context } = createContext(); + migrateToSeparatedPlugins()(tree, context); + + const result = tree.readContent('/vite.config.ts'); + const angularImports = result.match( + /import angular from '@analogjs\/vite-plugin-angular';/g, + ); + expect(angularImports?.length).toBe(1); + // The nitro import is still added since it wasn't there. + expect(result).toContain(`import { nitro } from 'nitro/vite';`); + }); + }); }); + +const MIGRATION_DOC_PHRASE = 'migrating-v2-to-v3'; diff --git a/packages/platform/migrations/migrate-to-separated-plugins/migrate-to-separated-plugins.ts b/packages/platform/migrations/migrate-to-separated-plugins/migrate-to-separated-plugins.ts index a24ccf7ab..d30e87681 100644 --- a/packages/platform/migrations/migrate-to-separated-plugins/migrate-to-separated-plugins.ts +++ b/packages/platform/migrations/migrate-to-separated-plugins/migrate-to-separated-plugins.ts @@ -1,5 +1,6 @@ import { Rule, SchematicContext, Tree } from '@angular-devkit/schematics'; import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks'; +import * as ts from 'typescript'; const ANALOG_PLATFORM_IMPORT = `from '@analogjs/platform'`; const ANGULAR_PLUGIN_IMPORT = `from '@analogjs/vite-plugin-angular'`; @@ -97,9 +98,192 @@ function addDependencies(tree: Tree, context: SchematicContext): boolean { return true; } +interface TransformResult { + source: string; + movedVite: boolean; + movedNitro: boolean; +} + +interface Edit { + start: number; + end: number; + text: string; +} + +function applyEdits(source: string, edits: Edit[]): string { + // Apply right-to-left so earlier offsets stay valid. + const sorted = [...edits].sort((a, b) => b.start - a.start); + let result = source; + for (const edit of sorted) { + result = result.slice(0, edit.start) + edit.text + result.slice(edit.end); + } + return result; +} + +function getLineIndent(source: string, pos: number): string { + let lineStart = pos; + while (lineStart > 0 && source[lineStart - 1] !== '\n') { + lineStart--; + } + let indent = ''; + for (let i = lineStart; i < source.length; i++) { + const ch = source[i]; + if (ch === ' ' || ch === '\t') indent += ch; + else break; + } + return indent; +} + +/** + * Tries to lift `vite: {...}` and `nitro: {...}` properties off the + * single `analog(...)` call into companion `angular(...)` / `nitro(...)` + * calls. Returns the rewritten source on success, or null if the file + * doesn't match the supported pattern (we then fall back to logging + * instructions rather than risking a corrupted file). + * + * Supported pattern: exactly one `analog(...)` call whose argument is + * an object literal. Properties named `vite` and `nitro` are recognized; + * everything else stays on `analog`. + */ +function tryTransformViteConfig( + filePath: string, + source: string, +): TransformResult | null { + let sourceFile: ts.SourceFile; + try { + sourceFile = ts.createSourceFile( + filePath, + source, + ts.ScriptTarget.Latest, + true, + ts.ScriptKind.TS, + ); + } catch { + return null; + } + + let analogImportEnd = -1; + let angularImported = false; + let nitroImported = false; + let analogCalls: ts.CallExpression[] = []; + + function visit(node: ts.Node): void { + if (ts.isImportDeclaration(node)) { + const ml = node.moduleSpecifier; + if (ts.isStringLiteral(ml)) { + if (ml.text === '@analogjs/platform') { + analogImportEnd = node.end; + } else if (ml.text === '@analogjs/vite-plugin-angular') { + angularImported = true; + } else if (ml.text === 'nitro/vite') { + nitroImported = true; + } + } + } else if (ts.isCallExpression(node)) { + if ( + ts.isIdentifier(node.expression) && + node.expression.text === 'analog' + ) { + analogCalls.push(node); + } + } + ts.forEachChild(node, visit); + } + visit(sourceFile); + + if (analogImportEnd === -1) return null; + if (analogCalls.length !== 1) return null; + + const analogCall = analogCalls[0]; + const callStart = analogCall.getStart(sourceFile); + const callEnd = analogCall.getEnd(); + + let viteValueText: string | null = null; + let nitroValueText: string | null = null; + let remainingPropsText: string | null = null; + + const arg = analogCall.arguments[0]; + if (arg && ts.isObjectLiteralExpression(arg)) { + const remaining: ts.ObjectLiteralElementLike[] = []; + for (const prop of arg.properties) { + if ( + ts.isPropertyAssignment(prop) && + ts.isIdentifier(prop.name) && + (prop.name.text === 'vite' || prop.name.text === 'nitro') + ) { + const value = prop.initializer; + const text = source.slice(value.getStart(sourceFile), value.getEnd()); + if (prop.name.text === 'vite') viteValueText = text; + else nitroValueText = text; + } else { + remaining.push(prop); + } + } + + if (viteValueText === null && nitroValueText === null) return null; + + if (remaining.length > 0) { + // Reconstruct the analog object literal from the remaining props, + // preserving their original source text (comments, spacing). + const propTexts = remaining.map((p) => + source.slice(p.getStart(sourceFile), p.getEnd()), + ); + const objStart = arg.getStart(sourceFile); + const indent = getLineIndent(source, objStart); + const propIndent = indent + ' '; + remainingPropsText = `{\n${propTexts.map((t) => `${propIndent}${t}`).join(',\n')},\n${indent}}`; + } + } else if (arg === undefined) { + // analog() with no options — nothing to move. + return null; + } else { + return null; + } + + if (viteValueText === null && nitroValueText === null) return null; + + const indent = getLineIndent(source, callStart); + const newAnalogCall = remainingPropsText + ? `analog(${remainingPropsText})` + : `analog()`; + const parts: string[] = [newAnalogCall]; + if (viteValueText !== null) parts.push(`angular(${viteValueText})`); + else parts.push('angular()'); + if (nitroValueText !== null) parts.push(`nitro(${nitroValueText})`); + else parts.push('nitro()'); + const replacement = parts.join(`,\n${indent}`); + + const edits: Edit[] = [{ start: callStart, end: callEnd, text: replacement }]; + + const importsToAdd: string[] = []; + if (!angularImported) { + importsToAdd.push(`import angular from '@analogjs/vite-plugin-angular';`); + } + if (!nitroImported) { + importsToAdd.push(`import { nitro } from 'nitro/vite';`); + } + if (importsToAdd.length > 0) { + edits.push({ + start: analogImportEnd, + end: analogImportEnd, + text: `\n${importsToAdd.join('\n')}`, + }); + } + + const rewritten = applyEdits(source, edits); + + return { + source: rewritten, + movedVite: viteValueText !== null, + movedNitro: nitroValueText !== null, + }; +} + export default function migrateToSeparatedPlugins(): Rule { return (tree: Tree, context: SchematicContext) => { const filesUsingLegacyShape: string[] = []; + const rewrittenFiles: string[] = []; + const unhandledFiles: string[] = []; tree.visit((filePath) => { if (filePath.includes('/node_modules/')) return; @@ -109,8 +293,16 @@ export default function migrateToSeparatedPlugins(): Rule { if (!content) return; const source = content.toString('utf-8'); - if (usesLegacyPluginShape(source)) { - filesUsingLegacyShape.push(filePath); + if (!usesLegacyPluginShape(source)) return; + + filesUsingLegacyShape.push(filePath); + + const transformed = tryTransformViteConfig(filePath, source); + if (transformed) { + tree.overwrite(filePath, transformed.source); + rewrittenFiles.push(filePath); + } else { + unhandledFiles.push(filePath); } }); @@ -118,15 +310,29 @@ export default function migrateToSeparatedPlugins(): Rule { return tree; } - context.logger.info( - `Detected ${filesUsingLegacyShape.length} vite.config file(s) using the legacy single-call \`analog()\` plugin shape:`, - ); - for (const file of filesUsingLegacyShape) { - context.logger.info(` - ${file}`); + if (rewrittenFiles.length > 0) { + context.logger.info( + `Rewrote ${rewrittenFiles.length} vite.config file(s) to the separated \`analog() + angular() + nitro()\` shape:`, + ); + for (const file of rewrittenFiles) { + context.logger.info(` - ${file}`); + } + context.logger.info( + `Review the result — only \`vite\` and \`nitro\` were moved automatically. Other angular-passthrough options (\`liveReload\`, \`fastCompile\`, \`fileReplacements\`, \`tailwindCss\`, etc.) still need to be relocated by hand.\nSee ${MIGRATION_DOC_URL}`, + ); + } + + if (unhandledFiles.length > 0) { + context.logger.info( + `Could not safely rewrite ${unhandledFiles.length} vite.config file(s); migrate them by hand:`, + ); + for (const file of unhandledFiles) { + context.logger.info(` - ${file}`); + } + context.logger.info( + `Split \`analog()\` into \`analog() + angular() + nitro()\` and move each option to its owning plugin.\nSee ${MIGRATION_DOC_URL}`, + ); } - context.logger.info( - `Split \`analog()\` into \`analog() + angular() + nitro()\` and move each option to its owning plugin.\nSee ${MIGRATION_DOC_URL}`, - ); addDependencies(tree, context); From 3725bd10537e3d359ebb93fc0d981a7e2c944176 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Sun, 24 May 2026 23:26:02 -0500 Subject: [PATCH 24/65] feat(platform): rewrite argumentless analog() to analog() + angular() + 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) --- .../migrate-to-separated-plugins.spec.ts | 38 ++++++++++++++++--- .../migrate-to-separated-plugins.ts | 13 +++---- 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/packages/platform/migrations/migrate-to-separated-plugins/migrate-to-separated-plugins.spec.ts b/packages/platform/migrations/migrate-to-separated-plugins/migrate-to-separated-plugins.spec.ts index bdb81f6ad..e4e81edfa 100644 --- a/packages/platform/migrations/migrate-to-separated-plugins/migrate-to-separated-plugins.spec.ts +++ b/packages/platform/migrations/migrate-to-separated-plugins/migrate-to-separated-plugins.spec.ts @@ -310,7 +310,7 @@ describe('migrate-to-separated-plugins', () => { expect(result).not.toMatch(/nitro:\s*\{/); }); - it('falls back to logging instructions when analog() has no argument', () => { + it('rewrites analog() with no argument into analog(), angular(), nitro()', () => { tree.create( '/vite.config.ts', [ @@ -323,14 +323,40 @@ describe('migrate-to-separated-plugins', () => { ); tree.create('/package.json', PKG); - const { context, infoLogs } = createContext(); + const { context } = createContext(); migrateToSeparatedPlugins()(tree, context); const result = tree.readContent('/vite.config.ts'); - // Source untouched. - expect(result).toContain(`plugins: [analog()]`); - // But user gets pointed at the doc. - expect(infoLogs.join('\n')).toContain(MIGRATION_DOC_PHRASE); + expect(result).toContain( + `import angular from '@analogjs/vite-plugin-angular';`, + ); + expect(result).toContain(`import { nitro } from 'nitro/vite';`); + expect(result).toMatch(/analog\(\),\s+angular\(\),\s+nitro\(\)/); + }); + + it('rewrites analog({ apiPrefix }) (no vite/nitro keys) and adds empty companion plugins', () => { + tree.create( + '/vite.config.ts', + [ + `import analog from '@analogjs/platform';`, + ``, + `export default {`, + ` plugins: [`, + ` analog({ apiPrefix: 'api' }),`, + ` ],`, + `};`, + ].join('\n'), + ); + tree.create('/package.json', PKG); + + const { context } = createContext(); + migrateToSeparatedPlugins()(tree, context); + + const result = tree.readContent('/vite.config.ts'); + expect(result).toContain(`analog({`); + expect(result).toContain(`apiPrefix: 'api'`); + expect(result).toContain(`angular()`); + expect(result).toContain(`nitro()`); }); it('does not rewrite when the analog() call argument is not an object literal', () => { diff --git a/packages/platform/migrations/migrate-to-separated-plugins/migrate-to-separated-plugins.ts b/packages/platform/migrations/migrate-to-separated-plugins/migrate-to-separated-plugins.ts index d30e87681..91a8b8a46 100644 --- a/packages/platform/migrations/migrate-to-separated-plugins/migrate-to-separated-plugins.ts +++ b/packages/platform/migrations/migrate-to-separated-plugins/migrate-to-separated-plugins.ts @@ -220,8 +220,6 @@ function tryTransformViteConfig( } } - if (viteValueText === null && nitroValueText === null) return null; - if (remaining.length > 0) { // Reconstruct the analog object literal from the remaining props, // preserving their original source text (comments, spacing). @@ -233,14 +231,13 @@ function tryTransformViteConfig( const propIndent = indent + ' '; remainingPropsText = `{\n${propTexts.map((t) => `${propIndent}${t}`).join(',\n')},\n${indent}}`; } - } else if (arg === undefined) { - // analog() with no options — nothing to move. - return null; - } else { + } else if (arg !== undefined) { + // analog(otherExpression) — can't statically split a variable or call + // expression. Fall back to logging instructions. return null; } - - if (viteValueText === null && nitroValueText === null) return null; + // analog() with no argument falls through to the rewrite below; we just + // emit `analog(), angular(), nitro()` to scaffold the new plugin chain. const indent = getLineIndent(source, callStart); const newAnalogCall = remainingPropsText From 5f5ce88baacd767c63ee604d00aba9e5dcab29ca Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Sun, 24 May 2026 23:31:45 -0500 Subject: [PATCH 25/65] feat(vite-plugin-angular): fall back to NX_WORKSPACE_ROOT before process.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//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) --- packages/vite-plugin-angular/src/lib/angular-vite-plugin.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/vite-plugin-angular/src/lib/angular-vite-plugin.ts b/packages/vite-plugin-angular/src/lib/angular-vite-plugin.ts index 4bb0362bb..30cf8fbc7 100644 --- a/packages/vite-plugin-angular/src/lib/angular-vite-plugin.ts +++ b/packages/vite-plugin-angular/src/lib/angular-vite-plugin.ts @@ -340,7 +340,10 @@ export function angular(options?: PluginOptions): Plugin[] { */ const pluginOptions = { tsconfigGetter: createTsConfigGetter(options?.tsconfig), - workspaceRoot: options?.workspaceRoot ?? process.cwd(), + workspaceRoot: + options?.workspaceRoot ?? + process.env['NX_WORKSPACE_ROOT'] ?? + process.cwd(), inlineStylesExtension: options?.inlineStylesExtension ?? 'css', advanced: { tsTransformers: { From 053705747091f8731613d8ebb7bae1a94d672db6 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Sun, 24 May 2026 23:31:57 -0500 Subject: [PATCH 26/65] chore: drop explicit workspaceRoot from app vite configs @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 : 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) --- apps/analog-app/vite.config.ts | 1 - apps/blog-app/vite.config.ts | 1 - apps/opt-catchall-app/vite.config.ts | 1 - apps/tailwind-debug-app/vite.config.ts | 1 - apps/tanstack-query-app/vite.config.ts | 4 +--- 5 files changed, 1 insertion(+), 7 deletions(-) diff --git a/apps/analog-app/vite.config.ts b/apps/analog-app/vite.config.ts index 6d395b0be..88d77e0b4 100644 --- a/apps/analog-app/vite.config.ts +++ b/apps/analog-app/vite.config.ts @@ -96,7 +96,6 @@ export default defineConfig(async ({ mode, command }) => { }, }), angular({ - workspaceRoot: resolve(__dirname, '../..'), include: [ ...explicitLibPages, ...pageGlobs(discoveredLibs.additionalPagesDirs), diff --git a/apps/blog-app/vite.config.ts b/apps/blog-app/vite.config.ts index 766c56021..6950c5a91 100644 --- a/apps/blog-app/vite.config.ts +++ b/apps/blog-app/vite.config.ts @@ -91,7 +91,6 @@ export default defineConfig(() => { }, }), angular({ - workspaceRoot: resolve(__dirname, '../..'), liveReload: true, }), nitro({ diff --git a/apps/opt-catchall-app/vite.config.ts b/apps/opt-catchall-app/vite.config.ts index adccfbdf8..a837397bf 100644 --- a/apps/opt-catchall-app/vite.config.ts +++ b/apps/opt-catchall-app/vite.config.ts @@ -35,7 +35,6 @@ export default defineConfig(() => { }, }), angular({ - workspaceRoot: resolve(__dirname, '../..'), liveReload: true, experimental: { useAngularCompilationAPI: true, diff --git a/apps/tailwind-debug-app/vite.config.ts b/apps/tailwind-debug-app/vite.config.ts index 545c3b628..476bc69ab 100644 --- a/apps/tailwind-debug-app/vite.config.ts +++ b/apps/tailwind-debug-app/vite.config.ts @@ -114,7 +114,6 @@ export default defineConfig(({ mode }) => ({ ssr: false, }), angular({ - workspaceRoot: resolve(__dirname, '../..'), experimental: { // Required to reproduce #2293: @apply inside :host with Tailwind // prefix configuration requires the Angular Compilation API path diff --git a/apps/tanstack-query-app/vite.config.ts b/apps/tanstack-query-app/vite.config.ts index 3ad16de5f..bbb00f64a 100644 --- a/apps/tanstack-query-app/vite.config.ts +++ b/apps/tanstack-query-app/vite.config.ts @@ -33,9 +33,7 @@ export default defineConfig(({ mode }) => { analog({ apiPrefix: 'api', }), - angular({ - workspaceRoot: resolve(__dirname, '../..'), - }), + angular(), nitro({}), ], test: { From 5eb6a83c5b6214920c301534ba186ca03ad12bc4 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Mon, 25 May 2026 11:06:36 -0500 Subject: [PATCH 27/65] fix: drop srvx@0.11.15 patch; no longer required 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> through the SSR handler path. Subsequent SSR-renderer refactors on this branch removed the second promise layer: - 7b938f705 fix(platform): install analog-owned SSR renderer to bypass Nitro auto-template - 16e73ab93 fix(platform): use Nitro virtual indirection for SSR dispatch instead of fetchViteEnv - d9d842ae2 fix(platform): gate SSR renderer + add Nitro externals The renderer virtual now returns service.fetch(event.req) directly, which is a single-level Promise; 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. --- patches/srvx@0.11.15.patch | 22 ---------------------- pnpm-lock.yaml | 27 +++++++++++---------------- pnpm-workspace.yaml | 8 -------- 3 files changed, 11 insertions(+), 46 deletions(-) delete mode 100644 patches/srvx@0.11.15.patch diff --git a/patches/srvx@0.11.15.patch b/patches/srvx@0.11.15.patch deleted file mode 100644 index 92ce31104..000000000 --- a/patches/srvx@0.11.15.patch +++ /dev/null @@ -1,22 +0,0 @@ -diff --git a/dist/adapters/node.mjs b/dist/adapters/node.mjs -index bf860db..845af0b 100644 ---- a/dist/adapters/node.mjs -+++ b/dist/adapters/node.mjs -@@ -717,7 +717,7 @@ function toNodeHandler(handler) { - req: nodeReq, - res: nodeRes - })); -- return res instanceof Promise ? res.then((resolvedRes) => sendNodeResponse(nodeRes, resolvedRes)) : sendNodeResponse(nodeRes, res); -+ return (res && typeof res.then === 'function') ? res.then((resolvedRes) => (resolvedRes && typeof resolvedRes.then === 'function') ? resolvedRes.then((r) => sendNodeResponse(nodeRes, r)) : sendNodeResponse(nodeRes, resolvedRes)) : sendNodeResponse(nodeRes, res); - } - convertedNodeHandler.__fetchHandler = handler; - assignFnName(convertedNodeHandler, handler, " (converted to Node handler)"); -@@ -772,7 +772,7 @@ var NodeServer = class { - }); - request.waitUntil = this.#wait?.waitUntil; - const res = fetchHandler(request); -- return res instanceof Promise ? res.then((resolvedRes) => sendNodeResponse(nodeRes, resolvedRes)) : sendNodeResponse(nodeRes, res); -+ return (res && typeof res.then === 'function') ? res.then((resolvedRes) => (resolvedRes && typeof resolvedRes.then === 'function') ? resolvedRes.then((r) => sendNodeResponse(nodeRes, r)) : sendNodeResponse(nodeRes, resolvedRes)) : sendNodeResponse(nodeRes, res); - }; - this.node = { - handler, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 04fc32882..716109ac7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -597,11 +597,6 @@ overrides: '@angular/compiler-cli': 21.2.8 '@angular/language-service': 21.2.8 -patchedDependencies: - srvx@0.11.15: - hash: 12566e1018bec67b6844444c1857232a989b8470cc6b12872dc3ce2900863591 - path: patches/srvx@0.11.15.patch - importers: .: @@ -27006,9 +27001,9 @@ snapshots: dependencies: uncrypto: 0.1.3 - crossws@0.4.5(srvx@0.11.15(patch_hash=12566e1018bec67b6844444c1857232a989b8470cc6b12872dc3ce2900863591)): + crossws@0.4.5(srvx@0.11.15): optionalDependencies: - srvx: 0.11.15(patch_hash=12566e1018bec67b6844444c1857232a989b8470cc6b12872dc3ce2900863591) + srvx: 0.11.15 crypto-random-string@4.0.0: dependencies: @@ -27786,10 +27781,10 @@ snapshots: env-runner@0.1.7: dependencies: - crossws: 0.4.5(srvx@0.11.15(patch_hash=12566e1018bec67b6844444c1857232a989b8470cc6b12872dc3ce2900863591)) + crossws: 0.4.5(srvx@0.11.15) exsolve: 1.0.8 httpxy: 0.5.0 - srvx: 0.11.15(patch_hash=12566e1018bec67b6844444c1857232a989b8470cc6b12872dc3ce2900863591) + srvx: 0.11.15 environment@1.1.0: {} @@ -28762,12 +28757,12 @@ snapshots: ufo: 1.6.3 uncrypto: 0.1.3 - h3@2.0.1-rc.20(crossws@0.4.5(srvx@0.11.15(patch_hash=12566e1018bec67b6844444c1857232a989b8470cc6b12872dc3ce2900863591))): + h3@2.0.1-rc.20(crossws@0.4.5(srvx@0.11.15)): dependencies: rou3: 0.8.1 - srvx: 0.11.15(patch_hash=12566e1018bec67b6844444c1857232a989b8470cc6b12872dc3ce2900863591) + srvx: 0.11.15 optionalDependencies: - crossws: 0.4.5(srvx@0.11.15(patch_hash=12566e1018bec67b6844444c1857232a989b8470cc6b12872dc3ce2900863591)) + crossws: 0.4.5(srvx@0.11.15) hachure-fill@0.5.2: {} @@ -31179,17 +31174,17 @@ snapshots: nitro@3.0.260415-beta(chokidar@5.0.0)(dotenv@16.4.7)(jiti@2.6.1)(lru-cache@11.3.5)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)): dependencies: consola: 3.4.2 - crossws: 0.4.5(srvx@0.11.15(patch_hash=12566e1018bec67b6844444c1857232a989b8470cc6b12872dc3ce2900863591)) + crossws: 0.4.5(srvx@0.11.15) db0: 0.3.4 env-runner: 0.1.7 - h3: 2.0.1-rc.20(crossws@0.4.5(srvx@0.11.15(patch_hash=12566e1018bec67b6844444c1857232a989b8470cc6b12872dc3ce2900863591))) + h3: 2.0.1-rc.20(crossws@0.4.5(srvx@0.11.15)) hookable: 6.1.1 nf3: 0.3.16 ocache: 0.1.4 ofetch: 2.0.0-alpha.3 ohash: 2.0.11 rolldown: 1.0.0-rc.15 - srvx: 0.11.15(patch_hash=12566e1018bec67b6844444c1857232a989b8470cc6b12872dc3ce2900863591) + srvx: 0.11.15 unenv: 2.0.0-rc.24 unstorage: 2.0.0-alpha.7(chokidar@5.0.0)(db0@0.3.4)(lru-cache@11.3.5)(ofetch@2.0.0-alpha.3) optionalDependencies: @@ -34247,7 +34242,7 @@ snapshots: srcset@4.0.0: {} - srvx@0.11.15(patch_hash=12566e1018bec67b6844444c1857232a989b8470cc6b12872dc3ce2900863591): {} + srvx@0.11.15: {} ssri@13.0.1: dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index b678269aa..a4593bf65 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -18,14 +18,6 @@ packages: # Integration / E2E test projects that need their own workspace deps. - 'tests/*' -patchedDependencies: - # srvx's toNodeHandler awaits only one level of promise; nitro/h3's fetch - # chain returns Promise>, so without this patch the - # outer promise leaks through as the "response" and srvx's - # sendNodeResponse throws `webRes.headers is not iterable`. Backport of - # the fix from analogjs/analog#2188. - srvx@0.11.15: patches/srvx@0.11.15.patch - catalog: # Keep the Angular runtime/compiler patch set aligned so compiler-cli, # language-service, and the framework runtime all resolve the same release. From c0aa081e9ffcdbdf12b1aeada745a3b8c2f38e97 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Mon, 25 May 2026 12:26:54 -0500 Subject: [PATCH 28/65] feat(vite-plugin-angular): expose architect builders as subpath exports 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. --- packages/vite-plugin-angular/package.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/vite-plugin-angular/package.json b/packages/vite-plugin-angular/package.json index 95bef33be..903cc563e 100644 --- a/packages/vite-plugin-angular/package.json +++ b/packages/vite-plugin-angular/package.json @@ -93,6 +93,16 @@ "import": "./dist/src/index.js", "require": "./dist/src/index.js", "default": "./dist/src/index.js" + }, + "./builders/vite": { + "import": "./dist/src/lib/tools/src/builders/vite/vite-build.impl.js", + "require": "./dist/src/lib/tools/src/builders/vite/vite-build.impl.js", + "default": "./dist/src/lib/tools/src/builders/vite/vite-build.impl.js" + }, + "./builders/vite-dev-server": { + "import": "./dist/src/lib/tools/src/builders/vite-dev-server/dev-server.impl.js", + "require": "./dist/src/lib/tools/src/builders/vite-dev-server/dev-server.impl.js", + "default": "./dist/src/lib/tools/src/builders/vite-dev-server/dev-server.impl.js" } }, "publishConfig": { From 1a59b6a5e502afff87f83ef583c0d501ca1a055d Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Mon, 25 May 2026 12:27:13 -0500 Subject: [PATCH 29/65] feat(platform)!: ship architect builder shims for @analogjs/platform: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. --- packages/nx-plugin/builders.json | 12 +- packages/nx-plugin/executors.json | 17 +- .../builders/vite-dev-server/dev-server.ts | 8 + .../src/builders/vite-dev-server/schema.json | 25 +++ .../nx-plugin/src/builders/vite/schema.json | 39 +++++ .../nx-plugin/src/builders/vite/vite-build.ts | 12 ++ .../nx-plugin/src/executors/vite/compat.ts | 7 - .../nx-plugin/src/executors/vite/schema.d.ts | 2 - .../nx-plugin/src/executors/vite/schema.json | 147 ------------------ .../nx-plugin/src/executors/vite/vite.impl.ts | 3 - packages/nx-plugin/vite.config.lib.ts | 12 +- 11 files changed, 111 insertions(+), 173 deletions(-) create mode 100644 packages/nx-plugin/src/builders/vite-dev-server/dev-server.ts create mode 100644 packages/nx-plugin/src/builders/vite-dev-server/schema.json create mode 100644 packages/nx-plugin/src/builders/vite/schema.json create mode 100644 packages/nx-plugin/src/builders/vite/vite-build.ts delete mode 100644 packages/nx-plugin/src/executors/vite/compat.ts delete mode 100644 packages/nx-plugin/src/executors/vite/schema.d.ts delete mode 100644 packages/nx-plugin/src/executors/vite/schema.json delete mode 100644 packages/nx-plugin/src/executors/vite/vite.impl.ts diff --git a/packages/nx-plugin/builders.json b/packages/nx-plugin/builders.json index 3eaec9e31..d851afe65 100644 --- a/packages/nx-plugin/builders.json +++ b/packages/nx-plugin/builders.json @@ -1,7 +1,15 @@ { "builders": { - "vite-dev-server": "@analogjs/vite-plugin-angular:vite-dev-server", - "vite": "@analogjs/vite-plugin-angular:vite", + "vite-dev-server": { + "implementation": "./src/builders/vite-dev-server/dev-server.js", + "schema": "./src/builders/vite-dev-server/schema.json", + "description": "Vite dev server." + }, + "vite": { + "implementation": "./src/builders/vite/vite-build.js", + "schema": "./src/builders/vite/schema.json", + "description": "Build with Vite." + }, "vitest": "@analogjs/vitest-angular:test" } } diff --git a/packages/nx-plugin/executors.json b/packages/nx-plugin/executors.json index 06935b94a..c0166c990 100644 --- a/packages/nx-plugin/executors.json +++ b/packages/nx-plugin/executors.json @@ -1,7 +1,15 @@ { "builders": { - "vite-dev-server": "@analogjs/vite-plugin-angular:vite-dev-server", - "vite": "@analogjs/vite-plugin-angular:vite", + "vite-dev-server": { + "implementation": "./src/builders/vite-dev-server/dev-server.js", + "schema": "./src/builders/vite-dev-server/schema.json", + "description": "Vite dev server." + }, + "vite": { + "implementation": "./src/builders/vite/vite-build.js", + "schema": "./src/builders/vite/schema.json", + "description": "Build with Vite." + }, "vitest": "@analogjs/vitest-angular:test" }, "executors": { @@ -10,11 +18,6 @@ "schema": "./src/executors/vite-dev-server/schema.json", "description": "Vite dev server." }, - "vite": { - "implementation": "./src/executors/vite/vite.impl", - "schema": "./src/executors/vite/schema.json", - "description": "Build with Vite." - }, "vitest": { "implementation": "./src/executors/vitest/vitest.impl", "schema": "./src/executors/vitest/schema.json", diff --git a/packages/nx-plugin/src/builders/vite-dev-server/dev-server.ts b/packages/nx-plugin/src/builders/vite-dev-server/dev-server.ts new file mode 100644 index 000000000..ffdbe8fb3 --- /dev/null +++ b/packages/nx-plugin/src/builders/vite-dev-server/dev-server.ts @@ -0,0 +1,8 @@ +/** + * Thin shim that re-exports the Angular Devkit Architect dev-server builder + * shipped by `@analogjs/vite-plugin-angular`. See `../vite/vite-build.ts` for + * the rationale; mirrors `@analogjs/storybook-angular`'s pattern. + */ +import devServerBuilder from '@analogjs/vite-plugin-angular/builders/vite-dev-server'; + +export default devServerBuilder; diff --git a/packages/nx-plugin/src/builders/vite-dev-server/schema.json b/packages/nx-plugin/src/builders/vite-dev-server/schema.json new file mode 100644 index 000000000..fa6f246d6 --- /dev/null +++ b/packages/nx-plugin/src/builders/vite-dev-server/schema.json @@ -0,0 +1,25 @@ +{ + "version": 2, + "outputCapture": "direct-nodejs", + "title": "Vite Dev Server", + "description": "Starts a dev server using Vite.", + "type": "object", + "properties": { + "buildTarget": { + "type": "string", + "description": "Target which builds the application. Only used to retrieve the configuration as the dev-server does not build the code.", + "x-priority": "important" + }, + "port": { + "type": "number", + "description": "Port to listen on.", + "x-priority": "important" + }, + "hmr": { + "description": "Enable hot module replacement. For more options, use the 'hmr' option in the Vite configuration file.", + "type": "boolean" + } + }, + "definitions": {}, + "required": ["buildTarget"] +} diff --git a/packages/nx-plugin/src/builders/vite/schema.json b/packages/nx-plugin/src/builders/vite/schema.json new file mode 100644 index 000000000..c96d6dfbd --- /dev/null +++ b/packages/nx-plugin/src/builders/vite/schema.json @@ -0,0 +1,39 @@ +{ + "version": 2, + "outputCapture": "direct-nodejs", + "title": "Vite Prod Builder", + "cli": "nx", + "description": "Builds a Vite.js application for production.", + "type": "object", + "properties": { + "outputPath": { + "type": "string", + "description": "The output path of the generated files.", + "x-completion-type": "directory", + "x-priority": "important" + }, + "configFile": { + "type": "string", + "description": "The name of the Vite.js configuration file.", + "x-completion-type": "file", + "x-completion-glob": "vite.config.@(js|ts)" + }, + "sourcemap": { + "description": "Output sourcemaps. Use 'hidden' for use with error reporting tools without generating sourcemap comment.", + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "string" + } + ] + }, + "mode": { + "type": "string", + "description": "Mode to run the build in." + } + }, + "definitions": {}, + "required": ["outputPath"] +} diff --git a/packages/nx-plugin/src/builders/vite/vite-build.ts b/packages/nx-plugin/src/builders/vite/vite-build.ts new file mode 100644 index 000000000..473b1a0f5 --- /dev/null +++ b/packages/nx-plugin/src/builders/vite/vite-build.ts @@ -0,0 +1,12 @@ +/** + * Thin shim that re-exports the Angular Devkit Architect build builder + * shipped by `@analogjs/vite-plugin-angular`. Architect resolves the + * implementation from the local `builders.json` entry without traversing a + * cross-package string redirect, so the user-facing `@analogjs/platform:vite` + * identifier stays stable while the real implementation continues to live in + * `@analogjs/vite-plugin-angular`. Mirrors `@analogjs/storybook-angular`'s + * pattern for surfacing `@storybook/angular`'s builders. + */ +import viteBuilder from '@analogjs/vite-plugin-angular/builders/vite'; + +export default viteBuilder; diff --git a/packages/nx-plugin/src/executors/vite/compat.ts b/packages/nx-plugin/src/executors/vite/compat.ts deleted file mode 100644 index 218bebba5..000000000 --- a/packages/nx-plugin/src/executors/vite/compat.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { convertNxExecutor } from '@nx/devkit'; - -import viteBuildExecutor from './vite.impl'; - -const compat: ReturnType = - convertNxExecutor(viteBuildExecutor); -export default compat; diff --git a/packages/nx-plugin/src/executors/vite/schema.d.ts b/packages/nx-plugin/src/executors/vite/schema.d.ts deleted file mode 100644 index bd161aeb9..000000000 --- a/packages/nx-plugin/src/executors/vite/schema.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -import type { FileReplacement } from '@nx/vite/plugins/rollup-replace-files.plugin'; -export type { ViteBuildExecutorOptions } from '@nx/vite/executors'; diff --git a/packages/nx-plugin/src/executors/vite/schema.json b/packages/nx-plugin/src/executors/vite/schema.json deleted file mode 100644 index 961943531..000000000 --- a/packages/nx-plugin/src/executors/vite/schema.json +++ /dev/null @@ -1,147 +0,0 @@ -{ - "version": 2, - "outputCapture": "direct-nodejs", - "title": "Vite Prod Builder", - "cli": "nx", - "description": "Builds a Vite.js application for production.", - "type": "object", - "presets": [ - { - "name": "Default minimum setup", - "keys": [] - } - ], - "properties": { - "outputPath": { - "type": "string", - "description": "The output path of the generated files.", - "x-completion-type": "directory", - "x-priority": "important" - }, - "buildLibsFromSource": { - "type": "boolean", - "description": "Read buildable libraries from source instead of building them separately.", - "default": true - }, - "skipTypeCheck": { - "type": "boolean", - "description": "Skip type-checking via TypeScript. Skipping type-checking speeds up the build but type errors are not caught.", - "default": false - }, - "base": { - "type": "string", - "description": "Base public path when served in development or production.", - "alias": "baseHref" - }, - "configFile": { - "type": "string", - "description": "The name of the Vite.js configuration file.", - "x-completion-type": "file", - "x-completion-glob": "vite.config.@(js|ts)" - }, - "emptyOutDir": { - "description": "When set to false, outputPath will not be emptied during the build process.", - "type": "boolean", - "default": true - }, - "sourcemap": { - "description": "Output sourcemaps. Use 'hidden' for use with error reporting tools without generating sourcemap comment.", - "oneOf": [ - { - "type": "boolean" - }, - { - "type": "string" - } - ] - }, - "target": { - "description": "Browser compatibility target for the final bundle. For more info: https://vitejs.dev/config/build-options.html#build-target", - "type": "string" - }, - "minify": { - "description": "Output sourcemaps. Use 'hidden' for use with error reporting tools without generating sourcemap comment.", - "oneOf": [ - { - "type": "boolean" - }, - { - "type": "string" - } - ] - }, - "manifest": { - "description": "Output sourcemaps. Use 'hidden' for use with error reporting tools without generating sourcemap comment.", - "oneOf": [ - { - "type": "boolean" - }, - { - "type": "string" - } - ] - }, - "ssrManifest": { - "description": "When set to true, the build will also generate an SSR manifest for determining style links and asset preload directives in production. When the value is a string, it will be used as the manifest file name.", - "oneOf": [ - { - "type": "boolean" - }, - { - "type": "string" - } - ] - }, - "ssr": { - "description": "Produce SSR-oriented build. The value can be a string to directly specify the SSR entry, or true, which requires specifying the SSR entry via rollupOptions.input.", - "oneOf": [ - { - "type": "boolean" - }, - { - "type": "string" - } - ] - }, - "logLevel": { - "type": "string", - "description": "Adjust console output verbosity.", - "enum": ["info", "warn", "error", "silent"] - }, - "mode": { - "type": "string", - "description": "Mode to run the build in." - }, - "force": { - "description": "Force the optimizer to ignore the cache and re-bundle", - "type": "boolean" - }, - "cssCodeSplit": { - "description": "Enable/disable CSS code splitting. When enabled, CSS imported in async chunks will be inlined into the async chunk itself and inserted when the chunk is loaded.", - "type": "boolean" - }, - "watch": { - "description": "Enable re-building when files change.", - "oneOf": [ - { - "type": "boolean" - }, - { - "type": "object" - } - ], - "default": false - }, - "generatePackageJson": { - "description": "Generate a package.json for the build output.", - "type": "boolean" - }, - "includeDevDependenciesInPackageJson": { - "description": "Include devDependencies in the generated package.json.", - "type": "boolean" - } - }, - "definitions": {}, - "required": [], - "examplesFile": "../../../docs/build-examples.md" -} diff --git a/packages/nx-plugin/src/executors/vite/vite.impl.ts b/packages/nx-plugin/src/executors/vite/vite.impl.ts deleted file mode 100644 index ffaf2b4c6..000000000 --- a/packages/nx-plugin/src/executors/vite/vite.impl.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { viteBuildExecutor } from '@nx/vite/executors'; - -export default viteBuildExecutor; diff --git a/packages/nx-plugin/vite.config.lib.ts b/packages/nx-plugin/vite.config.lib.ts index e3fa4a3e4..f5b34127a 100644 --- a/packages/nx-plugin/vite.config.lib.ts +++ b/packages/nx-plugin/vite.config.lib.ts @@ -122,15 +122,16 @@ export default defineConfig({ pkgDir, 'src/generators/setup-vitest/compat.ts', ), - // Executors - 'src/executors/vite/vite.impl': resolve( + // Architect builders (thin shims re-exporting from vpa) + 'src/builders/vite/vite-build': resolve( pkgDir, - 'src/executors/vite/vite.impl.ts', + 'src/builders/vite/vite-build.ts', ), - 'src/executors/vite/compat': resolve( + 'src/builders/vite-dev-server/dev-server': resolve( pkgDir, - 'src/executors/vite/compat.ts', + 'src/builders/vite-dev-server/dev-server.ts', ), + // Executors 'src/executors/vite-dev-server/vite-dev-server.impl': resolve( pkgDir, 'src/executors/vite-dev-server/vite-dev-server.impl.ts', @@ -153,6 +154,7 @@ export default defineConfig({ outDir: resolve(pkgDir, '../platform/dist/src/lib/nx-plugin'), rolldownOptions: { external: [ + /^@analogjs\//, /^@angular-devkit\//, /^@angular\//, /^@nx\//, From 6ac0f46173fb5c1fe462cb895bf12781268704bc Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Mon, 25 May 2026 12:27:23 -0500 Subject: [PATCH 30/65] chore: route demo apps' build target through @analogjs/platform:vite 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 ' 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`. --- apps/analog-app/project.json | 10 ++++++---- apps/blog-app/project.json | 10 ++++++---- apps/opt-catchall-app/project.json | 10 ++++++---- apps/tailwind-debug-app/project.json | 10 ++++++---- apps/tanstack-query-app/project.json | 10 ++++++---- 5 files changed, 30 insertions(+), 20 deletions(-) diff --git a/apps/analog-app/project.json b/apps/analog-app/project.json index 34a7bcba3..06519a7c3 100644 --- a/apps/analog-app/project.json +++ b/apps/analog-app/project.json @@ -7,7 +7,7 @@ "tags": [], "targets": { "build": { - "executor": "nx:run-commands", + "executor": "@analogjs/platform:vite", "dependsOn": [ "platform:build", "router:build", @@ -16,15 +16,17 @@ ], "outputs": ["{workspaceRoot}/apps/analog-app/.output"], "options": { - "command": "vite build -c apps/analog-app/vite.config.ts" + "configFile": "apps/analog-app/vite.config.ts", + "outputPath": "dist/apps/analog-app/client" }, "defaultConfiguration": "production", "configurations": { "development": { - "command": "vite build -c apps/analog-app/vite.config.ts --mode development" + "mode": "development" }, "production": { - "command": "vite build -c apps/analog-app/vite.config.ts --mode production" + "sourcemap": false, + "mode": "production" } } }, diff --git a/apps/blog-app/project.json b/apps/blog-app/project.json index 6781e2541..e655d981d 100644 --- a/apps/blog-app/project.json +++ b/apps/blog-app/project.json @@ -7,19 +7,21 @@ "tags": [], "targets": { "build": { - "executor": "nx:run-commands", + "executor": "@analogjs/platform:vite", "dependsOn": ["platform:build", "router:build", "content:build"], "outputs": ["{workspaceRoot}/apps/blog-app/.output"], "options": { - "command": "vite build -c apps/blog-app/vite.config.ts" + "configFile": "apps/blog-app/vite.config.ts", + "outputPath": "dist/apps/blog-app/client" }, "defaultConfiguration": "production", "configurations": { "development": { - "command": "vite build -c apps/blog-app/vite.config.ts --mode development" + "mode": "development" }, "production": { - "command": "vite build -c apps/blog-app/vite.config.ts --mode production" + "sourcemap": false, + "mode": "production" } } }, diff --git a/apps/opt-catchall-app/project.json b/apps/opt-catchall-app/project.json index 8c79fe493..b994ee92b 100644 --- a/apps/opt-catchall-app/project.json +++ b/apps/opt-catchall-app/project.json @@ -7,19 +7,21 @@ "tags": [], "targets": { "build": { - "executor": "nx:run-commands", + "executor": "@analogjs/platform:vite", "dependsOn": ["platform:build", "router:build", "content:build"], "outputs": ["{workspaceRoot}/apps/opt-catchall-app/.output"], "options": { - "command": "vite build -c apps/opt-catchall-app/vite.config.ts" + "configFile": "apps/opt-catchall-app/vite.config.ts", + "outputPath": "dist/apps/opt-catchall-app/client" }, "defaultConfiguration": "production", "configurations": { "development": { - "command": "vite build -c apps/opt-catchall-app/vite.config.ts --mode development" + "mode": "development" }, "production": { - "command": "vite build -c apps/opt-catchall-app/vite.config.ts --mode production" + "sourcemap": false, + "mode": "production" } } }, diff --git a/apps/tailwind-debug-app/project.json b/apps/tailwind-debug-app/project.json index 3a42cede6..f6fcf821a 100644 --- a/apps/tailwind-debug-app/project.json +++ b/apps/tailwind-debug-app/project.json @@ -7,19 +7,21 @@ "tags": [], "targets": { "build": { - "executor": "nx:run-commands", + "executor": "@analogjs/platform:vite", "dependsOn": ["platform:build", "router:build"], "outputs": ["{workspaceRoot}/apps/tailwind-debug-app/.output"], "options": { - "command": "vite build -c apps/tailwind-debug-app/vite.config.ts" + "configFile": "apps/tailwind-debug-app/vite.config.ts", + "outputPath": "dist/apps/tailwind-debug-app/client" }, "defaultConfiguration": "production", "configurations": { "development": { - "command": "vite build -c apps/tailwind-debug-app/vite.config.ts --mode development" + "mode": "development" }, "production": { - "command": "vite build -c apps/tailwind-debug-app/vite.config.ts --mode production" + "sourcemap": false, + "mode": "production" } } }, diff --git a/apps/tanstack-query-app/project.json b/apps/tanstack-query-app/project.json index be16066db..7be83fb81 100644 --- a/apps/tanstack-query-app/project.json +++ b/apps/tanstack-query-app/project.json @@ -7,19 +7,21 @@ "tags": [], "targets": { "build": { - "executor": "nx:run-commands", + "executor": "@analogjs/platform:vite", "dependsOn": ["platform:build", "router:build"], "outputs": ["{workspaceRoot}/apps/tanstack-query-app/.output"], "options": { - "command": "vite build -c apps/tanstack-query-app/vite.config.ts" + "configFile": "apps/tanstack-query-app/vite.config.ts", + "outputPath": "dist/apps/tanstack-query-app/client" }, "defaultConfiguration": "production", "configurations": { "development": { - "command": "vite build -c apps/tanstack-query-app/vite.config.ts --mode development" + "mode": "development" }, "production": { - "command": "vite build -c apps/tanstack-query-app/vite.config.ts --mode production" + "sourcemap": false, + "mode": "production" } } }, From 7d8a551f784bd286f8a04c3065291dd50ec0c0f3 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Mon, 25 May 2026 12:47:52 -0500 Subject: [PATCH 31/65] fix(platform): restore legacy dist//analog output layout nitro/vite defaults its final output to '/.output', which moved build artifacts away from the 'dist/analog/server/index.mjs' path the documentation, deploy scripts, and the canonical 'node dist/analog/server' start command have always referenced. Pin Nitro's output paths in analogNitroPlugin.setup() back to the legacy layout that @analogjs/vite-plugin-nitro produced: nitro.options.output.dir = /dist//analog nitro.options.output.publicDir = /dist//analog/public nitro.options.output.serverDir = /dist//analog/server Leave 'nitro.options.buildDir' at the Nitro default. It is an internal scratch dir and doesn't surface in docs; pinning it under 'dist/' breaks Rolldown's prerender rebundle, which walks up from the SSR chunks to find workspace dependencies in 'node_modules/'. Update each demo app's project.json 'outputs' to point at the new final location for Nx caching. --- apps/analog-app/project.json | 2 +- apps/blog-app/project.json | 2 +- apps/opt-catchall-app/project.json | 2 +- apps/tailwind-debug-app/project.json | 2 +- apps/tanstack-query-app/project.json | 2 +- .../src/lib/nitro/analog-nitro-plugin.ts | 31 +++++++++++++++++++ 6 files changed, 36 insertions(+), 5 deletions(-) diff --git a/apps/analog-app/project.json b/apps/analog-app/project.json index 06519a7c3..9350c3b01 100644 --- a/apps/analog-app/project.json +++ b/apps/analog-app/project.json @@ -14,7 +14,7 @@ "my-package:build", "top-bar:build" ], - "outputs": ["{workspaceRoot}/apps/analog-app/.output"], + "outputs": ["{workspaceRoot}/dist/apps/analog-app/analog"], "options": { "configFile": "apps/analog-app/vite.config.ts", "outputPath": "dist/apps/analog-app/client" diff --git a/apps/blog-app/project.json b/apps/blog-app/project.json index e655d981d..6ecbe0a79 100644 --- a/apps/blog-app/project.json +++ b/apps/blog-app/project.json @@ -9,7 +9,7 @@ "build": { "executor": "@analogjs/platform:vite", "dependsOn": ["platform:build", "router:build", "content:build"], - "outputs": ["{workspaceRoot}/apps/blog-app/.output"], + "outputs": ["{workspaceRoot}/dist/apps/blog-app/analog"], "options": { "configFile": "apps/blog-app/vite.config.ts", "outputPath": "dist/apps/blog-app/client" diff --git a/apps/opt-catchall-app/project.json b/apps/opt-catchall-app/project.json index b994ee92b..1eba6c4c2 100644 --- a/apps/opt-catchall-app/project.json +++ b/apps/opt-catchall-app/project.json @@ -9,7 +9,7 @@ "build": { "executor": "@analogjs/platform:vite", "dependsOn": ["platform:build", "router:build", "content:build"], - "outputs": ["{workspaceRoot}/apps/opt-catchall-app/.output"], + "outputs": ["{workspaceRoot}/dist/apps/opt-catchall-app/analog"], "options": { "configFile": "apps/opt-catchall-app/vite.config.ts", "outputPath": "dist/apps/opt-catchall-app/client" diff --git a/apps/tailwind-debug-app/project.json b/apps/tailwind-debug-app/project.json index f6fcf821a..549c110ac 100644 --- a/apps/tailwind-debug-app/project.json +++ b/apps/tailwind-debug-app/project.json @@ -9,7 +9,7 @@ "build": { "executor": "@analogjs/platform:vite", "dependsOn": ["platform:build", "router:build"], - "outputs": ["{workspaceRoot}/apps/tailwind-debug-app/.output"], + "outputs": ["{workspaceRoot}/dist/apps/tailwind-debug-app/analog"], "options": { "configFile": "apps/tailwind-debug-app/vite.config.ts", "outputPath": "dist/apps/tailwind-debug-app/client" diff --git a/apps/tanstack-query-app/project.json b/apps/tanstack-query-app/project.json index 7be83fb81..22710642e 100644 --- a/apps/tanstack-query-app/project.json +++ b/apps/tanstack-query-app/project.json @@ -9,7 +9,7 @@ "build": { "executor": "@analogjs/platform:vite", "dependsOn": ["platform:build", "router:build"], - "outputs": ["{workspaceRoot}/apps/tanstack-query-app/.output"], + "outputs": ["{workspaceRoot}/dist/apps/tanstack-query-app/analog"], "options": { "configFile": "apps/tanstack-query-app/vite.config.ts", "outputPath": "dist/apps/tanstack-query-app/client" diff --git a/packages/platform/src/lib/nitro/analog-nitro-plugin.ts b/packages/platform/src/lib/nitro/analog-nitro-plugin.ts index 023cff7f9..27e1d6641 100644 --- a/packages/platform/src/lib/nitro/analog-nitro-plugin.ts +++ b/packages/platform/src/lib/nitro/analog-nitro-plugin.ts @@ -140,6 +140,12 @@ export function analogNitroPlugin(options: Options = {}): Plugin { overrides.environments = { ssr: { build: { + outDir: resolve( + context.workspaceRoot, + 'dist', + context.rootDir, + 'ssr', + ), rollupOptions: { input: { index: ssrEntryMarkerPath }, }, @@ -177,6 +183,31 @@ export function analogNitroPlugin(options: Options = {}): Plugin { refreshContext(nitro.options.rootDir); } + // Preserve the legacy `@analogjs/vite-plugin-nitro` final output + // layout so downstream tooling (deploy scripts, docs, the + // `dist/analog/server` start command) keeps working. nitro/vite's + // default `/.output` would otherwise drop artifacts in an + // unexpected location for users upgrading from v2. + // + // `buildDir` (Nitro's intermediate scratch dir) stays at its default + // inside the project root. Nitro's prerender phase re-bundles SSR + // chunks out of `/vite/services/ssr/`, and Rolldown's + // resolver walks up from those files looking for `node_modules/`. + // Keeping `buildDir` adjacent to the project root means workspace + // packages installed at `/node_modules/` (the usual install + // shape for both standalone and Nx setups) remain reachable. + const distRoot = resolve( + context.workspaceRoot, + 'dist', + context.rootDir, + ); + nitro.options.output = { + ...nitro.options.output, + dir: resolve(distRoot, 'analog'), + publicDir: resolve(distRoot, 'analog/public'), + serverDir: resolve(distRoot, 'analog/server'), + }; + const hasAPIDir = existsSync( resolve( context.workspaceRoot, From 1141f763ec127a74f597a8fbb54031852b8fa197 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Mon, 25 May 2026 12:54:46 -0500 Subject: [PATCH 32/65] test(platform): make migrate-to-separated-plugins specs version-agnostic The schematic tests previously baked in '^3.0.0-alpha.55' as the @analogjs/platform pin and '3.0.260415-beta' as the expected nitro version. Both are point-in-time release coordinates that age with every alpha bump; the tests should pin to behavior, not version strings. Switch to a shared PLATFORM_VERSION constant for the synthetic package.json fixtures, assert that the mirrored vite-plugin-angular version matches whatever the consumer had pinned (instead of re-asserting the literal), and assert truthiness for nitro instead of the exact beta tag. Tests now describe what the schematic does, not which release was current when they were written. --- .../migrate-to-separated-plugins.spec.ts | 55 ++++++++++--------- 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/packages/platform/migrations/migrate-to-separated-plugins/migrate-to-separated-plugins.spec.ts b/packages/platform/migrations/migrate-to-separated-plugins/migrate-to-separated-plugins.spec.ts index e4e81edfa..54f879e37 100644 --- a/packages/platform/migrations/migrate-to-separated-plugins/migrate-to-separated-plugins.spec.ts +++ b/packages/platform/migrations/migrate-to-separated-plugins/migrate-to-separated-plugins.spec.ts @@ -46,14 +46,18 @@ describe('migrate-to-separated-plugins', () => { tree = new UnitTestTree(Tree.empty()); }); + // Stand-in for whatever `@analogjs/platform` version a consuming workspace + // happens to have pinned. The schematic mirrors this onto + // `@analogjs/vite-plugin-angular`; tests assert the mirroring behavior, + // not a specific release line. + const PLATFORM_VERSION = '^3.0.0'; + it('logs the migration notice and adds deps when a legacy vite.config.ts is detected', () => { tree.create('/vite.config.ts', LEGACY_CONFIG); tree.create( '/package.json', JSON.stringify({ - devDependencies: { - '@analogjs/platform': '^3.0.0-alpha.55', - }, + devDependencies: { '@analogjs/platform': PLATFORM_VERSION }, }), ); @@ -62,9 +66,9 @@ describe('migrate-to-separated-plugins', () => { const pkg = JSON.parse(tree.readContent('/package.json')); expect(pkg.devDependencies['@analogjs/vite-plugin-angular']).toBe( - '^3.0.0-alpha.55', + PLATFORM_VERSION, ); - expect(pkg.devDependencies['nitro']).toBe('3.0.260415-beta'); + expect(pkg.devDependencies['nitro']).toBeTruthy(); expect(infoLogs.join('\n')).toContain('/vite.config.ts'); expect(infoLogs.join('\n')).toContain('migrating-v2-to-v3'); }); @@ -74,9 +78,7 @@ describe('migrate-to-separated-plugins', () => { tree.create( '/package.json', JSON.stringify({ - devDependencies: { - '@analogjs/platform': '^3.0.0-alpha.55', - }, + devDependencies: { '@analogjs/platform': PLATFORM_VERSION }, }), ); @@ -92,14 +94,16 @@ describe('migrate-to-separated-plugins', () => { }); it('does not duplicate deps that are already declared', () => { + const PREEXISTING_VPA = '^2.5.0'; + const PREEXISTING_NITRO = '3.0.0-beta'; tree.create('/vite.config.ts', LEGACY_CONFIG); tree.create( '/package.json', JSON.stringify({ devDependencies: { - '@analogjs/platform': '^3.0.0-alpha.55', - '@analogjs/vite-plugin-angular': '^2.5.0', - nitro: '3.0.250101-beta', + '@analogjs/platform': PLATFORM_VERSION, + '@analogjs/vite-plugin-angular': PREEXISTING_VPA, + nitro: PREEXISTING_NITRO, }, }), ); @@ -108,18 +112,19 @@ describe('migrate-to-separated-plugins', () => { migrateToSeparatedPlugins()(tree, context); const pkg = JSON.parse(tree.readContent('/package.json')); - expect(pkg.devDependencies['@analogjs/vite-plugin-angular']).toBe('^2.5.0'); - expect(pkg.devDependencies['nitro']).toBe('3.0.250101-beta'); + expect(pkg.devDependencies['@analogjs/vite-plugin-angular']).toBe( + PREEXISTING_VPA, + ); + expect(pkg.devDependencies['nitro']).toBe(PREEXISTING_NITRO); }); it('reads version from `dependencies` if `devDependencies` is missing platform', () => { + const FROM_DEPS = '~3.0.0'; tree.create('/vite.config.ts', LEGACY_CONFIG); tree.create( '/package.json', JSON.stringify({ - dependencies: { - '@analogjs/platform': '~3.0.0-alpha.55', - }, + dependencies: { '@analogjs/platform': FROM_DEPS }, }), ); @@ -128,7 +133,7 @@ describe('migrate-to-separated-plugins', () => { const pkg = JSON.parse(tree.readContent('/package.json')); expect(pkg.devDependencies['@analogjs/vite-plugin-angular']).toBe( - '~3.0.0-alpha.55', + FROM_DEPS, ); }); @@ -137,7 +142,7 @@ describe('migrate-to-separated-plugins', () => { tree.create( '/package.json', JSON.stringify({ - devDependencies: { '@analogjs/platform': '^3.0.0-alpha.55' }, + devDependencies: { '@analogjs/platform': PLATFORM_VERSION }, }), ); @@ -154,7 +159,7 @@ describe('migrate-to-separated-plugins', () => { tree.create( '/package.json', JSON.stringify({ - devDependencies: { '@analogjs/platform': '^3.0.0-alpha.55' }, + devDependencies: { '@analogjs/platform': PLATFORM_VERSION }, }), ); @@ -170,7 +175,7 @@ describe('migrate-to-separated-plugins', () => { tree.create( '/package.json', JSON.stringify({ - devDependencies: { '@analogjs/platform': '^3.0.0-alpha.55' }, + devDependencies: { '@analogjs/platform': PLATFORM_VERSION }, }), ); @@ -178,7 +183,7 @@ describe('migrate-to-separated-plugins', () => { migrateToSeparatedPlugins()(tree, context); const pkg = JSON.parse(tree.readContent('/package.json')); - expect(pkg.devDependencies['nitro']).toBe('3.0.260415-beta'); + expect(pkg.devDependencies['nitro']).toBeTruthy(); }); it('does not treat a config that already imports the new plugins as legacy', () => { @@ -188,9 +193,9 @@ describe('migrate-to-separated-plugins', () => { '/package.json', JSON.stringify({ devDependencies: { - '@analogjs/platform': '^3.0.0-alpha.55', - '@analogjs/vite-plugin-angular': '^3.0.0-alpha.55', - nitro: '3.0.260415-beta', + '@analogjs/platform': PLATFORM_VERSION, + '@analogjs/vite-plugin-angular': PLATFORM_VERSION, + nitro: '3.0.0-beta', }, }), ); @@ -203,7 +208,7 @@ describe('migrate-to-separated-plugins', () => { describe('option transform', () => { const PKG = JSON.stringify({ - devDependencies: { '@analogjs/platform': '^3.0.0-alpha.55' }, + devDependencies: { '@analogjs/platform': PLATFORM_VERSION }, }); it('lifts `vite: {...}` into a companion angular() call', () => { From fc961b0708435a08453ca5510403deb72ed914fc Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Mon, 25 May 2026 13:01:57 -0500 Subject: [PATCH 33/65] fix(create-analog): add vite-plugin-angular and nitro to devDependencies The template vite.config.ts files now import @analogjs/vite-plugin-angular and nitro/vite directly (since commit 7e94cd70e wired the templates to the separated plugin shape), but create-analog's package.json only declared @analogjs/platform as a devDependency. The 'loads the generated tailwind vite config' integration test scaffolds a project inside packages/create-analog/__tests__/ and runs loadConfigFromFile on the generated vite.config.ts. Without the new deps in create-analog's own node_modules tree, that resolution fails with ERR_MODULE_NOT_FOUND for @analogjs/vite-plugin-angular. Add both as workspace devDependencies so the templates' resolution succeeds during the test. --- packages/create-analog/package.json | 4 +++- pnpm-lock.yaml | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/create-analog/package.json b/packages/create-analog/package.json index 6ad68788a..f2117c839 100644 --- a/packages/create-analog/package.json +++ b/packages/create-analog/package.json @@ -41,7 +41,9 @@ "prompts": "catalog:" }, "devDependencies": { - "@analogjs/platform": "workspace:*" + "@analogjs/platform": "workspace:*", + "@analogjs/vite-plugin-angular": "workspace:*", + "nitro": "catalog:" }, "publishConfig": { "access": "public", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 716109ac7..5d5309a1d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1491,6 +1491,12 @@ importers: '@analogjs/platform': specifier: workspace:* version: link:../platform + '@analogjs/vite-plugin-angular': + specifier: workspace:* + version: link:../vite-plugin-angular + nitro: + specifier: 'catalog:' + version: 3.0.260415-beta(chokidar@5.0.0)(dotenv@16.4.7)(jiti@2.6.1)(lru-cache@11.3.5)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) packages/nx-plugin: devDependencies: From 33554bd41b0ed16d33872587f1bc719322847ad0 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Mon, 25 May 2026 13:07:42 -0500 Subject: [PATCH 34/65] fix: unblock tailwind-debug-app vitest run Two changes to make 'nx test tailwind-debug-app' pass: 1. Strip 'experimental: { websocket: true }' from nitro() under Vitest. Vitest's headless Vite has 'server.httpServer === null', but nitro/vite's configureViteDevServer unconditionally calls 'server.httpServer.on("upgrade", ...)' when websocket is enabled, crashing the test runner. The websocket probe is only exercised by the dev server and e2e suite, so dropping the flag in test mode loses no coverage. 2. Add 'platform:build', 'router:build', and 'vitest-angular:build' to the test target's dependsOn. The test setup imports '@analogjs/vitest-angular/setup-*', which require the workspace vitest-angular dist to be built; without the dependency, the test fails on a stale node_modules tree in cold-cache runs. --- apps/tailwind-debug-app/project.json | 1 + apps/tailwind-debug-app/vite.config.ts | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/tailwind-debug-app/project.json b/apps/tailwind-debug-app/project.json index 549c110ac..2a7a3a6f5 100644 --- a/apps/tailwind-debug-app/project.json +++ b/apps/tailwind-debug-app/project.json @@ -47,6 +47,7 @@ }, "test": { "executor": "@nx/vitest:test", + "dependsOn": ["platform:build", "router:build", "vitest-angular:build"], "outputs": ["{projectRoot}/coverage"] }, "typecheck": { diff --git a/apps/tailwind-debug-app/vite.config.ts b/apps/tailwind-debug-app/vite.config.ts index 476bc69ab..1fed40e55 100644 --- a/apps/tailwind-debug-app/vite.config.ts +++ b/apps/tailwind-debug-app/vite.config.ts @@ -131,9 +131,12 @@ export default defineConfig(({ mode }) => ({ ssr: false, }, }, - experimental: { - websocket: true, - }, + // Vitest spins up a headless Vite (server.httpServer === null), and + // nitro/vite's configureViteDevServer unconditionally calls + // `server.httpServer.on('upgrade', ...)` when websocket is enabled, + // crashing the test runner. Drop the flag under Vitest; the websocket + // probe is only exercised by the dev server and e2e suite. + ...(process.env['VITEST'] ? {} : { experimental: { websocket: true } }), }), tailwindcss(), hmrWiretapPlugin(), From 06516a81b755244320f3321f5627c2f021ad3b10 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Mon, 25 May 2026 13:41:13 -0500 Subject: [PATCH 35/65] feat(platform)!: drop useAPIMiddleware and ssrBuildDir options MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both options were carried from `@analogjs/vite-plugin-nitro`'s Options interface but never wired into the new analogNitroPlugin: - `useAPIMiddleware` toggled a legacy '#ANALOG_API_MIDDLEWARE' virtual handler / api-proxy fallback for apps without a `src/server/routes/api/` directory. That fallback is removed in v3 — apps should use the conventional `src/server/routes/api/` directory for API routes. - `ssrBuildDir` redirected the legacy plugin's intermediate SSR env output. Under nitro/vite, the SSR env's outDir is managed by the platform plugin (`/dist//ssr` via `environments.ssr.build.outDir`) and Nitro's own buildDir defaults. Removing the typed surface so the option list reflects what's actually read by the plugin. --- packages/platform/src/lib/options.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/platform/src/lib/options.ts b/packages/platform/src/lib/options.ts index 322230126..8729171e5 100644 --- a/packages/platform/src/lib/options.ts +++ b/packages/platform/src/lib/options.ts @@ -79,7 +79,6 @@ export interface I18nOptions { export interface Options { ssr?: boolean; - ssrBuildDir?: string; /** * Prerender the static pages without producing the server output. */ @@ -131,12 +130,6 @@ export interface Options { * @default false */ discoverRoutes?: boolean; - /** - * Toggles internal API middleware. - * If disabled, a proxy request is used to route /api - * requests to / in the production server build. - */ - useAPIMiddleware?: boolean; /** * Configuration for runtime i18n support. * When set, enables locale detection on SSR and provides From 62a22921e8c309270de7323cd6220ecb36cd2936 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Mon, 25 May 2026 14:12:55 -0500 Subject: [PATCH 36/65] Revert "feat(vite-plugin-nitro)!: deprecate package; orchestration moved to @analogjs/platform" This reverts commit 88919d3e515500ce42b65f691fe684bf4898fc36. --- packages/platform/package.json | 1 + packages/vite-plugin-nitro/package.json | 29 +- packages/vite-plugin-nitro/src/index.spec.ts | 9 + packages/vite-plugin-nitro/src/index.ts | 47 +- .../src/lib/build-server.spec.ts | 73 + .../vite-plugin-nitro/src/lib/build-server.ts | 106 + .../src/lib/build-sitemap.spec.ts | 222 +++ .../src/lib/build-sitemap.ts | 375 ++++ .../src/lib/build-ssr.spec.ts | 65 + .../vite-plugin-nitro/src/lib/build-ssr.ts | 73 + .../src/lib/hooks/post-rendering-hook.ts | 12 + .../lib/hooks/post-rendering-hooks.spec.ts | 33 + packages/vite-plugin-nitro/src/lib/options.ts | 213 ++ .../src/lib/page-endpoints.spec.ts | 84 + .../src/lib/plugins/dev-server-plugin.ts | 176 ++ .../src/lib/plugins/page-endpoints.ts | 105 + .../vite-plugin-nitro/src/lib/utils/debug.ts | 8 + .../src/lib/utils/get-content-files.spec.ts | 77 + .../src/lib/utils/get-content-files.ts | 133 ++ .../src/lib/utils/get-page-handlers.ts | 112 ++ .../src/lib/utils/i18n-prerender.spec.ts | 186 ++ .../src/lib/utils/i18n-prerender.ts | 105 + .../src/lib/utils/load-esm.ts | 27 + .../src/lib/utils/node-web-bridge.spec.ts | 31 + .../src/lib/utils/node-web-bridge.ts | 110 ++ .../src/lib/utils/register-dev-middleware.ts | 67 + .../lib/utils/register-i18n-watcher.spec.ts | 84 + .../src/lib/utils/register-i18n-watcher.ts | 28 + .../src/lib/utils/renderers.spec.ts | 41 + .../src/lib/utils/renderers.ts | 141 ++ .../src/lib/utils/rolldown.spec.ts | 33 + .../src/lib/utils/rolldown.ts | 9 + .../src/lib/vite-nitro-plugin.spec.data.ts | 73 + .../src/lib/vite-plugin-nitro.spec.ts | 951 +++++++++ .../src/lib/vite-plugin-nitro.ts | 1735 +++++++++++++++++ .../test-data/content/01-first.md | 7 + .../test-data/content/02-second.md | 6 + .../test-data/content/03-third.md | 7 + packages/vite-plugin-nitro/vite.config.lib.ts | 1 + pnpm-lock.yaml | 410 +--- 40 files changed, 5645 insertions(+), 360 deletions(-) create mode 100644 packages/vite-plugin-nitro/src/index.spec.ts create mode 100644 packages/vite-plugin-nitro/src/lib/build-server.spec.ts create mode 100644 packages/vite-plugin-nitro/src/lib/build-server.ts create mode 100644 packages/vite-plugin-nitro/src/lib/build-sitemap.spec.ts create mode 100644 packages/vite-plugin-nitro/src/lib/build-sitemap.ts create mode 100644 packages/vite-plugin-nitro/src/lib/build-ssr.spec.ts create mode 100644 packages/vite-plugin-nitro/src/lib/build-ssr.ts create mode 100644 packages/vite-plugin-nitro/src/lib/hooks/post-rendering-hook.ts create mode 100644 packages/vite-plugin-nitro/src/lib/hooks/post-rendering-hooks.spec.ts create mode 100644 packages/vite-plugin-nitro/src/lib/options.ts create mode 100644 packages/vite-plugin-nitro/src/lib/page-endpoints.spec.ts create mode 100644 packages/vite-plugin-nitro/src/lib/plugins/dev-server-plugin.ts create mode 100644 packages/vite-plugin-nitro/src/lib/plugins/page-endpoints.ts create mode 100644 packages/vite-plugin-nitro/src/lib/utils/debug.ts create mode 100644 packages/vite-plugin-nitro/src/lib/utils/get-content-files.spec.ts create mode 100644 packages/vite-plugin-nitro/src/lib/utils/get-content-files.ts create mode 100644 packages/vite-plugin-nitro/src/lib/utils/get-page-handlers.ts create mode 100644 packages/vite-plugin-nitro/src/lib/utils/i18n-prerender.spec.ts create mode 100644 packages/vite-plugin-nitro/src/lib/utils/i18n-prerender.ts create mode 100644 packages/vite-plugin-nitro/src/lib/utils/load-esm.ts create mode 100644 packages/vite-plugin-nitro/src/lib/utils/node-web-bridge.spec.ts create mode 100644 packages/vite-plugin-nitro/src/lib/utils/node-web-bridge.ts create mode 100644 packages/vite-plugin-nitro/src/lib/utils/register-dev-middleware.ts create mode 100644 packages/vite-plugin-nitro/src/lib/utils/register-i18n-watcher.spec.ts create mode 100644 packages/vite-plugin-nitro/src/lib/utils/register-i18n-watcher.ts create mode 100644 packages/vite-plugin-nitro/src/lib/utils/renderers.spec.ts create mode 100644 packages/vite-plugin-nitro/src/lib/utils/renderers.ts create mode 100644 packages/vite-plugin-nitro/src/lib/utils/rolldown.spec.ts create mode 100644 packages/vite-plugin-nitro/src/lib/utils/rolldown.ts create mode 100644 packages/vite-plugin-nitro/src/lib/vite-nitro-plugin.spec.data.ts create mode 100644 packages/vite-plugin-nitro/src/lib/vite-plugin-nitro.spec.ts create mode 100644 packages/vite-plugin-nitro/src/lib/vite-plugin-nitro.ts create mode 100644 packages/vite-plugin-nitro/test-data/content/01-first.md create mode 100644 packages/vite-plugin-nitro/test-data/content/02-second.md create mode 100644 packages/vite-plugin-nitro/test-data/content/03-third.md diff --git a/packages/platform/package.json b/packages/platform/package.json index 68cc2db56..6dcca0f8b 100644 --- a/packages/platform/package.json +++ b/packages/platform/package.json @@ -46,6 +46,7 @@ "tinyglobby": "catalog:", "nitro": "catalog:", "@analogjs/vite-plugin-angular": "workspace:*", + "@analogjs/vite-plugin-nitro": "workspace:*", "@babel/core": "catalog:", "ofetch": "catalog:", "oxc-parser": "catalog:", diff --git a/packages/vite-plugin-nitro/package.json b/packages/vite-plugin-nitro/package.json index 249c7a3bb..c5a8411e6 100644 --- a/packages/vite-plugin-nitro/package.json +++ b/packages/vite-plugin-nitro/package.json @@ -1,7 +1,7 @@ { "name": "@analogjs/vite-plugin-nitro", "version": "3.0.0-alpha.55", - "description": "Deprecated — Nitro orchestration moved to @analogjs/platform. This package no longer exposes a plugin.", + "description": "A Vite plugin for adding a nitro API server", "type": "module", "author": "Brandon Roberts ", "exports": { @@ -10,6 +10,11 @@ "import": "./dist/src/index.js", "default": "./dist/src/index.js" }, + "./internal": { + "types": "./dist/src/lib/utils/debug.d.ts", + "import": "./dist/src/lib/utils/debug.js", + "default": "./dist/src/lib/utils/debug.js" + }, "./package.json": "./package.json" }, "keywords": [ @@ -33,7 +38,23 @@ "type": "github", "url": "https://github.com/sponsors/brandonroberts" }, - "dependencies": {}, + "peerDependencies": { + "sharp": ">=0.32.0" + }, + "peerDependenciesMeta": { + "sharp": { + "optional": true + } + }, + "dependencies": { + "defu": "catalog:", + "nitro": "catalog:", + "obug": "catalog:", + "ofetch": "catalog:", + "oxc-parser": "catalog:", + "radix3": "catalog:", + "xmlbuilder2": "catalog:" + }, "ng-update": { "packageGroup": [ "@analogjs/platform", @@ -46,6 +67,10 @@ ], "migrations": "./migrations/migration.json" }, + "imports": { + "#analog/ssr": "./dist/src/index.js", + "#analog/index": "./dist/src/index.js" + }, "publishConfig": { "access": "public", "provenance": true diff --git a/packages/vite-plugin-nitro/src/index.spec.ts b/packages/vite-plugin-nitro/src/index.spec.ts new file mode 100644 index 000000000..82bc0e57e --- /dev/null +++ b/packages/vite-plugin-nitro/src/index.spec.ts @@ -0,0 +1,9 @@ +import { describe, expect, it } from 'vitest'; + +import nitroDefault, { nitro } from './index.js'; + +describe('vite-plugin-nitro entrypoint', () => { + it('exports the nitro plugin as both named and default exports', () => { + expect(nitroDefault).toBe(nitro); + }); +}); diff --git a/packages/vite-plugin-nitro/src/index.ts b/packages/vite-plugin-nitro/src/index.ts index c9c7d8145..9fc349045 100644 --- a/packages/vite-plugin-nitro/src/index.ts +++ b/packages/vite-plugin-nitro/src/index.ts @@ -1,16 +1,31 @@ -/** - * @deprecated `@analogjs/vite-plugin-nitro` has been deprecated. The Nitro - * orchestration lives in `@analogjs/platform`, which composes Nitro's - * first-party Vite plugin (`nitro/vite`) with Analog's `analogNitroPlugin`. - * - * Migration: - * - Replace `import { nitro } from '@analogjs/vite-plugin-nitro'` with - * `import { analog } from '@analogjs/platform'` and use `analog()` in - * your Vite plugin chain. - * - Type exports (`SitemapConfig`, `PrerenderRouteConfig`, - * `PrerenderContentDir`, etc.) are re-exported from `@analogjs/platform`. - * - * This package is kept as a placeholder so existing dependency declarations - * don't fail to install, but it no longer exposes a Vite plugin. - */ -export {}; +import { nitro } from './lib/vite-plugin-nitro.js'; +export { debugInstances } from './lib/utils/debug.js'; +export { nitro } from './lib/vite-plugin-nitro.js'; +export type { + Options, + SitemapConfig, + SitemapEntry, + SitemapExcludeRule, + SitemapPriority, + SitemapRouteDefinition, + SitemapRouteInput, + SitemapRouteSource, + SitemapTransform, + PrerenderSitemapConfig, + PrerenderRouteConfig, + PrerenderContentDir, + PrerenderContentFile, + I18nPrerenderOptions, +} from './lib/options.js'; + +declare module 'nitro/types' { + interface NitroRouteConfig { + ssr?: boolean; + } + + interface NitroRouteRules { + ssr?: boolean; + } +} + +export default nitro; diff --git a/packages/vite-plugin-nitro/src/lib/build-server.spec.ts b/packages/vite-plugin-nitro/src/lib/build-server.spec.ts new file mode 100644 index 000000000..22818eb41 --- /dev/null +++ b/packages/vite-plugin-nitro/src/lib/build-server.spec.ts @@ -0,0 +1,73 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { mkdtempSync, mkdirSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join, resolve } from 'node:path'; + +vi.mock('nitro/builder', () => ({ + build: vi.fn(), + copyPublicAssets: vi.fn(), + createNitro: vi.fn(), + prepare: vi.fn(), + prerender: vi.fn(), +})); + +import { + build, + copyPublicAssets, + createNitro, + prepare, + prerender, +} from 'nitro/builder'; + +import { buildServer } from './build-server'; + +describe('buildServer', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('forces rollup bundler and builds successfully', async () => { + const workspaceRoot = mkdtempSync(join(tmpdir(), 'analog-build-server-')); + const outputDir = resolve(workspaceRoot, '.output'); + const serverDir = resolve(outputDir, 'server'); + const publicDir = resolve(outputDir, 'public'); + + mkdirSync(serverDir, { recursive: true }); + mkdirSync(publicDir, { recursive: true }); + + vi.mocked(createNitro).mockResolvedValue({ + options: { + framework: { + name: 'nitro', + version: '3.0.0', + }, + output: { + dir: outputDir, + publicDir, + serverDir, + }, + preset: 'node-server', + routeRules: {}, + static: false, + }, + close: vi.fn().mockResolvedValue(undefined), + } as never); + vi.mocked(prepare).mockResolvedValue(undefined as never); + vi.mocked(copyPublicAssets).mockResolvedValue(undefined as never); + vi.mocked(prerender).mockResolvedValue(undefined as never); + vi.mocked(build).mockResolvedValue(undefined as never); + + try { + await buildServer({}, { output: { publicDir } }); + + expect(createNitro).toHaveBeenCalledWith( + expect.objectContaining({ + builder: 'rollup', + }), + ); + expect(build).toHaveBeenCalled(); + } finally { + rmSync(workspaceRoot, { recursive: true, force: true }); + } + }); +}); diff --git a/packages/vite-plugin-nitro/src/lib/build-server.ts b/packages/vite-plugin-nitro/src/lib/build-server.ts new file mode 100644 index 000000000..046a639dd --- /dev/null +++ b/packages/vite-plugin-nitro/src/lib/build-server.ts @@ -0,0 +1,106 @@ +import type { NitroConfig } from 'nitro/types'; +import { + build, + copyPublicAssets, + createNitro, + prepare, + prerender, +} from 'nitro/builder'; +import { mkdirSync, rmSync, writeFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; + +import { Options } from './options.js'; +import { addPostRenderingHooks } from './hooks/post-rendering-hook.js'; + +export function isVercelPreset(preset: string | undefined): boolean { + return !!preset?.toLowerCase().includes('vercel'); +} + +export async function buildServer( + options?: Options, + nitroConfig?: NitroConfig, + routeSourceFiles?: Record, +): Promise { + // ── Force Rollup as the server bundler ──────────────────────────── + // + // Nitro v3 defaults to Rolldown when available. Rolldown is faster, + // but its module resolver cannot resolve relative chunk imports + // (e.g. `./assets/core-DTazUigR.js`) from a rebundled SSR entry on + // Windows. The prerender build fails with: + // + // [RESOLVE_ERROR] Could not resolve './assets/core-DTazUigR.js' + // in ../../dist/apps/blog-app/ssr/main.server.js + // + // This is a known Rolldown limitation with cross-directory relative + // paths on Windows (backslash vs forward-slash normalisation). + // Rollup handles these paths correctly on all platforms. + // + // The dev server already uses `builder: 'rollup'` for the same + // reason. Default to Rollup here too until Rolldown's resolver + // matures. The caller can still opt in to Rolldown explicitly via + // nitroConfig.builder if their platform supports it. + const nitro = await createNitro({ + dev: false, + preset: process.env['BUILD_PRESET'], + ...nitroConfig, + builder: nitroConfig?.builder ?? 'rollup', + }); + + if (options?.prerender?.postRenderingHooks) { + addPostRenderingHooks(nitro, options.prerender.postRenderingHooks); + } + + await prepare(nitro); + await copyPublicAssets(nitro); + + if ( + options?.ssr && + nitroConfig?.prerender?.routes && + (nitroConfig?.prerender?.routes.find((route) => route === '/') || + nitroConfig?.prerender?.routes?.length === 0) + ) { + const indexFileExts = ['', '.br', '.gz']; + + indexFileExts.forEach((fileExt) => { + // Remove the root index.html(.br|.gz) files + const indexFilePath = join( + nitroConfig?.output?.publicDir ?? '', + `index.html${fileExt}`, + ); + + rmSync(indexFilePath, { force: true }); + }); + } + + if ( + nitroConfig?.prerender?.routes && + nitroConfig?.prerender?.routes?.length > 0 + ) { + console.log(`Prerendering static pages...`); + await prerender(nitro); + } + + if (routeSourceFiles && Object.keys(routeSourceFiles).length > 0) { + const publicDir = nitroConfig?.output?.publicDir; + if (!publicDir) { + throw new Error( + 'Nitro public output directory is required to write route source files.', + ); + } + + for (const [route, content] of Object.entries(routeSourceFiles)) { + const outputPath = join(publicDir, `${route}.md`); + const outputDir = dirname(outputPath); + mkdirSync(outputDir, { recursive: true }); + + writeFileSync(outputPath, content, 'utf8'); + } + } + + if (!options?.static) { + console.log('Building Server...'); + await build(nitro); + } + + await nitro.close(); +} diff --git a/packages/vite-plugin-nitro/src/lib/build-sitemap.spec.ts b/packages/vite-plugin-nitro/src/lib/build-sitemap.spec.ts new file mode 100644 index 000000000..5d16b7477 --- /dev/null +++ b/packages/vite-plugin-nitro/src/lib/build-sitemap.spec.ts @@ -0,0 +1,222 @@ +import { existsSync, mkdirSync, writeFileSync } from 'node:fs'; +import { buildSitemap } from './build-sitemap'; + +vi.mock('node:fs', () => ({ + existsSync: vi.fn(() => true), + mkdirSync: vi.fn(), + writeFileSync: vi.fn(), +})); + +describe('build sitemap', () => { + const config = { root: 'root' }; + const existsSyncMock = vi.mocked(existsSync); + const mkdirSyncMock = vi.mocked(mkdirSync); + const writeFileSyncMock = vi.mocked(writeFileSync); + + afterEach(() => { + vi.restoreAllMocks(); + existsSyncMock.mockReturnValue(true); + mkdirSyncMock.mockReset(); + writeFileSyncMock.mockReset(); + }); + + it('should not perform functionality if no predefined routes are present', async () => { + await buildSitemap(config, { host: 'https://host.com' }, [], '', {}); + + expect(writeFileSyncMock).not.toHaveBeenCalled(); + }); + + it('should preserve route sitemap metadata when the host has a trailing slash', async () => { + existsSyncMock.mockReturnValue(true); + + await buildSitemap( + config, + { host: 'https://host.com/' }, + ['/blog'], + '/tmp/analog/public', + { + '/blog': { + lastmod: '2024-01-15', + changefreq: 'weekly', + priority: 0.8, + }, + }, + ); + + expect(writeFileSyncMock).toHaveBeenCalledWith( + expect.stringContaining('/tmp/analog/public/sitemap.xml'), + expect.stringContaining('https://host.com/blog'), + ); + expect(writeFileSyncMock.mock.calls[0]?.[1]).toContain( + '2024-01-15', + ); + expect(writeFileSyncMock.mock.calls[0]?.[1]).toContain( + 'weekly', + ); + expect(writeFileSyncMock.mock.calls[0]?.[1]).toContain( + '0.8', + ); + }); + + it('should apply include defaults transform exclude and internal route filtering', async () => { + existsSyncMock.mockReturnValue(true); + + await buildSitemap( + config, + { + host: 'https://host.com', + defaults: { + changefreq: 'monthly', + priority: 0.4, + }, + include: async () => [ + '/extra', + { + route: '/docs/hello world', + lastmod: '2024-01-01', + }, + ], + exclude: ['/drafts/**', /^\/admin/], + transform: (entry) => + entry.route === '/extra' + ? { + route: '/extra-updated', + priority: 0.9, + } + : { + route: entry.route, + }, + }, + [ + '/products', + '/products', + '/drafts/preview', + '/api/_analog/pages/products', + ], + '/tmp/analog/public', + {}, + ); + + const xml = writeFileSyncMock.mock.calls[0]?.[1] ?? ''; + expect(xml).toContain('https://host.com/products'); + expect(xml).toContain('monthly'); + expect(xml).toContain('0.4'); + expect(xml).toContain('https://host.com/extra-updated'); + expect(xml).toContain('0.9'); + expect(xml).toContain('https://host.com/docs/hello%20world'); + expect(xml).not.toContain('/drafts/preview'); + expect(xml).not.toContain('/api/_analog/pages/products'); + }); + + it('should support predicate exclude rules and transform returning false', async () => { + existsSyncMock.mockReturnValue(true); + + await buildSitemap( + config, + { + host: 'https://host.com', + exclude: [async (entry) => entry.route === '/private'], + transform: (entry) => + entry.route === '/skip-me' + ? false + : { + route: entry.route, + }, + }, + ['/public', '/private', '/skip-me'], + '/tmp/analog/public', + {}, + ); + + const xml = writeFileSyncMock.mock.calls[0]?.[1] ?? ''; + expect(xml).toContain('https://host.com/public'); + expect(xml).not.toContain('/private'); + expect(xml).not.toContain('/skip-me'); + expect(xml).not.toContain(''); + }); + + it('should resolve callable per-route sitemap metadata', async () => { + existsSyncMock.mockReturnValue(true); + + await buildSitemap( + config, + { host: 'https://host.com/' }, + ['/blog'], + '/tmp/analog/public', + { + '/blog': () => ({ + lastmod: '2024-04-01', + changefreq: 'daily', + }), + }, + ); + + const xml = writeFileSyncMock.mock.calls[0]?.[1] ?? ''; + expect(xml).toContain('https://host.com/blog'); + expect(xml).toContain('2024-04-01'); + expect(xml).toContain('daily'); + }); + + it('should filter internal routes when a custom api prefix is configured', async () => { + existsSyncMock.mockReturnValue(true); + + await buildSitemap( + config, + { host: 'https://host.com' }, + ['/shop', '/functions/_analog/pages/shop'], + '/tmp/analog/public', + {}, + { apiPrefix: 'functions' }, + ); + + const xml = writeFileSyncMock.mock.calls[0]?.[1] ?? ''; + expect(xml).toContain('https://host.com/shop'); + expect(xml).not.toContain('/functions/_analog/pages/shop'); + }); + + it('should create the output directory when it does not exist', async () => { + existsSyncMock.mockReturnValue(false); + + await buildSitemap( + config, + { host: 'https://host.com' }, + ['/'], + '/tmp/generated/public', + {}, + ); + + expect(mkdirSyncMock).toHaveBeenCalledWith('/tmp/generated/public', { + recursive: true, + }); + expect(writeFileSyncMock).toHaveBeenCalled(); + }); + + it('should refuse to write to the current working directory', async () => { + existsSyncMock.mockReturnValue(true); + const errorSpy = vi + .spyOn(console, 'error') + .mockImplementation(() => undefined); + + await buildSitemap(config, { host: 'https://host.com' }, ['/'], '', {}); + + expect(writeFileSyncMock).not.toHaveBeenCalled(); + expect(errorSpy).toHaveBeenCalledWith( + expect.stringContaining('Unable to write file at'), + expect.any(Error), + ); + }); + + it('should reject invalid sitemap hosts before writing output', async () => { + await expect( + buildSitemap( + config, + { host: 'not-a-valid-url' }, + ['/'], + '/tmp/analog/public', + {}, + ), + ).rejects.toThrow(); + + expect(writeFileSyncMock).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/vite-plugin-nitro/src/lib/build-sitemap.ts b/packages/vite-plugin-nitro/src/lib/build-sitemap.ts new file mode 100644 index 000000000..4f896d830 --- /dev/null +++ b/packages/vite-plugin-nitro/src/lib/build-sitemap.ts @@ -0,0 +1,375 @@ +import { existsSync, mkdirSync, writeFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { create } from 'xmlbuilder2'; +import { XMLBuilder } from 'xmlbuilder2/lib/interfaces'; +import { UserConfig } from 'vite'; +import { + PrerenderSitemapConfig, + SitemapConfig, + SitemapEntry, + SitemapExcludeRule, + SitemapRouteDefinition, + SitemapRouteInput, + SitemapRouteSource, +} from './options'; + +type RouteSitemapConfig = + | PrerenderSitemapConfig + | (() => PrerenderSitemapConfig) + | undefined; + +export type PagesJson = SitemapEntry; + +export interface BuildSitemapOptions { + apiPrefix?: string; +} + +export async function buildSitemap( + _config: UserConfig, + sitemapConfig: SitemapConfig, + routes: (string | undefined)[] | (() => Promise<(string | undefined)[]>), + outputDir: string, + routeSitemaps: Record, + buildOptions: BuildSitemapOptions = {}, +): Promise { + const host = normalizeSitemapHost(sitemapConfig.host); + const routeList = await collectSitemapRoutes(routes, sitemapConfig.include); + const sitemapData = await resolveSitemapEntries( + routeList, + host, + routeSitemaps, + sitemapConfig, + buildOptions, + ); + + if (!sitemapData.length) { + return; + } + + const sitemap = createXml('urlset'); + + for (const item of sitemapData) { + const page = sitemap.ele('url'); + page.ele('loc').txt(item.loc); + + if (item.lastmod) { + page.ele('lastmod').txt(item.lastmod); + } + + if (item.changefreq) { + page.ele('changefreq').txt(item.changefreq); + } + + if (item.priority !== undefined) { + page.ele('priority').txt(String(item.priority)); + } + } + + const resolvedOutputDir = resolve(outputDir); + const mapPath = resolve(resolvedOutputDir, 'sitemap.xml'); + try { + if (!resolvedOutputDir || resolvedOutputDir === resolve()) { + throw new Error( + 'Refusing to write the sitemap to the current working directory. Expected the Nitro public output directory instead.', + ); + } + + if (!existsSync(resolvedOutputDir)) { + mkdirSync(resolvedOutputDir, { recursive: true }); + } + console.log(`Writing sitemap at ${mapPath}`); + writeFileSync(mapPath, sitemap.end({ prettyPrint: true })); + } catch (e) { + console.error(`Unable to write file at ${mapPath}`, e); + } +} + +async function resolveSitemapEntries( + routes: SitemapRouteInput[], + host: string, + routeSitemaps: Record, + sitemapConfig: SitemapConfig, + buildOptions: BuildSitemapOptions, +): Promise { + const defaults = sitemapConfig.defaults ?? {}; + const seen = new Set(); + const entries: SitemapEntry[] = []; + + for (const route of routes) { + const entry = await toSitemapEntry( + route, + host, + routeSitemaps, + defaults, + sitemapConfig.transform, + ); + + if (!entry) { + continue; + } + + if ( + isInternalSitemapRoute(entry.route, buildOptions.apiPrefix) || + (await isExcludedSitemapRoute(entry, sitemapConfig.exclude)) + ) { + continue; + } + + if (seen.has(entry.loc)) { + continue; + } + + seen.add(entry.loc); + entries.push(entry); + } + + return entries; +} + +async function toSitemapEntry( + route: SitemapRouteInput, + host: string, + routeSitemaps: Record, + defaults: PrerenderSitemapConfig, + transform: SitemapConfig['transform'], +): Promise { + const normalizedRoute = normalizeSitemapRoute( + typeof route === 'string' ? route : route?.route, + ); + if (!normalizedRoute) { + return undefined; + } + + const baseEntry = createSitemapEntry( + { + ...defaults, + ...resolveRouteSitemapConfig(routeSitemaps[normalizedRoute]), + ...(typeof route === 'object' ? route : {}), + route: normalizedRoute, + }, + host, + ); + + if (!transform) { + return baseEntry; + } + + const transformed = await transform(baseEntry); + if (!transformed) { + return undefined; + } + + return createSitemapEntry( + { + ...baseEntry, + ...transformed, + }, + host, + ); +} + +function createSitemapEntry( + routeDefinition: SitemapRouteDefinition, + host: string, +): SitemapEntry { + const route = normalizeSitemapRoute(routeDefinition.route) ?? '/'; + + return { + route, + loc: new URL(route, ensureTrailingSlash(host)).toString(), + lastmod: routeDefinition.lastmod, + changefreq: routeDefinition.changefreq, + priority: routeDefinition.priority, + }; +} + +function resolveRouteSitemapConfig( + config: RouteSitemapConfig, +): PrerenderSitemapConfig { + if (!config) { + return {}; + } + + return typeof config === 'function' ? config() : config; +} + +function normalizeSitemapHost(host: string): string { + const resolvedHost = new URL(host); + resolvedHost.hash = ''; + return resolvedHost.toString(); +} + +function ensureTrailingSlash(host: string): string { + return host.endsWith('/') ? host : `${host}/`; +} + +function normalizeSitemapRoute(route: string | undefined): string | undefined { + if (!route) { + return undefined; + } + + const trimmedRoute = route.trim(); + if (!trimmedRoute) { + return undefined; + } + + const pathWithQuery = trimmedRoute.split('#', 1)[0] ?? ''; + const [pathname, search] = pathWithQuery.split('?', 2); + const normalizedPathname = pathname + ? `/${pathname.replace(/^\/+/, '').replace(/\/{2,}/g, '/')}` + : '/'; + + return search ? `${normalizedPathname}?${search}` : normalizedPathname; +} + +function isInternalSitemapRoute(route: string, apiPrefix = 'api'): boolean { + const normalizedApiPrefix = normalizeSitemapRoute(`/${apiPrefix}`) ?? '/api'; + return ( + route === `${normalizedApiPrefix}/_analog/pages` || + route.startsWith(`${normalizedApiPrefix}/_analog/pages/`) + ); +} + +async function isExcludedSitemapRoute( + entry: SitemapEntry, + excludeRules: SitemapExcludeRule[] | undefined, +): Promise { + if (!excludeRules?.length) { + return false; + } + + for (const rule of excludeRules) { + if (typeof rule === 'function') { + if (await rule(entry)) { + return true; + } + continue; + } + + if (rule instanceof RegExp) { + if (rule.test(entry.route)) { + return true; + } + continue; + } + + if (toGlobRegExp(rule).test(entry.route)) { + return true; + } + } + + return false; +} + +function toGlobRegExp(pattern: string): RegExp { + const doubleStarToken = '__ANALOG_DOUBLE_STAR__'; + const singleStarToken = '__ANALOG_SINGLE_STAR__'; + const escapedPattern = pattern + .replace(/\*\*/g, doubleStarToken) + .replace(/\*/g, singleStarToken) + .replace(/[.+^${}()|[\]\\]/g, '\\$&'); + const regexPattern = escapedPattern + .replace(new RegExp(doubleStarToken, 'g'), '.*') + .replace(new RegExp(singleStarToken, 'g'), '[^/]*'); + return new RegExp(`^${regexPattern}$`); +} + +async function collectSitemapRoutes( + routes: (string | undefined)[] | (() => Promise<(string | undefined)[]>), + include?: SitemapRouteSource, +): Promise { + const routeList = await resolveRouteInputs(routes); + const includedRoutes = include ? await resolveRouteInputs(include) : []; + return [...routeList, ...includedRoutes]; +} + +async function resolveRouteInputs( + routes: + | SitemapRouteSource + | (string | undefined)[] + | (() => Promise<(string | undefined)[]>), +): Promise { + let routeList: SitemapRouteInput[]; + + if (typeof routes === 'function') { + routeList = await routes(); + } else if (Array.isArray(routes)) { + routeList = routes; + } else { + routeList = []; + } + + return routeList.filter(Boolean); +} + +/** + * Generates hreflang alternate URLs for a given page URL. + * For a URL like `https://example.com/fr/about`, it produces alternates + * for all configured locales. + */ +export function getHreflangAlternates( + pageUrl: string, + host: string, + i18n: I18nPrerenderOptions, +): { locale: string; href: string }[] { + const alternates: { locale: string; href: string }[] = []; + const normalizedHost = host.replace(/\/+$/, ''); + + // Extract the path portion after the host + const path = pageUrl.replace(normalizedHost, ''); + + // Strip locale prefix to get the base path + const basePath = stripLocalePrefix(path, i18n.locales); + + for (const locale of i18n.locales) { + const localizedPath = + basePath === '/' || basePath === '' + ? `/${locale}` + : `/${locale}${basePath}`; + alternates.push({ + locale, + href: `${normalizedHost}${localizedPath}`, + }); + } + + // Add x-default pointing to the default locale variant + const defaultPath = + basePath === '/' || basePath === '' + ? `/${i18n.defaultLocale}` + : `/${i18n.defaultLocale}${basePath}`; + alternates.push({ + locale: 'x-default', + href: `${normalizedHost}${defaultPath}`, + }); + + return alternates; +} + +/** + * Strips a locale prefix from a URL path. + * E.g., '/fr/about' -> '/about', '/en' -> '/' + */ +export function stripLocalePrefix(path: string, locales: string[]): string { + const segments = path.split('/').filter(Boolean); + if (segments.length > 0 && locales.includes(segments[0])) { + const rest = segments.slice(1).join('/'); + return rest ? `/${rest}` : '/'; + } + return path || '/'; +} + +function createXml( + elementName: 'urlset' | 'sitemapindex', + includeXhtml = false, +): XMLBuilder { + const attrs: Record = { + xmlns: 'https://www.sitemaps.org/schemas/sitemap/0.9', + }; + if (includeXhtml) { + attrs['xmlns:xhtml'] = 'https://www.w3.org/1999/xhtml'; + } + + return create({ version: '1.0', encoding: 'UTF-8' }) + .ele(elementName, attrs) + .com(`This file was automatically generated by Analog.`); +} diff --git a/packages/vite-plugin-nitro/src/lib/build-ssr.spec.ts b/packages/vite-plugin-nitro/src/lib/build-ssr.spec.ts new file mode 100644 index 000000000..fce106e13 --- /dev/null +++ b/packages/vite-plugin-nitro/src/lib/build-ssr.spec.ts @@ -0,0 +1,65 @@ +import { describe, expect, it, vi } from 'vitest'; + +vi.mock('vite', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + build: vi.fn(), + }; +}); + +import { build } from 'vite'; + +import { buildClientApp, buildSSRApp } from './build-ssr'; + +describe('build helpers', () => { + it('uses the client output directory for the explicit legacy rebuild', async () => { + const workspaceRoot = '/workspace'; + + await buildClientApp( + { + root: '/workspace/apps/my-app', + }, + { + workspaceRoot, + }, + ); + + expect(build).toHaveBeenCalledWith( + expect.objectContaining({ + build: expect.objectContaining({ + ssr: false, + outDir: '/workspace/dist/apps/my-app/client', + emptyOutDir: true, + }), + }), + ); + }); + + it('preserves client output when starting the SSR sub-build', async () => { + const workspaceRoot = '/workspace'; + + await buildSSRApp( + { + root: '/workspace/apps/my-app', + build: { + outDir: '../../dist/apps/my-app/client', + emptyOutDir: true, + }, + }, + { + workspaceRoot, + }, + ); + + expect(build).toHaveBeenCalledWith( + expect.objectContaining({ + build: expect.objectContaining({ + ssr: true, + outDir: '/workspace/dist/apps/my-app/ssr', + emptyOutDir: false, + }), + }), + ); + }); +}); diff --git a/packages/vite-plugin-nitro/src/lib/build-ssr.ts b/packages/vite-plugin-nitro/src/lib/build-ssr.ts new file mode 100644 index 000000000..adc7fdee4 --- /dev/null +++ b/packages/vite-plugin-nitro/src/lib/build-ssr.ts @@ -0,0 +1,73 @@ +import { build, mergeConfig, UserConfig } from 'vite'; +import { relative, resolve } from 'node:path'; + +import { Options } from './options.js'; +import { getBundleOptionsKey } from './utils/rolldown.js'; + +export async function buildClientApp( + config: UserConfig, + options?: Options, +): Promise { + const workspaceRoot = options?.workspaceRoot ?? process.cwd(); + const rootDir = relative(workspaceRoot, config.root || '.') || '.'; + const clientBuildConfig = mergeConfig(config, { + build: { + ssr: false, + outDir: + config.build?.outDir || + resolve(workspaceRoot, 'dist', rootDir, 'client'), + emptyOutDir: true, + }, + }); + + await build(clientBuildConfig); +} + +export async function buildSSRApp( + config: UserConfig, + options?: Options, +): Promise { + const workspaceRoot = options?.workspaceRoot ?? process.cwd(); + const sourceRoot = options?.sourceRoot ?? 'src'; + const rootDir = relative(workspaceRoot, config.root || '.') || '.'; + const bundleOptionsKey = getBundleOptionsKey(); + + /** + * SSR is built as a second pass from the already prepared Analog/Vite config. + * + * That means we intentionally start from the same base config used for the + * client build and then merge only the SSR-specific overrides (entry, outDir, + * `build.ssr`, etc). + * + * A side effect of this design is that the resolved SSR config can expose the + * same high-level Analog plugin chain more than once when Vite/Nitro replays + * shared plugins for the server environment. In particular, + * `@analogjs/vite-plugin-angular` may appear twice in `config.plugins` during + * SSR resolution: + * - once from the normal Analog platform plugin expansion + * - once from the reused/shared plugin graph for the SSR pass + * + * That does NOT imply the client build has two competing style registries. + * The client-side duplicate-registration guard in `vite-plugin-angular` + * therefore explicitly ignores `build.ssr === true` to avoid treating this + * valid SSR orchestration detail as a style-map bug. + */ + const ssrBuildConfig = mergeConfig(config, { + build: { + ssr: true, + [bundleOptionsKey]: { + input: + options?.entryServer || + resolve(workspaceRoot, rootDir, `${sourceRoot}/main.server.ts`), + }, + outDir: + options?.ssrBuildDir || resolve(workspaceRoot, 'dist', rootDir, 'ssr'), + // Preserve the client build output. The client pass already handled its + // own cleanup, and on Windows this nested SSR build can otherwise remove + // sibling artifacts that Nitro needs to read immediately afterward. + emptyOutDir: false, + }, + }); + + await build(ssrBuildConfig); +} diff --git a/packages/vite-plugin-nitro/src/lib/hooks/post-rendering-hook.ts b/packages/vite-plugin-nitro/src/lib/hooks/post-rendering-hook.ts new file mode 100644 index 000000000..9e6dc379c --- /dev/null +++ b/packages/vite-plugin-nitro/src/lib/hooks/post-rendering-hook.ts @@ -0,0 +1,12 @@ +import type { Nitro, PrerenderRoute } from 'nitro/types'; + +export function addPostRenderingHooks( + nitro: Nitro, + hooks: ((pr: PrerenderRoute) => Promise)[], +): void { + hooks.forEach((hook: (preRoute: PrerenderRoute) => void) => { + nitro.hooks.hook('prerender:generate', (route: PrerenderRoute) => { + hook(route); + }); + }); +} diff --git a/packages/vite-plugin-nitro/src/lib/hooks/post-rendering-hooks.spec.ts b/packages/vite-plugin-nitro/src/lib/hooks/post-rendering-hooks.spec.ts new file mode 100644 index 000000000..a9e4439d5 --- /dev/null +++ b/packages/vite-plugin-nitro/src/lib/hooks/post-rendering-hooks.spec.ts @@ -0,0 +1,33 @@ +import type { Nitro } from 'nitro/types'; +import { vi } from 'vitest'; + +import { addPostRenderingHooks } from './post-rendering-hook'; + +describe('postRenderingHook', () => { + const genRoute = { + route: 'test/testRoute', + contents: 'This is a test.', + }; + + const nitroMock = { + hooks: { + hook: vi.fn((name: string, callback: (route: any) => void) => + callback(genRoute), + ), + }, + } as unknown as Nitro; + + const mockFunc1 = vi.fn(); + const mockFunc2 = vi.fn(); + + it('should not attempt to call nitro mocks if no callbacks provided', () => { + addPostRenderingHooks(nitroMock, []); + expect(nitroMock.hooks.hook).not.toHaveBeenCalled(); + }); + + it('should call provided hooks', () => { + addPostRenderingHooks(nitroMock, [mockFunc1, mockFunc2]); + expect(mockFunc1).toHaveBeenCalledWith(genRoute); + expect(mockFunc2).toHaveBeenCalled(); + }); +}); diff --git a/packages/vite-plugin-nitro/src/lib/options.ts b/packages/vite-plugin-nitro/src/lib/options.ts new file mode 100644 index 000000000..f710e2261 --- /dev/null +++ b/packages/vite-plugin-nitro/src/lib/options.ts @@ -0,0 +1,213 @@ +import type { PrerenderRoute } from 'nitro/types'; +import type { UserConfig } from 'vite'; + +export interface I18nPrerenderOptions { + /** + * The default/source locale for the application. + */ + defaultLocale: string; + + /** + * List of supported locale identifiers. + * Each route will be prerendered once per locale with a locale prefix. + */ + locales: string[]; +} + +export interface Options { + ssr?: boolean; + ssrBuildDir?: string; + /** + * Prerender the static pages without producing the server output. + */ + static?: boolean; + prerender?: PrerenderOptions; + entryServer?: string; + index?: string; + /** + * Relative path to source files. Default is 'src'. + */ + sourceRoot?: string; + /** + * Absolute path to workspace root. Default is 'process.cwd()' + */ + workspaceRoot?: string; + /** + * Additional page paths to include + */ + additionalPagesDirs?: string[]; + /** + * Additional API paths to include + */ + additionalAPIDirs?: string[]; + apiPrefix?: string; + + /** + * Toggles internal API middleware. + * If disabled, a proxy request is used to route /api + * requests to / in the production server build. + * + * @deprecated + * Use the src/server/routes/api folder + * for API routes. + */ + useAPIMiddleware?: boolean; + /** + * Vite-native build passthrough. Rolldown-only options such as + * `build.rolldownOptions.output.codeSplitting` are forwarded when present. + */ + vite?: { + build?: UserConfig['build']; + }; +} + +export interface PrerenderOptions { + /** + * Add additional routes to prerender through crawling page links. + */ + discover?: boolean; + + /** + * List of routes to prerender resolved statically or dynamically. + */ + routes?: + | (string | PrerenderContentDir | PrerenderRouteConfig)[] + | (() => Promise< + (string | PrerenderContentDir | PrerenderRouteConfig | undefined)[] + >); + sitemap?: SitemapConfig; + /** List of functions that run for each route after pre-rendering is complete. */ + postRenderingHooks?: ((routes: PrerenderRoute) => Promise)[]; +} + +export type SitemapPriority = number | `${number}`; + +export interface SitemapRouteDefinition { + route: string; + lastmod?: string; + changefreq?: + | 'always' + | 'hourly' + | 'daily' + | 'weekly' + | 'monthly' + | 'yearly' + | 'never'; + priority?: SitemapPriority; +} + +export interface SitemapEntry extends SitemapRouteDefinition { + loc: string; +} + +export type SitemapRouteInput = string | SitemapRouteDefinition | undefined; +export type SitemapRouteSource = + | SitemapRouteInput[] + | (() => Promise); +export type SitemapExcludeRule = + | string + | RegExp + | ((entry: SitemapEntry) => boolean | Promise); +export type SitemapTransform = ( + entry: SitemapEntry, +) => SitemapRouteDefinition | false | Promise; + +export interface SitemapConfig { + host: string; + include?: SitemapRouteSource; + exclude?: SitemapExcludeRule[]; + defaults?: PrerenderSitemapConfig; + transform?: SitemapTransform; +} + +export interface PrerenderContentDir { + /** + * The directory where files should be grabbed from. + * @example `/src/contents/blog` + */ + contentDir: string; + /** + * Transform the matching content files path into a route. + * The function is called for each matching content file within the specified contentDir. + * @param file information of the matching file (`path`, `name`, `extension`, `attributes`, `content`) + * @returns a string with the route should be returned (e. g. `/blog/`) or the value `false`, when the route should not be prerendered. + */ + transform: (file: PrerenderContentFile) => string | false; + + /** + * Customize the sitemap definition for the prerendered route + * + * https://www.sitemaps.org/protocol.html#xmlTagDefinitions + */ + sitemap?: + | PrerenderSitemapConfig + | ((file: PrerenderContentFile) => PrerenderSitemapConfig); + + /** + * Output the source markdown content alongside the prerendered route. + * The source file will be accessible at the route path with a .md extension. + * @param file information of the matching file including its content + * @returns the markdown content string to output, or `false` to skip outputting for this file + */ + outputSourceFile?: (file: PrerenderContentFile) => string | false; + + /** + * Recurse into subdirectories of `contentDir` when discovering files. + * When enabled, the matching file's directory relative to `contentDir` + * is exposed via `PrerenderContentFile.relativePath` so transforms can + * disambiguate identically-named files across subdirectories. + * @default false + */ + recursive?: boolean; +} + +/** + * @param path the path to the content file + * @param name the basename of the matching content file without the file extension + * @param extension the file extension + * @param attributes the frontmatter attributes extracted from the frontmatter section of the file + * @param content the raw file content including frontmatter + * @param relativePath when `recursive` is enabled, the directory of the file relative to `contentDir` (empty string for files at the top level) + * @returns a string with the route should be returned (e. g. `/blog/`) or the value `false`, when the route should not be prerendered. + */ +export interface PrerenderContentFile { + path: string; + attributes: Record; + name: string; + extension: string; + content: string; + relativePath?: string; +} + +export interface PrerenderSitemapConfig { + lastmod?: string; + changefreq?: + | 'always' + | 'hourly' + | 'daily' + | 'weekly' + | 'monthly' + | 'yearly' + | 'never'; + priority?: SitemapPriority; +} + +export interface PrerenderRouteConfig { + route: string; + /** + * Customize the sitemap definition for the prerendered route + * + * https://www.sitemaps.org/protocol.html#xmlTagDefinitions + */ + sitemap?: PrerenderSitemapConfig | (() => PrerenderSitemapConfig); + /** + * Prerender static data for the prerendered route + */ + staticData?: boolean; + /** + * Path to the source markdown file to output alongside the prerendered route. + * The source file will be accessible at the route path with a .md extension. + * @example 'src/content/overview.md' + */ + outputSourceFile?: string; +} diff --git a/packages/vite-plugin-nitro/src/lib/page-endpoints.spec.ts b/packages/vite-plugin-nitro/src/lib/page-endpoints.spec.ts new file mode 100644 index 000000000..90fe8ccd3 --- /dev/null +++ b/packages/vite-plugin-nitro/src/lib/page-endpoints.spec.ts @@ -0,0 +1,84 @@ +import { describe, expect, it } from 'vitest'; + +import { pageEndpointsPlugin } from './plugins/page-endpoints'; + +describe('pageEndpointsPlugin', () => { + const plugin = pageEndpointsPlugin(); + + it('uses Nitro runtime $fetch instead of a private nitro import', async () => { + const result = await plugin.transform?.( + `export const load = () => ({ ok: true });`, + '/src/app/pages/index.server.ts', + ); + + expect(result).toBeDefined(); + expect(result?.code).toContain( + 'export default defineHandler(async(event) => {', + ); + expect(result?.code).toContain(`import { createFetch } from 'ofetch';`); + expect(result?.code).toContain('fetchWithEvent'); + expect(result?.code).toContain('const serverFetch = createFetch'); + expect(result?.code).toContain('fetch: serverFetch'); + expect(result?.code).not.toContain(`nitro/deps/ofetch`); + }); + + it('generates a default load when only action is exported', async () => { + const result = await plugin.transform?.( + `export const action = () => ({ saved: true });`, + '/src/app/pages/index.server.ts', + ); + + expect(result).toBeDefined(); + expect(result?.code).toContain('export const load = () =>'); + expect(result?.code).toContain( + 'export const action = () => ({ saved: true })', + ); + }); + + it('uses both exports when load and action are provided', async () => { + const result = await plugin.transform?.( + `export const load = () => ({ ok: true });\nexport const action = () => ({ saved: true });`, + '/src/app/pages/index.server.ts', + ); + + expect(result).toBeDefined(); + expect(result?.code).toContain('export const load = () => ({ ok: true })'); + expect(result?.code).toContain( + 'export const action = () => ({ saved: true })', + ); + // should not generate default stubs + expect(result?.code).not.toContain('return {};'); + }); + + it('generates default load and action when neither is exported', async () => { + const result = await plugin.transform?.( + `export const helper = () => 42;`, + '/src/app/pages/index.server.ts', + ); + + expect(result).toBeDefined(); + expect(result?.code).toContain('export const load = () =>'); + expect(result?.code).toContain('export const action = () =>'); + // both stubs return empty objects + const stubs = (result?.code.match(/return \{\};/g) || []).length; + expect(stubs).toBe(2); + }); + + it('skips files that are not .server.ts', async () => { + const result = await plugin.transform?.( + `export const load = () => ({ ok: true });`, + '/src/app/pages/index.ts', + ); + + expect(result).toBeUndefined(); + }); + + it('skips .server.ts files outside /pages/', async () => { + const result = await plugin.transform?.( + `export const load = () => ({ ok: true });`, + '/src/app/services/auth.server.ts', + ); + + expect(result).toBeUndefined(); + }); +}); diff --git a/packages/vite-plugin-nitro/src/lib/plugins/dev-server-plugin.ts b/packages/vite-plugin-nitro/src/lib/plugins/dev-server-plugin.ts new file mode 100644 index 000000000..da4441617 --- /dev/null +++ b/packages/vite-plugin-nitro/src/lib/plugins/dev-server-plugin.ts @@ -0,0 +1,176 @@ +// SSR dev server, middleware and error page source modified from +// https://github.com/solidjs/solid-start/blob/main/packages/start/dev/server.js + +import { + Connect, + Plugin, + UserConfig, + ViteDevServer, + normalizePath, +} from 'vite'; +import { resolve } from 'node:path'; +import { readFileSync } from 'node:fs'; +import { createRouter as createRadixRouter, toRouteMatcher } from 'radix3'; +import { defu } from 'defu'; +import type { NitroRouteRules } from 'nitro/types'; + +import { registerDevServerMiddleware } from '../utils/register-dev-middleware.js'; +import { writeWebResponseToNode } from '../utils/node-web-bridge.js'; +import { Options } from '../options.js'; +import { detectLocaleFromRoute, setHtmlLang } from '../utils/i18n-prerender.js'; + +type ServerOptions = Options & { routeRules?: Record | undefined }; + +export function devServerPlugin(options: ServerOptions): Plugin { + const workspaceRoot = options?.workspaceRoot || process.cwd(); + const sourceRoot = options?.sourceRoot ?? 'src'; + const index = options.index || 'index.html'; + let config: UserConfig; + let root: string; + let isTest = false; + + return { + name: 'analogjs-dev-ssr-plugin', + config(userConfig, { mode }) { + config = userConfig; + root = normalizePath(resolve(workspaceRoot, config.root || '.') || '.'); + isTest = isTest ? isTest : mode === 'test'; + return { + appType: 'custom', + resolve: { + alias: { + '~analog/entry-server': + options.entryServer || `${root}/${sourceRoot}/main.server.ts`, + }, + }, + }; + }, + configureServer(viteServer) { + if (isTest) { + return; + } + + return async () => { + remove_html_middlewares(viteServer.middlewares); + registerDevServerMiddleware(root, sourceRoot, viteServer); + + if (options.i18n) { + registerI18nWatcher(viteServer); + } + + viteServer.middlewares.use(async (req, res) => { + let template = readFileSync( + resolve(viteServer.config.root, index), + 'utf-8', + ); + + template = await viteServer.transformIndexHtml( + req.originalUrl as string, + template, + ); + + const _routeRulesMatcher = toRouteMatcher( + createRadixRouter({ routes: options.routeRules }), + ); + const _getRouteRules = (path: string) => + defu( + {}, + ..._routeRulesMatcher.matchAll(path).reverse(), + ) as NitroRouteRules; + + try { + let result: string | Response; + // Check for route rules explicitly disabling SSR + if (_getRouteRules(req.originalUrl as string).ssr === false) { + result = template; + } else { + const entryServer = ( + await viteServer.ssrLoadModule('~analog/entry-server') + )['default']; + result = await entryServer(req.originalUrl, template, { + req, + res, + }); + } + + if (result instanceof Response) { + await writeWebResponseToNode(res, result); + return; + } + + // Inject lang attribute when i18n is configured + let html = typeof result === 'string' ? result : template; + if (options.i18n) { + const locale = detectLocaleFromRoute( + req.originalUrl as string, + options.i18n, + ); + html = setHtmlLang(html, locale); + } + + res.setHeader('Content-Type', 'text/html'); + res.end(html); + } catch (e) { + viteServer.ssrFixStacktrace(e as Error); + res.statusCode = 500; + res.end(` + + + + + Error + + + + + + `); + } + }); + }; + }, + }; +} + +/** + * Removes Vite internal middleware + * + * @param server + */ +function remove_html_middlewares(server: ViteDevServer['middlewares']) { + const html_middlewares = [ + 'viteIndexHtmlMiddleware', + 'vite404Middleware', + 'viteSpaFallbackMiddleware', + 'viteHtmlFallbackMiddleware', + ]; + for (let i = server.stack.length - 1; i > 0; i--) { + const handler = server.stack[i]?.handle; + const handlerName = + typeof handler === 'function' ? handler.name : undefined; + if (handlerName && html_middlewares.includes(handlerName)) { + server.stack.splice(i, 1); + } + } +} + +/** + * Formats error for SSR message in error overlay + * @param req + * @param error + * @returns + */ +function prepareError(req: Connect.IncomingMessage, error: unknown) { + const e = error as Error; + return { + message: `An error occured while server rendering ${req.url}:\n\n\t${ + typeof e === 'string' ? e : e.message + } `, + stack: typeof e === 'string' ? '' : e.stack, + }; +} diff --git a/packages/vite-plugin-nitro/src/lib/plugins/page-endpoints.ts b/packages/vite-plugin-nitro/src/lib/plugins/page-endpoints.ts new file mode 100644 index 000000000..bd1f18bf3 --- /dev/null +++ b/packages/vite-plugin-nitro/src/lib/plugins/page-endpoints.ts @@ -0,0 +1,105 @@ +import { parseSync } from 'oxc-parser'; +import { normalizePath } from 'vite'; +import { SERVER_FETCH_FACTORY_SNIPPET } from '../utils/renderers.js'; + +export function pageEndpointsPlugin() { + return { + name: 'analogjs-vite-plugin-nitro-rollup-page-endpoint', + async transform( + _code: string, + id: string, + ): Promise<{ code: string; map: null } | undefined> { + if (normalizePath(id).includes('/pages/') && id.endsWith('.server.ts')) { + const result = parseSync(id, _code, { + sourceType: 'module', + lang: 'ts', + }); + + const fileExports: string[] = result.module.staticExports.flatMap((e) => + e.entries + .filter((entry) => entry.exportName.name !== null) + .map((entry) => entry.exportName.name as string), + ); + + // In h3 v2 / Nitro v3, event.node is undefined during prerendering + // (which uses the fetch-based pipeline, not Node.js http). We use + // optional chaining so that page endpoints work in both Node.js + // server and fetch-based prerender contexts. + // Nitro v3 no longer guarantees the private `nitro/deps/ofetch` + // subpath that older codegen relied on. + // + // Page loaders expect Nitro-style `$fetch` semantics (parsed data plus + // internal relative-route support), so construct a request-local fetch + // using public APIs: + // - `createFetch` from `ofetch` for `$fetch` behavior + // - `fetchWithEvent` from `h3` for internal Nitro request routing + // + // This avoids both unstable private Nitro imports and assumptions about + // a global runtime `$fetch` being available during prerender. + const code = ` + import { defineHandler, fetchWithEvent } from 'nitro/h3'; + import { createFetch } from 'ofetch'; + + ${ + fileExports.includes('load') + ? _code + : ` + ${_code} + export const load = () => { + return {}; + }` + } + + ${ + fileExports.includes('action') + ? '' + : ` + export const action = () => { + return {}; + } + ` + } + + export default defineHandler(async(event) => { + ${SERVER_FETCH_FACTORY_SNIPPET} + + if (event.method === 'GET') { + try { + return await load({ + params: event.context.params, + req: event.node?.req, + res: event.node?.res, + fetch: serverFetch, + event + }); + } catch(e) { + console.error(\` An error occurred: \${e}\`) + throw e; + } + } else { + try { + return await action({ + params: event.context.params, + req: event.node?.req, + res: event.node?.res, + fetch: serverFetch, + event + }); + } catch(e) { + console.error(\` An error occurred: \${e}\`) + throw e; + } + } + }); + `; + + return { + code, + map: null, + }; + } + + return; + }, + }; +} diff --git a/packages/vite-plugin-nitro/src/lib/utils/debug.ts b/packages/vite-plugin-nitro/src/lib/utils/debug.ts new file mode 100644 index 000000000..14878f366 --- /dev/null +++ b/packages/vite-plugin-nitro/src/lib/utils/debug.ts @@ -0,0 +1,8 @@ +import { createDebug } from 'obug'; + +export const debugNitro = createDebug('analog:nitro'); +export const debugSsr = createDebug('analog:nitro:ssr'); +export const debugPrerender = createDebug('analog:nitro:prerender'); + +/** All debug instances in this package, for external wrapping (e.g. file logging). */ +export const debugInstances = [debugNitro, debugSsr, debugPrerender]; diff --git a/packages/vite-plugin-nitro/src/lib/utils/get-content-files.spec.ts b/packages/vite-plugin-nitro/src/lib/utils/get-content-files.spec.ts new file mode 100644 index 000000000..473cbe0d9 --- /dev/null +++ b/packages/vite-plugin-nitro/src/lib/utils/get-content-files.spec.ts @@ -0,0 +1,77 @@ +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +import { getMatchingContentFilesWithFrontMatter } from './get-content-files'; + +describe('getMatchingContentFilesWithFrontMatter', () => { + let workspaceRoot: string; + const rootDir = '.'; + const contentDir = '/src/content/docs'; + + beforeEach(() => { + workspaceRoot = mkdtempSync(join(tmpdir(), 'analog-content-')); + mkdirSync(join(workspaceRoot, 'src/content/docs/erste-schritte'), { + recursive: true, + }); + mkdirSync(join(workspaceRoot, 'src/content/docs/assets'), { + recursive: true, + }); + writeFileSync( + join(workspaceRoot, 'src/content/docs/intro.md'), + '---\ntitle: Intro\n---\n# Intro', + ); + writeFileSync( + join(workspaceRoot, 'src/content/docs/erste-schritte/willkommen.md'), + '---\ntitle: Willkommen\n---\n# Willkommen', + ); + writeFileSync( + join(workspaceRoot, 'src/content/docs/assets/hochladen.md'), + '---\ntitle: Hochladen\n---\n# Hochladen', + ); + }); + + afterEach(() => { + rmSync(workspaceRoot, { recursive: true, force: true }); + }); + + it('returns only top-level files by default', () => { + const files = getMatchingContentFilesWithFrontMatter( + workspaceRoot, + rootDir, + contentDir, + ); + + expect(files.map((f) => f.name).sort()).toEqual(['intro']); + }); + + it('returns nested files when recursive is enabled', () => { + const files = getMatchingContentFilesWithFrontMatter( + workspaceRoot, + rootDir, + contentDir, + true, + ); + + expect(files.map((f) => f.name).sort()).toEqual([ + 'hochladen', + 'intro', + 'willkommen', + ]); + }); + + it('exposes the directory relative to contentDir as relativePath', () => { + const files = getMatchingContentFilesWithFrontMatter( + workspaceRoot, + rootDir, + contentDir, + true, + ); + + const byName = Object.fromEntries(files.map((f) => [f.name, f])); + expect(byName['intro'].relativePath).toBe(''); + expect(byName['willkommen'].relativePath).toBe('erste-schritte'); + expect(byName['hochladen'].relativePath).toBe('assets'); + }); +}); diff --git a/packages/vite-plugin-nitro/src/lib/utils/get-content-files.ts b/packages/vite-plugin-nitro/src/lib/utils/get-content-files.ts new file mode 100644 index 000000000..0c2030fa8 --- /dev/null +++ b/packages/vite-plugin-nitro/src/lib/utils/get-content-files.ts @@ -0,0 +1,133 @@ +import { readFileSync } from 'node:fs'; +import { join, relative, resolve } from 'node:path'; +import { normalizePath } from 'vite'; +import { createRequire } from 'node:module'; +import { globSync } from 'tinyglobby'; + +import { PrerenderContentFile } from '../options'; + +const require = createRequire(import.meta.url); + +/** + * Discovers content files with front matter and extracts metadata for prerendering. + * + * This function: + * 1. Discovers all content files matching the specified glob pattern + * 2. Reads each file and parses front matter metadata + * 3. Extracts file name, extension, and path information + * 4. Returns structured data for prerendering content pages + * + * @param workspaceRoot The workspace root directory path + * @param rootDir The project root directory relative to workspace + * @param glob The glob pattern to match content files (e.g., 'content/blog') + * @returns Array of PrerenderContentFile objects with metadata and front matter + * + * Example usage: + * const contentFiles = getMatchingContentFilesWithFrontMatter( + * '/workspace', + * 'apps/my-app', + * 'content/blog' + * ); + * + * Sample discovered file paths: + * - /workspace/apps/my-app/content/blog/first-post.md + * - /workspace/apps/my-app/content/blog/2024/01/hello-world.md + * - /workspace/apps/my-app/content/blog/tech/angular-v17.mdx + * - /workspace/apps/my-app/content/blog/about/index.md + * + * Sample output structure: + * { + * name: 'first-post', + * extension: 'md', + * path: 'content/blog', + * attributes: { title: 'My First Post', date: '2024-01-01', tags: ['intro'] } + * } + * + * tinyglobby vs fast-glob comparison: + * - Both support the same glob patterns for file discovery + * - Both are efficient for finding content files + * - tinyglobby is now used instead of fast-glob + * - tinyglobby provides similar functionality with smaller bundle size + * - tinyglobby's globSync returns absolute paths when absolute: true is set + * + * Front matter parsing: + * - Uses front-matter library to parse YAML/TOML front matter + * - Extracts metadata like title, date, tags, author, etc. + * - Supports both YAML (---) and TOML (+++) delimiters + * - Returns structured attributes for prerendering + * + * File path processing: + * - Normalizes paths for cross-platform compatibility + * - Extracts file name without extension + * - Determines file extension for content type handling + * - Maintains relative path structure for routing + */ +export function getMatchingContentFilesWithFrontMatter( + workspaceRoot: string, + rootDir: string, + glob: string, + recursive = false, +): PrerenderContentFile[] { + // Dynamically require front-matter library + // eslint-disable-next-line @typescript-eslint/no-var-requires + const fm = require('front-matter'); + + // Normalize the project root path for consistent path handling + const root = normalizePath(resolve(workspaceRoot, rootDir)); + + // Resolve the content directory path relative to the project root + const resolvedDir = normalizePath(relative(root, join(root, glob))); + + // Discover all content files in the specified directory. + // Default pattern matches only top-level files; recursive opt-in walks subdirectories. + const pattern = recursive + ? `${root}/${resolvedDir}/**/*` + : `${root}/${resolvedDir}/*`; + const contentFiles: string[] = globSync([pattern], { + dot: true, + absolute: true, + onlyFiles: true, + }); + + const dirPrefix = `${root}/${resolvedDir}`; + + // Process each discovered content file to extract metadata and front matter + const mappedFilesWithFm: PrerenderContentFile[] = contentFiles.map((f) => { + // Read the file contents as UTF-8 text + const fileContents = readFileSync(f, 'utf8'); + + // Parse front matter from the file content + const raw = fm(fileContents); + + const filepath = normalizePath(f).replace(root, ''); + + const match = filepath.match(/\/([^/.]+)(\.([^/.]+))?$/); + let name = ''; + let extension = ''; + if (match) { + name = match[1]; // File name without extension + extension = match[3] || ''; // File extension or empty string if no extension + } + + // Path of the file's directory relative to the configured contentDir. + // For top-level files this is an empty string; for nested files it + // gives transforms enough context to disambiguate identically-named + // files (e.g. docs/a/post.md vs docs/b/post.md). + const relativeDir = normalizePath(relative(dirPrefix, f)); + const lastSlash = relativeDir.lastIndexOf('/'); + const relativePath = + lastSlash === -1 ? '' : relativeDir.slice(0, lastSlash); + + // Return structured content file data for prerendering + return { + name, + extension, + path: resolvedDir, + attributes: raw.attributes as { attributes: Record }, + content: fileContents, + relativePath, + }; + }); + + return mappedFilesWithFm; +} diff --git a/packages/vite-plugin-nitro/src/lib/utils/get-page-handlers.ts b/packages/vite-plugin-nitro/src/lib/utils/get-page-handlers.ts new file mode 100644 index 000000000..c4d965be0 --- /dev/null +++ b/packages/vite-plugin-nitro/src/lib/utils/get-page-handlers.ts @@ -0,0 +1,112 @@ +import { resolve, relative } from 'node:path'; +import { globSync } from 'tinyglobby'; + +import type { NitroEventHandler } from 'nitro/types'; +import { normalizePath } from 'vite'; + +type GetHandlersArgs = { + workspaceRoot: string; + sourceRoot: string; + rootDir: string; + additionalPagesDirs?: string[]; + hasAPIDir?: boolean; +}; + +/** + * Discovers and generates Nitro event handlers for server-side page routes. + * + * This function: + * 1. Discovers all .server.ts files in the app/pages directory and additional pages directories + * 2. Converts file paths to route patterns using Angular-style route syntax + * 3. Generates Nitro event handlers with proper route mapping and lazy loading + * 4. Handles dynamic route parameters and catch-all routes + * + * @param workspaceRoot The workspace root directory path + * @param sourceRoot The source directory path (e.g., 'src') + * @param rootDir The project root directory relative to workspace + * @param additionalPagesDirs Optional array of additional pages directories to scan + * @param hasAPIDir Whether the project has an API directory (affects route prefixing) + * @returns Array of NitroEventHandler objects with handler paths and route patterns + * + * Example usage: + * const handlers = getPageHandlers({ + * workspaceRoot: '/workspace', + * sourceRoot: 'src', + * rootDir: 'apps/my-app', + * additionalPagesDirs: ['/libs/shared/pages'], + * hasAPIDir: true + * }); + * + * Sample discovered file paths: + * - /workspace/apps/my-app/src/app/pages/index.server.ts + * - /workspace/apps/my-app/src/app/pages/users/[id].server.ts + * - /workspace/apps/my-app/src/app/pages/products/[...slug].server.ts + * - /workspace/apps/my-app/src/app/pages/(auth)/login.server.ts + * + * Route transformation examples: + * - index.server.ts → /_analog/pages/index + * - users/[id].server.ts → /_analog/pages/users/:id + * - products/[...slug].server.ts → /_analog/pages/products/**:slug + * - (auth)/login.server.ts → /_analog/pages/-auth-/login + * + * tinyglobby vs fast-glob comparison: + * - Both support the same glob patterns for file discovery + * - Both are efficient for finding server-side page files + * - tinyglobby is now used instead of fast-glob + * - tinyglobby provides similar functionality with smaller bundle size + * - tinyglobby's globSync returns absolute paths when absolute: true is set + * + * Route transformation rules: + * 1. Removes .server.ts extension + * 2. Converts [param] to :param for dynamic routes + * 3. Converts [...param] to **:param for catch-all routes + * 4. Converts (group) to -group- for route groups + * 5. Converts dots to forward slashes + * 6. Prefixes with /_analog/pages and optionally /api + */ +export function getPageHandlers({ + workspaceRoot, + sourceRoot, + rootDir, + additionalPagesDirs, + hasAPIDir, +}: GetHandlersArgs): NitroEventHandler[] { + // Normalize the project root path for consistent path handling + const root = normalizePath(resolve(workspaceRoot, rootDir)); + + // Discover all .server.ts files in the app/pages directory and additional pages directories + // Pattern: looks for any .server.ts files in app/pages/**/*.server.ts and additional directories + const endpointFiles: string[] = globSync( + [ + `${root}/${sourceRoot}/app/pages/**/*.server.ts`, + ...(additionalPagesDirs || []).map( + (dir) => `${workspaceRoot}${dir}/**/*.server.ts`, + ), + ], + { dot: true, absolute: true }, + ); + + // Transform each discovered file into a Nitro event handler + const handlers: NitroEventHandler[] = endpointFiles.map((endpointFile) => { + // Normalize the endpoint file path for consistent path handling + const normalized = normalizePath(endpointFile); + // Transform the normalized path into a route pattern + const route = normalized + .replace(/^(.*?)\/pages/, '/pages') + .replace(/\.server\.ts$/, '') // Remove .server.ts extension + .replace(/\[\.{3}(.+)\]/g, '**:$1') // Convert [...param] to **:param (catch-all routes) + .replace(/\[\.{3}(\w+)\]/g, '**:$1') // Alternative catch-all pattern + .replace(/\/\((.*?)\)$/, '/-$1-') // Convert (group) to -group- (route groups) + .replace(/\[(\w+)\]/g, ':$1') // Convert [param] to :param (dynamic routes) + .replace(/\./g, '/'); // Convert dots to forward slashes + + // Return Nitro event handler with absolute handler path and transformed route + return { + handler: endpointFile, + route: `${hasAPIDir ? '/api' : ''}/_analog${route}`, + lazy: true, + }; + }); + + return handlers; +} diff --git a/packages/vite-plugin-nitro/src/lib/utils/i18n-prerender.spec.ts b/packages/vite-plugin-nitro/src/lib/utils/i18n-prerender.spec.ts new file mode 100644 index 000000000..825b83141 --- /dev/null +++ b/packages/vite-plugin-nitro/src/lib/utils/i18n-prerender.spec.ts @@ -0,0 +1,186 @@ +import { describe, expect, it } from 'vitest'; +import { + expandRoutesWithLocales, + detectLocaleFromRoute, + setHtmlLang, +} from './i18n-prerender'; +import { getHreflangAlternates, stripLocalePrefix } from '../build-sitemap'; +import { I18nPrerenderOptions } from '../options'; + +const i18n: I18nPrerenderOptions = { + defaultLocale: 'en', + locales: ['en', 'fr', 'de'], +}; + +describe('expandRoutesWithLocales', () => { + it('should expand a single route to all locales', () => { + const result = expandRoutesWithLocales(['/about'], i18n); + + expect(result).toContain('/en/about'); + expect(result).toContain('/fr/about'); + expect(result).toContain('/de/about'); + }); + + it('should handle the root route', () => { + const result = expandRoutesWithLocales(['/'], i18n); + + expect(result).toContain('/en'); + expect(result).toContain('/fr'); + expect(result).toContain('/de'); + }); + + it('should keep the unprefixed root route for the default locale', () => { + const result = expandRoutesWithLocales(['/'], i18n); + + expect(result).toContain('/'); + }); + + it('should not prefix API routes', () => { + const result = expandRoutesWithLocales( + ['/about', '/api/v1/users', '/api/_analog/pages/about'], + i18n, + ); + + expect(result).toContain('/api/v1/users'); + expect(result).toContain('/api/_analog/pages/about'); + expect(result).not.toContain('/en/api/v1/users'); + }); + + it('should expand multiple routes', () => { + const result = expandRoutesWithLocales(['/about', '/contact'], i18n); + + expect(result).toContain('/en/about'); + expect(result).toContain('/fr/about'); + expect(result).toContain('/de/about'); + expect(result).toContain('/en/contact'); + expect(result).toContain('/fr/contact'); + expect(result).toContain('/de/contact'); + }); + + it('should not duplicate routes', () => { + const result = expandRoutesWithLocales(['/about'], i18n); + const aboutRoutes = result.filter((r) => r === '/about'); + + expect(aboutRoutes.length).toBeLessThanOrEqual(1); + }); +}); + +describe('detectLocaleFromRoute', () => { + it('should detect locale from route prefix', () => { + expect(detectLocaleFromRoute('/fr/about', i18n)).toBe('fr'); + expect(detectLocaleFromRoute('/de/contact', i18n)).toBe('de'); + expect(detectLocaleFromRoute('/en', i18n)).toBe('en'); + }); + + it('should return defaultLocale for routes without locale prefix', () => { + expect(detectLocaleFromRoute('/about', i18n)).toBe('en'); + expect(detectLocaleFromRoute('/', i18n)).toBe('en'); + }); + + it('should not match non-configured locales', () => { + expect(detectLocaleFromRoute('/es/about', i18n)).toBe('en'); + }); +}); + +describe('setHtmlLang', () => { + it('should add lang attribute to html tag', () => { + const html = ''; + const result = setHtmlLang(html, 'fr'); + + expect(result).toBe(''); + }); + + it('should replace existing lang attribute', () => { + const html = ''; + const result = setHtmlLang(html, 'de'); + + expect(result).toBe(''); + }); + + it('should preserve other attributes on html tag', () => { + const html = ''; + const result = setHtmlLang(html, 'fr'); + + expect(result).toContain('lang="fr"'); + expect(result).toContain('class="dark"'); + expect(result).toContain('dir="ltr"'); + }); +}); + +describe('getHreflangAlternates', () => { + it('should generate alternates for all locales plus x-default', () => { + const alternates = getHreflangAlternates( + 'https://example.com/fr/about', + 'https://example.com', + i18n, + ); + + expect(alternates).toContainEqual({ + locale: 'en', + href: 'https://example.com/en/about', + }); + expect(alternates).toContainEqual({ + locale: 'fr', + href: 'https://example.com/fr/about', + }); + expect(alternates).toContainEqual({ + locale: 'de', + href: 'https://example.com/de/about', + }); + expect(alternates).toContainEqual({ + locale: 'x-default', + href: 'https://example.com/en/about', + }); + }); + + it('should handle root locale paths', () => { + const alternates = getHreflangAlternates( + 'https://example.com/fr', + 'https://example.com', + i18n, + ); + + expect(alternates).toContainEqual({ + locale: 'en', + href: 'https://example.com/en', + }); + expect(alternates).toContainEqual({ + locale: 'fr', + href: 'https://example.com/fr', + }); + }); + + it('should handle host with trailing slash', () => { + const alternates = getHreflangAlternates( + 'https://example.com/en/about', + 'https://example.com/', + i18n, + ); + + expect(alternates).toContainEqual({ + locale: 'en', + href: 'https://example.com/en/about', + }); + }); +}); + +describe('stripLocalePrefix', () => { + it('should strip locale from path', () => { + expect(stripLocalePrefix('/fr/about', ['en', 'fr'])).toBe('/about'); + expect(stripLocalePrefix('/en/products/123', ['en', 'fr'])).toBe( + '/products/123', + ); + }); + + it('should return root for locale-only path', () => { + expect(stripLocalePrefix('/fr', ['en', 'fr'])).toBe('/'); + }); + + it('should return path unchanged if no locale prefix', () => { + expect(stripLocalePrefix('/about', ['en', 'fr'])).toBe('/about'); + }); + + it('should return root for empty path', () => { + expect(stripLocalePrefix('', ['en', 'fr'])).toBe('/'); + }); +}); diff --git a/packages/vite-plugin-nitro/src/lib/utils/i18n-prerender.ts b/packages/vite-plugin-nitro/src/lib/utils/i18n-prerender.ts new file mode 100644 index 000000000..632c37348 --- /dev/null +++ b/packages/vite-plugin-nitro/src/lib/utils/i18n-prerender.ts @@ -0,0 +1,105 @@ +import { PrerenderRoute } from 'nitropack'; +import { I18nPrerenderOptions } from '../options.js'; + +/** + * Expands a list of routes to include locale-prefixed variants. + * + * For each route and each locale, generates a prefixed route: + * '/' + locale + route + * + * The default locale's routes are included both with and without the prefix + * so that `/about` and `/en/about` both render. + * + * @param routes - The original routes to expand + * @param i18n - The i18n prerender configuration + * @returns Expanded routes with locale prefixes + */ +export function expandRoutesWithLocales( + routes: string[], + i18n: I18nPrerenderOptions, +): string[] { + const expanded: string[] = []; + + for (const route of routes) { + // Skip API routes — they don't need locale prefixes + if (route.includes('/_analog/') || route.startsWith('/api/')) { + expanded.push(route); + continue; + } + + for (const locale of i18n.locales) { + const prefix = `/${locale}`; + const localizedRoute = route === '/' ? prefix : `${prefix}${route}`; + expanded.push(localizedRoute); + } + + // Keep the unprefixed route for the default locale + if (!expanded.includes(route)) { + expanded.push(route); + } + } + + return expanded; +} + +/** + * Creates a post-rendering hook that injects the `lang` attribute + * into the `` tag of prerendered pages based on the route's + * locale prefix. + * + * @param i18n - The i18n prerender configuration + * @returns A post-rendering hook function + */ +export function createI18nPostRenderingHook( + i18n: I18nPrerenderOptions, +): (route: PrerenderRoute) => Promise { + return async (route: PrerenderRoute) => { + if (!route.contents || typeof route.contents !== 'string') { + return; + } + + const locale = detectLocaleFromRoute(route.route, i18n); + if (!locale) { + return; + } + + // Inject or replace the lang attribute on + route.contents = setHtmlLang(route.contents, locale); + }; +} + +/** + * Detects the locale from a prerendered route path by checking + * the first path segment against the configured locales. + */ +export function detectLocaleFromRoute( + route: string, + i18n: I18nPrerenderOptions, +): string { + const segments = route.split('/').filter(Boolean); + const firstSegment = segments[0]; + + if (firstSegment && i18n.locales.includes(firstSegment)) { + return firstSegment; + } + + return i18n.defaultLocale; +} + +/** + * Sets the `lang` attribute on the `` tag in an HTML string. + * If a `lang` attribute already exists, it is replaced. + * If no `lang` attribute exists, it is added. + */ +export function setHtmlLang(html: string, locale: string): string { + // Replace existing lang attribute + if (/]*\slang\s*=\s*["'][^"']*["']/i.test(html)) { + return html.replace( + /(]*\s)lang\s*=\s*["'][^"']*["']/i, + `$1lang="${locale}"`, + ); + } + + // Add lang attribute to tag + return html.replace(/(modulePath: string | URL): Promise { + return new Function('modulePath', `return import(modulePath);`)( + modulePath, + ) as Promise; +} diff --git a/packages/vite-plugin-nitro/src/lib/utils/node-web-bridge.spec.ts b/packages/vite-plugin-nitro/src/lib/utils/node-web-bridge.spec.ts new file mode 100644 index 000000000..ec8344a3c --- /dev/null +++ b/packages/vite-plugin-nitro/src/lib/utils/node-web-bridge.spec.ts @@ -0,0 +1,31 @@ +import type { IncomingMessage } from 'node:http'; +import { describe, expect, it } from 'vitest'; + +import { toWebRequest } from './node-web-bridge'; + +describe('toWebRequest', () => { + it('ignores HTTP/2 pseudo-headers when building web headers', () => { + const req = { + headers: { + ':authority': 'example.com', + ':method': 'GET', + ':path': '/blog', + accept: 'text/html', + host: 'example.com', + 'x-forwarded-proto': 'https', + }, + method: 'GET', + url: '/blog', + } as IncomingMessage; + + const request = toWebRequest(req); + const headerKeys = Array.from(request.headers.keys()); + + expect(request.url).toBe('http://example.com/blog'); + expect(request.headers.get('accept')).toBe('text/html'); + expect(request.headers.get('host')).toBe('example.com'); + expect(headerKeys).not.toContain(':authority'); + expect(headerKeys).not.toContain(':method'); + expect(headerKeys).not.toContain(':path'); + }); +}); diff --git a/packages/vite-plugin-nitro/src/lib/utils/node-web-bridge.ts b/packages/vite-plugin-nitro/src/lib/utils/node-web-bridge.ts new file mode 100644 index 000000000..fb97d424a --- /dev/null +++ b/packages/vite-plugin-nitro/src/lib/utils/node-web-bridge.ts @@ -0,0 +1,110 @@ +import type { + IncomingHttpHeaders, + IncomingMessage, + ServerResponse, +} from 'node:http'; +import { Readable } from 'node:stream'; +import { pipeline } from 'node:stream/promises'; + +function toWebHeaders(headers: IncomingHttpHeaders) { + return Object.entries(headers).reduce((acc, [key, value]) => { + if (value && !key.startsWith(':')) { + acc.set(key, Array.isArray(value) ? value.join(', ') : value); + } + + return acc; + }, new Headers()); +} + +export function toWebRequest(req: IncomingMessage): Request { + const protocol = 'http'; + const host = req.headers.host || 'localhost'; + const url = new URL(req.url || '/', `${protocol}://${host}`); + const body = + req.method && !['GET', 'HEAD'].includes(req.method) + ? (Readable.toWeb(req) as ReadableStream) + : undefined; + + return new Request(url, { + method: req.method, + headers: toWebHeaders(req.headers), + body, + // @ts-expect-error duplex is required for streaming request bodies in Node.js + duplex: body ? 'half' : undefined, + }); +} + +function isClientDisconnectError(error: unknown, res: ServerResponse): boolean { + if (!(error instanceof Error)) { + return false; + } + + const hasDisconnectCode = + 'code' in error && + typeof error.code === 'string' && + [ + 'ERR_STREAM_PREMATURE_CLOSE', + 'ERR_INVALID_STATE', + 'ECONNRESET', + 'EPIPE', + ].includes(error.code); + + const hasDisconnectMessage = /closed or destroyed stream/i.test( + error.message, + ); + + return ( + (res.destroyed || res.writableEnded) && + (hasDisconnectCode || hasDisconnectMessage) + ); +} + +export async function writeWebResponseToNode( + res: ServerResponse, + response: Response, +): Promise { + res.statusCode = response.status; + res.statusMessage = response.statusText; + + const setCookies = + 'getSetCookie' in response.headers && + typeof response.headers.getSetCookie === 'function' + ? response.headers.getSetCookie() + : []; + + if (setCookies.length > 0) { + res.setHeader('set-cookie', setCookies); + } + + response.headers.forEach((value, key) => { + if (key !== 'set-cookie') { + res.setHeader(key, value); + } + }); + + if (!response.body) { + res.end(); + return; + } + + // The Web ReadableStream and Node.js stream/web ReadableStream types + // are structurally identical at runtime but TypeScript treats them as + // distinct nominal types. The double-cast bridges this gap safely. + try { + await pipeline( + Readable.fromWeb( + response.body as unknown as import('node:stream/web').ReadableStream, + ), + res, + ); + } catch (error) { + // Long-lived dev responses such as SSE can be interrupted by a browser + // refresh or HMR-triggered reconnect. Those closed-stream cases are + // expected and should not surface as noisy server errors. + if (isClientDisconnectError(error, res)) { + return; + } + + throw error; + } +} diff --git a/packages/vite-plugin-nitro/src/lib/utils/register-dev-middleware.ts b/packages/vite-plugin-nitro/src/lib/utils/register-dev-middleware.ts new file mode 100644 index 000000000..4c3d5648a --- /dev/null +++ b/packages/vite-plugin-nitro/src/lib/utils/register-dev-middleware.ts @@ -0,0 +1,67 @@ +import { ViteDevServer } from 'vite'; +import { EventHandler, H3 } from 'nitro/h3'; +import { globSync } from 'tinyglobby'; + +import { toWebRequest, writeWebResponseToNode } from './node-web-bridge.js'; + +const PASSTHROUGH_HEADER = 'x-analog-passthrough'; + +/** + * Registers development server middleware by discovering and loading middleware files. + * + * Each discovered h3 middleware module is loaded through Vite's SSR loader, + * wrapped in a temporary H3 app, then bridged back into Vite's Connect stack. + * If the middleware does not write a response, control falls through to the + * next Vite middleware. + * + * @param root The project root directory path + * @param sourceRoot The source directory path (e.g., 'src') + * @param viteServer The Vite development server instance + */ +export async function registerDevServerMiddleware( + root: string, + sourceRoot: string, + viteServer: ViteDevServer, +): Promise { + const middlewareFiles = globSync( + [`${root}/${sourceRoot}/server/middleware/**/*.ts`], + { + dot: true, + absolute: true, + }, + ); + + middlewareFiles.forEach((file) => { + // Create the H3 app once per middleware file (not per request). + // The dynamic handler inside still loads the module fresh each request + // via ssrLoadModule, preserving HMR. + const app = new H3(); + app.use(async (event) => { + const handler: EventHandler = await viteServer + .ssrLoadModule(file) + .then((m: unknown) => (m as { default: EventHandler }).default); + return handler(event); + }); + // Sentinel catch-all: when the middleware returns undefined (does not + // handle the request), h3 does not emit its default 404 — instead we + // detect the passthrough header and let the Connect stack continue. + app.use( + () => + new Response(null, { + status: 204, + headers: { [PASSTHROUGH_HEADER]: '1' }, + }), + ); + + viteServer.middlewares.use(async (req, res, next) => { + const response = await app.fetch(toWebRequest(req)); + + if (response.headers.get(PASSTHROUGH_HEADER) === '1') { + next(); + return; + } + + await writeWebResponseToNode(res, response); + }); + }); +} diff --git a/packages/vite-plugin-nitro/src/lib/utils/register-i18n-watcher.spec.ts b/packages/vite-plugin-nitro/src/lib/utils/register-i18n-watcher.spec.ts new file mode 100644 index 000000000..a69195c25 --- /dev/null +++ b/packages/vite-plugin-nitro/src/lib/utils/register-i18n-watcher.spec.ts @@ -0,0 +1,84 @@ +import { describe, expect, it, vi } from 'vitest'; +import { + isTranslationFile, + registerI18nWatcher, +} from './register-i18n-watcher'; + +describe('isTranslationFile', () => { + it('should match JSON files in i18n directories', () => { + expect(isTranslationFile('src/i18n/en.json')).toBe(true); + expect(isTranslationFile('src/i18n/fr.json')).toBe(true); + expect(isTranslationFile('/abs/path/src/i18n/messages.json')).toBe(true); + }); + + it('should match XLIFF files in i18n directories', () => { + expect(isTranslationFile('src/i18n/messages.xlf')).toBe(true); + }); + + it('should match XMB files in i18n directories', () => { + expect(isTranslationFile('src/i18n/messages.xmb')).toBe(true); + }); + + it('should match ARB files in i18n directories', () => { + expect(isTranslationFile('src/i18n/intl_fr.arb')).toBe(true); + }); + + it('should not match non-translation files', () => { + expect(isTranslationFile('src/app/component.ts')).toBe(false); + expect(isTranslationFile('src/app/data.json')).toBe(false); + expect(isTranslationFile('package.json')).toBe(false); + }); + + it('should not match translation-like files outside i18n directories', () => { + expect(isTranslationFile('src/assets/config.json')).toBe(false); + }); +}); + +describe('registerI18nWatcher', () => { + it('should register change and add listeners on the watcher', () => { + const on = vi.fn(); + const viteServer = { + watcher: { on }, + ws: { send: vi.fn() }, + } as any; + + registerI18nWatcher(viteServer); + + expect(on).toHaveBeenCalledWith('change', expect.any(Function)); + expect(on).toHaveBeenCalledWith('add', expect.any(Function)); + }); + + it('should trigger full-reload when a translation file changes', () => { + const listeners: Record void> = {}; + const on = vi.fn((event: string, fn: (...args: string[]) => void) => { + listeners[event] = fn; + }); + const send = vi.fn(); + const viteServer = { + watcher: { on }, + ws: { send }, + } as any; + + registerI18nWatcher(viteServer); + listeners['change']('src/i18n/fr.json'); + + expect(send).toHaveBeenCalledWith({ type: 'full-reload' }); + }); + + it('should not trigger reload for non-translation files', () => { + const listeners: Record void> = {}; + const on = vi.fn((event: string, fn: (...args: string[]) => void) => { + listeners[event] = fn; + }); + const send = vi.fn(); + const viteServer = { + watcher: { on }, + ws: { send }, + } as any; + + registerI18nWatcher(viteServer); + listeners['change']('src/app/component.ts'); + + expect(send).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/vite-plugin-nitro/src/lib/utils/register-i18n-watcher.ts b/packages/vite-plugin-nitro/src/lib/utils/register-i18n-watcher.ts new file mode 100644 index 000000000..156a811c2 --- /dev/null +++ b/packages/vite-plugin-nitro/src/lib/utils/register-i18n-watcher.ts @@ -0,0 +1,28 @@ +import { ViteDevServer } from 'vite'; + +/** + * Registers a file watcher that triggers a full page reload + * when translation files are added or modified. + * + * Matches files in i18n directories with .json, .xlf, .xmb, or .arb extensions. + * + * @param viteServer The Vite development server instance + */ +export function registerI18nWatcher(viteServer: ViteDevServer): void { + const triggerReload = (path: string) => { + if (isTranslationFile(path)) { + viteServer.ws.send({ type: 'full-reload' }); + } + }; + + viteServer.watcher.on('change', triggerReload); + viteServer.watcher.on('add', triggerReload); +} + +/** + * Checks whether a file path looks like a translation file + * based on its location in an i18n directory and its extension. + */ +export function isTranslationFile(path: string): boolean { + return /i18n.*\.(json|xlf|xmb|arb)$/.test(path); +} diff --git a/packages/vite-plugin-nitro/src/lib/utils/renderers.spec.ts b/packages/vite-plugin-nitro/src/lib/utils/renderers.spec.ts new file mode 100644 index 000000000..b21878a5f --- /dev/null +++ b/packages/vite-plugin-nitro/src/lib/utils/renderers.spec.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from 'vitest'; + +import { apiMiddleware, clientRenderer, ssrRenderer } from './renderers'; + +describe('renderers virtual modules', () => { + it('emits an SSR renderer that serves HTML responses', () => { + const moduleSource = ssrRenderer(); + + expect(moduleSource).toContain("import template from '#analog/index';"); + expect(moduleSource).not.toContain('readFileSync('); + expect(moduleSource).toContain( + "event.res.headers.set('content-type', 'text/html; charset=utf-8');", + ); + expect(moduleSource).toContain( + 'const requestPath = normalizeHtmlRequestUrl(event.path);', + ); + expect(moduleSource).toContain('const req = event.node?.req'); + expect(moduleSource).toContain( + 'const html = await renderer(requestPath, template, { req, res, fetch: serverFetch });', + ); + expect(moduleSource).toContain("import renderer from '#analog/ssr';"); + }); + + it('emits a client renderer that serves HTML responses', () => { + const moduleSource = clientRenderer(); + + expect(moduleSource).toContain("import template from '#analog/index';"); + expect(moduleSource).not.toContain('readFileSync('); + expect(moduleSource).toContain( + "event.res.headers.set('content-type', 'text/html; charset=utf-8');", + ); + }); + + it('uses event-bound forwarding for API middleware', () => { + expect(apiMiddleware).toContain( + "import { defineHandler, fetchWithEvent, proxyRequest } from 'nitro/h3';", + ); + expect(apiMiddleware).toContain('return fetchWithEvent(event, reqUrl'); + expect(apiMiddleware).toContain('return proxyRequest(event, reqUrl);'); + }); +}); diff --git a/packages/vite-plugin-nitro/src/lib/utils/renderers.ts b/packages/vite-plugin-nitro/src/lib/utils/renderers.ts new file mode 100644 index 000000000..19fd29c10 --- /dev/null +++ b/packages/vite-plugin-nitro/src/lib/utils/renderers.ts @@ -0,0 +1,141 @@ +/** + * Code snippet emitted into virtual modules to create a request-scoped + * fetch using ofetch's `createFetch` + h3's `fetchWithEvent`. + * + * Shared between the SSR renderer and page-endpoint virtual modules so + * the fetch-wiring logic stays in sync. + * + * The emitted variable is named `serverFetch` — callers should reference it + * by that name. + */ +export const SERVER_FETCH_FACTORY_SNIPPET = ` + const serverFetch = createFetch({ + fetch: (resource, init) => { + const url = resource instanceof Request ? resource.url : resource.toString(); + return fetchWithEvent(event, url, init); + } + });`; + +/** + * SSR renderer virtual module content. + * + * This code runs inside Nitro's server runtime (Node.js context) where + * event.node is always populated. In h3 v2, event.node is typed as optional, + * so we use h3's first-class event properties (event.path, event.method) where + * possible and apply optional chaining when accessing the Node.js context for + * the Angular renderer which requires raw req/res objects. + * + * h3 v2 idiomatic APIs used: + * - defineHandler (replaces defineEventHandler / eventHandler) + * - event.path (replaces event.node.req.url) + * - getResponseHeader compat shim (still available in h3 v2) + */ +export function ssrRenderer() { + return ` +import { createFetch } from 'ofetch'; +import { defineHandler, fetchWithEvent } from 'nitro/h3'; +// @ts-ignore +import renderer from '#analog/ssr'; +import template from '#analog/index'; + +const normalizeHtmlRequestUrl = (url) => + url.replace(/\\/index\\.html(?=$|[?#])/, '/'); + +export default defineHandler(async (event) => { + event.res.headers.set('content-type', 'text/html; charset=utf-8'); + const noSSR = event.res.headers.get('x-analog-no-ssr'); + const requestPath = normalizeHtmlRequestUrl(event.path); + + if (noSSR === 'true') { + return template; + } + + // event.path is the canonical h3 v2 way to access the request URL. + // event.node?.req and event.node?.res are needed by the Angular SSR renderer + // which operates on raw Node.js request/response objects. + // During prerendering (Nitro v3 fetch-based pipeline), event.node is undefined. + // The Angular renderer requires a req object with at least { headers, url }, + // so we provide a minimal stub to avoid runtime errors in prerender context. + const req = event.node?.req + ? { + ...event.node.req, + url: requestPath, + originalUrl: requestPath, + } + : { + headers: { host: 'localhost' }, + url: requestPath, + originalUrl: requestPath, + connection: {}, + }; + const res = event.node?.res; +${SERVER_FETCH_FACTORY_SNIPPET} + + const html = await renderer(requestPath, template, { req, res, fetch: serverFetch }); + + return html; +});`; +} + +/** + * Client-only renderer virtual module content. + * + * Used when SSR is disabled — simply serves the static index.html template + * for every route, letting the client-side Angular router handle navigation. + */ +export function clientRenderer() { + return ` +import { defineHandler } from 'nitro/h3'; +import template from '#analog/index'; + +export default defineHandler(async (event) => { + event.res.headers.set('content-type', 'text/html; charset=utf-8'); + return template; +}); +`; +} + +/** + * API middleware virtual module content. + * + * Intercepts requests matching the configured API prefix and either: + * - Uses event-bound internal forwarding for GET requests (except .xml routes) + * - Uses request proxying for all other methods to forward the full request + * + * h3 v2 idiomatic APIs used: + * - defineHandler (replaces defineEventHandler / eventHandler) + * - event.path (replaces event.node.req.url) + * - event.method (replaces event.node.req.method) + * - proxyRequest is retained internally because it preserves Nitro route + * matching for event-bound server requests during SSR/prerender + * - Object.fromEntries(event.req.headers.entries()) replaces direct event.node.req.headers access + * + * `fetchWithEvent` keeps the active event context while forwarding to a + * rewritten path, which avoids falling through to the HTML renderer when + * SSR code makes relative API requests. + */ +export const apiMiddleware = ` +import { defineHandler, fetchWithEvent, proxyRequest } from 'nitro/h3'; +import { useRuntimeConfig } from 'nitro/runtime-config'; + +export default defineHandler(async (event) => { + const prefix = useRuntimeConfig().prefix; + const apiPrefix = \`\${prefix}/\${useRuntimeConfig().apiPrefix}\`; + + if (event.path?.startsWith(apiPrefix)) { + const reqUrl = event.path?.replace(apiPrefix, ''); + + if ( + event.method === 'GET' && + // in the case of XML routes, we want to proxy the request so that nitro gets the correct headers + // and can render the XML correctly as a static asset + !event.path?.endsWith('.xml') + ) { + return fetchWithEvent(event, reqUrl, { + headers: Object.fromEntries(event.req.headers.entries()), + }); + } + + return proxyRequest(event, reqUrl); + } +});`; diff --git a/packages/vite-plugin-nitro/src/lib/utils/rolldown.spec.ts b/packages/vite-plugin-nitro/src/lib/utils/rolldown.spec.ts new file mode 100644 index 000000000..031bbab0b --- /dev/null +++ b/packages/vite-plugin-nitro/src/lib/utils/rolldown.spec.ts @@ -0,0 +1,33 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +let mockRolldownVersion: string | undefined; + +vi.mock('vite', async () => { + const actual = await vi.importActual('vite'); + return { + ...actual, + get rolldownVersion() { + return mockRolldownVersion; + }, + }; +}); + +import { getBundleOptionsKey, isRolldown } from './rolldown.js'; + +describe('rolldown utils', () => { + beforeEach(() => { + mockRolldownVersion = undefined; + }); + + it('returns rolldown bundle config when rolldown is enabled', () => { + mockRolldownVersion = '1.0.0'; + + expect(isRolldown()).toBe(true); + expect(getBundleOptionsKey()).toBe('rolldownOptions'); + }); + + it('returns rollup bundle config when rolldown is unavailable', () => { + expect(isRolldown()).toBe(false); + expect(getBundleOptionsKey()).toBe('rollupOptions'); + }); +}); diff --git a/packages/vite-plugin-nitro/src/lib/utils/rolldown.ts b/packages/vite-plugin-nitro/src/lib/utils/rolldown.ts new file mode 100644 index 000000000..fd6825e4f --- /dev/null +++ b/packages/vite-plugin-nitro/src/lib/utils/rolldown.ts @@ -0,0 +1,9 @@ +import * as vite from 'vite'; + +export function isRolldown(): boolean { + return !!vite.rolldownVersion; +} + +export function getBundleOptionsKey(): 'rolldownOptions' | 'rollupOptions' { + return isRolldown() ? 'rolldownOptions' : 'rollupOptions'; +} diff --git a/packages/vite-plugin-nitro/src/lib/vite-nitro-plugin.spec.data.ts b/packages/vite-plugin-nitro/src/lib/vite-nitro-plugin.spec.data.ts new file mode 100644 index 000000000..ca4a2639c --- /dev/null +++ b/packages/vite-plugin-nitro/src/lib/vite-nitro-plugin.spec.data.ts @@ -0,0 +1,73 @@ +import type { NitroConfig } from 'nitro/types'; +import { ConfigEnv, UserConfig, Plugin } from 'vite'; +import { vi } from 'vitest'; +import { resolve } from 'node:path'; + +export const mockViteDevServer = { + middlewares: { + // eslint-disable-next-line @typescript-eslint/no-empty-function + use: () => {}, + }, +}; + +export const mockNitroConfig: NitroConfig = { + buildDir: resolve('./dist/.nitro'), + preset: undefined, + compatibilityDate: '2025-11-19', + handlers: [], + logLevel: 0, + output: { + dir: resolve('dist/analog'), + publicDir: resolve('dist/analog/public'), + }, + rootDir: '.', + scanDirs: ['src/server'], + serverDir: 'src/server', + prerender: { + crawlLinks: undefined, + }, + typescript: { + generateTsConfig: false, + }, + imports: { + autoImport: false, + }, + rollupConfig: { + plugins: [ + { + name: 'analogjs-vite-plugin-nitro-rollup-page-endpoint', + transform() { + return undefined; + }, + }, + ], + }, + routeRules: undefined, + runtimeConfig: { + apiPrefix: 'api', + }, + virtual: { + '#ANALOG_API_MIDDLEWARE': expect.anything(), + }, +}; + +export async function mockBuildFunctions() { + const buildServerImport = await import('./build-server'); + const buildServerImportSpy = vi.fn(); + buildServerImport.buildServer = buildServerImportSpy; + + const buildSitemapImport = await import('./build-sitemap'); + const buildSitemapImportSpy = vi.fn(); + buildSitemapImport.buildSitemap = buildSitemapImportSpy; + + return { buildServerImportSpy, buildSitemapImportSpy }; +} + +export async function runConfigAndCloseBundle(plugin: Plugin[]): Promise { + await ( + plugin[1].config as ( + config: UserConfig, + env: ConfigEnv, + ) => Promise + )({}, { command: 'build' } as ConfigEnv); +} diff --git a/packages/vite-plugin-nitro/src/lib/vite-plugin-nitro.spec.ts b/packages/vite-plugin-nitro/src/lib/vite-plugin-nitro.spec.ts new file mode 100644 index 000000000..e49bbe12e --- /dev/null +++ b/packages/vite-plugin-nitro/src/lib/vite-plugin-nitro.spec.ts @@ -0,0 +1,951 @@ +import { describe, expect, it, vi } from 'vitest'; +import * as vite from 'vite'; +import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'; +import { join, resolve } from 'node:path'; +import { tmpdir } from 'node:os'; + +vi.mock('nitro/builder', () => ({ + build: vi.fn(), + createDevServer: vi.fn(), + createNitro: vi.fn(), +})); + +vi.mock('./build-server'); +vi.mock('./build-sitemap'); + +vi.mock('./build-ssr', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + buildClientApp: vi.fn(), + buildSSRApp: vi.fn(), + }; +}); + +import { build, createDevServer, createNitro } from 'nitro/builder'; +import { buildClientApp } from './build-ssr'; +import { + mockBuildFunctions, + mockNitroConfig, + mockViteDevServer, + runConfigAndCloseBundle, +} from './vite-nitro-plugin.spec.data'; +import { nitro } from './vite-plugin-nitro'; + +function writeBuiltClientIndexHtml( + workspaceRoot: string, + html = '', + clientBuildDir = resolve(workspaceRoot, 'dist', 'client'), +) { + mkdirSync(clientBuildDir, { recursive: true }); + writeFileSync(resolve(clientBuildDir, 'index.html'), html); +} + +describe('nitro', () => { + afterEach(() => { + vi.restoreAllMocks(); + vi.unstubAllEnvs(); + }); + + it('should work', () => { + expect(nitro({})[1].name).toEqual('@analogjs/vite-plugin-nitro'); + }); + + it('should snapshot the incoming Vite config before later mutations', async () => { + vi.stubEnv('VITEST', ''); + vi.stubEnv('NODE_ENV', 'production'); + + const workspaceRoot = mkdtempSync(join(tmpdir(), 'analog-nitro-config-')); + const originalBuildOutDir = 'custom-client'; + const originalAlias = { '@app/root': '/virtual/original-entry.ts' }; + const originalClientEnvironmentOutDir = 'env-client-output'; + const pluginPrototype = { marker: 'user-plugin-prototype' }; + const originalHook = { handler: vi.fn(), order: 'pre' }; + const userPlugin = Object.assign(Object.create(pluginPrototype), { + name: 'user-plugin', + configResolved: originalHook, + }) as vite.Plugin; + const userConfig: vite.UserConfig = { + root: workspaceRoot, + build: { outDir: originalBuildOutDir }, + environments: { + client: { + build: { + outDir: originalClientEnvironmentOutDir, + }, + }, + } as vite.UserConfig['environments'], + plugins: [userPlugin], + resolve: { + alias: originalAlias, + }, + }; + const ssrBuildDir = resolve(workspaceRoot, 'dist', 'ssr'); + + const { buildServerImportSpy } = await mockBuildFunctions(); + vi.mocked(buildClientApp).mockImplementation(async () => { + writeBuiltClientIndexHtml( + workspaceRoot, + 'snapshot', + resolve(workspaceRoot, originalBuildOutDir), + ); + }); + + try { + mkdirSync(ssrBuildDir, { recursive: true }); + writeFileSync( + resolve(ssrBuildDir, 'main.server.js'), + 'export default async function renderer() {}', + ); + + const plugin = nitro({ workspaceRoot }); + + await (plugin[1].config as any)(userConfig, { + command: 'build', + mode: 'production', + }); + + const mutatedHook = { handler: vi.fn(), order: 'post' }; + // Simulate later config hooks mutating the user-owned object after + // Nitro's `config()` hook. The assertions below prove Nitro keeps + // building from the captured snapshot instead of drifting with those + // later edits. + userConfig.build!.outDir = 'mutated-client'; + userConfig.resolve!.alias = { + '@app/root': '/virtual/mutated-entry.ts', + }; + ( + userConfig.environments!['client'] as { build?: { outDir?: string } } + ).build = { + outDir: 'mutated-env-client-output', + }; + (userConfig.plugins![0] as Record)['configResolved'] = + mutatedHook; + userConfig.plugins!.push({ + name: 'mutated-plugin', + } as vite.Plugin); + + await (plugin[1].closeBundle as any)(); + + expect(buildClientApp).toHaveBeenCalledOnce(); + expect(buildServerImportSpy).toHaveBeenCalledOnce(); + + const capturedConfig = vi.mocked(buildClientApp).mock.calls[0]?.[0] as + | vite.UserConfig + | undefined; + const capturedPlugin = capturedConfig?.plugins?.[0] as + | Record + | undefined; + const capturedClientEnvironment = capturedConfig?.environments?.[ + 'client' + ] as { build?: { outDir?: string } } | undefined; + + expect(capturedConfig?.build?.outDir).toBe(originalBuildOutDir); + expect(capturedConfig?.resolve?.alias).toEqual(originalAlias); + expect(capturedClientEnvironment?.build?.outDir).toBe( + originalClientEnvironmentOutDir, + ); + expect(capturedConfig?.plugins).toHaveLength(1); + expect(capturedPlugin).toBeDefined(); + expect(Object.getPrototypeOf(capturedPlugin!)).toBe(pluginPrototype); + expect(capturedPlugin?.['configResolved']).toEqual(originalHook); + expect(capturedPlugin?.['configResolved']).not.toBe(originalHook); + expect(capturedPlugin?.['configResolved']).not.toBe(mutatedHook); + } finally { + rmSync(workspaceRoot, { recursive: true, force: true }); + } + }); + + it('should not call the route middleware in test mode', async () => { + // Arrange + const spy = vi.spyOn(mockViteDevServer.middlewares, 'use'); + + // Act + await (nitro({})[1].configureServer as any)(mockViteDevServer); + + // Assert + expect(spy).toHaveBeenCalledTimes(0); + expect(spy).not.toHaveBeenCalledWith('/api', expect.anything()); + }); + + it('should initialize Nitro dev mode with renderer virtual modules', async () => { + const nitroInstance = {} as never; + const devServer = { + fetch: vi.fn(), + upgrade: vi.fn(), + } as never; + const use = vi.fn(); + const once = vi.fn(); + const on = vi.fn(); + + vi.mocked(createNitro).mockResolvedValue(nitroInstance); + vi.mocked(createDevServer).mockReturnValue(devServer); + vi.mocked(build).mockResolvedValue(undefined as never); + vi.stubEnv('VITEST', ''); + vi.stubEnv('NODE_ENV', 'development'); + + const plugin = nitro({ ssr: true }); + await (plugin[1].config as any)( + {}, + { command: 'serve', mode: 'development' }, + ); + + const configureNitro = await (plugin[1].configureServer as any)({ + config: { + root: '/workspace/app', + server: { + host: '127.0.0.1', + port: 4300, + }, + }, + httpServer: { + once, + on, + }, + middlewares: { + stack: [], + use, + }, + watcher: { + on: vi.fn(), + }, + }); + + await configureNitro?.(); + + expect(createNitro).toHaveBeenCalledWith( + expect.objectContaining({ + builder: 'rollup', + dev: true, + virtual: expect.objectContaining({ + '#ANALOG_SSR_RENDERER': expect.stringContaining( + "import template from '#analog/index';", + ), + '#ANALOG_CLIENT_RENDERER': expect.stringContaining( + "import template from '#analog/index';", + ), + }), + }), + ); + expect(createDevServer).toHaveBeenCalledWith(nitroInstance); + expect(build).toHaveBeenCalledWith(nitroInstance); + expect(use).toHaveBeenCalled(); + expect(once).toHaveBeenCalledWith('listening', expect.any(Function)); + expect(on).not.toHaveBeenCalled(); + }); + + it('should use the active Vite SSR bundler config key', async () => { + vi.stubEnv('VITEST', ''); + vi.stubEnv('NODE_ENV', 'production'); + const plugin = nitro({}); + const result = await (plugin[1].config as any)( + {}, + { command: 'build', mode: 'production' }, + ); + const ssrBuild = result.environments.ssr.build; + const activeKey = vite.rolldownVersion + ? 'rolldownOptions' + : 'rollupOptions'; + const inactiveKey = vite.rolldownVersion + ? 'rollupOptions' + : 'rolldownOptions'; + + expect(ssrBuild).toHaveProperty(activeKey); + expect(ssrBuild[activeKey]).toEqual( + expect.objectContaining({ + input: expect.stringMatching(/src[\\/]+main\.server\.ts$/), + }), + ); + expect(ssrBuild.emptyOutDir).toBe(false); + expect(ssrBuild).not.toHaveProperty(inactiveKey); + }); + + it.runIf(vite.rolldownVersion)( + 'should forward nested vite rolldown codeSplitting config to the client build (Rolldown)', + async () => { + vi.stubEnv('VITEST', ''); + vi.stubEnv('NODE_ENV', 'production'); + const codeSplitting = { + groups: [{ test: /node_modules/, name: 'vendor' }], + }; + const plugin = nitro({ + vite: { + build: { + rolldownOptions: { + output: { + codeSplitting, + entryFileNames: 'assets/[name].js', + } as any, + }, + }, + }, + }); + const result = await (plugin[1].config as any)( + {}, + { command: 'build', mode: 'production' }, + ); + const clientBuild = result.environments.client.build; + + expect(clientBuild.rolldownOptions.output).toEqual( + expect.objectContaining({ + codeSplitting, + entryFileNames: 'assets/[name].js', + }), + ); + }, + ); + + it.runIf(!vite.rolldownVersion)( + 'should not have rolldownOptions when not using Rolldown', + async () => { + vi.stubEnv('VITEST', ''); + vi.stubEnv('NODE_ENV', 'production'); + const codeSplitting = { + groups: [{ test: /node_modules/, name: 'vendor' }], + }; + const plugin = nitro({ + vite: { + build: { + rolldownOptions: { + output: { + codeSplitting, + entryFileNames: 'assets/[name].js', + } as any, + }, + }, + }, + }); + const result = await (plugin[1].config as any)( + {}, + { command: 'build', mode: 'production' }, + ); + const clientBuild = result.environments.client.build; + + expect(clientBuild).not.toHaveProperty('rolldownOptions'); + }, + ); + + it.runIf(vite.rolldownVersion)( + 'should ignore codeSplitting forwarding when rolldown output is an array', + async () => { + vi.stubEnv('VITEST', ''); + vi.stubEnv('NODE_ENV', 'production'); + const plugin = nitro({ + vite: { + build: { + rolldownOptions: { + output: [{ entryFileNames: 'assets/[name].js' }] as any, + }, + }, + }, + }); + const result = await (plugin[1].config as any)( + {}, + { command: 'build', mode: 'production' }, + ); + const clientBuild = result.environments.client.build; + + expect(clientBuild.rolldownOptions).toBeUndefined(); + }, + ); + + it('should strip Rolldown-only codeSplitting from Nitro rollup builds', async () => { + vi.stubEnv('VITEST', ''); + vi.stubEnv('NODE_ENV', 'production'); + const { buildServerImportSpy } = await mockBuildFunctions(); + const workspaceRoot = mkdtempSync(join(tmpdir(), 'analog-nitro-')); + + try { + const ssrBuildDir = resolve(workspaceRoot, 'dist', 'ssr'); + const builtSsrEntry = resolve(ssrBuildDir, 'main.server.js'); + mkdirSync(ssrBuildDir, { recursive: true }); + writeFileSync( + builtSsrEntry, + 'export default async function renderer() {}', + ); + writeBuiltClientIndexHtml(workspaceRoot, 'rollup build'); + + const plugin = nitro({ + workspaceRoot, + ssrBuildDir, + }); + const result = await (plugin[1].config as any)( + {}, + { command: 'build', mode: 'production' }, + ); + await result.builder.buildApp({ + build: vi.fn().mockResolvedValue(undefined), + environments: { + client: {}, + ssr: {}, + }, + }); + + const nitroConfig = buildServerImportSpy.mock.calls[0][1]; + const bundlerConfig = { + output: { + codeSplitting: { groups: [{ test: /node_modules/, name: 'vendor' }] }, + entryFileNames: 'index.mjs', + }, + }; + + await nitroConfig.hooks['rollup:before']({}, bundlerConfig); + + expect(bundlerConfig.output).toEqual({ + entryFileNames: 'index.mjs', + }); + expect(nitroConfig.virtual?.['#analog/index']).toBe( + 'export default "rollup build";', + ); + } finally { + rmSync(workspaceRoot, { recursive: true, force: true }); + } + }); + + it('should alias the built SSR entry for Nitro server builds', async () => { + vi.stubEnv('VITEST', ''); + vi.stubEnv('NODE_ENV', 'production'); + const { buildServerImportSpy } = await mockBuildFunctions(); + const workspaceRoot = mkdtempSync(join(tmpdir(), 'analog-nitro-')); + + try { + const ssrBuildDir = resolve(workspaceRoot, 'dist', 'demo', 'ssr'); + const builtSsrEntry = resolve(ssrBuildDir, 'main.server.js'); + mkdirSync(ssrBuildDir, { recursive: true }); + writeFileSync( + builtSsrEntry, + 'export default async function renderer() {}', + ); + writeBuiltClientIndexHtml(workspaceRoot, 'ssr alias'); + + const plugin = nitro({ + ssr: true, + workspaceRoot, + ssrBuildDir, + }); + const result = await (plugin[1].config as any)( + {}, + { command: 'build', mode: 'production' }, + ); + + await result.builder.buildApp({ + build: vi.fn().mockResolvedValue(undefined), + environments: { + client: {}, + ssr: {}, + }, + }); + + const nitroConfig = buildServerImportSpy.mock.calls[0][1]; + const expectedAlias = vite.normalizePath(builtSsrEntry); + + expect(nitroConfig.alias).toEqual( + expect.objectContaining({ + '#analog/ssr': expectedAlias, + }), + ); + expect(nitroConfig.virtual?.['#ANALOG_SSR_RENDERER']).toContain( + "import renderer from '#analog/ssr';", + ); + expect(nitroConfig.virtual?.['#analog/index']).toBe( + 'export default "ssr alias";', + ); + } finally { + rmSync(workspaceRoot, { recursive: true, force: true }); + } + }); + + it('passes only canonical page routes to sitemap generation in builder.buildApp', async () => { + vi.stubEnv('VITEST', ''); + vi.stubEnv('NODE_ENV', 'production'); + const { buildSitemapImportSpy } = await mockBuildFunctions(); + const workspaceRoot = mkdtempSync(join(tmpdir(), 'analog-nitro-')); + + try { + const ssrBuildDir = resolve(workspaceRoot, 'dist', 'ssr'); + mkdirSync(ssrBuildDir, { recursive: true }); + writeFileSync( + resolve(ssrBuildDir, 'main.server.js'), + 'export default async function renderer() {}', + ); + writeBuiltClientIndexHtml(workspaceRoot, 'sitemap buildApp'); + + const plugin = nitro({ + workspaceRoot, + prerender: { + sitemap: { host: 'https://example.com' }, + routes: [ + '/about', + { + route: '/blog', + staticData: true, + sitemap: { + lastmod: '2024-02-10', + }, + }, + ], + }, + }); + const result = await (plugin[1].config as any)( + {}, + { command: 'build', mode: 'production' }, + ); + + await result.builder.buildApp({ + build: vi.fn().mockResolvedValue(undefined), + environments: { + client: {}, + ssr: {}, + }, + }); + + expect(buildSitemapImportSpy).toHaveBeenCalledWith( + {}, + { host: 'https://example.com' }, + ['/about', '/blog'], + resolve(workspaceRoot, 'dist', 'analog', 'public'), + { + '/blog': { lastmod: '2024-02-10' }, + }, + { apiPrefix: 'api' }, + ); + } finally { + rmSync(workspaceRoot, { recursive: true, force: true }); + } + }); + + it('does not require an SSR entry for client-only apps with explicit empty prerender routes', async () => { + vi.stubEnv('VITEST', ''); + vi.stubEnv('NODE_ENV', 'production'); + const { buildServerImportSpy } = await mockBuildFunctions(); + const workspaceRoot = mkdtempSync(join(tmpdir(), 'analog-nitro-')); + + try { + writeBuiltClientIndexHtml( + workspaceRoot, + 'client only explicit prerender opt-out', + ); + + const plugin = nitro({ + workspaceRoot, + ssr: false, + prerender: { + routes: [], + }, + }); + const result = await (plugin[1].config as any)( + {}, + { command: 'build', mode: 'production' }, + ); + + const builderBuild = vi.fn().mockResolvedValue(undefined); + await result.builder.buildApp({ + build: builderBuild, + environments: { + client: {}, + ssr: {}, + }, + }); + + expect(builderBuild).toHaveBeenCalledTimes(1); + expect(builderBuild).not.toHaveBeenCalledWith( + expect.objectContaining({ + config: expect.objectContaining({ + build: expect.objectContaining({ + ssr: true, + }), + }), + }), + ); + + const nitroConfig = buildServerImportSpy.mock.calls[0][1]; + expect(nitroConfig.alias?.['#analog/ssr']).toBeUndefined(); + expect(nitroConfig.virtual?.['#ANALOG_CLIENT_RENDERER']).toContain( + "import template from '#analog/index';", + ); + expect(nitroConfig.virtual?.['#analog/index']).toBe( + 'export default "client only explicit prerender opt-out";', + ); + } finally { + rmSync(workspaceRoot, { recursive: true, force: true }); + } + }); + + it('should resolve client output path correctly for nested roots without explicit build.outDir', async () => { + vi.stubEnv('VITEST', ''); + vi.stubEnv('NODE_ENV', 'production'); + const { buildServerImportSpy } = await mockBuildFunctions(); + const workspaceRoot = mkdtempSync(join(tmpdir(), 'analog-nitro-')); + + try { + const nestedRoot = 'apps/my-app'; + const ssrBuildDir = resolve(workspaceRoot, 'dist', nestedRoot, 'ssr'); + const builtSsrEntry = resolve(ssrBuildDir, 'main.server.js'); + mkdirSync(ssrBuildDir, { recursive: true }); + writeFileSync( + builtSsrEntry, + 'export default async function renderer() {}', + ); + + // The client build emits to /dist//client when no + // explicit build.outDir is set — write index.html there. + const clientBuildDir = resolve( + workspaceRoot, + 'dist', + nestedRoot, + 'client', + ); + mkdirSync(clientBuildDir, { recursive: true }); + writeFileSync( + resolve(clientBuildDir, 'index.html'), + 'nested root', + ); + + // Create the nested app source directory so the plugin can resolve it. + mkdirSync(resolve(workspaceRoot, nestedRoot, 'src/server'), { + recursive: true, + }); + + const plugin = nitro({ + workspaceRoot, + ssrBuildDir, + }); + const result = await (plugin[1].config as any)( + { root: nestedRoot }, + { command: 'build', mode: 'production' }, + ); + await result.builder.buildApp({ + build: vi.fn().mockResolvedValue(undefined), + environments: { + client: {}, + ssr: {}, + }, + }); + + const nitroConfig = buildServerImportSpy.mock.calls[0][1]; + + // registerIndexHtmlVirtual must read index.html from + // /dist//client — not //dist/client. + expect(nitroConfig.virtual?.['#analog/index']).toBe( + 'export default "nested root";', + ); + } finally { + rmSync(workspaceRoot, { recursive: true, force: true }); + } + }); + + it('uses the finalized client environment outDir during builder.buildApp', async () => { + vi.stubEnv('VITEST', ''); + vi.stubEnv('NODE_ENV', 'production'); + const { buildServerImportSpy } = await mockBuildFunctions(); + const workspaceRoot = mkdtempSync(join(tmpdir(), 'analog-nitro-')); + + try { + const nestedRoot = 'apps/my-app'; + const ssrBuildDir = resolve(workspaceRoot, 'dist', nestedRoot, 'ssr'); + const builtSsrEntry = resolve(ssrBuildDir, 'main.server.js'); + const staleClientDir = resolve( + workspaceRoot, + 'dist', + nestedRoot, + 'client', + ); + const finalClientDir = resolve( + workspaceRoot, + 'dist', + nestedRoot, + 'client-final', + ); + + mkdirSync(ssrBuildDir, { recursive: true }); + writeFileSync( + builtSsrEntry, + 'export default async function renderer() {}', + ); + writeBuiltClientIndexHtml( + workspaceRoot, + 'finalized client env', + finalClientDir, + ); + mkdirSync(resolve(workspaceRoot, nestedRoot, 'src/server'), { + recursive: true, + }); + + const plugin = nitro({ + workspaceRoot, + ssrBuildDir, + }); + const result = await (plugin[1].config as any)( + { + root: nestedRoot, + build: { + outDir: '../../dist/apps/my-app/client', + }, + }, + { command: 'build', mode: 'production' }, + ); + + await result.builder.buildApp({ + build: vi.fn().mockResolvedValue(undefined), + environments: { + client: { + config: { + build: { + outDir: '../../dist/apps/my-app/client-final', + }, + }, + }, + ssr: {}, + }, + }); + + const nitroConfig = buildServerImportSpy.mock.calls[0][1]; + + expect(nitroConfig.virtual?.['#analog/index']).toBe( + 'export default "finalized client env";', + ); + expect(nitroConfig.publicAssets).toEqual([ + { + dir: vite.normalizePath(finalClientDir), + maxAge: 0, + }, + ]); + expect(nitroConfig.publicAssets).not.toEqual([ + { + dir: vite.normalizePath(staleClientDir), + maxAge: 0, + }, + ]); + } finally { + rmSync(workspaceRoot, { recursive: true, force: true }); + } + }); + + it('falls back to the captured client index asset during closeBundle', async () => { + const { buildServerImportSpy } = await mockBuildFunctions(); + const workspaceRoot = mkdtempSync(join(tmpdir(), 'analog-nitro-')); + + try { + const ssrBuildDir = resolve(workspaceRoot, 'dist', 'ssr'); + mkdirSync(ssrBuildDir, { recursive: true }); + writeFileSync( + resolve(ssrBuildDir, 'main.server.js'), + 'export default async function renderer() {}', + ); + + const plugin = nitro({ + workspaceRoot, + ssrBuildDir, + }); + + await (plugin[1].config as any)( + {}, + { command: 'build', mode: 'production' }, + ); + + await (plugin[1].generateBundle as any)( + {}, + { + 'index.html': { + type: 'asset', + fileName: 'index.html', + source: 'captured bundle asset', + }, + }, + ); + + await (plugin[1].closeBundle as any)(); + + const nitroConfig = buildServerImportSpy.mock.calls[0][1]; + expect(nitroConfig.virtual?.['#analog/index']).toBe( + 'export default "captured bundle asset";', + ); + } finally { + rmSync(workspaceRoot, { recursive: true, force: true }); + } + }); + + it('rebuilds the client output during closeBundle when index.html is missing', async () => { + const { buildServerImportSpy } = await mockBuildFunctions(); + const workspaceRoot = mkdtempSync(join(tmpdir(), 'analog-nitro-')); + + try { + const ssrBuildDir = resolve(workspaceRoot, 'dist', 'ssr'); + mkdirSync(ssrBuildDir, { recursive: true }); + writeFileSync( + resolve(ssrBuildDir, 'main.server.js'), + 'export default async function renderer() {}', + ); + + vi.mocked(buildClientApp).mockImplementation(async () => { + writeBuiltClientIndexHtml( + workspaceRoot, + 'rebuilt client output', + ); + }); + + const plugin = nitro({ + workspaceRoot, + }); + + await (plugin[1].config as any)( + { + root: '.', + build: { + outDir: 'dist/client', + }, + }, + { command: 'build', mode: 'production' }, + ); + + await (plugin[1].closeBundle as any)(); + + expect(buildClientApp).toHaveBeenCalledWith( + expect.objectContaining({ + build: expect.objectContaining({ + outDir: 'dist/client', + }), + }), + expect.objectContaining({ + workspaceRoot, + }), + ); + + const nitroConfig = buildServerImportSpy.mock.calls[0][1]; + expect(nitroConfig.virtual?.['#analog/index']).toBe( + 'export default "rebuilt client output";', + ); + } finally { + rmSync(workspaceRoot, { recursive: true, force: true }); + } + }); + + describe.skip('preset output', () => { + it('should use the analog output paths when preset is not vercel', async () => { + // Arrange + vi.spyOn(process, 'cwd').mockReturnValue('/custom-root-directory'); + const { buildServerImportSpy } = await mockBuildFunctions(); + + const plugin = nitro({}, {}); + + // Act + await runConfigAndCloseBundle(plugin); + + // Assert + expect(buildServerImportSpy).toHaveBeenCalledWith( + {}, + expect.objectContaining({ + output: { + dir: '/custom-root-directory/dist/analog', + publicDir: '/custom-root-directory/dist/analog/public', + }, + }), + ); + }); + + it('should use the workspace root option when it is set', async () => { + // Arrange + vi.spyOn(process, 'cwd').mockReturnValue('/some-other-root-directory'); + const { buildServerImportSpy } = await mockBuildFunctions(); + + const plugin = nitro({ workspaceRoot: '/custom-root-directory' }, {}); + + // Act + await runConfigAndCloseBundle(plugin); + + // Assert + expect(buildServerImportSpy).toHaveBeenCalledWith( + { workspaceRoot: '/custom-root-directory' }, + expect.objectContaining({ + output: { + dir: '/custom-root-directory/some-other-root-directory/analog', + publicDir: + '/custom-root-directory/some-other-root-directory/analog/public', + }, + }), + ); + }); + + it('should use the .vercel output paths when preset is vercel', async () => { + // Arrange + vi.spyOn(process, 'cwd').mockReturnValue('/custom-root-directory'); + const { buildServerImportSpy } = await mockBuildFunctions(); + + const plugin = nitro({}, { preset: 'vercel' }); + + // Act + await runConfigAndCloseBundle(plugin); + + // Assert + expect(buildServerImportSpy).toHaveBeenCalledWith( + {}, + expect.objectContaining({ + preset: 'vercel', + output: { + dir: '/custom-root-directory/.vercel/output', + publicDir: '/custom-root-directory/.vercel/output/static', + }, + vercel: expect.objectContaining({ + entryFormat: 'node', + functions: expect.objectContaining({ + runtime: 'nodejs24.x', + }), + }), + }), + ); + }); + + it('should use the .vercel output paths without runtime config when preset is vercel', async () => { + // Arrange + vi.spyOn(process, 'cwd').mockReturnValue('/custom-root-directory'); + const { buildServerImportSpy } = await mockBuildFunctions(); + + const plugin = nitro({}, { preset: 'vercel' }); + + // Act + await runConfigAndCloseBundle(plugin); + + // Assert + expect(buildServerImportSpy).toHaveBeenCalledWith( + {}, + expect.objectContaining({ + preset: 'vercel', + output: { + dir: '/custom-root-directory/.vercel/output', + publicDir: '/custom-root-directory/.vercel/output/static', + }, + }), + ); + }); + + it('should use the .vercel output paths when preset is VERCEL environment variable is set', async () => { + // Arrange + vi.stubEnv('VERCEL', '1'); + vi.spyOn(process, 'cwd').mockReturnValue('/custom-root-directory'); + const { buildServerImportSpy } = await mockBuildFunctions(); + + const plugin = nitro({}, {}); + + // Act + await runConfigAndCloseBundle(plugin); + + // Assert + expect(buildServerImportSpy).toHaveBeenCalledWith( + {}, + expect.objectContaining({ + preset: 'vercel', + output: { + dir: '/custom-root-directory/.vercel/output', + publicDir: '/custom-root-directory/.vercel/output/static', + }, + vercel: expect.objectContaining({ + entryFormat: 'node', + functions: expect.objectContaining({ + runtime: 'nodejs24.x', + }), + }), + }), + ); + }); + }); +}); diff --git a/packages/vite-plugin-nitro/src/lib/vite-plugin-nitro.ts b/packages/vite-plugin-nitro/src/lib/vite-plugin-nitro.ts new file mode 100644 index 000000000..400038166 --- /dev/null +++ b/packages/vite-plugin-nitro/src/lib/vite-plugin-nitro.ts @@ -0,0 +1,1735 @@ +import type { NitroConfig, NitroEventHandler, RollupConfig } from 'nitro/types'; +import { build, createDevServer, createNitro } from 'nitro/builder'; +import * as vite from 'vite'; +import type { Plugin, UserConfig, ViteDevServer } from 'vite'; +import { mergeConfig, normalizePath } from 'vite'; +import { relative, resolve } from 'node:path'; +import { pathToFileURL } from 'node:url'; +import { existsSync, readFileSync, readdirSync } from 'node:fs'; +import type { IncomingMessage, ServerResponse } from 'node:http'; + +import { buildServer, isVercelPreset } from './build-server.js'; +import { buildClientApp, buildSSRApp } from './build-ssr.js'; +import { + Options, + PrerenderContentDir, + PrerenderContentFile, + PrerenderRouteConfig, + PrerenderSitemapConfig, +} from './options.js'; +import { pageEndpointsPlugin } from './plugins/page-endpoints.js'; +import { getPageHandlers } from './utils/get-page-handlers.js'; +import { buildSitemap } from './build-sitemap.js'; +import { devServerPlugin } from './plugins/dev-server-plugin.js'; +import { + toWebRequest, + writeWebResponseToNode, +} from './utils/node-web-bridge.js'; +import { getMatchingContentFilesWithFrontMatter } from './utils/get-content-files.js'; +import { + ssrRenderer, + clientRenderer, + apiMiddleware, +} from './utils/renderers.js'; +import { getBundleOptionsKey, isRolldown } from './utils/rolldown.js'; +import { debugNitro, debugSsr } from './utils/debug.js'; + +// Snapshot the caller-owned Vite config once so the client build, SSR handoff, +// and closeBundle all read the same view of the app. +// +// Guards against: +// - `build.outDir` can change after capture, sending the client sub-build to a +// different directory than the one Nitro later probes for `index.html`. +// - a plugin can replace `{ handler, order }` with a new wrapper object, which +// changes hook ordering for Nitro even though Nitro already "captured" config. +// - `resolve.alias` can be rewritten between the client and SSR passes, causing +// the two environments to build against different module graphs. +type ObjectHook = { handler: T; [key: string]: unknown }; + +function isObjectHook(value: unknown): value is ObjectHook { + return !!value && typeof value === 'object' && 'handler' in value; +} + +// Freeze hook-wrapper metadata without changing the executable handler. +// Value: Nitro keeps the same hook ordering and flags it captured at config() +// time, while still invoking the original plugin behavior. +// +// Guards against: a caller swapping `{ handler, order: 'pre' }` for a fresh +// `{ handler, order: 'post' }` object after Nitro's `config()` hook ran. Nitro +// should keep the captured ordering metadata instead of silently retargeting +// when the outer wrapper object changes. +function cloneObjectHook(hook: T): T { + if (!isObjectHook(hook)) { + return hook; + } + + return { + ...hook, + handler: hook.handler, + } as T; +} + +function cloneUserPlugin(plugin: T): T { + if (!plugin || typeof plugin !== 'object') return plugin; + const pluginRecord = plugin as Record; + // Preserve the original prototype because some plugins hang metadata or + // behavior off the instance instead of plain object fields. + const clone = Object.assign( + Object.create(Object.getPrototypeOf(plugin)), + pluginRecord, + ) as Record; + + for (const key of Object.keys(pluginRecord)) { + clone[key] = cloneObjectHook(pluginRecord[key]); + } + + return clone as T; +} + +function cloneEnvironmentEntries( + environments: UserConfig['environments'], +): UserConfig['environments'] { + if (!environments || typeof environments !== 'object') { + return environments; + } + + // Snapshot the per-environment overrides Nitro reads later. + // Value: environment-specific output paths and diagnostics stay aligned with + // the build Nitro already started coordinating. + // + // Guards against: a late write like + // `environments.client.build.outDir = ...` does not redirect follow-up + // diagnostics or asset lookups away from the client build Nitro already + // started coordinating. + return Object.fromEntries( + Object.entries(environments).map(([name, environment]) => { + if (!environment || typeof environment !== 'object') { + return [name, environment]; + } + + const environmentRecord = environment as Record; + return [ + name, + { + ...environmentRecord, + build: + environmentRecord['build'] && + typeof environmentRecord['build'] === 'object' + ? { ...(environmentRecord['build'] as Record) } + : environmentRecord['build'], + }, + ]; + }), + ) as UserConfig['environments']; +} + +// Take a selective snapshot of the mutable config branches Nitro re-reads +// after `config()` returns: plugin entries, build/server/test options, resolve +// aliases, and environment overrides. +// +// Value: Nitro can coordinate multiple build phases from one stable config +// view without breaking plugin identity or executable behavior. +// +// Guards against: +// - a later write to `config.build.outDir` can desynchronize where the client +// build writes files vs where Nitro tries to read them back. +// - plugin array edits after capture can add/remove behavior from one sub-build +// but not the other, which makes client and SSR resolution diverge. +// - alias rewrites after capture can make the SSR environment import different +// files than the client environment even though Nitro is orchestrating one app. +// - environment-specific build overrides can drift after capture, which makes +// diagnostics and any environment-aware follow-up logic observe the wrong +// client/SSR shape. +// +// We do not deep-clone functions or plugin instances because Nitro still needs +// the original executable behavior and plugin shape. The problem here is +// mutable container objects, not function identity. +function cloneUserConfig(userConfig: UserConfig): UserConfig { + const { environments, resolve, build, server, plugins } = userConfig; + const test = (userConfig as UserConfig & { test?: Record }) + .test; + return { + ...userConfig, + plugins: plugins?.map(cloneUserPlugin), + build: build && { ...build }, + environments: cloneEnvironmentEntries(environments), + server: server && { ...server }, + test: test && { ...test }, + resolve: resolve && { + ...resolve, + alias: Array.isArray(resolve.alias) + ? [...resolve.alias] + : resolve.alias && typeof resolve.alias === 'object' + ? { ...resolve.alias } + : resolve.alias, + }, + } as UserConfig; +} + +function createNitroMiddlewareHandler(handler: string): NitroEventHandler { + return { + route: '/**', + handler, + middleware: true, + }; +} + +/** + * Creates a `rollup:before` hook that marks specified packages as external + * in Nitro's bundler config (applied to both the server build and the + * prerender build). + * + * ## Subpath matching (Rolldown compatibility) + * + * When `bundlerConfig.external` is an **array**, Rollup automatically + * prefix-matches entries — `'rxjs'` in the array will also externalise + * `'rxjs/operators'`, `'rxjs/internal/Observable'`, etc. + * + * Rolldown (the default bundler in Nitro v3) does **not** do this. It + * treats array entries as exact strings. To keep behaviour consistent + * across both bundlers, the **function** branch already needed explicit + * subpath matching. We now use the same `isExternal` helper for all + * branches so that `'rxjs'` reliably matches `'rxjs/operators'` + * regardless of whether the existing `external` value is a function, + * array, or absent. + * + * Without this, the Nitro prerender build fails on Windows CI with: + * + * [RESOLVE_ERROR] Could not resolve 'rxjs/operators' + */ +function createRollupBeforeHook(externalEntries: string[]) { + const isExternal = (source: string) => + externalEntries.some( + (entry) => source === entry || source.startsWith(entry + '/'), + ); + + return (_nitro: unknown, bundlerConfig: RollupConfig) => { + sanitizeNitroBundlerConfig(_nitro, bundlerConfig); + + if (externalEntries.length === 0) { + return; + } + + const existing = bundlerConfig.external; + if (!existing) { + bundlerConfig.external = externalEntries; + } else if (typeof existing === 'function') { + bundlerConfig.external = ( + source: string, + importer: string | undefined, + isResolved: boolean, + ) => existing(source, importer, isResolved) || isExternal(source); + } else if (Array.isArray(existing)) { + bundlerConfig.external = [...existing, ...externalEntries]; + } else { + bundlerConfig.external = [existing as string, ...externalEntries]; + } + }; +} + +function appendNoExternals( + noExternals: NitroConfig['noExternals'], + ...entries: string[] +): NitroConfig['noExternals'] { + if (!noExternals) { + return entries; + } + + return Array.isArray(noExternals) + ? [...noExternals, ...entries] + : noExternals; +} + +/** + * Patches Nitro's internal Rollup/Rolldown bundler config to work around + * incompatibilities in the Nitro v3 alpha series. + * + * Called from the `rollup:before` hook, this function runs against the *final* + * bundler config that Nitro assembles for its server/prerender builds — it + * does NOT touch the normal Vite client or SSR environment configs. + * + * Each workaround is narrowly scoped and safe to remove once the corresponding + * upstream Nitro issue is resolved. + */ +function sanitizeNitroBundlerConfig( + _nitro: unknown, + bundlerConfig: RollupConfig, +) { + const output = bundlerConfig['output']; + if (!output || Array.isArray(output) || typeof output !== 'object') { + return; + } + + // ── 1. Remove invalid `output.codeSplitting` ──────────────────────── + // + // Nitro 3.0.1-alpha.2 adds `output.codeSplitting` to its internal bundler + // config, but Rolldown rejects it as an unknown key: + // + // Warning: Invalid output options (1 issue found) + // - For the "codeSplitting". Invalid key: Expected never but received "codeSplitting". + // + // Analog never sets this option. Removing it restores default bundler + // behavior without changing any Analog semantics. + if ('codeSplitting' in output) { + delete (output as Record)['codeSplitting']; + } + + // ── 2. Remove invalid `output.manualChunks` ───────────────────────── + // + // Nitro's default config enables manual chunking for node_modules. Under + // Nitro v3 alpha + Rollup 4.59 this crashes during the prerender rebundle: + // + // Cannot read properties of undefined (reading 'included') + // + // A single server bundle is acceptable for Analog's use case, so we strip + // `manualChunks` until the upstream bug is fixed. + if ('manualChunks' in output) { + delete (output as Record)['manualChunks']; + } + + // ── 3. Escape route params in `output.chunkFileNames` ─────────────── + // + // Nitro's `getChunkName()` derives chunk filenames from route patterns, + // using its internal `routeToFsPath()` helper to convert route params + // (`:productId` → `[productId]`) and catch-alls (`**` → `[...]`). + // + // Rollup/Rolldown interprets *any* `[token]` in the string returned by a + // `chunkFileNames` function as a placeholder. Only a handful are valid — + // `[name]`, `[hash]`, `[format]`, `[ext]` — so route-derived tokens like + // `[productId]` or `[...]` trigger a build error: + // + // "[productId]" is not a valid placeholder in the "output.chunkFileNames" pattern. + // + // We wrap the original function to replace non-standard `[token]` patterns + // with `_token_`, preserving the intended filename while avoiding the + // placeholder validation error. + // + // Example: `_routes/products/[productId].mjs` → `_routes/products/_productId_.mjs` + const VALID_ROLLUP_PLACEHOLDER = /^\[(?:name|hash|format|ext)\]$/; + const chunkFileNames = (output as Record)['chunkFileNames']; + if (typeof chunkFileNames === 'function') { + const originalFn = chunkFileNames as (...args: unknown[]) => unknown; + (output as Record)['chunkFileNames'] = ( + ...args: unknown[] + ) => { + const result = originalFn(...args); + if (typeof result !== 'string') return result; + return result.replace(/\[[^\]]+\]/g, (match: string) => + VALID_ROLLUP_PLACEHOLDER.test(match) + ? match + : `_${match.slice(1, -1)}_`, + ); + }; + } +} + +function resolveClientOutputPath( + cachedPath: string, + workspaceRoot: string, + rootDir: string, + configuredOutDir: string | undefined, +) { + if (cachedPath) { + debugNitro('resolveClientOutputPath using cached path', { + cachedPath, + workspaceRoot, + rootDir, + configuredOutDir, + }); + return cachedPath; + } + + if (configuredOutDir) { + const resolvedPath = normalizePath( + resolve(workspaceRoot, rootDir, configuredOutDir), + ); + debugNitro('resolveClientOutputPath using configured build.outDir', { + workspaceRoot, + rootDir, + configuredOutDir, + resolvedPath, + }); + return resolvedPath; + } + + // When no explicit build.outDir is set, the environment build config defaults + // to `/dist//client` for the client build. The non-SSR + // (client) and SSR paths must agree on this so that registerIndexHtmlVirtual() + // and publicAssets read from the directory the client build actually wrote to. + const resolvedPath = normalizePath( + resolve(workspaceRoot, 'dist', rootDir, 'client'), + ); + debugNitro('resolveClientOutputPath using default dist client path', { + workspaceRoot, + rootDir, + configuredOutDir, + resolvedPath, + }); + return resolvedPath; +} + +function getEnvironmentBuildOutDir(environment: unknown): string | undefined { + if (!environment || typeof environment !== 'object') { + return undefined; + } + + const environmentConfig = environment as { + config?: { + build?: { + outDir?: string; + }; + }; + build?: { + outDir?: string; + }; + }; + + return ( + environmentConfig.config?.build?.outDir ?? environmentConfig.build?.outDir + ); +} + +function resolveBuiltClientOutputPath( + cachedPath: string, + workspaceRoot: string, + rootDir: string, + configuredOutDir: string | undefined, + environment?: unknown, +) { + const environmentOutDir = getEnvironmentBuildOutDir(environment); + if (environmentOutDir) { + const resolvedPath = normalizePath( + resolve(workspaceRoot, rootDir, environmentOutDir), + ); + debugNitro('resolveBuiltClientOutputPath using environment outDir', { + cachedPath, + workspaceRoot, + rootDir, + configuredOutDir, + environmentOutDir, + resolvedPath, + }); + return resolvedPath; + } + + debugNitro('resolveBuiltClientOutputPath falling back to shared resolver', { + cachedPath, + workspaceRoot, + rootDir, + configuredOutDir, + environmentOutDir, + }); + return resolveClientOutputPath( + cachedPath, + workspaceRoot, + rootDir, + configuredOutDir, + ); +} + +function getNitroPublicOutputDir(nitroConfig: NitroConfig): string { + const publicDir = nitroConfig.output?.publicDir; + if (!publicDir) { + throw new Error( + 'Nitro public output directory is required to build the sitemap.', + ); + } + + return publicDir; +} + +function readDirectoryEntries(path: string): string[] { + try { + return readdirSync(path).sort(); + } catch (error) { + return [ + `<>`, + ]; + } +} + +function getPathDebugInfo(path: string) { + return { + rawPath: path, + normalizedPath: normalizePath(path), + exists: existsSync(path), + entries: existsSync(path) ? readDirectoryEntries(path) : [], + }; +} + +function assetSourceToString(source: string | Uint8Array) { + return typeof source === 'string' + ? source + : Buffer.from(source).toString('utf8'); +} + +function captureClientIndexHtmlFromBundle( + bundle: Record< + string, + { + type?: string; + fileName?: string; + source?: string | Uint8Array; + } + >, + hook: 'generateBundle' | 'writeBundle', +) { + const indexHtmlAsset = Object.values(bundle).find( + (chunk) => + chunk.type === 'asset' && + chunk.fileName === 'index.html' && + typeof chunk.source !== 'undefined', + ); + + if (!indexHtmlAsset?.source) { + debugNitro(`client bundle did not expose index.html during ${hook}`, { + hook, + bundleKeys: Object.keys(bundle).sort(), + assetFileNames: Object.values(bundle) + .filter((chunk) => chunk.type === 'asset') + .map((chunk) => chunk.fileName) + .filter(Boolean), + }); + return undefined; + } + + const indexHtml = assetSourceToString(indexHtmlAsset.source); + debugNitro(`captured client bundle index.html asset during ${hook}`, { + hook, + fileName: indexHtmlAsset.fileName, + htmlLength: indexHtml.length, + }); + return indexHtml; +} + +// Nitro only needs the HTML template string. Prefer the on-disk file when it +// exists, but allow the captured client asset to cover build flows where the +// client output directory disappears before Nitro assembles its virtual modules. +function registerIndexHtmlVirtual( + nitroConfig: NitroConfig, + clientOutputPath: string, + inlineIndexHtml?: string, +) { + const indexHtmlPath = resolve(clientOutputPath, 'index.html'); + debugNitro('registerIndexHtmlVirtual inspecting client output', { + platform: process.platform, + cwd: process.cwd(), + clientOutputPath, + clientOutputPathInfo: getPathDebugInfo(clientOutputPath), + indexHtmlPath, + indexHtmlExists: existsSync(indexHtmlPath), + hasInlineIndexHtml: typeof inlineIndexHtml === 'string', + }); + if (!existsSync(indexHtmlPath) && typeof inlineIndexHtml !== 'string') { + debugNitro('registerIndexHtmlVirtual missing index.html', { + platform: process.platform, + cwd: process.cwd(), + clientOutputPath, + clientOutputPathInfo: getPathDebugInfo(clientOutputPath), + indexHtmlPath, + hasInlineIndexHtml: typeof inlineIndexHtml === 'string', + nitroOutput: nitroConfig.output, + nitroPublicAssets: nitroConfig.publicAssets, + }); + throw new Error( + `[analog] Client build output not found at ${indexHtmlPath}.\n` + + `Ensure the client environment build completed successfully before the server build.`, + ); + } + const indexHtml = + typeof inlineIndexHtml === 'string' + ? inlineIndexHtml + : readFileSync(indexHtmlPath, 'utf8'); + debugNitro('registerIndexHtmlVirtual using HTML template source', { + source: + typeof inlineIndexHtml === 'string' + ? 'captured client bundle asset' + : 'client output index.html file', + indexHtmlPath, + }); + nitroConfig.virtual = { + ...nitroConfig.virtual, + '#analog/index': `export default ${JSON.stringify(indexHtml)};`, + }; +} + +/** + * Converts the built SSR entry path into a specifier that Nitro's bundler + * can resolve, including all relative `./assets/*` chunk imports inside + * the entry. + * + * The returned path **must** be an absolute filesystem path with forward + * slashes (e.g. `D:/a/analog/dist/apps/blog-app/ssr/main.server.js`). + * This lets Rollup/Rolldown determine the entry's directory and resolve + * sibling chunk imports like `./assets/core-DTazUigR.js` correctly. + * + * ## Why not pathToFileURL() on Windows? + * + * Earlier versions converted the path to a `file:///D:/a/...` URL on + * Windows, which worked with Nitro v2 + Rollup. Nitro v3 switched its + * default bundler to Rolldown, and Rolldown does **not** extract the + * importer directory from `file://` URLs. This caused every relative + * import inside the SSR entry to fail during the prerender build: + * + * [RESOLVE_ERROR] Could not resolve './assets/core-DTazUigR.js' + * in ../../dist/apps/blog-app/ssr/main.server.js + * + * `normalizePath()` (from Vite) simply converts backslashes to forward + * slashes, which both Rollup and Rolldown handle correctly on all + * platforms. + */ +function toNitroSsrEntrypointSpecifier(ssrEntryPath: string) { + return normalizePath(ssrEntryPath); +} + +function applySsrEntryAlias( + nitroConfig: NitroConfig, + options: Options | undefined, + workspaceRoot: string, + rootDir: string, +): void { + const ssrOutDir = + options?.ssrBuildDir || resolve(workspaceRoot, 'dist', rootDir, 'ssr'); + if (options?.ssr || nitroConfig.prerender?.routes?.length) { + const ssrEntryPath = resolveBuiltSsrEntryPath(ssrOutDir); + const ssrEntry = toNitroSsrEntrypointSpecifier(ssrEntryPath); + nitroConfig.alias = { + ...nitroConfig.alias, + '#analog/ssr': ssrEntry, + }; + } +} + +function resolveBuiltSsrEntryPath(ssrOutDir: string) { + const candidatePaths = [ + resolve(ssrOutDir, 'main.server.mjs'), + resolve(ssrOutDir, 'main.server.js'), + resolve(ssrOutDir, 'main.server'), + ]; + + const ssrEntryPath = candidatePaths.find((candidatePath) => + existsSync(candidatePath), + ); + + if (!ssrEntryPath) { + throw new Error( + `Unable to locate the built SSR entry in "${ssrOutDir}". Expected one of: ${candidatePaths.join( + ', ', + )}`, + ); + } + + return ssrEntryPath; +} + +export function nitro(options?: Options, nitroOptions?: NitroConfig): Plugin[] { + const workspaceRoot = options?.workspaceRoot ?? process.cwd(); + const sourceRoot = options?.sourceRoot ?? 'src'; + let isTest = process.env['NODE_ENV'] === 'test' || !!process.env['VITEST']; + const baseURL = process.env['NITRO_APP_BASE_URL'] || ''; + const prefix = baseURL ? baseURL.substring(0, baseURL.length - 1) : ''; + const apiPrefix = `/${options?.apiPrefix || 'api'}`; + const useAPIMiddleware = + typeof options?.useAPIMiddleware !== 'undefined' + ? options?.useAPIMiddleware + : true; + const viteRolldownOutput = options?.vite?.build?.rolldownOptions?.output; + // Vite's native build typing allows `output` to be either a single object or + // an array. Analog only forwards `codeSplitting` into the client environment + // when there is a single output object to merge into. + const viteRolldownOutputConfig = + viteRolldownOutput && !Array.isArray(viteRolldownOutput) + ? viteRolldownOutput + : undefined; + const codeSplitting = viteRolldownOutputConfig?.codeSplitting; + + let isBuild = false; + let isServe = false; + let ssrBuild = false; + let config: UserConfig; + let nitroConfig: NitroConfig; + let environmentBuild = false; + let hasAPIDir = false; + let clientOutputPath = ''; + let clientIndexHtml: string | undefined; + let legacyClientSubBuild = false; + const rollupExternalEntries: string[] = []; + const sitemapRoutes: string[] = []; + const routeSitemaps: Record< + string, + PrerenderSitemapConfig | (() => PrerenderSitemapConfig) + > = {}; + const routeSourceFiles: Record = {}; + let rootDir = workspaceRoot; + + return [ + (options?.ssr + ? devServerPlugin({ + entryServer: options?.entryServer, + index: options?.index, + routeRules: nitroOptions?.routeRules, + i18n: options?.i18n, + }) + : false) as Plugin, + { + name: '@analogjs/vite-plugin-nitro', + async config(userConfig, { mode, command }) { + isServe = command === 'serve'; + isBuild = command === 'build'; + ssrBuild = userConfig.build?.ssr === true; + // Capture the incoming config at the `config()` boundary so every later + // Nitro phase reads the same settings the build started with. + // + // Guards against: Nitro capturing `userConfig`, then another + // hook rewrites `build.outDir` or replaces a plugin hook wrapper. If + // we keep the live object, `closeBundle()` and the SSR handoff can end + // up reading a different config than the one the client pass started + // with. + config = cloneUserConfig(userConfig); + isTest = isTest ? isTest : mode === 'test'; + rollupExternalEntries.length = 0; + clientIndexHtml = undefined; + sitemapRoutes.length = 0; + for (const key of Object.keys(routeSitemaps)) { + delete routeSitemaps[key]; + } + for (const key of Object.keys(routeSourceFiles)) { + delete routeSourceFiles[key]; + } + + const resolvedConfigRoot = config.root + ? resolve(workspaceRoot, config.root) + : workspaceRoot; + rootDir = relative(workspaceRoot, resolvedConfigRoot) || '.'; + hasAPIDir = existsSync( + resolve( + workspaceRoot, + rootDir, + `${sourceRoot}/server/routes/${options?.apiPrefix || 'api'}`, + ), + ); + const buildPreset = + process.env['BUILD_PRESET'] ?? + (nitroOptions?.preset as string | undefined) ?? + (process.env['VERCEL'] ? 'vercel' : undefined); + + const pageHandlers = getPageHandlers({ + workspaceRoot, + sourceRoot, + rootDir, + additionalPagesDirs: options?.additionalPagesDirs, + hasAPIDir, + }); + const resolvedClientOutputPath = resolveClientOutputPath( + clientOutputPath, + workspaceRoot, + rootDir, + config.build?.outDir, + ); + debugNitro('nitro config resolved client output path', { + platform: process.platform, + workspaceRoot, + configRoot: config.root, + resolvedConfigRoot, + rootDir, + buildOutDir: config.build?.outDir, + clientOutputPath, + resolvedClientOutputPath, + hasEnvironmentConfig: !!config.environments, + clientEnvironmentOutDir: + config.environments?.['client'] && + typeof config.environments['client'] === 'object' && + 'build' in config.environments['client'] + ? ( + config.environments['client'] as { + build?: { outDir?: string }; + } + ).build?.outDir + : undefined, + }); + + nitroConfig = { + rootDir: normalizePath(rootDir), + preset: buildPreset, + compatibilityDate: '2025-11-19', + logLevel: nitroOptions?.logLevel || 0, + serverDir: normalizePath(`${sourceRoot}/server`), + scanDirs: [ + normalizePath(`${rootDir}/${sourceRoot}/server`), + ...(options?.additionalAPIDirs || []).map((dir) => + normalizePath(`${workspaceRoot}${dir}`), + ), + ], + output: { + dir: normalizePath( + resolve(workspaceRoot, 'dist', rootDir, 'analog'), + ), + publicDir: normalizePath( + resolve(workspaceRoot, 'dist', rootDir, 'analog/public'), + ), + }, + buildDir: normalizePath( + resolve(workspaceRoot, 'dist', rootDir, '.nitro'), + ), + typescript: { + generateTsConfig: false, + }, + runtimeConfig: { + apiPrefix: apiPrefix.substring(1), + prefix, + }, + // Analog provides its own renderer handler; prevent Nitro v3 from + // auto-detecting index.html in rootDir and adding a conflicting one. + renderer: false, + imports: { + autoImport: false, + }, + hooks: { + 'rollup:before': createRollupBeforeHook(rollupExternalEntries), + }, + rollupConfig: { + onwarn(warning) { + if ( + warning.message.includes('empty chunk') && + warning.message.endsWith('.server') + ) { + return; + } + }, + plugins: [pageEndpointsPlugin()], + }, + handlers: [ + ...(hasAPIDir + ? [] + : useAPIMiddleware + ? [createNitroMiddlewareHandler('#ANALOG_API_MIDDLEWARE')] + : []), + ...pageHandlers, + ], + routeRules: hasAPIDir + ? undefined + : useAPIMiddleware + ? undefined + : { + [`${prefix}${apiPrefix}/**`]: { + proxy: { to: '/**' }, + }, + }, + virtual: { + '#ANALOG_SSR_RENDERER': ssrRenderer(), + '#ANALOG_CLIENT_RENDERER': clientRenderer(), + ...(hasAPIDir ? {} : { '#ANALOG_API_MIDDLEWARE': apiMiddleware }), + }, + }; + + if (isVercelPreset(buildPreset)) { + nitroConfig = withVercelOutputAPI(nitroConfig, workspaceRoot); + } + + if (isCloudflarePreset(buildPreset)) { + nitroConfig = withCloudflareOutput(nitroConfig); + } + + if ( + isNetlifyPreset(buildPreset) && + rootDir === '.' && + !existsSync(resolve(workspaceRoot, 'netlify.toml')) + ) { + nitroConfig = withNetlifyOutputAPI(nitroConfig, workspaceRoot); + } + + if (isFirebaseAppHosting()) { + nitroConfig = withAppHostingOutput(nitroConfig); + } + + if (!ssrBuild && !isTest) { + // store the client output path for the SSR build config + clientOutputPath = resolvedClientOutputPath; + debugNitro( + 'nitro config cached client output path for later SSR/Nitro build', + { + ssrBuild, + isTest, + clientOutputPath, + }, + ); + } + + // Start with a clean alias map. #analog/index is registered as a Nitro + // virtual module after the client build, inlining the HTML template so + // the server bundle imports it instead of using readFileSync with an + // absolute path. + nitroConfig.alias = {}; + + if (isBuild) { + nitroConfig.publicAssets = [ + { dir: normalizePath(resolvedClientOutputPath), maxAge: 0 }, + ]; + + // In Nitro v3, renderer.entry is resolved via resolveModulePath() + // during options normalization, which requires a real filesystem path. + // Virtual modules (prefixed with #) can't survive this resolution. + // Instead, we add the renderer as a catch-all handler directly — + // this is functionally equivalent to what Nitro does internally + // (it converts renderer.entry into a { route: '/**', lazy: true } + // handler), but avoids the filesystem resolution step. + const rendererHandler = options?.ssr + ? '#ANALOG_SSR_RENDERER' + : '#ANALOG_CLIENT_RENDERER'; + nitroConfig.handlers = [ + ...(nitroConfig.handlers || []), + { + handler: rendererHandler, + route: '/**', + lazy: true, + }, + ]; + + if (isEmptyPrerenderRoutes(options)) { + nitroConfig.prerender = {}; + nitroConfig.prerender.routes = ['/']; + } + + if (options?.prerender) { + nitroConfig.prerender = nitroConfig.prerender ?? {}; + nitroConfig.prerender.crawlLinks = options?.prerender?.discover; + + let routes: ( + | string + | PrerenderContentDir + | PrerenderRouteConfig + | undefined + )[] = []; + + const prerenderRoutes = options?.prerender?.routes; + const hasExplicitPrerenderRoutes = + typeof prerenderRoutes === 'function' || + Array.isArray(prerenderRoutes); + if ( + isArrayWithElements(prerenderRoutes) + ) { + routes = prerenderRoutes; + } else if (typeof prerenderRoutes === 'function') { + routes = await prerenderRoutes(); + } + + const resolvedPrerenderRoutes = routes.reduce( + (prev, current) => { + if (!current) { + return prev; + } + if (typeof current === 'string') { + prev.push(current); + sitemapRoutes.push(current); + return prev; + } + + if ('route' in current) { + if (current.sitemap) { + routeSitemaps[current.route] = current.sitemap; + } + + if (current.outputSourceFile) { + const sourcePath = resolve( + workspaceRoot, + rootDir, + current.outputSourceFile, + ); + routeSourceFiles[current.route] = readFileSync( + sourcePath, + 'utf8', + ); + } + + prev.push(current.route); + sitemapRoutes.push(current.route); + + // Add the server-side data fetching endpoint URL + if ('staticData' in current) { + prev.push(`${apiPrefix}/_analog/pages/${current.route}`); + } + + return prev; + } + + const affectedFiles: PrerenderContentFile[] = + getMatchingContentFilesWithFrontMatter( + workspaceRoot, + rootDir, + current.contentDir, + current.recursive, + ); + + affectedFiles.forEach((f) => { + const result = current.transform(f); + + if (result) { + if (current.sitemap) { + routeSitemaps[result] = + current.sitemap && typeof current.sitemap === 'function' + ? current.sitemap?.(f) + : current.sitemap; + } + + if (current.outputSourceFile) { + const sourceContent = current.outputSourceFile(f); + if (sourceContent) { + routeSourceFiles[result] = sourceContent; + } + } + + prev.push(result); + sitemapRoutes.push(result); + + // Add the server-side data fetching endpoint URL + if ('staticData' in current) { + prev.push(`${apiPrefix}/_analog/pages/${result}`); + } + } + }); + + return prev; + }, + [], + ); + + nitroConfig.prerender.routes = + hasExplicitPrerenderRoutes || resolvedPrerenderRoutes.length + ? resolvedPrerenderRoutes + : (nitroConfig.prerender.routes ?? []); + } + + // ── SSR / prerender Nitro config ───────────────────────────── + // + // This block configures Nitro for builds that rebundle the SSR + // entry (main.server.{js,mjs}). That happens in two cases: + // + // 1. Full SSR apps — `options.ssr === true` + // 2. Prerender-only — no runtime SSR, but the prerender build + // still imports the SSR entry to render static pages. + // + // The original gate was `if (ssrBuild)`, which checks the Vite + // top-level `build.ssr` flag. That works for SSR-only builds but + // misses two Vite 6+ paths: + // + // a. **Vite Environment API (Vite 6+)** — SSR config lives in + // `environments.ssr.build.ssr`, not `build.ssr`, so + // `ssrBuild` is always `false`. + // b. **Prerender-only apps** (e.g. blog-app) — `options.ssr` + // is `false`, but prerender routes exist and the prerender + // build still processes the SSR entry. + // + // Without this block: + // - `rxjs` is never externalised → RESOLVE_ERROR in the + // Nitro prerender build (especially on Windows CI). + // - `moduleSideEffects` for zone.js is never set → zone.js + // side-effects may be tree-shaken. + // - The handlers list is not reassembled with page endpoints + // + the renderer catch-all. + // + // The widened condition covers all supported build paths: + // - `ssrBuild` → SSR-only build + // - `options?.ssr` → Environment API SSR + // - `nitroConfig.prerender?.routes?.length` → prerender-only + if ( + ssrBuild || + options?.ssr || + nitroConfig.prerender?.routes?.length + ) { + nitroConfig.noExternals = appendNoExternals( + nitroConfig.noExternals, + 'es-toolkit', + ); + + if (process.platform === 'win32') { + nitroConfig.noExternals = appendNoExternals( + nitroConfig.noExternals, + 'std-env', + ); + } + + rollupExternalEntries.push( + 'rxjs', + 'node-fetch-native/dist/polyfill', + // sharp is a native module with platform-specific binaries + // (e.g. @img/sharp-darwin-arm64). pnpm creates symlinks for + // ALL optional platform deps but only installs the matching + // one — leaving broken symlinks that crash Nitro's bundler + // with ENOENT during realpath(). Externalizing sharp avoids + // bundling it entirely; it resolves from node_modules at + // runtime instead. + 'sharp', + ); + + nitroConfig = { + ...nitroConfig, + handlers: [ + ...(hasAPIDir + ? [] + : useAPIMiddleware + ? [createNitroMiddlewareHandler('#ANALOG_API_MIDDLEWARE')] + : []), + ...pageHandlers, + // Preserve the renderer catch-all handler added above + { + handler: rendererHandler, + route: '/**', + lazy: true, + }, + ], + }; + } + } + + nitroConfig = mergeConfig( + nitroConfig, + nitroOptions as Record, + ); + + // Only configure Vite 8 environments + builder on the top-level + // build invocation. When buildApp's builder.build() calls re-enter + // the config hook, returning environments/builder again would create + // recursive buildApp invocations — each nesting another client build + // that re-triggers config, producing an infinite loop of + // "building client environment... ✓ 1 modules transformed". + // + // environmentBuild — already inside a buildApp call (recursion guard) + // ssrBuild — legacy SSR-only sub-build + // isServe — dev server / Vitest test runner (command: 'serve') + if (environmentBuild || ssrBuild || isServe) { + return {}; + } + + return { + environments: { + client: { + build: { + outDir: + config?.build?.outDir || + resolve(workspaceRoot, 'dist', rootDir, 'client'), + emptyOutDir: true, + // Forward code-splitting config to Rolldown when running + // under Vite 8+. `false` disables splitting (inlines all + // dynamic imports); an object configures chunk groups. + // The `!== undefined` check ensures `codeSplitting: false` + // is forwarded correctly (a truthy check would swallow it). + ...(isRolldown() && codeSplitting !== undefined + ? { + rolldownOptions: { + output: { + // Preserve any sibling Rolldown output options while + // overriding just `codeSplitting` for the client build. + ...viteRolldownOutputConfig, + codeSplitting, + }, + }, + } + : {}), + }, + }, + ssr: { + build: { + ssr: true, + [getBundleOptionsKey()]: { + input: + options?.entryServer || + resolve( + workspaceRoot, + rootDir, + `${sourceRoot}/main.server.ts`, + ), + }, + outDir: + options?.ssrBuildDir || + resolve(workspaceRoot, 'dist', rootDir, 'ssr'), + // Preserve the client build output. The client environment is + // built first and Nitro reads its index.html after SSR finishes. + emptyOutDir: false, + }, + }, + }, + builder: { + /** + * Reuse the already resolved Analog/Vite plugin graph across the + * client and SSR environments. + * + * This keeps both builds behaviorally aligned: route generation, + * content discovery, Angular transforms, Tailwind integration, and + * other Analog-specific config stay consistent between the browser + * bundle and the server bundle. + * + * The tradeoff is that the SSR environment can observe repeated + * plugin entries when Vite materializes the environment-specific + * build. Most notably, `@analogjs/vite-plugin-angular` can appear + * twice in the SSR resolved plugin list even though the app only + * configured `analog(...)` once. + * + * That duplicated name during SSR is an artifact of the shared + * plugin graph, not evidence of a broken client build. The + * duplicate-registration check in `vite-plugin-angular` therefore + * throws only for non-SSR builds, where duplicate Angular plugin + * instances would actually split the component style registries. + */ + sharedPlugins: true, + buildApp: async (builder) => { + environmentBuild = true; + debugNitro('builder.buildApp starting', { + platform: process.platform, + workspaceRoot, + rootDir, + cachedClientOutputPath: clientOutputPath, + configuredBuildOutDir: config.build?.outDir, + clientEnvironmentOutDir: getEnvironmentBuildOutDir( + builder.environments['client'], + ), + ssrEnvironmentOutDir: getEnvironmentBuildOutDir( + builder.environments['ssr'], + ), + }); + + // Client must complete before SSR — the server build reads the + // client's index.html via registerIndexHtmlVirtual(). Running + // them in parallel caused a race on Windows where emptyOutDir + // could delete client output before the server read it. + await builder.build(builder.environments['client']); + const postClientBuildOutputPath = resolveBuiltClientOutputPath( + clientOutputPath, + workspaceRoot, + rootDir, + config.build?.outDir, + builder.environments['client'], + ); + // Capture the client template before any SSR/prerender work runs. + // On Windows, later phases can leave the client output directory + // unavailable even though the client build itself succeeded. + registerIndexHtmlVirtual( + nitroConfig, + postClientBuildOutputPath, + clientIndexHtml, + ); + debugNitro('builder.buildApp completed client build', { + postClientBuildOutputPath, + postClientBuildOutputInfo: getPathDebugInfo( + postClientBuildOutputPath, + ), + postClientBuildIndexHtmlPath: resolve( + postClientBuildOutputPath, + 'index.html', + ), + postClientBuildIndexHtmlExists: existsSync( + resolve(postClientBuildOutputPath, 'index.html'), + ), + }); + + if (options?.ssr || nitroConfig.prerender?.routes?.length) { + debugSsr('builder.buildApp starting SSR build', { + ssrEnabled: options?.ssr, + prerenderRoutes: nitroConfig.prerender?.routes, + }); + + /** + * This launches the SSR environment as a second build from the + * shared plugin graph above. When debugging an apparent + * duplicate `@analogjs/vite-plugin-angular` registration, this + * is the handoff to inspect: the SSR builder replays the shared + * plugins for the server pass and may therefore expose multiple + * Angular-plugin entries in the SSR resolved config. + * + * That is expected for this orchestration path and should not + * be treated the same as a duplicated client build, where two + * Angular plugin instances would maintain separate style maps. + */ + await builder.build(builder.environments['ssr']); + debugSsr('builder.buildApp completed SSR build', { + ssrOutputPath: + options?.ssrBuildDir || + resolve(workspaceRoot, 'dist', rootDir, 'ssr'), + }); + } + + applySsrEntryAlias(nitroConfig, options, workspaceRoot, rootDir); + + const resolvedClientOutputPath = resolveBuiltClientOutputPath( + clientOutputPath, + workspaceRoot, + rootDir, + config.build?.outDir, + builder.environments['client'], + ); + + nitroConfig.publicAssets = [ + { dir: normalizePath(resolvedClientOutputPath), maxAge: 0 }, + ]; + debugNitro( + 'builder.buildApp resolved final client output path before Nitro build', + { + resolvedClientOutputPath, + resolvedClientOutputInfo: getPathDebugInfo( + resolvedClientOutputPath, + ), + nitroPublicAssets: nitroConfig.publicAssets, + }, + ); + + await buildServer(options, nitroConfig, routeSourceFiles); + + if ( + nitroConfig.prerender?.routes?.length && + options?.prerender?.sitemap + ) { + console.log('Building Sitemap...'); + // sitemap needs to be built after all directories are built + await buildSitemap( + config, + options.prerender.sitemap, + sitemapRoutes.length + ? sitemapRoutes + : nitroConfig.prerender.routes, + getNitroPublicOutputDir(nitroConfig), + routeSitemaps, + { apiPrefix: options?.apiPrefix || 'api' }, + ); + } + + console.log( + `\n\nThe '@analogjs/platform' server has been successfully built.`, + ); + }, + }, + }; + }, + generateBundle( + _options, + bundle: Record< + string, + { + type?: string; + fileName?: string; + source?: string | Uint8Array; + } + >, + ) { + if (!isBuild || ssrBuild) { + return; + } + + clientIndexHtml = + captureClientIndexHtmlFromBundle(bundle, 'generateBundle') ?? + clientIndexHtml; + }, + writeBundle( + _options, + bundle: Record< + string, + { + type?: string; + fileName?: string; + source?: string | Uint8Array; + } + >, + ) { + if (!isBuild || ssrBuild) { + return; + } + + clientIndexHtml = + captureClientIndexHtmlFromBundle(bundle, 'writeBundle') ?? + clientIndexHtml; + }, + async configureServer(viteServer: ViteDevServer) { + if (isServe && !isTest) { + const nitro = await createNitro({ + dev: true, + // Nitro's Vite builder now rejects `build()` in dev mode, but Analog's + // dev integration still relies on the builder-driven reload hooks. + // Force the server worker onto Rollup for this dev-only path. + builder: 'rollup', + ...nitroConfig, + }); + const server = createDevServer(nitro); + await build(nitro); + const nitroSourceRoots = [ + normalizePath( + resolve(workspaceRoot, rootDir, `${sourceRoot}/server`), + ), + ...(options?.additionalAPIDirs || []).map((dir) => + normalizePath(`${workspaceRoot}${dir}`), + ), + ]; + const isNitroSourceFile = (path: string) => { + const normalizedPath = normalizePath(path); + return nitroSourceRoots.some( + (root) => + normalizedPath === root || + normalizedPath.startsWith(`${root}/`), + ); + }; + let nitroRebuildPromise: Promise | undefined; + let nitroRebuildPending = false; + const rebuildNitroServer = () => { + if (nitroRebuildPromise) { + // Coalesce rapid file events so a save that touches multiple server + // route files results in one follow-up rebuild instead of many. + nitroRebuildPending = true; + return nitroRebuildPromise; + } + + nitroRebuildPromise = (async () => { + do { + nitroRebuildPending = false; + // Nitro API routes are not part of Vite's normal client HMR graph, + // so rebuild the Nitro dev server to pick up handler edits. + await build(nitro); + } while (nitroRebuildPending); + + // Reload the page after the server rebuild completes so the next + // request observes the updated API route implementation. + viteServer.ws.send('analog:debug-full-reload', { + plugin: 'vite-plugin-nitro', + reason: 'nitro-server-rebuilt', + }); + viteServer.ws.send({ type: 'full-reload' }); + })() + .catch((error: unknown) => { + viteServer.config.logger.error( + `[analog] Failed to rebuild Nitro dev server.\n${error instanceof Error ? error.stack || error.message : String(error)}`, + ); + }) + .finally(() => { + nitroRebuildPromise = undefined; + }); + + return nitroRebuildPromise; + }; + const onNitroSourceChange = (path: string) => { + if (!isNitroSourceFile(path)) { + return; + } + + void rebuildNitroServer(); + }; + + // Watch the full Nitro source roots instead of only the API route + // directory. API handlers often read helper modules, shared data, or + // middleware from elsewhere under `src/server`, and those edits should + // still rebuild the Nitro dev server and refresh connected browsers. + viteServer.watcher.on('add', onNitroSourceChange); + viteServer.watcher.on('change', onNitroSourceChange); + viteServer.watcher.on('unlink', onNitroSourceChange); + + const apiHandler = async ( + req: IncomingMessage, + res: ServerResponse, + ) => { + // Nitro v3's dev server is fetch-first, so adapt Vite's Node + // request once and let Nitro respond with a standard Web Response. + const response = await server.fetch(toWebRequest(req)); + await writeWebResponseToNode(res, response); + }; + + if (hasAPIDir) { + viteServer.middlewares.use( + ( + req: IncomingMessage, + res: ServerResponse, + next: (error?: unknown) => void, + ) => { + if (req.url?.startsWith(`${prefix}${apiPrefix}`)) { + void apiHandler(req, res).catch((error) => next(error)); + return; + } + + next(); + }, + ); + } else { + viteServer.middlewares.use( + apiPrefix, + ( + req: IncomingMessage, + res: ServerResponse, + next: (error?: unknown) => void, + ) => { + void apiHandler(req, res).catch((error) => next(error)); + }, + ); + } + + viteServer.httpServer?.once('listening', () => { + process.env['ANALOG_HOST'] = !viteServer.config.server.host + ? 'localhost' + : (viteServer.config.server.host as string); + process.env['ANALOG_PORT'] = `${viteServer.config.server.port}`; + }); + + // handle upgrades if websockets are enabled + if (nitroOptions?.experimental?.websocket) { + debugNitro('experimental websocket upgrade handler enabled'); + viteServer.httpServer?.on('upgrade', server.upgrade); + } + + console.log( + `\n\nThe server endpoints are accessible under the "${prefix}${apiPrefix}" path.`, + ); + } + }, + + async closeBundle() { + if (legacyClientSubBuild) { + return; + } + + // When builder.buildApp ran, it already handled the full + // client → SSR → Nitro pipeline. Skip to avoid double work. + if (environmentBuild) { + return; + } + + // SSR sub-build — Vite re-enters the plugin with build.ssr; + // Nitro server assembly happens only after the client pass. + if (ssrBuild) { + return; + } + + // Nx executors (and any caller that runs `vite build` without + // the Environment API) never trigger builder.buildApp, so + // closeBundle is the only place to drive the SSR + Nitro build. + if (isBuild) { + const resolvedClientOutputPath = resolveClientOutputPath( + clientOutputPath, + workspaceRoot, + rootDir, + config.build?.outDir, + ); + debugNitro( + 'closeBundle resolved client output path before legacy SSR build', + { + platform: process.platform, + workspaceRoot, + rootDir, + cachedClientOutputPath: clientOutputPath, + configuredBuildOutDir: config.build?.outDir, + resolvedClientOutputPath, + resolvedClientOutputInfo: getPathDebugInfo( + resolvedClientOutputPath, + ), + }, + ); + const indexHtmlPath = resolve(resolvedClientOutputPath, 'index.html'); + if ( + !existsSync(indexHtmlPath) && + typeof clientIndexHtml !== 'string' + ) { + debugNitro( + 'closeBundle rebuilding missing client output before SSR/Nitro', + { + platform: process.platform, + workspaceRoot, + rootDir, + configuredBuildOutDir: config.build?.outDir, + resolvedClientOutputPath, + indexHtmlPath, + }, + ); + legacyClientSubBuild = true; + try { + await buildClientApp(config, options); + } finally { + legacyClientSubBuild = false; + } + } + // Capture the client HTML before kicking off the standalone SSR build. + // This mirrors the successful sequencing from before the closeBundle + // refactor and avoids depending on the client directory surviving the + // nested SSR build on Windows. + registerIndexHtmlVirtual( + nitroConfig, + resolvedClientOutputPath, + clientIndexHtml, + ); + + if (options?.ssr) { + console.log('Building SSR application...'); + await buildSSRApp(config, options); + debugSsr('closeBundle completed standalone SSR build', { + ssrBuildDir: + options?.ssrBuildDir || + resolve(workspaceRoot, 'dist', rootDir, 'ssr'), + clientOutputPathInfo: clientOutputPath + ? getPathDebugInfo(clientOutputPath) + : null, + }); + } + + applySsrEntryAlias(nitroConfig, options, workspaceRoot, rootDir); + debugNitro( + 'closeBundle resolved client output path before Nitro build', + { + platform: process.platform, + workspaceRoot, + rootDir, + cachedClientOutputPath: clientOutputPath, + configuredBuildOutDir: config.build?.outDir, + resolvedClientOutputPath, + resolvedClientOutputInfo: getPathDebugInfo( + resolvedClientOutputPath, + ), + }, + ); + registerIndexHtmlVirtual( + nitroConfig, + resolvedClientOutputPath, + clientIndexHtml, + ); + + await buildServer(options, nitroConfig, routeSourceFiles); + + if ( + nitroConfig.prerender?.routes?.length && + options?.prerender?.sitemap + ) { + console.log('Building Sitemap...'); + await buildSitemap( + config, + options.prerender.sitemap, + sitemapRoutes.length + ? sitemapRoutes + : nitroConfig.prerender.routes, + getNitroPublicOutputDir(nitroConfig), + routeSitemaps, + { apiPrefix: options?.apiPrefix || 'api' }, + ); + } + + console.log( + `\n\nThe '@analogjs/platform' server has been successfully built.`, + ); + } + }, + }, + { + name: '@analogjs/vite-plugin-nitro-api-prefix', + config() { + return { + define: { + ANALOG_API_PREFIX: `"${baseURL.substring(1)}${apiPrefix.substring(1)}"`, + ...(options?.i18n + ? { + ANALOG_I18N_DEFAULT_LOCALE: JSON.stringify( + options.i18n.defaultLocale, + ), + ANALOG_I18N_LOCALES: JSON.stringify(options.i18n.locales), + } + : {}), + }, + }; + }, + }, + ]; +} + +function isEmptyPrerenderRoutes(options?: Options): boolean { + if (!options || isArrayWithElements(options?.prerender?.routes)) { + return false; + } + return !options.prerender?.routes; +} + +function isArrayWithElements(arr: unknown): arr is [T, ...T[]] { + return !!(Array.isArray(arr) && arr.length); +} + +const VERCEL_PRESET = 'vercel'; +// Nitro v3 consolidates the old `vercel-edge` preset into `vercel` with +// fluid compute enabled by default, so a single preset covers both +// serverless and edge deployments. +const withVercelOutputAPI = ( + nitroConfig: NitroConfig | undefined, + workspaceRoot: string, +) => ({ + ...nitroConfig, + preset: nitroConfig?.preset ?? 'vercel', + vercel: { + ...nitroConfig?.vercel, + entryFormat: nitroConfig?.vercel?.entryFormat ?? 'node', + functions: { + runtime: nitroConfig?.vercel?.functions?.runtime ?? 'nodejs24.x', + ...nitroConfig?.vercel?.functions, + }, + }, + output: { + ...nitroConfig?.output, + dir: normalizePath(resolve(workspaceRoot, '.vercel', 'output')), + publicDir: normalizePath( + resolve(workspaceRoot, '.vercel', 'output/static'), + ), + }, +}); + +// Nitro v3 uses underscore-separated preset names (e.g. `cloudflare_pages`), +// but we accept both hyphen and underscore forms for backwards compatibility. +const isCloudflarePreset = (buildPreset: string | undefined) => + process.env['CF_PAGES'] || + (buildPreset && + (buildPreset.toLowerCase().includes('cloudflare-pages') || + buildPreset.toLowerCase().includes('cloudflare_pages'))); + +const withCloudflareOutput = (nitroConfig: NitroConfig | undefined) => ({ + ...nitroConfig, + output: { + ...nitroConfig?.output, + serverDir: '{{ output.publicDir }}/_worker.js', + }, +}); + +const isFirebaseAppHosting = () => !!process.env['NG_BUILD_LOGS_JSON']; +const withAppHostingOutput = (nitroConfig: NitroConfig) => { + let hasOutput = false; + + return { + ...nitroConfig, + serveStatic: true, + rollupConfig: { + ...nitroConfig.rollupConfig, + output: { + ...nitroConfig.rollupConfig?.output, + entryFileNames: 'server.mjs', + }, + }, + hooks: { + ...nitroConfig.hooks, + compiled: () => { + if (!hasOutput) { + const buildOutput = { + errors: [], + warnings: [], + outputPaths: { + root: pathToFileURL(`${nitroConfig.output?.dir}`), + browser: pathToFileURL(`${nitroConfig.output?.publicDir}`), + server: pathToFileURL(`${nitroConfig.output?.dir}/server`), + }, + }; + + // Log the build output for Firebase App Hosting to pick up + console.log(JSON.stringify(buildOutput, null, 2)); + hasOutput = true; + } + }, + }, + }; +}; + +const isNetlifyPreset = (buildPreset: string | undefined) => + process.env['NETLIFY'] || + (buildPreset && buildPreset.toLowerCase().includes('netlify')); + +const withNetlifyOutputAPI = ( + nitroConfig: NitroConfig | undefined, + workspaceRoot: string, +) => ({ + ...nitroConfig, + output: { + ...nitroConfig?.output, + dir: normalizePath(resolve(workspaceRoot, 'netlify/functions')), + }, +}); diff --git a/packages/vite-plugin-nitro/test-data/content/01-first.md b/packages/vite-plugin-nitro/test-data/content/01-first.md new file mode 100644 index 000000000..ad9a3d21e --- /dev/null +++ b/packages/vite-plugin-nitro/test-data/content/01-first.md @@ -0,0 +1,7 @@ +--- +title: First +slug: first +description: My First Post +--- + +First Content File diff --git a/packages/vite-plugin-nitro/test-data/content/02-second.md b/packages/vite-plugin-nitro/test-data/content/02-second.md new file mode 100644 index 000000000..68492a999 --- /dev/null +++ b/packages/vite-plugin-nitro/test-data/content/02-second.md @@ -0,0 +1,6 @@ +--- +title: Second +description: My Second Post +--- + +Second Content File diff --git a/packages/vite-plugin-nitro/test-data/content/03-third.md b/packages/vite-plugin-nitro/test-data/content/03-third.md new file mode 100644 index 000000000..427717985 --- /dev/null +++ b/packages/vite-plugin-nitro/test-data/content/03-third.md @@ -0,0 +1,7 @@ +--- +title: Third (Draft) +description: My Third Post (Draft) +draft: true +--- + +Third Content File (Draft) diff --git a/packages/vite-plugin-nitro/vite.config.lib.ts b/packages/vite-plugin-nitro/vite.config.lib.ts index 42dab1f81..3a0d746d9 100644 --- a/packages/vite-plugin-nitro/vite.config.lib.ts +++ b/packages/vite-plugin-nitro/vite.config.lib.ts @@ -40,6 +40,7 @@ export default defineConfig({ lib: { entry: { 'src/index': resolve(pkgDir, 'src/index.ts'), + 'src/lib/utils/debug': resolve(pkgDir, 'src/lib/utils/debug.ts'), }, formats: ['es' as const], }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5d5309a1d..b2050aa50 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -54,9 +54,6 @@ catalogs: '@astrojs/react': specifier: ^5.0.2 version: 5.0.3 - '@babel/core': - specifier: ^7.28.6 - version: 7.29.0 '@commitlint/cli': specifier: ^20.5.0 version: 20.5.0 @@ -1509,9 +1506,9 @@ importers: '@analogjs/vite-plugin-angular': specifier: workspace:* version: link:../vite-plugin-angular - '@babel/core': - specifier: 'catalog:' - version: 7.29.0 + '@analogjs/vite-plugin-nitro': + specifier: workspace:* + version: link:../vite-plugin-nitro '@nx/angular': specifier: catalog:peerCompat version: 22.6.5(41fc5ff660d836e7ccf859f85688b300) @@ -1548,12 +1545,6 @@ importers: obug: specifier: 'catalog:' version: 2.1.1 - ofetch: - specifier: 'catalog:' - version: 2.0.0-alpha.3 - oxc-parser: - specifier: 'catalog:' - version: 0.124.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) rolldown: specifier: 'catalog:' version: 1.0.0-rc.15 @@ -1569,9 +1560,6 @@ importers: vitefu: specifier: 'catalog:' version: 1.1.3(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) - xmlbuilder2: - specifier: 'catalog:' - version: 4.0.3 packages/router: dependencies: @@ -1635,10 +1623,10 @@ importers: dependencies: '@angular-devkit/build-angular': specifier: catalog:peerAngularBuilders - version: 21.2.7(9d7c85fc87440b54c3f18e9dffd46aaa) + version: 21.2.7(0d8d439723faf789318b03d836c4657d) '@angular/build': specifier: catalog:peerAngularBuilders - version: 21.2.7(d9d75657d4631d31c9fdc49d18abebe5) + version: 21.2.7(6919e34d293f380cbe3efb02122eac30) es-toolkit: specifier: 'catalog:' version: 1.45.1 @@ -1663,7 +1651,32 @@ importers: packages/vite-plugin-angular-tools: {} - packages/vite-plugin-nitro: {} + packages/vite-plugin-nitro: + dependencies: + defu: + specifier: 'catalog:' + version: 6.1.7 + nitro: + specifier: 'catalog:' + version: 3.0.260415-beta(chokidar@5.0.0)(dotenv@16.4.7)(jiti@2.6.1)(lru-cache@11.3.5)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) + obug: + specifier: 'catalog:' + version: 2.1.1 + ofetch: + specifier: 'catalog:' + version: 2.0.0-alpha.3 + oxc-parser: + specifier: 'catalog:' + version: 0.124.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + radix3: + specifier: 'catalog:' + version: 1.1.2 + sharp: + specifier: '>=0.32.0' + version: 0.34.5 + xmlbuilder2: + specifier: 'catalog:' + version: 4.0.3 packages/vitest-angular: dependencies: @@ -16826,7 +16839,7 @@ snapshots: dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.2102.7(chokidar@5.0.0) - '@angular-devkit/build-webpack': 0.2102.7(chokidar@5.0.0)(webpack-dev-server@5.2.3(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + '@angular-devkit/build-webpack': 0.2102.7(chokidar@5.0.0)(webpack-dev-server@5.2.3(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) '@angular-devkit/core': 21.2.7(chokidar@5.0.0) '@angular/build': 21.2.7(be30e769aef7c15f3ae11d69182ce8ae) '@angular/compiler-cli': 21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2) @@ -16840,35 +16853,35 @@ snapshots: '@babel/preset-env': 7.29.0(@babel/core@7.29.0) '@babel/runtime': 7.28.6 '@discoveryjs/json-ext': 0.6.3 - '@ngtools/webpack': 21.2.7(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2))(typescript@6.0.2)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + '@ngtools/webpack': 21.2.7(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2))(typescript@6.0.2)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) ansi-colors: 4.1.3 autoprefixer: 10.4.27(postcss@8.5.6) - babel-loader: 10.0.0(@babel/core@7.29.0)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + babel-loader: 10.0.0(@babel/core@7.29.0)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) browserslist: 4.28.2 - copy-webpack-plugin: 14.0.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) - css-loader: 7.1.3(@rspack/core@1.7.11(@swc/helpers@0.5.21))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + copy-webpack-plugin: 14.0.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) + css-loader: 7.1.3(@rspack/core@1.7.11(@swc/helpers@0.5.21))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) esbuild-wasm: 0.27.3 http-proxy-middleware: 3.0.5 istanbul-lib-instrument: 6.0.3 jsonc-parser: 3.3.1 karma-source-map-support: 1.4.0 less: 4.4.2 - less-loader: 12.3.1(@rspack/core@1.7.11(@swc/helpers@0.5.21))(less@4.4.2)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) - license-webpack-plugin: 4.0.2(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + less-loader: 12.3.1(@rspack/core@1.7.11(@swc/helpers@0.5.21))(less@4.4.2)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) + license-webpack-plugin: 4.0.2(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) loader-utils: 3.3.1 - mini-css-extract-plugin: 2.10.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + mini-css-extract-plugin: 2.10.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) open: 11.0.0 ora: 9.3.0 picomatch: 4.0.4 piscina: 5.1.4 postcss: 8.5.6 - postcss-loader: 8.2.0(@rspack/core@1.7.11(@swc/helpers@0.5.21))(postcss@8.5.6)(typescript@6.0.2)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + postcss-loader: 8.2.0(@rspack/core@1.7.11(@swc/helpers@0.5.21))(postcss@8.5.6)(typescript@6.0.2)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) resolve-url-loader: 5.0.0 rxjs: 7.8.2 sass: 1.97.3 - sass-loader: 16.0.7(@rspack/core@1.7.11(@swc/helpers@0.5.21))(sass-embedded@1.99.0)(sass@1.97.3)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + sass-loader: 16.0.7(@rspack/core@1.7.11(@swc/helpers@0.5.21))(sass-embedded@1.99.0)(sass@1.97.3)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) semver: 7.7.4 - source-map-loader: 5.0.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + source-map-loader: 5.0.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) source-map-support: 0.5.21 terser: 5.46.0 tinyglobby: 0.2.15 @@ -16876,8 +16889,8 @@ snapshots: tslib: 2.8.1 typescript: 6.0.2 webpack: 5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3) - webpack-dev-middleware: 7.4.5(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) - webpack-dev-server: 5.2.3(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + webpack-dev-middleware: 7.4.5(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) + webpack-dev-server: 5.2.3(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) webpack-merge: 6.0.1 webpack-subresource-integrity: 5.1.0(html-webpack-plugin@5.6.6(@rspack/core@1.7.11(@swc/helpers@0.5.21))(webpack@5.106.1(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) optionalDependencies: @@ -16918,7 +16931,7 @@ snapshots: dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.2102.7(chokidar@5.0.0) - '@angular-devkit/build-webpack': 0.2102.7(chokidar@5.0.0)(webpack-dev-server@5.2.3(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + '@angular-devkit/build-webpack': 0.2102.7(chokidar@5.0.0)(webpack-dev-server@5.2.3(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) '@angular-devkit/core': 21.2.7(chokidar@5.0.0) '@angular/build': 21.2.7(be30e769aef7c15f3ae11d69182ce8ae) '@angular/compiler-cli': 21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2) @@ -16932,12 +16945,12 @@ snapshots: '@babel/preset-env': 7.29.0(@babel/core@7.29.0) '@babel/runtime': 7.28.6 '@discoveryjs/json-ext': 0.6.3 - '@ngtools/webpack': 21.2.7(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2))(typescript@6.0.2)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + '@ngtools/webpack': 21.2.7(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2))(typescript@6.0.2)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) ansi-colors: 4.1.3 autoprefixer: 10.4.27(postcss@8.5.6) - babel-loader: 10.0.0(@babel/core@7.29.0)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + babel-loader: 10.0.0(@babel/core@7.29.0)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) browserslist: 4.28.2 - copy-webpack-plugin: 14.0.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + copy-webpack-plugin: 14.0.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) css-loader: 7.1.3(@rspack/core@1.6.8(@swc/helpers@0.5.21))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) esbuild-wasm: 0.27.3 http-proxy-middleware: 3.0.5 @@ -16946,9 +16959,9 @@ snapshots: karma-source-map-support: 1.4.0 less: 4.4.2 less-loader: 12.3.1(@rspack/core@1.6.8(@swc/helpers@0.5.21))(less@4.4.2)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) - license-webpack-plugin: 4.0.2(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + license-webpack-plugin: 4.0.2(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) loader-utils: 3.3.1 - mini-css-extract-plugin: 2.10.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + mini-css-extract-plugin: 2.10.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) open: 11.0.0 ora: 9.3.0 picomatch: 4.0.4 @@ -16960,7 +16973,7 @@ snapshots: sass: 1.97.3 sass-loader: 16.0.7(@rspack/core@1.6.8(@swc/helpers@0.5.21))(sass-embedded@1.99.0)(sass@1.97.3)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) semver: 7.7.4 - source-map-loader: 5.0.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + source-map-loader: 5.0.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) source-map-support: 0.5.21 terser: 5.46.0 tinyglobby: 0.2.15 @@ -16968,8 +16981,8 @@ snapshots: tslib: 2.8.1 typescript: 6.0.2 webpack: 5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3) - webpack-dev-middleware: 7.4.5(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) - webpack-dev-server: 5.2.3(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + webpack-dev-middleware: 7.4.5(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) + webpack-dev-server: 5.2.3(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) webpack-merge: 6.0.1 webpack-subresource-integrity: 5.1.0(html-webpack-plugin@5.6.6(@rspack/core@1.6.8(@swc/helpers@0.5.21))(webpack@5.106.1(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) optionalDependencies: @@ -17007,104 +17020,12 @@ snapshots: - yaml optional: true - '@angular-devkit/build-angular@21.2.7(9d7c85fc87440b54c3f18e9dffd46aaa)': + '@angular-devkit/build-webpack@0.2102.7(chokidar@5.0.0)(webpack-dev-server@5.2.3(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3))': dependencies: - '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.2102.7(chokidar@5.0.0) - '@angular-devkit/build-webpack': 0.2102.7(chokidar@5.0.0)(webpack-dev-server@5.2.3(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) - '@angular-devkit/core': 21.2.7(chokidar@5.0.0) - '@angular/build': 21.2.7(be30e769aef7c15f3ae11d69182ce8ae) - '@angular/compiler-cli': 21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2) - '@babel/core': 7.29.0 - '@babel/generator': 7.29.1 - '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-split-export-declaration': 7.24.7 - '@babel/plugin-transform-async-generator-functions': 7.29.0(@babel/core@7.29.0) - '@babel/plugin-transform-async-to-generator': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-runtime': 7.29.0(@babel/core@7.29.0) - '@babel/preset-env': 7.29.0(@babel/core@7.29.0) - '@babel/runtime': 7.28.6 - '@discoveryjs/json-ext': 0.6.3 - '@ngtools/webpack': 21.2.7(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2))(typescript@6.0.2)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) - ansi-colors: 4.1.3 - autoprefixer: 10.4.27(postcss@8.5.6) - babel-loader: 10.0.0(@babel/core@7.29.0)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) - browserslist: 4.28.2 - copy-webpack-plugin: 14.0.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) - css-loader: 7.1.3(@rspack/core@1.7.11(@swc/helpers@0.5.21))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) - esbuild-wasm: 0.27.3 - http-proxy-middleware: 3.0.5 - istanbul-lib-instrument: 6.0.3 - jsonc-parser: 3.3.1 - karma-source-map-support: 1.4.0 - less: 4.4.2 - less-loader: 12.3.1(@rspack/core@1.7.11(@swc/helpers@0.5.21))(less@4.4.2)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) - license-webpack-plugin: 4.0.2(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) - loader-utils: 3.3.1 - mini-css-extract-plugin: 2.10.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) - open: 11.0.0 - ora: 9.3.0 - picomatch: 4.0.4 - piscina: 5.1.4 - postcss: 8.5.6 - postcss-loader: 8.2.0(@rspack/core@1.7.11(@swc/helpers@0.5.21))(postcss@8.5.6)(typescript@6.0.2)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) - resolve-url-loader: 5.0.0 rxjs: 7.8.2 - sass: 1.97.3 - sass-loader: 16.0.7(@rspack/core@1.7.11(@swc/helpers@0.5.21))(sass-embedded@1.99.0)(sass@1.97.3)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) - semver: 7.7.4 - source-map-loader: 5.0.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) - source-map-support: 0.5.21 - terser: 5.46.0 - tinyglobby: 0.2.15 - tree-kill: 1.2.2 - tslib: 2.8.1 - typescript: 6.0.2 webpack: 5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3) - webpack-dev-middleware: 7.4.5(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) - webpack-dev-server: 5.2.3(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) - webpack-merge: 6.0.1 - webpack-subresource-integrity: 5.1.0(html-webpack-plugin@5.6.6(@rspack/core@1.7.11(@swc/helpers@0.5.21))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) - optionalDependencies: - '@angular/core': 21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1) - '@angular/localize': 21.2.8(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2))(@angular/compiler@21.2.8) - '@angular/platform-browser': 21.2.8(@angular/animations@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)) - '@angular/platform-server': 21.2.8(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/compiler@21.2.8)(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.8(@angular/animations@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2) - '@angular/ssr': 21.2.7(9c898ff27dabd87c2e39c7d23b6394cf) - esbuild: 0.27.3 - ng-packagr: 21.2.2(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2))(tailwindcss@4.2.2)(tslib@2.8.1)(typescript@6.0.2) - tailwindcss: 4.2.2 - transitivePeerDependencies: - - '@angular/compiler' - - '@emnapi/core' - - '@emnapi/runtime' - - '@rspack/core' - - '@swc/core' - - '@types/node' - - bufferutil - - chokidar - - debug - - html-webpack-plugin - - jiti - - lightningcss - - node-sass - - sass-embedded - - stylus - - sugarss - - supports-color - - tsx - - uglify-js - - utf-8-validate - - vitest - - webpack-cli - - yaml - - '@angular-devkit/build-webpack@0.2102.7(chokidar@5.0.0)(webpack-dev-server@5.2.3(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7))': - dependencies: - '@angular-devkit/architect': 0.2102.7(chokidar@5.0.0) - rxjs: 7.8.2 - webpack: 5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3) - webpack-dev-server: 5.2.3(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + webpack-dev-server: 5.2.3(tslib@2.8.1)(webpack@5.106.1(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) transitivePeerDependencies: - chokidar @@ -17340,66 +17261,6 @@ snapshots: - tsx - yaml - '@angular/build@21.2.7(d9d75657d4631d31c9fdc49d18abebe5)': - dependencies: - '@ampproject/remapping': 2.3.0 - '@angular-devkit/architect': 0.2102.7(chokidar@5.0.0) - '@angular/compiler': 21.2.8 - '@angular/compiler-cli': 21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2) - '@babel/core': 7.29.0 - '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-split-export-declaration': 7.24.7 - '@inquirer/confirm': 5.1.21(@types/node@25.6.0) - '@vitejs/plugin-basic-ssl': 2.1.4(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.32.0)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3)) - beasties: 0.4.1 - browserslist: 4.28.2 - esbuild: 0.27.3 - https-proxy-agent: 7.0.6 - istanbul-lib-instrument: 6.0.3 - jsonc-parser: 3.3.1 - listr2: 9.0.5 - magic-string: 0.30.21 - mrmime: 2.0.1 - parse5-html-rewriting-stream: 8.0.0 - picomatch: 4.0.4 - piscina: 5.1.4 - rolldown: 1.0.0-rc.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) - sass: 1.97.3 - semver: 7.7.4 - source-map-support: 0.5.21 - tinyglobby: 0.2.15 - tslib: 2.8.1 - typescript: 6.0.2 - undici: 7.24.4 - vite: 7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.32.0)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3) - watchpack: 2.5.1 - optionalDependencies: - '@angular/core': 21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1) - '@angular/localize': 21.2.8(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2))(@angular/compiler@21.2.8) - '@angular/platform-browser': 21.2.8(@angular/animations@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)) - '@angular/platform-server': 21.2.8(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/compiler@21.2.8)(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.8(@angular/animations@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2) - '@angular/ssr': 21.2.7(9c898ff27dabd87c2e39c7d23b6394cf) - less: 4.4.2 - lmdb: 3.5.1 - ng-packagr: 21.2.2(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2))(tailwindcss@4.2.2)(tslib@2.8.1)(typescript@6.0.2) - postcss: 8.5.6 - tailwindcss: 4.2.2 - vitest: 4.1.4(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(@vitest/ui@4.1.4)(happy-dom@20.8.9)(jsdom@29.0.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3)) - transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' - - '@types/node' - - chokidar - - jiti - - lightningcss - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml - '@angular/cdk@21.2.6(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.8(@angular/animations@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2)': dependencies: '@angular/common': 21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2) @@ -21914,7 +21775,7 @@ snapshots: '@netlify/types@2.6.0': {} - '@ngtools/webpack@21.2.7(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2))(typescript@6.0.2)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7))': + '@ngtools/webpack@21.2.7(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2))(typescript@6.0.2)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3))': dependencies: '@angular/compiler-cli': 21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2) typescript: 6.0.2 @@ -25309,10 +25170,6 @@ snapshots: dependencies: vite: 7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.32.0)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.3) - '@vitejs/plugin-basic-ssl@2.1.4(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.32.0)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3))': - dependencies: - vite: 7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.32.0)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3) - '@vitejs/plugin-basic-ssl@2.1.4(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3))': dependencies: vite: 7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3) @@ -25329,20 +25186,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/browser-playwright@4.1.4(playwright@1.59.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.4)': - dependencies: - '@vitest/browser': 4.1.4(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.4) - '@vitest/mocker': 4.1.4(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3)) - playwright: 1.59.1 - tinyrainbow: 3.1.0 - vitest: 4.1.4(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(@vitest/ui@4.1.4)(happy-dom@20.8.9)(jsdom@29.0.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3)) - transitivePeerDependencies: - - bufferutil - - msw - - utf-8-validate - - vite - optional: true - '@vitest/browser-playwright@4.1.4(playwright@1.59.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.4)': dependencies: '@vitest/browser': 4.1.4(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.4) @@ -25370,24 +25213,6 @@ snapshots: - utf-8-validate - vite - '@vitest/browser@4.1.4(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.4)': - dependencies: - '@blazediff/core': 1.9.1 - '@vitest/mocker': 4.1.4(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3)) - '@vitest/utils': 4.1.4 - magic-string: 0.30.21 - pngjs: 7.0.0 - sirv: 3.0.2 - tinyrainbow: 3.1.0 - vitest: 4.1.4(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(@vitest/ui@4.1.4)(happy-dom@20.8.9)(jsdom@29.0.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3)) - ws: 8.20.0 - transitivePeerDependencies: - - bufferutil - - msw - - utf-8-validate - - vite - optional: true - '@vitest/browser@4.1.4(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.4)': dependencies: '@blazediff/core': 1.9.1 @@ -25456,15 +25281,6 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.4(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3))': - dependencies: - '@vitest/spy': 4.1.4 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3) - optional: true - '@vitest/mocker@4.1.4(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3))': dependencies: '@vitest/spy': 4.1.4 @@ -26115,7 +25931,7 @@ snapshots: transitivePeerDependencies: - supports-color - babel-loader@10.0.0(@babel/core@7.29.0)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)): + babel-loader@10.0.0(@babel/core@7.29.0)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)): dependencies: '@babel/core': 7.29.0 find-up: 5.0.0 @@ -26904,7 +26720,7 @@ snapshots: serialize-javascript: 6.0.2 webpack: 5.106.1(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7) - copy-webpack-plugin@14.0.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)): + copy-webpack-plugin@14.0.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)): dependencies: glob-parent: 6.0.2 normalize-path: 3.0.0 @@ -27082,7 +26898,7 @@ snapshots: webpack: 5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3) optional: true - css-loader@7.1.3(@rspack/core@1.7.11(@swc/helpers@0.5.21))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)): + css-loader@7.1.3(@rspack/core@1.7.11(@swc/helpers@0.5.21))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)): dependencies: icss-utils: 5.1.0(postcss@8.5.9) postcss: 8.5.9 @@ -29045,18 +28861,6 @@ snapshots: webpack: 5.106.1(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7) optional: true - html-webpack-plugin@5.6.6(@rspack/core@1.7.11(@swc/helpers@0.5.21))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)): - dependencies: - '@types/html-minifier-terser': 6.1.0 - html-minifier-terser: 6.1.0 - lodash: 4.18.1 - pretty-error: 4.0.0 - tapable: 2.3.2 - optionalDependencies: - '@rspack/core': 1.7.11(@swc/helpers@0.5.21) - webpack: 5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3) - optional: true - html-webpack-plugin@5.6.6(@rspack/core@1.7.11(@swc/helpers@0.5.21))(webpack@5.106.1(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)): dependencies: '@types/html-minifier-terser': 6.1.0 @@ -29981,7 +29785,7 @@ snapshots: webpack: 5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3) optional: true - less-loader@12.3.1(@rspack/core@1.7.11(@swc/helpers@0.5.21))(less@4.4.2)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)): + less-loader@12.3.1(@rspack/core@1.7.11(@swc/helpers@0.5.21))(less@4.4.2)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)): dependencies: less: 4.4.2 optionalDependencies: @@ -30057,7 +29861,7 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 - license-webpack-plugin@4.0.2(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)): + license-webpack-plugin@4.0.2(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)): dependencies: webpack-sources: 3.3.4 optionalDependencies: @@ -30988,7 +30792,7 @@ snapshots: min-indent@1.0.1: {} - mini-css-extract-plugin@2.10.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)): + mini-css-extract-plugin@2.10.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)): dependencies: schema-utils: 4.3.3 tapable: 2.3.2 @@ -32413,7 +32217,7 @@ snapshots: - typescript optional: true - postcss-loader@8.2.0(@rspack/core@1.7.11(@swc/helpers@0.5.21))(postcss@8.5.6)(typescript@6.0.2)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)): + postcss-loader@8.2.0(@rspack/core@1.7.11(@swc/helpers@0.5.21))(postcss@8.5.6)(typescript@6.0.2)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)): dependencies: cosmiconfig: 9.0.1(typescript@6.0.2) jiti: 2.6.1 @@ -33704,7 +33508,7 @@ snapshots: sass-embedded: 1.99.0 webpack: 5.106.1(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7) - sass-loader@16.0.7(@rspack/core@1.7.11(@swc/helpers@0.5.21))(sass-embedded@1.99.0)(sass@1.97.3)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)): + sass-loader@16.0.7(@rspack/core@1.7.11(@swc/helpers@0.5.21))(sass-embedded@1.99.0)(sass@1.97.3)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)): dependencies: neo-async: 2.6.2 optionalDependencies: @@ -34161,7 +33965,7 @@ snapshots: source-map-js@1.2.1: {} - source-map-loader@5.0.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)): + source-map-loader@5.0.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)): dependencies: iconv-lite: 0.6.3 source-map-js: 1.2.1 @@ -35208,25 +35012,6 @@ snapshots: terser: 5.46.0 yaml: 2.8.3 - vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.32.0)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3): - dependencies: - esbuild: 0.27.7 - fdir: 6.5.0(picomatch@4.0.4) - picomatch: 4.0.4 - postcss: 8.5.9 - rollup: 4.60.1 - tinyglobby: 0.2.16 - optionalDependencies: - '@types/node': 25.6.0 - fsevents: 2.3.3 - jiti: 2.6.1 - less: 4.4.2 - lightningcss: 1.32.0 - sass: 1.97.3 - sass-embedded: 1.99.0 - terser: 5.46.1 - yaml: 2.8.3 - vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3): dependencies: esbuild: 0.27.7 @@ -35265,25 +35050,6 @@ snapshots: terser: 5.46.1 yaml: 2.8.3 - vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3): - dependencies: - lightningcss: 1.32.0 - picomatch: 4.0.4 - postcss: 8.5.9 - rolldown: 1.0.0-rc.15 - tinyglobby: 0.2.16 - optionalDependencies: - '@types/node': 25.6.0 - esbuild: 0.27.7 - fsevents: 2.3.3 - jiti: 2.6.1 - less: 4.4.2 - sass: 1.97.3 - sass-embedded: 1.99.0 - terser: 5.46.1 - yaml: 2.8.3 - optional: true - vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3): dependencies: lightningcss: 1.32.0 @@ -35328,39 +35094,6 @@ snapshots: optionalDependencies: vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3) - vitest@4.1.4(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(@vitest/ui@4.1.4)(happy-dom@20.8.9)(jsdom@29.0.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3)): - dependencies: - '@vitest/expect': 4.1.4 - '@vitest/mocker': 4.1.4(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3)) - '@vitest/pretty-format': 4.1.4 - '@vitest/runner': 4.1.4 - '@vitest/snapshot': 4.1.4 - '@vitest/spy': 4.1.4 - '@vitest/utils': 4.1.4 - es-module-lexer: 2.0.0 - expect-type: 1.3.0 - magic-string: 0.30.21 - obug: 2.1.1 - pathe: 2.0.3 - picomatch: 4.0.4 - std-env: 4.0.0 - tinybench: 2.9.0 - tinyexec: 1.1.1 - tinyglobby: 0.2.16 - tinyrainbow: 3.1.0 - vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3) - why-is-node-running: 2.3.0 - optionalDependencies: - '@types/node': 25.6.0 - '@vitest/browser-playwright': 4.1.4(playwright@1.59.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.4) - '@vitest/coverage-v8': 4.1.4(@vitest/browser@4.1.4)(vitest@4.1.4) - '@vitest/ui': 4.1.4(vitest@4.1.4) - happy-dom: 20.8.9 - jsdom: 29.0.2 - transitivePeerDependencies: - - msw - optional: true - vitest@4.1.4(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(@vitest/ui@4.1.4)(happy-dom@20.8.9)(jsdom@29.0.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3)): dependencies: '@vitest/expect': 4.1.4 @@ -35514,7 +35247,7 @@ snapshots: optionalDependencies: webpack: 5.106.1(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7) - webpack-dev-middleware@7.4.5(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)): + webpack-dev-middleware@7.4.5(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)): dependencies: colorette: 2.0.20 memfs: 4.57.1(tslib@2.8.1) @@ -35540,7 +35273,7 @@ snapshots: transitivePeerDependencies: - tslib - webpack-dev-server@5.2.3(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)): + webpack-dev-server@5.2.3(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)): dependencies: '@types/bonjour': 3.5.13 '@types/connect-history-api-fallback': 1.5.4 @@ -35568,7 +35301,7 @@ snapshots: serve-index: 1.9.2 sockjs: 0.3.24 spdy: 4.0.2 - webpack-dev-middleware: 7.4.5(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + webpack-dev-middleware: 7.4.5(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) ws: 8.20.0 optionalDependencies: webpack: 5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3) @@ -35655,13 +35388,6 @@ snapshots: optionalDependencies: html-webpack-plugin: 5.6.6(@rspack/core@1.6.8(@swc/helpers@0.5.21))(webpack@5.106.1(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) - webpack-subresource-integrity@5.1.0(html-webpack-plugin@5.6.6(@rspack/core@1.7.11(@swc/helpers@0.5.21))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)): - dependencies: - typed-assert: 1.0.9 - webpack: 5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3) - optionalDependencies: - html-webpack-plugin: 5.6.6(@rspack/core@1.7.11(@swc/helpers@0.5.21))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) - webpack-subresource-integrity@5.1.0(html-webpack-plugin@5.6.6(@rspack/core@1.7.11(@swc/helpers@0.5.21))(webpack@5.106.1(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)): dependencies: typed-assert: 1.0.9 From e27db9c318338be23f7c42ce6b0e85edc3f2fbcb Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Mon, 25 May 2026 14:17:19 -0500 Subject: [PATCH 37/65] chore(platform): drop now-unused vite-plugin-nitro dep edge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The revert of '88919d3e5 feat(vite-plugin-nitro)!: deprecate package' restored vite-plugin-nitro's own source and reinstated the @analogjs/platform → @analogjs/vite-plugin-nitro runtime dependency. But platform's own nitro plumbing now lives entirely in 'packages/platform/src/lib/nitro/' (analogNitroPlugin + helpers) and doesn't import anything from @analogjs/vite-plugin-nitro. Drop the stale workspace dep edge and the implicitDependencies / dependsOn build-ordering hooks. @analogjs/vite-plugin-nitro stays in 'ng-update.packageGroup' so 'ng update @analogjs/platform' still bumps both packages in lockstep for consumers that depend on vpn directly. Update the migration guide to reflect that vpn is no longer deprecated — it ships as a standalone package; @analogjs/platform v3 has its own nitro/vite integration. --- .../docs/guides/migrating-v2-to-v3.md | 4 +- packages/platform/package.json | 1 - packages/platform/project.json | 6 +- pnpm-lock.yaml | 383 ++++++++++++++++-- 4 files changed, 346 insertions(+), 48 deletions(-) diff --git a/apps/docs-app/docs/guides/migrating-v2-to-v3.md b/apps/docs-app/docs/guides/migrating-v2-to-v3.md index 6c4a89144..b7a5be438 100644 --- a/apps/docs-app/docs/guides/migrating-v2-to-v3.md +++ b/apps/docs-app/docs/guides/migrating-v2-to-v3.md @@ -41,7 +41,7 @@ Analog SFC support was removed and `.agx` files are no longer supported. Replace Analog v3 splits the Vite plugin chain into three explicit calls. `analog()` no longer internally invokes `@analogjs/vite-plugin-angular` or `nitro/vite` — you call them yourself. Pass each plugin only the options it owns. -`@analogjs/vite-plugin-nitro` is deprecated; the Nitro orchestration moved into `@analogjs/platform`. Direct importers of `@analogjs/vite-plugin-nitro` must migrate to the separated shape below. +`@analogjs/platform` v3 owns its own Nitro orchestration via `nitro/vite` directly and no longer composes `@analogjs/vite-plugin-nitro` internally. `@analogjs/vite-plugin-nitro` continues to ship as a standalone package for projects that want to wire it themselves; users coming from a v2 `analog({ nitro: {...} })` shape should migrate to the separated shape below (analog + angular + nitro from `nitro/vite`). Before: @@ -319,7 +319,7 @@ Keep automated migration tooling focused on the breaking changes above: - require Angular v17 or newer before applying v3 changes - replace deep or internal imports with public package entrypoints - split `analog()` into `analog() + angular() + nitro()`, moving each option to the plugin that now owns it (see [plugin separation](#analog-angular-and-nitro-are-now-separate-plugins)) -- flag `@analogjs/vite-plugin-nitro` as deprecated; direct importers must migrate to `@analogjs/platform` + `nitro/vite` +- @analogjs/platform no longer composes @analogjs/vite-plugin-nitro internally; direct importers can either migrate to `@analogjs/platform` + `nitro/vite` (recommended) or continue using @analogjs/vite-plugin-nitro standalone - add `@analogjs/vite-plugin-angular` and `nitro` to app `devDependencies` (the separated shape imports them directly) - replace `@nx/vite:build` with `nx:run-commands` invoking `vite build -c apps//vite.config.ts`; drop the legacy `build.outDir` override and update `outputs` to `apps//.output` - add `server.fs.allow` pointing at the workspace root in `vite.config.ts` so Vite 8's strict fs allows nitro/vite's env runner to load its own dev runtime through pnpm content-hash paths diff --git a/packages/platform/package.json b/packages/platform/package.json index 6dcca0f8b..68cc2db56 100644 --- a/packages/platform/package.json +++ b/packages/platform/package.json @@ -46,7 +46,6 @@ "tinyglobby": "catalog:", "nitro": "catalog:", "@analogjs/vite-plugin-angular": "workspace:*", - "@analogjs/vite-plugin-nitro": "workspace:*", "@babel/core": "catalog:", "ofetch": "catalog:", "oxc-parser": "catalog:", diff --git a/packages/platform/project.json b/packages/platform/project.json index edbd2a0c0..6b8640dae 100644 --- a/packages/platform/project.json +++ b/packages/platform/project.json @@ -4,12 +4,12 @@ "sourceRoot": "packages/platform/src", "projectType": "library", "tags": ["type:release"], - "implicitDependencies": ["vite-plugin-angular", "vite-plugin-nitro"], + "implicitDependencies": ["vite-plugin-angular"], "targets": { "build-self": { "executor": "nx:run-commands", "cache": false, - "dependsOn": ["vite-plugin-angular:build", "vite-plugin-nitro:build"], + "dependsOn": ["vite-plugin-angular:build"], "outputs": ["{projectRoot}/dist"], "options": { "commands": [ @@ -34,7 +34,7 @@ }, "test": { "executor": "@nx/vitest:test", - "dependsOn": ["vite-plugin-angular:build", "vite-plugin-nitro:build"] + "dependsOn": ["vite-plugin-angular:build"] }, "version": { "executor": "@jscutlery/semver:version", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b2050aa50..84f2f4db4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -54,6 +54,9 @@ catalogs: '@astrojs/react': specifier: ^5.0.2 version: 5.0.3 + '@babel/core': + specifier: ^7.28.6 + version: 7.29.0 '@commitlint/cli': specifier: ^20.5.0 version: 20.5.0 @@ -1506,9 +1509,9 @@ importers: '@analogjs/vite-plugin-angular': specifier: workspace:* version: link:../vite-plugin-angular - '@analogjs/vite-plugin-nitro': - specifier: workspace:* - version: link:../vite-plugin-nitro + '@babel/core': + specifier: 'catalog:' + version: 7.29.0 '@nx/angular': specifier: catalog:peerCompat version: 22.6.5(41fc5ff660d836e7ccf859f85688b300) @@ -1545,6 +1548,12 @@ importers: obug: specifier: 'catalog:' version: 2.1.1 + ofetch: + specifier: 'catalog:' + version: 2.0.0-alpha.3 + oxc-parser: + specifier: 'catalog:' + version: 0.124.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) rolldown: specifier: 'catalog:' version: 1.0.0-rc.15 @@ -1560,6 +1569,9 @@ importers: vitefu: specifier: 'catalog:' version: 1.1.3(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) + xmlbuilder2: + specifier: 'catalog:' + version: 4.0.3 packages/router: dependencies: @@ -1623,10 +1635,10 @@ importers: dependencies: '@angular-devkit/build-angular': specifier: catalog:peerAngularBuilders - version: 21.2.7(0d8d439723faf789318b03d836c4657d) + version: 21.2.7(9d7c85fc87440b54c3f18e9dffd46aaa) '@angular/build': specifier: catalog:peerAngularBuilders - version: 21.2.7(6919e34d293f380cbe3efb02122eac30) + version: 21.2.7(d9d75657d4631d31c9fdc49d18abebe5) es-toolkit: specifier: 'catalog:' version: 1.45.1 @@ -16839,7 +16851,7 @@ snapshots: dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.2102.7(chokidar@5.0.0) - '@angular-devkit/build-webpack': 0.2102.7(chokidar@5.0.0)(webpack-dev-server@5.2.3(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) + '@angular-devkit/build-webpack': 0.2102.7(chokidar@5.0.0)(webpack-dev-server@5.2.3(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) '@angular-devkit/core': 21.2.7(chokidar@5.0.0) '@angular/build': 21.2.7(be30e769aef7c15f3ae11d69182ce8ae) '@angular/compiler-cli': 21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2) @@ -16853,35 +16865,35 @@ snapshots: '@babel/preset-env': 7.29.0(@babel/core@7.29.0) '@babel/runtime': 7.28.6 '@discoveryjs/json-ext': 0.6.3 - '@ngtools/webpack': 21.2.7(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2))(typescript@6.0.2)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) + '@ngtools/webpack': 21.2.7(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2))(typescript@6.0.2)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) ansi-colors: 4.1.3 autoprefixer: 10.4.27(postcss@8.5.6) - babel-loader: 10.0.0(@babel/core@7.29.0)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) + babel-loader: 10.0.0(@babel/core@7.29.0)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) browserslist: 4.28.2 - copy-webpack-plugin: 14.0.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) - css-loader: 7.1.3(@rspack/core@1.7.11(@swc/helpers@0.5.21))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) + copy-webpack-plugin: 14.0.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + css-loader: 7.1.3(@rspack/core@1.7.11(@swc/helpers@0.5.21))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) esbuild-wasm: 0.27.3 http-proxy-middleware: 3.0.5 istanbul-lib-instrument: 6.0.3 jsonc-parser: 3.3.1 karma-source-map-support: 1.4.0 less: 4.4.2 - less-loader: 12.3.1(@rspack/core@1.7.11(@swc/helpers@0.5.21))(less@4.4.2)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) - license-webpack-plugin: 4.0.2(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) + less-loader: 12.3.1(@rspack/core@1.7.11(@swc/helpers@0.5.21))(less@4.4.2)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + license-webpack-plugin: 4.0.2(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) loader-utils: 3.3.1 - mini-css-extract-plugin: 2.10.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) + mini-css-extract-plugin: 2.10.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) open: 11.0.0 ora: 9.3.0 picomatch: 4.0.4 piscina: 5.1.4 postcss: 8.5.6 - postcss-loader: 8.2.0(@rspack/core@1.7.11(@swc/helpers@0.5.21))(postcss@8.5.6)(typescript@6.0.2)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) + postcss-loader: 8.2.0(@rspack/core@1.7.11(@swc/helpers@0.5.21))(postcss@8.5.6)(typescript@6.0.2)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) resolve-url-loader: 5.0.0 rxjs: 7.8.2 sass: 1.97.3 - sass-loader: 16.0.7(@rspack/core@1.7.11(@swc/helpers@0.5.21))(sass-embedded@1.99.0)(sass@1.97.3)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) + sass-loader: 16.0.7(@rspack/core@1.7.11(@swc/helpers@0.5.21))(sass-embedded@1.99.0)(sass@1.97.3)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) semver: 7.7.4 - source-map-loader: 5.0.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) + source-map-loader: 5.0.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) source-map-support: 0.5.21 terser: 5.46.0 tinyglobby: 0.2.15 @@ -16889,8 +16901,8 @@ snapshots: tslib: 2.8.1 typescript: 6.0.2 webpack: 5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3) - webpack-dev-middleware: 7.4.5(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) - webpack-dev-server: 5.2.3(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) + webpack-dev-middleware: 7.4.5(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + webpack-dev-server: 5.2.3(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) webpack-merge: 6.0.1 webpack-subresource-integrity: 5.1.0(html-webpack-plugin@5.6.6(@rspack/core@1.7.11(@swc/helpers@0.5.21))(webpack@5.106.1(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) optionalDependencies: @@ -16931,7 +16943,7 @@ snapshots: dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.2102.7(chokidar@5.0.0) - '@angular-devkit/build-webpack': 0.2102.7(chokidar@5.0.0)(webpack-dev-server@5.2.3(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) + '@angular-devkit/build-webpack': 0.2102.7(chokidar@5.0.0)(webpack-dev-server@5.2.3(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) '@angular-devkit/core': 21.2.7(chokidar@5.0.0) '@angular/build': 21.2.7(be30e769aef7c15f3ae11d69182ce8ae) '@angular/compiler-cli': 21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2) @@ -16945,12 +16957,12 @@ snapshots: '@babel/preset-env': 7.29.0(@babel/core@7.29.0) '@babel/runtime': 7.28.6 '@discoveryjs/json-ext': 0.6.3 - '@ngtools/webpack': 21.2.7(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2))(typescript@6.0.2)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) + '@ngtools/webpack': 21.2.7(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2))(typescript@6.0.2)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) ansi-colors: 4.1.3 autoprefixer: 10.4.27(postcss@8.5.6) - babel-loader: 10.0.0(@babel/core@7.29.0)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) + babel-loader: 10.0.0(@babel/core@7.29.0)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) browserslist: 4.28.2 - copy-webpack-plugin: 14.0.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) + copy-webpack-plugin: 14.0.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) css-loader: 7.1.3(@rspack/core@1.6.8(@swc/helpers@0.5.21))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) esbuild-wasm: 0.27.3 http-proxy-middleware: 3.0.5 @@ -16959,9 +16971,9 @@ snapshots: karma-source-map-support: 1.4.0 less: 4.4.2 less-loader: 12.3.1(@rspack/core@1.6.8(@swc/helpers@0.5.21))(less@4.4.2)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) - license-webpack-plugin: 4.0.2(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) + license-webpack-plugin: 4.0.2(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) loader-utils: 3.3.1 - mini-css-extract-plugin: 2.10.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) + mini-css-extract-plugin: 2.10.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) open: 11.0.0 ora: 9.3.0 picomatch: 4.0.4 @@ -16973,7 +16985,7 @@ snapshots: sass: 1.97.3 sass-loader: 16.0.7(@rspack/core@1.6.8(@swc/helpers@0.5.21))(sass-embedded@1.99.0)(sass@1.97.3)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) semver: 7.7.4 - source-map-loader: 5.0.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) + source-map-loader: 5.0.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) source-map-support: 0.5.21 terser: 5.46.0 tinyglobby: 0.2.15 @@ -16981,8 +16993,8 @@ snapshots: tslib: 2.8.1 typescript: 6.0.2 webpack: 5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3) - webpack-dev-middleware: 7.4.5(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) - webpack-dev-server: 5.2.3(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) + webpack-dev-middleware: 7.4.5(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + webpack-dev-server: 5.2.3(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) webpack-merge: 6.0.1 webpack-subresource-integrity: 5.1.0(html-webpack-plugin@5.6.6(@rspack/core@1.6.8(@swc/helpers@0.5.21))(webpack@5.106.1(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) optionalDependencies: @@ -17020,12 +17032,104 @@ snapshots: - yaml optional: true - '@angular-devkit/build-webpack@0.2102.7(chokidar@5.0.0)(webpack-dev-server@5.2.3(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3))': + '@angular-devkit/build-angular@21.2.7(9d7c85fc87440b54c3f18e9dffd46aaa)': dependencies: + '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.2102.7(chokidar@5.0.0) + '@angular-devkit/build-webpack': 0.2102.7(chokidar@5.0.0)(webpack-dev-server@5.2.3(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + '@angular-devkit/core': 21.2.7(chokidar@5.0.0) + '@angular/build': 21.2.7(be30e769aef7c15f3ae11d69182ce8ae) + '@angular/compiler-cli': 21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2) + '@babel/core': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-split-export-declaration': 7.24.7 + '@babel/plugin-transform-async-generator-functions': 7.29.0(@babel/core@7.29.0) + '@babel/plugin-transform-async-to-generator': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-transform-runtime': 7.29.0(@babel/core@7.29.0) + '@babel/preset-env': 7.29.0(@babel/core@7.29.0) + '@babel/runtime': 7.28.6 + '@discoveryjs/json-ext': 0.6.3 + '@ngtools/webpack': 21.2.7(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2))(typescript@6.0.2)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + ansi-colors: 4.1.3 + autoprefixer: 10.4.27(postcss@8.5.6) + babel-loader: 10.0.0(@babel/core@7.29.0)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + browserslist: 4.28.2 + copy-webpack-plugin: 14.0.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + css-loader: 7.1.3(@rspack/core@1.7.11(@swc/helpers@0.5.21))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + esbuild-wasm: 0.27.3 + http-proxy-middleware: 3.0.5 + istanbul-lib-instrument: 6.0.3 + jsonc-parser: 3.3.1 + karma-source-map-support: 1.4.0 + less: 4.4.2 + less-loader: 12.3.1(@rspack/core@1.7.11(@swc/helpers@0.5.21))(less@4.4.2)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + license-webpack-plugin: 4.0.2(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + loader-utils: 3.3.1 + mini-css-extract-plugin: 2.10.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + open: 11.0.0 + ora: 9.3.0 + picomatch: 4.0.4 + piscina: 5.1.4 + postcss: 8.5.6 + postcss-loader: 8.2.0(@rspack/core@1.7.11(@swc/helpers@0.5.21))(postcss@8.5.6)(typescript@6.0.2)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + resolve-url-loader: 5.0.0 rxjs: 7.8.2 + sass: 1.97.3 + sass-loader: 16.0.7(@rspack/core@1.7.11(@swc/helpers@0.5.21))(sass-embedded@1.99.0)(sass@1.97.3)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + semver: 7.7.4 + source-map-loader: 5.0.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + source-map-support: 0.5.21 + terser: 5.46.0 + tinyglobby: 0.2.15 + tree-kill: 1.2.2 + tslib: 2.8.1 + typescript: 6.0.2 webpack: 5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3) - webpack-dev-server: 5.2.3(tslib@2.8.1)(webpack@5.106.1(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + webpack-dev-middleware: 7.4.5(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + webpack-dev-server: 5.2.3(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + webpack-merge: 6.0.1 + webpack-subresource-integrity: 5.1.0(html-webpack-plugin@5.6.6(@rspack/core@1.7.11(@swc/helpers@0.5.21))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + optionalDependencies: + '@angular/core': 21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1) + '@angular/localize': 21.2.8(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2))(@angular/compiler@21.2.8) + '@angular/platform-browser': 21.2.8(@angular/animations@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)) + '@angular/platform-server': 21.2.8(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/compiler@21.2.8)(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.8(@angular/animations@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2) + '@angular/ssr': 21.2.7(9c898ff27dabd87c2e39c7d23b6394cf) + esbuild: 0.27.3 + ng-packagr: 21.2.2(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2))(tailwindcss@4.2.2)(tslib@2.8.1)(typescript@6.0.2) + tailwindcss: 4.2.2 + transitivePeerDependencies: + - '@angular/compiler' + - '@emnapi/core' + - '@emnapi/runtime' + - '@rspack/core' + - '@swc/core' + - '@types/node' + - bufferutil + - chokidar + - debug + - html-webpack-plugin + - jiti + - lightningcss + - node-sass + - sass-embedded + - stylus + - sugarss + - supports-color + - tsx + - uglify-js + - utf-8-validate + - vitest + - webpack-cli + - yaml + + '@angular-devkit/build-webpack@0.2102.7(chokidar@5.0.0)(webpack-dev-server@5.2.3(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7))': + dependencies: + '@angular-devkit/architect': 0.2102.7(chokidar@5.0.0) + rxjs: 7.8.2 + webpack: 5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3) + webpack-dev-server: 5.2.3(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) transitivePeerDependencies: - chokidar @@ -17261,6 +17365,66 @@ snapshots: - tsx - yaml + '@angular/build@21.2.7(d9d75657d4631d31c9fdc49d18abebe5)': + dependencies: + '@ampproject/remapping': 2.3.0 + '@angular-devkit/architect': 0.2102.7(chokidar@5.0.0) + '@angular/compiler': 21.2.8 + '@angular/compiler-cli': 21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2) + '@babel/core': 7.29.0 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-split-export-declaration': 7.24.7 + '@inquirer/confirm': 5.1.21(@types/node@25.6.0) + '@vitejs/plugin-basic-ssl': 2.1.4(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.32.0)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3)) + beasties: 0.4.1 + browserslist: 4.28.2 + esbuild: 0.27.3 + https-proxy-agent: 7.0.6 + istanbul-lib-instrument: 6.0.3 + jsonc-parser: 3.3.1 + listr2: 9.0.5 + magic-string: 0.30.21 + mrmime: 2.0.1 + parse5-html-rewriting-stream: 8.0.0 + picomatch: 4.0.4 + piscina: 5.1.4 + rolldown: 1.0.0-rc.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + sass: 1.97.3 + semver: 7.7.4 + source-map-support: 0.5.21 + tinyglobby: 0.2.15 + tslib: 2.8.1 + typescript: 6.0.2 + undici: 7.24.4 + vite: 7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.32.0)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3) + watchpack: 2.5.1 + optionalDependencies: + '@angular/core': 21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1) + '@angular/localize': 21.2.8(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2))(@angular/compiler@21.2.8) + '@angular/platform-browser': 21.2.8(@angular/animations@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)) + '@angular/platform-server': 21.2.8(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/compiler@21.2.8)(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.8(@angular/animations@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2) + '@angular/ssr': 21.2.7(9c898ff27dabd87c2e39c7d23b6394cf) + less: 4.4.2 + lmdb: 3.5.1 + ng-packagr: 21.2.2(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2))(tailwindcss@4.2.2)(tslib@2.8.1)(typescript@6.0.2) + postcss: 8.5.6 + tailwindcss: 4.2.2 + vitest: 4.1.4(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(@vitest/ui@4.1.4)(happy-dom@20.8.9)(jsdom@29.0.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3)) + transitivePeerDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + - '@types/node' + - chokidar + - jiti + - lightningcss + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + '@angular/cdk@21.2.6(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.8(@angular/animations@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2)': dependencies: '@angular/common': 21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2) @@ -21775,7 +21939,7 @@ snapshots: '@netlify/types@2.6.0': {} - '@ngtools/webpack@21.2.7(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2))(typescript@6.0.2)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3))': + '@ngtools/webpack@21.2.7(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2))(typescript@6.0.2)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7))': dependencies: '@angular/compiler-cli': 21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2) typescript: 6.0.2 @@ -25170,6 +25334,10 @@ snapshots: dependencies: vite: 7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.32.0)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.0)(yaml@2.8.3) + '@vitejs/plugin-basic-ssl@2.1.4(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.32.0)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3))': + dependencies: + vite: 7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.32.0)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3) + '@vitejs/plugin-basic-ssl@2.1.4(vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3))': dependencies: vite: 7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3) @@ -25186,6 +25354,20 @@ snapshots: transitivePeerDependencies: - supports-color + '@vitest/browser-playwright@4.1.4(playwright@1.59.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.4)': + dependencies: + '@vitest/browser': 4.1.4(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.4) + '@vitest/mocker': 4.1.4(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3)) + playwright: 1.59.1 + tinyrainbow: 3.1.0 + vitest: 4.1.4(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(@vitest/ui@4.1.4)(happy-dom@20.8.9)(jsdom@29.0.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3)) + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + optional: true + '@vitest/browser-playwright@4.1.4(playwright@1.59.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.4)': dependencies: '@vitest/browser': 4.1.4(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.4) @@ -25213,6 +25395,24 @@ snapshots: - utf-8-validate - vite + '@vitest/browser@4.1.4(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.4)': + dependencies: + '@blazediff/core': 1.9.1 + '@vitest/mocker': 4.1.4(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3)) + '@vitest/utils': 4.1.4 + magic-string: 0.30.21 + pngjs: 7.0.0 + sirv: 3.0.2 + tinyrainbow: 3.1.0 + vitest: 4.1.4(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(@vitest/ui@4.1.4)(happy-dom@20.8.9)(jsdom@29.0.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3)) + ws: 8.20.0 + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + optional: true + '@vitest/browser@4.1.4(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.4)': dependencies: '@blazediff/core': 1.9.1 @@ -25281,6 +25481,15 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 + '@vitest/mocker@4.1.4(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3))': + dependencies: + '@vitest/spy': 4.1.4 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3) + optional: true + '@vitest/mocker@4.1.4(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3))': dependencies: '@vitest/spy': 4.1.4 @@ -25931,7 +26140,7 @@ snapshots: transitivePeerDependencies: - supports-color - babel-loader@10.0.0(@babel/core@7.29.0)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)): + babel-loader@10.0.0(@babel/core@7.29.0)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)): dependencies: '@babel/core': 7.29.0 find-up: 5.0.0 @@ -26720,7 +26929,7 @@ snapshots: serialize-javascript: 6.0.2 webpack: 5.106.1(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7) - copy-webpack-plugin@14.0.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)): + copy-webpack-plugin@14.0.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)): dependencies: glob-parent: 6.0.2 normalize-path: 3.0.0 @@ -26898,7 +27107,7 @@ snapshots: webpack: 5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3) optional: true - css-loader@7.1.3(@rspack/core@1.7.11(@swc/helpers@0.5.21))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)): + css-loader@7.1.3(@rspack/core@1.7.11(@swc/helpers@0.5.21))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)): dependencies: icss-utils: 5.1.0(postcss@8.5.9) postcss: 8.5.9 @@ -28861,6 +29070,18 @@ snapshots: webpack: 5.106.1(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7) optional: true + html-webpack-plugin@5.6.6(@rspack/core@1.7.11(@swc/helpers@0.5.21))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)): + dependencies: + '@types/html-minifier-terser': 6.1.0 + html-minifier-terser: 6.1.0 + lodash: 4.18.1 + pretty-error: 4.0.0 + tapable: 2.3.2 + optionalDependencies: + '@rspack/core': 1.7.11(@swc/helpers@0.5.21) + webpack: 5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3) + optional: true + html-webpack-plugin@5.6.6(@rspack/core@1.7.11(@swc/helpers@0.5.21))(webpack@5.106.1(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)): dependencies: '@types/html-minifier-terser': 6.1.0 @@ -29785,7 +30006,7 @@ snapshots: webpack: 5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3) optional: true - less-loader@12.3.1(@rspack/core@1.7.11(@swc/helpers@0.5.21))(less@4.4.2)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)): + less-loader@12.3.1(@rspack/core@1.7.11(@swc/helpers@0.5.21))(less@4.4.2)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)): dependencies: less: 4.4.2 optionalDependencies: @@ -29861,7 +30082,7 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 - license-webpack-plugin@4.0.2(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)): + license-webpack-plugin@4.0.2(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)): dependencies: webpack-sources: 3.3.4 optionalDependencies: @@ -30792,7 +31013,7 @@ snapshots: min-indent@1.0.1: {} - mini-css-extract-plugin@2.10.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)): + mini-css-extract-plugin@2.10.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)): dependencies: schema-utils: 4.3.3 tapable: 2.3.2 @@ -32217,7 +32438,7 @@ snapshots: - typescript optional: true - postcss-loader@8.2.0(@rspack/core@1.7.11(@swc/helpers@0.5.21))(postcss@8.5.6)(typescript@6.0.2)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)): + postcss-loader@8.2.0(@rspack/core@1.7.11(@swc/helpers@0.5.21))(postcss@8.5.6)(typescript@6.0.2)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)): dependencies: cosmiconfig: 9.0.1(typescript@6.0.2) jiti: 2.6.1 @@ -33508,7 +33729,7 @@ snapshots: sass-embedded: 1.99.0 webpack: 5.106.1(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7) - sass-loader@16.0.7(@rspack/core@1.7.11(@swc/helpers@0.5.21))(sass-embedded@1.99.0)(sass@1.97.3)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)): + sass-loader@16.0.7(@rspack/core@1.7.11(@swc/helpers@0.5.21))(sass-embedded@1.99.0)(sass@1.97.3)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)): dependencies: neo-async: 2.6.2 optionalDependencies: @@ -33965,7 +34186,7 @@ snapshots: source-map-js@1.2.1: {} - source-map-loader@5.0.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)): + source-map-loader@5.0.0(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)): dependencies: iconv-lite: 0.6.3 source-map-js: 1.2.1 @@ -35012,6 +35233,25 @@ snapshots: terser: 5.46.0 yaml: 2.8.3 + vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.4.2)(lightningcss@1.32.0)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3): + dependencies: + esbuild: 0.27.7 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.9 + rollup: 4.60.1 + tinyglobby: 0.2.16 + optionalDependencies: + '@types/node': 25.6.0 + fsevents: 2.3.3 + jiti: 2.6.1 + less: 4.4.2 + lightningcss: 1.32.0 + sass: 1.97.3 + sass-embedded: 1.99.0 + terser: 5.46.1 + yaml: 2.8.3 + vite@7.3.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3): dependencies: esbuild: 0.27.7 @@ -35050,6 +35290,25 @@ snapshots: terser: 5.46.1 yaml: 2.8.3 + vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.9 + rolldown: 1.0.0-rc.15 + tinyglobby: 0.2.16 + optionalDependencies: + '@types/node': 25.6.0 + esbuild: 0.27.7 + fsevents: 2.3.3 + jiti: 2.6.1 + less: 4.4.2 + sass: 1.97.3 + sass-embedded: 1.99.0 + terser: 5.46.1 + yaml: 2.8.3 + optional: true + vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3): dependencies: lightningcss: 1.32.0 @@ -35094,6 +35353,39 @@ snapshots: optionalDependencies: vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3) + vitest@4.1.4(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(@vitest/ui@4.1.4)(happy-dom@20.8.9)(jsdom@29.0.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3)): + dependencies: + '@vitest/expect': 4.1.4 + '@vitest/mocker': 4.1.4(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3)) + '@vitest/pretty-format': 4.1.4 + '@vitest/runner': 4.1.4 + '@vitest/snapshot': 4.1.4 + '@vitest/spy': 4.1.4 + '@vitest/utils': 4.1.4 + es-module-lexer: 2.0.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.0.0 + tinybench: 2.9.0 + tinyexec: 1.1.1 + tinyglobby: 0.2.16 + tinyrainbow: 3.1.0 + vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 25.6.0 + '@vitest/browser-playwright': 4.1.4(playwright@1.59.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.4.2)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.4) + '@vitest/coverage-v8': 4.1.4(@vitest/browser@4.1.4)(vitest@4.1.4) + '@vitest/ui': 4.1.4(vitest@4.1.4) + happy-dom: 20.8.9 + jsdom: 29.0.2 + transitivePeerDependencies: + - msw + optional: true + vitest@4.1.4(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(@vitest/ui@4.1.4)(happy-dom@20.8.9)(jsdom@29.0.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.97.3)(terser@5.46.1)(yaml@2.8.3)): dependencies: '@vitest/expect': 4.1.4 @@ -35247,7 +35539,7 @@ snapshots: optionalDependencies: webpack: 5.106.1(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7) - webpack-dev-middleware@7.4.5(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)): + webpack-dev-middleware@7.4.5(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)): dependencies: colorette: 2.0.20 memfs: 4.57.1(tslib@2.8.1) @@ -35273,7 +35565,7 @@ snapshots: transitivePeerDependencies: - tslib - webpack-dev-server@5.2.3(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)): + webpack-dev-server@5.2.3(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)): dependencies: '@types/bonjour': 3.5.13 '@types/connect-history-api-fallback': 1.5.4 @@ -35301,7 +35593,7 @@ snapshots: serve-index: 1.9.2 sockjs: 0.3.24 spdy: 4.0.2 - webpack-dev-middleware: 7.4.5(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) + webpack-dev-middleware: 7.4.5(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) ws: 8.20.0 optionalDependencies: webpack: 5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3) @@ -35388,6 +35680,13 @@ snapshots: optionalDependencies: html-webpack-plugin: 5.6.6(@rspack/core@1.6.8(@swc/helpers@0.5.21))(webpack@5.106.1(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + webpack-subresource-integrity@5.1.0(html-webpack-plugin@5.6.6(@rspack/core@1.7.11(@swc/helpers@0.5.21))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)): + dependencies: + typed-assert: 1.0.9 + webpack: 5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3) + optionalDependencies: + html-webpack-plugin: 5.6.6(@rspack/core@1.7.11(@swc/helpers@0.5.21))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + webpack-subresource-integrity@5.1.0(html-webpack-plugin@5.6.6(@rspack/core@1.7.11(@swc/helpers@0.5.21))(webpack@5.106.1(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)): dependencies: typed-assert: 1.0.9 From 49bb45ea5ca3f53fb7ff121827639030821ba06b Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Mon, 25 May 2026 14:43:46 -0500 Subject: [PATCH 38/65] feat(platform): own server.fs.allow + gate output overrides to build MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two cleanups so user vite.configs don't carry workarounds anymore: 1. analogNitroPlugin.config() now sets `server.fs.allow = [workspaceRoot]` so users don't have to write the workaround themselves. Vite 8 defaults this to `[searchForWorkspaceRoot(root)]`, which should cover pnpm content-hash paths under the workspace root — but in practice nitro/vite's env-runner load of its own `dev-entry.mjs` through Vite's ModuleRunner emits an ERR_LOAD_URL ('Does the file exist?') without the explicit allow entry. Move the workaround into the plugin and strip it from all five demo app vite.configs. 2. The legacy 'dist//analog/{server,public}' output override in analogNitroPlugin.setup() now runs only when `!nitro.options.dev`. Setting nitro.options.output.publicDir during dev pointed nitro's readAsset at a path that doesn't exist yet (the dist directory is only populated by the build), so the dev server's static asset middleware crashed with ERR_INVALID_ARG_TYPE from fileURLToPath on undefined. Production paths still resolve to the legacy 'dist//analog/' layout that docs and deploy scripts reference. --- apps/analog-app/vite.config.ts | 9 ---- apps/blog-app/vite.config.ts | 6 --- apps/opt-catchall-app/vite.config.ts | 8 ---- apps/tailwind-debug-app/vite.config.ts | 3 -- apps/tanstack-query-app/vite.config.ts | 6 --- .../src/lib/nitro/analog-nitro-plugin.ts | 44 ++++++++++++++----- 6 files changed, 32 insertions(+), 44 deletions(-) diff --git a/apps/analog-app/vite.config.ts b/apps/analog-app/vite.config.ts index 88d77e0b4..3afc4f02a 100644 --- a/apps/analog-app/vite.config.ts +++ b/apps/analog-app/vite.config.ts @@ -51,15 +51,6 @@ export default defineConfig(async ({ mode, command }) => { reportCompressedSize: true, target: ['es2020'], }, - server: { - fs: { - // Allow Vite's dev fs fallback to read pnpm content-hash paths under - // the workspace root (e.g. node_modules/.pnpm/nitro@.../...). Without - // this, nitro/vite's env-runner cannot resolve its own dev-entry.mjs - // through Vite's ModuleRunner. - allow: [resolve(__dirname, '../..')], - }, - }, optimizeDeps: { include: ['@angular/forms'], // Keep workspace Angular libraries on the source-transform path so Analog diff --git a/apps/blog-app/vite.config.ts b/apps/blog-app/vite.config.ts index 6950c5a91..ebc284956 100644 --- a/apps/blog-app/vite.config.ts +++ b/apps/blog-app/vite.config.ts @@ -3,7 +3,6 @@ import analog, { type PrerenderContentFile } from '@analogjs/platform'; import angular from '@analogjs/vite-plugin-angular'; import { nitro } from 'nitro/vite'; -import { resolve } from 'node:path'; import { defineConfig } from 'vite'; import { getWorkspaceDependencyExcludes } from '../../tools/vite/get-workspace-dependency-excludes.js'; @@ -32,11 +31,6 @@ export default defineConfig(() => { reportCompressedSize: true, target: ['es2020'], }, - server: { - fs: { - allow: [resolve(__dirname, '../..')], - }, - }, plugins: [ analog({ content: { diff --git a/apps/opt-catchall-app/vite.config.ts b/apps/opt-catchall-app/vite.config.ts index a837397bf..ffc541983 100644 --- a/apps/opt-catchall-app/vite.config.ts +++ b/apps/opt-catchall-app/vite.config.ts @@ -3,7 +3,6 @@ import analog from '@analogjs/platform'; import angular from '@analogjs/vite-plugin-angular'; import { nitro } from 'nitro/vite'; -import { resolve } from 'node:path'; import { defineConfig } from 'vite'; import { getWorkspaceDependencyExcludes } from '../../tools/vite/get-workspace-dependency-excludes.js'; @@ -21,13 +20,6 @@ export default defineConfig(() => { reportCompressedSize: true, target: ['es2020'], }, - server: { - fs: { - // Allow Vite's fs fallback to read pnpm content-hash paths so - // nitro/vite's env-runner can load its own dev runtime entry. - allow: [resolve(__dirname, '../..')], - }, - }, plugins: [ analog({ content: { diff --git a/apps/tailwind-debug-app/vite.config.ts b/apps/tailwind-debug-app/vite.config.ts index 1fed40e55..717ded16d 100644 --- a/apps/tailwind-debug-app/vite.config.ts +++ b/apps/tailwind-debug-app/vite.config.ts @@ -157,9 +157,6 @@ export default defineConfig(({ mode }) => ({ }, server: { port: 43040, - fs: { - allow: [resolve(__dirname, '../..')], - }, hmr: { clientPort: 4201, path: 'vite-hmr', diff --git a/apps/tanstack-query-app/vite.config.ts b/apps/tanstack-query-app/vite.config.ts index bbb00f64a..8dce71d2a 100644 --- a/apps/tanstack-query-app/vite.config.ts +++ b/apps/tanstack-query-app/vite.config.ts @@ -3,7 +3,6 @@ import analog from '@analogjs/platform'; import angular from '@analogjs/vite-plugin-angular'; import { nitro } from 'nitro/vite'; -import { resolve } from 'node:path'; import tailwindcss from '@tailwindcss/vite'; import { defineConfig } from 'vite'; import { getWorkspaceDependencyExcludes } from '../../tools/vite/get-workspace-dependency-excludes.js'; @@ -23,11 +22,6 @@ export default defineConfig(({ mode }) => { // can compile external templates/styles instead of Vite prebundling them. exclude: getWorkspaceDependencyExcludes(__dirname), }, - server: { - fs: { - allow: [resolve(__dirname, '../..')], - }, - }, plugins: [ tailwindcss(), analog({ diff --git a/packages/platform/src/lib/nitro/analog-nitro-plugin.ts b/packages/platform/src/lib/nitro/analog-nitro-plugin.ts index 27e1d6641..24ca6b05e 100644 --- a/packages/platform/src/lib/nitro/analog-nitro-plugin.ts +++ b/packages/platform/src/lib/nitro/analog-nitro-plugin.ts @@ -120,7 +120,20 @@ export function analogNitroPlugin(options: Options = {}): Plugin { config(userConfig) { refreshContext(userConfig.root); - const overrides: UserConfig = {}; + const overrides: UserConfig = { + // Vite 8 defaults `server.fs.allow` to `[searchForWorkspaceRoot(root)]`, + // which should already cover the workspace root. In practice, nitro/vite's + // env-runner loads its own `dev-entry.mjs` from a pnpm content-hash path + // (`node_modules/.pnpm/nitro@.../...`) through Vite's ModuleRunner and + // hits an `ERR_LOAD_URL`/"Does the file exist?" error unless an explicit + // allow entry covers the same root. Whitelist the workspace root here so + // users don't have to write the workaround in every vite.config.ts. + server: { + fs: { + allow: [context.workspaceRoot], + }, + }, + }; if (ssr) { // Two-pronged registration: `experimental.vite.services.ssr.entry` @@ -189,6 +202,11 @@ export function analogNitroPlugin(options: Options = {}): Plugin { // default `/.output` would otherwise drop artifacts in an // unexpected location for users upgrading from v2. // + // Build only. During dev, leaving the output paths at Nitro's + // defaults keeps the dev server's `readAsset` happy — those + // virtuals expect to read from the in-memory module graph, not + // from a `dist/` directory that doesn't exist yet. + // // `buildDir` (Nitro's intermediate scratch dir) stays at its default // inside the project root. Nitro's prerender phase re-bundles SSR // chunks out of `/vite/services/ssr/`, and Rolldown's @@ -196,17 +214,19 @@ export function analogNitroPlugin(options: Options = {}): Plugin { // Keeping `buildDir` adjacent to the project root means workspace // packages installed at `/node_modules/` (the usual install // shape for both standalone and Nx setups) remain reachable. - const distRoot = resolve( - context.workspaceRoot, - 'dist', - context.rootDir, - ); - nitro.options.output = { - ...nitro.options.output, - dir: resolve(distRoot, 'analog'), - publicDir: resolve(distRoot, 'analog/public'), - serverDir: resolve(distRoot, 'analog/server'), - }; + if (!nitro.options.dev) { + const distRoot = resolve( + context.workspaceRoot, + 'dist', + context.rootDir, + ); + nitro.options.output = { + ...nitro.options.output, + dir: resolve(distRoot, 'analog'), + publicDir: resolve(distRoot, 'analog/public'), + serverDir: resolve(distRoot, 'analog/server'), + }; + } const hasAPIDir = existsSync( resolve( From 91390ddad64e94c3cf0ac1e1d14fd6a9cb8bb1ee Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Mon, 25 May 2026 14:46:20 -0500 Subject: [PATCH 39/65] chore: drop empty-object arg from nitro() calls in demo apps + docs `nitro()` and `nitro({})` are functionally identical, but the empty literal adds noise. Match the templates' bare-call style. --- apps/docs-app/docs/guides/migrating-v2-to-v3.md | 2 +- apps/opt-catchall-app/vite.config.ts | 2 +- apps/tanstack-query-app/vite.config.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/docs-app/docs/guides/migrating-v2-to-v3.md b/apps/docs-app/docs/guides/migrating-v2-to-v3.md index b7a5be438..06a03ac40 100644 --- a/apps/docs-app/docs/guides/migrating-v2-to-v3.md +++ b/apps/docs-app/docs/guides/migrating-v2-to-v3.md @@ -159,7 +159,7 @@ plugins: [ include: pageGlobs(libs.additionalPagesDirs), additionalContentDirs: libs.additionalContentDirs, }), - nitro({}), + nitro(), ]; ``` diff --git a/apps/opt-catchall-app/vite.config.ts b/apps/opt-catchall-app/vite.config.ts index ffc541983..95751ee34 100644 --- a/apps/opt-catchall-app/vite.config.ts +++ b/apps/opt-catchall-app/vite.config.ts @@ -32,7 +32,7 @@ export default defineConfig(() => { useAngularCompilationAPI: true, }, }), - nitro({}), + nitro(), ], }; }); diff --git a/apps/tanstack-query-app/vite.config.ts b/apps/tanstack-query-app/vite.config.ts index 8dce71d2a..907bd9a53 100644 --- a/apps/tanstack-query-app/vite.config.ts +++ b/apps/tanstack-query-app/vite.config.ts @@ -28,7 +28,7 @@ export default defineConfig(({ mode }) => { apiPrefix: 'api', }), angular(), - nitro({}), + nitro(), ], test: { reporters: ['default'], From f17730eca3613295a36bf1f14982273f137b532a Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Mon, 25 May 2026 14:51:02 -0500 Subject: [PATCH 40/65] chore(create-analog): inject nitro into pnpm scaffolds only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit nitro is only imported via 'nitro/vite' in the generated vite.config.ts — it's strictly a build-time tool. npm/yarn auto-hoist transitive deps, so they pick nitro up through @analogjs/platform without it being declared at the top level. pnpm's strict node_modules layout doesn't hoist that way, so scaffolded pnpm projects still need an explicit top-level entry. Drop 'nitro' from template-latest, template-minimal, and template-blog package.json devDependencies and add it inside addPnpmDependencies instead — same H3_TEMPLATES guard the existing h3 / ofetch injection already uses. npm/yarn users now get cleaner package.json files without a build-tool entry that pnpm exclusively needs. --- packages/create-analog/index.js | 7 ++++++- packages/create-analog/template-blog/package.json | 1 - packages/create-analog/template-latest/package.json | 1 - packages/create-analog/template-minimal/package.json | 1 - 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/create-analog/index.js b/packages/create-analog/index.js index d84e456ee..8158db1c2 100755 --- a/packages/create-analog/index.js +++ b/packages/create-analog/index.js @@ -506,8 +506,13 @@ function addPnpmDependencies(pkg, template) { if (H3_TEMPLATES.includes(template)) { pkg.dependencies ??= {}; pkg.dependencies.h3 = '^1.13.0'; - pkg.dependencies.nitro = '3.0.260415-beta'; pkg.dependencies.ofetch = '2.0.0-alpha.3'; + // nitro is only imported via `nitro/vite` in vite.config.ts (a build-time + // tool). npm/yarn auto-hoist it as a transitive of @analogjs/platform; + // pnpm's strict node_modules layout doesn't, so add it explicitly for + // pnpm users only. + pkg.devDependencies ??= {}; + pkg.devDependencies.nitro = '3.0.260415-beta'; } } diff --git a/packages/create-analog/template-blog/package.json b/packages/create-analog/template-blog/package.json index 6bc602262..4a8e56709 100644 --- a/packages/create-analog/template-blog/package.json +++ b/packages/create-analog/template-blog/package.json @@ -42,7 +42,6 @@ "@angular/cli": "^21.0.0", "@angular/compiler-cli": "^21.0.0", "jsdom": "^22.0.0", - "nitro": "3.0.260415-beta", "rollup": "^4.40.0", "typescript": "~5.9.0", "vite": "^8.0.0", diff --git a/packages/create-analog/template-latest/package.json b/packages/create-analog/template-latest/package.json index 3f3b3f65a..6d82cf5b7 100644 --- a/packages/create-analog/template-latest/package.json +++ b/packages/create-analog/template-latest/package.json @@ -43,7 +43,6 @@ "@angular/cli": "^21.0.0", "@angular/compiler-cli": "^21.0.0", "jsdom": "^22.0.0", - "nitro": "3.0.260415-beta", "rollup": "^4.40.0", "typescript": "~5.9.0", "vite": "^8.0.0", diff --git a/packages/create-analog/template-minimal/package.json b/packages/create-analog/template-minimal/package.json index 3f3b3f65a..6d82cf5b7 100644 --- a/packages/create-analog/template-minimal/package.json +++ b/packages/create-analog/template-minimal/package.json @@ -43,7 +43,6 @@ "@angular/cli": "^21.0.0", "@angular/compiler-cli": "^21.0.0", "jsdom": "^22.0.0", - "nitro": "3.0.260415-beta", "rollup": "^4.40.0", "typescript": "~5.9.0", "vite": "^8.0.0", From 7cea109723f2622953456ea19dfc83a458b8b329 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Mon, 25 May 2026 14:58:35 -0500 Subject: [PATCH 41/65] chore: bump nitro to 3.0.260522-beta MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps the catalog pin from 3.0.260415-beta to the latest beta tag on npm (~5 weeks newer). Synchronizes the two literal pins that get emitted into user-scaffolded files: - packages/create-analog/index.js — the pnpm-only injection in addPnpmDependencies. - packages/platform/migrations/migrate-to-separated-plugins.ts — the NITRO_VERSION constant the ng-update schematic writes into a migrating workspace's package.json. Verified: - pnpm test: 18/18 projects pass - pnpm build: 22/22 projects build - analog-app production server returns 200 with SSR hydration tokens on / and JSON on /api/v1/products --- packages/create-analog/index.js | 2 +- .../migrate-to-separated-plugins.ts | 2 +- pnpm-lock.yaml | 694 ++++++++++++------ pnpm-workspace.yaml | 2 +- 4 files changed, 460 insertions(+), 240 deletions(-) diff --git a/packages/create-analog/index.js b/packages/create-analog/index.js index 8158db1c2..483b5457b 100755 --- a/packages/create-analog/index.js +++ b/packages/create-analog/index.js @@ -512,7 +512,7 @@ function addPnpmDependencies(pkg, template) { // pnpm's strict node_modules layout doesn't, so add it explicitly for // pnpm users only. pkg.devDependencies ??= {}; - pkg.devDependencies.nitro = '3.0.260415-beta'; + pkg.devDependencies.nitro = '3.0.260522-beta'; } } diff --git a/packages/platform/migrations/migrate-to-separated-plugins/migrate-to-separated-plugins.ts b/packages/platform/migrations/migrate-to-separated-plugins/migrate-to-separated-plugins.ts index 91a8b8a46..21f2275a9 100644 --- a/packages/platform/migrations/migrate-to-separated-plugins/migrate-to-separated-plugins.ts +++ b/packages/platform/migrations/migrate-to-separated-plugins/migrate-to-separated-plugins.ts @@ -7,7 +7,7 @@ const ANGULAR_PLUGIN_IMPORT = `from '@analogjs/vite-plugin-angular'`; const NITRO_VITE_IMPORT = `from 'nitro/vite'`; const ANGULAR_PLUGIN_PKG = '@analogjs/vite-plugin-angular'; const NITRO_PKG = 'nitro'; -const NITRO_VERSION = '3.0.260415-beta'; +const NITRO_VERSION = '3.0.260522-beta'; const MIGRATION_DOC_URL = 'https://analogjs.org/docs/guides/migrating-v2-to-v3#analog-angular-and-nitro-are-now-separate-plugins'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 84f2f4db4..4c545501c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -328,8 +328,8 @@ catalogs: specifier: 21.2.2 version: 21.2.2 nitro: - specifier: 3.0.260415-beta - version: 3.0.260415-beta + specifier: 3.0.260522-beta + version: 3.0.260522-beta nx: specifier: 22.7.0-beta.12 version: 22.7.0-beta.12 @@ -651,10 +651,10 @@ importers: version: 3.1.1(@types/react@19.2.14)(react@19.2.5) '@nx/angular': specifier: 'catalog:' - version: 22.7.0-beta.12(bbf4031437ab82b6be657ea8be05b0c3) + version: 22.7.0-beta.12(05b4503607f205d03d993da8e423f9be) '@nx/devkit': specifier: 'catalog:' - version: 22.7.0-beta.12(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + version: 22.7.0-beta.12(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) '@standard-schema/spec': specifier: 'catalog:' version: 1.1.0 @@ -733,7 +733,7 @@ importers: version: 0.2102.7(chokidar@5.0.0) '@angular-devkit/build-angular': specifier: 'catalog:' - version: 21.2.7(0d8d439723faf789318b03d836c4657d) + version: 21.2.7(b8f20a26ea5ae043ece601bbe8d4f9d0) '@angular-devkit/core': specifier: 'catalog:' version: 21.2.7(chokidar@5.0.0) @@ -751,7 +751,7 @@ importers: version: 21.3.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) '@angular/build': specifier: 'catalog:' - version: 21.2.7(6919e34d293f380cbe3efb02122eac30) + version: 21.2.7(0b8bb98daa82125a57df7497f4fcef59) '@angular/cli': specifier: 'catalog:' version: 21.2.7(@types/node@25.6.0)(chokidar@5.0.0) @@ -784,31 +784,31 @@ importers: version: 5.2.0 '@nx/eslint': specifier: 'catalog:' - version: 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(@zkochan/js-yaml@0.0.7)(eslint@10.2.0(jiti@2.6.1))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + version: 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(@zkochan/js-yaml@0.0.7)(eslint@10.2.0(jiti@2.6.1))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) '@nx/eslint-plugin': specifier: 'catalog:' - version: 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(@typescript-eslint/parser@8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint-config-prettier@10.1.8(eslint@10.2.0(jiti@2.6.1)))(eslint@10.2.0(jiti@2.6.1))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))(typescript@6.0.2) + version: 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(@typescript-eslint/parser@8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint-config-prettier@10.1.8(eslint@10.2.0(jiti@2.6.1)))(eslint@10.2.0(jiti@2.6.1))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))(typescript@6.0.2) '@nx/js': specifier: 'catalog:' - version: 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + version: 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) '@nx/playwright': specifier: 'catalog:' - version: 22.7.0-beta.12(@babel/traverse@7.29.0)(@playwright/test@1.59.1)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(@zkochan/js-yaml@0.0.7)(eslint@10.2.0(jiti@2.6.1))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + version: 22.7.0-beta.12(@babel/traverse@7.29.0)(@playwright/test@1.59.1)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(@zkochan/js-yaml@0.0.7)(eslint@10.2.0(jiti@2.6.1))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) '@nx/plugin': specifier: 'catalog:' - version: 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(@types/node@25.6.0)(@zkochan/js-yaml@0.0.7)(babel-plugin-macros@3.1.0)(eslint@10.2.0(jiti@2.6.1))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))(typescript@6.0.2) + version: 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(@types/node@25.6.0)(@zkochan/js-yaml@0.0.7)(babel-plugin-macros@3.1.0)(eslint@10.2.0(jiti@2.6.1))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))(typescript@6.0.2) '@nx/storybook': specifier: 'catalog:' - version: 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(@zkochan/js-yaml@0.0.7)(eslint@10.2.0(jiti@2.6.1))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.2) + version: 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(@zkochan/js-yaml@0.0.7)(eslint@10.2.0(jiti@2.6.1))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.2) '@nx/vite': specifier: 'catalog:' - version: 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))(typescript@6.0.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.4) + version: 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))(typescript@6.0.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.4) '@nx/vitest': specifier: 'catalog:' - version: 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))(typescript@6.0.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.4) + version: 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))(typescript@6.0.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.4) '@nx/web': specifier: 'catalog:' - version: 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + version: 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) '@oxc-angular/vite': specifier: 'catalog:' version: 0.0.23(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) @@ -841,13 +841,13 @@ importers: version: 10.3.5(@vitest/browser-playwright@4.1.4)(@vitest/browser@4.1.4)(@vitest/runner@4.1.4)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vitest@4.1.4) '@storybook/angular': specifier: 'catalog:' - version: 10.3.5(f55f33330bfbd6fb184961c0aca35221) + version: 10.3.5(56fe732b02a35e211a39a35ea47e2513) '@storybook/builder-vite': specifier: catalog:peerStorybook10 version: 10.3.5(esbuild@0.27.7)(rollup@4.60.1)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))(webpack@5.106.1(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) '@swc-node/register': specifier: 'catalog:' - version: 1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2) + version: 1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2) '@swc/core': specifier: 'catalog:' version: 1.15.24(@swc/helpers@0.5.21) @@ -997,10 +997,10 @@ importers: version: 21.2.2(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2))(tailwindcss@4.2.2)(tslib@2.8.1)(typescript@6.0.2) nitro: specifier: 'catalog:' - version: 3.0.260415-beta(chokidar@5.0.0)(dotenv@16.4.7)(jiti@2.6.1)(lru-cache@11.3.5)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) + version: 3.0.260522-beta(chokidar@5.0.0)(dotenv@16.4.7)(jiti@2.6.1)(lru-cache@11.3.5)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) nx: specifier: 'catalog:' - version: 22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)) + version: 22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)) obug: specifier: 'catalog:' version: 2.1.1 @@ -1009,13 +1009,13 @@ importers: version: 2.0.0-alpha.3 oxc-parser: specifier: 'catalog:' - version: 0.124.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + version: 0.124.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) oxc-resolver: specifier: 'catalog:' - version: 11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + version: 11.19.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) oxc-transform: specifier: 'catalog:' - version: 0.124.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + version: 0.124.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) oxlint: specifier: 'catalog:' version: 1.60.0(oxlint-tsgolint@0.20.0) @@ -1042,7 +1042,7 @@ importers: version: 1.0.0-rc.15 rolldown-plugin-dts: specifier: 'catalog:' - version: 0.23.2(@typescript/native-preview@7.0.0-dev.20260412.1)(oxc-resolver@11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2))(rolldown@1.0.0-rc.15)(typescript@6.0.2) + version: 0.23.2(@typescript/native-preview@7.0.0-dev.20260412.1)(oxc-resolver@11.19.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0))(rolldown@1.0.0-rc.15)(typescript@6.0.2) rollup: specifier: 'catalog:' version: 4.60.1 @@ -1096,7 +1096,7 @@ importers: version: 28.0.0 tsdown: specifier: 'catalog:' - version: 0.21.8(@typescript/native-preview@7.0.0-dev.20260412.1)(oxc-resolver@11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2))(synckit@0.11.12)(typescript@6.0.2) + version: 0.21.8(@typescript/native-preview@7.0.0-dev.20260412.1)(oxc-resolver@11.19.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0))(synckit@0.11.12)(typescript@6.0.2) typescript: specifier: 'catalog:' version: 6.0.2 @@ -1114,7 +1114,7 @@ importers: version: 12.0.0-beta.1(typescript@6.0.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))(ws@8.20.0) vite-tsconfig-paths: specifier: 'catalog:' - version: 7.0.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(typescript@6.0.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) + version: 7.0.0-alpha.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(typescript@6.0.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) vitefu: specifier: 'catalog:' version: 1.1.3(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) @@ -1157,7 +1157,7 @@ importers: version: link:../../packages/vitest-angular nitro: specifier: 'catalog:' - version: 3.0.260415-beta(chokidar@5.0.0)(dotenv@16.4.7)(jiti@2.6.1)(lru-cache@11.3.5)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) + version: 3.0.260522-beta(chokidar@5.0.0)(dotenv@16.4.7)(jiti@2.6.1)(lru-cache@11.3.5)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) apps/analog-app-e2e: {} @@ -1195,7 +1195,7 @@ importers: version: link:../../packages/vite-plugin-angular nitro: specifier: 'catalog:' - version: 3.0.260415-beta(chokidar@5.0.0)(dotenv@16.4.7)(jiti@2.6.1)(lru-cache@11.3.5)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) + version: 3.0.260522-beta(chokidar@5.0.0)(dotenv@16.4.7)(jiti@2.6.1)(lru-cache@11.3.5)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) apps/blog-app-e2e: {} @@ -1262,7 +1262,7 @@ importers: version: link:../../packages/vite-plugin-angular nitro: specifier: 'catalog:' - version: 3.0.260415-beta(chokidar@5.0.0)(dotenv@16.4.7)(jiti@2.6.1)(lru-cache@11.3.5)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) + version: 3.0.260522-beta(chokidar@5.0.0)(dotenv@16.4.7)(jiti@2.6.1)(lru-cache@11.3.5)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) apps/tailwind-debug-app: dependencies: @@ -1281,7 +1281,7 @@ importers: version: link:../../packages/vitest-angular nitro: specifier: 'catalog:' - version: 3.0.260415-beta(chokidar@5.0.0)(dotenv@16.4.7)(jiti@2.6.1)(lru-cache@11.3.5)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) + version: 3.0.260522-beta(chokidar@5.0.0)(dotenv@16.4.7)(jiti@2.6.1)(lru-cache@11.3.5)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) apps/tailwind-debug-app-e2e: {} @@ -1308,7 +1308,7 @@ importers: version: 5.5.19 nitro: specifier: 'catalog:' - version: 3.0.260415-beta(chokidar@5.0.0)(dotenv@16.4.7)(jiti@2.6.1)(lru-cache@11.3.5)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) + version: 3.0.260522-beta(chokidar@5.0.0)(dotenv@16.4.7)(jiti@2.6.1)(lru-cache@11.3.5)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) tailwindcss: specifier: 'catalog:' version: 4.2.2 @@ -1365,7 +1365,7 @@ importers: version: 21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)) '@angular/build': specifier: catalog:peerAngular20Plus - version: 21.2.7(6919e34d293f380cbe3efb02122eac30) + version: 21.2.7(0b8bb98daa82125a57df7497f4fcef59) '@angular/common': specifier: 21.2.8 version: 21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2) @@ -1416,7 +1416,7 @@ importers: version: 21.2.8(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(@angular/platform-browser@21.2.8(@angular/animations@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(@angular/common@21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2))(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1)))(rxjs@7.8.2) '@nx/devkit': specifier: catalog:peerCompat - version: 22.6.5(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + version: 22.6.5(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) '@standard-schema/spec': specifier: 'catalog:' version: 1.1.0 @@ -1468,10 +1468,10 @@ importers: dependencies: '@nx/devkit': specifier: catalog:peerCompat - version: 22.6.5(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + version: 22.6.5(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) tsdown: specifier: 'catalog:' - version: 0.21.8(@typescript/native-preview@7.0.0-dev.20260412.1)(oxc-resolver@11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2))(synckit@0.11.12)(typescript@6.0.2) + version: 0.21.8(@typescript/native-preview@7.0.0-dev.20260412.1)(oxc-resolver@11.19.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0))(synckit@0.11.12)(typescript@6.0.2) vite: specifier: catalog:peerCompat version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3) @@ -1496,7 +1496,7 @@ importers: version: link:../vite-plugin-angular nitro: specifier: 'catalog:' - version: 3.0.260415-beta(chokidar@5.0.0)(dotenv@16.4.7)(jiti@2.6.1)(lru-cache@11.3.5)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) + version: 3.0.260522-beta(chokidar@5.0.0)(dotenv@16.4.7)(jiti@2.6.1)(lru-cache@11.3.5)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) packages/nx-plugin: devDependencies: @@ -1514,13 +1514,13 @@ importers: version: 7.29.0 '@nx/angular': specifier: catalog:peerCompat - version: 22.6.5(41fc5ff660d836e7ccf859f85688b300) + version: 22.6.5(2ab26fa366bc8886b2297d83f6c4af1d) '@nx/devkit': specifier: catalog:peerCompat - version: 22.6.5(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + version: 22.6.5(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) '@nx/vite': specifier: catalog:peerCompat - version: 22.6.5(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))(typescript@6.0.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.4) + version: 22.6.5(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))(typescript@6.0.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.4) es-toolkit: specifier: 'catalog:' version: 1.45.1 @@ -1544,7 +1544,7 @@ importers: version: 1.2.1(marked@18.0.0)(shiki@4.0.2) nitro: specifier: 'catalog:' - version: 3.0.260415-beta(chokidar@5.0.0)(dotenv@16.4.7)(jiti@2.6.1)(lru-cache@11.3.5)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) + version: 3.0.260522-beta(chokidar@5.0.0)(dotenv@16.4.7)(jiti@2.6.1)(lru-cache@11.3.5)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) obug: specifier: 'catalog:' version: 2.1.1 @@ -1553,7 +1553,7 @@ importers: version: 2.0.0-alpha.3 oxc-parser: specifier: 'catalog:' - version: 0.124.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + version: 0.124.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) rolldown: specifier: 'catalog:' version: 1.0.0-rc.15 @@ -1614,10 +1614,10 @@ importers: dependencies: '@analogjs/vite-plugin-angular': specifier: catalog:peerVitestAngular - version: 2.4.7(@angular-devkit/build-angular@21.2.7(0d8d439723faf789318b03d836c4657d))(@angular/build@21.2.7(6919e34d293f380cbe3efb02122eac30)) + version: 2.4.7(@angular-devkit/build-angular@21.2.7(b8f20a26ea5ae043ece601bbe8d4f9d0))(@angular/build@21.2.7(0b8bb98daa82125a57df7497f4fcef59)) '@storybook/angular': specifier: catalog:peerStorybook10 - version: 10.3.5(f55f33330bfbd6fb184961c0aca35221) + version: 10.3.5(56fe732b02a35e211a39a35ea47e2513) '@storybook/builder-vite': specifier: catalog:peerStorybook10 version: 10.3.5(esbuild@0.27.7)(rollup@4.60.1)(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))(webpack@5.106.1(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) @@ -1635,10 +1635,10 @@ importers: dependencies: '@angular-devkit/build-angular': specifier: catalog:peerAngularBuilders - version: 21.2.7(9d7c85fc87440b54c3f18e9dffd46aaa) + version: 21.2.7(065a2e44cfc0f80119d956b5460fd850) '@angular/build': specifier: catalog:peerAngularBuilders - version: 21.2.7(d9d75657d4631d31c9fdc49d18abebe5) + version: 21.2.7(b690019c48ff0abf542b789736446534) es-toolkit: specifier: 'catalog:' version: 1.45.1 @@ -1650,10 +1650,10 @@ importers: version: 2.1.1 oxc-parser: specifier: 'catalog:' - version: 0.124.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + version: 0.124.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) oxc-resolver: specifier: 'catalog:' - version: 11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + version: 11.19.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) rolldown: specifier: 'catalog:' version: 1.0.0-rc.15 @@ -1670,7 +1670,7 @@ importers: version: 6.1.7 nitro: specifier: 'catalog:' - version: 3.0.260415-beta(chokidar@5.0.0)(dotenv@16.4.7)(jiti@2.6.1)(lru-cache@11.3.5)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) + version: 3.0.260522-beta(chokidar@5.0.0)(dotenv@16.4.7)(jiti@2.6.1)(lru-cache@11.3.5)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) obug: specifier: 'catalog:' version: 2.1.1 @@ -1679,7 +1679,7 @@ importers: version: 2.0.0-alpha.3 oxc-parser: specifier: 'catalog:' - version: 0.124.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + version: 0.124.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) radix3: specifier: 'catalog:' version: 1.1.2 @@ -1694,7 +1694,7 @@ importers: dependencies: '@analogjs/vite-plugin-angular': specifier: catalog:peerVitestAngular - version: 2.4.7(@angular-devkit/build-angular@21.2.7(0d8d439723faf789318b03d836c4657d))(@angular/build@21.2.7(6919e34d293f380cbe3efb02122eac30)) + version: 2.4.7(@angular-devkit/build-angular@21.2.7(b8f20a26ea5ae043ece601bbe8d4f9d0))(@angular/build@21.2.7(0b8bb98daa82125a57df7497f4fcef59)) '@angular-devkit/architect': specifier: catalog:peerVitestAngular version: 0.2102.7(chokidar@5.0.0) @@ -1703,7 +1703,7 @@ importers: version: 21.2.7(chokidar@5.0.0) oxc-transform: specifier: 'catalog:' - version: 0.124.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + version: 0.124.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) vitest: specifier: catalog:peerVitestAngular version: 4.1.4(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(@vitest/ui@4.1.4)(happy-dom@20.8.9)(jsdom@29.0.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)) @@ -3691,12 +3691,18 @@ packages: resolution: {integrity: sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==} engines: {node: '>=0.8.0'} + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + '@emnapi/core@1.4.5': resolution: {integrity: sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==} '@emnapi/core@1.9.2': resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + '@emnapi/runtime@1.4.5': resolution: {integrity: sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==} @@ -5022,6 +5028,12 @@ packages: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 + '@napi-rs/wasm-runtime@1.1.4': + resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + '@netlify/functions@5.2.0': resolution: {integrity: sha512-Pj93qeQd1tkQ5xm9gWJZmBf/1riLYqYHc0OzFukrJomrj82Ott53Rr/Q88H1ms5cF+P5QXRKWmA2JSxSybKfjA==} engines: {node: '>=18.0.0'} @@ -5638,6 +5650,9 @@ packages: '@oxc-project/types@0.124.0': resolution: {integrity: sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg==} + '@oxc-project/types@0.132.0': + resolution: {integrity: sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==} + '@oxc-resolver/binding-android-arm-eabi@11.19.1': resolution: {integrity: sha512-aUs47y+xyXHUKlbhqHUjBABjvycq6YSD7bpxSW7vplUmdzAlJ93yXY6ZR0c1o1x5A/QKbENCvs3+NlY8IpIVzg==} cpu: [arm] @@ -6201,6 +6216,12 @@ packages: cpu: [arm64] os: [android] + '@rolldown/binding-android-arm64@1.0.2': + resolution: {integrity: sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + '@rolldown/binding-darwin-arm64@1.0.0-rc.15': resolution: {integrity: sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -6213,6 +6234,12 @@ packages: cpu: [arm64] os: [darwin] + '@rolldown/binding-darwin-arm64@1.0.2': + resolution: {integrity: sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + '@rolldown/binding-darwin-x64@1.0.0-rc.15': resolution: {integrity: sha512-YzeVqOqjPYvUbJSWJ4EDL8ahbmsIXQpgL3JVipmN+MX0XnXMeWomLN3Fb+nwCmP/jfyqte5I3XRSm7OfQrbyxw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -6225,6 +6252,12 @@ packages: cpu: [x64] os: [darwin] + '@rolldown/binding-darwin-x64@1.0.2': + resolution: {integrity: sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + '@rolldown/binding-freebsd-x64@1.0.0-rc.15': resolution: {integrity: sha512-9Erhx956jeQ0nNTyif1+QWAXDRD38ZNjr//bSHrt6wDwB+QkAfl2q6Mn1k6OBPerznjRmbM10lgRb1Pli4xZPw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -6237,6 +6270,12 @@ packages: cpu: [x64] os: [freebsd] + '@rolldown/binding-freebsd-x64@1.0.2': + resolution: {integrity: sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15': resolution: {integrity: sha512-cVwk0w8QbZJGTnP/AHQBs5yNwmpgGYStL88t4UIaqcvYJWBfS0s3oqVLZPwsPU6M0zlW4GqjP0Zq5MnAGwFeGA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -6249,6 +6288,12 @@ packages: cpu: [arm] os: [linux] + '@rolldown/binding-linux-arm-gnueabihf@1.0.2': + resolution: {integrity: sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15': resolution: {integrity: sha512-eBZ/u8iAK9SoHGanqe/jrPnY0JvBN6iXbVOsbO38mbz+ZJsaobExAm1Iu+rxa4S1l2FjG0qEZn4Rc6X8n+9M+w==} engines: {node: ^20.19.0 || >=22.12.0} @@ -6263,6 +6308,13 @@ packages: os: [linux] libc: [glibc] + '@rolldown/binding-linux-arm64-gnu@1.0.2': + resolution: {integrity: sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.15': resolution: {integrity: sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -6277,6 +6329,13 @@ packages: os: [linux] libc: [musl] + '@rolldown/binding-linux-arm64-musl@1.0.2': + resolution: {integrity: sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15': resolution: {integrity: sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -6284,6 +6343,13 @@ packages: os: [linux] libc: [glibc] + '@rolldown/binding-linux-ppc64-gnu@1.0.2': + resolution: {integrity: sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15': resolution: {integrity: sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -6291,6 +6357,13 @@ packages: os: [linux] libc: [glibc] + '@rolldown/binding-linux-s390x-gnu@1.0.2': + resolution: {integrity: sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.15': resolution: {integrity: sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -6305,6 +6378,13 @@ packages: os: [linux] libc: [glibc] + '@rolldown/binding-linux-x64-gnu@1.0.2': + resolution: {integrity: sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + '@rolldown/binding-linux-x64-musl@1.0.0-rc.15': resolution: {integrity: sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -6319,6 +6399,13 @@ packages: os: [linux] libc: [musl] + '@rolldown/binding-linux-x64-musl@1.0.2': + resolution: {integrity: sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + '@rolldown/binding-openharmony-arm64@1.0.0-rc.15': resolution: {integrity: sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -6331,6 +6418,12 @@ packages: cpu: [arm64] os: [openharmony] + '@rolldown/binding-openharmony-arm64@1.0.2': + resolution: {integrity: sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + '@rolldown/binding-wasm32-wasi@1.0.0-rc.15': resolution: {integrity: sha512-ApLruZq/ig+nhaE7OJm4lDjayUnOHVUa77zGeqnqZ9pn0ovdVbbNPerVibLXDmWeUZXjIYIT8V3xkT58Rm9u5Q==} engines: {node: '>=14.0.0'} @@ -6341,6 +6434,11 @@ packages: engines: {node: '>=14.0.0'} cpu: [wasm32] + '@rolldown/binding-wasm32-wasi@1.0.2': + resolution: {integrity: sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15': resolution: {integrity: sha512-KmoUoU7HnN+Si5YWJigfTws1jz1bKBYDQKdbLspz0UaqjjFkddHsqorgiW1mxcAj88lYUE6NC/zJNwT+SloqtA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -6353,6 +6451,12 @@ packages: cpu: [arm64] os: [win32] + '@rolldown/binding-win32-arm64-msvc@1.0.2': + resolution: {integrity: sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.15': resolution: {integrity: sha512-3P2A8L+x75qavWLe/Dll3EYBJLQmtkJN8rfh+U/eR3MqMgL/h98PhYI+JFfXuDPgPeCB7iZAKiqii5vqOvnA0g==} engines: {node: ^20.19.0 || >=22.12.0} @@ -6365,6 +6469,12 @@ packages: cpu: [x64] os: [win32] + '@rolldown/binding-win32-x64-msvc@1.0.2': + resolution: {integrity: sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + '@rolldown/pluginutils@1.0.0-rc.15': resolution: {integrity: sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g==} @@ -6374,6 +6484,9 @@ packages: '@rolldown/pluginutils@1.0.0-rc.4': resolution: {integrity: sha512-1BrrmTu0TWfOP1riA8uakjFc9bpIUGzVKETsOtzY39pPga8zELGDl8eu1Dx7/gjM5CAz14UknsUMpBO8L+YntQ==} + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + '@rollup/plugin-json@6.1.0': resolution: {integrity: sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==} engines: {node: '>=14.0.0'} @@ -9936,15 +10049,18 @@ packages: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} - env-runner@0.1.7: - resolution: {integrity: sha512-i7h96jxETJYhXy5grgHNJ9xNzCzWIn9Ck/VkkYgOlE4gOqknsLX3CmlVb5LmwNex8sOoLFVZLz+TIw/+b5rktA==} + env-runner@0.1.9: + resolution: {integrity: sha512-W9AiZlPx0uXtghAJiTBkeZOgyQdecVvoln3cHoOEZswPq0cVMi+WBhUQjdUn+JcZFAFgOt+i5fcO7C2zniZoCg==} hasBin: true peerDependencies: - '@netlify/runtime': ^4 - miniflare: ^4.20260317.3 + '@netlify/runtime': ^4.1.23 + '@vercel/queue': ^0.2.0 + miniflare: ^4.20260515.0 peerDependenciesMeta: '@netlify/runtime': optional: true + '@vercel/queue': + optional: true miniflare: optional: true @@ -10702,8 +10818,8 @@ packages: h3@1.15.11: resolution: {integrity: sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==} - h3@2.0.1-rc.20: - resolution: {integrity: sha512-28ljodXuUp0fZovdiSRq4G9OgrxCztrJe5VdYzXAB7ueRvI7pIUqLU14Xi3XqdYJ/khXjfpUOOD2EQa6CmBgsg==} + h3@2.0.1-rc.22: + resolution: {integrity: sha512-Esv0DMIuPkCTSWCA0vO73vcTqwzH1wjSrAO1TXNu/K3up1sZHa9EKMapbmxCDYBeymC3fVTk4qxp7ogQWQ+KgA==} engines: {node: '>=20.11.1'} hasBin: true peerDependencies: @@ -10954,8 +11070,8 @@ packages: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} - httpxy@0.5.0: - resolution: {integrity: sha512-qwX7QX/rK2visT10/b7bSeZWQOMlSm3svTD0pZpU+vJjNUP0YHtNv4c3z+MO+MSnGuRFWJFdCZiV+7F7dXIOzg==} + httpxy@0.5.3: + resolution: {integrity: sha512-SMS9V6Sn7VWaS11lYhoAr0ceoaiolTWf4jYdJn0NJhCdKMu9R2H9Fh0LBDWBHQF6HRLI1PmaePYsjanSpE5PEw==} human-signals@1.1.1: resolution: {integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==} @@ -12514,8 +12630,8 @@ packages: nerf-dart@1.0.0: resolution: {integrity: sha512-EZSPZB70jiVsivaBLYDCyntd5eH8NTSMOn3rB+HxwdmKThGELLdYv8qVIMWvZEFy9w8ZZpW9h9OB32l1rGtj7g==} - nf3@0.3.16: - resolution: {integrity: sha512-Gs0xRPpUm2nDkqbi40NJ9g7qDIcjcJzgExiydnq6LAyqhI2jfno8wG3NKTL+IiJsx799UHOb1CnSd4Wg4SG4Pw==} + nf3@0.3.17: + resolution: {integrity: sha512-N9zEWySuJFw+gR0lhS5863YsvNeudOdqRyFvNb+jMXbeTJOdrjDqkCpDginIZfUm0LzT1t1nCRiDeqQm/8kirQ==} ng-packagr@21.2.2: resolution: {integrity: sha512-VO0y7RU3Ik8E14QdrryVyVbTAyqO2MK9W9GrG4e/4N8+ti+DWiBSQmw0tIhnV67lEjQwCccPA3ZBoIn3B1vJ1Q==} @@ -12530,16 +12646,16 @@ packages: tailwindcss: optional: true - nitro@3.0.260415-beta: - resolution: {integrity: sha512-J0ntJERWtIdvweZdmkCiF8eOFvP9fIAJR2gpeIDrHbAlYavK41WQfADo/YoZ/LF7RMTZBiPaH/pt2s/nPru9Iw==} + nitro@3.0.260522-beta: + resolution: {integrity: sha512-L/z2eOWgkiQHc65kv+SEMgau505afSRF7NJlbooaaZEZscFrNSD7rXZzeVubQlgIzPbhOG8o73bk9soIiGTHRA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: - '@vercel/queue': ^0.1.4 + '@vercel/queue': ^0.2.0 dotenv: '*' giget: '*' jiti: ^2.6.1 - rollup: ^4.60.1 + rollup: ^4.60.3 vite: ^7 || ^8 xml2js: ^0.6.2 zephyr-agent: ^0.2.0 @@ -14409,6 +14525,11 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + rolldown@1.0.2: + resolution: {integrity: sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + rollup-plugin-dts@6.4.1: resolution: {integrity: sha512-l//F3Zf7ID5GoOfLfD8kroBjQKEKpy1qfhtAdnpibFZMffPaylrg1CoDC2vGkPeTeyxUe4bVFCln2EFuL7IGGg==} engines: {node: '>=20'} @@ -16832,13 +16953,13 @@ snapshots: '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 - '@analogjs/vite-plugin-angular@2.4.7(@angular-devkit/build-angular@21.2.7(0d8d439723faf789318b03d836c4657d))(@angular/build@21.2.7(6919e34d293f380cbe3efb02122eac30))': + '@analogjs/vite-plugin-angular@2.4.7(@angular-devkit/build-angular@21.2.7(b8f20a26ea5ae043ece601bbe8d4f9d0))(@angular/build@21.2.7(0b8bb98daa82125a57df7497f4fcef59))': dependencies: tinyglobby: 0.2.16 ts-morph: 21.0.1 optionalDependencies: - '@angular-devkit/build-angular': 21.2.7(0d8d439723faf789318b03d836c4657d) - '@angular/build': 21.2.7(6919e34d293f380cbe3efb02122eac30) + '@angular-devkit/build-angular': 21.2.7(b8f20a26ea5ae043ece601bbe8d4f9d0) + '@angular/build': 21.2.7(0b8bb98daa82125a57df7497f4fcef59) '@angular-devkit/architect@0.2102.7(chokidar@5.0.0)': dependencies: @@ -16847,13 +16968,13 @@ snapshots: transitivePeerDependencies: - chokidar - '@angular-devkit/build-angular@21.2.7(0d8d439723faf789318b03d836c4657d)': + '@angular-devkit/build-angular@21.2.7(065a2e44cfc0f80119d956b5460fd850)': dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.2102.7(chokidar@5.0.0) '@angular-devkit/build-webpack': 0.2102.7(chokidar@5.0.0)(webpack-dev-server@5.2.3(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) '@angular-devkit/core': 21.2.7(chokidar@5.0.0) - '@angular/build': 21.2.7(be30e769aef7c15f3ae11d69182ce8ae) + '@angular/build': 21.2.7(9643f421a1d0ee4251e74315a0e70414) '@angular/compiler-cli': 21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2) '@babel/core': 7.29.0 '@babel/generator': 7.29.1 @@ -16904,7 +17025,7 @@ snapshots: webpack-dev-middleware: 7.4.5(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) webpack-dev-server: 5.2.3(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) webpack-merge: 6.0.1 - webpack-subresource-integrity: 5.1.0(html-webpack-plugin@5.6.6(@rspack/core@1.7.11(@swc/helpers@0.5.21))(webpack@5.106.1(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) + webpack-subresource-integrity: 5.1.0(html-webpack-plugin@5.6.6(@rspack/core@1.7.11(@swc/helpers@0.5.21))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) optionalDependencies: '@angular/core': 21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1) '@angular/localize': 21.2.8(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2))(@angular/compiler@21.2.8) @@ -16939,13 +17060,13 @@ snapshots: - webpack-cli - yaml - '@angular-devkit/build-angular@21.2.7(7a6d932c692adf7b91035f989a2c47b9)': + '@angular-devkit/build-angular@21.2.7(19efef94e4c51e719ac157ec3cc75c84)': dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.2102.7(chokidar@5.0.0) '@angular-devkit/build-webpack': 0.2102.7(chokidar@5.0.0)(webpack-dev-server@5.2.3(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) '@angular-devkit/core': 21.2.7(chokidar@5.0.0) - '@angular/build': 21.2.7(be30e769aef7c15f3ae11d69182ce8ae) + '@angular/build': 21.2.7(9643f421a1d0ee4251e74315a0e70414) '@angular/compiler-cli': 21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2) '@babel/core': 7.29.0 '@babel/generator': 7.29.1 @@ -17032,13 +17153,13 @@ snapshots: - yaml optional: true - '@angular-devkit/build-angular@21.2.7(9d7c85fc87440b54c3f18e9dffd46aaa)': + '@angular-devkit/build-angular@21.2.7(b8f20a26ea5ae043ece601bbe8d4f9d0)': dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.2102.7(chokidar@5.0.0) '@angular-devkit/build-webpack': 0.2102.7(chokidar@5.0.0)(webpack-dev-server@5.2.3(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) '@angular-devkit/core': 21.2.7(chokidar@5.0.0) - '@angular/build': 21.2.7(be30e769aef7c15f3ae11d69182ce8ae) + '@angular/build': 21.2.7(9643f421a1d0ee4251e74315a0e70414) '@angular/compiler-cli': 21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2) '@babel/core': 7.29.0 '@babel/generator': 7.29.1 @@ -17089,7 +17210,7 @@ snapshots: webpack-dev-middleware: 7.4.5(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) webpack-dev-server: 5.2.3(tslib@2.8.1)(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) webpack-merge: 6.0.1 - webpack-subresource-integrity: 5.1.0(html-webpack-plugin@5.6.6(@rspack/core@1.7.11(@swc/helpers@0.5.21))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) + webpack-subresource-integrity: 5.1.0(html-webpack-plugin@5.6.6(@rspack/core@1.7.11(@swc/helpers@0.5.21))(webpack@5.106.1(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)))(webpack@5.105.2(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.3)) optionalDependencies: '@angular/core': 21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1) '@angular/localize': 21.2.8(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2))(@angular/compiler@21.2.8) @@ -17245,7 +17366,7 @@ snapshots: '@angular/core': 21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1) tslib: 2.8.1 - '@angular/build@21.2.7(6919e34d293f380cbe3efb02122eac30)': + '@angular/build@21.2.7(0b8bb98daa82125a57df7497f4fcef59)': dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.2102.7(chokidar@5.0.0) @@ -17268,7 +17389,7 @@ snapshots: parse5-html-rewriting-stream: 8.0.0 picomatch: 4.0.4 piscina: 5.1.4 - rolldown: 1.0.0-rc.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + rolldown: 1.0.0-rc.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) sass: 1.97.3 semver: 7.7.4 source-map-support: 0.5.21 @@ -17305,7 +17426,7 @@ snapshots: - tsx - yaml - '@angular/build@21.2.7(be30e769aef7c15f3ae11d69182ce8ae)': + '@angular/build@21.2.7(9643f421a1d0ee4251e74315a0e70414)': dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.2102.7(chokidar@5.0.0) @@ -17328,7 +17449,7 @@ snapshots: parse5-html-rewriting-stream: 8.0.0 picomatch: 4.0.4 piscina: 5.1.4 - rolldown: 1.0.0-rc.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + rolldown: 1.0.0-rc.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) sass: 1.97.3 semver: 7.7.4 source-map-support: 0.5.21 @@ -17365,7 +17486,7 @@ snapshots: - tsx - yaml - '@angular/build@21.2.7(d9d75657d4631d31c9fdc49d18abebe5)': + '@angular/build@21.2.7(b690019c48ff0abf542b789736446534)': dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.2102.7(chokidar@5.0.0) @@ -17388,7 +17509,7 @@ snapshots: parse5-html-rewriting-stream: 8.0.0 picomatch: 4.0.4 piscina: 5.1.4 - rolldown: 1.0.0-rc.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + rolldown: 1.0.0-rc.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) sass: 1.97.3 semver: 7.7.4 source-map-support: 0.5.21 @@ -20597,6 +20718,12 @@ snapshots: dependencies: '@types/hammerjs': 2.0.46 + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + '@emnapi/core@1.4.5': dependencies: '@emnapi/wasi-threads': 1.0.4 @@ -20607,6 +20734,11 @@ snapshots: '@emnapi/wasi-threads': 1.2.1 tslib: 2.8.1 + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + '@emnapi/runtime@1.4.5': dependencies: tslib: 2.8.1 @@ -21926,6 +22058,13 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true + '@napi-rs/wasm-runtime@1.1.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.1 + optional: true + '@napi-rs/wasm-runtime@1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': dependencies: '@emnapi/core': 1.9.2 @@ -21933,6 +22072,13 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.1 + optional: true + '@netlify/functions@5.2.0': dependencies: '@netlify/types': 2.6.0 @@ -22017,18 +22163,18 @@ snapshots: transitivePeerDependencies: - supports-color - '@nx/angular@22.6.5(41fc5ff660d836e7ccf859f85688b300)': + '@nx/angular@22.6.5(2ab26fa366bc8886b2297d83f6c4af1d)': dependencies: '@angular-devkit/core': 21.2.7(chokidar@5.0.0) '@angular-devkit/schematics': 21.2.7(chokidar@5.0.0) - '@nx/devkit': 22.6.5(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) - '@nx/eslint': 22.6.5(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(@zkochan/js-yaml@0.0.7)(eslint@10.2.0(jiti@2.6.1))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) - '@nx/js': 22.6.5(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) - '@nx/module-federation': 22.6.5(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/helpers@0.5.21)(esbuild@0.27.7)(node-fetch@2.7.0(encoding@0.1.13))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2) - '@nx/rspack': 22.6.5(10ec6bc8cf7172a1e8f69047189d0c0c) - '@nx/web': 22.6.5(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) - '@nx/webpack': 22.6.5(@babel/traverse@7.29.0)(@rspack/core@1.6.8(@swc/helpers@0.5.21))(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)(html-webpack-plugin@5.6.6(@rspack/core@1.6.8(@swc/helpers@0.5.21))(webpack@5.106.1(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)))(lightningcss@1.32.0)(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))(typescript@6.0.2) - '@nx/workspace': 22.6.5(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)) + '@nx/devkit': 22.6.5(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/eslint': 22.6.5(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(@zkochan/js-yaml@0.0.7)(eslint@10.2.0(jiti@2.6.1))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/js': 22.6.5(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/module-federation': 22.6.5(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/helpers@0.5.21)(esbuild@0.27.7)(node-fetch@2.7.0(encoding@0.1.13))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2) + '@nx/rspack': 22.6.5(0f8f61c24ac41c470a42fabffc547d6a) + '@nx/web': 22.6.5(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/webpack': 22.6.5(@babel/traverse@7.29.0)(@rspack/core@1.6.8(@swc/helpers@0.5.21))(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)(html-webpack-plugin@5.6.6(@rspack/core@1.6.8(@swc/helpers@0.5.21))(webpack@5.106.1(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)))(lightningcss@1.32.0)(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))(typescript@6.0.2) + '@nx/workspace': 22.6.5(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)) '@phenomnomnominal/tsquery': 6.1.4(typescript@6.0.2) '@schematics/angular': 21.2.7(chokidar@5.0.0) '@typescript-eslint/type-utils': 8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) @@ -22041,8 +22187,8 @@ snapshots: tslib: 2.8.1 webpack-merge: 5.10.0 optionalDependencies: - '@angular-devkit/build-angular': 21.2.7(7a6d932c692adf7b91035f989a2c47b9) - '@angular/build': 21.2.7(6919e34d293f380cbe3efb02122eac30) + '@angular-devkit/build-angular': 21.2.7(19efef94e4c51e719ac157ec3cc75c84) + '@angular/build': 21.2.7(0b8bb98daa82125a57df7497f4fcef59) ng-packagr: 21.2.2(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2))(tailwindcss@4.2.2)(tslib@2.8.1)(typescript@6.0.2) transitivePeerDependencies: - '@babel/traverse' @@ -22079,18 +22225,18 @@ snapshots: - webpack-cli - webpack-hot-middleware - '@nx/angular@22.7.0-beta.12(bbf4031437ab82b6be657ea8be05b0c3)': + '@nx/angular@22.7.0-beta.12(05b4503607f205d03d993da8e423f9be)': dependencies: '@angular-devkit/core': 21.2.7(chokidar@5.0.0) '@angular-devkit/schematics': 21.2.7(chokidar@5.0.0) - '@nx/devkit': 22.7.0-beta.12(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) - '@nx/eslint': 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(@zkochan/js-yaml@0.0.7)(eslint@10.2.0(jiti@2.6.1))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) - '@nx/js': 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) - '@nx/module-federation': 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/helpers@0.5.21)(esbuild@0.27.7)(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2) - '@nx/rspack': 22.7.0-beta.12(48cd17b689492b71c69607744627af60) - '@nx/web': 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) - '@nx/webpack': 22.7.0-beta.12(@babel/traverse@7.29.0)(@rspack/core@1.7.11(@swc/helpers@0.5.21))(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)(html-webpack-plugin@5.6.6(@rspack/core@1.7.11(@swc/helpers@0.5.21))(webpack@5.106.1(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)))(lightningcss@1.32.0)(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))(typescript@6.0.2) - '@nx/workspace': 22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)) + '@nx/devkit': 22.7.0-beta.12(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/eslint': 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(@zkochan/js-yaml@0.0.7)(eslint@10.2.0(jiti@2.6.1))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/js': 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/module-federation': 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/helpers@0.5.21)(esbuild@0.27.7)(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2) + '@nx/rspack': 22.7.0-beta.12(592fb373ea7864f3acebaf5cd4494711) + '@nx/web': 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/webpack': 22.7.0-beta.12(@babel/traverse@7.29.0)(@rspack/core@1.7.11(@swc/helpers@0.5.21))(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)(html-webpack-plugin@5.6.6(@rspack/core@1.7.11(@swc/helpers@0.5.21))(webpack@5.106.1(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)))(lightningcss@1.32.0)(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))(typescript@6.0.2) + '@nx/workspace': 22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)) '@phenomnomnominal/tsquery': 6.1.4(typescript@6.0.2) '@schematics/angular': 21.2.7(chokidar@5.0.0) '@typescript-eslint/type-utils': 8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) @@ -22103,8 +22249,8 @@ snapshots: tslib: 2.8.1 webpack-merge: 5.10.0 optionalDependencies: - '@angular-devkit/build-angular': 21.2.7(0d8d439723faf789318b03d836c4657d) - '@angular/build': 21.2.7(6919e34d293f380cbe3efb02122eac30) + '@angular-devkit/build-angular': 21.2.7(b8f20a26ea5ae043ece601bbe8d4f9d0) + '@angular/build': 21.2.7(0b8bb98daa82125a57df7497f4fcef59) ng-packagr: 21.2.2(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2))(tailwindcss@4.2.2)(tslib@2.8.1)(typescript@6.0.2) transitivePeerDependencies: - '@babel/traverse' @@ -22141,11 +22287,11 @@ snapshots: - webpack-cli - webpack-hot-middleware - '@nx/cypress@22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(@zkochan/js-yaml@0.0.7)(eslint@10.2.0(jiti@2.6.1))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))(typescript@6.0.2)': + '@nx/cypress@22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(@zkochan/js-yaml@0.0.7)(eslint@10.2.0(jiti@2.6.1))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))(typescript@6.0.2)': dependencies: - '@nx/devkit': 22.7.0-beta.12(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) - '@nx/eslint': 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(@zkochan/js-yaml@0.0.7)(eslint@10.2.0(jiti@2.6.1))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) - '@nx/js': 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/devkit': 22.7.0-beta.12(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/eslint': 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(@zkochan/js-yaml@0.0.7)(eslint@10.2.0(jiti@2.6.1))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/js': 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) '@phenomnomnominal/tsquery': 6.1.4(typescript@6.0.2) detect-port: 1.6.1 semver: 7.7.4 @@ -22163,43 +22309,43 @@ snapshots: - typescript - verdaccio - '@nx/devkit@22.6.5(nx@22.6.5(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))': + '@nx/devkit@22.6.5(nx@22.6.5(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))': dependencies: '@zkochan/js-yaml': 0.0.7 ejs: 5.0.1 enquirer: 2.3.6 minimatch: 10.2.4 - nx: 22.6.5(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)) + nx: 22.6.5(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)) semver: 7.7.4 tslib: 2.8.1 yargs-parser: 21.1.1 - '@nx/devkit@22.6.5(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))': + '@nx/devkit@22.6.5(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))': dependencies: '@zkochan/js-yaml': 0.0.7 ejs: 5.0.1 enquirer: 2.3.6 minimatch: 10.2.4 - nx: 22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)) + nx: 22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)) semver: 7.7.4 tslib: 2.8.1 yargs-parser: 21.1.1 - '@nx/devkit@22.7.0-beta.12(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))': + '@nx/devkit@22.7.0-beta.12(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))': dependencies: '@zkochan/js-yaml': 0.0.7 ejs: 5.0.1 enquirer: 2.3.6 minimatch: 10.2.4 - nx: 22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)) + nx: 22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)) semver: 7.7.4 tslib: 2.8.1 yargs-parser: 21.1.1 - '@nx/eslint-plugin@22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(@typescript-eslint/parser@8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint-config-prettier@10.1.8(eslint@10.2.0(jiti@2.6.1)))(eslint@10.2.0(jiti@2.6.1))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))(typescript@6.0.2)': + '@nx/eslint-plugin@22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(@typescript-eslint/parser@8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2))(eslint-config-prettier@10.1.8(eslint@10.2.0(jiti@2.6.1)))(eslint@10.2.0(jiti@2.6.1))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))(typescript@6.0.2)': dependencies: - '@nx/devkit': 22.7.0-beta.12(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) - '@nx/js': 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/devkit': 22.7.0-beta.12(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/js': 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) '@phenomnomnominal/tsquery': 6.1.4(typescript@6.0.2) '@typescript-eslint/parser': 8.58.1(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) '@typescript-eslint/type-utils': 8.58.2(eslint@10.2.0(jiti@2.6.1))(typescript@6.0.2) @@ -22223,10 +22369,10 @@ snapshots: - typescript - verdaccio - '@nx/eslint@22.6.5(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(@zkochan/js-yaml@0.0.7)(eslint@10.2.0(jiti@2.6.1))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))': + '@nx/eslint@22.6.5(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(@zkochan/js-yaml@0.0.7)(eslint@10.2.0(jiti@2.6.1))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))': dependencies: - '@nx/devkit': 22.6.5(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) - '@nx/js': 22.6.5(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/devkit': 22.6.5(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/js': 22.6.5(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) eslint: 10.2.0(jiti@2.6.1) semver: 7.7.4 tslib: 2.8.1 @@ -22242,10 +22388,10 @@ snapshots: - supports-color - verdaccio - '@nx/eslint@22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(@zkochan/js-yaml@0.0.7)(eslint@10.2.0(jiti@2.6.1))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))': + '@nx/eslint@22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(@zkochan/js-yaml@0.0.7)(eslint@10.2.0(jiti@2.6.1))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))': dependencies: - '@nx/devkit': 22.7.0-beta.12(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) - '@nx/js': 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/devkit': 22.7.0-beta.12(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/js': 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) eslint: 10.2.0(jiti@2.6.1) semver: 7.7.4 tslib: 2.8.1 @@ -22261,12 +22407,12 @@ snapshots: - supports-color - verdaccio - '@nx/jest@22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(@types/node@25.6.0)(babel-plugin-macros@3.1.0)(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))(typescript@6.0.2)': + '@nx/jest@22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(@types/node@25.6.0)(babel-plugin-macros@3.1.0)(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))(typescript@6.0.2)': dependencies: '@jest/reporters': 30.3.0 '@jest/test-result': 30.3.0 - '@nx/devkit': 22.7.0-beta.12(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) - '@nx/js': 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/devkit': 22.7.0-beta.12(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/js': 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) '@phenomnomnominal/tsquery': 6.1.4(typescript@6.0.2) identity-obj-proxy: 3.0.0 jest-config: 30.3.0(@types/node@25.6.0)(babel-plugin-macros@3.1.0) @@ -22293,7 +22439,7 @@ snapshots: - typescript - verdaccio - '@nx/js@22.6.5(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))': + '@nx/js@22.6.5(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-proposal-decorators': 7.29.0(@babel/core@7.29.0) @@ -22302,8 +22448,8 @@ snapshots: '@babel/preset-env': 7.29.2(@babel/core@7.29.0) '@babel/preset-typescript': 7.28.5(@babel/core@7.29.0) '@babel/runtime': 7.29.2 - '@nx/devkit': 22.6.5(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) - '@nx/workspace': 22.6.5(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)) + '@nx/devkit': 22.6.5(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/workspace': 22.6.5(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)) '@zkochan/js-yaml': 0.0.7 babel-plugin-const-enum: 1.2.0(@babel/core@7.29.0) babel-plugin-macros: 3.1.0 @@ -22329,7 +22475,7 @@ snapshots: - nx - supports-color - '@nx/js@22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))': + '@nx/js@22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-proposal-decorators': 7.29.0(@babel/core@7.29.0) @@ -22338,8 +22484,8 @@ snapshots: '@babel/preset-env': 7.29.2(@babel/core@7.29.0) '@babel/preset-typescript': 7.28.5(@babel/core@7.29.0) '@babel/runtime': 7.29.2 - '@nx/devkit': 22.7.0-beta.12(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) - '@nx/workspace': 22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)) + '@nx/devkit': 22.7.0-beta.12(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/workspace': 22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)) '@zkochan/js-yaml': 0.0.7 babel-plugin-const-enum: 1.2.0(@babel/core@7.29.0) babel-plugin-macros: 3.1.0 @@ -22365,14 +22511,14 @@ snapshots: - nx - supports-color - '@nx/module-federation@22.6.5(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/helpers@0.5.21)(esbuild@0.27.7)(node-fetch@2.7.0(encoding@0.1.13))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2)': + '@nx/module-federation@22.6.5(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/helpers@0.5.21)(esbuild@0.27.7)(node-fetch@2.7.0(encoding@0.1.13))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2)': dependencies: '@module-federation/enhanced': 2.3.2(@rspack/core@1.6.8(@swc/helpers@0.5.21))(node-fetch@2.7.0(encoding@0.1.13))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2)(webpack@5.106.1(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) '@module-federation/node': 2.7.40(@rspack/core@1.6.8(@swc/helpers@0.5.21))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2)(webpack@5.106.1(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) '@module-federation/sdk': 2.3.2(node-fetch@2.7.0(encoding@0.1.13)) - '@nx/devkit': 22.6.5(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) - '@nx/js': 22.6.5(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) - '@nx/web': 22.6.5(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/devkit': 22.6.5(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/js': 22.6.5(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/web': 22.6.5(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) '@rspack/core': 1.6.8(@swc/helpers@0.5.21) express: 4.22.1 http-proxy-middleware: 3.0.5 @@ -22399,14 +22545,14 @@ snapshots: - vue-tsc - webpack-cli - '@nx/module-federation@22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/helpers@0.5.21)(esbuild@0.27.7)(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2)': + '@nx/module-federation@22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/helpers@0.5.21)(esbuild@0.27.7)(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2)': dependencies: '@module-federation/enhanced': 2.3.2(@rspack/core@1.6.8(@swc/helpers@0.5.21))(node-fetch@2.7.0(encoding@0.1.13))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2)(webpack@5.106.1(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) '@module-federation/node': 2.7.40(@rspack/core@1.6.8(@swc/helpers@0.5.21))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2)(webpack@5.106.1(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) '@module-federation/sdk': 2.3.2(node-fetch@2.7.0(encoding@0.1.13)) - '@nx/devkit': 22.7.0-beta.12(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) - '@nx/js': 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) - '@nx/web': 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/devkit': 22.7.0-beta.12(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/js': 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/web': 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) '@rspack/core': 1.6.8(@swc/helpers@0.5.21) express: 4.22.1 http-proxy-middleware: 3.0.5 @@ -22493,11 +22639,11 @@ snapshots: '@nx/nx-win32-x64-msvc@22.7.0-beta.12': optional: true - '@nx/playwright@22.7.0-beta.12(@babel/traverse@7.29.0)(@playwright/test@1.59.1)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(@zkochan/js-yaml@0.0.7)(eslint@10.2.0(jiti@2.6.1))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))': + '@nx/playwright@22.7.0-beta.12(@babel/traverse@7.29.0)(@playwright/test@1.59.1)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(@zkochan/js-yaml@0.0.7)(eslint@10.2.0(jiti@2.6.1))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))': dependencies: - '@nx/devkit': 22.7.0-beta.12(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) - '@nx/eslint': 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(@zkochan/js-yaml@0.0.7)(eslint@10.2.0(jiti@2.6.1))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) - '@nx/js': 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/devkit': 22.7.0-beta.12(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/eslint': 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(@zkochan/js-yaml@0.0.7)(eslint@10.2.0(jiti@2.6.1))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/js': 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) minimatch: 10.2.4 tslib: 2.8.1 optionalDependencies: @@ -22513,12 +22659,12 @@ snapshots: - supports-color - verdaccio - '@nx/plugin@22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(@types/node@25.6.0)(@zkochan/js-yaml@0.0.7)(babel-plugin-macros@3.1.0)(eslint@10.2.0(jiti@2.6.1))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))(typescript@6.0.2)': + '@nx/plugin@22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(@types/node@25.6.0)(@zkochan/js-yaml@0.0.7)(babel-plugin-macros@3.1.0)(eslint@10.2.0(jiti@2.6.1))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))(typescript@6.0.2)': dependencies: - '@nx/devkit': 22.7.0-beta.12(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) - '@nx/eslint': 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(@zkochan/js-yaml@0.0.7)(eslint@10.2.0(jiti@2.6.1))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) - '@nx/jest': 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(@types/node@25.6.0)(babel-plugin-macros@3.1.0)(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))(typescript@6.0.2) - '@nx/js': 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/devkit': 22.7.0-beta.12(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/eslint': 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(@zkochan/js-yaml@0.0.7)(eslint@10.2.0(jiti@2.6.1))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/jest': 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(@types/node@25.6.0)(babel-plugin-macros@3.1.0)(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))(typescript@6.0.2) + '@nx/js': 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) tslib: 2.8.1 transitivePeerDependencies: - '@babel/traverse' @@ -22537,14 +22683,14 @@ snapshots: - typescript - verdaccio - '@nx/rspack@22.6.5(10ec6bc8cf7172a1e8f69047189d0c0c)': + '@nx/rspack@22.6.5(0f8f61c24ac41c470a42fabffc547d6a)': dependencies: '@module-federation/enhanced': 2.3.2(@rspack/core@1.6.8(@swc/helpers@0.5.21))(node-fetch@2.7.0(encoding@0.1.13))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2)(webpack@5.106.1(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) '@module-federation/node': 2.7.40(@rspack/core@1.6.8(@swc/helpers@0.5.21))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2)(webpack@5.106.1(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) - '@nx/devkit': 22.6.5(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) - '@nx/js': 22.6.5(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) - '@nx/module-federation': 22.6.5(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/helpers@0.5.21)(esbuild@0.27.7)(node-fetch@2.7.0(encoding@0.1.13))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2) - '@nx/web': 22.6.5(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/devkit': 22.6.5(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/js': 22.6.5(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/module-federation': 22.6.5(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/helpers@0.5.21)(esbuild@0.27.7)(node-fetch@2.7.0(encoding@0.1.13))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2) + '@nx/web': 22.6.5(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) '@phenomnomnominal/tsquery': 6.1.4(typescript@6.0.2) '@rspack/core': 1.6.8(@swc/helpers@0.5.21) '@rspack/dev-server': 1.2.1(@rspack/core@1.6.8(@swc/helpers@0.5.21))(tslib@2.8.1)(webpack@5.106.1(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) @@ -22596,14 +22742,14 @@ snapshots: - webpack-cli - webpack-hot-middleware - '@nx/rspack@22.7.0-beta.12(48cd17b689492b71c69607744627af60)': + '@nx/rspack@22.7.0-beta.12(592fb373ea7864f3acebaf5cd4494711)': dependencies: '@module-federation/enhanced': 2.3.2(@rspack/core@1.7.11(@swc/helpers@0.5.21))(node-fetch@2.7.0(encoding@0.1.13))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2)(webpack@5.106.1(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) '@module-federation/node': 2.7.40(@rspack/core@1.7.11(@swc/helpers@0.5.21))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2)(webpack@5.106.1(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) - '@nx/devkit': 22.7.0-beta.12(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) - '@nx/js': 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) - '@nx/module-federation': 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/helpers@0.5.21)(esbuild@0.27.7)(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2) - '@nx/web': 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/devkit': 22.7.0-beta.12(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/js': 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/module-federation': 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/helpers@0.5.21)(esbuild@0.27.7)(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@6.0.2) + '@nx/web': 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) '@phenomnomnominal/tsquery': 6.1.4(typescript@6.0.2) '@rspack/core': 1.6.8(@swc/helpers@0.5.21) '@rspack/dev-server': 1.2.1(@rspack/core@1.6.8(@swc/helpers@0.5.21))(tslib@2.8.1)(webpack@5.106.1(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)) @@ -22655,12 +22801,12 @@ snapshots: - webpack-cli - webpack-hot-middleware - '@nx/storybook@22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(@zkochan/js-yaml@0.0.7)(eslint@10.2.0(jiti@2.6.1))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.2)': + '@nx/storybook@22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(@zkochan/js-yaml@0.0.7)(eslint@10.2.0(jiti@2.6.1))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))(storybook@10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(typescript@6.0.2)': dependencies: - '@nx/cypress': 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(@zkochan/js-yaml@0.0.7)(eslint@10.2.0(jiti@2.6.1))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))(typescript@6.0.2) - '@nx/devkit': 22.7.0-beta.12(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) - '@nx/eslint': 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(@zkochan/js-yaml@0.0.7)(eslint@10.2.0(jiti@2.6.1))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) - '@nx/js': 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/cypress': 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(@zkochan/js-yaml@0.0.7)(eslint@10.2.0(jiti@2.6.1))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))(typescript@6.0.2) + '@nx/devkit': 22.7.0-beta.12(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/eslint': 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(@zkochan/js-yaml@0.0.7)(eslint@10.2.0(jiti@2.6.1))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/js': 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) '@phenomnomnominal/tsquery': 6.1.4(typescript@6.0.2) semver: 7.7.4 storybook: 10.3.5(@testing-library/dom@10.4.1)(prettier@3.8.2)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) @@ -22678,11 +22824,11 @@ snapshots: - typescript - verdaccio - '@nx/vite@22.6.5(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))(typescript@6.0.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.4)': + '@nx/vite@22.6.5(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))(typescript@6.0.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.4)': dependencies: - '@nx/devkit': 22.6.5(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) - '@nx/js': 22.6.5(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) - '@nx/vitest': 22.6.5(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))(typescript@6.0.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.4) + '@nx/devkit': 22.6.5(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/js': 22.6.5(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/vitest': 22.6.5(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))(typescript@6.0.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.4) '@phenomnomnominal/tsquery': 6.1.4(typescript@6.0.2) ajv: 8.18.0 enquirer: 2.3.6 @@ -22702,11 +22848,11 @@ snapshots: - typescript - verdaccio - '@nx/vite@22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))(typescript@6.0.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.4)': + '@nx/vite@22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))(typescript@6.0.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.4)': dependencies: - '@nx/devkit': 22.7.0-beta.12(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) - '@nx/js': 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) - '@nx/vitest': 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))(typescript@6.0.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.4) + '@nx/devkit': 22.7.0-beta.12(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/js': 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/vitest': 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))(typescript@6.0.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.4) '@phenomnomnominal/tsquery': 6.1.4(typescript@6.0.2) ajv: 8.18.0 enquirer: 2.3.6 @@ -22726,10 +22872,10 @@ snapshots: - typescript - verdaccio - '@nx/vitest@22.6.5(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))(typescript@6.0.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.4)': + '@nx/vitest@22.6.5(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))(typescript@6.0.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.4)': dependencies: - '@nx/devkit': 22.6.5(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) - '@nx/js': 22.6.5(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/devkit': 22.6.5(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/js': 22.6.5(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) '@phenomnomnominal/tsquery': 6.1.4(typescript@6.0.2) semver: 7.7.4 tslib: 2.8.1 @@ -22746,10 +22892,10 @@ snapshots: - typescript - verdaccio - '@nx/vitest@22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))(typescript@6.0.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.4)': + '@nx/vitest@22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))(typescript@6.0.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.4)': dependencies: - '@nx/devkit': 22.7.0-beta.12(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) - '@nx/js': 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/devkit': 22.7.0-beta.12(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/js': 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) '@phenomnomnominal/tsquery': 6.1.4(typescript@6.0.2) semver: 7.7.4 tslib: 2.8.1 @@ -22766,10 +22912,10 @@ snapshots: - typescript - verdaccio - '@nx/web@22.6.5(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))': + '@nx/web@22.6.5(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))': dependencies: - '@nx/devkit': 22.6.5(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) - '@nx/js': 22.6.5(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/devkit': 22.6.5(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/js': 22.6.5(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) detect-port: 1.6.1 http-server: 14.1.1 picocolors: 1.1.1 @@ -22783,10 +22929,10 @@ snapshots: - supports-color - verdaccio - '@nx/web@22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))': + '@nx/web@22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))': dependencies: - '@nx/devkit': 22.7.0-beta.12(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) - '@nx/js': 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/devkit': 22.7.0-beta.12(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/js': 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) detect-port: 1.6.1 http-server: 14.1.1 picocolors: 1.1.1 @@ -22800,11 +22946,11 @@ snapshots: - supports-color - verdaccio - '@nx/webpack@22.6.5(@babel/traverse@7.29.0)(@rspack/core@1.6.8(@swc/helpers@0.5.21))(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)(html-webpack-plugin@5.6.6(@rspack/core@1.6.8(@swc/helpers@0.5.21))(webpack@5.106.1(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)))(lightningcss@1.32.0)(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))(typescript@6.0.2)': + '@nx/webpack@22.6.5(@babel/traverse@7.29.0)(@rspack/core@1.6.8(@swc/helpers@0.5.21))(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)(html-webpack-plugin@5.6.6(@rspack/core@1.6.8(@swc/helpers@0.5.21))(webpack@5.106.1(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)))(lightningcss@1.32.0)(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))(typescript@6.0.2)': dependencies: '@babel/core': 7.29.0 - '@nx/devkit': 22.6.5(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) - '@nx/js': 22.6.5(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/devkit': 22.6.5(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/js': 22.6.5(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) '@phenomnomnominal/tsquery': 6.1.4(typescript@6.0.2) ajv: 8.18.0 autoprefixer: 10.4.27(postcss@8.5.9) @@ -22861,11 +23007,11 @@ snapshots: - verdaccio - webpack-cli - '@nx/webpack@22.7.0-beta.12(@babel/traverse@7.29.0)(@rspack/core@1.7.11(@swc/helpers@0.5.21))(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)(html-webpack-plugin@5.6.6(@rspack/core@1.7.11(@swc/helpers@0.5.21))(webpack@5.106.1(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)))(lightningcss@1.32.0)(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))(typescript@6.0.2)': + '@nx/webpack@22.7.0-beta.12(@babel/traverse@7.29.0)(@rspack/core@1.7.11(@swc/helpers@0.5.21))(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)(html-webpack-plugin@5.6.6(@rspack/core@1.7.11(@swc/helpers@0.5.21))(webpack@5.106.1(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7)))(lightningcss@1.32.0)(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)))(typescript@6.0.2)': dependencies: '@babel/core': 7.29.0 - '@nx/devkit': 22.7.0-beta.12(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) - '@nx/js': 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/devkit': 22.7.0-beta.12(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/js': 22.7.0-beta.12(@babel/traverse@7.29.0)(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) '@phenomnomnominal/tsquery': 6.1.4(typescript@6.0.2) ajv: 8.18.0 autoprefixer: 10.4.27(postcss@8.5.9) @@ -22922,13 +23068,13 @@ snapshots: - verdaccio - webpack-cli - '@nx/workspace@22.6.5(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))': + '@nx/workspace@22.6.5(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))': dependencies: - '@nx/devkit': 22.6.5(nx@22.6.5(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/devkit': 22.6.5(nx@22.6.5(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) '@zkochan/js-yaml': 0.0.7 chalk: 4.1.2 enquirer: 2.3.6 - nx: 22.6.5(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)) + nx: 22.6.5(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)) picomatch: 4.0.4 semver: 7.7.4 tslib: 2.8.1 @@ -22938,13 +23084,13 @@ snapshots: - '@swc/core' - debug - '@nx/workspace@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))': + '@nx/workspace@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))': dependencies: - '@nx/devkit': 22.7.0-beta.12(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) + '@nx/devkit': 22.7.0-beta.12(nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21))) '@zkochan/js-yaml': 0.0.7 chalk: 4.1.2 enquirer: 2.3.6 - nx: 22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)) + nx: 22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)) picomatch: 4.0.4 semver: 7.7.4 tslib: 2.8.1 @@ -23119,9 +23265,9 @@ snapshots: '@oxc-parser/binding-openharmony-arm64@0.124.0': optional: true - '@oxc-parser/binding-wasm32-wasi@0.124.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': + '@oxc-parser/binding-wasm32-wasi@0.124.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': dependencies: - '@napi-rs/wasm-runtime': 1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + '@napi-rs/wasm-runtime': 1.1.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) transitivePeerDependencies: - '@emnapi/core' - '@emnapi/runtime' @@ -23142,6 +23288,8 @@ snapshots: '@oxc-project/types@0.124.0': {} + '@oxc-project/types@0.132.0': {} + '@oxc-resolver/binding-android-arm-eabi@11.19.1': optional: true @@ -23190,9 +23338,9 @@ snapshots: '@oxc-resolver/binding-openharmony-arm64@11.19.1': optional: true - '@oxc-resolver/binding-wasm32-wasi@11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': + '@oxc-resolver/binding-wasm32-wasi@11.19.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': dependencies: - '@napi-rs/wasm-runtime': 1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + '@napi-rs/wasm-runtime': 1.1.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) transitivePeerDependencies: - '@emnapi/core' - '@emnapi/runtime' @@ -23255,9 +23403,9 @@ snapshots: '@oxc-transform/binding-openharmony-arm64@0.124.0': optional: true - '@oxc-transform/binding-wasm32-wasi@0.124.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': + '@oxc-transform/binding-wasm32-wasi@0.124.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': dependencies: - '@napi-rs/wasm-runtime': 1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + '@napi-rs/wasm-runtime': 1.1.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) transitivePeerDependencies: - '@emnapi/core' - '@emnapi/runtime' @@ -23541,66 +23689,102 @@ snapshots: '@rolldown/binding-android-arm64@1.0.0-rc.4': optional: true + '@rolldown/binding-android-arm64@1.0.2': + optional: true + '@rolldown/binding-darwin-arm64@1.0.0-rc.15': optional: true '@rolldown/binding-darwin-arm64@1.0.0-rc.4': optional: true + '@rolldown/binding-darwin-arm64@1.0.2': + optional: true + '@rolldown/binding-darwin-x64@1.0.0-rc.15': optional: true '@rolldown/binding-darwin-x64@1.0.0-rc.4': optional: true + '@rolldown/binding-darwin-x64@1.0.2': + optional: true + '@rolldown/binding-freebsd-x64@1.0.0-rc.15': optional: true '@rolldown/binding-freebsd-x64@1.0.0-rc.4': optional: true + '@rolldown/binding-freebsd-x64@1.0.2': + optional: true + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.15': optional: true '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.4': optional: true + '@rolldown/binding-linux-arm-gnueabihf@1.0.2': + optional: true + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.15': optional: true '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.4': optional: true + '@rolldown/binding-linux-arm64-gnu@1.0.2': + optional: true + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.15': optional: true '@rolldown/binding-linux-arm64-musl@1.0.0-rc.4': optional: true + '@rolldown/binding-linux-arm64-musl@1.0.2': + optional: true + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15': optional: true + '@rolldown/binding-linux-ppc64-gnu@1.0.2': + optional: true + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15': optional: true + '@rolldown/binding-linux-s390x-gnu@1.0.2': + optional: true + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.15': optional: true '@rolldown/binding-linux-x64-gnu@1.0.0-rc.4': optional: true + '@rolldown/binding-linux-x64-gnu@1.0.2': + optional: true + '@rolldown/binding-linux-x64-musl@1.0.0-rc.15': optional: true '@rolldown/binding-linux-x64-musl@1.0.0-rc.4': optional: true + '@rolldown/binding-linux-x64-musl@1.0.2': + optional: true + '@rolldown/binding-openharmony-arm64@1.0.0-rc.15': optional: true '@rolldown/binding-openharmony-arm64@1.0.0-rc.4': optional: true + '@rolldown/binding-openharmony-arm64@1.0.2': + optional: true + '@rolldown/binding-wasm32-wasi@1.0.0-rc.15': dependencies: '@emnapi/core': 1.9.2 @@ -23608,32 +23792,47 @@ snapshots: '@napi-rs/wasm-runtime': 1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) optional: true - '@rolldown/binding-wasm32-wasi@1.0.0-rc.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': + '@rolldown/binding-wasm32-wasi@1.0.0-rc.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': dependencies: - '@napi-rs/wasm-runtime': 1.1.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + '@napi-rs/wasm-runtime': 1.1.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) transitivePeerDependencies: - '@emnapi/core' - '@emnapi/runtime' optional: true + '@rolldown/binding-wasm32-wasi@1.0.2': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.15': optional: true '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.4': optional: true + '@rolldown/binding-win32-arm64-msvc@1.0.2': + optional: true + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.15': optional: true '@rolldown/binding-win32-x64-msvc@1.0.0-rc.4': optional: true + '@rolldown/binding-win32-x64-msvc@1.0.2': + optional: true + '@rolldown/pluginutils@1.0.0-rc.15': {} '@rolldown/pluginutils@1.0.0-rc.3': {} '@rolldown/pluginutils@1.0.0-rc.4': {} + '@rolldown/pluginutils@1.0.1': {} + '@rollup/plugin-json@6.1.0(rollup@4.60.1)': dependencies: '@rollup/pluginutils': 5.3.0(rollup@4.60.1) @@ -24175,10 +24374,10 @@ snapshots: - react - react-dom - '@storybook/angular@10.3.5(f55f33330bfbd6fb184961c0aca35221)': + '@storybook/angular@10.3.5(56fe732b02a35e211a39a35ea47e2513)': dependencies: '@angular-devkit/architect': 0.2102.7(chokidar@5.0.0) - '@angular-devkit/build-angular': 21.2.7(0d8d439723faf789318b03d836c4657d) + '@angular-devkit/build-angular': 21.2.7(b8f20a26ea5ae043ece601bbe8d4f9d0) '@angular-devkit/core': 21.2.7(chokidar@5.0.0) '@angular/common': 21.2.8(@angular/core@21.2.8(@angular/compiler@21.2.8)(rxjs@7.8.2)(zone.js@0.16.1))(rxjs@7.8.2) '@angular/compiler': 21.2.8 @@ -24370,14 +24569,14 @@ snapshots: '@swc/core': 1.15.24(@swc/helpers@0.5.21) '@swc/types': 0.1.26 - '@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2)': + '@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2)': dependencies: '@swc-node/core': 1.14.1(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26) '@swc-node/sourcemap-support': 0.6.1 '@swc/core': 1.15.24(@swc/helpers@0.5.21) colorette: 2.0.20 debug: 4.4.3 - oxc-resolver: 11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + oxc-resolver: 11.19.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) pirates: 4.0.7 tslib: 2.8.1 typescript: 6.0.2 @@ -27714,9 +27913,9 @@ snapshots: dset@3.1.4: {} - dts-resolver@2.1.3(oxc-resolver@11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)): + dts-resolver@2.1.3(oxc-resolver@11.19.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)): optionalDependencies: - oxc-resolver: 11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + oxc-resolver: 11.19.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) dunder-proto@1.0.1: dependencies: @@ -27810,11 +28009,11 @@ snapshots: env-paths@2.2.1: {} - env-runner@0.1.7: + env-runner@0.1.9: dependencies: crossws: 0.4.5(srvx@0.11.15) exsolve: 1.0.8 - httpxy: 0.5.0 + httpxy: 0.5.3 srvx: 0.11.15 environment@1.1.0: {} @@ -28788,7 +28987,7 @@ snapshots: ufo: 1.6.3 uncrypto: 0.1.3 - h3@2.0.1-rc.20(crossws@0.4.5(srvx@0.11.15)): + h3@2.0.1-rc.22(crossws@0.4.5(srvx@0.11.15)): dependencies: rou3: 0.8.1 srvx: 0.11.15 @@ -29214,7 +29413,7 @@ snapshots: transitivePeerDependencies: - supports-color - httpxy@0.5.0: {} + httpxy@0.5.3: {} human-signals@1.1.1: {} @@ -31170,7 +31369,7 @@ snapshots: nerf-dart@1.0.0: {} - nf3@0.3.16: {} + nf3@0.3.17: {} ng-packagr@21.2.2(@angular/compiler-cli@21.2.8(@angular/compiler@21.2.8)(typescript@6.0.2))(tailwindcss@4.2.2)(tslib@2.8.1)(typescript@6.0.2): dependencies: @@ -31202,19 +31401,19 @@ snapshots: rollup: 4.60.1 tailwindcss: 4.2.2 - nitro@3.0.260415-beta(chokidar@5.0.0)(dotenv@16.4.7)(jiti@2.6.1)(lru-cache@11.3.5)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)): + nitro@3.0.260522-beta(chokidar@5.0.0)(dotenv@16.4.7)(jiti@2.6.1)(lru-cache@11.3.5)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)): dependencies: consola: 3.4.2 crossws: 0.4.5(srvx@0.11.15) db0: 0.3.4 - env-runner: 0.1.7 - h3: 2.0.1-rc.20(crossws@0.4.5(srvx@0.11.15)) + env-runner: 0.1.9 + h3: 2.0.1-rc.22(crossws@0.4.5(srvx@0.11.15)) hookable: 6.1.1 - nf3: 0.3.16 + nf3: 0.3.17 ocache: 0.1.4 ofetch: 2.0.0-alpha.3 ohash: 2.0.11 - rolldown: 1.0.0-rc.15 + rolldown: 1.0.2 srvx: 0.11.15 unenv: 2.0.0-rc.24 unstorage: 2.0.0-alpha.7(chokidar@5.0.0)(db0@0.3.4)(lru-cache@11.3.5)(ofetch@2.0.0-alpha.3) @@ -31417,7 +31616,7 @@ snapshots: schema-utils: 3.3.0 webpack: 5.106.1(@swc/core@1.15.24(@swc/helpers@0.5.21))(esbuild@0.27.7) - nx@22.6.5(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)): + nx@22.6.5(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)): dependencies: '@napi-rs/wasm-runtime': 0.2.4 '@yarnpkg/lockfile': 1.1.0 @@ -31466,12 +31665,12 @@ snapshots: '@nx/nx-linux-x64-musl': 22.6.5 '@nx/nx-win32-arm64-msvc': 22.6.5 '@nx/nx-win32-x64-msvc': 22.6.5 - '@swc-node/register': 1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2) + '@swc-node/register': 1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2) '@swc/core': 1.15.24(@swc/helpers@0.5.21) transitivePeerDependencies: - debug - nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)): + nx@22.7.0-beta.12(@swc-node/register@1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2))(@swc/core@1.15.24(@swc/helpers@0.5.21)): dependencies: '@emnapi/core': 1.4.5 '@emnapi/runtime': 1.4.5 @@ -31594,7 +31793,7 @@ snapshots: '@nx/nx-linux-x64-musl': 22.7.0-beta.12 '@nx/nx-win32-arm64-msvc': 22.7.0-beta.12 '@nx/nx-win32-x64-msvc': 22.7.0-beta.12 - '@swc-node/register': 1.11.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2) + '@swc-node/register': 1.11.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@swc/core@1.15.24(@swc/helpers@0.5.21))(@swc/types@0.1.26)(typescript@6.0.2) '@swc/core': 1.15.24(@swc/helpers@0.5.21) transitivePeerDependencies: - debug @@ -31751,7 +31950,7 @@ snapshots: os-tmpdir@1.0.2: {} - oxc-parser@0.124.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2): + oxc-parser@0.124.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0): dependencies: '@oxc-project/types': 0.124.0 optionalDependencies: @@ -31771,7 +31970,7 @@ snapshots: '@oxc-parser/binding-linux-x64-gnu': 0.124.0 '@oxc-parser/binding-linux-x64-musl': 0.124.0 '@oxc-parser/binding-openharmony-arm64': 0.124.0 - '@oxc-parser/binding-wasm32-wasi': 0.124.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + '@oxc-parser/binding-wasm32-wasi': 0.124.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) '@oxc-parser/binding-win32-arm64-msvc': 0.124.0 '@oxc-parser/binding-win32-ia32-msvc': 0.124.0 '@oxc-parser/binding-win32-x64-msvc': 0.124.0 @@ -31779,7 +31978,7 @@ snapshots: - '@emnapi/core' - '@emnapi/runtime' - oxc-resolver@11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2): + oxc-resolver@11.19.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0): optionalDependencies: '@oxc-resolver/binding-android-arm-eabi': 11.19.1 '@oxc-resolver/binding-android-arm64': 11.19.1 @@ -31797,7 +31996,7 @@ snapshots: '@oxc-resolver/binding-linux-x64-gnu': 11.19.1 '@oxc-resolver/binding-linux-x64-musl': 11.19.1 '@oxc-resolver/binding-openharmony-arm64': 11.19.1 - '@oxc-resolver/binding-wasm32-wasi': 11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + '@oxc-resolver/binding-wasm32-wasi': 11.19.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) '@oxc-resolver/binding-win32-arm64-msvc': 11.19.1 '@oxc-resolver/binding-win32-ia32-msvc': 11.19.1 '@oxc-resolver/binding-win32-x64-msvc': 11.19.1 @@ -31805,7 +32004,7 @@ snapshots: - '@emnapi/core' - '@emnapi/runtime' - oxc-transform@0.124.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2): + oxc-transform@0.124.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0): optionalDependencies: '@oxc-transform/binding-android-arm-eabi': 0.124.0 '@oxc-transform/binding-android-arm64': 0.124.0 @@ -31823,7 +32022,7 @@ snapshots: '@oxc-transform/binding-linux-x64-gnu': 0.124.0 '@oxc-transform/binding-linux-x64-musl': 0.124.0 '@oxc-transform/binding-openharmony-arm64': 0.124.0 - '@oxc-transform/binding-wasm32-wasi': 0.124.0(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + '@oxc-transform/binding-wasm32-wasi': 0.124.0(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) '@oxc-transform/binding-win32-arm64-msvc': 0.124.0 '@oxc-transform/binding-win32-ia32-msvc': 0.124.0 '@oxc-transform/binding-win32-x64-msvc': 0.124.0 @@ -33455,7 +33654,7 @@ snapshots: robust-predicates@3.0.3: {} - rolldown-plugin-dts@0.23.2(@typescript/native-preview@7.0.0-dev.20260412.1)(oxc-resolver@11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2))(rolldown@1.0.0-rc.15)(typescript@6.0.2): + rolldown-plugin-dts@0.23.2(@typescript/native-preview@7.0.0-dev.20260412.1)(oxc-resolver@11.19.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0))(rolldown@1.0.0-rc.15)(typescript@6.0.2): dependencies: '@babel/generator': 8.0.0-rc.3 '@babel/helper-validator-identifier': 8.0.0-rc.3 @@ -33463,7 +33662,7 @@ snapshots: '@babel/types': 8.0.0-rc.3 ast-kit: 3.0.0-beta.1 birpc: 4.0.0 - dts-resolver: 2.1.3(oxc-resolver@11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)) + dts-resolver: 2.1.3(oxc-resolver@11.19.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)) get-tsconfig: 4.13.7 obug: 2.1.1 picomatch: 4.0.4 @@ -33495,7 +33694,7 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.15 '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.15 - rolldown@1.0.0-rc.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2): + rolldown@1.0.0-rc.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0): dependencies: '@oxc-project/types': 0.113.0 '@rolldown/pluginutils': 1.0.0-rc.4 @@ -33510,13 +33709,34 @@ snapshots: '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.4 '@rolldown/binding-linux-x64-musl': 1.0.0-rc.4 '@rolldown/binding-openharmony-arm64': 1.0.0-rc.4 - '@rolldown/binding-wasm32-wasi': 1.0.0-rc.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.4 '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.4 transitivePeerDependencies: - '@emnapi/core' - '@emnapi/runtime' + rolldown@1.0.2: + dependencies: + '@oxc-project/types': 0.132.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.2 + '@rolldown/binding-darwin-arm64': 1.0.2 + '@rolldown/binding-darwin-x64': 1.0.2 + '@rolldown/binding-freebsd-x64': 1.0.2 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.2 + '@rolldown/binding-linux-arm64-gnu': 1.0.2 + '@rolldown/binding-linux-arm64-musl': 1.0.2 + '@rolldown/binding-linux-ppc64-gnu': 1.0.2 + '@rolldown/binding-linux-s390x-gnu': 1.0.2 + '@rolldown/binding-linux-x64-gnu': 1.0.2 + '@rolldown/binding-linux-x64-musl': 1.0.2 + '@rolldown/binding-openharmony-arm64': 1.0.2 + '@rolldown/binding-wasm32-wasi': 1.0.2 + '@rolldown/binding-win32-arm64-msvc': 1.0.2 + '@rolldown/binding-win32-x64-msvc': 1.0.2 + rollup-plugin-dts@6.4.1(rollup@4.60.1)(typescript@6.0.2): dependencies: '@jridgewell/remapping': 2.3.5 @@ -34774,7 +34994,7 @@ snapshots: minimist: 1.2.8 strip-bom: 3.0.0 - tsdown@0.21.8(@typescript/native-preview@7.0.0-dev.20260412.1)(oxc-resolver@11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2))(synckit@0.11.12)(typescript@6.0.2): + tsdown@0.21.8(@typescript/native-preview@7.0.0-dev.20260412.1)(oxc-resolver@11.19.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0))(synckit@0.11.12)(typescript@6.0.2): dependencies: ansis: 4.2.0 cac: 7.0.0 @@ -34785,7 +35005,7 @@ snapshots: obug: 2.1.1 picomatch: 4.0.4 rolldown: 1.0.0-rc.15 - rolldown-plugin-dts: 0.23.2(@typescript/native-preview@7.0.0-dev.20260412.1)(oxc-resolver@11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2))(rolldown@1.0.0-rc.15)(typescript@6.0.2) + rolldown-plugin-dts: 0.23.2(@typescript/native-preview@7.0.0-dev.20260412.1)(oxc-resolver@11.19.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0))(rolldown@1.0.0-rc.15)(typescript@6.0.2) semver: 7.7.4 tinyexec: 1.1.1 tinyglobby: 0.2.16 @@ -35202,10 +35422,10 @@ snapshots: - typescript - ws - vite-tsconfig-paths@7.0.0-alpha.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(typescript@6.0.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)): + vite-tsconfig-paths@7.0.0-alpha.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(typescript@6.0.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3)): dependencies: debug: 4.4.3 - oxc-resolver: 11.19.1(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + oxc-resolver: 11.19.1(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) tsconfck: 3.1.6(typescript@6.0.2) vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(less@4.6.4)(sass-embedded@1.99.0)(sass@1.99.0)(terser@5.46.1)(yaml@2.8.3) transitivePeerDependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index a4593bf65..8a3f33f61 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -139,7 +139,7 @@ catalog: mermaid: ^11.13.0 minimist: ^1.2.8 ng-packagr: 21.2.2 - nitro: 3.0.260415-beta + nitro: 3.0.260522-beta nx: 22.7.0-beta.12 obug: ^2.1.1 ofetch: 2.0.0-alpha.3 From 94667f4996f1b3132f6ce7293fb912352e76c001 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Mon, 25 May 2026 15:03:08 -0500 Subject: [PATCH 42/65] fix(platform): stop adding nitro to package.json in ng-update schematic `nitro` is a transitive dependency of `@analogjs/platform` and gets hoisted into the consuming workspace's node_modules by npm/yarn automatically; the schematic doesn't need to write a top-level declaration for it. Adding it forced the schematic to carry a version pin in lockstep with the platform's own catalog entry, and surfaced as a behavior change to users who would otherwise rely on the transitive. pnpm users still need an explicit top-level `nitro` entry due to strict isolation; the migration guide now calls that out directly instead of relying on the schematic to handle it. Drops NITRO_PKG / NITRO_VERSION constants, the second branch in addDependencies(), the corresponding spec assertions, and updates the 'Notes for automated migration' bullet. --- .../docs/guides/migrating-v2-to-v3.md | 2 +- .../migrate-to-separated-plugins.spec.ts | 22 +++++++++++-------- .../migrate-to-separated-plugins.ts | 10 --------- 3 files changed, 14 insertions(+), 20 deletions(-) diff --git a/apps/docs-app/docs/guides/migrating-v2-to-v3.md b/apps/docs-app/docs/guides/migrating-v2-to-v3.md index 06a03ac40..1d3146d6c 100644 --- a/apps/docs-app/docs/guides/migrating-v2-to-v3.md +++ b/apps/docs-app/docs/guides/migrating-v2-to-v3.md @@ -320,7 +320,7 @@ Keep automated migration tooling focused on the breaking changes above: - replace deep or internal imports with public package entrypoints - split `analog()` into `analog() + angular() + nitro()`, moving each option to the plugin that now owns it (see [plugin separation](#analog-angular-and-nitro-are-now-separate-plugins)) - @analogjs/platform no longer composes @analogjs/vite-plugin-nitro internally; direct importers can either migrate to `@analogjs/platform` + `nitro/vite` (recommended) or continue using @analogjs/vite-plugin-nitro standalone -- add `@analogjs/vite-plugin-angular` and `nitro` to app `devDependencies` (the separated shape imports them directly) +- add `@analogjs/vite-plugin-angular` to app `devDependencies` (the separated shape imports it directly). `nitro` is picked up as a transitive of `@analogjs/platform` for npm/yarn; pnpm users must add it to `devDependencies` explicitly - replace `@nx/vite:build` with `nx:run-commands` invoking `vite build -c apps//vite.config.ts`; drop the legacy `build.outDir` override and update `outputs` to `apps//.output` - add `server.fs.allow` pointing at the workspace root in `vite.config.ts` so Vite 8's strict fs allows nitro/vite's env runner to load its own dev runtime through pnpm content-hash paths - add explicit `analog({ content: { highlighter: 'shiki' } })` config when the app renders markdown content diff --git a/packages/platform/migrations/migrate-to-separated-plugins/migrate-to-separated-plugins.spec.ts b/packages/platform/migrations/migrate-to-separated-plugins/migrate-to-separated-plugins.spec.ts index 54f879e37..cde90921d 100644 --- a/packages/platform/migrations/migrate-to-separated-plugins/migrate-to-separated-plugins.spec.ts +++ b/packages/platform/migrations/migrate-to-separated-plugins/migrate-to-separated-plugins.spec.ts @@ -68,7 +68,10 @@ describe('migrate-to-separated-plugins', () => { expect(pkg.devDependencies['@analogjs/vite-plugin-angular']).toBe( PLATFORM_VERSION, ); - expect(pkg.devDependencies['nitro']).toBeTruthy(); + // The schematic does not pin `nitro` — npm/yarn pick it up as a transitive + // of @analogjs/platform; pnpm users add it themselves following the + // migration guide. + expect(pkg.devDependencies['nitro']).toBeUndefined(); expect(infoLogs.join('\n')).toContain('/vite.config.ts'); expect(infoLogs.join('\n')).toContain('migrating-v2-to-v3'); }); @@ -89,13 +92,11 @@ describe('migrate-to-separated-plugins', () => { expect( pkg.devDependencies['@analogjs/vite-plugin-angular'], ).toBeUndefined(); - expect(pkg.devDependencies['nitro']).toBeUndefined(); expect(infoLogs).toEqual([]); }); it('does not duplicate deps that are already declared', () => { const PREEXISTING_VPA = '^2.5.0'; - const PREEXISTING_NITRO = '3.0.0-beta'; tree.create('/vite.config.ts', LEGACY_CONFIG); tree.create( '/package.json', @@ -103,7 +104,6 @@ describe('migrate-to-separated-plugins', () => { devDependencies: { '@analogjs/platform': PLATFORM_VERSION, '@analogjs/vite-plugin-angular': PREEXISTING_VPA, - nitro: PREEXISTING_NITRO, }, }), ); @@ -115,7 +115,6 @@ describe('migrate-to-separated-plugins', () => { expect(pkg.devDependencies['@analogjs/vite-plugin-angular']).toBe( PREEXISTING_VPA, ); - expect(pkg.devDependencies['nitro']).toBe(PREEXISTING_NITRO); }); it('reads version from `dependencies` if `devDependencies` is missing platform', () => { @@ -150,7 +149,9 @@ describe('migrate-to-separated-plugins', () => { migrateToSeparatedPlugins()(tree, context); const pkg = JSON.parse(tree.readContent('/package.json')); - expect(pkg.devDependencies['nitro']).toBeUndefined(); + expect( + pkg.devDependencies['@analogjs/vite-plugin-angular'], + ).toBeUndefined(); }); it('only matches vite.config files (vite.config.ts, .mts, .js, .mjs)', () => { @@ -167,7 +168,9 @@ describe('migrate-to-separated-plugins', () => { migrateToSeparatedPlugins()(tree, context); const pkg = JSON.parse(tree.readContent('/package.json')); - expect(pkg.devDependencies['nitro']).toBeUndefined(); + expect( + pkg.devDependencies['@analogjs/vite-plugin-angular'], + ).toBeUndefined(); }); it('detects a vite.config.mts file', () => { @@ -183,7 +186,9 @@ describe('migrate-to-separated-plugins', () => { migrateToSeparatedPlugins()(tree, context); const pkg = JSON.parse(tree.readContent('/package.json')); - expect(pkg.devDependencies['nitro']).toBeTruthy(); + expect(pkg.devDependencies['@analogjs/vite-plugin-angular']).toBe( + PLATFORM_VERSION, + ); }); it('does not treat a config that already imports the new plugins as legacy', () => { @@ -195,7 +200,6 @@ describe('migrate-to-separated-plugins', () => { devDependencies: { '@analogjs/platform': PLATFORM_VERSION, '@analogjs/vite-plugin-angular': PLATFORM_VERSION, - nitro: '3.0.0-beta', }, }), ); diff --git a/packages/platform/migrations/migrate-to-separated-plugins/migrate-to-separated-plugins.ts b/packages/platform/migrations/migrate-to-separated-plugins/migrate-to-separated-plugins.ts index 21f2275a9..c36e8699f 100644 --- a/packages/platform/migrations/migrate-to-separated-plugins/migrate-to-separated-plugins.ts +++ b/packages/platform/migrations/migrate-to-separated-plugins/migrate-to-separated-plugins.ts @@ -6,8 +6,6 @@ const ANALOG_PLATFORM_IMPORT = `from '@analogjs/platform'`; const ANGULAR_PLUGIN_IMPORT = `from '@analogjs/vite-plugin-angular'`; const NITRO_VITE_IMPORT = `from 'nitro/vite'`; const ANGULAR_PLUGIN_PKG = '@analogjs/vite-plugin-angular'; -const NITRO_PKG = 'nitro'; -const NITRO_VERSION = '3.0.260522-beta'; const MIGRATION_DOC_URL = 'https://analogjs.org/docs/guides/migrating-v2-to-v3#analog-angular-and-nitro-are-now-separate-plugins'; @@ -78,14 +76,6 @@ function addDependencies(tree: Tree, context: SchematicContext): boolean { ); } - if (!getDepVersion(pkg, NITRO_PKG)) { - devDeps[NITRO_PKG] = NITRO_VERSION; - changed = true; - context.logger.info( - `Added '${NITRO_PKG}': '${NITRO_VERSION}' to devDependencies.`, - ); - } - if (!changed) return false; pkg['devDependencies'] = devDeps; From d923e157197a089d1ca03e18f26e9fdf0548bb11 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Mon, 25 May 2026 15:36:08 -0500 Subject: [PATCH 43/65] fix(platform): honor staticData and outputSourceFile in prerender config `collectRoutes` was reducing every prerender entry down to a route string, dropping the rest of the `PrerenderRouteConfig` / `PrerenderContentDir` shape. The legacy plugin used those fields: - `PrerenderRouteConfig.staticData: true` adds an extra prerender route at `/_analog/pages/` so the page's data endpoint gets a static JSON snapshot. - `PrerenderRouteConfig.outputSourceFile: '.md'` reads the source markdown and writes it at `/.md`. - `PrerenderContentDir.outputSourceFile(file)` returns per-file source content and writes the same way. Extend `collectRoutes` to return a `routeSourceFiles` map and to push the staticData endpoint into the prerender route list; register a `prerender:done` hook in `wirePrerender` that walks routeSourceFiles and writes each entry alongside the prerendered HTML. --- .../src/lib/nitro/analog-nitro-plugin.ts | 63 ++++++++++++++++--- 1 file changed, 56 insertions(+), 7 deletions(-) diff --git a/packages/platform/src/lib/nitro/analog-nitro-plugin.ts b/packages/platform/src/lib/nitro/analog-nitro-plugin.ts index 24ca6b05e..ea3e16b77 100644 --- a/packages/platform/src/lib/nitro/analog-nitro-plugin.ts +++ b/packages/platform/src/lib/nitro/analog-nitro-plugin.ts @@ -545,10 +545,11 @@ async function wirePrerender( const prerender = options.prerender; if (!prerender && !options.i18n) return; - const { routes: collected, sitemaps: routeSitemaps } = await collectRoutes( - prerender?.routes, - context, - ); + const { + routes: collected, + sitemaps: routeSitemaps, + routeSourceFiles, + } = await collectRoutes(prerender?.routes, context, apiPrefix); const expanded = options.i18n ? expandRoutesWithLocales(collected, { @@ -571,6 +572,24 @@ async function wirePrerender( addPostRenderingHooks(nitro, prerender.postRenderingHooks); } + if (Object.keys(routeSourceFiles).length > 0) { + // Mirror the legacy `@analogjs/vite-plugin-nitro` behavior: after + // prerender completes, write the route's source content alongside the + // prerendered HTML at `/.md`. Drives the + // `outputSourceFile` option on both `PrerenderRouteConfig` (string path + // to source markdown) and `PrerenderContentDir` (per-file callback). + nitro.hooks.hook('prerender:done', async () => { + const publicDir = resolve(nitro.options.output.publicDir); + const { mkdirSync, writeFileSync } = await import('node:fs'); + const { dirname, join } = await import('node:path'); + for (const [route, content] of Object.entries(routeSourceFiles)) { + const outputPath = join(publicDir, `${route}.md`); + mkdirSync(dirname(outputPath), { recursive: true }); + writeFileSync(outputPath, content, 'utf8'); + } + }); + } + const sitemapConfig = prerender?.sitemap; if (sitemapConfig) { nitro.hooks.hook('prerender:done', async (result) => { @@ -597,11 +616,17 @@ async function collectRoutes( : never : never, context: NitroPluginContext, -): Promise<{ routes: string[]; sitemaps: Record }> { + apiPrefix: string, +): Promise<{ + routes: string[]; + sitemaps: Record; + routeSourceFiles: Record; +}> { const out: string[] = []; const sitemaps: Record = {}; + const routeSourceFiles: Record = {}; - if (!routesInput) return { routes: out, sitemaps }; + if (!routesInput) return { routes: out, sitemaps, routeSourceFiles }; const inputs = Array.isArray(routesInput) ? routesInput @@ -639,6 +664,12 @@ async function collectRoutes( )(file) : dir.sitemap; } + if (dir.outputSourceFile) { + const sourceContent = dir.outputSourceFile(file); + if (typeof sourceContent === 'string') { + routeSourceFiles[route] = sourceContent; + } + } } continue; } @@ -649,8 +680,26 @@ async function collectRoutes( if (cfg.sitemap) { sitemaps[cfg.route] = cfg.sitemap; } + if (cfg.outputSourceFile) { + const sourcePath = resolve( + context.workspaceRoot, + context.rootDir, + cfg.outputSourceFile, + ); + if (existsSync(sourcePath)) { + routeSourceFiles[cfg.route] = readFileSync(sourcePath, 'utf8'); + } + } + if (cfg.staticData) { + // Mirror the legacy plugin: when staticData is requested, also + // prerender the page's data-fetching endpoint so the JSON payload + // is available statically. + const prefix = apiPrefix.startsWith('/') ? apiPrefix : `/${apiPrefix}`; + const route = cfg.route.startsWith('/') ? cfg.route : `/${cfg.route}`; + out.push(`${prefix}/_analog/pages${route}`); + } } } - return { routes: out, sitemaps }; + return { routes: out, sitemaps, routeSourceFiles }; } From bfcf98840a67e8eaa8c35605def7a26c80993b33 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Mon, 25 May 2026 15:36:39 -0500 Subject: [PATCH 44/65] fix(platform): preserve query string on SSR request shim The SSR entry wrapper built reqShim.url / reqShim.originalUrl from the normalized pathname only, dropping url.search. Angular's router sees a request like `/products?sort=price` arrive as `/products`, and any `injectQuery()` / server data loader that reads from req.url loses the query parameters during SSR. Append url.search to the requestPath when constructing reqShim so the renderer hands main.server.ts a full path+query while the pathname-only requestPath stays the input to normalizeRequestPath and routing key lookups. --- packages/platform/src/lib/nitro/analog-nitro-plugin.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/platform/src/lib/nitro/analog-nitro-plugin.ts b/packages/platform/src/lib/nitro/analog-nitro-plugin.ts index ea3e16b77..a99b7a4bc 100644 --- a/packages/platform/src/lib/nitro/analog-nitro-plugin.ts +++ b/packages/platform/src/lib/nitro/analog-nitro-plugin.ts @@ -503,6 +503,9 @@ export default { async fetch(req) { const url = new URL(req.url); const requestPath = normalizeRequestPath(url.pathname); + // Preserve the query string so Angular's router + injectQuery() and + // server data loaders that read from req.url see the full path+query. + const requestUrl = requestPath + url.search; if (req.headers.get('x-analog-no-ssr') === 'true') { return new Response(TEMPLATE, { @@ -513,8 +516,8 @@ export default { const reqShim = { headers: Object.fromEntries(req.headers.entries()), - url: requestPath, - originalUrl: requestPath, + url: requestUrl, + originalUrl: requestUrl, connection: {}, }; From 0e1c47d6172076f3f94e21883800724a9661c9a6 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Mon, 25 May 2026 15:37:48 -0500 Subject: [PATCH 45/65] fix(platform): honor user apiPrefix in page handler mounts 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. --- .../platform/src/lib/nitro/analog-nitro-plugin.ts | 3 ++- .../platform/src/lib/nitro/get-page-handlers.ts | 15 +++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/packages/platform/src/lib/nitro/analog-nitro-plugin.ts b/packages/platform/src/lib/nitro/analog-nitro-plugin.ts index a99b7a4bc..24594f0da 100644 --- a/packages/platform/src/lib/nitro/analog-nitro-plugin.ts +++ b/packages/platform/src/lib/nitro/analog-nitro-plugin.ts @@ -232,7 +232,7 @@ export function analogNitroPlugin(options: Options = {}): Plugin { resolve( context.workspaceRoot, context.rootDir, - `${context.sourceRoot}/server/routes/api`, + `${context.sourceRoot}/server/routes/${apiPrefix.replace(/^\/+/, '')}`, ), ); @@ -242,6 +242,7 @@ export function analogNitroPlugin(options: Options = {}): Plugin { rootDir: context.rootDir, additionalPagesDirs: options.additionalPagesDirs, hasAPIDir, + apiPrefix, }); nitro.options.handlers.push(...pageHandlers); diff --git a/packages/platform/src/lib/nitro/get-page-handlers.ts b/packages/platform/src/lib/nitro/get-page-handlers.ts index 2b8461af3..b4c99c608 100644 --- a/packages/platform/src/lib/nitro/get-page-handlers.ts +++ b/packages/platform/src/lib/nitro/get-page-handlers.ts @@ -10,6 +10,13 @@ type GetHandlersArgs = { rootDir: string; additionalPagesDirs?: string[]; hasAPIDir?: boolean; + /** + * API prefix without a leading slash (e.g. `'api'`, `'rpc'`). Mounted in + * front of the discovered `/_analog/pages/...` routes when `hasAPIDir` + * is set, mirroring user-defined API routes that already live under the + * configured prefix. + */ + apiPrefix?: string; }; /** @@ -17,7 +24,8 @@ type GetHandlersArgs = { * * Discovers all `.server.ts` files under `app/pages/**` and any additional * pages directories, then maps each file to a Nitro route pattern under - * `/_analog/pages/...` (prefixed with `/api` when the project has an API dir). + * `/_analog/pages/...` (prefixed with the configured `apiPrefix` when the + * project has an API dir). * * Route transformation examples: * - index.server.ts → /_analog/pages/index @@ -31,6 +39,7 @@ export function getPageHandlers({ rootDir, additionalPagesDirs, hasAPIDir, + apiPrefix = 'api', }: GetHandlersArgs): NitroEventHandler[] { const root = normalizePath(resolve(workspaceRoot, rootDir)); @@ -55,9 +64,11 @@ export function getPageHandlers({ .replace(/\[(\w+)\]/g, ':$1') .replace(/\./g, '/'); + const prefix = hasAPIDir ? `/${apiPrefix.replace(/^\/+/, '')}` : ''; + return { handler: endpointFile, - route: `${hasAPIDir ? '/api' : ''}/_analog${route}`, + route: `${prefix}/_analog${route}`, lazy: true, }; }); From d386a4e8b45652d892ecebc3911f898639bbdf02 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Mon, 25 May 2026 15:38:21 -0500 Subject: [PATCH 46/65] fix(platform): strip route group parens from non-trailing segments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- packages/platform/src/lib/nitro/get-page-handlers.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/platform/src/lib/nitro/get-page-handlers.ts b/packages/platform/src/lib/nitro/get-page-handlers.ts index b4c99c608..a146565aa 100644 --- a/packages/platform/src/lib/nitro/get-page-handlers.ts +++ b/packages/platform/src/lib/nitro/get-page-handlers.ts @@ -60,7 +60,11 @@ export function getPageHandlers({ .replace(/\.server\.ts$/, '') .replace(/\[\.{3}(.+)\]/g, '**:$1') .replace(/\[\.{3}(\w+)\]/g, '**:$1') - .replace(/\/\((.*?)\)$/, '/-$1-') + // Strip Angular Router group syntax `(group)` from any segment, not + // just trailing ones. Routes like `(auth)/login.server.ts` need to + // become `/-auth-/login`, otherwise the literal parens leak through + // and the handler is mounted under an invalid Nitro path. + .replace(/\/\(([^/]+)\)/g, '/-$1-') .replace(/\[(\w+)\]/g, ':$1') .replace(/\./g, '/'); From 4d27f19ed27483ca411050c4b2160b1a60f04f3e Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Mon, 25 May 2026 15:39:15 -0500 Subject: [PATCH 47/65] fix(platform): match apiPrefix as a full path segment in API middleware 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. --- packages/platform/src/lib/nitro/renderers.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/platform/src/lib/nitro/renderers.ts b/packages/platform/src/lib/nitro/renderers.ts index 19fd29c10..b4b003b4e 100644 --- a/packages/platform/src/lib/nitro/renderers.ts +++ b/packages/platform/src/lib/nitro/renderers.ts @@ -122,7 +122,12 @@ export default defineHandler(async (event) => { const prefix = useRuntimeConfig().prefix; const apiPrefix = \`\${prefix}/\${useRuntimeConfig().apiPrefix}\`; - if (event.path?.startsWith(apiPrefix)) { + // Match the configured prefix as a full path segment. A bare + // startsWith would false-match /apiary against an apiPrefix of /api. + if ( + event.path === apiPrefix || + event.path?.startsWith(apiPrefix + '/') + ) { const reqUrl = event.path?.replace(apiPrefix, ''); if ( From 57337a677cbe02020ccd3be0e6abc84f6b4d9912 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Mon, 25 May 2026 15:39:41 -0500 Subject: [PATCH 48/65] fix(platform): await async post-rendering hooks 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` so sync callers stay supported. --- packages/platform/src/lib/nitro/post-rendering-hook.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/platform/src/lib/nitro/post-rendering-hook.ts b/packages/platform/src/lib/nitro/post-rendering-hook.ts index 9e6dc379c..4423c6f83 100644 --- a/packages/platform/src/lib/nitro/post-rendering-hook.ts +++ b/packages/platform/src/lib/nitro/post-rendering-hook.ts @@ -2,11 +2,11 @@ import type { Nitro, PrerenderRoute } from 'nitro/types'; export function addPostRenderingHooks( nitro: Nitro, - hooks: ((pr: PrerenderRoute) => Promise)[], + hooks: ((pr: PrerenderRoute) => Promise | void)[], ): void { - hooks.forEach((hook: (preRoute: PrerenderRoute) => void) => { - nitro.hooks.hook('prerender:generate', (route: PrerenderRoute) => { - hook(route); + for (const hook of hooks) { + nitro.hooks.hook('prerender:generate', async (route: PrerenderRoute) => { + await hook(route); }); - }); + } } From d1f470ce2bce9e9a529598d8bdf32640baa27863 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Mon, 25 May 2026 15:40:14 -0500 Subject: [PATCH 49/65] fix(platform): keep inner dots in content file names 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'. --- .../src/lib/nitro/get-content-files.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/platform/src/lib/nitro/get-content-files.ts b/packages/platform/src/lib/nitro/get-content-files.ts index 2e8101c7a..eb6925f74 100644 --- a/packages/platform/src/lib/nitro/get-content-files.ts +++ b/packages/platform/src/lib/nitro/get-content-files.ts @@ -1,5 +1,5 @@ import { readFileSync } from 'node:fs'; -import { join, relative, resolve } from 'node:path'; +import { basename, join, relative, resolve } from 'node:path'; import { normalizePath } from 'vite'; import { createRequire } from 'node:module'; import { globSync } from 'tinyglobby'; @@ -46,14 +46,15 @@ export function getMatchingContentFilesWithFrontMatter( const fileContents = readFileSync(f, 'utf8'); const raw = fm(fileContents); - const filepath = normalizePath(f).replace(root, ''); - const match = filepath.match(/\/([^/.]+)(\.([^/.]+))?$/); - let name = ''; - let extension = ''; - if (match) { - name = match[1]; - extension = match[3] || ''; - } + // Split the basename on the LAST dot so file names that contain + // additional dots (e.g. locale suffixes like `post.en.md`) keep the + // inner dots in `name` and only the trailing segment becomes the + // extension. The previous regex stopped at the first dot and + // discarded everything between it and the extension. + const filename = basename(normalizePath(f)); + const dot = filename.lastIndexOf('.'); + const name = dot === -1 ? filename : filename.slice(0, dot); + const extension = dot === -1 ? '' : filename.slice(dot + 1); const relativeDir = normalizePath(relative(dirPrefix, f)); const lastSlash = relativeDir.lastIndexOf('/'); From 976d10661e6f2838d13081817463566a391da142 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Mon, 25 May 2026 15:40:45 -0500 Subject: [PATCH 50/65] fix(platform): skip locale expansion for the bare /api route 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. --- packages/platform/src/lib/nitro/i18n-prerender.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/platform/src/lib/nitro/i18n-prerender.ts b/packages/platform/src/lib/nitro/i18n-prerender.ts index 1d2802d46..388d4b2a0 100644 --- a/packages/platform/src/lib/nitro/i18n-prerender.ts +++ b/packages/platform/src/lib/nitro/i18n-prerender.ts @@ -17,7 +17,15 @@ export function expandRoutesWithLocales( const expanded: string[] = []; for (const route of routes) { - if (route.includes('/_analog/') || route.startsWith('/api/')) { + // Skip locale expansion for internal analog endpoints and API routes. + // `startsWith('/api/')` alone misses the bare `/api` path, which + // would otherwise get locale-prefixed and produce phantom routes like + // `/en/api`. + if ( + route.includes('/_analog/') || + route === '/api' || + route.startsWith('/api/') + ) { expanded.push(route); continue; } From c3e8ac0f747371a75f5b4367273c85b6b62ee458 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Mon, 25 May 2026 15:41:09 -0500 Subject: [PATCH 51/65] refactor(platform): make XMLBuilder a type-only import 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. --- packages/platform/src/lib/nitro/build-sitemap.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/platform/src/lib/nitro/build-sitemap.ts b/packages/platform/src/lib/nitro/build-sitemap.ts index 9c8282d8b..cb329b93d 100644 --- a/packages/platform/src/lib/nitro/build-sitemap.ts +++ b/packages/platform/src/lib/nitro/build-sitemap.ts @@ -1,7 +1,7 @@ import { existsSync, mkdirSync, writeFileSync } from 'node:fs'; import { resolve } from 'node:path'; import { create } from 'xmlbuilder2'; -import { XMLBuilder } from 'xmlbuilder2/lib/interfaces'; +import type { XMLBuilder } from 'xmlbuilder2/lib/interfaces'; import { UserConfig } from 'vite'; import type { I18nPrerenderOptions, From 6db73b983e71894a975c8598948ee643ef487e80 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Mon, 25 May 2026 15:41:47 -0500 Subject: [PATCH 52/65] fix(platform): tighten vite-dev-server port schema to integer 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. --- packages/nx-plugin/src/builders/vite-dev-server/schema.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/nx-plugin/src/builders/vite-dev-server/schema.json b/packages/nx-plugin/src/builders/vite-dev-server/schema.json index fa6f246d6..82e1ff61d 100644 --- a/packages/nx-plugin/src/builders/vite-dev-server/schema.json +++ b/packages/nx-plugin/src/builders/vite-dev-server/schema.json @@ -11,7 +11,9 @@ "x-priority": "important" }, "port": { - "type": "number", + "type": "integer", + "minimum": 1, + "maximum": 65535, "description": "Port to listen on.", "x-priority": "important" }, From 2e7257053a10c262058e79706e2ee445c5a82575 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Mon, 25 May 2026 15:54:11 -0500 Subject: [PATCH 53/65] fix(platform): wire Vercel preset detection and runtime defaults MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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//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//analog/ paths. --- .../src/lib/nitro/analog-nitro-plugin.ts | 59 +++++++++++++++---- 1 file changed, 48 insertions(+), 11 deletions(-) diff --git a/packages/platform/src/lib/nitro/analog-nitro-plugin.ts b/packages/platform/src/lib/nitro/analog-nitro-plugin.ts index 24594f0da..2b89381f5 100644 --- a/packages/platform/src/lib/nitro/analog-nitro-plugin.ts +++ b/packages/platform/src/lib/nitro/analog-nitro-plugin.ts @@ -120,6 +120,19 @@ export function analogNitroPlugin(options: Options = {}): Plugin { config(userConfig) { refreshContext(userConfig.root); + // Bridge the legacy `BUILD_PRESET` env var that `@analogjs/vite-plugin-nitro` + // accepted into Nitro v3's `NITRO_PRESET`, and auto-pick the `vercel` + // preset when the build runs inside Vercel CI (`process.env.VERCEL` + // is set on every Vercel build). Mirrors the legacy plugin's behavior + // so users upgrading don't need to change their CI configuration. + if (!process.env['NITRO_PRESET']) { + if (process.env['BUILD_PRESET']) { + process.env['NITRO_PRESET'] = process.env['BUILD_PRESET']; + } else if (process.env['VERCEL']) { + process.env['NITRO_PRESET'] = 'vercel'; + } + } + const overrides: UserConfig = { // Vite 8 defaults `server.fs.allow` to `[searchForWorkspaceRoot(root)]`, // which should already cover the workspace root. In practice, nitro/vite's @@ -215,17 +228,41 @@ export function analogNitroPlugin(options: Options = {}): Plugin { // packages installed at `/node_modules/` (the usual install // shape for both standalone and Nx setups) remain reachable. if (!nitro.options.dev) { - const distRoot = resolve( - context.workspaceRoot, - 'dist', - context.rootDir, - ); - nitro.options.output = { - ...nitro.options.output, - dir: resolve(distRoot, 'analog'), - publicDir: resolve(distRoot, 'analog/public'), - serverDir: resolve(distRoot, 'analog/server'), - }; + // Vercel's preset owns its own output layout (the Build Output + // API expects `/.vercel/output/{functions,static}/...` + // populated together); overriding to the legacy + // `dist//analog/` paths leaves functions and static + // files in different trees. For Vercel, defer to Nitro's preset. + // For everything else, restore the legacy paths so docs and + // `dist/analog/server` start commands keep working. + const isVercel = + (nitro.options.preset ?? '').toLowerCase().includes('vercel') || + !!process.env['VERCEL']; + + if (isVercel) { + const vercel = (nitro.options as { vercel?: Record }) + .vercel; + (nitro.options as { vercel?: Record }).vercel = { + ...vercel, + entryFormat: vercel?.entryFormat ?? 'node', + functions: { + runtime: vercel?.functions?.runtime ?? 'nodejs24.x', + ...vercel?.functions, + }, + }; + } else { + const distRoot = resolve( + context.workspaceRoot, + 'dist', + context.rootDir, + ); + nitro.options.output = { + ...nitro.options.output, + dir: resolve(distRoot, 'analog'), + publicDir: resolve(distRoot, 'analog/public'), + serverDir: resolve(distRoot, 'analog/server'), + }; + } } const hasAPIDir = existsSync( From 7aadebba44ab39965a0281204789efdf97a03963 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Mon, 25 May 2026 15:59:21 -0500 Subject: [PATCH 54/65] fix: refresh platform required-artifacts list for builder shim layout Commit 1a59b6a5e 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. --- tools/scripts/verify-package-artifacts.mts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/scripts/verify-package-artifacts.mts b/tools/scripts/verify-package-artifacts.mts index 8fc9ee5ab..a003c2e01 100644 --- a/tools/scripts/verify-package-artifacts.mts +++ b/tools/scripts/verify-package-artifacts.mts @@ -119,7 +119,8 @@ const packageConfigs: Record = { manifestFields: ['builders', 'executors', 'generators', 'schematics'], requiredPaths: [ 'packages/platform/dist/src/lib/nx-plugin', - 'packages/platform/dist/src/lib/nx-plugin/src/executors/vite/vite.impl.js', + 'packages/platform/dist/src/lib/nx-plugin/src/builders/vite/vite-build.js', + 'packages/platform/dist/src/lib/nx-plugin/src/builders/vite-dev-server/dev-server.js', 'packages/platform/dist/src/lib/nx-plugin/src/generators/preset/generator.js', ], }, From d8c24f02f84bfe4b669dbd14d062e60638e06a01 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Mon, 25 May 2026 16:06:28 -0500 Subject: [PATCH 55/65] fix: make the prepare script Windows-compatible 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. --- package.json | 2 +- tools/scripts/build-release.mts | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 8e17bf68b..dbcf65f7e 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "lint": "pnpm run /^lint:/", "ng": "nx", "preinstall": "npx only-allow pnpm", - "prepare": "git config core.hookspath .githooks || true && node tools/scripts/build-release.mts", + "prepare": "node tools/scripts/build-release.mts", "prettier:check": "prettier --check .", "release:pack": "node tools/scripts/release-artifacts.mts pack", "release:smoke": "node tools/scripts/smoke-release-consumers.mts", diff --git a/tools/scripts/build-release.mts b/tools/scripts/build-release.mts index ae4d333b4..b24f34baf 100644 --- a/tools/scripts/build-release.mts +++ b/tools/scripts/build-release.mts @@ -213,9 +213,26 @@ function runStep(step: Step): void { cwd: root, stdio: 'inherit', env: process.env, + shell: process.platform === 'win32', }); } +// Wire git hooks. Previously the prepare script ran +// `git config core.hookspath .githooks || true && node tools/scripts/build-release.mts`, +// but cmd.exe doesn't have a `true` builtin, so the entire chain short- +// circuits on Windows before the build steps run. Move the git-config +// step into the script so a missing/failed git invocation can't block +// the rest of the prepare work. +try { + execFileSync('git', ['config', 'core.hookspath', '.githooks'], { + cwd: root, + stdio: 'ignore', + shell: process.platform === 'win32', + }); +} catch { + // Not a git checkout, or git isn't available — skip silently. +} + for (const step of steps) { runStep(step); } From ba336804b10951bc4cf9faa90969e1f5d7f9ef33 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Mon, 25 May 2026 16:11:58 -0500 Subject: [PATCH 56/65] fix(platform): use const for the immutable analogCalls array in schematic 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. --- .../migrate-to-separated-plugins.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/platform/migrations/migrate-to-separated-plugins/migrate-to-separated-plugins.ts b/packages/platform/migrations/migrate-to-separated-plugins/migrate-to-separated-plugins.ts index c36e8699f..2bc3a996c 100644 --- a/packages/platform/migrations/migrate-to-separated-plugins/migrate-to-separated-plugins.ts +++ b/packages/platform/migrations/migrate-to-separated-plugins/migrate-to-separated-plugins.ts @@ -155,7 +155,7 @@ function tryTransformViteConfig( let analogImportEnd = -1; let angularImported = false; let nitroImported = false; - let analogCalls: ts.CallExpression[] = []; + const analogCalls: ts.CallExpression[] = []; function visit(node: ts.Node): void { if (ts.isImportDeclaration(node)) { From c6da6a73b35bc7a83f9cb56cc6336c28de3e3e79 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Mon, 25 May 2026 16:23:53 -0500 Subject: [PATCH 57/65] fix(storybook-angular): guard against plugins without a name in viteFinal 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. --- packages/storybook-angular/src/lib/preset.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/storybook-angular/src/lib/preset.ts b/packages/storybook-angular/src/lib/preset.ts index 3946cefbc..cbd24b5a5 100644 --- a/packages/storybook-angular/src/lib/preset.ts +++ b/packages/storybook-angular/src/lib/preset.ts @@ -72,10 +72,13 @@ async function resolveExperimentalZoneless( } export const viteFinal = async (config: any, options: any): Promise => { - // Remove any loaded analogjs plugins from a vite.config.(m)ts file + // Remove any loaded analogjs plugins from a vite.config.(m)ts file. + // Anonymous plugin entries (no `name` property) and falsy entries from + // conditional plugin arrays must pass through untouched — only filter + // plugins whose name explicitly contains "analogjs". config.plugins = (config.plugins ?? []) .flat() - .filter((plugin: any) => !plugin.name.includes('analogjs')); + .filter((plugin: any) => !plugin?.name?.includes?.('analogjs')); // @ts-expect-error - untyped storybook presets API const framework = await options.presets.apply('framework'); From c4d9d5109b84777f78c902b17578cc1d7cfb5f08 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Mon, 25 May 2026 16:42:51 -0500 Subject: [PATCH 58/65] Revert "fix(platform): honor user apiPrefix in page handler mounts" This reverts commit 0e1c47d6172076f3f94e21883800724a9661c9a6. --- .../platform/src/lib/nitro/analog-nitro-plugin.ts | 3 +-- .../platform/src/lib/nitro/get-page-handlers.ts | 15 ++------------- 2 files changed, 3 insertions(+), 15 deletions(-) diff --git a/packages/platform/src/lib/nitro/analog-nitro-plugin.ts b/packages/platform/src/lib/nitro/analog-nitro-plugin.ts index 2b89381f5..880f2dd44 100644 --- a/packages/platform/src/lib/nitro/analog-nitro-plugin.ts +++ b/packages/platform/src/lib/nitro/analog-nitro-plugin.ts @@ -269,7 +269,7 @@ export function analogNitroPlugin(options: Options = {}): Plugin { resolve( context.workspaceRoot, context.rootDir, - `${context.sourceRoot}/server/routes/${apiPrefix.replace(/^\/+/, '')}`, + `${context.sourceRoot}/server/routes/api`, ), ); @@ -279,7 +279,6 @@ export function analogNitroPlugin(options: Options = {}): Plugin { rootDir: context.rootDir, additionalPagesDirs: options.additionalPagesDirs, hasAPIDir, - apiPrefix, }); nitro.options.handlers.push(...pageHandlers); diff --git a/packages/platform/src/lib/nitro/get-page-handlers.ts b/packages/platform/src/lib/nitro/get-page-handlers.ts index a146565aa..e69f65dfe 100644 --- a/packages/platform/src/lib/nitro/get-page-handlers.ts +++ b/packages/platform/src/lib/nitro/get-page-handlers.ts @@ -10,13 +10,6 @@ type GetHandlersArgs = { rootDir: string; additionalPagesDirs?: string[]; hasAPIDir?: boolean; - /** - * API prefix without a leading slash (e.g. `'api'`, `'rpc'`). Mounted in - * front of the discovered `/_analog/pages/...` routes when `hasAPIDir` - * is set, mirroring user-defined API routes that already live under the - * configured prefix. - */ - apiPrefix?: string; }; /** @@ -24,8 +17,7 @@ type GetHandlersArgs = { * * Discovers all `.server.ts` files under `app/pages/**` and any additional * pages directories, then maps each file to a Nitro route pattern under - * `/_analog/pages/...` (prefixed with the configured `apiPrefix` when the - * project has an API dir). + * `/_analog/pages/...` (prefixed with `/api` when the project has an API dir). * * Route transformation examples: * - index.server.ts → /_analog/pages/index @@ -39,7 +31,6 @@ export function getPageHandlers({ rootDir, additionalPagesDirs, hasAPIDir, - apiPrefix = 'api', }: GetHandlersArgs): NitroEventHandler[] { const root = normalizePath(resolve(workspaceRoot, rootDir)); @@ -68,11 +59,9 @@ export function getPageHandlers({ .replace(/\[(\w+)\]/g, ':$1') .replace(/\./g, '/'); - const prefix = hasAPIDir ? `/${apiPrefix.replace(/^\/+/, '')}` : ''; - return { handler: endpointFile, - route: `${prefix}/_analog${route}`, + route: `${hasAPIDir ? '/api' : ''}/_analog${route}`, lazy: true, }; }); From 5f3bd32792864340a781c4c2b607a0de1481828b Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Mon, 25 May 2026 17:05:14 -0500 Subject: [PATCH 59/65] chore: move blog-app server routes under routes/api/ 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. --- apps/blog-app/src/server/routes/{ => api}/rss.xml.ts | 0 apps/blog-app/src/server/routes/{ => api}/v1/[...slug].ts | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename apps/blog-app/src/server/routes/{ => api}/rss.xml.ts (100%) rename apps/blog-app/src/server/routes/{ => api}/v1/[...slug].ts (100%) diff --git a/apps/blog-app/src/server/routes/rss.xml.ts b/apps/blog-app/src/server/routes/api/rss.xml.ts similarity index 100% rename from apps/blog-app/src/server/routes/rss.xml.ts rename to apps/blog-app/src/server/routes/api/rss.xml.ts diff --git a/apps/blog-app/src/server/routes/v1/[...slug].ts b/apps/blog-app/src/server/routes/api/v1/[...slug].ts similarity index 100% rename from apps/blog-app/src/server/routes/v1/[...slug].ts rename to apps/blog-app/src/server/routes/api/v1/[...slug].ts From 30566cc3d903392ba4f83b22b27ae15a23a3c040 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Mon, 25 May 2026 17:05:30 -0500 Subject: [PATCH 60/65] fix(platform): wire Nitro serverFetch into SSR for in-process data loads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../src/lib/nitro/analog-nitro-plugin.ts | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/packages/platform/src/lib/nitro/analog-nitro-plugin.ts b/packages/platform/src/lib/nitro/analog-nitro-plugin.ts index 880f2dd44..ecdd03cab 100644 --- a/packages/platform/src/lib/nitro/analog-nitro-plugin.ts +++ b/packages/platform/src/lib/nitro/analog-nitro-plugin.ts @@ -529,6 +529,7 @@ function generateSsrEntryWrapper( template: string, ): string { return ` +import { serverFetch as nitroServerFetch } from 'nitro/app'; import renderer from ${JSON.stringify(entryServer)}; const TEMPLATE = ${JSON.stringify(template)}; @@ -536,6 +537,27 @@ const TEMPLATE = ${JSON.stringify(template)}; const normalizeRequestPath = (url) => url.replace(/\\/index\\.html(?=$|[?#])/, '/'); +// In-process fetch wired into Nitro's request pipeline. Angular's HttpClient +// (via withFetch()) and Analog's injectLoad() call this during SSR/prerender +// so they hit the running app's page-endpoint and API routes without going +// through the network — the prerender pipeline doesn't have a listening +// socket. Without this, every SSR data fetch ECONNREFUSEs and Angular's +// router fails to resolve any data-bound route, producing an empty +// in the prerendered HTML. +const ssrFetch = (resource, init) => { + let url = typeof resource === 'string' + ? resource + : resource instanceof URL + ? resource.href + : resource.url; + // Relative URLs from injectAPIPrefix() etc. need a host for Nitro's + // Request constructor to accept them. + if (typeof url === 'string' && url.startsWith('/')) { + url = 'http://localhost' + url; + } + return nitroServerFetch(url, init); +}; + export default { async fetch(req) { const url = new URL(req.url); @@ -559,7 +581,10 @@ export default { }; try { - const html = await renderer(requestPath, TEMPLATE, { req: reqShim }); + const html = await renderer(requestPath, TEMPLATE, { + req: reqShim, + fetch: ssrFetch, + }); return new Response(html, { status: 200, headers: { 'content-type': 'text/html; charset=utf-8' }, From 8b333bec1b5a925284f47f11f2d9240971949363 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Mon, 25 May 2026 18:47:29 -0500 Subject: [PATCH 61/65] fix(platform): hoist Netlify functions to workspace root and wrap SSR fetch in ofetch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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//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//analog/ paths for the default node-server preset. Managed presets (Vercel, Netlify, Cloudflare, ...) use their own layouts. - For Netlify, hoist functions to /.netlify/functions-internal/ so the deploy auto-discovers them; keep publicDir at dist//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) --- .../src/lib/nitro/analog-nitro-plugin.ts | 89 +++++++++++++++---- 1 file changed, 71 insertions(+), 18 deletions(-) diff --git a/packages/platform/src/lib/nitro/analog-nitro-plugin.ts b/packages/platform/src/lib/nitro/analog-nitro-plugin.ts index ecdd03cab..c3ae84014 100644 --- a/packages/platform/src/lib/nitro/analog-nitro-plugin.ts +++ b/packages/platform/src/lib/nitro/analog-nitro-plugin.ts @@ -121,15 +121,19 @@ export function analogNitroPlugin(options: Options = {}): Plugin { refreshContext(userConfig.root); // Bridge the legacy `BUILD_PRESET` env var that `@analogjs/vite-plugin-nitro` - // accepted into Nitro v3's `NITRO_PRESET`, and auto-pick the `vercel` - // preset when the build runs inside Vercel CI (`process.env.VERCEL` - // is set on every Vercel build). Mirrors the legacy plugin's behavior - // so users upgrading don't need to change their CI configuration. + // accepted into Nitro v3's `NITRO_PRESET`, and auto-pick the matching + // preset when the build runs inside a host's CI (each host sets its + // own well-known env var). Mirrors the legacy plugin's behavior so + // users upgrading don't need to change their CI configuration. if (!process.env['NITRO_PRESET']) { if (process.env['BUILD_PRESET']) { process.env['NITRO_PRESET'] = process.env['BUILD_PRESET']; } else if (process.env['VERCEL']) { process.env['NITRO_PRESET'] = 'vercel'; + } else if (process.env['NETLIFY']) { + process.env['NITRO_PRESET'] = 'netlify'; + } else if (process.env['CF_PAGES']) { + process.env['NITRO_PRESET'] = 'cloudflare-pages'; } } @@ -228,18 +232,24 @@ export function analogNitroPlugin(options: Options = {}): Plugin { // packages installed at `/node_modules/` (the usual install // shape for both standalone and Nx setups) remain reachable. if (!nitro.options.dev) { - // Vercel's preset owns its own output layout (the Build Output - // API expects `/.vercel/output/{functions,static}/...` - // populated together); overriding to the legacy - // `dist//analog/` paths leaves functions and static - // files in different trees. For Vercel, defer to Nitro's preset. - // For everything else, restore the legacy paths so docs and - // `dist/analog/server` start commands keep working. - const isVercel = - (nitro.options.preset ?? '').toLowerCase().includes('vercel') || - !!process.env['VERCEL']; - - if (isVercel) { + // Deployment presets (Vercel, Netlify, Cloudflare, Firebase, ...) + // own their own output layout — Vercel writes the Build Output API + // tree under `.vercel/output/`, Netlify expects functions under + // `.netlify/functions-internal/` with static assets under `dist/`, + // and so on. Clobbering `output.{dir,publicDir,serverDir}` for + // those presets leaves functions and static files in different + // trees and breaks the deploy. Only override when running the + // default node-server preset (or no preset at all) so docs and + // the legacy `dist/analog/server` start command keep working for + // standalone Node deployments. + const preset = (nitro.options.preset ?? '').toLowerCase(); + const isManagedPreset = + preset !== '' && + preset !== 'node-server' && + preset !== 'node' && + preset !== 'nitro-dev'; + + if (preset.includes('vercel')) { const vercel = (nitro.options as { vercel?: Record }) .vercel; (nitro.options as { vercel?: Record }).vercel = { @@ -250,7 +260,36 @@ export function analogNitroPlugin(options: Options = {}): Plugin { ...vercel?.functions, }, }; - } else { + } + + // Netlify auto-discovers functions under + // `/.netlify/functions-internal/`. Nitro's netlify + // preset anchors that to `{{rootDir}}`, which in Nx monorepos + // becomes `apps//.netlify/...` and is invisible to the + // Netlify deploy. Hoist the functions to the workspace root so + // `nx build ` produces a deploy-ready tree at the repo root. + // Keep `publicDir` under `dist//analog/public/` (the + // legacy Analog Netlify publish layout) — the user wires their + // `netlify.toml` publish path to it. + if (preset === 'netlify' || preset === 'netlify-edge') { + const netlifyDir = resolve( + context.workspaceRoot, + '.netlify/functions-internal', + ); + nitro.options.output = { + ...nitro.options.output, + dir: netlifyDir, + serverDir: resolve(netlifyDir, 'server'), + publicDir: resolve( + context.workspaceRoot, + 'dist', + context.rootDir, + 'analog/public', + ), + }; + } + + if (!isManagedPreset) { const distRoot = resolve( context.workspaceRoot, 'dist', @@ -530,6 +569,7 @@ function generateSsrEntryWrapper( ): string { return ` import { serverFetch as nitroServerFetch } from 'nitro/app'; +import { createFetch } from 'ofetch'; import renderer from ${JSON.stringify(entryServer)}; const TEMPLATE = ${JSON.stringify(template)}; @@ -558,6 +598,15 @@ const ssrFetch = (resource, init) => { return nitroServerFetch(url, init); }; +// Wrap in ofetch so consumers that expect \`$fetch.raw()\` (the router's +// request-context interceptor short-circuits SSR HttpClient calls through +// \`globalThis.$fetch.raw\`) can call it. Set on globalThis so router code +// running inside the Angular renderer can find it. +const ssrOFetch = createFetch({ fetch: ssrFetch }); +if (typeof globalThis.$fetch === 'undefined') { + globalThis.$fetch = ssrOFetch; +} + export default { async fetch(req) { const url = new URL(req.url); @@ -583,7 +632,11 @@ export default { try { const html = await renderer(requestPath, TEMPLATE, { req: reqShim, - fetch: ssrFetch, + // Pass the ofetch-wrapped fetch — INTERNAL_FETCH is consumed by the + // router's request-context interceptor via \`serverFetch.raw(...)\`, + // which is ofetch's response-shape API. Plain fetch lacks \`.raw\` + // and throws TypeError during prerender/SSR. + fetch: ssrOFetch, }); return new Response(html, { status: 200, From 3632f637ca0c2b1605164dbe2735f74ad97f2cbb Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Mon, 25 May 2026 19:08:26 -0500 Subject: [PATCH 62/65] fix(platform): import serverFetch from "nitro" root, not "nitro/app" 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) --- packages/platform/src/lib/nitro/analog-nitro-plugin.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/platform/src/lib/nitro/analog-nitro-plugin.ts b/packages/platform/src/lib/nitro/analog-nitro-plugin.ts index c3ae84014..3da9fc842 100644 --- a/packages/platform/src/lib/nitro/analog-nitro-plugin.ts +++ b/packages/platform/src/lib/nitro/analog-nitro-plugin.ts @@ -568,7 +568,14 @@ function generateSsrEntryWrapper( template: string, ): string { return ` -import { serverFetch as nitroServerFetch } from 'nitro/app'; +// Import \`serverFetch\` from the root \`nitro\` entry rather than \`nitro/app\`. +// The /app subpath creates a fresh \`useNitroApp()\` instance scoped to the +// importing bundle, which in our setup is the standalone SSR vite bundle — +// it has no route handlers, so every fetch 404s. The root entry instead +// reads \`globalThis.__nitro__.{default,prerender}\`, which the surrounding +// Nitro server (prerender pass or production runtime) has already +// populated with the real app + handlers. +import { serverFetch as nitroServerFetch } from 'nitro'; import { createFetch } from 'ofetch'; import renderer from ${JSON.stringify(entryServer)}; From a95732001e0dcf84fae2fc77818c5c5b517e6c36 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Mon, 25 May 2026 19:17:56 -0500 Subject: [PATCH 63/65] fix(platform): hoist Vercel and Cloudflare preset outputs to workspace root Nitro's Vercel and Cloudflare presets anchor their output paths at `{{rootDir}}`, which for Nx monorepos lands deployment artifacts under `apps//` 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: `/.vercel/output/{functions/__server.func,static}/` so `vercel build` at the repo root finds the Build Output API tree. - Cloudflare Pages/Workers: `/dist//` with `_worker.js/` alongside static assets, so `wrangler pages deploy dist/` from the workspace root works. Verified each preset emits to the expected workspace-root layout with `BUILD_PRESET= nx build analog-app`. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/lib/nitro/analog-nitro-plugin.ts | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/packages/platform/src/lib/nitro/analog-nitro-plugin.ts b/packages/platform/src/lib/nitro/analog-nitro-plugin.ts index 3da9fc842..a8e0fbf47 100644 --- a/packages/platform/src/lib/nitro/analog-nitro-plugin.ts +++ b/packages/platform/src/lib/nitro/analog-nitro-plugin.ts @@ -249,6 +249,11 @@ export function analogNitroPlugin(options: Options = {}): Plugin { preset !== 'node' && preset !== 'nitro-dev'; + // Vercel's Build Output API expects `.vercel/output/` at the cwd + // `vercel build` runs from (the repo root). Nitro's preset anchors + // to `{{rootDir}}`, so in Nx monorepos artifacts land at + // `apps//.vercel/output/` and the deploy can't find them. + // Hoist to workspace root and apply Analog's runtime defaults. if (preset.includes('vercel')) { const vercel = (nitro.options as { vercel?: Record }) .vercel; @@ -260,6 +265,13 @@ export function analogNitroPlugin(options: Options = {}): Plugin { ...vercel?.functions, }, }; + const vercelDir = resolve(context.workspaceRoot, '.vercel/output'); + nitro.options.output = { + ...nitro.options.output, + dir: vercelDir, + serverDir: resolve(vercelDir, 'functions/__server.func'), + publicDir: resolve(vercelDir, 'static'), + }; } // Netlify auto-discovers functions under @@ -289,6 +301,26 @@ export function analogNitroPlugin(options: Options = {}): Plugin { }; } + // Cloudflare Pages/Workers presets anchor their output at + // `{{rootDir}}/dist` or `{{rootDir}}/.output`, which puts the + // deploy tree at `apps//...` for Nx monorepos. Hoist to + // `/dist//` so `wrangler pages deploy + // dist/` from the workspace root finds `_worker.js/` + // alongside the static assets. + if (preset.includes('cloudflare')) { + const cfDir = resolve( + context.workspaceRoot, + 'dist', + context.rootDir, + ); + nitro.options.output = { + ...nitro.options.output, + dir: cfDir, + publicDir: cfDir, + serverDir: resolve(cfDir, '_worker.js'), + }; + } + if (!isManagedPreset) { const distRoot = resolve( context.workspaceRoot, From 8217182e35439092cb50c7f3e787dbea542c7c74 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Mon, 25 May 2026 19:59:07 -0500 Subject: [PATCH 64/65] fix(platform): rescue Nitro v3 prerender writes on Windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../src/lib/nitro/analog-nitro-plugin.ts | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/packages/platform/src/lib/nitro/analog-nitro-plugin.ts b/packages/platform/src/lib/nitro/analog-nitro-plugin.ts index a8e0fbf47..7af46ab0a 100644 --- a/packages/platform/src/lib/nitro/analog-nitro-plugin.ts +++ b/packages/platform/src/lib/nitro/analog-nitro-plugin.ts @@ -1,5 +1,6 @@ import { existsSync, readFileSync, readdirSync } from 'node:fs'; -import { relative, resolve } from 'node:path'; +import { mkdir, writeFile } from 'node:fs/promises'; +import { dirname, relative, resolve } from 'node:path'; import type { Nitro, NitroEventHandler, PrerenderRoute } from 'nitro/types'; import type { Plugin, UserConfig } from 'vite'; @@ -334,6 +335,30 @@ export function analogNitroPlugin(options: Options = {}): Plugin { serverDir: resolve(distRoot, 'analog/server'), }; } + + // Nitro v3's prerender writer compares `filePath.startsWith(publicDir)` + // with `filePath` built via `node:path.join` (platform-native + // separators on Windows) and `publicDir` set via `resolveNitroPath` + // (always forward slashes via pathe). On Windows the two sides + // disagree and every route is marked `(skipped)` — no HTML is + // written. Hook `prerender:route` and write the file ourselves + // when Nitro has skipped it but the route generated content. + nitro.hooks.hook('prerender:route', async (route) => { + if (!route.skip || route.error) return; + const buffer = route.data; + if (!buffer || !route.fileName) return; + const filePath = resolve( + nitro.options.output.publicDir, + route.fileName.replace(/^[\\/]+/, ''), + ); + try { + await mkdir(dirname(filePath), { recursive: true }); + await writeFile(filePath, Buffer.from(buffer)); + route.skip = false; + } catch { + // leave Nitro's skip in place if the manual write also fails + } + }); } const hasAPIDir = existsSync( From df6f6a309a6397def85be9a00c48f795fd6e2a10 Mon Sep 17 00:00:00 2001 From: Brandon Roberts Date: Mon, 25 May 2026 20:25:46 -0500 Subject: [PATCH 65/65] fix(platform): bridge Vite publicDir to Nitro publicAssets and preserve SSR query string MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two distinct SSR regressions in this PR's nitro/vite migration: 1) /assets/ 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) --- .../src/lib/nitro/analog-nitro-plugin.ts | 61 ++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/packages/platform/src/lib/nitro/analog-nitro-plugin.ts b/packages/platform/src/lib/nitro/analog-nitro-plugin.ts index 7af46ab0a..0a10c2e7a 100644 --- a/packages/platform/src/lib/nitro/analog-nitro-plugin.ts +++ b/packages/platform/src/lib/nitro/analog-nitro-plugin.ts @@ -66,6 +66,7 @@ export function analogNitroPlugin(options: Options = {}): Plugin { sourceRoot, }; let ssrEntryMarkerPath = ''; + let userPublicDir: string | undefined; function refreshContext(viteRoot: string | undefined) { const root = viteRoot ?? process.cwd(); @@ -121,6 +122,21 @@ export function analogNitroPlugin(options: Options = {}): Plugin { config(userConfig) { refreshContext(userConfig.root); + // Capture the user's Vite `publicDir` so the nitro `setup()` hook can + // register it as a Nitro `publicAssets` source. nitro/vite forces the + // client environment's `build.copyPublicDir` to `false` + // (vite.mjs:248), expecting Nitro to manage public assets — but + // doesn't auto-add the user's `publicDir`. Without this, anything + // under `src/public/` (e.g. `/assets/shipping.json`) 404s during SSR + // and ofetch consumers parse the catch-all SSR HTML as JSON. + if (userConfig.publicDir !== false) { + userPublicDir = resolve( + context.workspaceRoot, + context.rootDir, + (userConfig.publicDir as string | undefined) ?? 'public', + ); + } + // Bridge the legacy `BUILD_PRESET` env var that `@analogjs/vite-plugin-nitro` // accepted into Nitro v3's `NITRO_PRESET`, and auto-pick the matching // preset when the build runs inside a host's CI (each host sets its @@ -361,6 +377,49 @@ export function analogNitroPlugin(options: Options = {}): Plugin { }); } + // Register the user's Vite `publicDir` (e.g. `src/public/`) as a + // Nitro public asset source. nitro/vite turns off Vite's own copy + // of publicDir so Nitro can take over, but doesn't auto-bridge the + // user's setting — without this entry, files like + // `src/public/assets/shipping.json` aren't served and `HttpClient` + // SSR fetches fall through to the catch-all SSR renderer, leaking + // HTML where consumers expected JSON. + if (userPublicDir && existsSync(userPublicDir)) { + const already = nitro.options.publicAssets.some( + (asset) => asset.dir === userPublicDir, + ); + if (!already) { + nitro.options.publicAssets.push({ + dir: userPublicDir, + baseURL: '/', + maxAge: 0, + fallthrough: true, + }); + } + } + + // Bridge the outer Nitro's `output.publicDir` into the prerender's + // own Nitro instance. Nitro spawns a nested `nitroRenderer` for the + // prerender pass (`createNitro({ preset: 'nitro-prerender' })`) and + // builds the public-assets manifest by glob-scanning that + // instance's `output.publicDir`. The nested config resets + // `output.publicDir` to undefined and resolves it against the + // prerender preset defaults (`/.output/public`), so the + // scan hits an empty directory and the manifest is empty — every + // `HttpClient.get('/assets/...')` during SSR then falls through to + // the catch-all SSR renderer and the consumer parses HTML as JSON. + // Force the nested publicDir to match the outer publicDir (which + // `copyPublicAssets` has already populated by this point). + nitro.hooks.hook( + 'prerender:config', + (prerendererConfig: { output?: Record }) => { + prerendererConfig.output = { + ...prerendererConfig.output, + publicDir: nitro.options.output.publicDir, + }; + }, + ); + const hasAPIDir = existsSync( resolve( context.workspaceRoot, @@ -694,7 +753,7 @@ export default { }; try { - const html = await renderer(requestPath, TEMPLATE, { + const html = await renderer(requestUrl, TEMPLATE, { req: reqShim, // Pass the ofetch-wrapped fetch — INTERNAL_FETCH is consumed by the // router's request-context interceptor via \`serverFetch.raw(...)\`,