@analogjs/platform 3.0.0-alpha.23 → 3.0.0-alpha.25
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/package.json +5 -4
- package/src/lib/content-plugin.js +6 -1
- package/src/lib/content-plugin.js.map +1 -1
- package/src/lib/deps-plugin.js +1 -0
- package/src/lib/deps-plugin.js.map +1 -1
- package/src/lib/discover-library-routes.js +1 -1
- package/src/lib/nx-plugin/node_modules/.pnpm/es-toolkit@1.45.1/node_modules/es-toolkit/dist/string/camelCase.js +13 -0
- package/src/lib/nx-plugin/node_modules/.pnpm/es-toolkit@1.45.1/node_modules/es-toolkit/dist/string/camelCase.js.map +1 -0
- package/src/lib/nx-plugin/node_modules/.pnpm/es-toolkit@1.45.1/node_modules/es-toolkit/dist/string/capitalize.js +8 -0
- package/src/lib/nx-plugin/node_modules/.pnpm/es-toolkit@1.45.1/node_modules/es-toolkit/dist/string/capitalize.js.map +1 -0
- package/src/lib/nx-plugin/node_modules/.pnpm/es-toolkit@1.45.1/node_modules/es-toolkit/dist/string/words.js +9 -0
- package/src/lib/nx-plugin/node_modules/.pnpm/es-toolkit@1.45.1/node_modules/es-toolkit/dist/string/words.js.map +1 -0
- package/src/lib/nx-plugin/src/generators/app/lib/add-tailwind-config.js +4 -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.d.ts +1 -0
- package/src/lib/nx-plugin/src/generators/app/lib/add-tailwind-helpers.js +13 -0
- package/src/lib/nx-plugin/src/generators/app/lib/add-tailwind-helpers.js.map +1 -1
- package/src/lib/nx-plugin/src/generators/app/versions/nx_18_X/versions.d.ts +7 -5
- package/src/lib/nx-plugin/src/generators/app/versions/nx_18_X/versions.js +4 -0
- package/src/lib/nx-plugin/src/generators/app/versions/nx_18_X/versions.js.map +1 -1
- package/src/lib/nx-plugin/src/generators/app/versions/tailwind-dependencies.d.ts +1 -1
- package/src/lib/nx-plugin/src/generators/app/versions/tailwind-dependencies.js +2 -0
- package/src/lib/nx-plugin/src/generators/app/versions/tailwind-dependencies.js.map +1 -1
- package/src/lib/nx-plugin/src/generators/page/generator.js +2 -1
- package/src/lib/nx-plugin/src/generators/page/generator.js.map +1 -1
- package/src/lib/nx-plugin/src/utils/versions/ng_19_X/versions.d.ts +5 -5
- package/src/lib/nx-plugin/src/utils/versions/ng_19_X/versions.js +5 -5
- package/src/lib/nx-plugin/src/utils/versions/ng_19_X/versions.js.map +1 -1
- package/src/lib/options.d.ts +8 -2
- package/src/lib/platform-plugin.js +13 -16
- package/src/lib/platform-plugin.js.map +1 -1
- package/src/lib/route-idiom-diagnostics.d.ts +13 -0
- package/src/lib/route-idiom-diagnostics.js +160 -0
- package/src/lib/route-idiom-diagnostics.js.map +1 -0
- package/src/lib/route-manifest.d.ts +0 -6
- package/src/lib/route-manifest.js.map +1 -1
- package/src/lib/router-plugin.js +41 -0
- package/src/lib/router-plugin.js.map +1 -1
- package/src/lib/tailwind-preprocessor.js +1 -1
- package/src/lib/typed-routes-plugin.js +1 -1
- package/src/lib/utils/debug-harness.d.ts +23 -0
- package/src/lib/utils/debug-harness.js +88 -0
- package/src/lib/utils/debug-harness.js.map +1 -0
- package/src/lib/utils/debug-log-file.d.ts +5 -0
- package/src/lib/utils/debug-log-file.js +56 -0
- package/src/lib/utils/debug-log-file.js.map +1 -0
- package/src/lib/utils/debug.d.ts +9 -28
- package/src/lib/utils/debug.js +18 -67
- package/src/lib/utils/debug.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"platform-plugin.js","names":[],"sources":["../../../src/lib/platform-plugin.ts"],"sourcesContent":["import { Plugin } from 'vite';\nimport viteNitroPlugin from '@analogjs/vite-plugin-nitro';\nimport angular from '@analogjs/vite-plugin-angular';\n\nimport { Options } from './options.js';\nimport {\n activateDeferredDebug,\n applyDebugOption,\n debugPlatform,\n} from './utils/debug.js';\nimport { discoverLibraryRoutes } from './discover-library-routes.js';\nimport { routerPlugin } from './router-plugin.js';\nimport { ssrBuildPlugin } 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 { serverModePlugin } from '../server-mode-plugin.js';\nimport { routeGenerationPlugin } from './route-generation-plugin.js';\n\n// Bridge Plugin types from external @analogjs packages that resolve a different vite instance\nfunction externalPlugins(plugins: unknown): Plugin[] {\n return plugins as Plugin[];\n}\n\nexport function platformPlugin(opts: Options = {}): Plugin[] {\n applyDebugOption(opts.debug);\n\n const isTest = process.env['NODE_ENV'] === 'test' || !!process.env['VITEST'];\n const viteOptions = opts?.vite === false ? undefined : opts?.vite;\n const { ...platformOptions } = {\n ssr: true,\n ...opts,\n };\n if (platformOptions.discoverRoutes) {\n const workspaceRoot =\n platformOptions.workspaceRoot ??\n process.env['NX_WORKSPACE_ROOT'] ??\n process.cwd();\n const discovered = discoverLibraryRoutes(workspaceRoot);\n platformOptions.additionalPagesDirs =
|
|
1
|
+
{"version":3,"file":"platform-plugin.js","names":[],"sources":["../../../src/lib/platform-plugin.ts"],"sourcesContent":["import { Plugin } from 'vite';\nimport viteNitroPlugin from '@analogjs/vite-plugin-nitro';\nimport angular from '@analogjs/vite-plugin-angular';\nimport { mapValues, union } from 'es-toolkit';\n\nimport { Options } from './options.js';\nimport {\n activateDeferredDebug,\n applyDebugOption,\n debugPlatform,\n} from './utils/debug.js';\nimport { discoverLibraryRoutes } from './discover-library-routes.js';\nimport { routerPlugin } from './router-plugin.js';\nimport { ssrBuildPlugin } 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 { serverModePlugin } from '../server-mode-plugin.js';\nimport { routeGenerationPlugin } from './route-generation-plugin.js';\n\n// Bridge Plugin types from external @analogjs packages that resolve a different vite instance\nfunction externalPlugins(plugins: unknown): Plugin[] {\n return plugins as Plugin[];\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 viteOptions = opts?.vite === false ? undefined : opts?.vite;\n const { ...platformOptions } = {\n ssr: true,\n ...opts,\n };\n if (platformOptions.discoverRoutes) {\n const workspaceRoot =\n platformOptions.workspaceRoot ??\n process.env['NX_WORKSPACE_ROOT'] ??\n process.cwd();\n const discovered = discoverLibraryRoutes(workspaceRoot);\n platformOptions.additionalPagesDirs = union(\n platformOptions.additionalPagesDirs ?? [],\n discovered.additionalPagesDirs,\n );\n platformOptions.additionalContentDirs = union(\n platformOptions.additionalContentDirs ?? [],\n discovered.additionalContentDirs,\n );\n platformOptions.additionalAPIDirs = union(\n platformOptions.additionalAPIDirs ?? [],\n discovered.additionalAPIDirs,\n );\n }\n\n const useAngularCompilationAPI =\n platformOptions.experimental?.useAngularCompilationAPI ??\n viteOptions?.experimental?.useAngularCompilationAPI;\n debugPlatform('experimental options resolved', {\n useAngularCompilationAPI: !!useAngularCompilationAPI,\n typedRouter: platformOptions.experimental?.typedRouter,\n });\n let nitroOptions = platformOptions?.nitro;\n\n if (nitroOptions?.routeRules) {\n nitroOptions = {\n ...nitroOptions,\n routeRules: mapValues(nitroOptions.routeRules, (rule) => ({\n ...rule,\n headers: {\n ...rule.headers,\n 'x-analog-no-ssr': rule?.ssr === false ? 'true' : undefined,\n } as any,\n })),\n };\n }\n\n return [\n {\n name: 'analogjs-debug-activate',\n config(_, { command }) {\n activateDeferredDebug(command);\n },\n },\n ...externalPlugins(viteNitroPlugin(platformOptions as any, nitroOptions)),\n ...(platformOptions.ssr\n ? [...ssrBuildPlugin(), ...injectHTMLPlugin()]\n : []),\n ...(!isTest ? depsPlugin(platformOptions) : []),\n ...routerPlugin(platformOptions),\n routeGenerationPlugin(platformOptions),\n ...contentPlugin(platformOptions?.content, platformOptions),\n ...(opts?.vite === false\n ? []\n : externalPlugins(\n angular({\n jit: platformOptions.jit,\n workspaceRoot: platformOptions.workspaceRoot,\n // Let the Angular plugin keep its own dev-friendly default unless the\n // app explicitly opts into stricter serve-time diagnostics.\n disableTypeChecking: platformOptions.disableTypeChecking,\n include: [\n ...(platformOptions.include ?? []),\n ...(platformOptions.additionalPagesDirs ?? []).map(\n (pageDir) => `${pageDir}/**/*.page.ts`,\n ),\n ],\n additionalContentDirs: platformOptions.additionalContentDirs,\n hmr: platformOptions.hmr,\n liveReload: platformOptions.liveReload,\n inlineStylesExtension: platformOptions.inlineStylesExtension,\n fileReplacements: platformOptions.fileReplacements,\n debug: platformOptions.debug,\n ...(viteOptions ?? {}),\n experimental: {\n ...(viteOptions?.experimental ?? {}),\n useAngularCompilationAPI,\n },\n }),\n )),\n ...serverModePlugin(),\n ...clearClientPageEndpointsPlugin(),\n ];\n}\n"],"mappings":";;;;;;;;;;;;;;AAsBA,SAAS,gBAAgB,SAA4B;AACnD,QAAO;;AAGT,SAAgB,eAAe,OAAgB,EAAE,EAAY;AAC3D,kBAAiB,KAAK,OAAO,KAAK,cAAc;CAEhD,MAAM,SAAA,QAAA,IAAA,aAAqC,UAAU,CAAC,CAAC,QAAQ,IAAI;CACnE,MAAM,cAAc,MAAM,SAAS,QAAQ,KAAA,IAAY,MAAM;CAC7D,MAAM,EAAE,GAAG,oBAAoB;EAC7B,KAAK;EACL,GAAG;EACJ;AACD,KAAI,gBAAgB,gBAAgB;EAKlC,MAAM,aAAa,sBAHjB,gBAAgB,iBAChB,QAAQ,IAAI,wBACZ,QAAQ,KAAK,CACwC;AACvD,kBAAgB,sBAAsB,MACpC,gBAAgB,uBAAuB,EAAE,EACzC,WAAW,oBACZ;AACD,kBAAgB,wBAAwB,MACtC,gBAAgB,yBAAyB,EAAE,EAC3C,WAAW,sBACZ;AACD,kBAAgB,oBAAoB,MAClC,gBAAgB,qBAAqB,EAAE,EACvC,WAAW,kBACZ;;CAGH,MAAM,2BACJ,gBAAgB,cAAc,4BAC9B,aAAa,cAAc;AAC7B,eAAc,iCAAiC;EAC7C,0BAA0B,CAAC,CAAC;EAC5B,aAAa,gBAAgB,cAAc;EAC5C,CAAC;CACF,IAAI,eAAe,iBAAiB;AAEpC,KAAI,cAAc,WAChB,gBAAe;EACb,GAAG;EACH,YAAY,UAAU,aAAa,aAAa,UAAU;GACxD,GAAG;GACH,SAAS;IACP,GAAG,KAAK;IACR,mBAAmB,MAAM,QAAQ,QAAQ,SAAS,KAAA;IACnD;GACF,EAAE;EACJ;AAGH,QAAO;EACL;GACE,MAAM;GACN,OAAO,GAAG,EAAE,WAAW;AACrB,0BAAsB,QAAQ;;GAEjC;EACD,GAAG,gBAAgB,gBAAgB,iBAAwB,aAAa,CAAC;EACzE,GAAI,gBAAgB,MAChB,CAAC,GAAG,gBAAgB,EAAE,GAAG,kBAAkB,CAAC,GAC5C,EAAE;EACN,GAAI,CAAC,SAAS,WAAW,gBAAgB,GAAG,EAAE;EAC9C,GAAG,aAAa,gBAAgB;EAChC,sBAAsB,gBAAgB;EACtC,GAAG,cAAc,iBAAiB,SAAS,gBAAgB;EAC3D,GAAI,MAAM,SAAS,QACf,EAAE,GACF,gBACE,QAAQ;GACN,KAAK,gBAAgB;GACrB,eAAe,gBAAgB;GAG/B,qBAAqB,gBAAgB;GACrC,SAAS,CACP,GAAI,gBAAgB,WAAW,EAAE,EACjC,IAAI,gBAAgB,uBAAuB,EAAE,EAAE,KAC5C,YAAY,GAAG,QAAQ,eACzB,CACF;GACD,uBAAuB,gBAAgB;GACvC,KAAK,gBAAgB;GACrB,YAAY,gBAAgB;GAC5B,uBAAuB,gBAAgB;GACvC,kBAAkB,gBAAgB;GAClC,OAAO,gBAAgB;GACvB,GAAI,eAAe,EAAE;GACrB,cAAc;IACZ,GAAI,aAAa,gBAAgB,EAAE;IACnC;IACD;GACF,CAAC,CACH;EACL,GAAG,kBAAkB;EACrB,GAAG,gCAAgC;EACpC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface AnalogRouteIdiomDiagnostic {
|
|
2
|
+
code: string;
|
|
3
|
+
severity: "error" | "warning";
|
|
4
|
+
message: string;
|
|
5
|
+
details?: string;
|
|
6
|
+
}
|
|
7
|
+
export interface AnalyzeAnalogRouteFileOptions {
|
|
8
|
+
filename: string;
|
|
9
|
+
code: string;
|
|
10
|
+
routeFiles?: string[];
|
|
11
|
+
}
|
|
12
|
+
export declare function analyzeAnalogRouteFile(options: AnalyzeAnalogRouteFileOptions): AnalogRouteIdiomDiagnostic[];
|
|
13
|
+
export declare function formatAnalogRouteIdiomDiagnostic(diagnostic: AnalogRouteIdiomDiagnostic, filename: string, workspaceRoot: string): string;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { relative } from "node:path";
|
|
2
|
+
import { normalizePath } from "vite";
|
|
3
|
+
import { parseSync } from "oxc-parser";
|
|
4
|
+
//#region packages/platform/src/lib/route-idiom-diagnostics.ts
|
|
5
|
+
var PAGE_FILE_RE = /\.page\.ts$/;
|
|
6
|
+
var ROUTER_OUTLET_RE = /\bRouterOutlet\b|<router-outlet(?:\s|>)/;
|
|
7
|
+
function analyzeAnalogRouteFile(options) {
|
|
8
|
+
const { filename, code, routeFiles = [] } = options;
|
|
9
|
+
const parseResult = parseSync(filename, code, {
|
|
10
|
+
lang: "ts",
|
|
11
|
+
sourceType: "module",
|
|
12
|
+
range: true,
|
|
13
|
+
showSemanticErrors: true
|
|
14
|
+
});
|
|
15
|
+
const parseDiagnostics = parseResult.errors.map((error) => toParseDiagnostic(error));
|
|
16
|
+
if (parseDiagnostics.some((diagnostic) => diagnostic.severity === "error")) return parseDiagnostics;
|
|
17
|
+
const program = parseResult.program;
|
|
18
|
+
const exportedBindings = collectExportedBindings(program);
|
|
19
|
+
const routeMetaBindingName = exportedBindings.routeMeta;
|
|
20
|
+
const routeJsonLdBindingName = exportedBindings.routeJsonLd;
|
|
21
|
+
const diagnostics = [...parseDiagnostics];
|
|
22
|
+
const routeMetaInfo = routeMetaBindingName ? getRouteMetaInfo(program, routeMetaBindingName) : null;
|
|
23
|
+
if (!exportedBindings.hasDefaultExport && !routeMetaInfo?.hasRedirect) diagnostics.push({
|
|
24
|
+
code: "missing-default-export",
|
|
25
|
+
severity: "warning",
|
|
26
|
+
message: "Route files should default-export the page component, unless they are redirect-only routes.",
|
|
27
|
+
details: "Add `export default class ...` or define `routeMeta.redirectTo` for a redirect route."
|
|
28
|
+
});
|
|
29
|
+
if (exportedBindings.hasDefaultExport && routeMetaInfo?.hasRedirect) diagnostics.push({
|
|
30
|
+
code: "redirect-with-component",
|
|
31
|
+
severity: "warning",
|
|
32
|
+
message: "Redirect routes should not also export a page component.",
|
|
33
|
+
details: "Analog ignores the default export when `routeMeta.redirectTo` is present. Remove the component export or remove the redirect."
|
|
34
|
+
});
|
|
35
|
+
if (routeMetaInfo?.hasRedirect && routeMetaInfo.redirectTo && !routeMetaInfo.redirectTo.startsWith("/")) diagnostics.push({
|
|
36
|
+
code: "relative-redirect",
|
|
37
|
+
severity: "warning",
|
|
38
|
+
message: "`routeMeta.redirectTo` should use an absolute path.",
|
|
39
|
+
details: "Nested redirects are documented to use absolute targets such as `/cities/new-york`."
|
|
40
|
+
});
|
|
41
|
+
if (routeJsonLdBindingName) diagnostics.push({
|
|
42
|
+
code: "legacy-route-jsonld-export",
|
|
43
|
+
severity: "warning",
|
|
44
|
+
message: "Prefer `routeMeta.jsonLd` over the legacy top-level `routeJsonLd` export.",
|
|
45
|
+
details: "Keeping JSON-LD inside `routeMeta` makes the route module easier to read and matches the current docs."
|
|
46
|
+
});
|
|
47
|
+
if (isLikelyLayoutRoute(filename, routeFiles) && !ROUTER_OUTLET_RE.test(code)) diagnostics.push({
|
|
48
|
+
code: "layout-without-router-outlet",
|
|
49
|
+
severity: "warning",
|
|
50
|
+
message: "This route file looks like a layout shell, but it does not reference `RouterOutlet` or `<router-outlet>`.",
|
|
51
|
+
details: "Parent layout pages usually import `RouterOutlet` and render an outlet so child routes have somewhere to mount."
|
|
52
|
+
});
|
|
53
|
+
return diagnostics;
|
|
54
|
+
}
|
|
55
|
+
function formatAnalogRouteIdiomDiagnostic(diagnostic, filename, workspaceRoot) {
|
|
56
|
+
const header = `[Analog] ${toDisplayPath(filename, workspaceRoot)} (${diagnostic.code})`;
|
|
57
|
+
const severity = diagnostic.severity.toUpperCase();
|
|
58
|
+
if (diagnostic.details) return `${header}\n${severity}: ${diagnostic.message}\n${diagnostic.details}`;
|
|
59
|
+
return `${header}\n${severity}: ${diagnostic.message}`;
|
|
60
|
+
}
|
|
61
|
+
function toDisplayPath(filename, workspaceRoot) {
|
|
62
|
+
const normalizedFilename = normalizePath(filename);
|
|
63
|
+
const relativePath = normalizePath(relative(normalizePath(workspaceRoot), normalizedFilename));
|
|
64
|
+
if (relativePath && !relativePath.startsWith("..")) return `/${relativePath}`;
|
|
65
|
+
return normalizedFilename;
|
|
66
|
+
}
|
|
67
|
+
function toParseDiagnostic(error) {
|
|
68
|
+
return {
|
|
69
|
+
code: "oxc-parse",
|
|
70
|
+
severity: error.severity === severityError ? "error" : "warning",
|
|
71
|
+
message: error.message,
|
|
72
|
+
details: error.codeframe ?? error.helpMessage ?? void 0
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
var severityError = "Error";
|
|
76
|
+
function isLikelyLayoutRoute(filename, routeFiles) {
|
|
77
|
+
if (!PAGE_FILE_RE.test(filename)) return false;
|
|
78
|
+
const routeStem = filename.replace(PAGE_FILE_RE, "");
|
|
79
|
+
return routeFiles.some((routeFile) => routeFile !== filename && routeFile.startsWith(`${routeStem}/`));
|
|
80
|
+
}
|
|
81
|
+
function getRouteMetaInfo(program, bindingName) {
|
|
82
|
+
const routeMetaNode = unwrapRouteMetaObject(getExportedBindingInitializer(program, bindingName));
|
|
83
|
+
if (!routeMetaNode) return null;
|
|
84
|
+
let redirectTo;
|
|
85
|
+
for (const property of routeMetaNode.properties ?? []) {
|
|
86
|
+
if (property?.type !== "Property") continue;
|
|
87
|
+
if (getPropertyName(property.key) !== "redirectTo") continue;
|
|
88
|
+
redirectTo = getStringValue(property.value);
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
hasRedirect: typeof redirectTo === "string" && redirectTo.length > 0,
|
|
92
|
+
redirectTo
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function unwrapRouteMetaObject(initializer) {
|
|
96
|
+
if (!initializer) return null;
|
|
97
|
+
if (initializer.type === "ObjectExpression") return initializer;
|
|
98
|
+
if (initializer.type === "CallExpression" && initializer.callee?.type === "Identifier" && initializer.callee.name === "defineRouteMeta") {
|
|
99
|
+
const firstArgument = initializer.arguments?.[0];
|
|
100
|
+
return firstArgument?.type === "ObjectExpression" ? firstArgument : null;
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
function getExportedBindingInitializer(program, bindingName) {
|
|
105
|
+
for (const statement of program.body ?? []) {
|
|
106
|
+
if (statement?.type === "VariableDeclaration") {
|
|
107
|
+
const initializer = getVariableInitializer(statement, bindingName);
|
|
108
|
+
if (initializer) return initializer;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (statement?.type === "ExportNamedDeclaration" && statement.declaration?.type === "VariableDeclaration") {
|
|
112
|
+
const initializer = getVariableInitializer(statement.declaration, bindingName);
|
|
113
|
+
if (initializer) return initializer;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function getVariableInitializer(declaration, bindingName) {
|
|
118
|
+
for (const declarator of declaration.declarations ?? []) if (declarator.id?.type === "Identifier" && declarator.id.name === bindingName) return declarator.init;
|
|
119
|
+
}
|
|
120
|
+
function collectExportedBindings(program) {
|
|
121
|
+
let hasDefaultExport = false;
|
|
122
|
+
let routeMeta;
|
|
123
|
+
let routeJsonLd;
|
|
124
|
+
for (const statement of program.body ?? []) {
|
|
125
|
+
if (statement?.type === "ExportDefaultDeclaration") {
|
|
126
|
+
hasDefaultExport = true;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (statement?.type !== "ExportNamedDeclaration") continue;
|
|
130
|
+
const declaration = statement.declaration;
|
|
131
|
+
if (declaration?.type === "VariableDeclaration") for (const declarator of declaration.declarations ?? []) {
|
|
132
|
+
if (declarator.id?.type !== "Identifier") continue;
|
|
133
|
+
if (declarator.id.name === "routeMeta") routeMeta = "routeMeta";
|
|
134
|
+
if (declarator.id.name === "routeJsonLd") routeJsonLd = "routeJsonLd";
|
|
135
|
+
}
|
|
136
|
+
for (const specifier of statement.specifiers ?? []) {
|
|
137
|
+
if (specifier?.type !== "ExportSpecifier" || specifier.local?.type !== "Identifier") continue;
|
|
138
|
+
if (specifier.exported?.type !== "Identifier") continue;
|
|
139
|
+
if (specifier.exported.name === "routeMeta") routeMeta = specifier.local.name;
|
|
140
|
+
if (specifier.exported.name === "routeJsonLd") routeJsonLd = specifier.local.name;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
hasDefaultExport,
|
|
145
|
+
routeMeta,
|
|
146
|
+
routeJsonLd
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function getPropertyName(node) {
|
|
150
|
+
if (node?.type === "Identifier") return node.name;
|
|
151
|
+
if ((node?.type === "Literal" || node?.type === "StringLiteral") && typeof node.value === "string") return node.value;
|
|
152
|
+
}
|
|
153
|
+
function getStringValue(node) {
|
|
154
|
+
if ((node?.type === "Literal" || node?.type === "StringLiteral") && typeof node.value === "string") return node.value;
|
|
155
|
+
if (node?.type === "TemplateLiteral" && node.expressions?.length === 0 && node.quasis?.length === 1) return node.quasis[0].value.cooked ?? node.quasis[0].value.raw;
|
|
156
|
+
}
|
|
157
|
+
//#endregion
|
|
158
|
+
export { analyzeAnalogRouteFile, formatAnalogRouteIdiomDiagnostic };
|
|
159
|
+
|
|
160
|
+
//# sourceMappingURL=route-idiom-diagnostics.js.map
|
|
@@ -0,0 +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 routeJsonLdBindingName = exportedBindings.routeJsonLd;\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 (routeJsonLdBindingName) {\n diagnostics.push({\n code: 'legacy-route-jsonld-export',\n severity: 'warning',\n message:\n 'Prefer `routeMeta.jsonLd` over the legacy top-level `routeJsonLd` export.',\n details:\n 'Keeping JSON-LD inside `routeMeta` makes the route module easier to read and matches the current docs.',\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 initializer = getExportedBindingInitializer(program, bindingName);\n const routeMetaNode = unwrapRouteMetaObject(initializer);\n\n if (!routeMetaNode) {\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 unwrapRouteMetaObject(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n initializer: any,\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n): any | null {\n if (!initializer) {\n return null;\n }\n\n if (initializer.type === 'ObjectExpression') {\n return initializer;\n }\n\n if (\n initializer.type === 'CallExpression' &&\n initializer.callee?.type === 'Identifier' &&\n initializer.callee.name === 'defineRouteMeta'\n ) {\n const firstArgument = initializer.arguments?.[0];\n return firstArgument?.type === 'ObjectExpression' ? firstArgument : null;\n }\n\n return null;\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 routeJsonLd?: string;\n} {\n let hasDefaultExport = false;\n let routeMeta: string | undefined;\n let routeJsonLd: 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 if (declarator.id.name === 'routeJsonLd') {\n routeJsonLd = 'routeJsonLd';\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 if (specifier.exported.name === 'routeJsonLd') {\n routeJsonLd = specifier.local.name;\n }\n }\n }\n\n return {\n hasDefaultExport,\n routeMeta,\n routeJsonLd,\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,9 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Route-manifest engine for typed file routes.
|
|
3
|
-
*
|
|
4
|
-
* Pure functions (no Angular dependencies) for converting discovered
|
|
5
|
-
* filenames into typed route manifests and generated declarations.
|
|
6
|
-
*/
|
|
7
1
|
export interface RouteParamInfo {
|
|
8
2
|
name: string;
|
|
9
3
|
type: "dynamic" | "catchAll" | "optionalCatchAll";
|
|
@@ -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 RouteSchemaInfo {\n hasParamsSchema: boolean;\n hasQuerySchema: boolean;\n}\n\nexport interface GenerateRouteTreeDeclarationOptions {\n jsonLdFiles?: Iterable<string>;\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 /** Schema export info (detected from file content) */\n schemas: RouteSchemaInfo;\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}\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}\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[\\\\/](?:routes|pages)|src[\\\\/]content)[\\\\/]` — non-anchored\n * fallback for legacy paths where the directory marker appears mid-string.\n * 3. `\\.page\\.(js|ts|analog|ag)$` — strips page file extensions.\n * 4. `\\.(ts|md|analog|ag)$` — strips remaining file extensions.\n *\n * Examples:\n * - '/app/routes/index.ts' -> '/'\n * - '/app/routes/about.ts' -> '/about'\n * - '/src/app/pages/users/[id].page.ts' -> '/users/[id]'\n * - '/app/routes/blog.[slug].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[\\\\/](?:routes|pages)|src[\\\\/]content)[\\\\/])|(\\.page\\.(js|ts|analog|ag)$)|(\\.(ts|md|analog|ag)$)/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 const processed: string[] = [];\n\n for (const segment of segments) {\n if (/^\\([^.[\\]]*\\)$/.test(segment)) continue;\n processed.push(segment);\n }\n\n if (processed.length > 0 && processed[processed.length - 1] === 'index') {\n processed.pop();\n }\n\n return '/' + processed.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[\\\\/](?:routes|pages)|src[\\\\/]content)[\\\\/])|(\\.page\\.(js|ts|analog|ag)$)|(\\.(ts|md|analog|ag)$)/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 * Detects whether a route file exports schema constants.\n */\nexport function detectSchemaExports(fileContent: string): RouteSchemaInfo {\n return {\n hasParamsSchema: /export\\s+const\\s+routeParamsSchema\\b/.test(fileContent),\n hasQuerySchema: /export\\s+const\\s+routeQuerySchema\\b/.test(fileContent),\n };\n}\n\nconst NO_SCHEMAS: RouteSchemaInfo = {\n hasParamsSchema: false,\n hasQuerySchema: false,\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 schemaDetector?: (filename: string) => RouteSchemaInfo,\n collisionPriority?: (filename: string) => number,\n): RouteManifest {\n const routes: RouteEntry[] = [];\n const collisions: RouteCollision[] = [];\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 for (const filename of prioritizedFilenames) {\n const fullPath = filenameToRoutePath(filename);\n const params = extractRouteParams(fullPath);\n const schemas = schemaDetector ? schemaDetector(filename) : NO_SCHEMAS;\n const id = filenameToRouteId(filename);\n const isPathlessLayout = isPathlessLayoutId(id);\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(fullPath)) {\n const winner = seenByFullPath.get(fullPath)!;\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|analog|ag)$/;\n const layoutRe = /\\.(page\\.)?(ts|js|analog|ag)$/;\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 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(fullPath, { filename, priority: currentPriority });\n }\n\n routes.push({\n id,\n path: fullPath,\n fullPath,\n params,\n filename,\n schemas,\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 });\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 }\n\n for (const route of routes) {\n if (route.schemas.hasParamsSchema && route.params.length === 0) {\n console.warn(\n `[Analog] Route '${route.fullPath}' exports routeParamsSchema` +\n ` but has no dynamic params in the filename.`,\n );\n }\n }\n\n // Build-time consistency check: every parentId and child reference must\n // point to a real route in the manifest. Invalid references indicate a\n // bug in the hierarchy computation.\n for (const route of routes) {\n if (route.parentId && !routeById.has(route.parentId)) {\n console.warn(\n `[Analog] Route '${route.id}' has parentId '${route.parentId}' ` +\n `which does not match any route id in the manifest.`,\n );\n }\n for (const childId of route.children) {\n if (!routeById.has(childId)) {\n console.warn(\n `[Analog] Route '${route.id}' lists child '${childId}' ` +\n `which does not match any route id in the manifest.`,\n );\n }\n }\n }\n\n return { routes, collisions, canonicalByFullPath: routeByFullPath };\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// Matches group names like (auth), (home) — intentionally excludes dots and\n// brackets so names like (auth.v2) or ([id]) are NOT treated as pathless\n// layouts. Dot-containing names collide with dynamic-segment syntax.\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('/src/app/routes/') ||\n filename.includes('/app/pages/') ||\n filename.includes('/app/routes/') ||\n filename.includes('/src/content/')\n ) {\n return 0;\n }\n\n return 1;\n}\n\n/**\n * Produces a human-readable summary of the generated route manifest.\n */\nexport function formatManifestSummary(manifest: RouteManifest): string {\n const lines: string[] = [];\n const total = manifest.routes.length;\n const withSchemas = manifest.routes.filter(\n (r) => r.schemas.hasParamsSchema || r.schemas.hasQuerySchema,\n ).length;\n const staticCount = manifest.routes.filter(\n (r) => r.params.length === 0,\n ).length;\n const dynamicCount = total - staticCount;\n\n lines.push(`[Analog] Generated typed routes:`);\n lines.push(\n ` ${total} routes (${staticCount} static, ${dynamicCount} dynamic)`,\n );\n if (withSchemas > 0) {\n lines.push(` ${withSchemas} with schema validation`);\n }\n\n for (const route of manifest.routes) {\n const flags: string[] = [];\n if (route.schemas.hasParamsSchema) flags.push('params-schema');\n if (route.schemas.hasQuerySchema) flags.push('query-schema');\n const suffix = flags.length > 0 ? ` [${flags.join(', ')}]` : '';\n lines.push(` ${route.fullPath}${suffix}`);\n }\n\n return lines.join('\\n');\n}\n\n/**\n * Generates the route-table section for the combined generated route module.\n */\nexport function generateRouteTableDeclaration(manifest: RouteManifest): string {\n const lines: string[] = [];\n const hasAnySchema = manifest.routes.some(\n (r) => r.schemas.hasParamsSchema || r.schemas.hasQuerySchema,\n );\n\n lines.push('// This file is auto-generated by @analogjs/platform');\n lines.push('// Do not edit manually');\n lines.push('');\n\n if (hasAnySchema) {\n lines.push(\n \"import type { StandardSchemaV1 } from '@standard-schema/spec';\",\n );\n }\n\n const schemaImports = new Map<string, string>();\n let aliasIndex = 0;\n\n for (const route of manifest.routes) {\n if (route.schemas.hasParamsSchema || route.schemas.hasQuerySchema) {\n const importPath = filenameToImportPath(route.filename);\n const names: string[] = [];\n\n if (route.schemas.hasParamsSchema) {\n const alias = `_p${aliasIndex}`;\n names.push(`routeParamsSchema as ${alias}`);\n schemaImports.set(`${route.fullPath}:params`, alias);\n }\n if (route.schemas.hasQuerySchema) {\n const alias = `_q${aliasIndex}`;\n names.push(`routeQuerySchema as ${alias}`);\n schemaImports.set(`${route.fullPath}:query`, alias);\n }\n\n lines.push(`import type { ${names.join(', ')} } from '${importPath}';`);\n aliasIndex++;\n }\n }\n\n if (hasAnySchema) {\n lines.push('');\n }\n\n lines.push(\"declare module '@analogjs/router' {\");\n lines.push(' interface AnalogRouteTable {');\n\n for (const route of manifest.canonicalByFullPath.values()) {\n const paramsAlias = schemaImports.get(`${route.fullPath}:params`);\n const queryAlias = schemaImports.get(`${route.fullPath}:query`);\n\n const paramsType = generateParamsType(route.params);\n const queryType = 'Record<string, string | string[] | undefined>';\n\n const paramsOutputType = paramsAlias\n ? `StandardSchemaV1.InferOutput<typeof ${paramsAlias}>`\n : paramsType;\n const queryOutputType = queryAlias\n ? `StandardSchemaV1.InferOutput<typeof ${queryAlias}>`\n : queryType;\n\n lines.push(` '${route.fullPath}': {`);\n lines.push(` params: ${paramsType};`);\n lines.push(` paramsOutput: ${paramsOutputType};`);\n lines.push(` query: ${queryType};`);\n lines.push(` queryOutput: ${queryOutputType};`);\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\n/**\n * Generates the route-tree section for the combined generated route module.\n */\nexport function generateRouteTreeDeclaration(\n manifest: RouteManifest,\n options: GenerateRouteTreeDeclarationOptions = {},\n): string {\n const lines: string[] = [];\n const jsonLdFiles = new Set(options.jsonLdFiles ?? []);\n\n lines.push('// This file is auto-generated by @analogjs/platform');\n lines.push('// Do not edit manually');\n lines.push('');\n lines.push('export interface AnalogGeneratedRouteRecord<');\n lines.push(' TId extends string = string,');\n lines.push(' TPath extends string = string,');\n lines.push(' TFullPath extends string = string,');\n lines.push(' TParentId extends string | null = string | null,');\n lines.push(' TChildren extends readonly string[] = readonly string[],');\n lines.push('> {');\n lines.push(' id: TId;');\n lines.push(' path: TPath;');\n lines.push(' fullPath: TFullPath;');\n lines.push(' parentId: TParentId;');\n lines.push(' children: TChildren;');\n lines.push(' sourceFile: string;');\n lines.push(\" kind: 'page' | 'content';\");\n lines.push(' hasParamsSchema: boolean;');\n lines.push(' hasQuerySchema: boolean;');\n lines.push(' hasJsonLd: boolean;');\n lines.push(' isIndex: boolean;');\n lines.push(' isGroup: boolean;');\n lines.push(' isCatchAll: boolean;');\n lines.push(' isOptionalCatchAll: boolean;');\n lines.push('}');\n lines.push('');\n lines.push('export interface AnalogFileRoutesById {');\n for (const route of manifest.routes) {\n lines.push(\n ` ${toTsKey(route.id)}: AnalogGeneratedRouteRecord<${toTsStringLiteral(route.id)}, ${toTsStringLiteral(route.path)}, ${toTsStringLiteral(route.fullPath)}, ${route.parentId ? toTsStringLiteral(route.parentId) : 'null'}, ${toReadonlyTupleType(route.children)}>;`,\n );\n }\n lines.push('}');\n lines.push('');\n lines.push('export interface AnalogFileRoutesByFullPath {');\n for (const route of manifest.canonicalByFullPath.values()) {\n lines.push(\n ` ${toTsKey(route.fullPath)}: AnalogFileRoutesById[${toTsStringLiteral(route.id)}];`,\n );\n }\n lines.push('}');\n lines.push('');\n lines.push('export type AnalogRouteTreeId = keyof AnalogFileRoutesById;');\n lines.push(\n 'export type AnalogRouteTreeFullPath = keyof AnalogFileRoutesByFullPath;',\n );\n lines.push('');\n lines.push('export const analogRouteTree = {');\n lines.push(' byId: {');\n for (const route of manifest.routes) {\n lines.push(` ${toObjectKey(route.id)}: {`);\n lines.push(` id: ${toTsStringLiteral(route.id)},`);\n lines.push(` path: ${toTsStringLiteral(route.path)},`);\n lines.push(` fullPath: ${toTsStringLiteral(route.fullPath)},`);\n lines.push(\n ` parentId: ${route.parentId ? toTsStringLiteral(route.parentId) : 'null'},`,\n );\n lines.push(` children: ${toReadonlyTupleValue(route.children)},`);\n lines.push(` sourceFile: ${toTsStringLiteral(route.filename)},`);\n lines.push(` kind: ${toTsStringLiteral(route.kind)},`);\n lines.push(\n ` hasParamsSchema: ${String(route.schemas.hasParamsSchema)},`,\n );\n lines.push(\n ` hasQuerySchema: ${String(route.schemas.hasQuerySchema)},`,\n );\n lines.push(` hasJsonLd: ${String(jsonLdFiles.has(route.filename))},`);\n lines.push(` isIndex: ${String(route.isIndex)},`);\n lines.push(` isGroup: ${String(route.isGroup)},`);\n lines.push(` isCatchAll: ${String(route.isCatchAll)},`);\n lines.push(\n ` isOptionalCatchAll: ${String(route.isOptionalCatchAll)},`,\n );\n lines.push(\n ` } satisfies AnalogFileRoutesById[${toTsStringLiteral(route.id)}],`,\n );\n }\n lines.push(' },');\n lines.push(' byFullPath: {');\n for (const route of manifest.canonicalByFullPath.values()) {\n lines.push(\n ` ${toObjectKey(route.fullPath)}: ${toTsStringLiteral(route.id)},`,\n );\n }\n lines.push(' },');\n lines.push('} as const;');\n lines.push('');\n\n return lines.join('\\n');\n}\n\nfunction filenameToImportPath(filename: string): string {\n const stripped = filename.replace(/^\\//, '').replace(/\\.ts$/, '');\n return '../' + stripped;\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 : `'${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\nfunction toTsStringLiteral(value: string): string {\n return JSON.stringify(value);\n}\n\nfunction toTsKey(value: string): string {\n return toTsStringLiteral(value);\n}\n\nfunction toObjectKey(value: string): string {\n return isValidIdentifier(value) ? value : toTsStringLiteral(value);\n}\n\nfunction toReadonlyTupleType(values: readonly string[]): string {\n if (values.length === 0) {\n return 'readonly []';\n }\n\n return `readonly [${values.map((value) => toTsStringLiteral(value)).join(', ')}]`;\n}\n\nfunction toReadonlyTupleValue(values: readonly string[]): string {\n if (values.length === 0) {\n return '[] as const';\n }\n\n return `[${values.map((value) => toTsStringLiteral(value)).join(', ')}] as const`;\n}\n\n// --- JSON-LD utilities ---\n\nexport type JsonLdObject = Record<string, unknown>;\n\nexport function isJsonLdObject(value: unknown): value is JsonLdObject {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nexport function normalizeJsonLd(value: unknown): JsonLdObject[] {\n if (Array.isArray(value)) {\n return value.filter(isJsonLdObject);\n }\n\n return isJsonLdObject(value) ? [value] : [];\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\nimport { isPlainObject } from 'es-toolkit';\n\nexport interface RouteParamInfo {\n name: string;\n type: 'dynamic' | 'catchAll' | 'optionalCatchAll';\n}\n\nexport interface RouteSchemaInfo {\n hasParamsSchema: boolean;\n hasQuerySchema: boolean;\n}\n\nexport interface GenerateRouteTreeDeclarationOptions {\n jsonLdFiles?: Iterable<string>;\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 /** Schema export info (detected from file content) */\n schemas: RouteSchemaInfo;\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}\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}\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[\\\\/](?:routes|pages)|src[\\\\/]content)[\\\\/]` — non-anchored\n * fallback for legacy paths where the directory marker appears mid-string.\n * 3. `\\.page\\.(js|ts|analog|ag)$` — strips page file extensions.\n * 4. `\\.(ts|md|analog|ag)$` — strips remaining file extensions.\n *\n * Examples:\n * - '/app/routes/index.ts' -> '/'\n * - '/app/routes/about.ts' -> '/about'\n * - '/src/app/pages/users/[id].page.ts' -> '/users/[id]'\n * - '/app/routes/blog.[slug].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[\\\\/](?:routes|pages)|src[\\\\/]content)[\\\\/])|(\\.page\\.(js|ts|analog|ag)$)|(\\.(ts|md|analog|ag)$)/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 const processed: string[] = [];\n\n for (const segment of segments) {\n if (/^\\([^.[\\]]*\\)$/.test(segment)) continue;\n processed.push(segment);\n }\n\n if (processed.length > 0 && processed[processed.length - 1] === 'index') {\n processed.pop();\n }\n\n return '/' + processed.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[\\\\/](?:routes|pages)|src[\\\\/]content)[\\\\/])|(\\.page\\.(js|ts|analog|ag)$)|(\\.(ts|md|analog|ag)$)/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 * Detects whether a route file exports schema constants.\n */\nexport function detectSchemaExports(fileContent: string): RouteSchemaInfo {\n return {\n hasParamsSchema: /export\\s+const\\s+routeParamsSchema\\b/.test(fileContent),\n hasQuerySchema: /export\\s+const\\s+routeQuerySchema\\b/.test(fileContent),\n };\n}\n\nconst NO_SCHEMAS: RouteSchemaInfo = {\n hasParamsSchema: false,\n hasQuerySchema: false,\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 schemaDetector?: (filename: string) => RouteSchemaInfo,\n collisionPriority?: (filename: string) => number,\n): RouteManifest {\n const routes: RouteEntry[] = [];\n const collisions: RouteCollision[] = [];\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 for (const filename of prioritizedFilenames) {\n const fullPath = filenameToRoutePath(filename);\n const params = extractRouteParams(fullPath);\n const schemas = schemaDetector ? schemaDetector(filename) : NO_SCHEMAS;\n const id = filenameToRouteId(filename);\n const isPathlessLayout = isPathlessLayoutId(id);\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(fullPath)) {\n const winner = seenByFullPath.get(fullPath)!;\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|analog|ag)$/;\n const layoutRe = /\\.(page\\.)?(ts|js|analog|ag)$/;\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 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(fullPath, { filename, priority: currentPriority });\n }\n\n routes.push({\n id,\n path: fullPath,\n fullPath,\n params,\n filename,\n schemas,\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 });\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 }\n\n for (const route of routes) {\n if (route.schemas.hasParamsSchema && route.params.length === 0) {\n console.warn(\n `[Analog] Route '${route.fullPath}' exports routeParamsSchema` +\n ` but has no dynamic params in the filename.`,\n );\n }\n }\n\n // Build-time consistency check: every parentId and child reference must\n // point to a real route in the manifest. Invalid references indicate a\n // bug in the hierarchy computation.\n for (const route of routes) {\n if (route.parentId && !routeById.has(route.parentId)) {\n console.warn(\n `[Analog] Route '${route.id}' has parentId '${route.parentId}' ` +\n `which does not match any route id in the manifest.`,\n );\n }\n for (const childId of route.children) {\n if (!routeById.has(childId)) {\n console.warn(\n `[Analog] Route '${route.id}' lists child '${childId}' ` +\n `which does not match any route id in the manifest.`,\n );\n }\n }\n }\n\n return { routes, collisions, canonicalByFullPath: routeByFullPath };\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// Matches group names like (auth), (home) — intentionally excludes dots and\n// brackets so names like (auth.v2) or ([id]) are NOT treated as pathless\n// layouts. Dot-containing names collide with dynamic-segment syntax.\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('/src/app/routes/') ||\n filename.includes('/app/pages/') ||\n filename.includes('/app/routes/') ||\n filename.includes('/src/content/')\n ) {\n return 0;\n }\n\n return 1;\n}\n\n/**\n * Produces a human-readable summary of the generated route manifest.\n */\nexport function formatManifestSummary(manifest: RouteManifest): string {\n const lines: string[] = [];\n const total = manifest.routes.length;\n const withSchemas = manifest.routes.filter(\n (r) => r.schemas.hasParamsSchema || r.schemas.hasQuerySchema,\n ).length;\n const staticCount = manifest.routes.filter(\n (r) => r.params.length === 0,\n ).length;\n const dynamicCount = total - staticCount;\n\n lines.push(`[Analog] Generated typed routes:`);\n lines.push(\n ` ${total} routes (${staticCount} static, ${dynamicCount} dynamic)`,\n );\n if (withSchemas > 0) {\n lines.push(` ${withSchemas} with schema validation`);\n }\n\n for (const route of manifest.routes) {\n const flags: string[] = [];\n if (route.schemas.hasParamsSchema) flags.push('params-schema');\n if (route.schemas.hasQuerySchema) flags.push('query-schema');\n const suffix = flags.length > 0 ? ` [${flags.join(', ')}]` : '';\n lines.push(` ${route.fullPath}${suffix}`);\n }\n\n return lines.join('\\n');\n}\n\n/**\n * Generates the route-table section for the combined generated route module.\n */\nexport function generateRouteTableDeclaration(manifest: RouteManifest): string {\n const lines: string[] = [];\n const hasAnySchema = manifest.routes.some(\n (r) => r.schemas.hasParamsSchema || r.schemas.hasQuerySchema,\n );\n\n lines.push('// This file is auto-generated by @analogjs/platform');\n lines.push('// Do not edit manually');\n lines.push('');\n\n if (hasAnySchema) {\n lines.push(\n \"import type { StandardSchemaV1 } from '@standard-schema/spec';\",\n );\n }\n\n const schemaImports = new Map<string, string>();\n let aliasIndex = 0;\n\n for (const route of manifest.routes) {\n if (route.schemas.hasParamsSchema || route.schemas.hasQuerySchema) {\n const importPath = filenameToImportPath(route.filename);\n const names: string[] = [];\n\n if (route.schemas.hasParamsSchema) {\n const alias = `_p${aliasIndex}`;\n names.push(`routeParamsSchema as ${alias}`);\n schemaImports.set(`${route.fullPath}:params`, alias);\n }\n if (route.schemas.hasQuerySchema) {\n const alias = `_q${aliasIndex}`;\n names.push(`routeQuerySchema as ${alias}`);\n schemaImports.set(`${route.fullPath}:query`, alias);\n }\n\n lines.push(`import type { ${names.join(', ')} } from '${importPath}';`);\n aliasIndex++;\n }\n }\n\n if (hasAnySchema) {\n lines.push('');\n }\n\n lines.push(\"declare module '@analogjs/router' {\");\n lines.push(' interface AnalogRouteTable {');\n\n for (const route of manifest.canonicalByFullPath.values()) {\n const paramsAlias = schemaImports.get(`${route.fullPath}:params`);\n const queryAlias = schemaImports.get(`${route.fullPath}:query`);\n\n const paramsType = generateParamsType(route.params);\n const queryType = 'Record<string, string | string[] | undefined>';\n\n const paramsOutputType = paramsAlias\n ? `StandardSchemaV1.InferOutput<typeof ${paramsAlias}>`\n : paramsType;\n const queryOutputType = queryAlias\n ? `StandardSchemaV1.InferOutput<typeof ${queryAlias}>`\n : queryType;\n\n lines.push(` '${route.fullPath}': {`);\n lines.push(` params: ${paramsType};`);\n lines.push(` paramsOutput: ${paramsOutputType};`);\n lines.push(` query: ${queryType};`);\n lines.push(` queryOutput: ${queryOutputType};`);\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\n/**\n * Generates the route-tree section for the combined generated route module.\n */\nexport function generateRouteTreeDeclaration(\n manifest: RouteManifest,\n options: GenerateRouteTreeDeclarationOptions = {},\n): string {\n const lines: string[] = [];\n const jsonLdFiles = new Set(options.jsonLdFiles ?? []);\n\n lines.push('// This file is auto-generated by @analogjs/platform');\n lines.push('// Do not edit manually');\n lines.push('');\n lines.push('export interface AnalogGeneratedRouteRecord<');\n lines.push(' TId extends string = string,');\n lines.push(' TPath extends string = string,');\n lines.push(' TFullPath extends string = string,');\n lines.push(' TParentId extends string | null = string | null,');\n lines.push(' TChildren extends readonly string[] = readonly string[],');\n lines.push('> {');\n lines.push(' id: TId;');\n lines.push(' path: TPath;');\n lines.push(' fullPath: TFullPath;');\n lines.push(' parentId: TParentId;');\n lines.push(' children: TChildren;');\n lines.push(' sourceFile: string;');\n lines.push(\" kind: 'page' | 'content';\");\n lines.push(' hasParamsSchema: boolean;');\n lines.push(' hasQuerySchema: boolean;');\n lines.push(' hasJsonLd: boolean;');\n lines.push(' isIndex: boolean;');\n lines.push(' isGroup: boolean;');\n lines.push(' isCatchAll: boolean;');\n lines.push(' isOptionalCatchAll: boolean;');\n lines.push('}');\n lines.push('');\n lines.push('export interface AnalogFileRoutesById {');\n for (const route of manifest.routes) {\n lines.push(\n ` ${toTsKey(route.id)}: AnalogGeneratedRouteRecord<${toTsStringLiteral(route.id)}, ${toTsStringLiteral(route.path)}, ${toTsStringLiteral(route.fullPath)}, ${route.parentId ? toTsStringLiteral(route.parentId) : 'null'}, ${toReadonlyTupleType(route.children)}>;`,\n );\n }\n lines.push('}');\n lines.push('');\n lines.push('export interface AnalogFileRoutesByFullPath {');\n for (const route of manifest.canonicalByFullPath.values()) {\n lines.push(\n ` ${toTsKey(route.fullPath)}: AnalogFileRoutesById[${toTsStringLiteral(route.id)}];`,\n );\n }\n lines.push('}');\n lines.push('');\n lines.push('export type AnalogRouteTreeId = keyof AnalogFileRoutesById;');\n lines.push(\n 'export type AnalogRouteTreeFullPath = keyof AnalogFileRoutesByFullPath;',\n );\n lines.push('');\n lines.push('export const analogRouteTree = {');\n lines.push(' byId: {');\n for (const route of manifest.routes) {\n lines.push(` ${toObjectKey(route.id)}: {`);\n lines.push(` id: ${toTsStringLiteral(route.id)},`);\n lines.push(` path: ${toTsStringLiteral(route.path)},`);\n lines.push(` fullPath: ${toTsStringLiteral(route.fullPath)},`);\n lines.push(\n ` parentId: ${route.parentId ? toTsStringLiteral(route.parentId) : 'null'},`,\n );\n lines.push(` children: ${toReadonlyTupleValue(route.children)},`);\n lines.push(` sourceFile: ${toTsStringLiteral(route.filename)},`);\n lines.push(` kind: ${toTsStringLiteral(route.kind)},`);\n lines.push(\n ` hasParamsSchema: ${String(route.schemas.hasParamsSchema)},`,\n );\n lines.push(\n ` hasQuerySchema: ${String(route.schemas.hasQuerySchema)},`,\n );\n lines.push(` hasJsonLd: ${String(jsonLdFiles.has(route.filename))},`);\n lines.push(` isIndex: ${String(route.isIndex)},`);\n lines.push(` isGroup: ${String(route.isGroup)},`);\n lines.push(` isCatchAll: ${String(route.isCatchAll)},`);\n lines.push(\n ` isOptionalCatchAll: ${String(route.isOptionalCatchAll)},`,\n );\n lines.push(\n ` } satisfies AnalogFileRoutesById[${toTsStringLiteral(route.id)}],`,\n );\n }\n lines.push(' },');\n lines.push(' byFullPath: {');\n for (const route of manifest.canonicalByFullPath.values()) {\n lines.push(\n ` ${toObjectKey(route.fullPath)}: ${toTsStringLiteral(route.id)},`,\n );\n }\n lines.push(' },');\n lines.push('} as const;');\n lines.push('');\n\n return lines.join('\\n');\n}\n\nfunction filenameToImportPath(filename: string): string {\n const stripped = filename.replace(/^\\//, '').replace(/\\.ts$/, '');\n return '../' + stripped;\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 : `'${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\nfunction toTsStringLiteral(value: string): string {\n return JSON.stringify(value);\n}\n\nfunction toTsKey(value: string): string {\n return toTsStringLiteral(value);\n}\n\nfunction toObjectKey(value: string): string {\n return isValidIdentifier(value) ? value : toTsStringLiteral(value);\n}\n\nfunction toReadonlyTupleType(values: readonly string[]): string {\n if (values.length === 0) {\n return 'readonly []';\n }\n\n return `readonly [${values.map((value) => toTsStringLiteral(value)).join(', ')}]`;\n}\n\nfunction toReadonlyTupleValue(values: readonly string[]): string {\n if (values.length === 0) {\n return '[] as const';\n }\n\n return `[${values.map((value) => toTsStringLiteral(value)).join(', ')}] as const`;\n}\n\n// --- JSON-LD utilities ---\n\nexport type JsonLdObject = Record<string, unknown>;\n\nexport function isJsonLdObject(value: unknown): value is JsonLdObject {\n return typeof value === 'object' && value !== null && !Array.isArray(value);\n}\n\nexport function normalizeJsonLd(value: unknown): JsonLdObject[] {\n if (Array.isArray(value)) {\n return value.filter(isJsonLdObject);\n }\n\n return isJsonLdObject(value) ? [value] : [];\n}\n"],"mappings":"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"}
|
package/src/lib/router-plugin.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { analyzeAnalogRouteFile, formatAnalogRouteIdiomDiagnostic } from "./route-idiom-diagnostics.js";
|
|
1
2
|
import { isAbsolute, relative, resolve } from "node:path";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
2
4
|
import { normalizePath } from "vite";
|
|
3
5
|
import { globSync } from "tinyglobby";
|
|
4
6
|
//#region packages/platform/src/lib/router-plugin.ts
|
|
@@ -45,8 +47,10 @@ function routerPlugin(options) {
|
|
|
45
47
|
...additionalContentDirs
|
|
46
48
|
];
|
|
47
49
|
let routeFilesCache;
|
|
50
|
+
let pageRouteFilesCache;
|
|
48
51
|
let contentRouteFilesCache;
|
|
49
52
|
let endpointFilesCache;
|
|
53
|
+
const routeDiagnosticCache = /* @__PURE__ */ new Map();
|
|
50
54
|
const isRouteLikeFile = (path) => {
|
|
51
55
|
const normalizedPath = normalizePath(path);
|
|
52
56
|
return getRouteLikeDirs().some((dir) => normalizedPath === dir || normalizedPath.startsWith(`${dir}/`));
|
|
@@ -82,11 +86,36 @@ function routerPlugin(options) {
|
|
|
82
86
|
});
|
|
83
87
|
return endpointFilesCache;
|
|
84
88
|
};
|
|
89
|
+
const discoverPageRouteFiles = () => {
|
|
90
|
+
pageRouteFilesCache ??= discoverRouteFiles().filter((file) => file.endsWith(".page.ts"));
|
|
91
|
+
return pageRouteFilesCache;
|
|
92
|
+
};
|
|
85
93
|
const invalidateDiscoveryCaches = () => {
|
|
86
94
|
routeFilesCache = void 0;
|
|
95
|
+
pageRouteFilesCache = void 0;
|
|
87
96
|
contentRouteFilesCache = void 0;
|
|
88
97
|
endpointFilesCache = void 0;
|
|
89
98
|
};
|
|
99
|
+
const reportRouteDiagnostics = (path) => {
|
|
100
|
+
if (!path.endsWith(".page.ts")) return;
|
|
101
|
+
try {
|
|
102
|
+
const rendered = analyzeAnalogRouteFile({
|
|
103
|
+
filename: path,
|
|
104
|
+
code: readFileSync(path, "utf-8"),
|
|
105
|
+
routeFiles: discoverPageRouteFiles()
|
|
106
|
+
}).map((diagnostic) => formatAnalogRouteIdiomDiagnostic(diagnostic, path, workspaceRoot));
|
|
107
|
+
const fingerprint = rendered.join("\n\n");
|
|
108
|
+
if (!fingerprint) {
|
|
109
|
+
routeDiagnosticCache.delete(path);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (routeDiagnosticCache.get(path) === fingerprint) return;
|
|
113
|
+
routeDiagnosticCache.set(path, fingerprint);
|
|
114
|
+
rendered.forEach((message) => console.warn(message));
|
|
115
|
+
} catch {
|
|
116
|
+
routeDiagnosticCache.delete(path);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
90
119
|
const getModuleKey = (module) => {
|
|
91
120
|
if (!root) return `/${normalizePath(relative(workspaceRoot, module))}`;
|
|
92
121
|
const relToRoot = normalizePath(relative(root, module));
|
|
@@ -115,6 +144,11 @@ function routerPlugin(options) {
|
|
|
115
144
|
function invalidateRoutes(path, event) {
|
|
116
145
|
if (!isRouteLikeFile(path)) return;
|
|
117
146
|
if (event !== "change") invalidateDiscoveryCaches();
|
|
147
|
+
if (event === "change") reportRouteDiagnostics(path);
|
|
148
|
+
else if (event === "unlink") {
|
|
149
|
+
routeDiagnosticCache.delete(path);
|
|
150
|
+
discoverPageRouteFiles().forEach((file) => reportRouteDiagnostics(file));
|
|
151
|
+
} else discoverPageRouteFiles().forEach((file) => reportRouteDiagnostics(file));
|
|
118
152
|
invalidateFileModules(server, path);
|
|
119
153
|
if (event === "change") return;
|
|
120
154
|
server.moduleGraph.fileToModulesMap.forEach((mods) => {
|
|
@@ -127,12 +161,19 @@ function routerPlugin(options) {
|
|
|
127
161
|
}
|
|
128
162
|
});
|
|
129
163
|
});
|
|
164
|
+
server.ws.send("analog:debug-full-reload", {
|
|
165
|
+
plugin: "platform:router-plugin",
|
|
166
|
+
reason: "route-graph-shape-changed",
|
|
167
|
+
event,
|
|
168
|
+
path
|
|
169
|
+
});
|
|
130
170
|
server.ws.send({ type: "full-reload" });
|
|
131
171
|
}
|
|
132
172
|
server.watcher.on("add", (path) => invalidateRoutes(path, "add"));
|
|
133
173
|
server.watcher.on("change", (path) => invalidateRoutes(path, "change"));
|
|
134
174
|
server.watcher.on("unlink", (path) => invalidateRoutes(path, "unlink"));
|
|
135
175
|
for (const dir of [...additionalPagesDirs, ...additionalContentDirs]) server.watcher.add(dir);
|
|
176
|
+
discoverPageRouteFiles().forEach((file) => reportRouteDiagnostics(file));
|
|
136
177
|
}
|
|
137
178
|
},
|
|
138
179
|
{
|