@analogjs/platform 3.0.0-alpha.99 → 3.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/migrations/migrate-setup-testbed-browser-mode/migrate-setup-testbed-browser-mode.js +8 -6
- package/migrations/migrate-setup-testbed-browser-mode/migrate-setup-testbed-browser-mode.js.map +1 -1
- package/migrations/migrate-to-separated-plugins/migrate-to-separated-plugins.js.map +1 -1
- package/migrations/migrate-vitest-builder-aliases/migrate-vitest-builder-aliases.js.map +1 -1
- package/migrations/utils/source-edits.js.map +1 -1
- package/package.json +2 -2
- package/src/lib/analog-integration-plugin.js.map +1 -1
- package/src/lib/clear-client-page-endpoint.js.map +1 -1
- package/src/lib/content/marked/index.js.map +1 -1
- package/src/lib/content/marked/marked-content-highlighter.js.map +1 -1
- package/src/lib/content/marked/marked-setup.service.js.map +1 -1
- package/src/lib/content/prism/angular.js.map +1 -1
- package/src/lib/content/prism/index.js +1 -1
- package/src/lib/content/prism/index.js.map +1 -1
- package/src/lib/content/prism/prism-highlighter.js.map +1 -1
- package/src/lib/content/shiki/index.js +6 -4
- package/src/lib/content/shiki/index.js.map +1 -1
- package/src/lib/content/shiki/shiki-highlighter.js.map +1 -1
- package/src/lib/content/utils/escape-html.js.map +1 -1
- package/src/lib/content-plugin.js +18 -16
- package/src/lib/content-plugin.js.map +1 -1
- package/src/lib/deps-plugin.js.map +1 -1
- package/src/lib/discover-library-routes.js.map +1 -1
- package/src/lib/inject-server-fn-ids.js.map +1 -1
- package/src/lib/nitro/analog-nitro-plugin.js +8 -4
- package/src/lib/nitro/analog-nitro-plugin.js.map +1 -1
- package/src/lib/nitro/angular-linker-plugin.js +2 -1
- package/src/lib/nitro/angular-linker-plugin.js.map +1 -1
- package/src/lib/nitro/build-sitemap.js.map +1 -1
- package/src/lib/nitro/debug.js +8 -4
- package/src/lib/nitro/debug.js.map +1 -1
- package/src/lib/nitro/get-content-files.js +2 -1
- package/src/lib/nitro/get-content-files.js.map +1 -1
- package/src/lib/nitro/get-page-handlers.js +2 -1
- package/src/lib/nitro/get-page-handlers.js.map +1 -1
- package/src/lib/nitro/get-server-fn-handlers.js.map +1 -1
- package/src/lib/nitro/i18n-prerender.js +1 -1
- package/src/lib/nitro/i18n-prerender.js.map +1 -1
- package/src/lib/nitro/page-endpoints-plugin.js.map +1 -1
- package/src/lib/nitro/post-rendering-hook.js.map +1 -1
- package/src/lib/nitro/server-fn-endpoints.js +1 -1
- package/src/lib/nitro/server-fn-endpoints.js.map +1 -1
- package/src/lib/nx-plugin/_virtual/_rolldown/runtime.js +1 -1
- package/src/lib/nx-plugin/node_modules/.pnpm/es-toolkit@1.45.1/node_modules/es-toolkit/dist/string/camelCase.js.map +1 -1
- package/src/lib/nx-plugin/node_modules/.pnpm/es-toolkit@1.45.1/node_modules/es-toolkit/dist/string/capitalize.js.map +1 -1
- package/src/lib/nx-plugin/node_modules/.pnpm/es-toolkit@1.45.1/node_modules/es-toolkit/dist/string/words.js +1 -0
- package/src/lib/nx-plugin/node_modules/.pnpm/es-toolkit@1.45.1/node_modules/es-toolkit/dist/string/words.js.map +1 -1
- package/src/lib/nx-plugin/src/executors/vite-dev-server/compat.js +0 -1
- package/src/lib/nx-plugin/src/executors/vite-dev-server/compat.js.map +1 -1
- package/src/lib/nx-plugin/src/executors/vite-dev-server/vite-dev-server.impl.js +0 -1
- package/src/lib/nx-plugin/src/executors/vite-dev-server/vite-dev-server.impl.js.map +1 -1
- package/src/lib/nx-plugin/src/generators/app/compat.js +0 -1
- package/src/lib/nx-plugin/src/generators/app/compat.js.map +1 -1
- package/src/lib/nx-plugin/src/generators/app/generator.js +0 -1
- package/src/lib/nx-plugin/src/generators/app/generator.js.map +1 -1
- package/src/lib/nx-plugin/src/generators/app/lib/add-agent-context.js +0 -1
- package/src/lib/nx-plugin/src/generators/app/lib/add-agent-context.js.map +1 -1
- package/src/lib/nx-plugin/src/generators/app/lib/add-analog-project-config.js +48 -47
- package/src/lib/nx-plugin/src/generators/app/lib/add-analog-project-config.js.map +1 -1
- package/src/lib/nx-plugin/src/generators/app/lib/add-angular-app.js +0 -1
- package/src/lib/nx-plugin/src/generators/app/lib/add-angular-app.js.map +1 -1
- package/src/lib/nx-plugin/src/generators/app/lib/add-files.js +0 -1
- package/src/lib/nx-plugin/src/generators/app/lib/add-files.js.map +1 -1
- package/src/lib/nx-plugin/src/generators/app/lib/add-home-page.js +0 -1
- package/src/lib/nx-plugin/src/generators/app/lib/add-home-page.js.map +1 -1
- package/src/lib/nx-plugin/src/generators/app/lib/add-tailwind-config.js +1 -1
- package/src/lib/nx-plugin/src/generators/app/lib/add-tailwind-config.js.map +1 -1
- package/src/lib/nx-plugin/src/generators/app/lib/add-tailwind-helpers.js +0 -1
- package/src/lib/nx-plugin/src/generators/app/lib/add-tailwind-helpers.js.map +1 -1
- package/src/lib/nx-plugin/src/generators/app/lib/cleanup-files.js.map +1 -1
- package/src/lib/nx-plugin/src/generators/app/lib/update-index-html.js +0 -1
- package/src/lib/nx-plugin/src/generators/app/lib/update-index-html.js.map +1 -1
- package/src/lib/nx-plugin/src/generators/app/versions/minimum-supported-versions.js +0 -1
- package/src/lib/nx-plugin/src/generators/app/versions/minimum-supported-versions.js.map +1 -1
- package/src/lib/nx-plugin/src/generators/app/versions/tailwind-dependencies.js.map +1 -1
- package/src/lib/nx-plugin/src/generators/init/compat.js +0 -1
- package/src/lib/nx-plugin/src/generators/init/compat.js.map +1 -1
- package/src/lib/nx-plugin/src/generators/init/generator.js +0 -1
- package/src/lib/nx-plugin/src/generators/init/generator.js.map +1 -1
- package/src/lib/nx-plugin/src/generators/init/lib/add-analog-dependencies.js +0 -1
- package/src/lib/nx-plugin/src/generators/init/lib/add-analog-dependencies.js.map +1 -1
- package/src/lib/nx-plugin/src/generators/init/lib/update-app-tsconfig.js +0 -1
- package/src/lib/nx-plugin/src/generators/init/lib/update-app-tsconfig.js.map +1 -1
- package/src/lib/nx-plugin/src/generators/init/lib/update-build-target.js +0 -1
- package/src/lib/nx-plugin/src/generators/init/lib/update-build-target.js.map +1 -1
- package/src/lib/nx-plugin/src/generators/init/lib/update-git-ignore.js.map +1 -1
- package/src/lib/nx-plugin/src/generators/init/lib/update-index-html.js +0 -1
- package/src/lib/nx-plugin/src/generators/init/lib/update-index-html.js.map +1 -1
- package/src/lib/nx-plugin/src/generators/init/lib/update-main.js +0 -1
- package/src/lib/nx-plugin/src/generators/init/lib/update-main.js.map +1 -1
- package/src/lib/nx-plugin/src/generators/init/lib/update-package-json.js +2 -2
- package/src/lib/nx-plugin/src/generators/init/lib/update-package-json.js.map +1 -1
- package/src/lib/nx-plugin/src/generators/init/lib/update-serve-target.js +0 -1
- package/src/lib/nx-plugin/src/generators/init/lib/update-serve-target.js.map +1 -1
- package/src/lib/nx-plugin/src/generators/init/lib/update-test-target.js +0 -1
- package/src/lib/nx-plugin/src/generators/init/lib/update-test-target.js.map +1 -1
- package/src/lib/nx-plugin/src/generators/init/lib/update-test-tsconfig.js +0 -1
- package/src/lib/nx-plugin/src/generators/init/lib/update-test-tsconfig.js.map +1 -1
- package/src/lib/nx-plugin/src/generators/page/compat.js +0 -1
- package/src/lib/nx-plugin/src/generators/page/compat.js.map +1 -1
- package/src/lib/nx-plugin/src/generators/page/generator.js +2 -2
- package/src/lib/nx-plugin/src/generators/page/generator.js.map +1 -1
- package/src/lib/nx-plugin/src/generators/preset/generator.js +0 -1
- package/src/lib/nx-plugin/src/generators/preset/generator.js.map +1 -1
- package/src/lib/nx-plugin/src/utils/version-utils.js +0 -1
- package/src/lib/nx-plugin/src/utils/version-utils.js.map +1 -1
- package/src/lib/nx-plugin/src/utils/versions/dependencies.js +0 -1
- package/src/lib/nx-plugin/src/utils/versions/dependencies.js.map +1 -1
- package/src/lib/nx-plugin/src/utils/versions/dev-dependencies.js +2 -2
- package/src/lib/nx-plugin/src/utils/versions/dev-dependencies.js.map +1 -1
- package/src/lib/nx-plugin/src/utils/versions/versions.d.ts +5 -5
- package/src/lib/nx-plugin/src/utils/versions/versions.js +5 -5
- package/src/lib/nx-plugin/src/utils/versions/versions.js.map +1 -1
- package/src/lib/platform-plugin.js.map +1 -1
- package/src/lib/route-file-discovery.js.map +1 -1
- package/src/lib/route-idiom-diagnostics.js +2 -1
- package/src/lib/route-idiom-diagnostics.js.map +1 -1
- package/src/lib/route-manifest.js +1 -1
- package/src/lib/route-manifest.js.map +1 -1
- package/src/lib/router-plugin.js +18 -2
- package/src/lib/router-plugin.js.map +1 -1
- package/src/lib/server-fn-client-transform.js.map +1 -1
- package/src/lib/server-fn-id.js.map +1 -1
- package/src/lib/ssr/defer-streaming-plugin.js.map +1 -1
- package/src/lib/ssr/inject-html-plugin.js.map +1 -1
- package/src/lib/ssr/ssr-build-plugin.js.map +1 -1
- package/src/lib/typed-routes-plugin.js.map +1 -1
- package/src/lib/utils/debug-harness.js.map +1 -1
- package/src/lib/utils/debug-log-file.js +2 -1
- package/src/lib/utils/debug-log-file.js.map +1 -1
- package/src/lib/utils/debug.js +7 -4
- package/src/lib/utils/debug.js.map +1 -1
- package/src/lib/utils/rolldown.js.map +1 -1
- package/src/server-mode-plugin.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"platform-plugin.js","names":[],"sources":["../../../src/lib/platform-plugin.ts"],"sourcesContent":["import { createRequire } from 'node:module';\nimport { join } from 'node:path';\nimport { Plugin } from 'vite';\n\nimport { Options } from './options.js';\nimport {\n activateDeferredDebug,\n applyDebugOption,\n debugPlatform,\n} from './utils/debug.js';\nimport { routerPlugin } from './router-plugin.js';\nimport { analogIntegrationPlugin } from './analog-integration-plugin.js';\nimport {\n i18nDefRegistryPlugin,\n ssrBuildPlugin,\n} from './ssr/ssr-build-plugin.js';\nimport { contentPlugin } from './content-plugin.js';\nimport { clearClientPageEndpointsPlugin } from './clear-client-page-endpoint.js';\nimport { depsPlugin } from './deps-plugin.js';\nimport { injectHTMLPlugin } from './ssr/inject-html-plugin.js';\nimport {\n deferStreamingPlugin,\n MIN_STREAMING_ANGULAR_MAJOR,\n streamingSupportedOnAngular,\n} from './ssr/defer-streaming-plugin.js';\nimport { serverModePlugin } from '../server-mode-plugin.js';\nimport { typedRoutes } from './typed-routes-plugin.js';\nimport { analogNitroPlugin } from './nitro/analog-nitro-plugin.js';\n\n/**\n * The installed `@angular/core` major version, resolved from the workspace root\n * (where the app's Angular is installed). Returns `null` when it cannot be\n * detected, in which case streaming is not blocked and the build-time\n * anchor-drift detection is relied on instead.\n */\nfunction getAngularCoreMajor(workspaceRoot: string): number | null {\n try {\n const req = createRequire(join(workspaceRoot, 'noop.js'));\n const { version } = req('@angular/core/package.json') as {\n version: string;\n };\n const major = Number.parseInt(version.split('.')[0], 10);\n return Number.isNaN(major) ? null : major;\n } catch {\n return null;\n }\n}\n\nexport function platformPlugin(opts: Options = {}): Plugin[] {\n applyDebugOption(opts.debug, opts.workspaceRoot);\n\n const isTest = process.env['NODE_ENV'] === 'test' || !!process.env['VITEST'];\n const { ...platformOptions } = {\n ssr: true,\n ...opts,\n };\n\n // Streaming patches the compiled @angular/core, which needs a supported\n // Angular version. Turn it off otherwise so the renderer stays buffered.\n if (platformOptions.ssr && platformOptions.experimental?.streaming) {\n const major = getAngularCoreMajor(\n platformOptions.workspaceRoot ?? process.cwd(),\n );\n if (!streamingSupportedOnAngular(major)) {\n console.warn(\n `[@analogjs/platform] experimental.streaming requires Angular ` +\n `${MIN_STREAMING_ANGULAR_MAJOR}+ (detected v${major}); streaming is ` +\n `disabled and rendering falls back to buffered SSR.`,\n );\n platformOptions.experimental = {\n ...platformOptions.experimental,\n streaming: false,\n };\n }\n }\n\n debugPlatform('experimental options resolved', {\n typedRouting: platformOptions.experimental?.typedRouting,\n });\n\n return [\n ...(platformOptions.experimental?.typedRouting\n ? [\n typedRoutes({\n ...(typeof platformOptions.experimental.typedRouting === 'object'\n ? platformOptions.experimental.typedRouting\n : {}),\n workspaceRoot: platformOptions.workspaceRoot,\n additionalPagesDirs: platformOptions.additionalPagesDirs,\n additionalContentDirs: platformOptions.additionalContentDirs,\n }),\n ]\n : []),\n {\n name: 'analogjs-debug-activate',\n config(_, { command }) {\n activateDeferredDebug(command);\n },\n },\n // `nitro()` from `@analogjs/vite-plugin-nitro` reads these as its defaults.\n {\n name: '@analogjs/platform-options',\n api: { analogOptions: platformOptions },\n },\n ...(!isTest ? [analogNitroPlugin(platformOptions)] : []),\n ...(platformOptions.ssr\n ? [...ssrBuildPlugin(), ...injectHTMLPlugin()]\n : []),\n ...(platformOptions.ssr && platformOptions.experimental?.streaming\n ? [deferStreamingPlugin()]\n : []),\n ...(!isTest ? depsPlugin(platformOptions) : []),\n ...routerPlugin(platformOptions),\n analogIntegrationPlugin(platformOptions),\n ...contentPlugin(platformOptions?.content, platformOptions),\n ...(platformOptions.i18n ? [i18nDefRegistryPlugin()] : []),\n ...serverModePlugin(platformOptions.entryServer),\n clearClientPageEndpointsPlugin(),\n ];\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAmCA,SAAS,oBAAoB,eAAsC;
|
|
1
|
+
{"version":3,"file":"platform-plugin.js","names":[],"sources":["../../../src/lib/platform-plugin.ts"],"sourcesContent":["import { createRequire } from 'node:module';\nimport { join } from 'node:path';\nimport { Plugin } from 'vite';\n\nimport { Options } from './options.js';\nimport {\n activateDeferredDebug,\n applyDebugOption,\n debugPlatform,\n} from './utils/debug.js';\nimport { routerPlugin } from './router-plugin.js';\nimport { analogIntegrationPlugin } from './analog-integration-plugin.js';\nimport {\n i18nDefRegistryPlugin,\n ssrBuildPlugin,\n} from './ssr/ssr-build-plugin.js';\nimport { contentPlugin } from './content-plugin.js';\nimport { clearClientPageEndpointsPlugin } from './clear-client-page-endpoint.js';\nimport { depsPlugin } from './deps-plugin.js';\nimport { injectHTMLPlugin } from './ssr/inject-html-plugin.js';\nimport {\n deferStreamingPlugin,\n MIN_STREAMING_ANGULAR_MAJOR,\n streamingSupportedOnAngular,\n} from './ssr/defer-streaming-plugin.js';\nimport { serverModePlugin } from '../server-mode-plugin.js';\nimport { typedRoutes } from './typed-routes-plugin.js';\nimport { analogNitroPlugin } from './nitro/analog-nitro-plugin.js';\n\n/**\n * The installed `@angular/core` major version, resolved from the workspace root\n * (where the app's Angular is installed). Returns `null` when it cannot be\n * detected, in which case streaming is not blocked and the build-time\n * anchor-drift detection is relied on instead.\n */\nfunction getAngularCoreMajor(workspaceRoot: string): number | null {\n try {\n const req = createRequire(join(workspaceRoot, 'noop.js'));\n const { version } = req('@angular/core/package.json') as {\n version: string;\n };\n const major = Number.parseInt(version.split('.')[0], 10);\n return Number.isNaN(major) ? null : major;\n } catch {\n return null;\n }\n}\n\nexport function platformPlugin(opts: Options = {}): Plugin[] {\n applyDebugOption(opts.debug, opts.workspaceRoot);\n\n const isTest = process.env['NODE_ENV'] === 'test' || !!process.env['VITEST'];\n const { ...platformOptions } = {\n ssr: true,\n ...opts,\n };\n\n // Streaming patches the compiled @angular/core, which needs a supported\n // Angular version. Turn it off otherwise so the renderer stays buffered.\n if (platformOptions.ssr && platformOptions.experimental?.streaming) {\n const major = getAngularCoreMajor(\n platformOptions.workspaceRoot ?? process.cwd(),\n );\n if (!streamingSupportedOnAngular(major)) {\n console.warn(\n `[@analogjs/platform] experimental.streaming requires Angular ` +\n `${MIN_STREAMING_ANGULAR_MAJOR}+ (detected v${major}); streaming is ` +\n `disabled and rendering falls back to buffered SSR.`,\n );\n platformOptions.experimental = {\n ...platformOptions.experimental,\n streaming: false,\n };\n }\n }\n\n debugPlatform('experimental options resolved', {\n typedRouting: platformOptions.experimental?.typedRouting,\n });\n\n return [\n ...(platformOptions.experimental?.typedRouting\n ? [\n typedRoutes({\n ...(typeof platformOptions.experimental.typedRouting === 'object'\n ? platformOptions.experimental.typedRouting\n : {}),\n workspaceRoot: platformOptions.workspaceRoot,\n additionalPagesDirs: platformOptions.additionalPagesDirs,\n additionalContentDirs: platformOptions.additionalContentDirs,\n }),\n ]\n : []),\n {\n name: 'analogjs-debug-activate',\n config(_, { command }) {\n activateDeferredDebug(command);\n },\n },\n // `nitro()` from `@analogjs/vite-plugin-nitro` reads these as its defaults.\n {\n name: '@analogjs/platform-options',\n api: { analogOptions: platformOptions },\n },\n ...(!isTest ? [analogNitroPlugin(platformOptions)] : []),\n ...(platformOptions.ssr\n ? [...ssrBuildPlugin(), ...injectHTMLPlugin()]\n : []),\n ...(platformOptions.ssr && platformOptions.experimental?.streaming\n ? [deferStreamingPlugin()]\n : []),\n ...(!isTest ? depsPlugin(platformOptions) : []),\n ...routerPlugin(platformOptions),\n analogIntegrationPlugin(platformOptions),\n ...contentPlugin(platformOptions?.content, platformOptions),\n ...(platformOptions.i18n ? [i18nDefRegistryPlugin()] : []),\n ...serverModePlugin(platformOptions.entryServer),\n clearClientPageEndpointsPlugin(),\n ];\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAmCA,SAAS,oBAAoB,eAAsC;CACjE,IAAI;EAEF,MAAM,EAAE,YADI,cAAc,KAAK,eAAe,SAAS,CACnC,CAAA,CAAI,4BAA4B;EAGpD,MAAM,QAAQ,OAAO,SAAS,QAAQ,MAAM,GAAG,CAAC,CAAC,IAAI,EAAE;EACvD,OAAO,OAAO,MAAM,KAAK,IAAI,OAAO;CACtC,QAAQ;EACN,OAAO;CACT;AACF;AAEA,SAAgB,eAAe,OAAgB,CAAC,GAAa;CAC3D,iBAAiB,KAAK,OAAO,KAAK,aAAa;CAE/C,MAAM,SAAA,QAAA,IAAA,aAAqC,UAAU,CAAC,CAAC,QAAQ,IAAI;CACnE,MAAM,EAAE,GAAG,oBAAoB;EAC7B,KAAK;EACL,GAAG;CACL;CAIA,IAAI,gBAAgB,OAAO,gBAAgB,cAAc,WAAW;EAClE,MAAM,QAAQ,oBACZ,gBAAgB,iBAAiB,QAAQ,IAAI,CAC/C;EACA,IAAI,CAAC,4BAA4B,KAAK,GAAG;GACvC,QAAQ,KACN,+EACgD,MAAM,mEAExD;GACA,gBAAgB,eAAe;IAC7B,GAAG,gBAAgB;IACnB,WAAW;GACb;EACF;CACF;CAEA,cAAc,iCAAiC,EAC7C,cAAc,gBAAgB,cAAc,aAC9C,CAAC;CAED,OAAO;EACL,GAAI,gBAAgB,cAAc,eAC9B,CACE,YAAY;GACV,GAAI,OAAO,gBAAgB,aAAa,iBAAiB,WACrD,gBAAgB,aAAa,eAC7B,CAAC;GACL,eAAe,gBAAgB;GAC/B,qBAAqB,gBAAgB;GACrC,uBAAuB,gBAAgB;EACzC,CAAC,CACH,IACA,CAAC;EACL;GACE,MAAM;GACN,OAAO,GAAG,EAAE,WAAW;IACrB,sBAAsB,OAAO;GAC/B;EACF;EAEA;GACE,MAAM;GACN,KAAK,EAAE,eAAe,gBAAgB;EACxC;EACA,GAAI,CAAC,SAAS,CAAC,kBAAkB,eAAe,CAAC,IAAI,CAAC;EACtD,GAAI,gBAAgB,MAChB,CAAC,GAAG,eAAe,GAAG,GAAG,iBAAiB,CAAC,IAC3C,CAAC;EACL,GAAI,gBAAgB,OAAO,gBAAgB,cAAc,YACrD,CAAC,qBAAqB,CAAC,IACvB,CAAC;EACL,GAAI,CAAC,SAAS,WAAW,eAAe,IAAI,CAAC;EAC7C,GAAG,aAAa,eAAe;EAC/B,wBAAwB,eAAe;EACvC,GAAG,cAAc,iBAAiB,SAAS,eAAe;EAC1D,GAAI,gBAAgB,OAAO,CAAC,sBAAsB,CAAC,IAAI,CAAC;EACxD,GAAG,iBAAiB,gBAAgB,WAAW;EAC/C,+BAA+B;CACjC;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"route-file-discovery.js","names":[],"sources":["../../../src/lib/route-file-discovery.ts"],"sourcesContent":["import { normalizePath } from 'vite';\nimport { resolve } from 'node:path';\nimport { globSync } from 'tinyglobby';\n\nexport interface RouteFileDiscoveryOptions {\n root: string;\n workspaceRoot: string;\n additionalPagesDirs: string[];\n additionalContentDirs: string[];\n}\n\nexport type RouteFileKind = 'route' | 'content' | 'server';\n\nexport interface RouteFileDiscovery {\n getRouteFiles(): string[];\n getContentFiles(): string[];\n /** `server` marks page `.server.ts` files, which affect `load` types only. */\n getDiscoveredFileKind(path: string): RouteFileKind | null;\n updateDiscoveredFile(path: string, event: 'add' | 'change' | 'unlink'): void;\n /**\n * Returns true if the normalized filename was discovered from an app-local\n * glob (under the configured root) rather than from an additional directory.\n *\n * Used by collision priority to prefer app-local routes over shared/external\n * routes based on actual configured roots instead of hard-coded path\n * substrings.\n */\n isAppLocal(normalizedFilename: string): boolean;\n reset(): void;\n}\n\nexport function createRouteFileDiscovery(\n options: RouteFileDiscoveryOptions,\n): RouteFileDiscovery {\n const { root, workspaceRoot, additionalPagesDirs, additionalContentDirs } =\n options;\n\n let routeFilesCache = new Set<string>();\n let contentFilesCache = new Set<string>();\n let appLocalFilesCache = new Set<string>();\n let initialized = false;\n const joinDir = (dir: string) =>\n isWithinDir(dir, workspaceRoot)\n ? normalizePath(dir)\n : dir.startsWith('/')\n ? normalizePath(`${workspaceRoot}${dir}`)\n : normalizePath(resolve(workspaceRoot, dir));\n const additionalPagesRoots = additionalPagesDirs.map(joinDir);\n const additionalContentRoots = additionalContentDirs.map(joinDir);\n\n function normalizePath_(absolutePath: string): string {\n const normalized = normalizePath(absolutePath);\n if (isWithinDir(normalized, root)) {\n return normalized.slice(root.length);\n }\n if (isWithinDir(normalized, workspaceRoot)) {\n return normalized.slice(workspaceRoot.length);\n }\n return normalized;\n }\n\n function isWithinDir(path: string, dir: string): boolean {\n return path === dir || path.startsWith(`${dir}/`);\n }\n\n function getAppLocalFileKind(path: string): 'route' | 'content' | null {\n if (\n path.endsWith('.md') &&\n ['src/app/pages', 'src/content'].some((dir) =>\n isWithinDir(path, `${root}/${dir}`),\n )\n )\n return 'content';\n if (path.endsWith('.page.ts') && isWithinDir(path, `${root}/src/app/pages`))\n return 'route';\n return null;\n }\n\n function getDiscoveredFileKind(path: string): RouteFileKind | null {\n const normalized = normalizePath(path);\n const localKind = getAppLocalFileKind(normalized);\n if (localKind) return localKind;\n if (\n normalized.endsWith('.md') &&\n additionalContentRoots.some((dir) => isWithinDir(normalized, dir))\n ) {\n return 'content';\n }\n if (\n normalized.endsWith('.page.ts') &&\n additionalPagesRoots.some((dir) => isWithinDir(normalized, dir))\n ) {\n return 'route';\n }\n if (\n normalized.endsWith('.server.ts') &&\n [`${root}/src/app/pages`, ...additionalPagesRoots].some((dir) =>\n isWithinDir(normalized, dir),\n )\n ) {\n return 'server';\n }\n return null;\n }\n\n function scanRouteFiles(): string[] {\n const appLocalGlobs = [`${root}/src/app/pages/**/*.page.ts`];\n const additionalGlobs = additionalPagesDirs.map(\n (dir) => `${joinDir(dir)}/**/*.page.ts`,\n );\n\n const appLocal = globSync(appLocalGlobs, {\n dot: true,\n absolute: true,\n }).map(normalizePath_);\n\n const additional = globSync(additionalGlobs, {\n dot: true,\n absolute: true,\n }).map(normalizePath_);\n\n for (const f of appLocal) {\n appLocalFilesCache.add(f);\n }\n\n return [...appLocal, ...additional];\n }\n\n function scanContentFiles(): string[] {\n const appLocalGlobs = [\n `${root}/src/app/pages/**/*.md`,\n `${root}/src/content/**/*.md`,\n ];\n const additionalGlobs = additionalContentDirs.map(\n (dir) => `${joinDir(dir)}/**/*.md`,\n );\n\n const appLocal = globSync(appLocalGlobs, {\n dot: true,\n absolute: true,\n }).map(normalizePath_);\n\n const additional = globSync(additionalGlobs, {\n dot: true,\n absolute: true,\n }).map(normalizePath_);\n\n for (const f of appLocal) {\n appLocalFilesCache.add(f);\n }\n\n return [...appLocal, ...additional];\n }\n\n function isAppLocalByPath(absolutePath: string): boolean {\n const normalized = normalizePath(absolutePath);\n return getAppLocalFileKind(normalized) !== null;\n }\n\n function ensureInitialized(): void {\n if (initialized) {\n return;\n }\n\n routeFilesCache = new Set(scanRouteFiles());\n contentFilesCache = new Set(scanContentFiles());\n initialized = true;\n }\n\n return {\n getRouteFiles(): string[] {\n ensureInitialized();\n return [...routeFilesCache].sort();\n },\n\n getContentFiles(): string[] {\n ensureInitialized();\n return [...contentFilesCache].sort();\n },\n\n getDiscoveredFileKind(path: string): RouteFileKind | null {\n return getDiscoveredFileKind(path);\n },\n\n updateDiscoveredFile(\n path: string,\n event: 'add' | 'change' | 'unlink',\n ): void {\n ensureInitialized();\n const kind = getDiscoveredFileKind(path);\n if (!kind || kind === 'server') {\n return;\n }\n\n const normalizedPath = normalizePath_(path);\n const targetCache =\n kind === 'content' ? contentFilesCache : routeFilesCache;\n\n if (event === 'unlink') {\n targetCache.delete(normalizedPath);\n appLocalFilesCache.delete(normalizedPath);\n return;\n }\n\n targetCache.add(normalizedPath);\n if (isAppLocalByPath(path)) {\n appLocalFilesCache.add(normalizedPath);\n }\n },\n\n isAppLocal(normalizedFilename: string): boolean {\n ensureInitialized();\n return appLocalFilesCache.has(normalizedFilename);\n },\n\n reset(): void {\n initialized = false;\n appLocalFilesCache = new Set<string>();\n },\n };\n}\n"],"mappings":";;;;AA+BA,SAAgB,yBACd,SACoB;CACpB,MAAM,EAAE,MAAM,eAAe,qBAAqB,0BAChD;CAEF,IAAI,kCAAkB,IAAI,
|
|
1
|
+
{"version":3,"file":"route-file-discovery.js","names":[],"sources":["../../../src/lib/route-file-discovery.ts"],"sourcesContent":["import { normalizePath } from 'vite';\nimport { resolve } from 'node:path';\nimport { globSync } from 'tinyglobby';\n\nexport interface RouteFileDiscoveryOptions {\n root: string;\n workspaceRoot: string;\n additionalPagesDirs: string[];\n additionalContentDirs: string[];\n}\n\nexport type RouteFileKind = 'route' | 'content' | 'server';\n\nexport interface RouteFileDiscovery {\n getRouteFiles(): string[];\n getContentFiles(): string[];\n /** `server` marks page `.server.ts` files, which affect `load` types only. */\n getDiscoveredFileKind(path: string): RouteFileKind | null;\n updateDiscoveredFile(path: string, event: 'add' | 'change' | 'unlink'): void;\n /**\n * Returns true if the normalized filename was discovered from an app-local\n * glob (under the configured root) rather than from an additional directory.\n *\n * Used by collision priority to prefer app-local routes over shared/external\n * routes based on actual configured roots instead of hard-coded path\n * substrings.\n */\n isAppLocal(normalizedFilename: string): boolean;\n reset(): void;\n}\n\nexport function createRouteFileDiscovery(\n options: RouteFileDiscoveryOptions,\n): RouteFileDiscovery {\n const { root, workspaceRoot, additionalPagesDirs, additionalContentDirs } =\n options;\n\n let routeFilesCache = new Set<string>();\n let contentFilesCache = new Set<string>();\n let appLocalFilesCache = new Set<string>();\n let initialized = false;\n const joinDir = (dir: string) =>\n isWithinDir(dir, workspaceRoot)\n ? normalizePath(dir)\n : dir.startsWith('/')\n ? normalizePath(`${workspaceRoot}${dir}`)\n : normalizePath(resolve(workspaceRoot, dir));\n const additionalPagesRoots = additionalPagesDirs.map(joinDir);\n const additionalContentRoots = additionalContentDirs.map(joinDir);\n\n function normalizePath_(absolutePath: string): string {\n const normalized = normalizePath(absolutePath);\n if (isWithinDir(normalized, root)) {\n return normalized.slice(root.length);\n }\n if (isWithinDir(normalized, workspaceRoot)) {\n return normalized.slice(workspaceRoot.length);\n }\n return normalized;\n }\n\n function isWithinDir(path: string, dir: string): boolean {\n return path === dir || path.startsWith(`${dir}/`);\n }\n\n function getAppLocalFileKind(path: string): 'route' | 'content' | null {\n if (\n path.endsWith('.md') &&\n ['src/app/pages', 'src/content'].some((dir) =>\n isWithinDir(path, `${root}/${dir}`),\n )\n )\n return 'content';\n if (path.endsWith('.page.ts') && isWithinDir(path, `${root}/src/app/pages`))\n return 'route';\n return null;\n }\n\n function getDiscoveredFileKind(path: string): RouteFileKind | null {\n const normalized = normalizePath(path);\n const localKind = getAppLocalFileKind(normalized);\n if (localKind) return localKind;\n if (\n normalized.endsWith('.md') &&\n additionalContentRoots.some((dir) => isWithinDir(normalized, dir))\n ) {\n return 'content';\n }\n if (\n normalized.endsWith('.page.ts') &&\n additionalPagesRoots.some((dir) => isWithinDir(normalized, dir))\n ) {\n return 'route';\n }\n if (\n normalized.endsWith('.server.ts') &&\n [`${root}/src/app/pages`, ...additionalPagesRoots].some((dir) =>\n isWithinDir(normalized, dir),\n )\n ) {\n return 'server';\n }\n return null;\n }\n\n function scanRouteFiles(): string[] {\n const appLocalGlobs = [`${root}/src/app/pages/**/*.page.ts`];\n const additionalGlobs = additionalPagesDirs.map(\n (dir) => `${joinDir(dir)}/**/*.page.ts`,\n );\n\n const appLocal = globSync(appLocalGlobs, {\n dot: true,\n absolute: true,\n }).map(normalizePath_);\n\n const additional = globSync(additionalGlobs, {\n dot: true,\n absolute: true,\n }).map(normalizePath_);\n\n for (const f of appLocal) {\n appLocalFilesCache.add(f);\n }\n\n return [...appLocal, ...additional];\n }\n\n function scanContentFiles(): string[] {\n const appLocalGlobs = [\n `${root}/src/app/pages/**/*.md`,\n `${root}/src/content/**/*.md`,\n ];\n const additionalGlobs = additionalContentDirs.map(\n (dir) => `${joinDir(dir)}/**/*.md`,\n );\n\n const appLocal = globSync(appLocalGlobs, {\n dot: true,\n absolute: true,\n }).map(normalizePath_);\n\n const additional = globSync(additionalGlobs, {\n dot: true,\n absolute: true,\n }).map(normalizePath_);\n\n for (const f of appLocal) {\n appLocalFilesCache.add(f);\n }\n\n return [...appLocal, ...additional];\n }\n\n function isAppLocalByPath(absolutePath: string): boolean {\n const normalized = normalizePath(absolutePath);\n return getAppLocalFileKind(normalized) !== null;\n }\n\n function ensureInitialized(): void {\n if (initialized) {\n return;\n }\n\n routeFilesCache = new Set(scanRouteFiles());\n contentFilesCache = new Set(scanContentFiles());\n initialized = true;\n }\n\n return {\n getRouteFiles(): string[] {\n ensureInitialized();\n return [...routeFilesCache].sort();\n },\n\n getContentFiles(): string[] {\n ensureInitialized();\n return [...contentFilesCache].sort();\n },\n\n getDiscoveredFileKind(path: string): RouteFileKind | null {\n return getDiscoveredFileKind(path);\n },\n\n updateDiscoveredFile(\n path: string,\n event: 'add' | 'change' | 'unlink',\n ): void {\n ensureInitialized();\n const kind = getDiscoveredFileKind(path);\n if (!kind || kind === 'server') {\n return;\n }\n\n const normalizedPath = normalizePath_(path);\n const targetCache =\n kind === 'content' ? contentFilesCache : routeFilesCache;\n\n if (event === 'unlink') {\n targetCache.delete(normalizedPath);\n appLocalFilesCache.delete(normalizedPath);\n return;\n }\n\n targetCache.add(normalizedPath);\n if (isAppLocalByPath(path)) {\n appLocalFilesCache.add(normalizedPath);\n }\n },\n\n isAppLocal(normalizedFilename: string): boolean {\n ensureInitialized();\n return appLocalFilesCache.has(normalizedFilename);\n },\n\n reset(): void {\n initialized = false;\n appLocalFilesCache = new Set<string>();\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -53,7 +53,8 @@ function formatAnalogRouteIdiomDiagnostic(diagnostic, filename, workspaceRoot) {
|
|
|
53
53
|
}
|
|
54
54
|
function toDisplayPath(filename, workspaceRoot) {
|
|
55
55
|
const normalizedFilename = normalizePath(filename);
|
|
56
|
-
const
|
|
56
|
+
const normalizedRoot = normalizePath(workspaceRoot);
|
|
57
|
+
const relativePath = normalizePath(relative(normalizedRoot, normalizedFilename));
|
|
57
58
|
if (relativePath && !relativePath.startsWith("..")) return `/${relativePath}`;
|
|
58
59
|
return normalizedFilename;
|
|
59
60
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"route-idiom-diagnostics.js","names":[],"sources":["../../../src/lib/route-idiom-diagnostics.ts"],"sourcesContent":["import { parseSync, type OxcError, type Severity } from 'oxc-parser';\nimport { relative } from 'node:path';\nimport { normalizePath } from 'vite';\n\nexport interface AnalogRouteIdiomDiagnostic {\n code: string;\n severity: 'error' | 'warning';\n message: string;\n details?: string;\n}\n\nexport interface AnalyzeAnalogRouteFileOptions {\n filename: string;\n code: string;\n routeFiles?: string[];\n}\n\nconst PAGE_FILE_RE = /\\.page\\.ts$/;\nconst ROUTER_OUTLET_RE = /\\bRouterOutlet\\b|<router-outlet(?:\\s|>)/;\n\nexport function analyzeAnalogRouteFile(\n options: AnalyzeAnalogRouteFileOptions,\n): AnalogRouteIdiomDiagnostic[] {\n const { filename, code, routeFiles = [] } = options;\n const parseResult = parseSync(filename, code, {\n lang: 'ts',\n sourceType: 'module',\n range: true,\n showSemanticErrors: true,\n });\n\n const parseDiagnostics = parseResult.errors.map((error) =>\n toParseDiagnostic(error),\n );\n\n if (parseDiagnostics.some((diagnostic) => diagnostic.severity === 'error')) {\n return parseDiagnostics;\n }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const program: any = parseResult.program;\n const exportedBindings = collectExportedBindings(program);\n const routeMetaBindingName = exportedBindings.routeMeta;\n const diagnostics = [...parseDiagnostics];\n\n const routeMetaInfo = routeMetaBindingName\n ? getRouteMetaInfo(program, routeMetaBindingName)\n : null;\n\n if (!exportedBindings.hasDefaultExport && !routeMetaInfo?.hasRedirect) {\n diagnostics.push({\n code: 'missing-default-export',\n severity: 'warning',\n message:\n 'Route files should default-export the page component, unless they are redirect-only routes.',\n details:\n 'Add `export default class ...` or define `routeMeta.redirectTo` for a redirect route.',\n });\n }\n\n if (exportedBindings.hasDefaultExport && routeMetaInfo?.hasRedirect) {\n diagnostics.push({\n code: 'redirect-with-component',\n severity: 'warning',\n message: 'Redirect routes should not also export a page component.',\n details:\n 'Analog ignores the default export when `routeMeta.redirectTo` is present. Remove the component export or remove the redirect.',\n });\n }\n\n if (\n routeMetaInfo?.hasRedirect &&\n routeMetaInfo.redirectTo &&\n !routeMetaInfo.redirectTo.startsWith('/')\n ) {\n diagnostics.push({\n code: 'relative-redirect',\n severity: 'warning',\n message: '`routeMeta.redirectTo` should use an absolute path.',\n details:\n 'Nested redirects are documented to use absolute targets such as `/cities/new-york`.',\n });\n }\n\n if (\n isLikelyLayoutRoute(filename, routeFiles) &&\n !ROUTER_OUTLET_RE.test(code)\n ) {\n diagnostics.push({\n code: 'layout-without-router-outlet',\n severity: 'warning',\n message:\n 'This route file looks like a layout shell, but it does not reference `RouterOutlet` or `<router-outlet>`.',\n details:\n 'Parent layout pages usually import `RouterOutlet` and render an outlet so child routes have somewhere to mount.',\n });\n }\n\n return diagnostics;\n}\n\nexport function formatAnalogRouteIdiomDiagnostic(\n diagnostic: AnalogRouteIdiomDiagnostic,\n filename: string,\n workspaceRoot: string,\n): string {\n const displayName = toDisplayPath(filename, workspaceRoot);\n const header = `[Analog] ${displayName} (${diagnostic.code})`;\n const severity = diagnostic.severity.toUpperCase();\n\n if (diagnostic.details) {\n return `${header}\\n${severity}: ${diagnostic.message}\\n${diagnostic.details}`;\n }\n\n return `${header}\\n${severity}: ${diagnostic.message}`;\n}\n\nfunction toDisplayPath(filename: string, workspaceRoot: string): string {\n const normalizedFilename = normalizePath(filename);\n const normalizedRoot = normalizePath(workspaceRoot);\n const relativePath = normalizePath(\n relative(normalizedRoot, normalizedFilename),\n );\n\n if (relativePath && !relativePath.startsWith('..')) {\n return `/${relativePath}`;\n }\n\n return normalizedFilename;\n}\n\nfunction toParseDiagnostic(error: OxcError): AnalogRouteIdiomDiagnostic {\n return {\n code: 'oxc-parse',\n severity: error.severity === severityError ? 'error' : 'warning',\n message: error.message,\n details: error.codeframe ?? error.helpMessage ?? undefined,\n };\n}\n\nconst severityError: Severity = 'Error';\n\nfunction isLikelyLayoutRoute(filename: string, routeFiles: string[]): boolean {\n if (!PAGE_FILE_RE.test(filename)) {\n return false;\n }\n\n const routeStem = filename.replace(PAGE_FILE_RE, '');\n return routeFiles.some(\n (routeFile) =>\n routeFile !== filename && routeFile.startsWith(`${routeStem}/`),\n );\n}\n\nfunction getRouteMetaInfo(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n program: any,\n bindingName: string,\n): {\n hasRedirect: boolean;\n redirectTo?: string;\n} | null {\n const routeMetaNode = getExportedBindingInitializer(program, bindingName);\n\n if (routeMetaNode?.type !== 'ObjectExpression') {\n return null;\n }\n\n let redirectTo: string | undefined;\n\n for (const property of routeMetaNode.properties ?? []) {\n if (property?.type !== 'Property') {\n continue;\n }\n\n const keyName = getPropertyName(property.key);\n if (keyName !== 'redirectTo') {\n continue;\n }\n\n redirectTo = getStringValue(property.value);\n }\n\n return {\n hasRedirect: typeof redirectTo === 'string' && redirectTo.length > 0,\n redirectTo,\n };\n}\n\nfunction getExportedBindingInitializer(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n program: any,\n bindingName: string,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n): any | undefined {\n for (const statement of program.body ?? []) {\n if (statement?.type === 'VariableDeclaration') {\n const initializer = getVariableInitializer(statement, bindingName);\n if (initializer) {\n return initializer;\n }\n continue;\n }\n\n if (\n statement?.type === 'ExportNamedDeclaration' &&\n statement.declaration?.type === 'VariableDeclaration'\n ) {\n const initializer = getVariableInitializer(\n statement.declaration,\n bindingName,\n );\n if (initializer) {\n return initializer;\n }\n }\n }\n\n return undefined;\n}\n\nfunction getVariableInitializer(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n declaration: any,\n bindingName: string,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n): any | undefined {\n for (const declarator of declaration.declarations ?? []) {\n if (\n declarator.id?.type === 'Identifier' &&\n declarator.id.name === bindingName\n ) {\n return declarator.init;\n }\n }\n\n return undefined;\n}\n\nfunction collectExportedBindings(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n program: any,\n): {\n hasDefaultExport: boolean;\n routeMeta?: string;\n} {\n let hasDefaultExport = false;\n let routeMeta: string | undefined;\n\n for (const statement of program.body ?? []) {\n if (statement?.type === 'ExportDefaultDeclaration') {\n hasDefaultExport = true;\n continue;\n }\n\n if (statement?.type !== 'ExportNamedDeclaration') {\n continue;\n }\n\n const declaration = statement.declaration;\n if (declaration?.type === 'VariableDeclaration') {\n for (const declarator of declaration.declarations ?? []) {\n if (declarator.id?.type !== 'Identifier') {\n continue;\n }\n\n if (declarator.id.name === 'routeMeta') {\n routeMeta = 'routeMeta';\n }\n }\n }\n\n for (const specifier of statement.specifiers ?? []) {\n if (\n specifier?.type !== 'ExportSpecifier' ||\n specifier.local?.type !== 'Identifier'\n ) {\n continue;\n }\n\n if (specifier.exported?.type !== 'Identifier') {\n continue;\n }\n\n if (specifier.exported.name === 'routeMeta') {\n routeMeta = specifier.local.name;\n }\n }\n }\n\n return {\n hasDefaultExport,\n routeMeta,\n };\n}\n\nfunction getPropertyName(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n node: any,\n): string | undefined {\n if (node?.type === 'Identifier') {\n return node.name;\n }\n\n if (\n (node?.type === 'Literal' || node?.type === 'StringLiteral') &&\n typeof node.value === 'string'\n ) {\n return node.value;\n }\n\n return undefined;\n}\n\nfunction getStringValue(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n node: any,\n): string | undefined {\n if (\n (node?.type === 'Literal' || node?.type === 'StringLiteral') &&\n typeof node.value === 'string'\n ) {\n return node.value;\n }\n\n if (\n node?.type === 'TemplateLiteral' &&\n node.expressions?.length === 0 &&\n node.quasis?.length === 1\n ) {\n return node.quasis[0].value.cooked ?? node.quasis[0].value.raw;\n }\n\n return undefined;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"route-idiom-diagnostics.js","names":[],"sources":["../../../src/lib/route-idiom-diagnostics.ts"],"sourcesContent":["import { parseSync, type OxcError, type Severity } from 'oxc-parser';\nimport { relative } from 'node:path';\nimport { normalizePath } from 'vite';\n\nexport interface AnalogRouteIdiomDiagnostic {\n code: string;\n severity: 'error' | 'warning';\n message: string;\n details?: string;\n}\n\nexport interface AnalyzeAnalogRouteFileOptions {\n filename: string;\n code: string;\n routeFiles?: string[];\n}\n\nconst PAGE_FILE_RE = /\\.page\\.ts$/;\nconst ROUTER_OUTLET_RE = /\\bRouterOutlet\\b|<router-outlet(?:\\s|>)/;\n\nexport function analyzeAnalogRouteFile(\n options: AnalyzeAnalogRouteFileOptions,\n): AnalogRouteIdiomDiagnostic[] {\n const { filename, code, routeFiles = [] } = options;\n const parseResult = parseSync(filename, code, {\n lang: 'ts',\n sourceType: 'module',\n range: true,\n showSemanticErrors: true,\n });\n\n const parseDiagnostics = parseResult.errors.map((error) =>\n toParseDiagnostic(error),\n );\n\n if (parseDiagnostics.some((diagnostic) => diagnostic.severity === 'error')) {\n return parseDiagnostics;\n }\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const program: any = parseResult.program;\n const exportedBindings = collectExportedBindings(program);\n const routeMetaBindingName = exportedBindings.routeMeta;\n const diagnostics = [...parseDiagnostics];\n\n const routeMetaInfo = routeMetaBindingName\n ? getRouteMetaInfo(program, routeMetaBindingName)\n : null;\n\n if (!exportedBindings.hasDefaultExport && !routeMetaInfo?.hasRedirect) {\n diagnostics.push({\n code: 'missing-default-export',\n severity: 'warning',\n message:\n 'Route files should default-export the page component, unless they are redirect-only routes.',\n details:\n 'Add `export default class ...` or define `routeMeta.redirectTo` for a redirect route.',\n });\n }\n\n if (exportedBindings.hasDefaultExport && routeMetaInfo?.hasRedirect) {\n diagnostics.push({\n code: 'redirect-with-component',\n severity: 'warning',\n message: 'Redirect routes should not also export a page component.',\n details:\n 'Analog ignores the default export when `routeMeta.redirectTo` is present. Remove the component export or remove the redirect.',\n });\n }\n\n if (\n routeMetaInfo?.hasRedirect &&\n routeMetaInfo.redirectTo &&\n !routeMetaInfo.redirectTo.startsWith('/')\n ) {\n diagnostics.push({\n code: 'relative-redirect',\n severity: 'warning',\n message: '`routeMeta.redirectTo` should use an absolute path.',\n details:\n 'Nested redirects are documented to use absolute targets such as `/cities/new-york`.',\n });\n }\n\n if (\n isLikelyLayoutRoute(filename, routeFiles) &&\n !ROUTER_OUTLET_RE.test(code)\n ) {\n diagnostics.push({\n code: 'layout-without-router-outlet',\n severity: 'warning',\n message:\n 'This route file looks like a layout shell, but it does not reference `RouterOutlet` or `<router-outlet>`.',\n details:\n 'Parent layout pages usually import `RouterOutlet` and render an outlet so child routes have somewhere to mount.',\n });\n }\n\n return diagnostics;\n}\n\nexport function formatAnalogRouteIdiomDiagnostic(\n diagnostic: AnalogRouteIdiomDiagnostic,\n filename: string,\n workspaceRoot: string,\n): string {\n const displayName = toDisplayPath(filename, workspaceRoot);\n const header = `[Analog] ${displayName} (${diagnostic.code})`;\n const severity = diagnostic.severity.toUpperCase();\n\n if (diagnostic.details) {\n return `${header}\\n${severity}: ${diagnostic.message}\\n${diagnostic.details}`;\n }\n\n return `${header}\\n${severity}: ${diagnostic.message}`;\n}\n\nfunction toDisplayPath(filename: string, workspaceRoot: string): string {\n const normalizedFilename = normalizePath(filename);\n const normalizedRoot = normalizePath(workspaceRoot);\n const relativePath = normalizePath(\n relative(normalizedRoot, normalizedFilename),\n );\n\n if (relativePath && !relativePath.startsWith('..')) {\n return `/${relativePath}`;\n }\n\n return normalizedFilename;\n}\n\nfunction toParseDiagnostic(error: OxcError): AnalogRouteIdiomDiagnostic {\n return {\n code: 'oxc-parse',\n severity: error.severity === severityError ? 'error' : 'warning',\n message: error.message,\n details: error.codeframe ?? error.helpMessage ?? undefined,\n };\n}\n\nconst severityError: Severity = 'Error';\n\nfunction isLikelyLayoutRoute(filename: string, routeFiles: string[]): boolean {\n if (!PAGE_FILE_RE.test(filename)) {\n return false;\n }\n\n const routeStem = filename.replace(PAGE_FILE_RE, '');\n return routeFiles.some(\n (routeFile) =>\n routeFile !== filename && routeFile.startsWith(`${routeStem}/`),\n );\n}\n\nfunction getRouteMetaInfo(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n program: any,\n bindingName: string,\n): {\n hasRedirect: boolean;\n redirectTo?: string;\n} | null {\n const routeMetaNode = getExportedBindingInitializer(program, bindingName);\n\n if (routeMetaNode?.type !== 'ObjectExpression') {\n return null;\n }\n\n let redirectTo: string | undefined;\n\n for (const property of routeMetaNode.properties ?? []) {\n if (property?.type !== 'Property') {\n continue;\n }\n\n const keyName = getPropertyName(property.key);\n if (keyName !== 'redirectTo') {\n continue;\n }\n\n redirectTo = getStringValue(property.value);\n }\n\n return {\n hasRedirect: typeof redirectTo === 'string' && redirectTo.length > 0,\n redirectTo,\n };\n}\n\nfunction getExportedBindingInitializer(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n program: any,\n bindingName: string,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n): any | undefined {\n for (const statement of program.body ?? []) {\n if (statement?.type === 'VariableDeclaration') {\n const initializer = getVariableInitializer(statement, bindingName);\n if (initializer) {\n return initializer;\n }\n continue;\n }\n\n if (\n statement?.type === 'ExportNamedDeclaration' &&\n statement.declaration?.type === 'VariableDeclaration'\n ) {\n const initializer = getVariableInitializer(\n statement.declaration,\n bindingName,\n );\n if (initializer) {\n return initializer;\n }\n }\n }\n\n return undefined;\n}\n\nfunction getVariableInitializer(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n declaration: any,\n bindingName: string,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n): any | undefined {\n for (const declarator of declaration.declarations ?? []) {\n if (\n declarator.id?.type === 'Identifier' &&\n declarator.id.name === bindingName\n ) {\n return declarator.init;\n }\n }\n\n return undefined;\n}\n\nfunction collectExportedBindings(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n program: any,\n): {\n hasDefaultExport: boolean;\n routeMeta?: string;\n} {\n let hasDefaultExport = false;\n let routeMeta: string | undefined;\n\n for (const statement of program.body ?? []) {\n if (statement?.type === 'ExportDefaultDeclaration') {\n hasDefaultExport = true;\n continue;\n }\n\n if (statement?.type !== 'ExportNamedDeclaration') {\n continue;\n }\n\n const declaration = statement.declaration;\n if (declaration?.type === 'VariableDeclaration') {\n for (const declarator of declaration.declarations ?? []) {\n if (declarator.id?.type !== 'Identifier') {\n continue;\n }\n\n if (declarator.id.name === 'routeMeta') {\n routeMeta = 'routeMeta';\n }\n }\n }\n\n for (const specifier of statement.specifiers ?? []) {\n if (\n specifier?.type !== 'ExportSpecifier' ||\n specifier.local?.type !== 'Identifier'\n ) {\n continue;\n }\n\n if (specifier.exported?.type !== 'Identifier') {\n continue;\n }\n\n if (specifier.exported.name === 'routeMeta') {\n routeMeta = specifier.local.name;\n }\n }\n }\n\n return {\n hasDefaultExport,\n routeMeta,\n };\n}\n\nfunction getPropertyName(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n node: any,\n): string | undefined {\n if (node?.type === 'Identifier') {\n return node.name;\n }\n\n if (\n (node?.type === 'Literal' || node?.type === 'StringLiteral') &&\n typeof node.value === 'string'\n ) {\n return node.value;\n }\n\n return undefined;\n}\n\nfunction getStringValue(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n node: any,\n): string | undefined {\n if (\n (node?.type === 'Literal' || node?.type === 'StringLiteral') &&\n typeof node.value === 'string'\n ) {\n return node.value;\n }\n\n if (\n node?.type === 'TemplateLiteral' &&\n node.expressions?.length === 0 &&\n node.quasis?.length === 1\n ) {\n return node.quasis[0].value.cooked ?? node.quasis[0].value.raw;\n }\n\n return undefined;\n}\n"],"mappings":"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"}
|
|
@@ -295,6 +295,6 @@ function computeLocalPath(fullPath, parentFullPath) {
|
|
|
295
295
|
return fullPath.slice(parentFullPath.length).replace(/^\/+/, "") || "/";
|
|
296
296
|
}
|
|
297
297
|
//#endregion
|
|
298
|
-
export { generateRouteManifest, generateRouteTableDeclaration };
|
|
298
|
+
export { extractRouteParams, filenameToRouteId, filenameToRoutePath, generateRouteManifest, generateRouteTableDeclaration };
|
|
299
299
|
|
|
300
300
|
//# sourceMappingURL=route-manifest.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"route-manifest.js","names":[],"sources":["../../../src/lib/route-manifest.ts"],"sourcesContent":["/**\n * Route-manifest engine for typed file routes.\n *\n * Pure functions (no Angular dependencies) for converting discovered\n * filenames into typed route manifests and generated declarations.\n */\n\nexport interface RouteParamInfo {\n name: string;\n type: 'dynamic' | 'catchAll' | 'optionalCatchAll';\n}\n\nexport interface RouteEntry {\n /** Stable structural route id derived from the source filename */\n id: string;\n /** The route path segment relative to the nearest existing parent route */\n path: string;\n /** The fully resolved navigation path pattern (e.g., '/users/[id]') */\n fullPath: string;\n /** Extracted parameter information */\n params: RouteParamInfo[];\n /** Original filename that produced this route */\n filename: string;\n /** Type of source that produced this route */\n kind: 'page' | 'content';\n /** Parent route id, or null for top-level routes */\n parentId: string | null;\n /** Child route ids */\n children: string[];\n /** Whether the source filename represents an index route */\n isIndex: boolean;\n /** Whether the source filename includes route-group/pathless segments */\n isGroup: boolean;\n /** Whether the route contains a required catch-all parameter */\n isCatchAll: boolean;\n /** Whether the route contains an optional catch-all parameter */\n isOptionalCatchAll: boolean;\n /** Layout files that wrap this route, outermost first */\n layouts: string[];\n}\n\nexport interface RouteCollision {\n fullPath: string;\n keptFile: string;\n droppedFile: string;\n /** True when both files have the same collision priority (hard error). */\n samePriority: boolean;\n}\n\nexport interface RouteManifest {\n routes: RouteEntry[];\n collisions: RouteCollision[];\n /** Canonical route per fullPath — precomputed once to avoid redundant work. */\n canonicalByFullPath: Map<string, RouteEntry>;\n /** Full paths defined by more than one file, such as a layout and its index page. */\n sharedFullPaths: Set<string>;\n}\n\n/**\n * Converts a discovered filename to a route path pattern.\n *\n * Uses the same stripping rules as the existing route system\n * but preserves bracket param syntax instead of converting to\n * Angular's `:param` syntax.\n *\n * The regex applies four alternations (left to right, all replaced with ''):\n * 1. `^(.*?)[\\\\/](?:routes|pages|content)[\\\\/]` — anchored, strips everything\n * up to and including the first /routes/, /pages/, or /content/ segment.\n * Handles app-local paths (`/src/app/pages/`) AND additional dirs\n * (`/libs/shared/feature/src/content/`) uniformly.\n * 2. `[\\\\/](?:app[\\\\/]pages|src[\\\\/]content)[\\\\/]` — non-anchored\n * fallback for paths where the directory marker appears mid-string.\n * 3. `\\.page\\.(js|ts)$` — strips page file extensions.\n * 4. `\\.(ts|md)$` — strips remaining file extensions.\n *\n * Examples:\n * - '/src/app/pages/index.page.ts' -> '/'\n * - '/src/app/pages/about.page.ts' -> '/about'\n * - '/src/app/pages/users/[id].page.ts' -> '/users/[id]'\n * - '/src/app/pages/blog.[slug].page.ts' -> '/blog/[slug]'\n * - '/src/app/pages/(auth)/login.page.ts' -> '/login'\n * - '/src/app/pages/docs/[...slug].page.ts' -> '/docs/[...slug]'\n * - '/src/app/pages/shop/[[...category]].page.ts' -> '/shop/[[...category]]'\n * - '/libs/shared/feature/src/content/test.md' -> '/test'\n */\nexport function filenameToRoutePath(filename: string): string {\n let path = filename.replace(\n /^(?:[a-zA-Z]:[\\\\/])?(.*?)[\\\\/](?:routes|pages|content)[\\\\/]|(?:[\\\\/](?:app[\\\\/]pages|src[\\\\/]content)[\\\\/])|(\\.page\\.(js|ts)$)|(\\.(ts|md)$)/g,\n '',\n );\n\n const brackets: string[] = [];\n path = path.replace(/\\[\\[?\\.{0,3}[^\\]]*\\]?\\]/g, (match) => {\n brackets.push(match);\n // eslint-disable-next-line no-control-regex\n return `\\0B${brackets.length - 1}\\0`;\n });\n // Match the runtime router: strip index segments and groups before dots.\n path = path\n .replace(/\\(.*?\\)/g, '')\n .replace(/(^|[./])index(?=[./]|$)/g, '$1')\n .replace(/\\./g, '/');\n // eslint-disable-next-line no-control-regex\n path = path.replace(/\\0B(\\d+)\\0/g, (_, idx) => brackets[Number(idx)]);\n return '/' + path.split('/').filter(Boolean).join('/');\n}\n\n/**\n * Converts a discovered filename to a stable structural route id.\n *\n * Unlike `filenameToRoutePath`, this preserves route groups and `index`\n * segments so that multiple files resolving to the same URL shape can still\n * have distinct structural identities in the generated route tree metadata.\n *\n * Uses the same directory-stripping regex as `filenameToRoutePath` —\n * changes to the regex must be kept in sync between both functions.\n */\nexport function filenameToRouteId(filename: string): string {\n let path = filename.replace(\n /^(?:[a-zA-Z]:[\\\\/])?(.*?)[\\\\/](?:routes|pages|content)[\\\\/]|(?:[\\\\/](?:app[\\\\/]pages|src[\\\\/]content)[\\\\/])|(\\.page\\.(js|ts)$)|(\\.(ts|md)$)/g,\n '',\n );\n\n const brackets: string[] = [];\n path = path.replace(/\\(.*?\\)|\\[\\[?\\.{0,3}[^\\]]*\\]?\\]/g, (match) => {\n brackets.push(match);\n // eslint-disable-next-line no-control-regex\n return `\\0B${brackets.length - 1}\\0`;\n });\n path = path.replace(/\\./g, '/');\n // eslint-disable-next-line no-control-regex\n path = path.replace(/\\0B(\\d+)\\0/g, (_, idx) => brackets[Number(idx)]);\n\n const segments = path.split('/').filter(Boolean);\n\n return '/' + segments.join('/');\n}\n\n/**\n * Extracts parameter information from a route path pattern.\n */\nexport function extractRouteParams(routePath: string): RouteParamInfo[] {\n const params: RouteParamInfo[] = [];\n\n for (const match of routePath.matchAll(/\\[\\[\\.\\.\\.([^\\]]+)\\]\\]/g)) {\n params.push({ name: match[1], type: 'optionalCatchAll' });\n }\n for (const match of routePath.matchAll(/(?<!\\[)\\[\\.\\.\\.([^\\]]+)\\](?!\\])/g)) {\n params.push({ name: match[1], type: 'catchAll' });\n }\n for (const match of routePath.matchAll(/(?<!\\[)\\[(?!\\.)([^\\]]+)\\](?!\\])/g)) {\n params.push({ name: match[1], type: 'dynamic' });\n }\n\n return params;\n}\n\n/**\n * Generates a route manifest from a list of discovered filenames.\n *\n * @param collisionPriority - Optional callback that returns a numeric priority\n * for each filename (lower wins). When provided, this replaces the default\n * hard-coded path-substring heuristic with config-derived precedence.\n */\nexport function generateRouteManifest(\n filenames: string[],\n collisionPriority?: (filename: string) => number,\n): RouteManifest {\n const routes: RouteEntry[] = [];\n const collisions: RouteCollision[] = [];\n const sharedFullPaths = new Set<string>();\n const seenByFullPath = new Map<\n string,\n { filename: string; priority: number }\n >();\n const getPriority = collisionPriority ?? getCollisionPriority;\n\n // Prefer app-local route files over shared/external sources when two files\n // resolve to the same URL. This keeps `additionalPagesDirs` additive instead\n // of unexpectedly overriding the route that lives inside the app itself.\n const prioritizedFilenames = [...filenames].sort((a, b) => {\n const aPriority = getPriority(a);\n const bPriority = getPriority(b);\n if (aPriority !== bPriority) {\n return aPriority - bPriority;\n }\n return a.localeCompare(b);\n });\n\n // Layout routes are a file and a folder with the same name, so users.page.ts\n // wraps users/[id].page.ts but not users.[id].page.ts. Folders from all\n // pages dirs share one tree.\n const layoutByKey = new Map<string, string>();\n for (const filename of prioritizedFilenames) {\n const key = toRouteFileKey(filename);\n if (!filename.endsWith('.md') && !layoutByKey.has(key)) {\n layoutByKey.set(key, filename);\n }\n }\n\n for (const filename of prioritizedFilenames) {\n const fullPath = filenameToRoutePath(filename);\n const params = extractRouteParams(fullPath);\n const id = filenameToRouteId(filename);\n const isPathlessLayout = isPathlessLayoutId(id);\n // Different pathless branches can select the same URL with canMatch guards.\n const groupScope = id.slice(0, id.lastIndexOf(')') + 1);\n const collisionKey = JSON.stringify([fullPath, groupScope]);\n\n const currentPriority = getPriority(filename);\n\n // Pathless layouts (e.g. (auth).page.ts) are structural wrappers that\n // render a <router-outlet> — they coexist with index.page.ts at the same\n // fullPath without collision. The Angular router handles them as nested\n // layout routes, not competing page components.\n if (!isPathlessLayout) {\n if (seenByFullPath.has(collisionKey)) {\n const winner = seenByFullPath.get(collisionKey)!;\n if (winner.filename === filename) {\n continue;\n }\n // A layout file (e.g., docs.page.ts) and its index child\n // (e.g., docs/index.page.ts) intentionally share the same route\n // path — the layout wraps the index as a parent-child pair.\n const isLayoutIndexPair = (a: string, b: string) => {\n const indexRe = /\\/index\\.(page\\.)?(ts|js|md)$/;\n const layoutRe = /\\.(page\\.)?(ts|js)$/;\n if (indexRe.test(a) && layoutRe.test(b)) {\n const dir = a.replace(indexRe, '');\n const layout = b.replace(layoutRe, '');\n return dir === layout;\n }\n return false;\n };\n if (\n isLayoutIndexPair(winner.filename, filename) ||\n isLayoutIndexPair(filename, winner.filename)\n ) {\n sharedFullPaths.add(fullPath);\n continue;\n }\n collisions.push({\n fullPath,\n keptFile: winner.filename,\n droppedFile: filename,\n samePriority: winner.priority === currentPriority,\n });\n console.warn(\n `[Analog] Route collision: '${fullPath}' is defined by both ` +\n `'${winner.filename}' and '${filename}'. ` +\n `Keeping '${winner.filename}' based on route source precedence and skipping duplicate.`,\n );\n continue;\n }\n seenByFullPath.set(collisionKey, { filename, priority: currentPriority });\n }\n\n const segments = toRouteFileKey(filename).split('/');\n const layouts: string[] = [];\n for (let i = 1; i < segments.length; i++) {\n const layout = layoutByKey.get(segments.slice(0, i).join('/'));\n if (layout) layouts.push(layout);\n }\n\n routes.push({\n id,\n path: fullPath,\n fullPath,\n params,\n filename,\n kind: filename.endsWith('.md') ? 'content' : 'page',\n parentId: null,\n children: [],\n isIndex: id === '/index' || id.endsWith('/index'),\n isGroup: isPathlessLayout,\n isCatchAll: params.some((param) => param.type === 'catchAll'),\n isOptionalCatchAll: params.some(\n (param) => param.type === 'optionalCatchAll',\n ),\n layouts,\n });\n }\n\n routes.sort((a, b) => {\n const aW = getRouteWeight(a.fullPath);\n const bW = getRouteWeight(b.fullPath);\n if (aW !== bW) return aW - bW;\n return a.fullPath.localeCompare(b.fullPath);\n });\n\n const routeByFullPath = canonicalRoutesByFullPath(routes);\n\n const routeById = new Map(routes.map((route) => [route.id, route]));\n\n for (const route of routes) {\n // Use structural id-based parent lookup for any route whose id\n // contains a group segment — this wires group children (e.g.\n // /(auth)/sign-up) to their pathless layout parent (/(auth)).\n // This also correctly handles nested groups like\n // /dashboard/(settings)/profile: findNearestParentById walks up\n // id segments and finds /(settings) if it exists, otherwise falls\n // through to fullPathParent which resolves to /dashboard.\n // Non-group routes always use the canonical fullPath-based lookup.\n const hasGroupSegment = route.id.includes('/(');\n const structuralParent = hasGroupSegment\n ? findNearestParentById(route.id, routeById)\n : undefined;\n const fullPathParent = findNearestParentRoute(\n route.fullPath,\n routeByFullPath,\n );\n const parent = structuralParent ?? fullPathParent;\n route.parentId = parent?.id ?? null;\n route.path = computeLocalPath(route.fullPath, parent?.fullPath ?? null);\n }\n\n for (const route of routes) {\n if (route.parentId) {\n routeById.get(route.parentId)?.children.push(route.id);\n }\n if (routeByFullPath.get(route.fullPath) !== route) {\n sharedFullPaths.add(route.fullPath);\n }\n }\n\n return {\n routes,\n collisions,\n canonicalByFullPath: routeByFullPath,\n sharedFullPaths,\n };\n}\n\nfunction canonicalRoutesByFullPath(\n routes: RouteEntry[],\n): Map<string, RouteEntry> {\n const map = new Map<string, RouteEntry>();\n for (const route of routes) {\n const existing = map.get(route.fullPath);\n if (!existing) {\n map.set(route.fullPath, route);\n } else if (existing.isGroup && !route.isGroup) {\n // Non-group routes always take precedence over group layouts.\n map.set(route.fullPath, route);\n } else if (existing.isGroup && route.isGroup) {\n // Both are group layouts — tiebreak by id to ensure stable selection\n // regardless of filesystem or glob ordering across platforms.\n if (route.id.localeCompare(existing.id) < 0) {\n map.set(route.fullPath, route);\n }\n }\n }\n return map;\n}\n\n/** A route file's path within its pages directory, e.g. `users/[id]`. */\nfunction toRouteFileKey(filename: string): string {\n return filename.replace(\n /^.*?[\\\\/](?:routes|pages|content)[\\\\/]|\\.page\\.(?:js|ts)$|\\.(?:ts|md)$/g,\n '',\n );\n}\n\nfunction isPathlessLayoutId(id: string): boolean {\n const segments = id.split('/').filter(Boolean);\n if (segments.length === 0) return false;\n return /^\\([^)]*\\)$/.test(segments[segments.length - 1]);\n}\n\nfunction getRouteWeight(path: string): number {\n if (path.includes('[[...')) return 3;\n if (path.includes('[...')) return 2;\n if (path.includes('[')) return 1;\n return 0;\n}\n\nfunction getCollisionPriority(filename: string): number {\n if (\n filename.includes('/src/app/pages/') ||\n filename.includes('/app/pages/') ||\n filename.includes('/src/content/')\n ) {\n return 0;\n }\n\n return 1;\n}\n\n/** Import specifiers for a page module's typed exports. */\nexport interface RouteModuleImports {\n routeMeta?: string;\n load?: string;\n}\n\n/**\n * Generates the route-table section for the combined generated route module.\n *\n * `resolveModuleImports` adds `routeMeta` and `load` types for pages whose\n * URL is defined by a single file, along with the `routeMeta` of the layouts\n * that wrap them. URLs shared by several files, such as a layout and its\n * index page, are ambiguous and only get params and query types.\n */\nexport function generateRouteTableDeclaration(\n manifest: RouteManifest,\n resolveModuleImports?: (filename: string) => RouteModuleImports | undefined,\n): string {\n const lines: string[] = [];\n lines.push('/** @noprettier */');\n lines.push('// This file is auto-generated by @analogjs/platform');\n lines.push(\"import type {} from '@analogjs/router';\");\n lines.push(\"declare module '@analogjs/router' {\");\n lines.push(' interface AnalogRouteTable {');\n\n const importType = (specifier: string, name: string) =>\n `typeof import(${JSON.stringify(specifier)}).${name}`;\n\n for (const route of manifest.canonicalByFullPath.values()) {\n const paramsType = generateParamsType(route.params);\n const queryType = 'Record<string, string | string[] | undefined>';\n\n lines.push(` ${JSON.stringify(route.fullPath)}: {`);\n lines.push(` params: ${paramsType};`);\n lines.push(` query: ${queryType};`);\n if (\n resolveModuleImports &&\n route.kind === 'page' &&\n !manifest.sharedFullPaths.has(route.fullPath)\n ) {\n const layoutRouteMeta = route.layouts.flatMap((layout) => {\n const specifier = resolveModuleImports(layout)?.routeMeta;\n return specifier ? [importType(specifier, 'routeMeta')] : [];\n });\n const imports = resolveModuleImports(route.filename);\n if (layoutRouteMeta.length) {\n lines.push(` layoutRouteMeta: [${layoutRouteMeta.join(', ')}];`);\n }\n if (imports?.routeMeta) {\n lines.push(\n ` routeMeta: ${importType(imports.routeMeta, 'routeMeta')};`,\n );\n }\n if (imports?.load) {\n lines.push(` load: ${importType(imports.load, 'load')};`);\n }\n }\n lines.push(` };`);\n }\n\n lines.push(' }');\n lines.push('}');\n lines.push('');\n lines.push('export {};');\n lines.push('');\n\n return lines.join('\\n');\n}\n\nfunction generateParamsType(params: RouteParamInfo[]): string {\n if (params.length === 0) return 'Record<string, never>';\n\n const entries = params.map((p) => {\n const key = isValidIdentifier(p.name) ? p.name : JSON.stringify(p.name);\n switch (p.type) {\n case 'dynamic':\n return `${key}: string`;\n case 'catchAll':\n return `${key}: string[]`;\n case 'optionalCatchAll':\n return `${key}?: string[]`;\n }\n });\n\n return `{ ${entries.join('; ')} }`;\n}\n\nfunction isValidIdentifier(name: string): boolean {\n return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);\n}\n\nfunction findNearestParentById(\n id: string,\n routesById: Map<string, RouteEntry>,\n): RouteEntry | undefined {\n if (id === '/') {\n return undefined;\n }\n\n const segments = id.split('/').filter(Boolean);\n for (let index = segments.length - 1; index > 0; index--) {\n const candidate = '/' + segments.slice(0, index).join('/');\n const route = routesById.get(candidate);\n if (route) {\n return route;\n }\n }\n\n return undefined;\n}\n\nfunction findNearestParentRoute(\n fullPath: string,\n routesByFullPath: Map<string, RouteEntry>,\n): RouteEntry | undefined {\n if (fullPath === '/') {\n return undefined;\n }\n\n const segments = fullPath.slice(1).split('/');\n for (let index = segments.length - 1; index > 0; index--) {\n const candidate = '/' + segments.slice(0, index).join('/');\n const route = routesByFullPath.get(candidate);\n if (route) {\n return route;\n }\n }\n\n return undefined;\n}\n\nfunction computeLocalPath(\n fullPath: string,\n parentFullPath: string | null,\n): string {\n if (fullPath === '/') {\n return '/';\n }\n\n if (!parentFullPath) {\n return fullPath.slice(1);\n }\n\n const suffix = fullPath.slice(parentFullPath.length).replace(/^\\/+/, '');\n return suffix || '/';\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"route-manifest.js","names":[],"sources":["../../../src/lib/route-manifest.ts"],"sourcesContent":["/**\n * Route-manifest engine for typed file routes.\n *\n * Pure functions (no Angular dependencies) for converting discovered\n * filenames into typed route manifests and generated declarations.\n */\n\nexport interface RouteParamInfo {\n name: string;\n type: 'dynamic' | 'catchAll' | 'optionalCatchAll';\n}\n\nexport interface RouteEntry {\n /** Stable structural route id derived from the source filename */\n id: string;\n /** The route path segment relative to the nearest existing parent route */\n path: string;\n /** The fully resolved navigation path pattern (e.g., '/users/[id]') */\n fullPath: string;\n /** Extracted parameter information */\n params: RouteParamInfo[];\n /** Original filename that produced this route */\n filename: string;\n /** Type of source that produced this route */\n kind: 'page' | 'content';\n /** Parent route id, or null for top-level routes */\n parentId: string | null;\n /** Child route ids */\n children: string[];\n /** Whether the source filename represents an index route */\n isIndex: boolean;\n /** Whether the source filename includes route-group/pathless segments */\n isGroup: boolean;\n /** Whether the route contains a required catch-all parameter */\n isCatchAll: boolean;\n /** Whether the route contains an optional catch-all parameter */\n isOptionalCatchAll: boolean;\n /** Layout files that wrap this route, outermost first */\n layouts: string[];\n}\n\nexport interface RouteCollision {\n fullPath: string;\n keptFile: string;\n droppedFile: string;\n /** True when both files have the same collision priority (hard error). */\n samePriority: boolean;\n}\n\nexport interface RouteManifest {\n routes: RouteEntry[];\n collisions: RouteCollision[];\n /** Canonical route per fullPath — precomputed once to avoid redundant work. */\n canonicalByFullPath: Map<string, RouteEntry>;\n /** Full paths defined by more than one file, such as a layout and its index page. */\n sharedFullPaths: Set<string>;\n}\n\n/**\n * Converts a discovered filename to a route path pattern.\n *\n * Uses the same stripping rules as the existing route system\n * but preserves bracket param syntax instead of converting to\n * Angular's `:param` syntax.\n *\n * The regex applies four alternations (left to right, all replaced with ''):\n * 1. `^(.*?)[\\\\/](?:routes|pages|content)[\\\\/]` — anchored, strips everything\n * up to and including the first /routes/, /pages/, or /content/ segment.\n * Handles app-local paths (`/src/app/pages/`) AND additional dirs\n * (`/libs/shared/feature/src/content/`) uniformly.\n * 2. `[\\\\/](?:app[\\\\/]pages|src[\\\\/]content)[\\\\/]` — non-anchored\n * fallback for paths where the directory marker appears mid-string.\n * 3. `\\.page\\.(js|ts)$` — strips page file extensions.\n * 4. `\\.(ts|md)$` — strips remaining file extensions.\n *\n * Examples:\n * - '/src/app/pages/index.page.ts' -> '/'\n * - '/src/app/pages/about.page.ts' -> '/about'\n * - '/src/app/pages/users/[id].page.ts' -> '/users/[id]'\n * - '/src/app/pages/blog.[slug].page.ts' -> '/blog/[slug]'\n * - '/src/app/pages/(auth)/login.page.ts' -> '/login'\n * - '/src/app/pages/docs/[...slug].page.ts' -> '/docs/[...slug]'\n * - '/src/app/pages/shop/[[...category]].page.ts' -> '/shop/[[...category]]'\n * - '/libs/shared/feature/src/content/test.md' -> '/test'\n */\nexport function filenameToRoutePath(filename: string): string {\n let path = filename.replace(\n /^(?:[a-zA-Z]:[\\\\/])?(.*?)[\\\\/](?:routes|pages|content)[\\\\/]|(?:[\\\\/](?:app[\\\\/]pages|src[\\\\/]content)[\\\\/])|(\\.page\\.(js|ts)$)|(\\.(ts|md)$)/g,\n '',\n );\n\n const brackets: string[] = [];\n path = path.replace(/\\[\\[?\\.{0,3}[^\\]]*\\]?\\]/g, (match) => {\n brackets.push(match);\n // eslint-disable-next-line no-control-regex\n return `\\0B${brackets.length - 1}\\0`;\n });\n // Match the runtime router: strip index segments and groups before dots.\n path = path\n .replace(/\\(.*?\\)/g, '')\n .replace(/(^|[./])index(?=[./]|$)/g, '$1')\n .replace(/\\./g, '/');\n // eslint-disable-next-line no-control-regex\n path = path.replace(/\\0B(\\d+)\\0/g, (_, idx) => brackets[Number(idx)]);\n return '/' + path.split('/').filter(Boolean).join('/');\n}\n\n/**\n * Converts a discovered filename to a stable structural route id.\n *\n * Unlike `filenameToRoutePath`, this preserves route groups and `index`\n * segments so that multiple files resolving to the same URL shape can still\n * have distinct structural identities in the generated route tree metadata.\n *\n * Uses the same directory-stripping regex as `filenameToRoutePath` —\n * changes to the regex must be kept in sync between both functions.\n */\nexport function filenameToRouteId(filename: string): string {\n let path = filename.replace(\n /^(?:[a-zA-Z]:[\\\\/])?(.*?)[\\\\/](?:routes|pages|content)[\\\\/]|(?:[\\\\/](?:app[\\\\/]pages|src[\\\\/]content)[\\\\/])|(\\.page\\.(js|ts)$)|(\\.(ts|md)$)/g,\n '',\n );\n\n const brackets: string[] = [];\n path = path.replace(/\\(.*?\\)|\\[\\[?\\.{0,3}[^\\]]*\\]?\\]/g, (match) => {\n brackets.push(match);\n // eslint-disable-next-line no-control-regex\n return `\\0B${brackets.length - 1}\\0`;\n });\n path = path.replace(/\\./g, '/');\n // eslint-disable-next-line no-control-regex\n path = path.replace(/\\0B(\\d+)\\0/g, (_, idx) => brackets[Number(idx)]);\n\n const segments = path.split('/').filter(Boolean);\n\n return '/' + segments.join('/');\n}\n\n/**\n * Extracts parameter information from a route path pattern.\n */\nexport function extractRouteParams(routePath: string): RouteParamInfo[] {\n const params: RouteParamInfo[] = [];\n\n for (const match of routePath.matchAll(/\\[\\[\\.\\.\\.([^\\]]+)\\]\\]/g)) {\n params.push({ name: match[1], type: 'optionalCatchAll' });\n }\n for (const match of routePath.matchAll(/(?<!\\[)\\[\\.\\.\\.([^\\]]+)\\](?!\\])/g)) {\n params.push({ name: match[1], type: 'catchAll' });\n }\n for (const match of routePath.matchAll(/(?<!\\[)\\[(?!\\.)([^\\]]+)\\](?!\\])/g)) {\n params.push({ name: match[1], type: 'dynamic' });\n }\n\n return params;\n}\n\n/**\n * Generates a route manifest from a list of discovered filenames.\n *\n * @param collisionPriority - Optional callback that returns a numeric priority\n * for each filename (lower wins). When provided, this replaces the default\n * hard-coded path-substring heuristic with config-derived precedence.\n */\nexport function generateRouteManifest(\n filenames: string[],\n collisionPriority?: (filename: string) => number,\n): RouteManifest {\n const routes: RouteEntry[] = [];\n const collisions: RouteCollision[] = [];\n const sharedFullPaths = new Set<string>();\n const seenByFullPath = new Map<\n string,\n { filename: string; priority: number }\n >();\n const getPriority = collisionPriority ?? getCollisionPriority;\n\n // Prefer app-local route files over shared/external sources when two files\n // resolve to the same URL. This keeps `additionalPagesDirs` additive instead\n // of unexpectedly overriding the route that lives inside the app itself.\n const prioritizedFilenames = [...filenames].sort((a, b) => {\n const aPriority = getPriority(a);\n const bPriority = getPriority(b);\n if (aPriority !== bPriority) {\n return aPriority - bPriority;\n }\n return a.localeCompare(b);\n });\n\n // Layout routes are a file and a folder with the same name, so users.page.ts\n // wraps users/[id].page.ts but not users.[id].page.ts. Folders from all\n // pages dirs share one tree.\n const layoutByKey = new Map<string, string>();\n for (const filename of prioritizedFilenames) {\n const key = toRouteFileKey(filename);\n if (!filename.endsWith('.md') && !layoutByKey.has(key)) {\n layoutByKey.set(key, filename);\n }\n }\n\n for (const filename of prioritizedFilenames) {\n const fullPath = filenameToRoutePath(filename);\n const params = extractRouteParams(fullPath);\n const id = filenameToRouteId(filename);\n const isPathlessLayout = isPathlessLayoutId(id);\n // Different pathless branches can select the same URL with canMatch guards.\n const groupScope = id.slice(0, id.lastIndexOf(')') + 1);\n const collisionKey = JSON.stringify([fullPath, groupScope]);\n\n const currentPriority = getPriority(filename);\n\n // Pathless layouts (e.g. (auth).page.ts) are structural wrappers that\n // render a <router-outlet> — they coexist with index.page.ts at the same\n // fullPath without collision. The Angular router handles them as nested\n // layout routes, not competing page components.\n if (!isPathlessLayout) {\n if (seenByFullPath.has(collisionKey)) {\n const winner = seenByFullPath.get(collisionKey)!;\n if (winner.filename === filename) {\n continue;\n }\n // A layout file (e.g., docs.page.ts) and its index child\n // (e.g., docs/index.page.ts) intentionally share the same route\n // path — the layout wraps the index as a parent-child pair.\n const isLayoutIndexPair = (a: string, b: string) => {\n const indexRe = /\\/index\\.(page\\.)?(ts|js|md)$/;\n const layoutRe = /\\.(page\\.)?(ts|js)$/;\n if (indexRe.test(a) && layoutRe.test(b)) {\n const dir = a.replace(indexRe, '');\n const layout = b.replace(layoutRe, '');\n return dir === layout;\n }\n return false;\n };\n if (\n isLayoutIndexPair(winner.filename, filename) ||\n isLayoutIndexPair(filename, winner.filename)\n ) {\n sharedFullPaths.add(fullPath);\n continue;\n }\n collisions.push({\n fullPath,\n keptFile: winner.filename,\n droppedFile: filename,\n samePriority: winner.priority === currentPriority,\n });\n console.warn(\n `[Analog] Route collision: '${fullPath}' is defined by both ` +\n `'${winner.filename}' and '${filename}'. ` +\n `Keeping '${winner.filename}' based on route source precedence and skipping duplicate.`,\n );\n continue;\n }\n seenByFullPath.set(collisionKey, { filename, priority: currentPriority });\n }\n\n const segments = toRouteFileKey(filename).split('/');\n const layouts: string[] = [];\n for (let i = 1; i < segments.length; i++) {\n const layout = layoutByKey.get(segments.slice(0, i).join('/'));\n if (layout) layouts.push(layout);\n }\n\n routes.push({\n id,\n path: fullPath,\n fullPath,\n params,\n filename,\n kind: filename.endsWith('.md') ? 'content' : 'page',\n parentId: null,\n children: [],\n isIndex: id === '/index' || id.endsWith('/index'),\n isGroup: isPathlessLayout,\n isCatchAll: params.some((param) => param.type === 'catchAll'),\n isOptionalCatchAll: params.some(\n (param) => param.type === 'optionalCatchAll',\n ),\n layouts,\n });\n }\n\n routes.sort((a, b) => {\n const aW = getRouteWeight(a.fullPath);\n const bW = getRouteWeight(b.fullPath);\n if (aW !== bW) return aW - bW;\n return a.fullPath.localeCompare(b.fullPath);\n });\n\n const routeByFullPath = canonicalRoutesByFullPath(routes);\n\n const routeById = new Map(routes.map((route) => [route.id, route]));\n\n for (const route of routes) {\n // Use structural id-based parent lookup for any route whose id\n // contains a group segment — this wires group children (e.g.\n // /(auth)/sign-up) to their pathless layout parent (/(auth)).\n // This also correctly handles nested groups like\n // /dashboard/(settings)/profile: findNearestParentById walks up\n // id segments and finds /(settings) if it exists, otherwise falls\n // through to fullPathParent which resolves to /dashboard.\n // Non-group routes always use the canonical fullPath-based lookup.\n const hasGroupSegment = route.id.includes('/(');\n const structuralParent = hasGroupSegment\n ? findNearestParentById(route.id, routeById)\n : undefined;\n const fullPathParent = findNearestParentRoute(\n route.fullPath,\n routeByFullPath,\n );\n const parent = structuralParent ?? fullPathParent;\n route.parentId = parent?.id ?? null;\n route.path = computeLocalPath(route.fullPath, parent?.fullPath ?? null);\n }\n\n for (const route of routes) {\n if (route.parentId) {\n routeById.get(route.parentId)?.children.push(route.id);\n }\n if (routeByFullPath.get(route.fullPath) !== route) {\n sharedFullPaths.add(route.fullPath);\n }\n }\n\n return {\n routes,\n collisions,\n canonicalByFullPath: routeByFullPath,\n sharedFullPaths,\n };\n}\n\nfunction canonicalRoutesByFullPath(\n routes: RouteEntry[],\n): Map<string, RouteEntry> {\n const map = new Map<string, RouteEntry>();\n for (const route of routes) {\n const existing = map.get(route.fullPath);\n if (!existing) {\n map.set(route.fullPath, route);\n } else if (existing.isGroup && !route.isGroup) {\n // Non-group routes always take precedence over group layouts.\n map.set(route.fullPath, route);\n } else if (existing.isGroup && route.isGroup) {\n // Both are group layouts — tiebreak by id to ensure stable selection\n // regardless of filesystem or glob ordering across platforms.\n if (route.id.localeCompare(existing.id) < 0) {\n map.set(route.fullPath, route);\n }\n }\n }\n return map;\n}\n\n/** A route file's path within its pages directory, e.g. `users/[id]`. */\nfunction toRouteFileKey(filename: string): string {\n return filename.replace(\n /^.*?[\\\\/](?:routes|pages|content)[\\\\/]|\\.page\\.(?:js|ts)$|\\.(?:ts|md)$/g,\n '',\n );\n}\n\nfunction isPathlessLayoutId(id: string): boolean {\n const segments = id.split('/').filter(Boolean);\n if (segments.length === 0) return false;\n return /^\\([^)]*\\)$/.test(segments[segments.length - 1]);\n}\n\nfunction getRouteWeight(path: string): number {\n if (path.includes('[[...')) return 3;\n if (path.includes('[...')) return 2;\n if (path.includes('[')) return 1;\n return 0;\n}\n\nfunction getCollisionPriority(filename: string): number {\n if (\n filename.includes('/src/app/pages/') ||\n filename.includes('/app/pages/') ||\n filename.includes('/src/content/')\n ) {\n return 0;\n }\n\n return 1;\n}\n\n/** Import specifiers for a page module's typed exports. */\nexport interface RouteModuleImports {\n routeMeta?: string;\n load?: string;\n}\n\n/**\n * Generates the route-table section for the combined generated route module.\n *\n * `resolveModuleImports` adds `routeMeta` and `load` types for pages whose\n * URL is defined by a single file, along with the `routeMeta` of the layouts\n * that wrap them. URLs shared by several files, such as a layout and its\n * index page, are ambiguous and only get params and query types.\n */\nexport function generateRouteTableDeclaration(\n manifest: RouteManifest,\n resolveModuleImports?: (filename: string) => RouteModuleImports | undefined,\n): string {\n const lines: string[] = [];\n lines.push('/** @noprettier */');\n lines.push('// This file is auto-generated by @analogjs/platform');\n lines.push(\"import type {} from '@analogjs/router';\");\n lines.push(\"declare module '@analogjs/router' {\");\n lines.push(' interface AnalogRouteTable {');\n\n const importType = (specifier: string, name: string) =>\n `typeof import(${JSON.stringify(specifier)}).${name}`;\n\n for (const route of manifest.canonicalByFullPath.values()) {\n const paramsType = generateParamsType(route.params);\n const queryType = 'Record<string, string | string[] | undefined>';\n\n lines.push(` ${JSON.stringify(route.fullPath)}: {`);\n lines.push(` params: ${paramsType};`);\n lines.push(` query: ${queryType};`);\n if (\n resolveModuleImports &&\n route.kind === 'page' &&\n !manifest.sharedFullPaths.has(route.fullPath)\n ) {\n const layoutRouteMeta = route.layouts.flatMap((layout) => {\n const specifier = resolveModuleImports(layout)?.routeMeta;\n return specifier ? [importType(specifier, 'routeMeta')] : [];\n });\n const imports = resolveModuleImports(route.filename);\n if (layoutRouteMeta.length) {\n lines.push(` layoutRouteMeta: [${layoutRouteMeta.join(', ')}];`);\n }\n if (imports?.routeMeta) {\n lines.push(\n ` routeMeta: ${importType(imports.routeMeta, 'routeMeta')};`,\n );\n }\n if (imports?.load) {\n lines.push(` load: ${importType(imports.load, 'load')};`);\n }\n }\n lines.push(` };`);\n }\n\n lines.push(' }');\n lines.push('}');\n lines.push('');\n lines.push('export {};');\n lines.push('');\n\n return lines.join('\\n');\n}\n\nfunction generateParamsType(params: RouteParamInfo[]): string {\n if (params.length === 0) return 'Record<string, never>';\n\n const entries = params.map((p) => {\n const key = isValidIdentifier(p.name) ? p.name : JSON.stringify(p.name);\n switch (p.type) {\n case 'dynamic':\n return `${key}: string`;\n case 'catchAll':\n return `${key}: string[]`;\n case 'optionalCatchAll':\n return `${key}?: string[]`;\n }\n });\n\n return `{ ${entries.join('; ')} }`;\n}\n\nfunction isValidIdentifier(name: string): boolean {\n return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name);\n}\n\nfunction findNearestParentById(\n id: string,\n routesById: Map<string, RouteEntry>,\n): RouteEntry | undefined {\n if (id === '/') {\n return undefined;\n }\n\n const segments = id.split('/').filter(Boolean);\n for (let index = segments.length - 1; index > 0; index--) {\n const candidate = '/' + segments.slice(0, index).join('/');\n const route = routesById.get(candidate);\n if (route) {\n return route;\n }\n }\n\n return undefined;\n}\n\nfunction findNearestParentRoute(\n fullPath: string,\n routesByFullPath: Map<string, RouteEntry>,\n): RouteEntry | undefined {\n if (fullPath === '/') {\n return undefined;\n }\n\n const segments = fullPath.slice(1).split('/');\n for (let index = segments.length - 1; index > 0; index--) {\n const candidate = '/' + segments.slice(0, index).join('/');\n const route = routesByFullPath.get(candidate);\n if (route) {\n return route;\n }\n }\n\n return undefined;\n}\n\nfunction computeLocalPath(\n fullPath: string,\n parentFullPath: string | null,\n): string {\n if (fullPath === '/') {\n return '/';\n }\n\n if (!parentFullPath) {\n return fullPath.slice(1);\n }\n\n const suffix = fullPath.slice(parentFullPath.length).replace(/^\\/+/, '');\n return suffix || '/';\n}\n"],"mappings":"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"}
|
package/src/lib/router-plugin.js
CHANGED
|
@@ -86,10 +86,12 @@ function routerPlugin(options) {
|
|
|
86
86
|
const reportRouteDiagnostics = (path) => {
|
|
87
87
|
if (!path.endsWith(".page.ts")) return;
|
|
88
88
|
try {
|
|
89
|
+
const code = readFileSync(path, "utf-8");
|
|
90
|
+
const routeFiles = discoverPageRouteFiles();
|
|
89
91
|
const rendered = analyzeAnalogRouteFile({
|
|
90
92
|
filename: path,
|
|
91
|
-
code
|
|
92
|
-
routeFiles
|
|
93
|
+
code,
|
|
94
|
+
routeFiles
|
|
93
95
|
}).map((diagnostic) => formatAnalogRouteIdiomDiagnostic(diagnostic, path, workspaceRoot));
|
|
94
96
|
const fingerprint = rendered.join("\n\n");
|
|
95
97
|
if (!fingerprint) {
|
|
@@ -170,6 +172,13 @@ function routerPlugin(options) {
|
|
|
170
172
|
config = _config;
|
|
171
173
|
root = normalizePath(resolve(workspaceRoot, config.root || ".") || ".");
|
|
172
174
|
},
|
|
175
|
+
/**
|
|
176
|
+
* Transforms code to replace ANALOG_ROUTE_FILES and ANALOG_CONTENT_ROUTE_FILES
|
|
177
|
+
* placeholders with actual dynamic imports of discovered route and content files.
|
|
178
|
+
*
|
|
179
|
+
* @param code The source code to transform
|
|
180
|
+
* @returns Transformed code with dynamic imports or undefined if no transformation needed
|
|
181
|
+
*/
|
|
173
182
|
transform: {
|
|
174
183
|
filter: { code: "_ROUTE_FILES" },
|
|
175
184
|
handler(code) {
|
|
@@ -198,6 +207,13 @@ function routerPlugin(options) {
|
|
|
198
207
|
{
|
|
199
208
|
name: "analog-glob-endpoints",
|
|
200
209
|
enforce: "post",
|
|
210
|
+
/**
|
|
211
|
+
* Transforms code to replace ANALOG_PAGE_ENDPOINTS placeholder
|
|
212
|
+
* with actual dynamic imports of discovered server endpoint files.
|
|
213
|
+
*
|
|
214
|
+
* @param code The source code to transform
|
|
215
|
+
* @returns Transformed code with dynamic imports or undefined if no transformation needed
|
|
216
|
+
*/
|
|
201
217
|
transform: {
|
|
202
218
|
filter: { code: "ANALOG_PAGE_ENDPOINTS" },
|
|
203
219
|
handler(code) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router-plugin.js","names":[],"sources":["../../../src/lib/router-plugin.ts"],"sourcesContent":["import { normalizePath, Plugin, UserConfig, ViteDevServer } from 'vite';\nimport { globSync } from 'tinyglobby';\nimport { readFileSync } from 'node:fs';\nimport { isAbsolute, relative, resolve } from 'node:path';\n\nimport { Options } from './options.js';\nimport {\n analyzeAnalogRouteFile,\n formatAnalogRouteIdiomDiagnostic,\n} from './route-idiom-diagnostics.js';\n\n/**\n * Router plugin that handles route file discovery and hot module replacement.\n *\n * This plugin provides three main functionalities:\n * 1. Route invalidation when files are added/deleted (HMR workaround)\n * 2. Dynamic route file discovery and import generation\n * 3. Content route file discovery for markdown and Analog content\n *\n * @param options Configuration options for the router plugin\n * @returns Array of Vite plugins for route handling\n *\n * IMPORTANT: This plugin uses tinyglobby for file discovery.\n * Key behavior with { dot: true, absolute: true }:\n * - Returns absolute paths for ALL discovered files\n * - Path normalization is required to match expected output format\n * - Files within project root must use relative paths in object keys\n * - Files outside project root keep absolute paths in object keys\n */\nexport function routerPlugin(options?: Options): Plugin[] {\n const workspaceRoot = normalizePath(\n options?.workspaceRoot ?? process.env['NX_WORKSPACE_ROOT'] ?? process.cwd(),\n );\n let config: UserConfig;\n let root: string;\n // Option dirs are workspace-relative, often written with a leading `/`.\n // Normalize them once into absolute workspace paths so watcher events,\n // glob patterns, and key generation all compare against the same shape.\n const normalizeWatchedDir = (dir: string) => {\n const normalizedDir = normalizePath(\n dir.startsWith(`${workspaceRoot}/`) || dir === workspaceRoot\n ? dir\n : dir.startsWith('/')\n ? `${workspaceRoot}${dir}`\n : resolve(workspaceRoot, dir),\n );\n return normalizedDir.endsWith('/')\n ? normalizedDir.slice(0, -1)\n : normalizedDir;\n };\n // Computed eagerly — these depend only on `options` and `workspaceRoot`,\n // both of which are fixed at construction time.\n const additionalPagesDirs = (options?.additionalPagesDirs || []).map((dir) =>\n normalizeWatchedDir(dir),\n );\n const additionalContentDirs = (options?.additionalContentDirs || []).map(\n (dir) => normalizeWatchedDir(dir),\n );\n // Returns every directory that can contain route-like files. The root-\n // relative entries are only available after the Vite `config` hook sets\n // `root`. The short-form fallbacks (`/src/app/pages`, etc.) let watcher\n // events match before `config` runs — they cover the common convention\n // where paths start with these prefixes.\n const getRouteLikeDirs = () => [\n ...(root\n ? [\n normalizeWatchedDir(`${root}/src/app/pages`),\n normalizeWatchedDir(`${root}/src/content`),\n ]\n : []),\n '/src/app/pages',\n '/src/content',\n ...additionalPagesDirs,\n ...additionalContentDirs,\n ];\n // These lists are used repeatedly by transform hooks during serve. Keeping\n // them warm avoids a full glob on every route/content invalidation.\n let routeFilesCache: string[] | undefined;\n let pageRouteFilesCache: string[] | undefined;\n let contentRouteFilesCache: string[] | undefined;\n let endpointFilesCache: string[] | undefined;\n const routeDiagnosticCache = new Map<string, string>();\n const isRouteLikeFile = (path: string) => {\n // Watcher paths from chokidar are already absolute — `normalizePath`\n // (forward-slash only) is sufficient; `resolve()` would be a no-op.\n const normalizedPath = normalizePath(path);\n\n return getRouteLikeDirs().some(\n (dir) => normalizedPath === dir || normalizedPath.startsWith(`${dir}/`),\n );\n };\n const discoverRouteFiles = () => {\n routeFilesCache ??= globSync(\n [\n `${root}/src/app/pages/**/*.page.ts`,\n ...additionalPagesDirs.map((dir) => `${dir}/**/*.page.ts`),\n ],\n { dot: true, absolute: true },\n );\n\n return routeFilesCache;\n };\n const discoverContentRouteFiles = () => {\n contentRouteFilesCache ??= globSync(\n [\n `${root}/src/app/pages/**/*.md`,\n `${root}/src/content/**/*.md`,\n ...additionalContentDirs.map((dir) => `${dir}/**/*.md`),\n ],\n { dot: true, absolute: true },\n );\n\n return contentRouteFilesCache;\n };\n const discoverEndpointFiles = () => {\n endpointFilesCache ??= globSync(\n [\n `${root}/src/app/pages/**/*.server.ts`,\n ...additionalPagesDirs.map((dir) => `${dir}/**/*.server.ts`),\n ],\n { dot: true, absolute: true },\n );\n\n return endpointFilesCache;\n };\n const discoverPageRouteFiles = () => {\n pageRouteFilesCache ??= discoverRouteFiles().filter((file) =>\n file.endsWith('.page.ts'),\n );\n\n return pageRouteFilesCache;\n };\n const invalidateDiscoveryCaches = () => {\n routeFilesCache = undefined;\n pageRouteFilesCache = undefined;\n contentRouteFilesCache = undefined;\n endpointFilesCache = undefined;\n };\n const reportRouteDiagnostics = (path: string) => {\n if (!path.endsWith('.page.ts')) {\n return;\n }\n\n try {\n const code = readFileSync(path, 'utf-8');\n const routeFiles = discoverPageRouteFiles();\n const diagnostics = analyzeAnalogRouteFile({\n filename: path,\n code,\n routeFiles,\n });\n\n const rendered = diagnostics.map((diagnostic) =>\n formatAnalogRouteIdiomDiagnostic(diagnostic, path, workspaceRoot),\n );\n const fingerprint = rendered.join('\\n\\n');\n\n if (!fingerprint) {\n routeDiagnosticCache.delete(path);\n return;\n }\n\n if (routeDiagnosticCache.get(path) === fingerprint) {\n return;\n }\n\n routeDiagnosticCache.set(path, fingerprint);\n rendered.forEach((message) => console.warn(message));\n } catch {\n routeDiagnosticCache.delete(path);\n }\n };\n const getModuleKey = (module: string) => {\n // Before config sets `root`, fall back to workspace-relative keys.\n if (!root) {\n return `/${normalizePath(relative(workspaceRoot, module))}`;\n }\n\n const relToRoot = normalizePath(relative(root, module));\n // Use true path containment instead of a raw prefix check so siblings like\n // `/apps/my-app-tools/...` are not mistaken for files inside `/apps/my-app`.\n const isInRoot = !relToRoot.startsWith('..') && !isAbsolute(relToRoot);\n\n if (isInRoot) {\n return `/${relToRoot}`;\n }\n\n return `/${normalizePath(relative(workspaceRoot, module))}`;\n };\n const invalidateFileModules = (server: ViteDevServer, path: string) => {\n const normalizedPath = normalizePath(path);\n // A newly added page can be discovered before its final contents settle.\n // Invalidate the page module itself so later edits don't keep serving the\n // first incomplete transform from Vite's module graph.\n const fileModules =\n server.moduleGraph.getModulesByFile?.(normalizedPath) ??\n server.moduleGraph.fileToModulesMap.get(normalizedPath);\n\n fileModules?.forEach((mod) => {\n server.moduleGraph.invalidateModule(mod);\n\n mod.importers.forEach((imp) => {\n server.moduleGraph.invalidateModule(imp);\n });\n });\n };\n\n return [\n {\n name: 'analogjs-router-invalidate-routes',\n configureServer(server) {\n /**\n * Invalidates route modules when files are added or deleted.\n * This is a workaround for Vite's HMR limitations with dynamic imports.\n *\n * @param path The file path that was added or deleted\n */\n function invalidateRoutes(\n path: string,\n event: 'add' | 'change' | 'unlink',\n ) {\n if (!isRouteLikeFile(path)) {\n return;\n }\n\n // Add/remove changes the route graph shape, so the discovery caches\n // must be rebuilt. Plain edits can keep using the current file set.\n if (event !== 'change') {\n invalidateDiscoveryCaches();\n }\n\n if (event === 'change') {\n reportRouteDiagnostics(path);\n } else if (event === 'unlink') {\n routeDiagnosticCache.delete(path);\n discoverPageRouteFiles().forEach((file) =>\n reportRouteDiagnostics(file),\n );\n } else {\n discoverPageRouteFiles().forEach((file) =>\n reportRouteDiagnostics(file),\n );\n }\n\n invalidateFileModules(server, path);\n\n // For an in-place edit we only need module invalidation. Keeping the\n // app alive here lets Angular/Vite attempt the narrower HMR path.\n if (event === 'change') {\n return;\n }\n\n server.moduleGraph.fileToModulesMap.forEach((mods) => {\n mods.forEach((mod) => {\n if (mod.id?.includes('analogjs') && mod.id?.includes('fesm')) {\n server.moduleGraph.invalidateModule(mod);\n\n mod.importers.forEach((imp) => {\n server.moduleGraph.invalidateModule(imp);\n });\n }\n });\n });\n\n server.ws.send('analog:debug-full-reload', {\n plugin: 'platform:router-plugin',\n reason: 'route-graph-shape-changed',\n event,\n path,\n });\n server.ws.send({\n type: 'full-reload',\n });\n }\n\n server.watcher.on('add', (path) => invalidateRoutes(path, 'add'));\n server.watcher.on('change', (path) => invalidateRoutes(path, 'change'));\n server.watcher.on('unlink', (path) => invalidateRoutes(path, 'unlink'));\n\n // Vite's watcher only covers the app root by default.\n // additionalPagesDirs / additionalContentDirs live outside the\n // root (e.g. libs/shared/feature in a monorepo), so file\n // add/rename/delete events are never fired for them. Explicitly\n // add these directories to chokidar so route invalidation works.\n for (const dir of [...additionalPagesDirs, ...additionalContentDirs]) {\n server.watcher.add(dir);\n }\n\n discoverPageRouteFiles().forEach((file) =>\n reportRouteDiagnostics(file),\n );\n },\n },\n {\n name: 'analog-glob-routes',\n // enforce: 'post' ensures this transform runs AFTER the Angular compiler\n // plugin, which replaces module content with its own compiled output.\n // Without this, the Angular plugin would overwrite the route replacements.\n enforce: 'post',\n config(_config) {\n config = _config;\n root = normalizePath(resolve(workspaceRoot, config.root || '.') || '.');\n },\n /**\n * Transforms code to replace ANALOG_ROUTE_FILES and ANALOG_CONTENT_ROUTE_FILES\n * placeholders with actual dynamic imports of discovered route and content files.\n *\n * @param code The source code to transform\n * @returns Transformed code with dynamic imports or undefined if no transformation needed\n */\n // Vite 8 / Rolldown filtered transform: the `filter.code` substring\n // pre-filter lets the bundler skip modules that don't contain the\n // marker. '_ROUTE_FILES' is a common substring of both\n // 'ANALOG_ROUTE_FILES' and 'ANALOG_CONTENT_ROUTE_FILES'.\n //\n // IMPORTANT: Do NOT change this to 'ANALOG_ROUTE_FILES' — that is NOT\n // a substring of 'ANALOG_CONTENT_ROUTE_FILES' (they diverge at\n // position 7: 'ANALOG_C...' vs 'ANALOG_R...'). When tsconfig path\n // aliases resolve @analogjs/router and @analogjs/router/content to\n // separate source files, each variable lives in its own module and\n // the filter must match both independently.\n transform: {\n filter: {\n code: '_ROUTE_FILES',\n },\n handler(code) {\n if (\n code.includes('ANALOG_ROUTE_FILES') ||\n code.includes('ANALOG_CONTENT_ROUTE_FILES')\n ) {\n // Discover route files using tinyglobby\n // NOTE: { absolute: true } returns absolute paths for ALL files\n const routeFiles = discoverRouteFiles();\n\n // Discover content files using tinyglobby\n const contentRouteFiles = discoverContentRouteFiles();\n\n let result = code.replace(\n 'ANALOG_ROUTE_FILES = {};',\n `\n ANALOG_ROUTE_FILES = {${routeFiles.map((module) => {\n // Keys are app-root-relative for in-app files,\n // workspace-relative for library files (additionalPagesDirs).\n // import() keeps absolute paths for Vite's module resolution.\n const key = getModuleKey(module);\n return `\"${key}\": () => import('${module}')`;\n })}};\n `,\n );\n\n result = result.replace(\n 'ANALOG_CONTENT_ROUTE_FILES = {};',\n `\n ANALOG_CONTENT_ROUTE_FILES = {${contentRouteFiles.map((module) => {\n const key = getModuleKey(module);\n return `\"${key}\": () => import('${module}?analog-content-file=true').then(m => m.default)`;\n })}};\n `,\n );\n\n result = result.replace(\n 'ANALOG_CONTENT_FILE_COUNT = 0',\n `ANALOG_CONTENT_FILE_COUNT = ${contentRouteFiles.length}`,\n );\n\n return {\n code: result,\n map: { mappings: '' },\n };\n }\n\n return;\n },\n },\n },\n {\n name: 'analog-glob-endpoints',\n // enforce: 'post' ensures this transform runs AFTER the Angular compiler\n // plugin, which replaces module content with its own compiled output.\n // Without this, the Angular plugin would overwrite the endpoint replacements.\n enforce: 'post',\n /**\n * Transforms code to replace ANALOG_PAGE_ENDPOINTS placeholder\n * with actual dynamic imports of discovered server endpoint files.\n *\n * @param code The source code to transform\n * @returns Transformed code with dynamic imports or undefined if no transformation needed\n */\n transform: {\n filter: {\n code: 'ANALOG_PAGE_ENDPOINTS',\n },\n handler(code) {\n if (code.includes('ANALOG_PAGE_ENDPOINTS')) {\n // Discover server endpoint files using tinyglobby\n const endpointFiles = discoverEndpointFiles();\n\n const result = code.replace(\n 'ANALOG_PAGE_ENDPOINTS = {};',\n `\n ANALOG_PAGE_ENDPOINTS = {${endpointFiles.map((module) => {\n const key = getModuleKey(module);\n return `\"${key}\": () => import('${module}')`;\n })}};\n `,\n );\n\n return {\n code: result,\n map: { mappings: '' },\n };\n }\n\n return;\n },\n },\n },\n ];\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"router-plugin.js","names":[],"sources":["../../../src/lib/router-plugin.ts"],"sourcesContent":["import { normalizePath, Plugin, UserConfig, ViteDevServer } from 'vite';\nimport { globSync } from 'tinyglobby';\nimport { readFileSync } from 'node:fs';\nimport { isAbsolute, relative, resolve } from 'node:path';\n\nimport { Options } from './options.js';\nimport {\n analyzeAnalogRouteFile,\n formatAnalogRouteIdiomDiagnostic,\n} from './route-idiom-diagnostics.js';\n\n/**\n * Router plugin that handles route file discovery and hot module replacement.\n *\n * This plugin provides three main functionalities:\n * 1. Route invalidation when files are added/deleted (HMR workaround)\n * 2. Dynamic route file discovery and import generation\n * 3. Content route file discovery for markdown and Analog content\n *\n * @param options Configuration options for the router plugin\n * @returns Array of Vite plugins for route handling\n *\n * IMPORTANT: This plugin uses tinyglobby for file discovery.\n * Key behavior with { dot: true, absolute: true }:\n * - Returns absolute paths for ALL discovered files\n * - Path normalization is required to match expected output format\n * - Files within project root must use relative paths in object keys\n * - Files outside project root keep absolute paths in object keys\n */\nexport function routerPlugin(options?: Options): Plugin[] {\n const workspaceRoot = normalizePath(\n options?.workspaceRoot ?? process.env['NX_WORKSPACE_ROOT'] ?? process.cwd(),\n );\n let config: UserConfig;\n let root: string;\n // Option dirs are workspace-relative, often written with a leading `/`.\n // Normalize them once into absolute workspace paths so watcher events,\n // glob patterns, and key generation all compare against the same shape.\n const normalizeWatchedDir = (dir: string) => {\n const normalizedDir = normalizePath(\n dir.startsWith(`${workspaceRoot}/`) || dir === workspaceRoot\n ? dir\n : dir.startsWith('/')\n ? `${workspaceRoot}${dir}`\n : resolve(workspaceRoot, dir),\n );\n return normalizedDir.endsWith('/')\n ? normalizedDir.slice(0, -1)\n : normalizedDir;\n };\n // Computed eagerly — these depend only on `options` and `workspaceRoot`,\n // both of which are fixed at construction time.\n const additionalPagesDirs = (options?.additionalPagesDirs || []).map((dir) =>\n normalizeWatchedDir(dir),\n );\n const additionalContentDirs = (options?.additionalContentDirs || []).map(\n (dir) => normalizeWatchedDir(dir),\n );\n // Returns every directory that can contain route-like files. The root-\n // relative entries are only available after the Vite `config` hook sets\n // `root`. The short-form fallbacks (`/src/app/pages`, etc.) let watcher\n // events match before `config` runs — they cover the common convention\n // where paths start with these prefixes.\n const getRouteLikeDirs = () => [\n ...(root\n ? [\n normalizeWatchedDir(`${root}/src/app/pages`),\n normalizeWatchedDir(`${root}/src/content`),\n ]\n : []),\n '/src/app/pages',\n '/src/content',\n ...additionalPagesDirs,\n ...additionalContentDirs,\n ];\n // These lists are used repeatedly by transform hooks during serve. Keeping\n // them warm avoids a full glob on every route/content invalidation.\n let routeFilesCache: string[] | undefined;\n let pageRouteFilesCache: string[] | undefined;\n let contentRouteFilesCache: string[] | undefined;\n let endpointFilesCache: string[] | undefined;\n const routeDiagnosticCache = new Map<string, string>();\n const isRouteLikeFile = (path: string) => {\n // Watcher paths from chokidar are already absolute — `normalizePath`\n // (forward-slash only) is sufficient; `resolve()` would be a no-op.\n const normalizedPath = normalizePath(path);\n\n return getRouteLikeDirs().some(\n (dir) => normalizedPath === dir || normalizedPath.startsWith(`${dir}/`),\n );\n };\n const discoverRouteFiles = () => {\n routeFilesCache ??= globSync(\n [\n `${root}/src/app/pages/**/*.page.ts`,\n ...additionalPagesDirs.map((dir) => `${dir}/**/*.page.ts`),\n ],\n { dot: true, absolute: true },\n );\n\n return routeFilesCache;\n };\n const discoverContentRouteFiles = () => {\n contentRouteFilesCache ??= globSync(\n [\n `${root}/src/app/pages/**/*.md`,\n `${root}/src/content/**/*.md`,\n ...additionalContentDirs.map((dir) => `${dir}/**/*.md`),\n ],\n { dot: true, absolute: true },\n );\n\n return contentRouteFilesCache;\n };\n const discoverEndpointFiles = () => {\n endpointFilesCache ??= globSync(\n [\n `${root}/src/app/pages/**/*.server.ts`,\n ...additionalPagesDirs.map((dir) => `${dir}/**/*.server.ts`),\n ],\n { dot: true, absolute: true },\n );\n\n return endpointFilesCache;\n };\n const discoverPageRouteFiles = () => {\n pageRouteFilesCache ??= discoverRouteFiles().filter((file) =>\n file.endsWith('.page.ts'),\n );\n\n return pageRouteFilesCache;\n };\n const invalidateDiscoveryCaches = () => {\n routeFilesCache = undefined;\n pageRouteFilesCache = undefined;\n contentRouteFilesCache = undefined;\n endpointFilesCache = undefined;\n };\n const reportRouteDiagnostics = (path: string) => {\n if (!path.endsWith('.page.ts')) {\n return;\n }\n\n try {\n const code = readFileSync(path, 'utf-8');\n const routeFiles = discoverPageRouteFiles();\n const diagnostics = analyzeAnalogRouteFile({\n filename: path,\n code,\n routeFiles,\n });\n\n const rendered = diagnostics.map((diagnostic) =>\n formatAnalogRouteIdiomDiagnostic(diagnostic, path, workspaceRoot),\n );\n const fingerprint = rendered.join('\\n\\n');\n\n if (!fingerprint) {\n routeDiagnosticCache.delete(path);\n return;\n }\n\n if (routeDiagnosticCache.get(path) === fingerprint) {\n return;\n }\n\n routeDiagnosticCache.set(path, fingerprint);\n rendered.forEach((message) => console.warn(message));\n } catch {\n routeDiagnosticCache.delete(path);\n }\n };\n const getModuleKey = (module: string) => {\n // Before config sets `root`, fall back to workspace-relative keys.\n if (!root) {\n return `/${normalizePath(relative(workspaceRoot, module))}`;\n }\n\n const relToRoot = normalizePath(relative(root, module));\n // Use true path containment instead of a raw prefix check so siblings like\n // `/apps/my-app-tools/...` are not mistaken for files inside `/apps/my-app`.\n const isInRoot = !relToRoot.startsWith('..') && !isAbsolute(relToRoot);\n\n if (isInRoot) {\n return `/${relToRoot}`;\n }\n\n return `/${normalizePath(relative(workspaceRoot, module))}`;\n };\n const invalidateFileModules = (server: ViteDevServer, path: string) => {\n const normalizedPath = normalizePath(path);\n // A newly added page can be discovered before its final contents settle.\n // Invalidate the page module itself so later edits don't keep serving the\n // first incomplete transform from Vite's module graph.\n const fileModules =\n server.moduleGraph.getModulesByFile?.(normalizedPath) ??\n server.moduleGraph.fileToModulesMap.get(normalizedPath);\n\n fileModules?.forEach((mod) => {\n server.moduleGraph.invalidateModule(mod);\n\n mod.importers.forEach((imp) => {\n server.moduleGraph.invalidateModule(imp);\n });\n });\n };\n\n return [\n {\n name: 'analogjs-router-invalidate-routes',\n configureServer(server) {\n /**\n * Invalidates route modules when files are added or deleted.\n * This is a workaround for Vite's HMR limitations with dynamic imports.\n *\n * @param path The file path that was added or deleted\n */\n function invalidateRoutes(\n path: string,\n event: 'add' | 'change' | 'unlink',\n ) {\n if (!isRouteLikeFile(path)) {\n return;\n }\n\n // Add/remove changes the route graph shape, so the discovery caches\n // must be rebuilt. Plain edits can keep using the current file set.\n if (event !== 'change') {\n invalidateDiscoveryCaches();\n }\n\n if (event === 'change') {\n reportRouteDiagnostics(path);\n } else if (event === 'unlink') {\n routeDiagnosticCache.delete(path);\n discoverPageRouteFiles().forEach((file) =>\n reportRouteDiagnostics(file),\n );\n } else {\n discoverPageRouteFiles().forEach((file) =>\n reportRouteDiagnostics(file),\n );\n }\n\n invalidateFileModules(server, path);\n\n // For an in-place edit we only need module invalidation. Keeping the\n // app alive here lets Angular/Vite attempt the narrower HMR path.\n if (event === 'change') {\n return;\n }\n\n server.moduleGraph.fileToModulesMap.forEach((mods) => {\n mods.forEach((mod) => {\n if (mod.id?.includes('analogjs') && mod.id?.includes('fesm')) {\n server.moduleGraph.invalidateModule(mod);\n\n mod.importers.forEach((imp) => {\n server.moduleGraph.invalidateModule(imp);\n });\n }\n });\n });\n\n server.ws.send('analog:debug-full-reload', {\n plugin: 'platform:router-plugin',\n reason: 'route-graph-shape-changed',\n event,\n path,\n });\n server.ws.send({\n type: 'full-reload',\n });\n }\n\n server.watcher.on('add', (path) => invalidateRoutes(path, 'add'));\n server.watcher.on('change', (path) => invalidateRoutes(path, 'change'));\n server.watcher.on('unlink', (path) => invalidateRoutes(path, 'unlink'));\n\n // Vite's watcher only covers the app root by default.\n // additionalPagesDirs / additionalContentDirs live outside the\n // root (e.g. libs/shared/feature in a monorepo), so file\n // add/rename/delete events are never fired for them. Explicitly\n // add these directories to chokidar so route invalidation works.\n for (const dir of [...additionalPagesDirs, ...additionalContentDirs]) {\n server.watcher.add(dir);\n }\n\n discoverPageRouteFiles().forEach((file) =>\n reportRouteDiagnostics(file),\n );\n },\n },\n {\n name: 'analog-glob-routes',\n // enforce: 'post' ensures this transform runs AFTER the Angular compiler\n // plugin, which replaces module content with its own compiled output.\n // Without this, the Angular plugin would overwrite the route replacements.\n enforce: 'post',\n config(_config) {\n config = _config;\n root = normalizePath(resolve(workspaceRoot, config.root || '.') || '.');\n },\n /**\n * Transforms code to replace ANALOG_ROUTE_FILES and ANALOG_CONTENT_ROUTE_FILES\n * placeholders with actual dynamic imports of discovered route and content files.\n *\n * @param code The source code to transform\n * @returns Transformed code with dynamic imports or undefined if no transformation needed\n */\n // Vite 8 / Rolldown filtered transform: the `filter.code` substring\n // pre-filter lets the bundler skip modules that don't contain the\n // marker. '_ROUTE_FILES' is a common substring of both\n // 'ANALOG_ROUTE_FILES' and 'ANALOG_CONTENT_ROUTE_FILES'.\n //\n // IMPORTANT: Do NOT change this to 'ANALOG_ROUTE_FILES' — that is NOT\n // a substring of 'ANALOG_CONTENT_ROUTE_FILES' (they diverge at\n // position 7: 'ANALOG_C...' vs 'ANALOG_R...'). When tsconfig path\n // aliases resolve @analogjs/router and @analogjs/router/content to\n // separate source files, each variable lives in its own module and\n // the filter must match both independently.\n transform: {\n filter: {\n code: '_ROUTE_FILES',\n },\n handler(code) {\n if (\n code.includes('ANALOG_ROUTE_FILES') ||\n code.includes('ANALOG_CONTENT_ROUTE_FILES')\n ) {\n // Discover route files using tinyglobby\n // NOTE: { absolute: true } returns absolute paths for ALL files\n const routeFiles = discoverRouteFiles();\n\n // Discover content files using tinyglobby\n const contentRouteFiles = discoverContentRouteFiles();\n\n let result = code.replace(\n 'ANALOG_ROUTE_FILES = {};',\n `\n ANALOG_ROUTE_FILES = {${routeFiles.map((module) => {\n // Keys are app-root-relative for in-app files,\n // workspace-relative for library files (additionalPagesDirs).\n // import() keeps absolute paths for Vite's module resolution.\n const key = getModuleKey(module);\n return `\"${key}\": () => import('${module}')`;\n })}};\n `,\n );\n\n result = result.replace(\n 'ANALOG_CONTENT_ROUTE_FILES = {};',\n `\n ANALOG_CONTENT_ROUTE_FILES = {${contentRouteFiles.map((module) => {\n const key = getModuleKey(module);\n return `\"${key}\": () => import('${module}?analog-content-file=true').then(m => m.default)`;\n })}};\n `,\n );\n\n result = result.replace(\n 'ANALOG_CONTENT_FILE_COUNT = 0',\n `ANALOG_CONTENT_FILE_COUNT = ${contentRouteFiles.length}`,\n );\n\n return {\n code: result,\n map: { mappings: '' },\n };\n }\n\n return;\n },\n },\n },\n {\n name: 'analog-glob-endpoints',\n // enforce: 'post' ensures this transform runs AFTER the Angular compiler\n // plugin, which replaces module content with its own compiled output.\n // Without this, the Angular plugin would overwrite the endpoint replacements.\n enforce: 'post',\n /**\n * Transforms code to replace ANALOG_PAGE_ENDPOINTS placeholder\n * with actual dynamic imports of discovered server endpoint files.\n *\n * @param code The source code to transform\n * @returns Transformed code with dynamic imports or undefined if no transformation needed\n */\n transform: {\n filter: {\n code: 'ANALOG_PAGE_ENDPOINTS',\n },\n handler(code) {\n if (code.includes('ANALOG_PAGE_ENDPOINTS')) {\n // Discover server endpoint files using tinyglobby\n const endpointFiles = discoverEndpointFiles();\n\n const result = code.replace(\n 'ANALOG_PAGE_ENDPOINTS = {};',\n `\n ANALOG_PAGE_ENDPOINTS = {${endpointFiles.map((module) => {\n const key = getModuleKey(module);\n return `\"${key}\": () => import('${module}')`;\n })}};\n `,\n );\n\n return {\n code: result,\n map: { mappings: '' },\n };\n }\n\n return;\n },\n },\n },\n ];\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server-fn-client-transform.js","names":[],"sources":["../../../src/lib/server-fn-client-transform.ts"],"sourcesContent":["import { parseSync } from 'oxc-parser';\n// Light subpath (no vite/nitro barrel): the built scrub is loaded outside a\n// build too, so it must not pull the plugin graph in.\nimport { deriveServerFnId } from './server-fn-id.js';\n\n/**\n * Client-build scrub + proxy generation for server functions.\n *\n * On the client build a `*.server.ts` module must not carry server handler\n * bodies (which `inject()` server services, read the request, hit the database).\n * But the client still imports the named `serverFn` exports to dispatch them.\n * This transform regenerates the module so each `export const NAME = serverFn(\n * config, handler)` becomes a call to the client-safe `createServerFnRef`\n * factory — keeping the `{ id, url, method }` metadata `injectServerFn` needs,\n * dropping the handler and every server-only import. Because the regenerated\n * module references nothing else, the server code tree-shakes away entirely.\n *\n * Returns the new module source, or `null` when the module defines no server\n * function (the caller falls back to the page-endpoint scrub for those).\n */\nexport interface ServerFnProxy {\n name: string;\n id: string;\n method: 'GET' | 'POST';\n}\n\nexport interface ScrubResult {\n code: string;\n proxies: ServerFnProxy[];\n hadDefaultExport: boolean;\n}\n\nconst SERVER_FN_SOURCE = '@analogjs/router/server';\n\nexport function scrubServerFnModule(\n code: string,\n fileId = 'file.ts',\n): ScrubResult | null {\n // Cheap bail-out before parsing: no `serverFn` token, nothing to do.\n if (!code.includes('serverFn')) {\n return null;\n }\n\n const { program } = parseSync(fileId, code);\n const body = (program as { body: any[] }).body;\n\n const serverFnLocalNames = collectServerFnLocalNames(body);\n const hadDefaultExport = body.some(\n (node) => node.type === 'ExportDefaultDeclaration',\n );\n\n const proxies: ServerFnProxy[] = [];\n for (const node of body) {\n if (\n node.type !== 'ExportNamedDeclaration' ||\n node.declaration?.type !== 'VariableDeclaration'\n ) {\n continue;\n }\n for (const declarator of node.declaration.declarations) {\n if (\n declarator.id?.type !== 'Identifier' ||\n declarator.init?.type !== 'CallExpression' ||\n declarator.init.callee?.type !== 'Identifier' ||\n !serverFnLocalNames.has(declarator.init.callee.name)\n ) {\n continue;\n }\n proxies.push(\n toProxy(declarator.id.name, declarator.init.arguments ?? [], fileId),\n );\n }\n }\n\n // Every `serverFn(...)` call in the module must have become a proxy. If one\n // did not, the module uses a shape this transform cannot rewrite (e.g.\n // `const fn = serverFn(...); export { fn };`) and returning `null` would leave\n // it untouched — shipping the handler and its server imports to the browser.\n // Fail the build instead: a scrub that silently opts out is a code leak.\n const callCount = countServerFnCalls(program, serverFnLocalNames);\n if (callCount > proxies.length) {\n throw new Error(\n `[analog] ${fileId}: found ${callCount} serverFn call(s) but could only rewrite ${proxies.length} for the client build. ` +\n `Server functions must be declared as a directly exported const — \\`export const name = serverFn(…)\\` — so the handler can be stripped from the browser bundle.`,\n );\n }\n\n if (proxies.length === 0) {\n return null;\n }\n\n const lines = [\n `import { createServerFnRef } from '@analogjs/router';`,\n '',\n ...proxies.map(\n (p) =>\n `export const ${p.name} = createServerFnRef({ id: ${JSON.stringify(\n p.id,\n )}, method: ${JSON.stringify(p.method)} });`,\n ),\n ];\n // A file that is also a page endpoint keeps the load-shim the router expects.\n if (hadDefaultExport) {\n lines.push('', 'export default undefined;');\n }\n\n return { code: lines.join('\\n') + '\\n', proxies, hadDefaultExport };\n}\n\n/** Local identifier(s) `serverFn` is imported under from `@analogjs/router/server`. */\nfunction collectServerFnLocalNames(body: any[]): Set<string> {\n const names = new Set<string>();\n for (const node of body) {\n if (\n node.type !== 'ImportDeclaration' ||\n node.source?.value !== SERVER_FN_SOURCE\n ) {\n continue;\n }\n for (const spec of node.specifiers ?? []) {\n if (\n spec.type === 'ImportSpecifier' &&\n spec.imported?.name === 'serverFn'\n ) {\n names.add(spec.local.name);\n }\n }\n }\n // Fallback: if the module never imported it (unusual), still match the\n // conventional name so a mis-resolved import doesn't leak server code.\n if (names.size === 0) {\n names.add('serverFn');\n }\n return names;\n}\n\nfunction toProxy(name: string, args: any[], fileId: string): ServerFnProxy {\n const method = methodForCall(name, args, fileId);\n // The id is derived, never read from source — same algorithm the server\n // registration uses, so client proxy and server route always match.\n return { name, id: deriveServerFnId(fileId, name), method };\n}\n\n/**\n * Resolve the HTTP method from the call shape, matching the runtime overloads:\n * `serverFn(handler)` ⇒ GET, `serverFn(schema, handler)` ⇒ POST,\n * `serverFn(config, handler)` ⇒ config-driven.\n */\nfunction methodForCall(\n name: string,\n args: any[],\n fileId: string,\n): 'GET' | 'POST' {\n const arg0 = args[0];\n\n // serverFn(handler) — input-less GET.\n if (isFunctionNode(arg0)) return 'GET';\n\n // serverFn(config, handler) — read method/input off the object.\n if (arg0?.type === 'ObjectExpression') {\n let method: 'GET' | 'POST' | undefined;\n let hasInput = false;\n for (const prop of arg0.properties ?? []) {\n if (prop.type !== 'Property') continue;\n const key =\n prop.key?.type === 'Identifier' ? prop.key.name : prop.key?.value;\n if (key === 'method' && isStringLiteral(prop.value)) {\n method = prop.value.value as 'GET' | 'POST';\n } else if (key === 'input') {\n hasInput = true;\n }\n }\n if (method === 'GET' && hasInput) {\n throw new Error(\n `[analog] serverFn \"${name}\" in ${fileId} declares method: 'GET' with an input schema; GET carries no body. Use POST or drop the input.`,\n );\n }\n return method ?? (hasInput ? 'POST' : 'GET');\n }\n\n // serverFn(schema, handler) — a schema ⇒ POST + input.\n if (arg0 && args.length >= 2) return 'POST';\n\n throw new Error(\n `[analog] serverFn \"${name}\" in ${fileId} must be called as serverFn(handler), serverFn(schema, handler), or serverFn(config, handler).`,\n );\n}\n\nfunction isFunctionNode(node: any): boolean {\n return (\n node?.type === 'ArrowFunctionExpression' ||\n node?.type === 'FunctionExpression'\n );\n}\n\nfunction isStringLiteral(\n node: any,\n): node is { type: 'Literal'; value: string } {\n return node?.type === 'Literal' && typeof node.value === 'string';\n}\n\n/** Every `serverFn(...)` call anywhere in the module, not just exported ones. */\nfunction countServerFnCalls(node: unknown, names: Set<string>): number {\n if (!node || typeof node !== 'object') {\n return 0;\n }\n if (Array.isArray(node)) {\n return node.reduce(\n (total, child) => total + countServerFnCalls(child, names),\n 0,\n );\n }\n\n const record = node as Record<string, unknown> & { type?: string };\n let count = 0;\n if (record.type === 'CallExpression') {\n const callee = record['callee'] as { type?: string; name?: string };\n if (\n callee?.type === 'Identifier' &&\n callee.name &&\n names.has(callee.name)\n ) {\n count += 1;\n }\n }\n for (const key of Object.keys(record)) {\n count += countServerFnCalls(record[key], names);\n }\n return count;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"server-fn-client-transform.js","names":[],"sources":["../../../src/lib/server-fn-client-transform.ts"],"sourcesContent":["import { parseSync } from 'oxc-parser';\n// Light subpath (no vite/nitro barrel): the built scrub is loaded outside a\n// build too, so it must not pull the plugin graph in.\nimport { deriveServerFnId } from './server-fn-id.js';\n\n/**\n * Client-build scrub + proxy generation for server functions.\n *\n * On the client build a `*.server.ts` module must not carry server handler\n * bodies (which `inject()` server services, read the request, hit the database).\n * But the client still imports the named `serverFn` exports to dispatch them.\n * This transform regenerates the module so each `export const NAME = serverFn(\n * config, handler)` becomes a call to the client-safe `createServerFnRef`\n * factory — keeping the `{ id, url, method }` metadata `injectServerFn` needs,\n * dropping the handler and every server-only import. Because the regenerated\n * module references nothing else, the server code tree-shakes away entirely.\n *\n * Returns the new module source, or `null` when the module defines no server\n * function (the caller falls back to the page-endpoint scrub for those).\n */\nexport interface ServerFnProxy {\n name: string;\n id: string;\n method: 'GET' | 'POST';\n}\n\nexport interface ScrubResult {\n code: string;\n proxies: ServerFnProxy[];\n hadDefaultExport: boolean;\n}\n\nconst SERVER_FN_SOURCE = '@analogjs/router/server';\n\nexport function scrubServerFnModule(\n code: string,\n fileId = 'file.ts',\n): ScrubResult | null {\n // Cheap bail-out before parsing: no `serverFn` token, nothing to do.\n if (!code.includes('serverFn')) {\n return null;\n }\n\n const { program } = parseSync(fileId, code);\n const body = (program as { body: any[] }).body;\n\n const serverFnLocalNames = collectServerFnLocalNames(body);\n const hadDefaultExport = body.some(\n (node) => node.type === 'ExportDefaultDeclaration',\n );\n\n const proxies: ServerFnProxy[] = [];\n for (const node of body) {\n if (\n node.type !== 'ExportNamedDeclaration' ||\n node.declaration?.type !== 'VariableDeclaration'\n ) {\n continue;\n }\n for (const declarator of node.declaration.declarations) {\n if (\n declarator.id?.type !== 'Identifier' ||\n declarator.init?.type !== 'CallExpression' ||\n declarator.init.callee?.type !== 'Identifier' ||\n !serverFnLocalNames.has(declarator.init.callee.name)\n ) {\n continue;\n }\n proxies.push(\n toProxy(declarator.id.name, declarator.init.arguments ?? [], fileId),\n );\n }\n }\n\n // Every `serverFn(...)` call in the module must have become a proxy. If one\n // did not, the module uses a shape this transform cannot rewrite (e.g.\n // `const fn = serverFn(...); export { fn };`) and returning `null` would leave\n // it untouched — shipping the handler and its server imports to the browser.\n // Fail the build instead: a scrub that silently opts out is a code leak.\n const callCount = countServerFnCalls(program, serverFnLocalNames);\n if (callCount > proxies.length) {\n throw new Error(\n `[analog] ${fileId}: found ${callCount} serverFn call(s) but could only rewrite ${proxies.length} for the client build. ` +\n `Server functions must be declared as a directly exported const — \\`export const name = serverFn(…)\\` — so the handler can be stripped from the browser bundle.`,\n );\n }\n\n if (proxies.length === 0) {\n return null;\n }\n\n const lines = [\n `import { createServerFnRef } from '@analogjs/router';`,\n '',\n ...proxies.map(\n (p) =>\n `export const ${p.name} = createServerFnRef({ id: ${JSON.stringify(\n p.id,\n )}, method: ${JSON.stringify(p.method)} });`,\n ),\n ];\n // A file that is also a page endpoint keeps the load-shim the router expects.\n if (hadDefaultExport) {\n lines.push('', 'export default undefined;');\n }\n\n return { code: lines.join('\\n') + '\\n', proxies, hadDefaultExport };\n}\n\n/** Local identifier(s) `serverFn` is imported under from `@analogjs/router/server`. */\nfunction collectServerFnLocalNames(body: any[]): Set<string> {\n const names = new Set<string>();\n for (const node of body) {\n if (\n node.type !== 'ImportDeclaration' ||\n node.source?.value !== SERVER_FN_SOURCE\n ) {\n continue;\n }\n for (const spec of node.specifiers ?? []) {\n if (\n spec.type === 'ImportSpecifier' &&\n spec.imported?.name === 'serverFn'\n ) {\n names.add(spec.local.name);\n }\n }\n }\n // Fallback: if the module never imported it (unusual), still match the\n // conventional name so a mis-resolved import doesn't leak server code.\n if (names.size === 0) {\n names.add('serverFn');\n }\n return names;\n}\n\nfunction toProxy(name: string, args: any[], fileId: string): ServerFnProxy {\n const method = methodForCall(name, args, fileId);\n // The id is derived, never read from source — same algorithm the server\n // registration uses, so client proxy and server route always match.\n return { name, id: deriveServerFnId(fileId, name), method };\n}\n\n/**\n * Resolve the HTTP method from the call shape, matching the runtime overloads:\n * `serverFn(handler)` ⇒ GET, `serverFn(schema, handler)` ⇒ POST,\n * `serverFn(config, handler)` ⇒ config-driven.\n */\nfunction methodForCall(\n name: string,\n args: any[],\n fileId: string,\n): 'GET' | 'POST' {\n const arg0 = args[0];\n\n // serverFn(handler) — input-less GET.\n if (isFunctionNode(arg0)) return 'GET';\n\n // serverFn(config, handler) — read method/input off the object.\n if (arg0?.type === 'ObjectExpression') {\n let method: 'GET' | 'POST' | undefined;\n let hasInput = false;\n for (const prop of arg0.properties ?? []) {\n if (prop.type !== 'Property') continue;\n const key =\n prop.key?.type === 'Identifier' ? prop.key.name : prop.key?.value;\n if (key === 'method' && isStringLiteral(prop.value)) {\n method = prop.value.value as 'GET' | 'POST';\n } else if (key === 'input') {\n hasInput = true;\n }\n }\n if (method === 'GET' && hasInput) {\n throw new Error(\n `[analog] serverFn \"${name}\" in ${fileId} declares method: 'GET' with an input schema; GET carries no body. Use POST or drop the input.`,\n );\n }\n return method ?? (hasInput ? 'POST' : 'GET');\n }\n\n // serverFn(schema, handler) — a schema ⇒ POST + input.\n if (arg0 && args.length >= 2) return 'POST';\n\n throw new Error(\n `[analog] serverFn \"${name}\" in ${fileId} must be called as serverFn(handler), serverFn(schema, handler), or serverFn(config, handler).`,\n );\n}\n\nfunction isFunctionNode(node: any): boolean {\n return (\n node?.type === 'ArrowFunctionExpression' ||\n node?.type === 'FunctionExpression'\n );\n}\n\nfunction isStringLiteral(\n node: any,\n): node is { type: 'Literal'; value: string } {\n return node?.type === 'Literal' && typeof node.value === 'string';\n}\n\n/** Every `serverFn(...)` call anywhere in the module, not just exported ones. */\nfunction countServerFnCalls(node: unknown, names: Set<string>): number {\n if (!node || typeof node !== 'object') {\n return 0;\n }\n if (Array.isArray(node)) {\n return node.reduce(\n (total, child) => total + countServerFnCalls(child, names),\n 0,\n );\n }\n\n const record = node as Record<string, unknown> & { type?: string };\n let count = 0;\n if (record.type === 'CallExpression') {\n const callee = record['callee'] as { type?: string; name?: string };\n if (\n callee?.type === 'Identifier' &&\n callee.name &&\n names.has(callee.name)\n ) {\n count += 1;\n }\n }\n for (const key of Object.keys(record)) {\n count += countServerFnCalls(record[key], names);\n }\n return count;\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server-fn-id.js","names":[],"sources":["../../../src/lib/server-fn-id.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport { relative } from 'node:path';\n\n// Intentionally dependency-light (node built-ins only): this module is loaded by\n// the client scrub, so it must not drag in vite/nitro. POSIX-normalize inline.\nconst toPosix = (p: string): string => p.replace(/\\\\/g, '/');\n\n/**\n * Server-function ids are derived, never author-chosen, for security:\n *\n * - **Collision-free.** `hash(fileId + exportName)` is unique per file+export, so\n * two functions can never share a route and hijack each other's dispatch — a\n * real risk with hand-picked ids where two authors independently pick `getUser`.\n * - **Non-enumerable.** The public route is an opaque digest, not a guessable\n * verb like `/_analog/fn/deleteAccount`, shrinking the discoverable surface.\n * - **Not author-controlled.** Authors cannot expose a meaningful or duplicate\n * route by accident; the id is a pure function of location.\n *\n * Both build transforms (server registration + client proxy) MUST derive the\n * same id, so this is the single source of truth for the algorithm and the\n * `fileId` must be the **project-root-relative** POSIX path — identical across\n * the client, SSR, and Nitro builds regardless of absolute checkout location.\n */\n\n/** Project-root-relative POSIX module path used as the stable hash input. */\nexport function serverFnFileId(absPath: string, projectRoot: string): string {\n return toPosix(relative(projectRoot, absPath));\n}\n\n/**\n * Derives the opaque, collision-free id for a server function from its\n * project-root-relative file id and export name. 16 hex chars (64 bits) is far\n * beyond birthday-collision range for any realistic function count while keeping\n * the route short.\n */\nexport function deriveServerFnId(fileId: string, exportName: string): string {\n return createHash('sha256')\n .update(`${fileId}#${exportName}`)\n .digest('hex')\n .slice(0, 16);\n}\n"],"mappings":";;;AAKA,IAAM,WAAW,MAAsB,EAAE,QAAQ,OAAO,
|
|
1
|
+
{"version":3,"file":"server-fn-id.js","names":[],"sources":["../../../src/lib/server-fn-id.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport { relative } from 'node:path';\n\n// Intentionally dependency-light (node built-ins only): this module is loaded by\n// the client scrub, so it must not drag in vite/nitro. POSIX-normalize inline.\nconst toPosix = (p: string): string => p.replace(/\\\\/g, '/');\n\n/**\n * Server-function ids are derived, never author-chosen, for security:\n *\n * - **Collision-free.** `hash(fileId + exportName)` is unique per file+export, so\n * two functions can never share a route and hijack each other's dispatch — a\n * real risk with hand-picked ids where two authors independently pick `getUser`.\n * - **Non-enumerable.** The public route is an opaque digest, not a guessable\n * verb like `/_analog/fn/deleteAccount`, shrinking the discoverable surface.\n * - **Not author-controlled.** Authors cannot expose a meaningful or duplicate\n * route by accident; the id is a pure function of location.\n *\n * Both build transforms (server registration + client proxy) MUST derive the\n * same id, so this is the single source of truth for the algorithm and the\n * `fileId` must be the **project-root-relative** POSIX path — identical across\n * the client, SSR, and Nitro builds regardless of absolute checkout location.\n */\n\n/** Project-root-relative POSIX module path used as the stable hash input. */\nexport function serverFnFileId(absPath: string, projectRoot: string): string {\n return toPosix(relative(projectRoot, absPath));\n}\n\n/**\n * Derives the opaque, collision-free id for a server function from its\n * project-root-relative file id and export name. 16 hex chars (64 bits) is far\n * beyond birthday-collision range for any realistic function count while keeping\n * the route short.\n */\nexport function deriveServerFnId(fileId: string, exportName: string): string {\n return createHash('sha256')\n .update(`${fileId}#${exportName}`)\n .digest('hex')\n .slice(0, 16);\n}\n"],"mappings":";;;AAKA,IAAM,WAAW,MAAsB,EAAE,QAAQ,OAAO,GAAG;;;;;;;;;;;;;;;;;;AAoB3D,SAAgB,eAAe,SAAiB,aAA6B;CAC3E,OAAO,QAAQ,SAAS,aAAa,OAAO,CAAC;AAC/C;;;;;;;AAQA,SAAgB,iBAAiB,QAAgB,YAA4B;CAC3E,OAAO,WAAW,QAAQ,CAAC,CACxB,OAAO,GAAG,OAAO,GAAG,YAAY,CAAC,CACjC,OAAO,KAAK,CAAC,CACb,MAAM,GAAG,EAAE;AAChB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"defer-streaming-plugin.js","names":[],"sources":["../../../../src/lib/ssr/defer-streaming-plugin.ts"],"sourcesContent":["import type { Plugin } from 'vite';\nimport { parseSync, type Argument, type Statement } from 'oxc-parser';\n\n/** Minimum Angular major qualified for streaming; the actual runtime shape is checked below. */\nexport const MIN_STREAMING_ANGULAR_MAJOR = 21;\n\n/**\n * Whether the streaming SSR patch can be applied to the installed Angular.\n * A `null` major (version undetectable) returns `true`: don't block on a failed\n * detection — the plugin's anchor-drift detection catches a real mismatch at\n * build time and falls back to buffered rendering.\n */\nexport function streamingSupportedOnAngular(major: number | null): boolean {\n return major === null || major >= MIN_STREAMING_ANGULAR_MAJOR;\n}\n\ntype DeferAnalysis =\n | { kind: 'not-target' }\n | { kind: 'patchable'; offset: number }\n | { kind: 'drifted'; reason: string };\n\nfunction analyzeDeferRuntime(code: string): DeferAnalysis {\n if (!code.includes('function applyDeferBlockState('))\n return { kind: 'not-target' };\n const parsed = parseSync('angular-core.mjs', code);\n if (parsed.errors.length)\n return { kind: 'drifted', reason: 'invalid Angular core JavaScript' };\n const fn = parsed.program.body.find(\n (node) =>\n node.type === 'FunctionDeclaration' &&\n node.id?.name === 'applyDeferBlockState',\n );\n if (!fn || fn.type !== 'FunctionDeclaration' || !fn.body)\n return { kind: 'not-target' };\n const names = fn.params\n .map((param) => (param.type === 'Identifier' ? param.name : ''))\n .join(',');\n if (names !== 'newState,lDetails,lContainer,tNode,hostLView')\n return {\n kind: 'drifted',\n reason: 'changed applyDeferBlockState parameters',\n };\n const collector = parsed.program.body.some(\n (node) =>\n node.type === 'FunctionDeclaration' &&\n node.id?.name === 'collectNativeNodesInLContainer',\n );\n if (!collector)\n return {\n kind: 'drifted',\n reason: 'missing collectNativeNodesInLContainer',\n };\n const offset = profilerEndOffset(fn.body.body[fn.body.body.length - 1]);\n return offset === undefined\n ? { kind: 'drifted', reason: 'missing DeferBlockStateEnd profiler anchor' }\n : { kind: 'patchable', offset };\n}\n\nfunction profilerEndOffset(\n statement: Statement | undefined,\n): number | undefined {\n if (\n statement?.type !== 'ExpressionStatement' ||\n statement.expression.type !== 'CallExpression'\n )\n return;\n const call = statement.expression;\n if (\n call.callee.type !== 'Identifier' ||\n call.callee.name !== 'profiler' ||\n call.arguments.length !== 1\n )\n return;\n return isEndEvent(call.arguments[0]) ? statement.start : undefined;\n}\n\nfunction isEndEvent(event: Argument | undefined): boolean {\n if (event?.type === 'Literal') return typeof event.value === 'number';\n return (\n event?.type === 'MemberExpression' &&\n !event.computed &&\n event.object.type === 'Identifier' &&\n event.object.name === 'ProfilerEvent' &&\n event.property.type === 'Identifier' &&\n event.property.name === 'DeferBlockStateEnd'\n );\n}\n\nfunction patchDeferRuntime(code: string, offset: number): string {\n if (code.includes('globalThis.__analogSsrDeferCapture({')) return code;\n const capture =\n `try { if (newState === DeferBlockState.Complete && ` +\n `typeof ngServerMode !== 'undefined' && ngServerMode && ` +\n `typeof globalThis.__analogSsrDeferCapture === 'function') { ` +\n `globalThis.__analogSsrDeferCapture({ ssrUniqueId: lDetails[SSR_UNIQUE_ID], lContainer, hostLView }); } } catch (e) {}\\n `;\n return (\n code.slice(0, offset) +\n capture +\n code.slice(offset) +\n '\\nglobalThis.__analogSsrInternals = Object.assign(globalThis.__analogSsrInternals || {}, { collectNativeNodesInLContainer });\\n'\n );\n}\n\n/** Inject capture before the named defer function's final profiler call, preserving its original source. */\nexport function injectDeferStreamingHook(code: string): string | null {\n const info = analyzeDeferRuntime(code);\n return info.kind === 'patchable'\n ? patchDeferRuntime(code, info.offset)\n : null;\n}\n\n/** Inspect the named function's final profiler call without depending on enum ordinals. */\nexport function inspectAngularCoreModule(\n code: string,\n):\n | { kind: 'not-target' }\n | { kind: 'patchable' }\n | { kind: 'drifted'; reason: string } {\n const info = analyzeDeferRuntime(code);\n return info.kind === 'patchable' ? { kind: 'patchable' } : info;\n}\n\n/**\n * Vite plugin that applies {@link injectDeferStreamingHook} to `@angular/core`\n * during SSR builds, so `@analogjs/router`'s `renderStream` can flush `@defer`\n * blocks as they resolve on the server — without hand-patching node_modules.\n *\n * Only active for SSR. Mirrors the enforce/filter/ssr-gate shape of\n * `i18nDefRegistryPlugin`. If the `@defer` runtime module is found but its\n * anchors have drifted (Angular changed internals), or if it is never\n * encountered at all, the plugin warns rather than silently producing a build\n * that falls back to buffered rendering.\n */\nexport function deferStreamingPlugin(): Plugin {\n const applied = new Set<string>();\n const warnedDrift = new Set<string>();\n return {\n name: 'analogjs-defer-streaming',\n enforce: 'post',\n config() {\n return {\n ssr: { noExternal: ['@angular/core'] },\n environments: { ssr: { optimizeDeps: { exclude: ['@angular/core'] } } },\n };\n },\n transform: {\n filter: {\n id: /\\/@angular\\/core\\//,\n },\n handler(code, _id, options) {\n if (!options?.ssr) return;\n const environment = this.environment.name;\n const info = analyzeDeferRuntime(code);\n if (info.kind === 'not-target') return;\n if (info.kind === 'drifted') {\n if (!warnedDrift.has(environment)) {\n warnedDrift.add(environment);\n this.warn(\n `experimental streaming SSR: found @angular/core's @defer runtime ` +\n `but could not apply the resolution hook (${info.reason}). The ` +\n `installed Angular version likely changed internals the patch ` +\n `depends on; streaming will fall back to buffered rendering.`,\n );\n }\n return;\n }\n const out = patchDeferRuntime(code, info.offset);\n applied.add(environment);\n return { code: out };\n },\n },\n buildEnd() {\n // Require coverage from the SSR service. A later Nitro rebundle may\n // also encounter Angular modules imported by endpoint handlers.\n const environment = this.environment.name;\n if (environment !== 'ssr') return;\n if (!applied.has(environment) && !warnedDrift.has(environment)) {\n this.warn(\n `experimental streaming SSR is enabled but @angular/core's @defer ` +\n `runtime module was never encountered during the SSR build, so the ` +\n `resolution hook was not injected. Streaming will fall back to ` +\n `buffered rendering.`,\n );\n }\n },\n };\n}\n"],"mappings":";;;;;;;AAYA,SAAgB,4BAA4B,OAA+B;
|
|
1
|
+
{"version":3,"file":"defer-streaming-plugin.js","names":[],"sources":["../../../../src/lib/ssr/defer-streaming-plugin.ts"],"sourcesContent":["import type { Plugin } from 'vite';\nimport { parseSync, type Argument, type Statement } from 'oxc-parser';\n\n/** Minimum Angular major qualified for streaming; the actual runtime shape is checked below. */\nexport const MIN_STREAMING_ANGULAR_MAJOR = 21;\n\n/**\n * Whether the streaming SSR patch can be applied to the installed Angular.\n * A `null` major (version undetectable) returns `true`: don't block on a failed\n * detection — the plugin's anchor-drift detection catches a real mismatch at\n * build time and falls back to buffered rendering.\n */\nexport function streamingSupportedOnAngular(major: number | null): boolean {\n return major === null || major >= MIN_STREAMING_ANGULAR_MAJOR;\n}\n\ntype DeferAnalysis =\n | { kind: 'not-target' }\n | { kind: 'patchable'; offset: number }\n | { kind: 'drifted'; reason: string };\n\nfunction analyzeDeferRuntime(code: string): DeferAnalysis {\n if (!code.includes('function applyDeferBlockState('))\n return { kind: 'not-target' };\n const parsed = parseSync('angular-core.mjs', code);\n if (parsed.errors.length)\n return { kind: 'drifted', reason: 'invalid Angular core JavaScript' };\n const fn = parsed.program.body.find(\n (node) =>\n node.type === 'FunctionDeclaration' &&\n node.id?.name === 'applyDeferBlockState',\n );\n if (!fn || fn.type !== 'FunctionDeclaration' || !fn.body)\n return { kind: 'not-target' };\n const names = fn.params\n .map((param) => (param.type === 'Identifier' ? param.name : ''))\n .join(',');\n if (names !== 'newState,lDetails,lContainer,tNode,hostLView')\n return {\n kind: 'drifted',\n reason: 'changed applyDeferBlockState parameters',\n };\n const collector = parsed.program.body.some(\n (node) =>\n node.type === 'FunctionDeclaration' &&\n node.id?.name === 'collectNativeNodesInLContainer',\n );\n if (!collector)\n return {\n kind: 'drifted',\n reason: 'missing collectNativeNodesInLContainer',\n };\n const offset = profilerEndOffset(fn.body.body[fn.body.body.length - 1]);\n return offset === undefined\n ? { kind: 'drifted', reason: 'missing DeferBlockStateEnd profiler anchor' }\n : { kind: 'patchable', offset };\n}\n\nfunction profilerEndOffset(\n statement: Statement | undefined,\n): number | undefined {\n if (\n statement?.type !== 'ExpressionStatement' ||\n statement.expression.type !== 'CallExpression'\n )\n return;\n const call = statement.expression;\n if (\n call.callee.type !== 'Identifier' ||\n call.callee.name !== 'profiler' ||\n call.arguments.length !== 1\n )\n return;\n return isEndEvent(call.arguments[0]) ? statement.start : undefined;\n}\n\nfunction isEndEvent(event: Argument | undefined): boolean {\n if (event?.type === 'Literal') return typeof event.value === 'number';\n return (\n event?.type === 'MemberExpression' &&\n !event.computed &&\n event.object.type === 'Identifier' &&\n event.object.name === 'ProfilerEvent' &&\n event.property.type === 'Identifier' &&\n event.property.name === 'DeferBlockStateEnd'\n );\n}\n\nfunction patchDeferRuntime(code: string, offset: number): string {\n if (code.includes('globalThis.__analogSsrDeferCapture({')) return code;\n const capture =\n `try { if (newState === DeferBlockState.Complete && ` +\n `typeof ngServerMode !== 'undefined' && ngServerMode && ` +\n `typeof globalThis.__analogSsrDeferCapture === 'function') { ` +\n `globalThis.__analogSsrDeferCapture({ ssrUniqueId: lDetails[SSR_UNIQUE_ID], lContainer, hostLView }); } } catch (e) {}\\n `;\n return (\n code.slice(0, offset) +\n capture +\n code.slice(offset) +\n '\\nglobalThis.__analogSsrInternals = Object.assign(globalThis.__analogSsrInternals || {}, { collectNativeNodesInLContainer });\\n'\n );\n}\n\n/** Inject capture before the named defer function's final profiler call, preserving its original source. */\nexport function injectDeferStreamingHook(code: string): string | null {\n const info = analyzeDeferRuntime(code);\n return info.kind === 'patchable'\n ? patchDeferRuntime(code, info.offset)\n : null;\n}\n\n/** Inspect the named function's final profiler call without depending on enum ordinals. */\nexport function inspectAngularCoreModule(\n code: string,\n):\n | { kind: 'not-target' }\n | { kind: 'patchable' }\n | { kind: 'drifted'; reason: string } {\n const info = analyzeDeferRuntime(code);\n return info.kind === 'patchable' ? { kind: 'patchable' } : info;\n}\n\n/**\n * Vite plugin that applies {@link injectDeferStreamingHook} to `@angular/core`\n * during SSR builds, so `@analogjs/router`'s `renderStream` can flush `@defer`\n * blocks as they resolve on the server — without hand-patching node_modules.\n *\n * Only active for SSR. Mirrors the enforce/filter/ssr-gate shape of\n * `i18nDefRegistryPlugin`. If the `@defer` runtime module is found but its\n * anchors have drifted (Angular changed internals), or if it is never\n * encountered at all, the plugin warns rather than silently producing a build\n * that falls back to buffered rendering.\n */\nexport function deferStreamingPlugin(): Plugin {\n const applied = new Set<string>();\n const warnedDrift = new Set<string>();\n return {\n name: 'analogjs-defer-streaming',\n enforce: 'post',\n config() {\n return {\n ssr: { noExternal: ['@angular/core'] },\n environments: { ssr: { optimizeDeps: { exclude: ['@angular/core'] } } },\n };\n },\n transform: {\n filter: {\n id: /\\/@angular\\/core\\//,\n },\n handler(code, _id, options) {\n if (!options?.ssr) return;\n const environment = this.environment.name;\n const info = analyzeDeferRuntime(code);\n if (info.kind === 'not-target') return;\n if (info.kind === 'drifted') {\n if (!warnedDrift.has(environment)) {\n warnedDrift.add(environment);\n this.warn(\n `experimental streaming SSR: found @angular/core's @defer runtime ` +\n `but could not apply the resolution hook (${info.reason}). The ` +\n `installed Angular version likely changed internals the patch ` +\n `depends on; streaming will fall back to buffered rendering.`,\n );\n }\n return;\n }\n const out = patchDeferRuntime(code, info.offset);\n applied.add(environment);\n return { code: out };\n },\n },\n buildEnd() {\n // Require coverage from the SSR service. A later Nitro rebundle may\n // also encounter Angular modules imported by endpoint handlers.\n const environment = this.environment.name;\n if (environment !== 'ssr') return;\n if (!applied.has(environment) && !warnedDrift.has(environment)) {\n this.warn(\n `experimental streaming SSR is enabled but @angular/core's @defer ` +\n `runtime module was never encountered during the SSR build, so the ` +\n `resolution hook was not injected. Streaming will fall back to ` +\n `buffered rendering.`,\n );\n }\n },\n };\n}\n"],"mappings":"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"}
|