@dereekb/dbx-cli 13.16.0 → 13.17.0
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/eslint/index.cjs.default.js +1 -0
- package/eslint/index.cjs.js +1050 -0
- package/eslint/index.cjs.mjs +2 -0
- package/eslint/index.d.ts +1 -0
- package/eslint/index.esm.js +1046 -0
- package/eslint/package.json +25 -0
- package/eslint/rollup.alias-internal.config.d.ts +11 -0
- package/eslint/src/index.d.ts +1 -0
- package/eslint/src/lib/index.d.ts +2 -0
- package/eslint/src/lib/plugin.d.ts +22 -0
- package/eslint/src/lib/valid-dbx-route-model-tags.rule.d.ts +59 -0
- package/firebase-api-manifest/main.js +177 -102
- package/firebase-api-manifest/package.json +3 -3
- package/generate-firestore-indexes/main.js +2 -2
- package/generate-firestore-indexes/package.json +2 -2
- package/generate-mcp-manifest/main.js +8 -2
- package/generate-mcp-manifest/package.json +3 -3
- package/generate-route-manifest/main.js +1137 -0
- package/generate-route-manifest/package.json +10 -0
- package/index.cjs.js +4375 -1687
- package/index.esm.js +4355 -1688
- package/lint-cache/package.json +2 -2
- package/manifest-extract/index.cjs.js +54 -132
- package/manifest-extract/index.esm.js +53 -131
- package/manifest-extract/package.json +9 -4
- package/package.json +16 -6
- package/src/lib/index.d.ts +2 -0
- package/src/lib/manifest/types.d.ts +53 -0
- package/src/lib/mcp-scan/manifest/package-root.d.ts +17 -0
- package/src/lib/mcp-scan/manifest/tokens-schema.d.ts +5 -4
- package/src/lib/mcp-scan/scan/extract-models/assemble.d.ts +17 -0
- package/src/lib/route/component-resolve.d.ts +48 -0
- package/src/lib/route/index.d.ts +18 -0
- package/src/lib/route/route-build-tree.d.ts +31 -0
- package/src/lib/route/route-extract.d.ts +46 -0
- package/src/lib/route/route-load-tree.d.ts +17 -0
- package/src/lib/route/route-manifest.d.ts +132 -0
- package/src/lib/route/route-model-tag.d.ts +89 -0
- package/src/lib/route/route-models-extract.d.ts +22 -0
- package/src/lib/route/route-resolve-sources.d.ts +39 -0
- package/src/lib/route/route-types.d.ts +136 -0
- package/src/lib/route/url-match.d.ts +116 -0
- package/src/lib/scan-helpers/firestore-model-extract-utils.d.ts +43 -0
- package/test/package.json +9 -9
|
@@ -0,0 +1,1137 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createRequire as __createRequire } from 'node:module';
|
|
3
|
+
const require = __createRequire(import.meta.url);
|
|
4
|
+
|
|
5
|
+
// packages/dbx-cli/generate-route-manifest/src/generate-route-manifest/main.ts
|
|
6
|
+
import { glob as fsGlob, mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
7
|
+
import { existsSync } from "node:fs";
|
|
8
|
+
import { dirname as dirname2, isAbsolute, relative, resolve } from "node:path";
|
|
9
|
+
|
|
10
|
+
// packages/dbx-cli/src/lib/route/route-extract.ts
|
|
11
|
+
import { Node, Project, SyntaxKind } from "ts-morph";
|
|
12
|
+
var ROUTE_MODEL_TAG_PREFIX = "dbxRouteModel";
|
|
13
|
+
function extractFile(source) {
|
|
14
|
+
const project = new Project({ useInMemoryFileSystem: true, skipAddingFilesFromTsConfig: true });
|
|
15
|
+
const sourceFile = project.createSourceFile(source.name, source.text, { overwrite: true });
|
|
16
|
+
const issues = [];
|
|
17
|
+
const objectLiteralsByName = collectStateConsts(sourceFile);
|
|
18
|
+
const collected = /* @__PURE__ */ new Map();
|
|
19
|
+
collectTypedStateConsts({ source, objectLiteralsByName, collected, issues });
|
|
20
|
+
collectArrayLiteralStates({ source, sourceFile, objectLiteralsByName, collected, issues });
|
|
21
|
+
const importedFromRelative = collectRelativeImports(sourceFile, source.name);
|
|
22
|
+
const result = {
|
|
23
|
+
nodes: Array.from(collected.values()),
|
|
24
|
+
issues,
|
|
25
|
+
importedFromRelative
|
|
26
|
+
};
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
function collectTypedStateConsts(input) {
|
|
30
|
+
const { source, objectLiteralsByName, collected, issues } = input;
|
|
31
|
+
for (const [name, info] of objectLiteralsByName) {
|
|
32
|
+
if (!info.typedAsState) continue;
|
|
33
|
+
addNode({ file: source.name, literal: info.literal, fallbackConstName: name, jsDocTags: info.jsDocTags, into: collected, issues });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function collectArrayLiteralStates(input) {
|
|
37
|
+
const { source, sourceFile, objectLiteralsByName, collected, issues } = input;
|
|
38
|
+
for (const arrayLit of findStateArrayLiterals(sourceFile)) {
|
|
39
|
+
for (const element of arrayLit.getElements()) {
|
|
40
|
+
collectArrayElement({ element, source, objectLiteralsByName, collected, issues });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function collectArrayElement(input) {
|
|
45
|
+
const { element, source, objectLiteralsByName, collected, issues } = input;
|
|
46
|
+
if (Node.isObjectLiteralExpression(element)) {
|
|
47
|
+
addNode({ file: source.name, literal: element, fallbackConstName: void 0, jsDocTags: void 0, into: collected, issues });
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (Node.isIdentifier(element)) {
|
|
51
|
+
const ref = objectLiteralsByName.get(element.getText());
|
|
52
|
+
if (ref) {
|
|
53
|
+
addNode({ file: source.name, literal: ref.literal, fallbackConstName: element.getText(), jsDocTags: ref.jsDocTags, into: collected, issues });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function collectStateConsts(sourceFile) {
|
|
58
|
+
const out = /* @__PURE__ */ new Map();
|
|
59
|
+
for (const stmt of sourceFile.getVariableStatements()) {
|
|
60
|
+
const jsDocTags = collectRouteModelTags(stmt.getJsDocs());
|
|
61
|
+
for (const decl of stmt.getDeclarations()) {
|
|
62
|
+
const initializer = decl.getInitializer();
|
|
63
|
+
if (!initializer || !Node.isObjectLiteralExpression(initializer)) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const typeNode = decl.getTypeNode();
|
|
67
|
+
const typedAsState = typeNode ? typeNode.getText() === "Ng2StateDeclaration" : false;
|
|
68
|
+
out.set(decl.getName(), { literal: initializer, typedAsState, jsDocTags });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
function collectRouteModelTags(jsDocs) {
|
|
74
|
+
const out = [];
|
|
75
|
+
for (const jsDoc of jsDocs) {
|
|
76
|
+
for (const tag of jsDoc.getTags()) {
|
|
77
|
+
const name = tag.getTagName();
|
|
78
|
+
if (name.startsWith(ROUTE_MODEL_TAG_PREFIX)) {
|
|
79
|
+
out.push({ name, text: tag.getCommentText()?.trim() ?? "" });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return out.length > 0 ? out : void 0;
|
|
84
|
+
}
|
|
85
|
+
function findStateArrayLiterals(sourceFile) {
|
|
86
|
+
const arrays = [];
|
|
87
|
+
collectTopLevelStateArrays(sourceFile, arrays);
|
|
88
|
+
collectProvideStatesArrays(sourceFile, arrays);
|
|
89
|
+
return arrays;
|
|
90
|
+
}
|
|
91
|
+
function collectTopLevelStateArrays(sourceFile, arrays) {
|
|
92
|
+
for (const stmt of sourceFile.getVariableStatements()) {
|
|
93
|
+
for (const decl of stmt.getDeclarations()) {
|
|
94
|
+
const initializer = decl.getInitializer();
|
|
95
|
+
if (initializer && Node.isArrayLiteralExpression(initializer) && declarationIsStateArray(stmt, decl.getName(), initializer)) {
|
|
96
|
+
arrays.push(initializer);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function collectProvideStatesArrays(sourceFile, arrays) {
|
|
102
|
+
for (const call of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
103
|
+
if (!isProvideStatesCall(call.getExpression().getText())) continue;
|
|
104
|
+
for (const arg of call.getArguments()) {
|
|
105
|
+
if (!Node.isObjectLiteralExpression(arg)) continue;
|
|
106
|
+
collectStatesArrayFromObjectArg(arg, sourceFile, arrays);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function isProvideStatesCall(exprText) {
|
|
111
|
+
if (exprText === "provideStates") return true;
|
|
112
|
+
return exprText.endsWith(".forChild") || exprText.endsWith(".forRoot");
|
|
113
|
+
}
|
|
114
|
+
function collectStatesArrayFromObjectArg(arg, sourceFile, arrays) {
|
|
115
|
+
const statesProp = arg.getProperty("states");
|
|
116
|
+
if (!statesProp || !Node.isPropertyAssignment(statesProp)) return;
|
|
117
|
+
const initializer = statesProp.getInitializer();
|
|
118
|
+
if (!initializer) return;
|
|
119
|
+
if (Node.isArrayLiteralExpression(initializer)) {
|
|
120
|
+
arrays.push(initializer);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (Node.isIdentifier(initializer)) {
|
|
124
|
+
const referenced = findArrayLiteralForIdentifier(sourceFile, initializer);
|
|
125
|
+
if (referenced) {
|
|
126
|
+
arrays.push(referenced);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function declarationIsStateArray(stmt, name, literal) {
|
|
131
|
+
const decl = stmt.getDeclarations().find((d) => d.getName() === name);
|
|
132
|
+
const typeNode = decl?.getTypeNode();
|
|
133
|
+
if (typeNode?.getText() === "Ng2StateDeclaration[]") {
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
for (const element of literal.getElements()) {
|
|
137
|
+
if (Node.isObjectLiteralExpression(element) && hasStringNameProperty(element)) {
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
if (Node.isIdentifier(element)) {
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
function hasStringNameProperty(literal) {
|
|
147
|
+
const nameProp = literal.getProperty("name");
|
|
148
|
+
if (!nameProp || !Node.isPropertyAssignment(nameProp)) {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
const initializer = nameProp.getInitializer();
|
|
152
|
+
return initializer !== void 0 && (Node.isStringLiteral(initializer) || Node.isNoSubstitutionTemplateLiteral(initializer));
|
|
153
|
+
}
|
|
154
|
+
function findArrayLiteralForIdentifier(sourceFile, identifier) {
|
|
155
|
+
const name = identifier.getText();
|
|
156
|
+
for (const stmt of sourceFile.getVariableStatements()) {
|
|
157
|
+
for (const decl of stmt.getDeclarations()) {
|
|
158
|
+
if (decl.getName() !== name) {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
const initializer = decl.getInitializer();
|
|
162
|
+
if (initializer && Node.isArrayLiteralExpression(initializer)) {
|
|
163
|
+
return initializer;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return void 0;
|
|
168
|
+
}
|
|
169
|
+
function addNode(options) {
|
|
170
|
+
const { file, literal, fallbackConstName, jsDocTags, into, issues } = options;
|
|
171
|
+
const stringField = (key) => stringPropertyValue(literal, key);
|
|
172
|
+
const name = stringField("name");
|
|
173
|
+
const line = literal.getStartLineNumber();
|
|
174
|
+
if (!name) {
|
|
175
|
+
issues.push({
|
|
176
|
+
code: "DYNAMIC_STATE_NAME",
|
|
177
|
+
severity: "info",
|
|
178
|
+
message: fallbackConstName ? `State \`${fallbackConstName}\` has no string-literal \`name\` field; skipping.` : `State at line ${line} has no string-literal \`name\` field; skipping.`,
|
|
179
|
+
file,
|
|
180
|
+
line,
|
|
181
|
+
stateName: void 0
|
|
182
|
+
});
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const futureState = name.endsWith(".**");
|
|
186
|
+
const explicitParent = stringField("parent");
|
|
187
|
+
const url = stringField("url");
|
|
188
|
+
const redirectTo = stringField("redirectTo");
|
|
189
|
+
const abstract = booleanPropertyValue(literal, "abstract") ?? false;
|
|
190
|
+
const component = identifierPropertyValue(literal, "component");
|
|
191
|
+
const paramKeys = objectPropertyKeys(literal, "params");
|
|
192
|
+
const resolveKeys = collectResolveKeys(literal);
|
|
193
|
+
const node = {
|
|
194
|
+
name,
|
|
195
|
+
url,
|
|
196
|
+
component,
|
|
197
|
+
explicitParent,
|
|
198
|
+
redirectTo,
|
|
199
|
+
abstract,
|
|
200
|
+
futureState,
|
|
201
|
+
paramKeys,
|
|
202
|
+
resolveKeys,
|
|
203
|
+
file,
|
|
204
|
+
line,
|
|
205
|
+
declaredAs: fallbackConstName,
|
|
206
|
+
jsDocTags
|
|
207
|
+
};
|
|
208
|
+
if (!into.has(name)) {
|
|
209
|
+
into.set(name, node);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function stringPropertyValue(literal, key) {
|
|
213
|
+
const prop = literal.getProperty(key);
|
|
214
|
+
if (!prop || !Node.isPropertyAssignment(prop)) {
|
|
215
|
+
return void 0;
|
|
216
|
+
}
|
|
217
|
+
const initializer = prop.getInitializer();
|
|
218
|
+
if (!initializer) {
|
|
219
|
+
return void 0;
|
|
220
|
+
}
|
|
221
|
+
if (Node.isStringLiteral(initializer) || Node.isNoSubstitutionTemplateLiteral(initializer)) {
|
|
222
|
+
return initializer.getLiteralText();
|
|
223
|
+
}
|
|
224
|
+
return void 0;
|
|
225
|
+
}
|
|
226
|
+
function booleanPropertyValue(literal, key) {
|
|
227
|
+
const prop = literal.getProperty(key);
|
|
228
|
+
if (!prop || !Node.isPropertyAssignment(prop)) {
|
|
229
|
+
return void 0;
|
|
230
|
+
}
|
|
231
|
+
const initializer = prop.getInitializer();
|
|
232
|
+
if (!initializer) {
|
|
233
|
+
return void 0;
|
|
234
|
+
}
|
|
235
|
+
if (initializer.getKind() === SyntaxKind.TrueKeyword) {
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
if (initializer.getKind() === SyntaxKind.FalseKeyword) {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
return void 0;
|
|
242
|
+
}
|
|
243
|
+
function identifierPropertyValue(literal, key) {
|
|
244
|
+
const prop = literal.getProperty(key);
|
|
245
|
+
if (!prop || !Node.isPropertyAssignment(prop)) {
|
|
246
|
+
return void 0;
|
|
247
|
+
}
|
|
248
|
+
const initializer = prop.getInitializer();
|
|
249
|
+
if (!initializer) {
|
|
250
|
+
return void 0;
|
|
251
|
+
}
|
|
252
|
+
if (Node.isIdentifier(initializer)) {
|
|
253
|
+
return initializer.getText();
|
|
254
|
+
}
|
|
255
|
+
if (Node.isPropertyAccessExpression(initializer)) {
|
|
256
|
+
return initializer.getText();
|
|
257
|
+
}
|
|
258
|
+
return void 0;
|
|
259
|
+
}
|
|
260
|
+
function objectPropertyKeys(literal, key) {
|
|
261
|
+
const prop = literal.getProperty(key);
|
|
262
|
+
if (!prop || !Node.isPropertyAssignment(prop)) {
|
|
263
|
+
return [];
|
|
264
|
+
}
|
|
265
|
+
const initializer = prop.getInitializer();
|
|
266
|
+
if (!initializer || !Node.isObjectLiteralExpression(initializer)) {
|
|
267
|
+
return [];
|
|
268
|
+
}
|
|
269
|
+
const out = [];
|
|
270
|
+
for (const member of initializer.getProperties()) {
|
|
271
|
+
if (Node.isPropertyAssignment(member) || Node.isShorthandPropertyAssignment(member) || Node.isMethodDeclaration(member)) {
|
|
272
|
+
const nameNode = member.getNameNode();
|
|
273
|
+
if (nameNode) {
|
|
274
|
+
out.push(nameNode.getText());
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return out;
|
|
279
|
+
}
|
|
280
|
+
function collectResolveKeys(literal) {
|
|
281
|
+
const prop = literal.getProperty("resolve");
|
|
282
|
+
if (!prop || !Node.isPropertyAssignment(prop)) {
|
|
283
|
+
return [];
|
|
284
|
+
}
|
|
285
|
+
const initializer = prop.getInitializer();
|
|
286
|
+
if (!initializer) {
|
|
287
|
+
return [];
|
|
288
|
+
}
|
|
289
|
+
if (Node.isObjectLiteralExpression(initializer)) {
|
|
290
|
+
return collectResolveKeysFromObject(initializer);
|
|
291
|
+
}
|
|
292
|
+
if (Node.isArrayLiteralExpression(initializer)) {
|
|
293
|
+
return collectResolveKeysFromArray(initializer);
|
|
294
|
+
}
|
|
295
|
+
return [];
|
|
296
|
+
}
|
|
297
|
+
function collectResolveKeysFromObject(initializer) {
|
|
298
|
+
const keys = [];
|
|
299
|
+
for (const member of initializer.getProperties()) {
|
|
300
|
+
if (!Node.isPropertyAssignment(member) && !Node.isShorthandPropertyAssignment(member) && !Node.isMethodDeclaration(member)) continue;
|
|
301
|
+
const nameNode = member.getNameNode();
|
|
302
|
+
if (nameNode) {
|
|
303
|
+
keys.push(nameNode.getText());
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return keys;
|
|
307
|
+
}
|
|
308
|
+
function collectResolveKeysFromArray(initializer) {
|
|
309
|
+
const keys = [];
|
|
310
|
+
for (const element of initializer.getElements()) {
|
|
311
|
+
if (!Node.isObjectLiteralExpression(element)) continue;
|
|
312
|
+
const token = readResolveTokenValue(element);
|
|
313
|
+
if (token !== void 0) {
|
|
314
|
+
keys.push(token);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return keys;
|
|
318
|
+
}
|
|
319
|
+
function readResolveTokenValue(element) {
|
|
320
|
+
const tokenProp = element.getProperty("token");
|
|
321
|
+
if (!tokenProp || !Node.isPropertyAssignment(tokenProp)) return void 0;
|
|
322
|
+
const tokenInit = tokenProp.getInitializer();
|
|
323
|
+
if (!tokenInit || !Node.isStringLiteral(tokenInit) && !Node.isIdentifier(tokenInit)) return void 0;
|
|
324
|
+
return tokenInit.getText().replaceAll(/^['"]|['"]$/g, "");
|
|
325
|
+
}
|
|
326
|
+
function collectRelativeImports(sourceFile, fileName) {
|
|
327
|
+
const out = [];
|
|
328
|
+
for (const decl of sourceFile.getImportDeclarations()) {
|
|
329
|
+
const specifier = decl.getModuleSpecifierValue();
|
|
330
|
+
if (specifier.startsWith(".")) {
|
|
331
|
+
out.push({ moduleSpecifier: specifier, file: fileName });
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return out;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// packages/dbx-cli/src/lib/route/route-build-tree.ts
|
|
338
|
+
// @__NO_SIDE_EFFECTS__
|
|
339
|
+
function buildRouteTree(nodes, extractIssues) {
|
|
340
|
+
const issues = [...extractIssues];
|
|
341
|
+
const byName = /* @__PURE__ */ new Map();
|
|
342
|
+
insertNodes(nodes, byName, issues);
|
|
343
|
+
wireParentLinks(byName, issues);
|
|
344
|
+
detectCycles(byName, issues);
|
|
345
|
+
composeFullUrls(byName);
|
|
346
|
+
const roots = collectRoots(byName);
|
|
347
|
+
sortChildren(byName, roots);
|
|
348
|
+
const { frozen, frozenRoots } = freezeTree(roots);
|
|
349
|
+
const result = {
|
|
350
|
+
roots: frozenRoots,
|
|
351
|
+
byName: frozen,
|
|
352
|
+
issues,
|
|
353
|
+
filesChecked: 0,
|
|
354
|
+
nodeCount: byName.size
|
|
355
|
+
};
|
|
356
|
+
return result;
|
|
357
|
+
}
|
|
358
|
+
function insertNodes(nodes, byName, issues) {
|
|
359
|
+
for (const node of nodes) {
|
|
360
|
+
if (byName.has(node.name)) {
|
|
361
|
+
issues.push({
|
|
362
|
+
code: "DUPLICATE_STATE_NAME",
|
|
363
|
+
severity: "error",
|
|
364
|
+
message: `State \`${node.name}\` is declared in multiple places. Keeping the first declaration.`,
|
|
365
|
+
file: node.file,
|
|
366
|
+
line: node.line,
|
|
367
|
+
stateName: node.name
|
|
368
|
+
});
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
byName.set(node.name, { data: node, fullUrl: void 0, parent: void 0, children: [] });
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
function wireParentLinks(byName, issues) {
|
|
375
|
+
for (const treeNode of byName.values()) {
|
|
376
|
+
const parentName = resolveParentName(treeNode.data, byName);
|
|
377
|
+
if (!parentName) continue;
|
|
378
|
+
const parent = byName.get(parentName);
|
|
379
|
+
if (!parent) {
|
|
380
|
+
issues.push({
|
|
381
|
+
code: "ORPHAN_STATE",
|
|
382
|
+
severity: "warning",
|
|
383
|
+
message: `State \`${treeNode.data.name}\` references parent \`${parentName}\` which is not declared in the analyzed sources.`,
|
|
384
|
+
file: treeNode.data.file,
|
|
385
|
+
line: treeNode.data.line,
|
|
386
|
+
stateName: treeNode.data.name
|
|
387
|
+
});
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
treeNode.parent = parent;
|
|
391
|
+
parent.children.push(treeNode);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
function detectCycles(byName, issues) {
|
|
395
|
+
for (const treeNode of byName.values()) {
|
|
396
|
+
if (!treeNode.parent) continue;
|
|
397
|
+
detectCycleFor(treeNode, issues);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
function detectCycleFor(treeNode, issues) {
|
|
401
|
+
const seen = /* @__PURE__ */ new Set();
|
|
402
|
+
seen.add(treeNode.data.name);
|
|
403
|
+
let cursor = treeNode.parent;
|
|
404
|
+
while (cursor) {
|
|
405
|
+
if (seen.has(cursor.data.name)) {
|
|
406
|
+
issues.push({
|
|
407
|
+
code: "CYCLE_DETECTED",
|
|
408
|
+
severity: "error",
|
|
409
|
+
message: `Cycle detected involving \`${treeNode.data.name}\` and \`${cursor.data.name}\`. Severing the parent link.`,
|
|
410
|
+
file: treeNode.data.file,
|
|
411
|
+
line: treeNode.data.line,
|
|
412
|
+
stateName: treeNode.data.name
|
|
413
|
+
});
|
|
414
|
+
detachFromParent(treeNode);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
seen.add(cursor.data.name);
|
|
418
|
+
cursor = cursor.parent;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
function detachFromParent(treeNode) {
|
|
422
|
+
if (!treeNode.parent) return;
|
|
423
|
+
const idx = treeNode.parent.children.indexOf(treeNode);
|
|
424
|
+
if (idx >= 0) {
|
|
425
|
+
treeNode.parent.children.splice(idx, 1);
|
|
426
|
+
}
|
|
427
|
+
treeNode.parent = void 0;
|
|
428
|
+
}
|
|
429
|
+
function composeFullUrls(byName) {
|
|
430
|
+
for (const treeNode of byName.values()) {
|
|
431
|
+
treeNode.fullUrl = composeFullUrl(treeNode);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
function collectRoots(byName) {
|
|
435
|
+
const roots = [];
|
|
436
|
+
for (const treeNode of byName.values()) {
|
|
437
|
+
if (!treeNode.parent) {
|
|
438
|
+
roots.push(treeNode);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return roots;
|
|
442
|
+
}
|
|
443
|
+
function sortChildren(byName, roots) {
|
|
444
|
+
for (const treeNode of byName.values()) {
|
|
445
|
+
treeNode.children.sort(compareByName);
|
|
446
|
+
}
|
|
447
|
+
roots.sort(compareByName);
|
|
448
|
+
}
|
|
449
|
+
function freezeTree(roots) {
|
|
450
|
+
const frozen = /* @__PURE__ */ new Map();
|
|
451
|
+
const freeze = (mut) => {
|
|
452
|
+
const existing = frozen.get(mut.data.name);
|
|
453
|
+
if (existing) return existing;
|
|
454
|
+
const placeholder = {
|
|
455
|
+
data: mut.data,
|
|
456
|
+
fullUrl: mut.fullUrl,
|
|
457
|
+
parent: void 0,
|
|
458
|
+
children: []
|
|
459
|
+
};
|
|
460
|
+
frozen.set(mut.data.name, placeholder);
|
|
461
|
+
placeholder.parent = mut.parent ? freeze(mut.parent) : void 0;
|
|
462
|
+
placeholder.children = mut.children.map(freeze);
|
|
463
|
+
return placeholder;
|
|
464
|
+
};
|
|
465
|
+
const frozenRoots = roots.map(freeze);
|
|
466
|
+
return { frozen, frozenRoots };
|
|
467
|
+
}
|
|
468
|
+
function resolveParentName(node, byName) {
|
|
469
|
+
if (node.explicitParent) {
|
|
470
|
+
return node.explicitParent;
|
|
471
|
+
}
|
|
472
|
+
const lookupName = node.name.endsWith(".**") ? node.name.slice(0, -3) : node.name;
|
|
473
|
+
const lastDot = lookupName.lastIndexOf(".");
|
|
474
|
+
if (lastDot < 0) {
|
|
475
|
+
return void 0;
|
|
476
|
+
}
|
|
477
|
+
const candidate = lookupName.slice(0, lastDot);
|
|
478
|
+
if (byName.has(candidate)) {
|
|
479
|
+
return candidate;
|
|
480
|
+
}
|
|
481
|
+
return candidate;
|
|
482
|
+
}
|
|
483
|
+
function composeFullUrl(node) {
|
|
484
|
+
const segments = [];
|
|
485
|
+
let cursor = node;
|
|
486
|
+
while (cursor) {
|
|
487
|
+
if (cursor.data.url !== void 0) {
|
|
488
|
+
segments.unshift(cursor.data.url);
|
|
489
|
+
}
|
|
490
|
+
cursor = cursor.parent;
|
|
491
|
+
}
|
|
492
|
+
if (segments.length === 0) {
|
|
493
|
+
return void 0;
|
|
494
|
+
}
|
|
495
|
+
const joined = segments.join("");
|
|
496
|
+
const collapsed = joined.replaceAll(/\/{2,}/g, "/");
|
|
497
|
+
return collapsed.length === 0 ? "/" : collapsed;
|
|
498
|
+
}
|
|
499
|
+
function compareByName(a, b) {
|
|
500
|
+
if (a.data.name < b.data.name) {
|
|
501
|
+
return -1;
|
|
502
|
+
}
|
|
503
|
+
if (a.data.name > b.data.name) {
|
|
504
|
+
return 1;
|
|
505
|
+
}
|
|
506
|
+
return 0;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// packages/dbx-cli/src/lib/route/route-resolve-sources.ts
|
|
510
|
+
function resolveRouteSources(sources) {
|
|
511
|
+
const nodes = [];
|
|
512
|
+
const issues = [];
|
|
513
|
+
for (const source of sources) {
|
|
514
|
+
const extracted = extractFile(source);
|
|
515
|
+
for (const node of extracted.nodes) {
|
|
516
|
+
nodes.push(node);
|
|
517
|
+
}
|
|
518
|
+
for (const issue of extracted.issues) {
|
|
519
|
+
issues.push(issue);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
const result = {
|
|
523
|
+
nodes,
|
|
524
|
+
issues,
|
|
525
|
+
filesChecked: sources.length
|
|
526
|
+
};
|
|
527
|
+
return result;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// packages/dbx-cli/src/lib/route/route-load-tree.ts
|
|
531
|
+
function loadRouteTree(args) {
|
|
532
|
+
const resolved = resolveRouteSources(args.sources);
|
|
533
|
+
const tree = buildRouteTree(resolved.nodes, resolved.issues);
|
|
534
|
+
const result = {
|
|
535
|
+
roots: tree.roots,
|
|
536
|
+
byName: tree.byName,
|
|
537
|
+
issues: tree.issues,
|
|
538
|
+
filesChecked: resolved.filesChecked,
|
|
539
|
+
nodeCount: tree.nodeCount
|
|
540
|
+
};
|
|
541
|
+
return result;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// packages/dbx-cli/src/lib/route/url-match.ts
|
|
545
|
+
function extractUrlParamKeys(fullUrl) {
|
|
546
|
+
if (fullUrl === void 0 || fullUrl.length === 0) {
|
|
547
|
+
return [];
|
|
548
|
+
}
|
|
549
|
+
const seen = /* @__PURE__ */ new Set();
|
|
550
|
+
const keys = [];
|
|
551
|
+
for (const segment of fullUrl.split("/")) {
|
|
552
|
+
const key = extractParamKeyFromSegment(segment);
|
|
553
|
+
if (key !== void 0 && !seen.has(key)) {
|
|
554
|
+
seen.add(key);
|
|
555
|
+
keys.push(key);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
return keys;
|
|
559
|
+
}
|
|
560
|
+
function extractParamKeyFromSegment(segment) {
|
|
561
|
+
if (segment.startsWith(":")) {
|
|
562
|
+
const key = segment.slice(1);
|
|
563
|
+
return key.length > 0 ? key : void 0;
|
|
564
|
+
}
|
|
565
|
+
if (segment.startsWith("{") && segment.endsWith("}")) {
|
|
566
|
+
const inner = segment.slice(1, -1);
|
|
567
|
+
const colonIdx = inner.indexOf(":");
|
|
568
|
+
const rawKey = colonIdx >= 0 ? inner.slice(0, colonIdx) : inner;
|
|
569
|
+
const key = rawKey.trim();
|
|
570
|
+
return key.length > 0 ? key : void 0;
|
|
571
|
+
}
|
|
572
|
+
return void 0;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// packages/dbx-cli/src/lib/route/component-resolve.ts
|
|
576
|
+
import { dirname, join, normalize } from "node:path/posix";
|
|
577
|
+
var TRY_EXTENSIONS = [".ts", ".tsx", "/index.ts", "/index.tsx"];
|
|
578
|
+
function resolveComponentSourceFromSources(input) {
|
|
579
|
+
const byName = /* @__PURE__ */ new Map();
|
|
580
|
+
for (const source of input.sources) {
|
|
581
|
+
byName.set(source.name, source);
|
|
582
|
+
}
|
|
583
|
+
const routerSource = byName.get(input.routerFile);
|
|
584
|
+
let result;
|
|
585
|
+
if (routerSource !== void 0) {
|
|
586
|
+
const specifier = findImportSpecifier(routerSource.text, input.component);
|
|
587
|
+
if (specifier !== void 0) {
|
|
588
|
+
result = resolveSpecifier(specifier, input.routerFile, byName);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
return result;
|
|
592
|
+
}
|
|
593
|
+
function resolveSpecifier(specifier, routerFile, byName) {
|
|
594
|
+
let result;
|
|
595
|
+
if (specifier.startsWith(".")) {
|
|
596
|
+
const base = normalize(join(dirname(routerFile), specifier));
|
|
597
|
+
let resolved;
|
|
598
|
+
for (const ext of TRY_EXTENSIONS) {
|
|
599
|
+
const candidate = base + ext;
|
|
600
|
+
if (byName.has(candidate)) {
|
|
601
|
+
resolved = candidate;
|
|
602
|
+
break;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
result = { path: resolved ?? specifier, moduleSpecifier: specifier };
|
|
606
|
+
} else {
|
|
607
|
+
result = { path: specifier, moduleSpecifier: specifier };
|
|
608
|
+
}
|
|
609
|
+
return result;
|
|
610
|
+
}
|
|
611
|
+
function findImportSpecifier(text, component) {
|
|
612
|
+
const importRegex = /import\s+[^'"]+from\s+['"]([^'"]+)['"]/gu;
|
|
613
|
+
let result;
|
|
614
|
+
let match = importRegex.exec(text);
|
|
615
|
+
while (match !== null) {
|
|
616
|
+
if (containsImportedSymbol(match[0], component)) {
|
|
617
|
+
result = match[1];
|
|
618
|
+
break;
|
|
619
|
+
}
|
|
620
|
+
match = importRegex.exec(text);
|
|
621
|
+
}
|
|
622
|
+
return result;
|
|
623
|
+
}
|
|
624
|
+
function containsImportedSymbol(importStatement, symbol) {
|
|
625
|
+
const braceStart = importStatement.indexOf("{");
|
|
626
|
+
const braceEnd = importStatement.indexOf("}");
|
|
627
|
+
let result = false;
|
|
628
|
+
if (braceStart >= 0 && braceEnd > braceStart) {
|
|
629
|
+
const inside = importStatement.slice(braceStart + 1, braceEnd);
|
|
630
|
+
for (const raw of inside.split(",")) {
|
|
631
|
+
const cleaned = raw.replace(/\s+as\s+\w+/u, "").trim();
|
|
632
|
+
if (cleaned === symbol) {
|
|
633
|
+
result = true;
|
|
634
|
+
break;
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
if (!result) {
|
|
639
|
+
const defaultRegex = new RegExp(String.raw`^import\s+(?:type\s+)?(${symbol})\s+from\s+`, "u");
|
|
640
|
+
result = defaultRegex.test(importStatement);
|
|
641
|
+
}
|
|
642
|
+
return result;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// packages/dbx-cli/src/lib/route/route-model-tag.ts
|
|
646
|
+
var MODEL_TYPE_RE = /^[a-zA-Z][a-zA-Z0-9]*$/u;
|
|
647
|
+
var LITERAL_SEGMENT_RE = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/u;
|
|
648
|
+
var AUTH_UID_PLACEHOLDER = "{authUid}";
|
|
649
|
+
var ROUTE_MODEL_TAG = "dbxRouteModel";
|
|
650
|
+
var ROUTE_MODEL_LIST_TAG = "dbxRouteModelList";
|
|
651
|
+
function parseRouteModelTag(tag) {
|
|
652
|
+
const dashIdx = tag.text.indexOf(" - ");
|
|
653
|
+
const head = (dashIdx >= 0 ? tag.text.slice(0, dashIdx) : tag.text).trim();
|
|
654
|
+
const description = dashIdx >= 0 ? tag.text.slice(dashIdx + 3).trim() : void 0;
|
|
655
|
+
const tokens = head.split(/\s+/u).filter((t) => t.length > 0);
|
|
656
|
+
let result;
|
|
657
|
+
if (tag.name === ROUTE_MODEL_LIST_TAG) {
|
|
658
|
+
result = parseListTag(tokens, description);
|
|
659
|
+
} else if (tag.name === ROUTE_MODEL_TAG) {
|
|
660
|
+
result = parseModelTag(tokens, description);
|
|
661
|
+
} else {
|
|
662
|
+
result = { ok: false, message: `Unknown route-model tag \`@${tag.name}\`. Expected \`@${ROUTE_MODEL_TAG}\` or \`@${ROUTE_MODEL_LIST_TAG}\`.` };
|
|
663
|
+
}
|
|
664
|
+
return result;
|
|
665
|
+
}
|
|
666
|
+
function parseListTag(tokens, description) {
|
|
667
|
+
let result;
|
|
668
|
+
if (tokens.length !== 1) {
|
|
669
|
+
result = { ok: false, message: `\`@${ROUTE_MODEL_LIST_TAG}\` expects a single \`<modelType>\` token; got ${tokens.length}.` };
|
|
670
|
+
} else if (MODEL_TYPE_RE.test(tokens[0])) {
|
|
671
|
+
result = { ok: true, model: { modelType: tokens[0], kind: "list", description, routeParams: [] } };
|
|
672
|
+
} else {
|
|
673
|
+
result = { ok: false, message: `\`@${ROUTE_MODEL_LIST_TAG}\` model type \`${tokens[0]}\` is not a valid identifier.` };
|
|
674
|
+
}
|
|
675
|
+
return result;
|
|
676
|
+
}
|
|
677
|
+
function parseModelTag(tokens, description) {
|
|
678
|
+
let result;
|
|
679
|
+
if (tokens.length !== 2) {
|
|
680
|
+
result = { ok: false, message: `\`@${ROUTE_MODEL_TAG}\` expects \`<modelType> <keyTemplate>\`; got ${tokens.length} token(s).` };
|
|
681
|
+
} else if (MODEL_TYPE_RE.test(tokens[0])) {
|
|
682
|
+
const parsedKey = parseKeyTemplate(tokens[1]);
|
|
683
|
+
if (parsedKey.ok) {
|
|
684
|
+
result = { ok: true, model: { modelType: tokens[0], kind: parsedKey.kind, keyTemplate: tokens[1], description, routeParams: parsedKey.routeParams } };
|
|
685
|
+
} else {
|
|
686
|
+
result = { ok: false, message: parsedKey.message };
|
|
687
|
+
}
|
|
688
|
+
} else {
|
|
689
|
+
result = { ok: false, message: `\`@${ROUTE_MODEL_TAG}\` model type \`${tokens[0]}\` is not a valid identifier.` };
|
|
690
|
+
}
|
|
691
|
+
return result;
|
|
692
|
+
}
|
|
693
|
+
function parseKeyTemplate(keyTemplate) {
|
|
694
|
+
const segments = keyTemplate.split("/");
|
|
695
|
+
let result;
|
|
696
|
+
if (segments.length === 1) {
|
|
697
|
+
result = parseSingleSegmentKey(segments[0], keyTemplate);
|
|
698
|
+
} else if (segments.length % 2 === 0) {
|
|
699
|
+
result = parseAlternatingKey(segments, keyTemplate);
|
|
700
|
+
} else {
|
|
701
|
+
result = { ok: false, message: `Key template \`${keyTemplate}\` must be a single placeholder or an even number of literal/placeholder segments.` };
|
|
702
|
+
}
|
|
703
|
+
return result;
|
|
704
|
+
}
|
|
705
|
+
function parseSingleSegmentKey(segment, keyTemplate) {
|
|
706
|
+
const placeholder = placeholderParam(segment);
|
|
707
|
+
let result;
|
|
708
|
+
if (placeholder === void 0) {
|
|
709
|
+
result = { ok: false, message: `Single-segment key template \`${keyTemplate}\` must be a placeholder (\`:param\` or \`${AUTH_UID_PLACEHOLDER}\`).` };
|
|
710
|
+
} else {
|
|
711
|
+
result = { ok: true, kind: "id", routeParams: placeholder.routeParam === void 0 ? [] : [placeholder.routeParam] };
|
|
712
|
+
}
|
|
713
|
+
return result;
|
|
714
|
+
}
|
|
715
|
+
function parseAlternatingKey(segments, keyTemplate) {
|
|
716
|
+
const routeParams = [];
|
|
717
|
+
let message;
|
|
718
|
+
for (const [i, segment] of segments.entries()) {
|
|
719
|
+
if (i % 2 === 0) {
|
|
720
|
+
if (!LITERAL_SEGMENT_RE.test(segment)) {
|
|
721
|
+
message = `Key template \`${keyTemplate}\` segment \`${segment}\` must be a literal collection name.`;
|
|
722
|
+
break;
|
|
723
|
+
}
|
|
724
|
+
} else {
|
|
725
|
+
const placeholder = placeholderParam(segment);
|
|
726
|
+
if (placeholder === void 0) {
|
|
727
|
+
message = `Key template \`${keyTemplate}\` segment \`${segment}\` must be a placeholder (\`:param\` or \`${AUTH_UID_PLACEHOLDER}\`).`;
|
|
728
|
+
break;
|
|
729
|
+
}
|
|
730
|
+
if (placeholder.routeParam !== void 0) {
|
|
731
|
+
routeParams.push(placeholder.routeParam);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
return message === void 0 ? { ok: true, kind: "key", routeParams } : { ok: false, message };
|
|
736
|
+
}
|
|
737
|
+
function placeholderParam(segment) {
|
|
738
|
+
let result;
|
|
739
|
+
if (segment.startsWith(":") && segment.length > 1) {
|
|
740
|
+
result = { routeParam: segment.slice(1) };
|
|
741
|
+
} else if (segment === AUTH_UID_PLACEHOLDER) {
|
|
742
|
+
result = { routeParam: void 0 };
|
|
743
|
+
} else {
|
|
744
|
+
result = void 0;
|
|
745
|
+
}
|
|
746
|
+
return result;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// packages/dbx-cli/src/lib/route/route-models-extract.ts
|
|
750
|
+
function extractComponentRouteModelTags(sourceFile, component) {
|
|
751
|
+
const cls = sourceFile.getClass(component);
|
|
752
|
+
return cls === void 0 ? [] : collectRouteModelTags2(cls.getJsDocs());
|
|
753
|
+
}
|
|
754
|
+
function collectRouteModelTags2(jsDocs) {
|
|
755
|
+
const out = [];
|
|
756
|
+
for (const jsDoc of jsDocs) {
|
|
757
|
+
for (const tag of jsDoc.getTags()) {
|
|
758
|
+
const name = tag.getTagName();
|
|
759
|
+
if (name.startsWith(ROUTE_MODEL_TAG)) {
|
|
760
|
+
out.push({ name, text: tag.getCommentText()?.trim() ?? "" });
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
return out;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// packages/dbx-cli/src/lib/route/route-manifest.ts
|
|
768
|
+
import { Project as Project2 } from "ts-morph";
|
|
769
|
+
var ROUTE_MANIFEST_VERSION = 1;
|
|
770
|
+
function buildRouteManifest(input, now = /* @__PURE__ */ new Date()) {
|
|
771
|
+
const warnings = [];
|
|
772
|
+
const tree = loadRouteTree({ sources: input.sources });
|
|
773
|
+
const project = buildSourceProject(input.sources);
|
|
774
|
+
const modelTypeSet = input.modelTypes == null ? void 0 : new Set(input.modelTypes);
|
|
775
|
+
const realNames = collectRealStateNames(tree);
|
|
776
|
+
warnDroppedFutureStates(tree, realNames, warnings);
|
|
777
|
+
const ownModelsByName = /* @__PURE__ */ new Map();
|
|
778
|
+
for (const node of tree.byName.values()) {
|
|
779
|
+
if (node.data.futureState) {
|
|
780
|
+
continue;
|
|
781
|
+
}
|
|
782
|
+
ownModelsByName.set(node.data.name, computeOwnModels({ node, project, sources: input.sources, modelTypeSet, warnings }));
|
|
783
|
+
}
|
|
784
|
+
const states = [];
|
|
785
|
+
for (const node of tree.byName.values()) {
|
|
786
|
+
if (node.data.futureState) {
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
789
|
+
states.push(buildStateEntry({ node, ownModelsByName, sources: input.sources }));
|
|
790
|
+
}
|
|
791
|
+
states.sort((a, b) => a.name.localeCompare(b.name));
|
|
792
|
+
detectMissingRouteModels(states, warnings);
|
|
793
|
+
const manifest = {
|
|
794
|
+
version: ROUTE_MANIFEST_VERSION,
|
|
795
|
+
generatedAt: now.toISOString(),
|
|
796
|
+
app: input.app.baseUrl == null ? { name: input.app.name } : { name: input.app.name, baseUrl: input.app.baseUrl },
|
|
797
|
+
states
|
|
798
|
+
};
|
|
799
|
+
return { manifest, warnings };
|
|
800
|
+
}
|
|
801
|
+
function buildSourceProject(sources) {
|
|
802
|
+
const project = new Project2({ useInMemoryFileSystem: true, skipAddingFilesFromTsConfig: true });
|
|
803
|
+
for (const source of sources) {
|
|
804
|
+
project.createSourceFile(source.name, source.text, { overwrite: true });
|
|
805
|
+
}
|
|
806
|
+
return project;
|
|
807
|
+
}
|
|
808
|
+
function collectRealStateNames(tree) {
|
|
809
|
+
const names = /* @__PURE__ */ new Set();
|
|
810
|
+
for (const node of tree.byName.values()) {
|
|
811
|
+
if (!node.data.futureState) {
|
|
812
|
+
names.add(node.data.name);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
return names;
|
|
816
|
+
}
|
|
817
|
+
function warnDroppedFutureStates(tree, realNames, warnings) {
|
|
818
|
+
for (const node of tree.byName.values()) {
|
|
819
|
+
if (!node.data.futureState) {
|
|
820
|
+
continue;
|
|
821
|
+
}
|
|
822
|
+
const prefix = node.data.name.slice(0, -3);
|
|
823
|
+
const hasRealSubtree = realNames.has(prefix) || [...realNames].some((name) => name.startsWith(`${prefix}.`));
|
|
824
|
+
if (!hasRealSubtree) {
|
|
825
|
+
warnings.push({ kind: "dropped-future-state", severity: "warning", message: `Future state \`${node.data.name}\` was dropped and has no real subtree under \`${prefix}\`; its page will not match any URL.`, stateName: node.data.name });
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
function computeOwnModels(input) {
|
|
830
|
+
const { node, project, sources, modelTypeSet, warnings } = input;
|
|
831
|
+
const componentModels = parseComponentModels({ node, project, sources, warnings });
|
|
832
|
+
const stateModels = parseStateModels({ node, warnings });
|
|
833
|
+
const overriddenTypes = new Set(stateModels.map((m) => m.modelType));
|
|
834
|
+
const merged = [...componentModels.filter((m) => !overriddenTypes.has(m.modelType)), ...stateModels];
|
|
835
|
+
const urlParamKeys = new Set(extractUrlParamKeys(node.fullUrl));
|
|
836
|
+
const deduped = dedupeModels({ models: merged, stateName: node.data.name, warnings });
|
|
837
|
+
return deduped.map((m) => finalizeModel({ model: m, stateName: node.data.name, urlParamKeys, modelTypeSet, warnings }));
|
|
838
|
+
}
|
|
839
|
+
function parseComponentModels(input) {
|
|
840
|
+
const { node, project, sources, warnings } = input;
|
|
841
|
+
const component = node.data.component;
|
|
842
|
+
if (component == null) {
|
|
843
|
+
return [];
|
|
844
|
+
}
|
|
845
|
+
const resolved = resolveComponentSourceFromSources({ routerFile: node.data.file, component, sources });
|
|
846
|
+
const componentFile = resolved?.path;
|
|
847
|
+
if (componentFile == null) {
|
|
848
|
+
return [];
|
|
849
|
+
}
|
|
850
|
+
const sourceFile = project.getSourceFile(componentFile);
|
|
851
|
+
if (sourceFile == null) {
|
|
852
|
+
return [];
|
|
853
|
+
}
|
|
854
|
+
return parseTags({ tags: extractComponentRouteModelTags(sourceFile, component), stateName: node.data.name, warnings });
|
|
855
|
+
}
|
|
856
|
+
function parseStateModels(input) {
|
|
857
|
+
const tags = input.node.data.jsDocTags ?? [];
|
|
858
|
+
return parseTags({ tags, stateName: input.node.data.name, warnings: input.warnings });
|
|
859
|
+
}
|
|
860
|
+
function parseTags(input) {
|
|
861
|
+
const out = [];
|
|
862
|
+
for (const tag of input.tags) {
|
|
863
|
+
const parsed = parseRouteModelTag(tag);
|
|
864
|
+
if (parsed.ok) {
|
|
865
|
+
out.push(parsed.model);
|
|
866
|
+
} else {
|
|
867
|
+
input.warnings.push({ kind: "malformed-tag", severity: "error", message: parsed.message, stateName: input.stateName });
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
return out;
|
|
871
|
+
}
|
|
872
|
+
function dedupeModels(input) {
|
|
873
|
+
const seen = /* @__PURE__ */ new Set();
|
|
874
|
+
const out = [];
|
|
875
|
+
for (const model of input.models) {
|
|
876
|
+
const key = modelDedupeKey(model.modelType, model.keyTemplate);
|
|
877
|
+
if (seen.has(key)) {
|
|
878
|
+
const keyPart = model.keyTemplate == null ? "" : ` ${model.keyTemplate}`;
|
|
879
|
+
input.warnings.push({ kind: "duplicate-route-model", severity: "warning", message: `State \`${input.stateName}\` declares \`${model.modelType}\`${keyPart} more than once; keeping the first.`, stateName: input.stateName, modelType: model.modelType });
|
|
880
|
+
continue;
|
|
881
|
+
}
|
|
882
|
+
seen.add(key);
|
|
883
|
+
out.push(model);
|
|
884
|
+
}
|
|
885
|
+
return out;
|
|
886
|
+
}
|
|
887
|
+
function finalizeModel(input) {
|
|
888
|
+
const { model, stateName, urlParamKeys, modelTypeSet, warnings } = input;
|
|
889
|
+
for (const routeParam of model.routeParams) {
|
|
890
|
+
if (!urlParamKeys.has(routeParam)) {
|
|
891
|
+
warnings.push({ kind: "unknown-route-param", severity: "warning", message: `State \`${stateName}\` model \`${model.modelType}\` references route param \`:${routeParam}\` not present in the composed URL.`, stateName, modelType: model.modelType });
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
if (modelTypeSet != null && !modelTypeSet.has(model.modelType)) {
|
|
895
|
+
warnings.push({ kind: "unknown-model-type", severity: "warning", message: `State \`${stateName}\` references unknown model type \`${model.modelType}\`.`, stateName, modelType: model.modelType });
|
|
896
|
+
}
|
|
897
|
+
return {
|
|
898
|
+
modelType: model.modelType,
|
|
899
|
+
kind: model.kind,
|
|
900
|
+
...model.keyTemplate == null ? {} : { keyTemplate: model.keyTemplate },
|
|
901
|
+
...model.description == null ? {} : { description: model.description }
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
function modelDedupeKey(modelType, keyTemplate) {
|
|
905
|
+
return `${modelType}#${keyTemplate ?? ""}`;
|
|
906
|
+
}
|
|
907
|
+
function buildStateEntry(input) {
|
|
908
|
+
const { node, ownModelsByName, sources } = input;
|
|
909
|
+
const models = flattenInheritedModels(node, ownModelsByName);
|
|
910
|
+
const componentFile = resolveComponentFilePath(node, sources);
|
|
911
|
+
const parentName = node.parent != null && !node.parent.data.futureState ? node.parent.data.name : void 0;
|
|
912
|
+
const urlParamKeys = extractUrlParamKeys(node.fullUrl);
|
|
913
|
+
return {
|
|
914
|
+
name: node.data.name,
|
|
915
|
+
...node.data.url == null ? {} : { url: node.data.url },
|
|
916
|
+
...node.fullUrl == null ? {} : { fullUrl: node.fullUrl },
|
|
917
|
+
...parentName == null ? {} : { parentName },
|
|
918
|
+
paramKeys: node.data.paramKeys,
|
|
919
|
+
urlParamKeys,
|
|
920
|
+
...node.data.component == null ? {} : { component: node.data.component },
|
|
921
|
+
...componentFile == null ? {} : { componentFile },
|
|
922
|
+
...node.data.abstract ? { abstract: true } : {},
|
|
923
|
+
...node.data.redirectTo == null ? {} : { redirectTo: node.data.redirectTo },
|
|
924
|
+
models
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
function flattenInheritedModels(node, ownModelsByName) {
|
|
928
|
+
const out = [...ownModelsByName.get(node.data.name) ?? []];
|
|
929
|
+
const seen = new Set(out.map((m) => modelDedupeKey(m.modelType, m.keyTemplate)));
|
|
930
|
+
let cursor = node.parent;
|
|
931
|
+
while (cursor) {
|
|
932
|
+
if (!cursor.data.futureState) {
|
|
933
|
+
for (const model of ownModelsByName.get(cursor.data.name) ?? []) {
|
|
934
|
+
const key = modelDedupeKey(model.modelType, model.keyTemplate);
|
|
935
|
+
if (!seen.has(key)) {
|
|
936
|
+
seen.add(key);
|
|
937
|
+
out.push({ ...model, from: cursor.data.name });
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
cursor = cursor.parent;
|
|
942
|
+
}
|
|
943
|
+
return out;
|
|
944
|
+
}
|
|
945
|
+
function resolveComponentFilePath(node, sources) {
|
|
946
|
+
const component = node.data.component;
|
|
947
|
+
if (component == null) {
|
|
948
|
+
return void 0;
|
|
949
|
+
}
|
|
950
|
+
const resolved = resolveComponentSourceFromSources({ routerFile: node.data.file, component, sources });
|
|
951
|
+
if (resolved == null) {
|
|
952
|
+
return void 0;
|
|
953
|
+
}
|
|
954
|
+
return sources.some((s) => s.name === resolved.path) ? resolved.path : void 0;
|
|
955
|
+
}
|
|
956
|
+
var ID_LIKE_ROUTE_PARAM_RE = /(?:id|uid)$/iu;
|
|
957
|
+
function isIdLikeRouteParam(name) {
|
|
958
|
+
return ID_LIKE_ROUTE_PARAM_RE.test(name);
|
|
959
|
+
}
|
|
960
|
+
function routeParamsFromKeyTemplate(keyTemplate) {
|
|
961
|
+
if (keyTemplate == null) {
|
|
962
|
+
return [];
|
|
963
|
+
}
|
|
964
|
+
return keyTemplate.split("/").filter((segment) => segment.startsWith(":") && segment.length > 1).map((segment) => segment.slice(1));
|
|
965
|
+
}
|
|
966
|
+
function detectMissingRouteModels(states, warnings) {
|
|
967
|
+
for (const state of states) {
|
|
968
|
+
if (state.abstract) {
|
|
969
|
+
continue;
|
|
970
|
+
}
|
|
971
|
+
const covered = /* @__PURE__ */ new Set();
|
|
972
|
+
for (const model of state.models) {
|
|
973
|
+
for (const param of routeParamsFromKeyTemplate(model.keyTemplate)) {
|
|
974
|
+
covered.add(param);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
for (const param of state.urlParamKeys) {
|
|
978
|
+
if (isIdLikeRouteParam(param) && !covered.has(param)) {
|
|
979
|
+
warnings.push({ kind: "missing-route-model", severity: "warning", message: `State \`${state.name}\` has id-like route param \`:${param}\` but no \`@dbxRouteModel\` binding covers it; annotate the component class or state.`, stateName: state.name, param });
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// packages/dbx-cli/generate-route-manifest/src/generate-route-manifest/render.ts
|
|
986
|
+
function renderRouteManifest(input, now = /* @__PURE__ */ new Date()) {
|
|
987
|
+
return buildRouteManifest({ app: input.app, sources: input.sources, ...input.modelTypes == null ? {} : { modelTypes: input.modelTypes } }, now);
|
|
988
|
+
}
|
|
989
|
+
function formatRouteManifestWarning(warning) {
|
|
990
|
+
return `[generate-route-manifest] ${warning.severity}: ${warning.kind}: ${warning.message}`;
|
|
991
|
+
}
|
|
992
|
+
function countRouteManifestGenerationErrors(input) {
|
|
993
|
+
return input.warnings.filter((warning) => warning.severity === "error" || input.strict && warning.severity === "warning").length;
|
|
994
|
+
}
|
|
995
|
+
function extractModelTypesFromModelsInput(parsed) {
|
|
996
|
+
const models = parsed?.models;
|
|
997
|
+
const out = [];
|
|
998
|
+
if (Array.isArray(models)) {
|
|
999
|
+
for (const model of models) {
|
|
1000
|
+
const modelType = model?.modelType;
|
|
1001
|
+
if (typeof modelType === "string" && modelType.length > 0) {
|
|
1002
|
+
out.push(modelType);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
return out;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
// packages/dbx-cli/generate-route-manifest/src/generate-route-manifest/main.ts
|
|
1010
|
+
var WORKSPACE_ROOT = process.cwd();
|
|
1011
|
+
async function main() {
|
|
1012
|
+
const flags = parseFlags(process.argv.slice(2));
|
|
1013
|
+
if (flags.src.length === 0 || flags.app == null || flags.output == null) {
|
|
1014
|
+
printUsageAndExit();
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
const outputPath = resolveWorkspacePath(flags.output);
|
|
1018
|
+
const sources = await loadSources(flags.src);
|
|
1019
|
+
if (sources.length === 0) {
|
|
1020
|
+
console.error(`generate-route-manifest: no source files matched ${describePatterns(flags.src)}.`);
|
|
1021
|
+
process.exit(1);
|
|
1022
|
+
}
|
|
1023
|
+
const modelTypes = await maybeLoadModelTypes(flags.modelsInput);
|
|
1024
|
+
const app = flags.baseUrl == null ? { name: flags.app } : { name: flags.app, baseUrl: flags.baseUrl };
|
|
1025
|
+
const { manifest, warnings } = renderRouteManifest({ app, sources, ...modelTypes == null ? {} : { modelTypes } });
|
|
1026
|
+
for (const warning of warnings) {
|
|
1027
|
+
console.error(formatRouteManifestWarning(warning));
|
|
1028
|
+
}
|
|
1029
|
+
if (manifest.states.length === 0) {
|
|
1030
|
+
console.error(`generate-route-manifest: 0 states extracted from ${describePatterns(flags.src)}; not writing ${relative(WORKSPACE_ROOT, outputPath)}.`);
|
|
1031
|
+
process.exit(1);
|
|
1032
|
+
}
|
|
1033
|
+
const errorCount = countRouteManifestGenerationErrors({ warnings, strict: flags.strict });
|
|
1034
|
+
if (errorCount > 0) {
|
|
1035
|
+
console.error(`generate-route-manifest: ${errorCount} error-severity issue(s)${flags.strict ? " (--strict)" : ""}; not writing ${relative(WORKSPACE_ROOT, outputPath)}.`);
|
|
1036
|
+
process.exit(1);
|
|
1037
|
+
}
|
|
1038
|
+
const serialized = `${JSON.stringify(manifest, null, 2)}
|
|
1039
|
+
`;
|
|
1040
|
+
await ensureOutputDir(dirname2(outputPath));
|
|
1041
|
+
const tmpPath = `${outputPath}.tmp`;
|
|
1042
|
+
await writeFile(tmpPath, serialized);
|
|
1043
|
+
await rename(tmpPath, outputPath);
|
|
1044
|
+
const modelCount = manifest.states.reduce((sum, state) => sum + state.models.length, 0);
|
|
1045
|
+
console.log(`[wrote] ${relative(WORKSPACE_ROOT, outputPath)} \u2014 ${manifest.states.length} states, ${modelCount} model bindings, ${warnings.length} warning(s)`);
|
|
1046
|
+
}
|
|
1047
|
+
async function loadSources(patterns) {
|
|
1048
|
+
const byName = /* @__PURE__ */ new Map();
|
|
1049
|
+
for (const pattern of patterns) {
|
|
1050
|
+
for await (const match of fsGlob(pattern, { cwd: WORKSPACE_ROOT })) {
|
|
1051
|
+
const name = normalizeName(match);
|
|
1052
|
+
if (!byName.has(name)) {
|
|
1053
|
+
const text = await readFile(resolve(WORKSPACE_ROOT, match), "utf8");
|
|
1054
|
+
byName.set(name, { name, text });
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
return Array.from(byName.values());
|
|
1059
|
+
}
|
|
1060
|
+
async function maybeLoadModelTypes(modelsInput) {
|
|
1061
|
+
if (modelsInput == null) {
|
|
1062
|
+
return void 0;
|
|
1063
|
+
}
|
|
1064
|
+
const path = resolveWorkspacePath(modelsInput);
|
|
1065
|
+
if (!existsSync(path)) {
|
|
1066
|
+
console.error(`[generate-route-manifest] --models-input not found: ${relative(WORKSPACE_ROOT, path)}; skipping unknown-model-type validation.`);
|
|
1067
|
+
return void 0;
|
|
1068
|
+
}
|
|
1069
|
+
const parsed = JSON.parse(await readFile(path, "utf8"));
|
|
1070
|
+
return extractModelTypesFromModelsInput(parsed);
|
|
1071
|
+
}
|
|
1072
|
+
function normalizeName(match) {
|
|
1073
|
+
return match.split("\\").join("/");
|
|
1074
|
+
}
|
|
1075
|
+
function describePatterns(patterns) {
|
|
1076
|
+
return patterns.map((pattern) => `\`${pattern}\``).join(", ");
|
|
1077
|
+
}
|
|
1078
|
+
async function ensureOutputDir(outputDir) {
|
|
1079
|
+
if (!existsSync(outputDir)) {
|
|
1080
|
+
await mkdir(outputDir, { recursive: true });
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
function resolveWorkspacePath(value) {
|
|
1084
|
+
return isAbsolute(value) ? value : resolve(WORKSPACE_ROOT, value);
|
|
1085
|
+
}
|
|
1086
|
+
function parseFlags(argv) {
|
|
1087
|
+
const src = [];
|
|
1088
|
+
let app;
|
|
1089
|
+
let baseUrl;
|
|
1090
|
+
let output;
|
|
1091
|
+
let modelsInput;
|
|
1092
|
+
let strict = false;
|
|
1093
|
+
for (const arg of argv) {
|
|
1094
|
+
if (arg.startsWith("--src=")) {
|
|
1095
|
+
src.push(arg.slice("--src=".length));
|
|
1096
|
+
} else if (arg.startsWith("--app=")) {
|
|
1097
|
+
app = arg.slice("--app=".length);
|
|
1098
|
+
} else if (arg.startsWith("--base-url=")) {
|
|
1099
|
+
baseUrl = arg.slice("--base-url=".length);
|
|
1100
|
+
} else if (arg.startsWith("--output=")) {
|
|
1101
|
+
output = arg.slice("--output=".length);
|
|
1102
|
+
} else if (arg.startsWith("--models-input=")) {
|
|
1103
|
+
modelsInput = arg.slice("--models-input=".length);
|
|
1104
|
+
} else if (arg === "--strict") {
|
|
1105
|
+
strict = true;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
return { src, app, baseUrl, output, modelsInput, strict };
|
|
1109
|
+
}
|
|
1110
|
+
function printUsageAndExit() {
|
|
1111
|
+
console.error(String.raw`generate-route-manifest
|
|
1112
|
+
|
|
1113
|
+
Usage:
|
|
1114
|
+
node dist/packages/dbx-cli/generate-route-manifest/main.js \
|
|
1115
|
+
--src='apps/demo/src/**/*.ts' \
|
|
1116
|
+
--app=demo \
|
|
1117
|
+
--output=dist/apps/demo-api/route.manifest.json
|
|
1118
|
+
|
|
1119
|
+
Required flags:
|
|
1120
|
+
--src=<glob> Source glob (repeatable), e.g. 'apps/demo/src/**/*.ts'.
|
|
1121
|
+
--app=<slug> App name stamped onto the manifest.
|
|
1122
|
+
--output=<path> Path to the route manifest JSON to write (workspace-relative ok).
|
|
1123
|
+
|
|
1124
|
+
Optional:
|
|
1125
|
+
--base-url=<url> Public base URL stamped onto the manifest.
|
|
1126
|
+
--models-input=<path> MCP manifest JSON whose models[].modelType seed the
|
|
1127
|
+
unknown-model-type validation.
|
|
1128
|
+
--strict Promote all warnings to errors for the exit decision
|
|
1129
|
+
(any finding then fails generation).`);
|
|
1130
|
+
process.exit(1);
|
|
1131
|
+
}
|
|
1132
|
+
try {
|
|
1133
|
+
await main();
|
|
1134
|
+
} catch (e) {
|
|
1135
|
+
console.error(e);
|
|
1136
|
+
process.exit(1);
|
|
1137
|
+
}
|