@analogjs/platform 3.0.0-alpha.90 → 3.0.0-alpha.92
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-to-separated-plugins/migrate-to-separated-plugins.d.ts +2 -0
- package/migrations/migrate-to-separated-plugins/migrate-to-separated-plugins.js +203 -0
- package/migrations/migrate-to-separated-plugins/migrate-to-separated-plugins.js.map +1 -0
- package/package.json +11 -7
- package/src/index.d.ts +1 -2
- package/src/index.js +1 -2
- package/src/index.js.map +1 -1
- package/src/lib/clear-client-page-endpoint.js +1 -0
- package/src/lib/clear-client-page-endpoint.js.map +1 -1
- package/src/lib/deps-plugin.js +17 -6
- package/src/lib/deps-plugin.js.map +1 -1
- package/src/lib/nitro/analog-nitro-plugin.js +102 -45
- package/src/lib/nitro/analog-nitro-plugin.js.map +1 -1
- package/src/lib/nitro/get-server-fn-handlers.d.ts +29 -0
- package/src/lib/nitro/get-server-fn-handlers.js +66 -0
- package/src/lib/nitro/get-server-fn-handlers.js.map +1 -0
- package/src/lib/nitro/server-fn-endpoints.d.ts +41 -0
- package/src/lib/nitro/server-fn-endpoints.js +57 -0
- package/src/lib/nitro/server-fn-endpoints.js.map +1 -0
- package/src/lib/nx-plugin/src/generators/app/files/template-angular/src/server/routes/api/v1/hello.ts__template__ +2 -2
- package/src/lib/nx-plugin/src/generators/app/files/template-angular/vite.config.ts__template__ +4 -0
- 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.map +1 -1
- package/src/lib/nx-plugin/src/generators/preset/__snapshots__/generator.spec.ts.snap +3 -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 +9 -49
- package/src/lib/platform-plugin.js +42 -6
- package/src/lib/platform-plugin.js.map +1 -1
- package/src/lib/route-file-discovery.d.ts +3 -1
- package/src/lib/route-file-discovery.js +19 -8
- package/src/lib/route-file-discovery.js.map +1 -1
- package/src/lib/route-manifest.d.ts +22 -26
- package/src/lib/route-manifest.js +58 -189
- package/src/lib/route-manifest.js.map +1 -1
- package/src/lib/ssr/defer-streaming-plugin.js +97 -0
- package/src/lib/ssr/defer-streaming-plugin.js.map +1 -0
- package/src/lib/typed-routes-plugin.d.ts +6 -43
- package/src/lib/typed-routes-plugin.js +80 -195
- package/src/lib/typed-routes-plugin.js.map +1 -1
- package/src/lib/utils/debug.js +4 -7
- package/src/lib/utils/debug.js.map +1 -1
- package/src/lib/utils/rolldown.js +1 -1
- package/src/server-mode-plugin.d.ts +1 -1
- package/src/server-mode-plugin.js +13 -1
- package/src/server-mode-plugin.js.map +1 -1
- package/src/lib/json-ld-manifest-plugin.d.ts +0 -41
- package/src/lib/json-ld-manifest-plugin.js +0 -84
- package/src/lib/json-ld-manifest-plugin.js.map +0 -1
- package/src/lib/route-generation-plugin.d.ts +0 -3
- package/src/lib/route-generation-plugin.js +0 -45
- package/src/lib/route-generation-plugin.js.map +0 -1
|
@@ -1,14 +1,13 @@
|
|
|
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
|
+
*/
|
|
1
7
|
export interface RouteParamInfo {
|
|
2
8
|
name: string;
|
|
3
9
|
type: "dynamic" | "catchAll" | "optionalCatchAll";
|
|
4
10
|
}
|
|
5
|
-
export interface RouteSchemaInfo {
|
|
6
|
-
hasParamsSchema: boolean;
|
|
7
|
-
hasQuerySchema: boolean;
|
|
8
|
-
}
|
|
9
|
-
export interface GenerateRouteTreeDeclarationOptions {
|
|
10
|
-
jsonLdFiles?: Iterable<string>;
|
|
11
|
-
}
|
|
12
11
|
export interface RouteEntry {
|
|
13
12
|
/** Stable structural route id derived from the source filename */
|
|
14
13
|
id: string;
|
|
@@ -20,8 +19,6 @@ export interface RouteEntry {
|
|
|
20
19
|
params: RouteParamInfo[];
|
|
21
20
|
/** Original filename that produced this route */
|
|
22
21
|
filename: string;
|
|
23
|
-
/** Schema export info (detected from file content) */
|
|
24
|
-
schemas: RouteSchemaInfo;
|
|
25
22
|
/** Type of source that produced this route */
|
|
26
23
|
kind: "page" | "content";
|
|
27
24
|
/** Parent route id, or null for top-level routes */
|
|
@@ -36,6 +33,8 @@ export interface RouteEntry {
|
|
|
36
33
|
isCatchAll: boolean;
|
|
37
34
|
/** Whether the route contains an optional catch-all parameter */
|
|
38
35
|
isOptionalCatchAll: boolean;
|
|
36
|
+
/** Layout files that wrap this route, outermost first */
|
|
37
|
+
layouts: string[];
|
|
39
38
|
}
|
|
40
39
|
export interface RouteCollision {
|
|
41
40
|
fullPath: string;
|
|
@@ -49,6 +48,8 @@ export interface RouteManifest {
|
|
|
49
48
|
collisions: RouteCollision[];
|
|
50
49
|
/** Canonical route per fullPath — precomputed once to avoid redundant work. */
|
|
51
50
|
canonicalByFullPath: Map<string, RouteEntry>;
|
|
51
|
+
/** Full paths defined by more than one file, such as a layout and its index page. */
|
|
52
|
+
sharedFullPaths: Set<string>;
|
|
52
53
|
}
|
|
53
54
|
/**
|
|
54
55
|
* Converts a discovered filename to a route path pattern.
|
|
@@ -94,29 +95,24 @@ export declare function filenameToRouteId(filename: string): string;
|
|
|
94
95
|
*/
|
|
95
96
|
export declare function extractRouteParams(routePath: string): RouteParamInfo[];
|
|
96
97
|
/**
|
|
97
|
-
* Detects whether a route file exports schema constants.
|
|
98
|
-
*/
|
|
99
|
-
export declare function detectSchemaExports(fileContent: string): RouteSchemaInfo;
|
|
100
|
-
/**
|
|
101
98
|
* Generates a route manifest from a list of discovered filenames.
|
|
102
99
|
*
|
|
103
100
|
* @param collisionPriority - Optional callback that returns a numeric priority
|
|
104
101
|
* for each filename (lower wins). When provided, this replaces the default
|
|
105
102
|
* hard-coded path-substring heuristic with config-derived precedence.
|
|
106
103
|
*/
|
|
107
|
-
export declare function generateRouteManifest(filenames: string[],
|
|
108
|
-
/**
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
104
|
+
export declare function generateRouteManifest(filenames: string[], collisionPriority?: (filename: string) => number): RouteManifest;
|
|
105
|
+
/** Import specifiers for a page module's typed exports. */
|
|
106
|
+
export interface RouteModuleImports {
|
|
107
|
+
routeMeta?: string;
|
|
108
|
+
load?: string;
|
|
109
|
+
}
|
|
112
110
|
/**
|
|
113
111
|
* Generates the route-table section for the combined generated route module.
|
|
112
|
+
*
|
|
113
|
+
* `resolveModuleImports` adds `routeMeta` and `load` types for pages whose
|
|
114
|
+
* URL is defined by a single file, along with the `routeMeta` of the layouts
|
|
115
|
+
* that wrap them. URLs shared by several files, such as a layout and its
|
|
116
|
+
* index page, are ambiguous and only get params and query types.
|
|
114
117
|
*/
|
|
115
|
-
export declare function generateRouteTableDeclaration(manifest: RouteManifest): string;
|
|
116
|
-
/**
|
|
117
|
-
* Generates the route-tree section for the combined generated route module.
|
|
118
|
-
*/
|
|
119
|
-
export declare function generateRouteTreeDeclaration(manifest: RouteManifest, options?: GenerateRouteTreeDeclarationOptions): string;
|
|
120
|
-
export type JsonLdObject = Record<string, unknown>;
|
|
121
|
-
export declare function isJsonLdObject(value: unknown): value is JsonLdObject;
|
|
122
|
-
export declare function normalizeJsonLd(value: unknown): JsonLdObject[];
|
|
118
|
+
export declare function generateRouteTableDeclaration(manifest: RouteManifest, resolveModuleImports?: (filename: string) => RouteModuleImports | undefined): string;
|
|
@@ -33,16 +33,9 @@ function filenameToRoutePath(filename) {
|
|
|
33
33
|
brackets.push(match);
|
|
34
34
|
return `\0B${brackets.length - 1}\0`;
|
|
35
35
|
});
|
|
36
|
-
path = path.replace(/\./g, "/");
|
|
36
|
+
path = path.replace(/\(.*?\)/g, "").replace(/(^|[./])index(?=[./]|$)/g, "$1").replace(/\./g, "/");
|
|
37
37
|
path = path.replace(/\0B(\d+)\0/g, (_, idx) => brackets[Number(idx)]);
|
|
38
|
-
|
|
39
|
-
const processed = [];
|
|
40
|
-
for (const segment of segments) {
|
|
41
|
-
if (/^\([^.[\]]*\)$/.test(segment)) continue;
|
|
42
|
-
processed.push(segment);
|
|
43
|
-
}
|
|
44
|
-
if (processed.length > 0 && processed[processed.length - 1] === "index") processed.pop();
|
|
45
|
-
return "/" + processed.join("/");
|
|
38
|
+
return "/" + path.split("/").filter(Boolean).join("/");
|
|
46
39
|
}
|
|
47
40
|
/**
|
|
48
41
|
* Converts a discovered filename to a stable structural route id.
|
|
@@ -57,7 +50,7 @@ function filenameToRoutePath(filename) {
|
|
|
57
50
|
function filenameToRouteId(filename) {
|
|
58
51
|
let path = filename.replace(/^(?:[a-zA-Z]:[\\/])?(.*?)[\\/](?:routes|pages|content)[\\/]|(?:[\\/](?:app[\\/](?:routes|pages)|src[\\/]content)[\\/])|(\.page\.(js|ts|analog|ag)$)|(\.(ts|md|analog|ag)$)/g, "");
|
|
59
52
|
const brackets = [];
|
|
60
|
-
path = path.replace(/\[\[?\.{0,3}[^\]]*\]?\]/g, (match) => {
|
|
53
|
+
path = path.replace(/\(.*?\)|\[\[?\.{0,3}[^\]]*\]?\]/g, (match) => {
|
|
61
54
|
brackets.push(match);
|
|
62
55
|
return `\0B${brackets.length - 1}\0`;
|
|
63
56
|
});
|
|
@@ -85,28 +78,16 @@ function extractRouteParams(routePath) {
|
|
|
85
78
|
return params;
|
|
86
79
|
}
|
|
87
80
|
/**
|
|
88
|
-
* Detects whether a route file exports schema constants.
|
|
89
|
-
*/
|
|
90
|
-
function detectSchemaExports(fileContent) {
|
|
91
|
-
return {
|
|
92
|
-
hasParamsSchema: /export\s+const\s+routeParamsSchema\b/.test(fileContent),
|
|
93
|
-
hasQuerySchema: /export\s+const\s+routeQuerySchema\b/.test(fileContent)
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
var NO_SCHEMAS = {
|
|
97
|
-
hasParamsSchema: false,
|
|
98
|
-
hasQuerySchema: false
|
|
99
|
-
};
|
|
100
|
-
/**
|
|
101
81
|
* Generates a route manifest from a list of discovered filenames.
|
|
102
82
|
*
|
|
103
83
|
* @param collisionPriority - Optional callback that returns a numeric priority
|
|
104
84
|
* for each filename (lower wins). When provided, this replaces the default
|
|
105
85
|
* hard-coded path-substring heuristic with config-derived precedence.
|
|
106
86
|
*/
|
|
107
|
-
function generateRouteManifest(filenames,
|
|
87
|
+
function generateRouteManifest(filenames, collisionPriority) {
|
|
108
88
|
const routes = [];
|
|
109
89
|
const collisions = [];
|
|
90
|
+
const sharedFullPaths = /* @__PURE__ */ new Set();
|
|
110
91
|
const seenByFullPath = /* @__PURE__ */ new Map();
|
|
111
92
|
const getPriority = collisionPriority ?? getCollisionPriority;
|
|
112
93
|
const prioritizedFilenames = [...filenames].sort((a, b) => {
|
|
@@ -115,16 +96,22 @@ function generateRouteManifest(filenames, schemaDetector, collisionPriority) {
|
|
|
115
96
|
if (aPriority !== bPriority) return aPriority - bPriority;
|
|
116
97
|
return a.localeCompare(b);
|
|
117
98
|
});
|
|
99
|
+
const layoutByKey = /* @__PURE__ */ new Map();
|
|
100
|
+
for (const filename of prioritizedFilenames) {
|
|
101
|
+
const key = toRouteFileKey(filename);
|
|
102
|
+
if (!filename.endsWith(".md") && !layoutByKey.has(key)) layoutByKey.set(key, filename);
|
|
103
|
+
}
|
|
118
104
|
for (const filename of prioritizedFilenames) {
|
|
119
105
|
const fullPath = filenameToRoutePath(filename);
|
|
120
106
|
const params = extractRouteParams(fullPath);
|
|
121
|
-
const schemas = schemaDetector ? schemaDetector(filename) : NO_SCHEMAS;
|
|
122
107
|
const id = filenameToRouteId(filename);
|
|
123
108
|
const isPathlessLayout = isPathlessLayoutId(id);
|
|
109
|
+
const groupScope = id.slice(0, id.lastIndexOf(")") + 1);
|
|
110
|
+
const collisionKey = JSON.stringify([fullPath, groupScope]);
|
|
124
111
|
const currentPriority = getPriority(filename);
|
|
125
112
|
if (!isPathlessLayout) {
|
|
126
|
-
if (seenByFullPath.has(
|
|
127
|
-
const winner = seenByFullPath.get(
|
|
113
|
+
if (seenByFullPath.has(collisionKey)) {
|
|
114
|
+
const winner = seenByFullPath.get(collisionKey);
|
|
128
115
|
if (winner.filename === filename) continue;
|
|
129
116
|
const isLayoutIndexPair = (a, b) => {
|
|
130
117
|
const indexRe = /\/index\.(page\.)?(ts|js|md|analog|ag)$/;
|
|
@@ -132,7 +119,10 @@ function generateRouteManifest(filenames, schemaDetector, collisionPriority) {
|
|
|
132
119
|
if (indexRe.test(a) && layoutRe.test(b)) return a.replace(indexRe, "") === b.replace(layoutRe, "");
|
|
133
120
|
return false;
|
|
134
121
|
};
|
|
135
|
-
if (isLayoutIndexPair(winner.filename, filename) || isLayoutIndexPair(filename, winner.filename))
|
|
122
|
+
if (isLayoutIndexPair(winner.filename, filename) || isLayoutIndexPair(filename, winner.filename)) {
|
|
123
|
+
sharedFullPaths.add(fullPath);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
136
126
|
collisions.push({
|
|
137
127
|
fullPath,
|
|
138
128
|
keptFile: winner.filename,
|
|
@@ -142,25 +132,31 @@ function generateRouteManifest(filenames, schemaDetector, collisionPriority) {
|
|
|
142
132
|
console.warn(`[Analog] Route collision: '${fullPath}' is defined by both '${winner.filename}' and '${filename}'. Keeping '${winner.filename}' based on route source precedence and skipping duplicate.`);
|
|
143
133
|
continue;
|
|
144
134
|
}
|
|
145
|
-
seenByFullPath.set(
|
|
135
|
+
seenByFullPath.set(collisionKey, {
|
|
146
136
|
filename,
|
|
147
137
|
priority: currentPriority
|
|
148
138
|
});
|
|
149
139
|
}
|
|
140
|
+
const segments = toRouteFileKey(filename).split("/");
|
|
141
|
+
const layouts = [];
|
|
142
|
+
for (let i = 1; i < segments.length; i++) {
|
|
143
|
+
const layout = layoutByKey.get(segments.slice(0, i).join("/"));
|
|
144
|
+
if (layout) layouts.push(layout);
|
|
145
|
+
}
|
|
150
146
|
routes.push({
|
|
151
147
|
id,
|
|
152
148
|
path: fullPath,
|
|
153
149
|
fullPath,
|
|
154
150
|
params,
|
|
155
151
|
filename,
|
|
156
|
-
schemas,
|
|
157
152
|
kind: filename.endsWith(".md") ? "content" : "page",
|
|
158
153
|
parentId: null,
|
|
159
154
|
children: [],
|
|
160
155
|
isIndex: id === "/index" || id.endsWith("/index"),
|
|
161
156
|
isGroup: isPathlessLayout,
|
|
162
157
|
isCatchAll: params.some((param) => param.type === "catchAll"),
|
|
163
|
-
isOptionalCatchAll: params.some((param) => param.type === "optionalCatchAll")
|
|
158
|
+
isOptionalCatchAll: params.some((param) => param.type === "optionalCatchAll"),
|
|
159
|
+
layouts
|
|
164
160
|
});
|
|
165
161
|
}
|
|
166
162
|
routes.sort((a, b) => {
|
|
@@ -178,16 +174,15 @@ function generateRouteManifest(filenames, schemaDetector, collisionPriority) {
|
|
|
178
174
|
route.parentId = parent?.id ?? null;
|
|
179
175
|
route.path = computeLocalPath(route.fullPath, parent?.fullPath ?? null);
|
|
180
176
|
}
|
|
181
|
-
for (const route of routes) if (route.parentId) routeById.get(route.parentId)?.children.push(route.id);
|
|
182
|
-
for (const route of routes) if (route.schemas.hasParamsSchema && route.params.length === 0) console.warn(`[Analog] Route '${route.fullPath}' exports routeParamsSchema but has no dynamic params in the filename.`);
|
|
183
177
|
for (const route of routes) {
|
|
184
|
-
if (route.parentId
|
|
185
|
-
|
|
178
|
+
if (route.parentId) routeById.get(route.parentId)?.children.push(route.id);
|
|
179
|
+
if (routeByFullPath.get(route.fullPath) !== route) sharedFullPaths.add(route.fullPath);
|
|
186
180
|
}
|
|
187
181
|
return {
|
|
188
182
|
routes,
|
|
189
183
|
collisions,
|
|
190
|
-
canonicalByFullPath: routeByFullPath
|
|
184
|
+
canonicalByFullPath: routeByFullPath,
|
|
185
|
+
sharedFullPaths
|
|
191
186
|
};
|
|
192
187
|
}
|
|
193
188
|
function canonicalRoutesByFullPath(routes) {
|
|
@@ -202,10 +197,14 @@ function canonicalRoutesByFullPath(routes) {
|
|
|
202
197
|
}
|
|
203
198
|
return map;
|
|
204
199
|
}
|
|
200
|
+
/** A route file's path within its pages directory, e.g. `users/[id]`. */
|
|
201
|
+
function toRouteFileKey(filename) {
|
|
202
|
+
return filename.replace(/^.*?[\\/](?:routes|pages|content)[\\/]|\.page\.(?:js|ts|analog|ag)$|\.(?:ts|md|analog|ag)$/g, "");
|
|
203
|
+
}
|
|
205
204
|
function isPathlessLayoutId(id) {
|
|
206
205
|
const segments = id.split("/").filter(Boolean);
|
|
207
206
|
if (segments.length === 0) return false;
|
|
208
|
-
return /^\([
|
|
207
|
+
return /^\([^)]*\)$/.test(segments[segments.length - 1]);
|
|
209
208
|
}
|
|
210
209
|
function getRouteWeight(path) {
|
|
211
210
|
if (path.includes("[[...")) return 3;
|
|
@@ -218,69 +217,37 @@ function getCollisionPriority(filename) {
|
|
|
218
217
|
return 1;
|
|
219
218
|
}
|
|
220
219
|
/**
|
|
221
|
-
* Produces a human-readable summary of the generated route manifest.
|
|
222
|
-
*/
|
|
223
|
-
function formatManifestSummary(manifest) {
|
|
224
|
-
const lines = [];
|
|
225
|
-
const total = manifest.routes.length;
|
|
226
|
-
const withSchemas = manifest.routes.filter((r) => r.schemas.hasParamsSchema || r.schemas.hasQuerySchema).length;
|
|
227
|
-
const staticCount = manifest.routes.filter((r) => r.params.length === 0).length;
|
|
228
|
-
const dynamicCount = total - staticCount;
|
|
229
|
-
lines.push(`[Analog] Generated typed routes:`);
|
|
230
|
-
lines.push(` ${total} routes (${staticCount} static, ${dynamicCount} dynamic)`);
|
|
231
|
-
if (withSchemas > 0) lines.push(` ${withSchemas} with schema validation`);
|
|
232
|
-
for (const route of manifest.routes) {
|
|
233
|
-
const flags = [];
|
|
234
|
-
if (route.schemas.hasParamsSchema) flags.push("params-schema");
|
|
235
|
-
if (route.schemas.hasQuerySchema) flags.push("query-schema");
|
|
236
|
-
const suffix = flags.length > 0 ? ` [${flags.join(", ")}]` : "";
|
|
237
|
-
lines.push(` ${route.fullPath}${suffix}`);
|
|
238
|
-
}
|
|
239
|
-
return lines.join("\n");
|
|
240
|
-
}
|
|
241
|
-
/**
|
|
242
220
|
* Generates the route-table section for the combined generated route module.
|
|
221
|
+
*
|
|
222
|
+
* `resolveModuleImports` adds `routeMeta` and `load` types for pages whose
|
|
223
|
+
* URL is defined by a single file, along with the `routeMeta` of the layouts
|
|
224
|
+
* that wrap them. URLs shared by several files, such as a layout and its
|
|
225
|
+
* index page, are ambiguous and only get params and query types.
|
|
243
226
|
*/
|
|
244
|
-
function generateRouteTableDeclaration(manifest) {
|
|
227
|
+
function generateRouteTableDeclaration(manifest, resolveModuleImports) {
|
|
245
228
|
const lines = [];
|
|
246
|
-
|
|
229
|
+
lines.push("/** @noprettier */");
|
|
247
230
|
lines.push("// This file is auto-generated by @analogjs/platform");
|
|
248
|
-
lines.push("
|
|
249
|
-
lines.push("");
|
|
250
|
-
if (hasAnySchema) lines.push("import type { StandardSchemaV1 } from '@standard-schema/spec';");
|
|
251
|
-
const schemaImports = /* @__PURE__ */ new Map();
|
|
252
|
-
let aliasIndex = 0;
|
|
253
|
-
for (const route of manifest.routes) if (route.schemas.hasParamsSchema || route.schemas.hasQuerySchema) {
|
|
254
|
-
const importPath = filenameToImportPath(route.filename);
|
|
255
|
-
const names = [];
|
|
256
|
-
if (route.schemas.hasParamsSchema) {
|
|
257
|
-
const alias = `_p${aliasIndex}`;
|
|
258
|
-
names.push(`routeParamsSchema as ${alias}`);
|
|
259
|
-
schemaImports.set(`${route.fullPath}:params`, alias);
|
|
260
|
-
}
|
|
261
|
-
if (route.schemas.hasQuerySchema) {
|
|
262
|
-
const alias = `_q${aliasIndex}`;
|
|
263
|
-
names.push(`routeQuerySchema as ${alias}`);
|
|
264
|
-
schemaImports.set(`${route.fullPath}:query`, alias);
|
|
265
|
-
}
|
|
266
|
-
lines.push(`import type { ${names.join(", ")} } from '${importPath}';`);
|
|
267
|
-
aliasIndex++;
|
|
268
|
-
}
|
|
269
|
-
if (hasAnySchema) lines.push("");
|
|
231
|
+
lines.push("import type {} from '@analogjs/router';");
|
|
270
232
|
lines.push("declare module '@analogjs/router' {");
|
|
271
233
|
lines.push(" interface AnalogRouteTable {");
|
|
234
|
+
const importType = (specifier, name) => `typeof import(${JSON.stringify(specifier)}).${name}`;
|
|
272
235
|
for (const route of manifest.canonicalByFullPath.values()) {
|
|
273
|
-
const paramsAlias = schemaImports.get(`${route.fullPath}:params`);
|
|
274
|
-
const queryAlias = schemaImports.get(`${route.fullPath}:query`);
|
|
275
236
|
const paramsType = generateParamsType(route.params);
|
|
276
237
|
const queryType = "Record<string, string | string[] | undefined>";
|
|
277
|
-
|
|
278
|
-
const queryOutputType = queryAlias ? `StandardSchemaV1.InferOutput<typeof ${queryAlias}>` : queryType;
|
|
279
|
-
lines.push(` '${route.fullPath}': {`);
|
|
238
|
+
lines.push(` ${JSON.stringify(route.fullPath)}: {`);
|
|
280
239
|
lines.push(` params: ${paramsType};`);
|
|
281
|
-
lines.push(` paramsOutput: ${paramsOutputType};`);
|
|
282
240
|
lines.push(` query: ${queryType};`);
|
|
283
|
-
|
|
241
|
+
if (resolveModuleImports && route.kind === "page" && !manifest.sharedFullPaths.has(route.fullPath)) {
|
|
242
|
+
const layoutRouteMeta = route.layouts.flatMap((layout) => {
|
|
243
|
+
const specifier = resolveModuleImports(layout)?.routeMeta;
|
|
244
|
+
return specifier ? [importType(specifier, "routeMeta")] : [];
|
|
245
|
+
});
|
|
246
|
+
const imports = resolveModuleImports(route.filename);
|
|
247
|
+
if (layoutRouteMeta.length) lines.push(` layoutRouteMeta: [${layoutRouteMeta.join(", ")}];`);
|
|
248
|
+
if (imports?.routeMeta) lines.push(` routeMeta: ${importType(imports.routeMeta, "routeMeta")};`);
|
|
249
|
+
if (imports?.load) lines.push(` load: ${importType(imports.load, "load")};`);
|
|
250
|
+
}
|
|
284
251
|
lines.push(` };`);
|
|
285
252
|
}
|
|
286
253
|
lines.push(" }");
|
|
@@ -290,84 +257,10 @@ function generateRouteTableDeclaration(manifest) {
|
|
|
290
257
|
lines.push("");
|
|
291
258
|
return lines.join("\n");
|
|
292
259
|
}
|
|
293
|
-
/**
|
|
294
|
-
* Generates the route-tree section for the combined generated route module.
|
|
295
|
-
*/
|
|
296
|
-
function generateRouteTreeDeclaration(manifest, options = {}) {
|
|
297
|
-
const lines = [];
|
|
298
|
-
const jsonLdFiles = new Set(options.jsonLdFiles ?? []);
|
|
299
|
-
lines.push("// This file is auto-generated by @analogjs/platform");
|
|
300
|
-
lines.push("// Do not edit manually");
|
|
301
|
-
lines.push("");
|
|
302
|
-
lines.push("export interface AnalogGeneratedRouteRecord<");
|
|
303
|
-
lines.push(" TId extends string = string,");
|
|
304
|
-
lines.push(" TPath extends string = string,");
|
|
305
|
-
lines.push(" TFullPath extends string = string,");
|
|
306
|
-
lines.push(" TParentId extends string | null = string | null,");
|
|
307
|
-
lines.push(" TChildren extends readonly string[] = readonly string[],");
|
|
308
|
-
lines.push("> {");
|
|
309
|
-
lines.push(" id: TId;");
|
|
310
|
-
lines.push(" path: TPath;");
|
|
311
|
-
lines.push(" fullPath: TFullPath;");
|
|
312
|
-
lines.push(" parentId: TParentId;");
|
|
313
|
-
lines.push(" children: TChildren;");
|
|
314
|
-
lines.push(" sourceFile: string;");
|
|
315
|
-
lines.push(" kind: 'page' | 'content';");
|
|
316
|
-
lines.push(" hasParamsSchema: boolean;");
|
|
317
|
-
lines.push(" hasQuerySchema: boolean;");
|
|
318
|
-
lines.push(" hasJsonLd: boolean;");
|
|
319
|
-
lines.push(" isIndex: boolean;");
|
|
320
|
-
lines.push(" isGroup: boolean;");
|
|
321
|
-
lines.push(" isCatchAll: boolean;");
|
|
322
|
-
lines.push(" isOptionalCatchAll: boolean;");
|
|
323
|
-
lines.push("}");
|
|
324
|
-
lines.push("");
|
|
325
|
-
lines.push("export interface AnalogFileRoutesById {");
|
|
326
|
-
for (const route of manifest.routes) lines.push(` ${toTsKey(route.id)}: AnalogGeneratedRouteRecord<${toTsStringLiteral(route.id)}, ${toTsStringLiteral(route.path)}, ${toTsStringLiteral(route.fullPath)}, ${route.parentId ? toTsStringLiteral(route.parentId) : "null"}, ${toReadonlyTupleType(route.children)}>;`);
|
|
327
|
-
lines.push("}");
|
|
328
|
-
lines.push("");
|
|
329
|
-
lines.push("export interface AnalogFileRoutesByFullPath {");
|
|
330
|
-
for (const route of manifest.canonicalByFullPath.values()) lines.push(` ${toTsKey(route.fullPath)}: AnalogFileRoutesById[${toTsStringLiteral(route.id)}];`);
|
|
331
|
-
lines.push("}");
|
|
332
|
-
lines.push("");
|
|
333
|
-
lines.push("export type AnalogRouteTreeId = keyof AnalogFileRoutesById;");
|
|
334
|
-
lines.push("export type AnalogRouteTreeFullPath = keyof AnalogFileRoutesByFullPath;");
|
|
335
|
-
lines.push("");
|
|
336
|
-
lines.push("export const analogRouteTree = {");
|
|
337
|
-
lines.push(" byId: {");
|
|
338
|
-
for (const route of manifest.routes) {
|
|
339
|
-
lines.push(` ${toObjectKey(route.id)}: {`);
|
|
340
|
-
lines.push(` id: ${toTsStringLiteral(route.id)},`);
|
|
341
|
-
lines.push(` path: ${toTsStringLiteral(route.path)},`);
|
|
342
|
-
lines.push(` fullPath: ${toTsStringLiteral(route.fullPath)},`);
|
|
343
|
-
lines.push(` parentId: ${route.parentId ? toTsStringLiteral(route.parentId) : "null"},`);
|
|
344
|
-
lines.push(` children: ${toReadonlyTupleValue(route.children)},`);
|
|
345
|
-
lines.push(` sourceFile: ${toTsStringLiteral(route.filename)},`);
|
|
346
|
-
lines.push(` kind: ${toTsStringLiteral(route.kind)},`);
|
|
347
|
-
lines.push(` hasParamsSchema: ${String(route.schemas.hasParamsSchema)},`);
|
|
348
|
-
lines.push(` hasQuerySchema: ${String(route.schemas.hasQuerySchema)},`);
|
|
349
|
-
lines.push(` hasJsonLd: ${String(jsonLdFiles.has(route.filename))},`);
|
|
350
|
-
lines.push(` isIndex: ${String(route.isIndex)},`);
|
|
351
|
-
lines.push(` isGroup: ${String(route.isGroup)},`);
|
|
352
|
-
lines.push(` isCatchAll: ${String(route.isCatchAll)},`);
|
|
353
|
-
lines.push(` isOptionalCatchAll: ${String(route.isOptionalCatchAll)},`);
|
|
354
|
-
lines.push(` } satisfies AnalogFileRoutesById[${toTsStringLiteral(route.id)}],`);
|
|
355
|
-
}
|
|
356
|
-
lines.push(" },");
|
|
357
|
-
lines.push(" byFullPath: {");
|
|
358
|
-
for (const route of manifest.canonicalByFullPath.values()) lines.push(` ${toObjectKey(route.fullPath)}: ${toTsStringLiteral(route.id)},`);
|
|
359
|
-
lines.push(" },");
|
|
360
|
-
lines.push("} as const;");
|
|
361
|
-
lines.push("");
|
|
362
|
-
return lines.join("\n");
|
|
363
|
-
}
|
|
364
|
-
function filenameToImportPath(filename) {
|
|
365
|
-
return "../" + filename.replace(/^\//, "").replace(/\.ts$/, "");
|
|
366
|
-
}
|
|
367
260
|
function generateParamsType(params) {
|
|
368
261
|
if (params.length === 0) return "Record<string, never>";
|
|
369
262
|
return `{ ${params.map((p) => {
|
|
370
|
-
const key = isValidIdentifier(p.name) ? p.name :
|
|
263
|
+
const key = isValidIdentifier(p.name) ? p.name : JSON.stringify(p.name);
|
|
371
264
|
switch (p.type) {
|
|
372
265
|
case "dynamic": return `${key}: string`;
|
|
373
266
|
case "catchAll": return `${key}: string[]`;
|
|
@@ -401,31 +294,7 @@ function computeLocalPath(fullPath, parentFullPath) {
|
|
|
401
294
|
if (!parentFullPath) return fullPath.slice(1);
|
|
402
295
|
return fullPath.slice(parentFullPath.length).replace(/^\/+/, "") || "/";
|
|
403
296
|
}
|
|
404
|
-
function toTsStringLiteral(value) {
|
|
405
|
-
return JSON.stringify(value);
|
|
406
|
-
}
|
|
407
|
-
function toTsKey(value) {
|
|
408
|
-
return toTsStringLiteral(value);
|
|
409
|
-
}
|
|
410
|
-
function toObjectKey(value) {
|
|
411
|
-
return isValidIdentifier(value) ? value : toTsStringLiteral(value);
|
|
412
|
-
}
|
|
413
|
-
function toReadonlyTupleType(values) {
|
|
414
|
-
if (values.length === 0) return "readonly []";
|
|
415
|
-
return `readonly [${values.map((value) => toTsStringLiteral(value)).join(", ")}]`;
|
|
416
|
-
}
|
|
417
|
-
function toReadonlyTupleValue(values) {
|
|
418
|
-
if (values.length === 0) return "[] as const";
|
|
419
|
-
return `[${values.map((value) => toTsStringLiteral(value)).join(", ")}] as const`;
|
|
420
|
-
}
|
|
421
|
-
function isJsonLdObject(value) {
|
|
422
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
423
|
-
}
|
|
424
|
-
function normalizeJsonLd(value) {
|
|
425
|
-
if (Array.isArray(value)) return value.filter(isJsonLdObject);
|
|
426
|
-
return isJsonLdObject(value) ? [value] : [];
|
|
427
|
-
}
|
|
428
297
|
//#endregion
|
|
429
|
-
export {
|
|
298
|
+
export { generateRouteManifest, generateRouteTableDeclaration };
|
|
430
299
|
|
|
431
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\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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"}
|
|
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[\\\\/](?: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 // 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[\\\\/](?: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 * 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|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 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|analog|ag)$|\\.(?:ts|md|analog|ag)$/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('/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/** 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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"}
|