@reforgium/presentia 2.0.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +169 -144
- package/README.md +1 -1
- package/bin/presentia-gen-namespaces.mjs +1248 -1248
- package/fesm2022/reforgium-presentia.mjs +571 -525
- package/package.json +2 -2
- package/types/reforgium-presentia.d.ts +18 -9
|
@@ -1,1248 +1,1248 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import fs from 'node:fs';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import { pathToFileURL } from 'node:url';
|
|
5
|
-
|
|
6
|
-
let projectRoot = '';
|
|
7
|
-
let localeValidator = null;
|
|
8
|
-
let generatorReport = null;
|
|
9
|
-
|
|
10
|
-
const getArg = (args, name, fallback) => {
|
|
11
|
-
const idx = args.indexOf(name);
|
|
12
|
-
|
|
13
|
-
return idx >= 0 && args[idx + 1] ? args[idx + 1] : fallback;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
const KEY_RE = /^[a-z0-9_-]+\.[a-z0-9_.-]+$/i;
|
|
17
|
-
const visitedFiles = new Set();
|
|
18
|
-
const visitedRouteExports = new Set();
|
|
19
|
-
|
|
20
|
-
function fail(message) {
|
|
21
|
-
throw new Error(`[presentia-gen-namespaces] ${message}`);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function createReport() {
|
|
25
|
-
return {
|
|
26
|
-
issues: [],
|
|
27
|
-
addIssue(issue) {
|
|
28
|
-
this.issues.push(issue);
|
|
29
|
-
},
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function readText(filePath) {
|
|
34
|
-
return fs.readFileSync(filePath, 'utf8');
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function tryReadJson(filePath) {
|
|
38
|
-
if (!fs.existsSync(filePath)) {
|
|
39
|
-
return null;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return JSON.parse(readText(filePath));
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function collectJsonFiles(dir) {
|
|
46
|
-
const out = [];
|
|
47
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
48
|
-
|
|
49
|
-
for (const entry of entries) {
|
|
50
|
-
const full = path.join(dir, entry.name);
|
|
51
|
-
|
|
52
|
-
if (entry.isDirectory()) {
|
|
53
|
-
out.push(...collectJsonFiles(full));
|
|
54
|
-
continue;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (entry.isFile() && entry.name.endsWith('.json')) {
|
|
58
|
-
out.push(full);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return out;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function namespaceFromLocaleFile(filePath) {
|
|
66
|
-
const base = path.basename(filePath);
|
|
67
|
-
const parts = base.split('.');
|
|
68
|
-
|
|
69
|
-
if (parts.length >= 3) {
|
|
70
|
-
return parts.slice(0, -2).join('.');
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return parts.slice(0, -1).join('.');
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function flattenLocaleKeys(value, prefix = '') {
|
|
77
|
-
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
78
|
-
return [];
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const out = [];
|
|
82
|
-
|
|
83
|
-
for (const [key, nested] of Object.entries(value)) {
|
|
84
|
-
const next = prefix ? `${prefix}.${key}` : key;
|
|
85
|
-
|
|
86
|
-
if (nested && typeof nested === 'object' && !Array.isArray(nested)) {
|
|
87
|
-
out.push(...flattenLocaleKeys(nested, next));
|
|
88
|
-
} else {
|
|
89
|
-
out.push(next);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return out;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function createLocaleValidator(localesPath) {
|
|
97
|
-
if (!localesPath) {
|
|
98
|
-
return null;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (!fs.existsSync(localesPath)) {
|
|
102
|
-
fail(`locales directory not found: ${localesPath}`);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const namespaceSet = new Set();
|
|
106
|
-
const keySet = new Set();
|
|
107
|
-
|
|
108
|
-
for (const file of collectJsonFiles(localesPath)) {
|
|
109
|
-
const ns = namespaceFromLocaleFile(file);
|
|
110
|
-
|
|
111
|
-
if (!ns) {
|
|
112
|
-
continue;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
namespaceSet.add(ns);
|
|
116
|
-
|
|
117
|
-
try {
|
|
118
|
-
const json = JSON.parse(readText(file));
|
|
119
|
-
|
|
120
|
-
for (const key of flattenLocaleKeys(json)) {
|
|
121
|
-
keySet.add(`${ns}.${key}`);
|
|
122
|
-
}
|
|
123
|
-
} catch {
|
|
124
|
-
// Ignore malformed locale files in generator mode.
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return {
|
|
129
|
-
hasNamespace(ns) {
|
|
130
|
-
return namespaceSet.has(ns);
|
|
131
|
-
},
|
|
132
|
-
hasKey(key) {
|
|
133
|
-
return keySet.has(key);
|
|
134
|
-
},
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function loadTsconfig(filePath, seen = new Set()) {
|
|
139
|
-
const resolved = path.resolve(filePath);
|
|
140
|
-
|
|
141
|
-
if (!fs.existsSync(resolved) || seen.has(resolved)) {
|
|
142
|
-
return { compilerOptions: {}, _dir: path.dirname(resolved) };
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
seen.add(resolved);
|
|
146
|
-
const json = JSON.parse(readText(resolved));
|
|
147
|
-
const base = json.extends
|
|
148
|
-
? loadTsconfig(resolveExtendsPath(path.dirname(resolved), json.extends), seen)
|
|
149
|
-
: { compilerOptions: {}, _dir: path.dirname(resolved) };
|
|
150
|
-
const compilerOptions = {
|
|
151
|
-
...(base.compilerOptions ?? {}),
|
|
152
|
-
...(json.compilerOptions ?? {}),
|
|
153
|
-
paths: {
|
|
154
|
-
...((base.compilerOptions && base.compilerOptions.paths) || {}),
|
|
155
|
-
...((json.compilerOptions && json.compilerOptions.paths) || {}),
|
|
156
|
-
},
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
return { ...base, ...json, compilerOptions, _dir: path.dirname(resolved) };
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function resolveExtendsPath(baseDir, value) {
|
|
163
|
-
if (value.startsWith('.')) {
|
|
164
|
-
return withJsonSuffix(path.resolve(baseDir, value));
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
return withJsonSuffix(path.resolve(baseDir, value));
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function withJsonSuffix(filePath) {
|
|
171
|
-
return filePath.endsWith('.json') ? filePath : `${filePath}.json`;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function stripComments(source) {
|
|
175
|
-
let out = '';
|
|
176
|
-
let quote = null;
|
|
177
|
-
let escape = false;
|
|
178
|
-
|
|
179
|
-
for (let i = 0; i < source.length; i++) {
|
|
180
|
-
const ch = source[i];
|
|
181
|
-
const next = source[i + 1];
|
|
182
|
-
|
|
183
|
-
if (quote) {
|
|
184
|
-
out += ch;
|
|
185
|
-
|
|
186
|
-
if (escape) {
|
|
187
|
-
escape = false;
|
|
188
|
-
} else if (ch === '\\') {
|
|
189
|
-
escape = true;
|
|
190
|
-
} else if (ch === quote) {
|
|
191
|
-
quote = null;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
continue;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
if (ch === '"' || ch === "'" || ch === '`') {
|
|
198
|
-
quote = ch;
|
|
199
|
-
out += ch;
|
|
200
|
-
continue;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
if (ch === '/' && next === '/') {
|
|
204
|
-
while (i < source.length && source[i] !== '\n') {
|
|
205
|
-
i++;
|
|
206
|
-
}
|
|
207
|
-
out += '\n';
|
|
208
|
-
continue;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if (ch === '/' && next === '*') {
|
|
212
|
-
i += 2;
|
|
213
|
-
while (i < source.length && !(source[i] === '*' && source[i + 1] === '/')) {
|
|
214
|
-
i++;
|
|
215
|
-
}
|
|
216
|
-
i++;
|
|
217
|
-
continue;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
out += ch;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
return out;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function findMatching(source, start, openChar, closeChar) {
|
|
227
|
-
let depth = 0;
|
|
228
|
-
let quote = null;
|
|
229
|
-
let escape = false;
|
|
230
|
-
|
|
231
|
-
for (let i = start; i < source.length; i++) {
|
|
232
|
-
const ch = source[i];
|
|
233
|
-
const next = source[i + 1];
|
|
234
|
-
|
|
235
|
-
if (quote) {
|
|
236
|
-
if (escape) {
|
|
237
|
-
escape = false;
|
|
238
|
-
} else if (ch === '\\') {
|
|
239
|
-
escape = true;
|
|
240
|
-
} else if (ch === quote) {
|
|
241
|
-
quote = null;
|
|
242
|
-
}
|
|
243
|
-
continue;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
if (ch === '"' || ch === "'" || ch === '`') {
|
|
247
|
-
quote = ch;
|
|
248
|
-
continue;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
if (ch === '/' && next === '/') {
|
|
252
|
-
while (i < source.length && source[i] !== '\n') {
|
|
253
|
-
i++;
|
|
254
|
-
}
|
|
255
|
-
continue;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
if (ch === '/' && next === '*') {
|
|
259
|
-
i += 2;
|
|
260
|
-
while (i < source.length && !(source[i] === '*' && source[i + 1] === '/')) {
|
|
261
|
-
i++;
|
|
262
|
-
}
|
|
263
|
-
i++;
|
|
264
|
-
continue;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
if (ch === openChar) {
|
|
268
|
-
depth++;
|
|
269
|
-
continue;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
if (ch === closeChar) {
|
|
273
|
-
depth--;
|
|
274
|
-
if (depth === 0) {
|
|
275
|
-
return i;
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
return -1;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
function extractArrayLiteral(source, anchorPattern) {
|
|
284
|
-
const match = anchorPattern.exec(source);
|
|
285
|
-
|
|
286
|
-
if (!match) {
|
|
287
|
-
return null;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
const preferredStart = match.index + match[0].length - 1;
|
|
291
|
-
const start = source[preferredStart] === '[' ? preferredStart : source.indexOf('[', preferredStart);
|
|
292
|
-
const end = findMatching(source, start, '[', ']');
|
|
293
|
-
|
|
294
|
-
if (start < 0 || end < 0) {
|
|
295
|
-
return null;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
return source.slice(start, end + 1);
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
function splitTopLevelItems(arrayLiteral) {
|
|
302
|
-
const inner = arrayLiteral.trim().replace(/^\[/, '').replace(/\]$/, '');
|
|
303
|
-
const items = [];
|
|
304
|
-
let token = '';
|
|
305
|
-
let depthBrace = 0;
|
|
306
|
-
let depthBracket = 0;
|
|
307
|
-
let depthParen = 0;
|
|
308
|
-
let quote = null;
|
|
309
|
-
let escape = false;
|
|
310
|
-
|
|
311
|
-
for (let i = 0; i < inner.length; i++) {
|
|
312
|
-
const ch = inner[i];
|
|
313
|
-
const next = inner[i + 1];
|
|
314
|
-
|
|
315
|
-
token += ch;
|
|
316
|
-
|
|
317
|
-
if (quote) {
|
|
318
|
-
if (escape) {
|
|
319
|
-
escape = false;
|
|
320
|
-
} else if (ch === '\\') {
|
|
321
|
-
escape = true;
|
|
322
|
-
} else if (ch === quote) {
|
|
323
|
-
quote = null;
|
|
324
|
-
}
|
|
325
|
-
continue;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
if (ch === '"' || ch === "'" || ch === '`') {
|
|
329
|
-
quote = ch;
|
|
330
|
-
continue;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
if (ch === '/' && next === '/') {
|
|
334
|
-
while (i < inner.length && inner[i] !== '\n') {
|
|
335
|
-
token += inner[++i] ?? '';
|
|
336
|
-
}
|
|
337
|
-
continue;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
if (ch === '/' && next === '*') {
|
|
341
|
-
token += inner[++i] ?? '';
|
|
342
|
-
while (i < inner.length && !(inner[i] === '*' && inner[i + 1] === '/')) {
|
|
343
|
-
token += inner[++i] ?? '';
|
|
344
|
-
}
|
|
345
|
-
token += inner[++i] ?? '';
|
|
346
|
-
continue;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
if (ch === '{') depthBrace++;
|
|
350
|
-
if (ch === '}') depthBrace--;
|
|
351
|
-
if (ch === '[') depthBracket++;
|
|
352
|
-
if (ch === ']') depthBracket--;
|
|
353
|
-
if (ch === '(') depthParen++;
|
|
354
|
-
if (ch === ')') depthParen--;
|
|
355
|
-
|
|
356
|
-
if (ch === ',' && depthBrace === 0 && depthBracket === 0 && depthParen === 0) {
|
|
357
|
-
const trimmed = token.slice(0, -1).trim();
|
|
358
|
-
if (trimmed) {
|
|
359
|
-
items.push(trimmed);
|
|
360
|
-
}
|
|
361
|
-
token = '';
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
const trimmed = token.trim();
|
|
366
|
-
if (trimmed) {
|
|
367
|
-
items.push(trimmed);
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
return items;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
function extractObjectLiterals(arrayLiteral) {
|
|
374
|
-
return splitTopLevelItems(arrayLiteral).filter((item) => item.startsWith('{'));
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
function parseImports(source) {
|
|
378
|
-
const imports = new Map();
|
|
379
|
-
const re = /import\s+\{([^}]+)\}\s+from\s+['"]([^'"]+)['"]/g;
|
|
380
|
-
let match;
|
|
381
|
-
|
|
382
|
-
while ((match = re.exec(source))) {
|
|
383
|
-
const specifier = match[2];
|
|
384
|
-
const names = match[1]
|
|
385
|
-
.split(',')
|
|
386
|
-
.map((part) => part.trim())
|
|
387
|
-
.filter(Boolean);
|
|
388
|
-
|
|
389
|
-
for (const name of names) {
|
|
390
|
-
const [imported, local] = name.split(/\s+as\s+/);
|
|
391
|
-
|
|
392
|
-
imports.set((local ?? imported).trim(), {
|
|
393
|
-
imported: imported.trim(),
|
|
394
|
-
specifier,
|
|
395
|
-
});
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
return imports;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
function resolveFile(basePath) {
|
|
403
|
-
const candidates = [
|
|
404
|
-
basePath,
|
|
405
|
-
`${basePath}.ts`,
|
|
406
|
-
`${basePath}.tsx`,
|
|
407
|
-
`${basePath}.js`,
|
|
408
|
-
`${basePath}.mjs`,
|
|
409
|
-
path.join(basePath, 'index.ts'),
|
|
410
|
-
path.join(basePath, 'index.tsx'),
|
|
411
|
-
path.join(basePath, 'index.js'),
|
|
412
|
-
path.join(basePath, 'index.mjs'),
|
|
413
|
-
];
|
|
414
|
-
|
|
415
|
-
return candidates.find((candidate) => fs.existsSync(candidate) && fs.statSync(candidate).isFile()) ?? null;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
function createPathResolver(routesFilePath, tsconfig) {
|
|
419
|
-
const importerDir = path.dirname(routesFilePath);
|
|
420
|
-
const configBase = path.resolve(tsconfig._dir, tsconfig.compilerOptions?.baseUrl ?? '.');
|
|
421
|
-
const pathEntries = Object.entries(tsconfig.compilerOptions?.paths ?? {});
|
|
422
|
-
|
|
423
|
-
return (specifier, fromFile) => {
|
|
424
|
-
const fromDir = fromFile ? path.dirname(fromFile) : importerDir;
|
|
425
|
-
|
|
426
|
-
if (specifier.startsWith('.')) {
|
|
427
|
-
return resolveFile(path.resolve(fromDir, specifier));
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
for (const [pattern, targets] of pathEntries) {
|
|
431
|
-
const matches = matchPathPattern(specifier, pattern);
|
|
432
|
-
|
|
433
|
-
if (!matches) {
|
|
434
|
-
continue;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
for (const target of targets) {
|
|
438
|
-
const candidate = target.replace('*', matches);
|
|
439
|
-
const resolved = resolveFile(path.resolve(configBase, candidate));
|
|
440
|
-
|
|
441
|
-
if (resolved) {
|
|
442
|
-
return resolved;
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
return resolveFile(path.resolve(configBase, specifier));
|
|
448
|
-
};
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
function matchPathPattern(specifier, pattern) {
|
|
452
|
-
if (!pattern.includes('*')) {
|
|
453
|
-
return specifier === pattern ? '' : null;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
const [prefix, suffix] = pattern.split('*');
|
|
457
|
-
|
|
458
|
-
if (!specifier.startsWith(prefix) || !specifier.endsWith(suffix)) {
|
|
459
|
-
return null;
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
return specifier.slice(prefix.length, specifier.length - suffix.length);
|
|
463
|
-
}
|
|
464
|
-
|
|
465
|
-
function parseConstObjects(filePath, resolver, cache = new Map()) {
|
|
466
|
-
if (cache.has(filePath)) {
|
|
467
|
-
return cache.get(filePath);
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
const source = stripComments(readText(filePath));
|
|
471
|
-
const objects = new Map();
|
|
472
|
-
const exportedRe = /export\s+const\s+(\w+)\s*=\s*\{/g;
|
|
473
|
-
let match;
|
|
474
|
-
|
|
475
|
-
while ((match = exportedRe.exec(source))) {
|
|
476
|
-
const name = match[1];
|
|
477
|
-
const start = source.indexOf('{', match.index);
|
|
478
|
-
const end = findMatching(source, start, '{', '}');
|
|
479
|
-
|
|
480
|
-
if (start < 0 || end < 0) {
|
|
481
|
-
continue;
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
const parsed = parseSimpleValueExpression(source.slice(start, end + 1));
|
|
485
|
-
|
|
486
|
-
if (parsed && typeof parsed === 'object') {
|
|
487
|
-
objects.set(name, parsed);
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
const exportedArrayRe = /export\s+const\s+(\w+)\s*=\s*\[/g;
|
|
492
|
-
|
|
493
|
-
while ((match = exportedArrayRe.exec(source))) {
|
|
494
|
-
const name = match[1];
|
|
495
|
-
const start = source.indexOf('[', match.index);
|
|
496
|
-
const end = findMatching(source, start, '[', ']');
|
|
497
|
-
|
|
498
|
-
if (start < 0 || end < 0) {
|
|
499
|
-
continue;
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
const parsed = parseSimpleValueExpression(source.slice(start, end + 1));
|
|
503
|
-
|
|
504
|
-
if (Array.isArray(parsed)) {
|
|
505
|
-
objects.set(name, parsed);
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
const imports = parseImports(source);
|
|
510
|
-
|
|
511
|
-
for (const [localName, imported] of imports) {
|
|
512
|
-
const resolved = resolver(imported.specifier, filePath);
|
|
513
|
-
|
|
514
|
-
if (!resolved) {
|
|
515
|
-
continue;
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
const nestedObjects = parseConstObjects(resolved, resolver, cache);
|
|
519
|
-
const importedValue = nestedObjects.get(imported.imported);
|
|
520
|
-
|
|
521
|
-
if (importedValue) {
|
|
522
|
-
objects.set(localName, importedValue);
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
cache.set(filePath, objects);
|
|
527
|
-
|
|
528
|
-
return objects;
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
function parseSimpleObjectLiteral(source) {
|
|
532
|
-
const object = {};
|
|
533
|
-
const inner = source.trim().replace(/^\{/, '').replace(/\}$/, '');
|
|
534
|
-
|
|
535
|
-
for (const entry of splitTopLevelItems(`[${inner}]`)) {
|
|
536
|
-
const idx = entry.indexOf(':');
|
|
537
|
-
|
|
538
|
-
if (idx < 0) {
|
|
539
|
-
continue;
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
const key = entry.slice(0, idx).trim().replace(/^['"]|['"]$/g, '');
|
|
543
|
-
const valueExpr = entry.slice(idx + 1).trim();
|
|
544
|
-
const value = parseSimpleValueExpression(valueExpr);
|
|
545
|
-
|
|
546
|
-
if (key && value !== null) {
|
|
547
|
-
object[key] = value;
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
return object;
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
function parseSimpleValueExpression(value) {
|
|
555
|
-
const trimmed = value.trim();
|
|
556
|
-
const stringLiteral = parseStringLiteral(trimmed);
|
|
557
|
-
|
|
558
|
-
if (stringLiteral !== null) {
|
|
559
|
-
return stringLiteral;
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
|
|
563
|
-
const items = splitTopLevelItems(trimmed)
|
|
564
|
-
.map((item) => parseSimpleValueExpression(item))
|
|
565
|
-
.filter((item) => item !== null);
|
|
566
|
-
|
|
567
|
-
return items.every((item) => typeof item === 'string') ? items : null;
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
|
|
571
|
-
return parseSimpleObjectLiteral(trimmed);
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
return null;
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
function parseStringLiteral(value) {
|
|
578
|
-
const trimmed = value.trim();
|
|
579
|
-
|
|
580
|
-
if ((trimmed.startsWith("'") && trimmed.endsWith("'")) || (trimmed.startsWith('"') && trimmed.endsWith('"'))) {
|
|
581
|
-
return trimmed.slice(1, -1);
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
return null;
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
function resolvePathExpression(expr, constObjects) {
|
|
588
|
-
const trimmed = expr.trim();
|
|
589
|
-
const literal = parseStringLiteral(trimmed);
|
|
590
|
-
|
|
591
|
-
if (literal !== null) {
|
|
592
|
-
return literal;
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
const refMatch = /^(\w+)\.(\w+)$/.exec(trimmed);
|
|
596
|
-
|
|
597
|
-
if (!refMatch) {
|
|
598
|
-
return null;
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
const object = constObjects.get(refMatch[1]);
|
|
602
|
-
|
|
603
|
-
if (!object) {
|
|
604
|
-
return null;
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
return object[refMatch[2]] ?? null;
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
function resolveConstExpression(expr, constObjects) {
|
|
611
|
-
const trimmed = expr.trim();
|
|
612
|
-
const literal = parseSimpleValueExpression(trimmed);
|
|
613
|
-
|
|
614
|
-
if (literal !== null) {
|
|
615
|
-
return literal;
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
if (constObjects.has(trimmed)) {
|
|
619
|
-
return constObjects.get(trimmed);
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
const refMatch = /^(\w+)\.(\w+)$/.exec(trimmed);
|
|
623
|
-
|
|
624
|
-
if (!refMatch) {
|
|
625
|
-
return null;
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
const object = constObjects.get(refMatch[1]);
|
|
629
|
-
|
|
630
|
-
if (!object || typeof object !== 'object') {
|
|
631
|
-
return null;
|
|
632
|
-
}
|
|
633
|
-
|
|
634
|
-
return object[refMatch[2]] ?? null;
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
function normalizePathPath(value) {
|
|
638
|
-
const trimmed = (value ?? '').trim().replace(/^\/+|\/+$/g, '');
|
|
639
|
-
|
|
640
|
-
return trimmed;
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
function joinRoutePath(parentPath, childPath) {
|
|
644
|
-
const normalizedParent = normalizePathPath(parentPath);
|
|
645
|
-
const normalizedChild = normalizePathPath(childPath);
|
|
646
|
-
|
|
647
|
-
if (!normalizedParent && !normalizedChild) {
|
|
648
|
-
return '/';
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
if (!normalizedParent) {
|
|
652
|
-
return `/${normalizedChild}`;
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
if (!normalizedChild) {
|
|
656
|
-
return `/${normalizedParent}`;
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
return `/${normalizedParent}/${normalizedChild}`;
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
function extractFieldExpression(objectText, fieldName) {
|
|
663
|
-
const inner = objectText.trim().replace(/^\{/, '').replace(/\}$/, '');
|
|
664
|
-
|
|
665
|
-
for (const entry of splitTopLevelItems(`[${inner}]`)) {
|
|
666
|
-
const idx = entry.indexOf(':');
|
|
667
|
-
|
|
668
|
-
if (idx < 0) {
|
|
669
|
-
continue;
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
const key = entry.slice(0, idx).trim().replace(/^['"]|['"]$/g, '');
|
|
673
|
-
|
|
674
|
-
if (key === fieldName) {
|
|
675
|
-
return entry.slice(idx + 1).trim();
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
return null;
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
function extractChildrenArray(objectText) {
|
|
683
|
-
const expr = extractFieldExpression(objectText, 'children');
|
|
684
|
-
|
|
685
|
-
if (!expr || !expr.startsWith('[')) {
|
|
686
|
-
return null;
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
return expr;
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
function extractRouteDataNamespaces(objectText, constObjects, reportContext) {
|
|
693
|
-
const dataExpr = extractFieldExpression(objectText, 'data');
|
|
694
|
-
|
|
695
|
-
if (!dataExpr || !dataExpr.startsWith('{')) {
|
|
696
|
-
return [];
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
const namespaces = new Set(extractKeyLiterals(dataExpr).filter(isAcceptedKey).map(namespaceFromKey).filter(Boolean));
|
|
700
|
-
const explicitNamespaces = extractExplicitNamespaceArray(dataExpr, 'presentiaNamespaces', constObjects);
|
|
701
|
-
|
|
702
|
-
if (extractFieldExpression(dataExpr, 'presentiaNamespaces') && !explicitNamespaces.length) {
|
|
703
|
-
generatorReport?.addIssue({
|
|
704
|
-
type: 'unsupported-data-namespaces',
|
|
705
|
-
filePath: reportContext.routesFile,
|
|
706
|
-
routePath: reportContext.fullPath,
|
|
707
|
-
message: 'Could not statically resolve data.presentiaNamespaces',
|
|
708
|
-
});
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
for (const ns of explicitNamespaces) {
|
|
712
|
-
namespaces.add(ns);
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
return [...namespaces];
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
function extractExplicitNamespaceArray(objectText, fieldName, constObjects) {
|
|
719
|
-
const expr = extractFieldExpression(objectText, fieldName);
|
|
720
|
-
|
|
721
|
-
if (!expr) {
|
|
722
|
-
return [];
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
const resolved = resolveConstExpression(expr, constObjects);
|
|
726
|
-
|
|
727
|
-
if (!Array.isArray(resolved)) {
|
|
728
|
-
return [];
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
return resolved.map((value) => String(value).trim()).filter(isAcceptedNamespace);
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
function parseLoadComponent(objectText) {
|
|
735
|
-
return parseLazyImportExpression(extractFieldExpression(objectText, 'loadComponent'));
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
function parseLoadChildren(objectText) {
|
|
739
|
-
return parseLazyImportExpression(extractFieldExpression(objectText, 'loadChildren'));
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
function parseLazyImportExpression(expr) {
|
|
743
|
-
|
|
744
|
-
if (!expr) {
|
|
745
|
-
return null;
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
const importMatch = /import\(\s*['"]([^'"]+)['"]\s*\)/.exec(expr);
|
|
749
|
-
const exportMatch = /\.([A-Za-z0-9_]+)\s*\)?\s*$/.exec(expr);
|
|
750
|
-
|
|
751
|
-
if (!importMatch || !exportMatch) {
|
|
752
|
-
return null;
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
return { specifier: importMatch[1], exportName: exportMatch[1] };
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
function parseRoutes(arrayLiteral, context) {
|
|
759
|
-
const routes = [];
|
|
760
|
-
|
|
761
|
-
for (const objectText of extractObjectLiterals(arrayLiteral)) {
|
|
762
|
-
const pathExpr = extractFieldExpression(objectText, 'path');
|
|
763
|
-
const redirectExpr = extractFieldExpression(objectText, 'redirectTo');
|
|
764
|
-
const pathValue = pathExpr ? resolvePathExpression(pathExpr, context.constObjects) : null;
|
|
765
|
-
const ownPath = normalizePathPath(pathValue ?? '');
|
|
766
|
-
const fullPath = joinRoutePath(context.parentPath, ownPath);
|
|
767
|
-
const routeDataNamespaces = extractRouteDataNamespaces(objectText, context.constObjects, {
|
|
768
|
-
routesFile: context.routesFile,
|
|
769
|
-
fullPath,
|
|
770
|
-
});
|
|
771
|
-
const componentRefs = [];
|
|
772
|
-
const componentExpr = extractFieldExpression(objectText, 'component');
|
|
773
|
-
|
|
774
|
-
if (componentExpr) {
|
|
775
|
-
const localName = componentExpr.trim();
|
|
776
|
-
const imported = context.imports.get(localName);
|
|
777
|
-
|
|
778
|
-
if (imported) {
|
|
779
|
-
const resolved = context.resolver(imported.specifier, context.routesFile);
|
|
780
|
-
|
|
781
|
-
if (!resolved) {
|
|
782
|
-
generatorReport?.addIssue({
|
|
783
|
-
type: 'unresolved-component',
|
|
784
|
-
filePath: context.routesFile,
|
|
785
|
-
routePath: fullPath,
|
|
786
|
-
message: `Could not resolve component import "${imported.specifier}"`,
|
|
787
|
-
});
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
componentRefs.push({
|
|
791
|
-
filePath: resolved,
|
|
792
|
-
exportName: imported.imported,
|
|
793
|
-
});
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
const lazyComponent = parseLoadComponent(objectText);
|
|
798
|
-
if (lazyComponent) {
|
|
799
|
-
const resolved = context.resolver(lazyComponent.specifier, context.routesFile);
|
|
800
|
-
|
|
801
|
-
if (!resolved) {
|
|
802
|
-
generatorReport?.addIssue({
|
|
803
|
-
type: 'unresolved-load-component',
|
|
804
|
-
filePath: context.routesFile,
|
|
805
|
-
routePath: fullPath,
|
|
806
|
-
message: `Could not resolve loadComponent import "${lazyComponent.specifier}"`,
|
|
807
|
-
});
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
componentRefs.push({
|
|
811
|
-
filePath: resolved,
|
|
812
|
-
exportName: lazyComponent.exportName,
|
|
813
|
-
});
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
const lazyChildren = parseLoadChildren(objectText);
|
|
817
|
-
|
|
818
|
-
const componentNamespaces = componentRefs.flatMap((componentRef) =>
|
|
819
|
-
componentRef.filePath ? collectComponentNamespaces(componentRef.filePath, componentRef.exportName, context) : [],
|
|
820
|
-
);
|
|
821
|
-
const inheritedNamespaces = unique([...context.inheritedNamespaces, ...componentNamespaces, ...routeDataNamespaces]);
|
|
822
|
-
const childrenArray = extractChildrenArray(objectText);
|
|
823
|
-
|
|
824
|
-
if (childrenArray) {
|
|
825
|
-
routes.push(
|
|
826
|
-
...parseRoutes(childrenArray, {
|
|
827
|
-
...context,
|
|
828
|
-
parentPath: fullPath,
|
|
829
|
-
inheritedNamespaces,
|
|
830
|
-
}),
|
|
831
|
-
);
|
|
832
|
-
}
|
|
833
|
-
|
|
834
|
-
if (lazyChildren) {
|
|
835
|
-
const childRoutesFile = context.resolver(lazyChildren.specifier, context.routesFile);
|
|
836
|
-
|
|
837
|
-
if (childRoutesFile) {
|
|
838
|
-
routes.push(
|
|
839
|
-
...parseRoutesFromExport(childRoutesFile, lazyChildren.exportName, {
|
|
840
|
-
...context,
|
|
841
|
-
inheritedNamespaces,
|
|
842
|
-
parentPath: fullPath,
|
|
843
|
-
routesFile: childRoutesFile,
|
|
844
|
-
}),
|
|
845
|
-
);
|
|
846
|
-
} else {
|
|
847
|
-
generatorReport?.addIssue({
|
|
848
|
-
type: 'unresolved-load-children',
|
|
849
|
-
filePath: context.routesFile,
|
|
850
|
-
routePath: fullPath,
|
|
851
|
-
message: `Could not resolve loadChildren import "${lazyChildren.specifier}"`,
|
|
852
|
-
});
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
const isConcreteRoute = !redirectExpr && (componentRefs.length > 0 || routeDataNamespaces.length > 0);
|
|
857
|
-
|
|
858
|
-
if (isConcreteRoute) {
|
|
859
|
-
routes.push({
|
|
860
|
-
path: fullPath,
|
|
861
|
-
namespaces: inheritedNamespaces,
|
|
862
|
-
});
|
|
863
|
-
}
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
return routes;
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
function parseRoutesFromExport(filePath, exportName, context) {
|
|
870
|
-
const visitKey = `${filePath}#${exportName}#${context.parentPath}`;
|
|
871
|
-
|
|
872
|
-
if (visitedRouteExports.has(visitKey)) {
|
|
873
|
-
return [];
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
visitedRouteExports.add(visitKey);
|
|
877
|
-
|
|
878
|
-
const source = stripComments(readText(filePath));
|
|
879
|
-
const arrayLiteral =
|
|
880
|
-
extractArrayLiteral(source, new RegExp(`\\bexport\\s+const\\s+${exportName}\\s*:\\s*[^=]+=\\s*\\[`, 'g')) ??
|
|
881
|
-
extractArrayLiteral(source, new RegExp(`\\bexport\\s+const\\s+${exportName}\\s*=\\s*\\[`, 'g'));
|
|
882
|
-
|
|
883
|
-
if (!arrayLiteral) {
|
|
884
|
-
return [];
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
return parseRoutes(arrayLiteral, {
|
|
888
|
-
...context,
|
|
889
|
-
constObjects: parseConstObjects(filePath, context.resolver),
|
|
890
|
-
imports: parseImports(source),
|
|
891
|
-
routesFile: filePath,
|
|
892
|
-
});
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
function collectComponentNamespaces(filePath, exportName, context) {
|
|
896
|
-
const key = `${filePath}#${exportName}`;
|
|
897
|
-
|
|
898
|
-
if (visitedFiles.has(key) || !filePath || !fs.existsSync(filePath)) {
|
|
899
|
-
return [];
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
visitedFiles.add(key);
|
|
903
|
-
|
|
904
|
-
const source = stripComments(readText(filePath));
|
|
905
|
-
const imports = parseImports(source);
|
|
906
|
-
const namespaces = new Set(collectNamespacesFromCode(source));
|
|
907
|
-
|
|
908
|
-
for (const htmlContent of extractTemplateContents(source, filePath)) {
|
|
909
|
-
for (const ns of collectNamespacesFromTemplate(htmlContent)) {
|
|
910
|
-
namespaces.add(ns);
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
for (const importedName of extractStandaloneImports(source)) {
|
|
915
|
-
const imported = imports.get(importedName);
|
|
916
|
-
|
|
917
|
-
if (!imported) {
|
|
918
|
-
generatorReport?.addIssue({
|
|
919
|
-
type: 'unsupported-standalone-import',
|
|
920
|
-
filePath,
|
|
921
|
-
exportName,
|
|
922
|
-
message: `Could not resolve standalone import "${importedName}" from component imports`,
|
|
923
|
-
});
|
|
924
|
-
continue;
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
const resolved = context.resolver(imported.specifier, filePath);
|
|
928
|
-
|
|
929
|
-
if (!resolved || !resolved.startsWith(projectRoot)) {
|
|
930
|
-
continue;
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
for (const ns of collectComponentNamespaces(resolved, imported.imported, context)) {
|
|
934
|
-
namespaces.add(ns);
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
return [...namespaces];
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
function extractTemplateContents(source, filePath) {
|
|
942
|
-
const contents = [];
|
|
943
|
-
const templateUrlRe = /templateUrl\s*:\s*['"]([^'"]+)['"]/g;
|
|
944
|
-
let match;
|
|
945
|
-
|
|
946
|
-
while ((match = templateUrlRe.exec(source))) {
|
|
947
|
-
const resolved = resolveFile(path.resolve(path.dirname(filePath), match[1]));
|
|
948
|
-
|
|
949
|
-
if (resolved) {
|
|
950
|
-
contents.push(readText(resolved));
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
const templateRe = /template\s*:\s*(`[\s\S]*?`|'(?:\\.|[^'])*'|"(?:\\.|[^"])*")/g;
|
|
955
|
-
|
|
956
|
-
while ((match = templateRe.exec(source))) {
|
|
957
|
-
const raw = match[1];
|
|
958
|
-
|
|
959
|
-
if (raw.startsWith('`')) {
|
|
960
|
-
contents.push(raw.slice(1, -1));
|
|
961
|
-
} else {
|
|
962
|
-
contents.push(raw.slice(1, -1));
|
|
963
|
-
}
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
return contents;
|
|
967
|
-
}
|
|
968
|
-
|
|
969
|
-
function extractStandaloneImports(source) {
|
|
970
|
-
const importsArray = extractArrayLiteral(source, /\bimports\s*:/g);
|
|
971
|
-
|
|
972
|
-
if (!importsArray) {
|
|
973
|
-
return [];
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
return splitTopLevelItems(importsArray)
|
|
977
|
-
.map((item) => item.replace(/\s/g, ''))
|
|
978
|
-
.filter((item) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(item));
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
function collectNamespacesFromTemplate(content) {
|
|
982
|
-
const namespaces = new Set();
|
|
983
|
-
const langPipeRe = /['"`]([A-Za-z0-9_.-]+)['"`]\s*\|\s*lang\b/g;
|
|
984
|
-
const methodLikeRe = /\[\s*reLang(?:Key)?\s*\]\s*=\s*['"`]([^'"`]+)['"`]/g;
|
|
985
|
-
const tagWithReLangRe = /<[^>]*\breLang\b[^>]*>/g;
|
|
986
|
-
const reLangAttrsBindings = extractBindingExpressions(content, 'reLangAttrs');
|
|
987
|
-
const reLangConfigBindings = extractBindingExpressions(content, 'reLang');
|
|
988
|
-
let match;
|
|
989
|
-
|
|
990
|
-
while ((match = langPipeRe.exec(content))) {
|
|
991
|
-
const ns = acceptedNamespaceFromKey(match[1]);
|
|
992
|
-
if (ns) namespaces.add(ns);
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
while ((match = methodLikeRe.exec(content))) {
|
|
996
|
-
const ns = acceptedNamespaceFromKey(match[1]);
|
|
997
|
-
if (ns) namespaces.add(ns);
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
while ((match = tagWithReLangRe.exec(content))) {
|
|
1001
|
-
for (const key of extractKeyLiterals(match[0])) {
|
|
1002
|
-
const ns = acceptedNamespaceFromKey(key);
|
|
1003
|
-
if (ns) namespaces.add(ns);
|
|
1004
|
-
}
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
for (const expr of reLangAttrsBindings) {
|
|
1008
|
-
const parsed = parseSimpleValueExpression(expr);
|
|
1009
|
-
|
|
1010
|
-
if (!parsed || typeof parsed !== 'object') {
|
|
1011
|
-
continue;
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
for (const value of Object.values(parsed)) {
|
|
1015
|
-
if (typeof value !== 'string') {
|
|
1016
|
-
continue;
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
|
-
const ns = acceptedNamespaceFromKey(value);
|
|
1020
|
-
if (ns) namespaces.add(ns);
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
for (const expr of reLangConfigBindings) {
|
|
1025
|
-
const parsed = parseSimpleValueExpression(expr);
|
|
1026
|
-
|
|
1027
|
-
if (!parsed || typeof parsed !== 'object') {
|
|
1028
|
-
continue;
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
if (typeof parsed.textKey === 'string') {
|
|
1032
|
-
const ns = acceptedNamespaceFromKey(parsed.textKey);
|
|
1033
|
-
if (ns) namespaces.add(ns);
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
if (parsed.attrs && typeof parsed.attrs === 'object') {
|
|
1037
|
-
for (const value of Object.values(parsed.attrs)) {
|
|
1038
|
-
if (typeof value !== 'string') {
|
|
1039
|
-
continue;
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
const ns = acceptedNamespaceFromKey(value);
|
|
1043
|
-
if (ns) namespaces.add(ns);
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
}
|
|
1047
|
-
|
|
1048
|
-
return [...namespaces];
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
function extractBindingExpressions(content, bindingName) {
|
|
1052
|
-
const out = [];
|
|
1053
|
-
const re = new RegExp(`\\[\\s*${bindingName}\\s*\\]\\s*=\\s*([\"'])([\\s\\S]*?)\\1`, 'g');
|
|
1054
|
-
let match;
|
|
1055
|
-
|
|
1056
|
-
while ((match = re.exec(content))) {
|
|
1057
|
-
out.push(match[2]);
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
return out;
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
function collectNamespacesFromCode(content) {
|
|
1064
|
-
const namespaces = new Set();
|
|
1065
|
-
const methodRe = /\.\s*(?:get|observe|translate)\s*\(\s*['"`]([^'"`]+)['"`]/g;
|
|
1066
|
-
let match;
|
|
1067
|
-
|
|
1068
|
-
while ((match = methodRe.exec(content))) {
|
|
1069
|
-
const ns = acceptedNamespaceFromKey(match[1]);
|
|
1070
|
-
if (ns) namespaces.add(ns);
|
|
1071
|
-
}
|
|
1072
|
-
|
|
1073
|
-
return [...namespaces];
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
|
-
function extractKeyLiterals(content) {
|
|
1077
|
-
const out = [];
|
|
1078
|
-
const re = /['"`]([A-Za-z0-9_.-]+)['"`]/g;
|
|
1079
|
-
let match;
|
|
1080
|
-
|
|
1081
|
-
while ((match = re.exec(content))) {
|
|
1082
|
-
if (KEY_RE.test(match[1])) {
|
|
1083
|
-
out.push(match[1]);
|
|
1084
|
-
}
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
|
-
return out;
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
function namespaceFromKey(key) {
|
|
1091
|
-
if (!KEY_RE.test(key)) {
|
|
1092
|
-
return null;
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
const [ns] = key.split('.', 1);
|
|
1096
|
-
|
|
1097
|
-
return ns || null;
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
function acceptedNamespaceFromKey(key) {
|
|
1101
|
-
if (!isAcceptedKey(key)) {
|
|
1102
|
-
return null;
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
return namespaceFromKey(key);
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
function isAcceptedKey(key) {
|
|
1109
|
-
if (!KEY_RE.test(key)) {
|
|
1110
|
-
return false;
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
|
-
if (!localeValidator) {
|
|
1114
|
-
return true;
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
return localeValidator.hasKey(key);
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
function isAcceptedNamespace(ns) {
|
|
1121
|
-
if (!ns) {
|
|
1122
|
-
return false;
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
if (!localeValidator) {
|
|
1126
|
-
return true;
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
return localeValidator.hasNamespace(ns);
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
function unique(values) {
|
|
1133
|
-
return [...new Set(values.filter(Boolean))];
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
function toManifest(routes) {
|
|
1137
|
-
const manifest = new Map();
|
|
1138
|
-
|
|
1139
|
-
for (const route of routes) {
|
|
1140
|
-
if (!route.namespaces.length) {
|
|
1141
|
-
continue;
|
|
1142
|
-
}
|
|
1143
|
-
|
|
1144
|
-
const existing = manifest.get(route.path) ?? [];
|
|
1145
|
-
|
|
1146
|
-
manifest.set(route.path, unique([...existing, ...route.namespaces]).sort());
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
|
-
return manifest;
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
function formatManifest(manifest, name) {
|
|
1153
|
-
const entries = [...manifest.entries()].sort(([a], [b]) => a.localeCompare(b));
|
|
1154
|
-
const lines = entries.map(([routePath, namespaces]) => {
|
|
1155
|
-
const nsList = namespaces.map((ns) => `'${ns}'`).join(', ');
|
|
1156
|
-
|
|
1157
|
-
return ` '${routePath}': [${nsList}],`;
|
|
1158
|
-
});
|
|
1159
|
-
|
|
1160
|
-
return `/* auto-generated by presentia-gen-namespaces */\nexport const ${name} = {\n${lines.join('\n')}\n} as const;\n`;
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
function formatReport(report) {
|
|
1164
|
-
return JSON.stringify(report, null, 2);
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
|
-
export function generateNamespaceManifest(cliArgs = process.argv.slice(2), currentWorkingDir = process.cwd()) {
|
|
1168
|
-
const routesFile = path.resolve(currentWorkingDir, getArg(cliArgs, '--routes', 'src/app/app.routes.ts'));
|
|
1169
|
-
const outputFile = path.resolve(currentWorkingDir, getArg(cliArgs, '--out', 'src/app/presentia-route-namespaces.ts'));
|
|
1170
|
-
const tsconfigFile = path.resolve(currentWorkingDir, getArg(cliArgs, '--tsconfig', 'tsconfig.json'));
|
|
1171
|
-
const localesDirArg = getArg(cliArgs, '--locales', '');
|
|
1172
|
-
const exportName = getArg(cliArgs, '--name', 'PRESENTIA_ROUTE_NAMESPACES');
|
|
1173
|
-
const reportFileArg = getArg(cliArgs, '--report', '');
|
|
1174
|
-
const printReport = cliArgs.includes('--print-report');
|
|
1175
|
-
|
|
1176
|
-
projectRoot = path.resolve(currentWorkingDir, getArg(cliArgs, '--project', path.dirname(routesFile)));
|
|
1177
|
-
localeValidator = createLocaleValidator(localesDirArg ? path.resolve(currentWorkingDir, localesDirArg) : null);
|
|
1178
|
-
generatorReport = createReport();
|
|
1179
|
-
|
|
1180
|
-
visitedFiles.clear();
|
|
1181
|
-
visitedRouteExports.clear();
|
|
1182
|
-
|
|
1183
|
-
if (!fs.existsSync(routesFile)) {
|
|
1184
|
-
fail(`routes file not found: ${routesFile}`);
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
const tsconfig = loadTsconfig(tsconfigFile);
|
|
1188
|
-
const resolver = createPathResolver(routesFile, tsconfig);
|
|
1189
|
-
const routesSource = stripComments(readText(routesFile));
|
|
1190
|
-
const routesArray = extractArrayLiteral(routesSource, /\b(?:const|let|var)\s+\w+\s*:\s*[^=]+=\s*\[/) ??
|
|
1191
|
-
extractArrayLiteral(routesSource, /\b(?:const|let|var)\s+\w+\s*=\s*\[/);
|
|
1192
|
-
|
|
1193
|
-
if (!routesArray) {
|
|
1194
|
-
fail(`unable to find routes array in: ${routesFile}`);
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
const constObjects = parseConstObjects(routesFile, resolver);
|
|
1198
|
-
const imports = parseImports(routesSource);
|
|
1199
|
-
const routes = parseRoutes(routesArray, {
|
|
1200
|
-
constObjects,
|
|
1201
|
-
imports,
|
|
1202
|
-
inheritedNamespaces: [],
|
|
1203
|
-
parentPath: '',
|
|
1204
|
-
resolver,
|
|
1205
|
-
routesFile,
|
|
1206
|
-
});
|
|
1207
|
-
const manifest = toManifest(routes);
|
|
1208
|
-
const content = formatManifest(manifest, exportName);
|
|
1209
|
-
const report = {
|
|
1210
|
-
routesFile,
|
|
1211
|
-
outputFile,
|
|
1212
|
-
routeCount: manifest.size,
|
|
1213
|
-
issueCount: generatorReport.issues.length,
|
|
1214
|
-
issues: generatorReport.issues,
|
|
1215
|
-
};
|
|
1216
|
-
|
|
1217
|
-
fs.mkdirSync(path.dirname(outputFile), { recursive: true });
|
|
1218
|
-
fs.writeFileSync(outputFile, content, 'utf8');
|
|
1219
|
-
|
|
1220
|
-
if (reportFileArg) {
|
|
1221
|
-
const reportFile = path.resolve(currentWorkingDir, reportFileArg);
|
|
1222
|
-
|
|
1223
|
-
fs.mkdirSync(path.dirname(reportFile), { recursive: true });
|
|
1224
|
-
fs.writeFileSync(reportFile, formatReport(report), 'utf8');
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
|
-
if (printReport && report.issueCount) {
|
|
1228
|
-
console.warn(formatReport(report));
|
|
1229
|
-
}
|
|
1230
|
-
|
|
1231
|
-
return { manifest, content, outputFile, report };
|
|
1232
|
-
}
|
|
1233
|
-
|
|
1234
|
-
const isCliEntrypoint = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
1235
|
-
|
|
1236
|
-
if (isCliEntrypoint) {
|
|
1237
|
-
try {
|
|
1238
|
-
const { manifest, outputFile, report } = generateNamespaceManifest();
|
|
1239
|
-
|
|
1240
|
-
console.log(`[presentia-gen-namespaces] Generated ${manifest.size} routes -> ${outputFile}`);
|
|
1241
|
-
if (report.issueCount) {
|
|
1242
|
-
console.warn(`[presentia-gen-namespaces] Reported ${report.issueCount} static-analysis issue(s)`);
|
|
1243
|
-
}
|
|
1244
|
-
} catch (error) {
|
|
1245
|
-
console.error(error instanceof Error ? error.message : String(error));
|
|
1246
|
-
process.exit(1);
|
|
1247
|
-
}
|
|
1248
|
-
}
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
|
|
6
|
+
let projectRoot = '';
|
|
7
|
+
let localeValidator = null;
|
|
8
|
+
let generatorReport = null;
|
|
9
|
+
|
|
10
|
+
const getArg = (args, name, fallback) => {
|
|
11
|
+
const idx = args.indexOf(name);
|
|
12
|
+
|
|
13
|
+
return idx >= 0 && args[idx + 1] ? args[idx + 1] : fallback;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const KEY_RE = /^[a-z0-9_-]+\.[a-z0-9_.-]+$/i;
|
|
17
|
+
const visitedFiles = new Set();
|
|
18
|
+
const visitedRouteExports = new Set();
|
|
19
|
+
|
|
20
|
+
function fail(message) {
|
|
21
|
+
throw new Error(`[presentia-gen-namespaces] ${message}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function createReport() {
|
|
25
|
+
return {
|
|
26
|
+
issues: [],
|
|
27
|
+
addIssue(issue) {
|
|
28
|
+
this.issues.push(issue);
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function readText(filePath) {
|
|
34
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function tryReadJson(filePath) {
|
|
38
|
+
if (!fs.existsSync(filePath)) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return JSON.parse(readText(filePath));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function collectJsonFiles(dir) {
|
|
46
|
+
const out = [];
|
|
47
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
48
|
+
|
|
49
|
+
for (const entry of entries) {
|
|
50
|
+
const full = path.join(dir, entry.name);
|
|
51
|
+
|
|
52
|
+
if (entry.isDirectory()) {
|
|
53
|
+
out.push(...collectJsonFiles(full));
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (entry.isFile() && entry.name.endsWith('.json')) {
|
|
58
|
+
out.push(full);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function namespaceFromLocaleFile(filePath) {
|
|
66
|
+
const base = path.basename(filePath);
|
|
67
|
+
const parts = base.split('.');
|
|
68
|
+
|
|
69
|
+
if (parts.length >= 3) {
|
|
70
|
+
return parts.slice(0, -2).join('.');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return parts.slice(0, -1).join('.');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function flattenLocaleKeys(value, prefix = '') {
|
|
77
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const out = [];
|
|
82
|
+
|
|
83
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
84
|
+
const next = prefix ? `${prefix}.${key}` : key;
|
|
85
|
+
|
|
86
|
+
if (nested && typeof nested === 'object' && !Array.isArray(nested)) {
|
|
87
|
+
out.push(...flattenLocaleKeys(nested, next));
|
|
88
|
+
} else {
|
|
89
|
+
out.push(next);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function createLocaleValidator(localesPath) {
|
|
97
|
+
if (!localesPath) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!fs.existsSync(localesPath)) {
|
|
102
|
+
fail(`locales directory not found: ${localesPath}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const namespaceSet = new Set();
|
|
106
|
+
const keySet = new Set();
|
|
107
|
+
|
|
108
|
+
for (const file of collectJsonFiles(localesPath)) {
|
|
109
|
+
const ns = namespaceFromLocaleFile(file);
|
|
110
|
+
|
|
111
|
+
if (!ns) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
namespaceSet.add(ns);
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
const json = JSON.parse(readText(file));
|
|
119
|
+
|
|
120
|
+
for (const key of flattenLocaleKeys(json)) {
|
|
121
|
+
keySet.add(`${ns}.${key}`);
|
|
122
|
+
}
|
|
123
|
+
} catch {
|
|
124
|
+
// Ignore malformed locale files in generator mode.
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
hasNamespace(ns) {
|
|
130
|
+
return namespaceSet.has(ns);
|
|
131
|
+
},
|
|
132
|
+
hasKey(key) {
|
|
133
|
+
return keySet.has(key);
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function loadTsconfig(filePath, seen = new Set()) {
|
|
139
|
+
const resolved = path.resolve(filePath);
|
|
140
|
+
|
|
141
|
+
if (!fs.existsSync(resolved) || seen.has(resolved)) {
|
|
142
|
+
return { compilerOptions: {}, _dir: path.dirname(resolved) };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
seen.add(resolved);
|
|
146
|
+
const json = JSON.parse(stripComments(readText(resolved)));
|
|
147
|
+
const base = json.extends
|
|
148
|
+
? loadTsconfig(resolveExtendsPath(path.dirname(resolved), json.extends), seen)
|
|
149
|
+
: { compilerOptions: {}, _dir: path.dirname(resolved) };
|
|
150
|
+
const compilerOptions = {
|
|
151
|
+
...(base.compilerOptions ?? {}),
|
|
152
|
+
...(json.compilerOptions ?? {}),
|
|
153
|
+
paths: {
|
|
154
|
+
...((base.compilerOptions && base.compilerOptions.paths) || {}),
|
|
155
|
+
...((json.compilerOptions && json.compilerOptions.paths) || {}),
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
return { ...base, ...json, compilerOptions, _dir: path.dirname(resolved) };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function resolveExtendsPath(baseDir, value) {
|
|
163
|
+
if (value.startsWith('.')) {
|
|
164
|
+
return withJsonSuffix(path.resolve(baseDir, value));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return withJsonSuffix(path.resolve(baseDir, value));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function withJsonSuffix(filePath) {
|
|
171
|
+
return filePath.endsWith('.json') ? filePath : `${filePath}.json`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function stripComments(source) {
|
|
175
|
+
let out = '';
|
|
176
|
+
let quote = null;
|
|
177
|
+
let escape = false;
|
|
178
|
+
|
|
179
|
+
for (let i = 0; i < source.length; i++) {
|
|
180
|
+
const ch = source[i];
|
|
181
|
+
const next = source[i + 1];
|
|
182
|
+
|
|
183
|
+
if (quote) {
|
|
184
|
+
out += ch;
|
|
185
|
+
|
|
186
|
+
if (escape) {
|
|
187
|
+
escape = false;
|
|
188
|
+
} else if (ch === '\\') {
|
|
189
|
+
escape = true;
|
|
190
|
+
} else if (ch === quote) {
|
|
191
|
+
quote = null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
198
|
+
quote = ch;
|
|
199
|
+
out += ch;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (ch === '/' && next === '/') {
|
|
204
|
+
while (i < source.length && source[i] !== '\n') {
|
|
205
|
+
i++;
|
|
206
|
+
}
|
|
207
|
+
out += '\n';
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (ch === '/' && next === '*') {
|
|
212
|
+
i += 2;
|
|
213
|
+
while (i < source.length && !(source[i] === '*' && source[i + 1] === '/')) {
|
|
214
|
+
i++;
|
|
215
|
+
}
|
|
216
|
+
i++;
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
out += ch;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return out;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function findMatching(source, start, openChar, closeChar) {
|
|
227
|
+
let depth = 0;
|
|
228
|
+
let quote = null;
|
|
229
|
+
let escape = false;
|
|
230
|
+
|
|
231
|
+
for (let i = start; i < source.length; i++) {
|
|
232
|
+
const ch = source[i];
|
|
233
|
+
const next = source[i + 1];
|
|
234
|
+
|
|
235
|
+
if (quote) {
|
|
236
|
+
if (escape) {
|
|
237
|
+
escape = false;
|
|
238
|
+
} else if (ch === '\\') {
|
|
239
|
+
escape = true;
|
|
240
|
+
} else if (ch === quote) {
|
|
241
|
+
quote = null;
|
|
242
|
+
}
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
247
|
+
quote = ch;
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (ch === '/' && next === '/') {
|
|
252
|
+
while (i < source.length && source[i] !== '\n') {
|
|
253
|
+
i++;
|
|
254
|
+
}
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (ch === '/' && next === '*') {
|
|
259
|
+
i += 2;
|
|
260
|
+
while (i < source.length && !(source[i] === '*' && source[i + 1] === '/')) {
|
|
261
|
+
i++;
|
|
262
|
+
}
|
|
263
|
+
i++;
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (ch === openChar) {
|
|
268
|
+
depth++;
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (ch === closeChar) {
|
|
273
|
+
depth--;
|
|
274
|
+
if (depth === 0) {
|
|
275
|
+
return i;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return -1;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function extractArrayLiteral(source, anchorPattern) {
|
|
284
|
+
const match = anchorPattern.exec(source);
|
|
285
|
+
|
|
286
|
+
if (!match) {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const preferredStart = match.index + match[0].length - 1;
|
|
291
|
+
const start = source[preferredStart] === '[' ? preferredStart : source.indexOf('[', preferredStart);
|
|
292
|
+
const end = findMatching(source, start, '[', ']');
|
|
293
|
+
|
|
294
|
+
if (start < 0 || end < 0) {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return source.slice(start, end + 1);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function splitTopLevelItems(arrayLiteral) {
|
|
302
|
+
const inner = arrayLiteral.trim().replace(/^\[/, '').replace(/\]$/, '');
|
|
303
|
+
const items = [];
|
|
304
|
+
let token = '';
|
|
305
|
+
let depthBrace = 0;
|
|
306
|
+
let depthBracket = 0;
|
|
307
|
+
let depthParen = 0;
|
|
308
|
+
let quote = null;
|
|
309
|
+
let escape = false;
|
|
310
|
+
|
|
311
|
+
for (let i = 0; i < inner.length; i++) {
|
|
312
|
+
const ch = inner[i];
|
|
313
|
+
const next = inner[i + 1];
|
|
314
|
+
|
|
315
|
+
token += ch;
|
|
316
|
+
|
|
317
|
+
if (quote) {
|
|
318
|
+
if (escape) {
|
|
319
|
+
escape = false;
|
|
320
|
+
} else if (ch === '\\') {
|
|
321
|
+
escape = true;
|
|
322
|
+
} else if (ch === quote) {
|
|
323
|
+
quote = null;
|
|
324
|
+
}
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
329
|
+
quote = ch;
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (ch === '/' && next === '/') {
|
|
334
|
+
while (i < inner.length && inner[i] !== '\n') {
|
|
335
|
+
token += inner[++i] ?? '';
|
|
336
|
+
}
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (ch === '/' && next === '*') {
|
|
341
|
+
token += inner[++i] ?? '';
|
|
342
|
+
while (i < inner.length && !(inner[i] === '*' && inner[i + 1] === '/')) {
|
|
343
|
+
token += inner[++i] ?? '';
|
|
344
|
+
}
|
|
345
|
+
token += inner[++i] ?? '';
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (ch === '{') depthBrace++;
|
|
350
|
+
if (ch === '}') depthBrace--;
|
|
351
|
+
if (ch === '[') depthBracket++;
|
|
352
|
+
if (ch === ']') depthBracket--;
|
|
353
|
+
if (ch === '(') depthParen++;
|
|
354
|
+
if (ch === ')') depthParen--;
|
|
355
|
+
|
|
356
|
+
if (ch === ',' && depthBrace === 0 && depthBracket === 0 && depthParen === 0) {
|
|
357
|
+
const trimmed = token.slice(0, -1).trim();
|
|
358
|
+
if (trimmed) {
|
|
359
|
+
items.push(trimmed);
|
|
360
|
+
}
|
|
361
|
+
token = '';
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const trimmed = token.trim();
|
|
366
|
+
if (trimmed) {
|
|
367
|
+
items.push(trimmed);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return items;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function extractObjectLiterals(arrayLiteral) {
|
|
374
|
+
return splitTopLevelItems(arrayLiteral).filter((item) => item.startsWith('{'));
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function parseImports(source) {
|
|
378
|
+
const imports = new Map();
|
|
379
|
+
const re = /import\s+\{([^}]+)\}\s+from\s+['"]([^'"]+)['"]/g;
|
|
380
|
+
let match;
|
|
381
|
+
|
|
382
|
+
while ((match = re.exec(source))) {
|
|
383
|
+
const specifier = match[2];
|
|
384
|
+
const names = match[1]
|
|
385
|
+
.split(',')
|
|
386
|
+
.map((part) => part.trim())
|
|
387
|
+
.filter(Boolean);
|
|
388
|
+
|
|
389
|
+
for (const name of names) {
|
|
390
|
+
const [imported, local] = name.split(/\s+as\s+/);
|
|
391
|
+
|
|
392
|
+
imports.set((local ?? imported).trim(), {
|
|
393
|
+
imported: imported.trim(),
|
|
394
|
+
specifier,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return imports;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function resolveFile(basePath) {
|
|
403
|
+
const candidates = [
|
|
404
|
+
basePath,
|
|
405
|
+
`${basePath}.ts`,
|
|
406
|
+
`${basePath}.tsx`,
|
|
407
|
+
`${basePath}.js`,
|
|
408
|
+
`${basePath}.mjs`,
|
|
409
|
+
path.join(basePath, 'index.ts'),
|
|
410
|
+
path.join(basePath, 'index.tsx'),
|
|
411
|
+
path.join(basePath, 'index.js'),
|
|
412
|
+
path.join(basePath, 'index.mjs'),
|
|
413
|
+
];
|
|
414
|
+
|
|
415
|
+
return candidates.find((candidate) => fs.existsSync(candidate) && fs.statSync(candidate).isFile()) ?? null;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function createPathResolver(routesFilePath, tsconfig) {
|
|
419
|
+
const importerDir = path.dirname(routesFilePath);
|
|
420
|
+
const configBase = path.resolve(tsconfig._dir, tsconfig.compilerOptions?.baseUrl ?? '.');
|
|
421
|
+
const pathEntries = Object.entries(tsconfig.compilerOptions?.paths ?? {});
|
|
422
|
+
|
|
423
|
+
return (specifier, fromFile) => {
|
|
424
|
+
const fromDir = fromFile ? path.dirname(fromFile) : importerDir;
|
|
425
|
+
|
|
426
|
+
if (specifier.startsWith('.')) {
|
|
427
|
+
return resolveFile(path.resolve(fromDir, specifier));
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
for (const [pattern, targets] of pathEntries) {
|
|
431
|
+
const matches = matchPathPattern(specifier, pattern);
|
|
432
|
+
|
|
433
|
+
if (!matches) {
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
for (const target of targets) {
|
|
438
|
+
const candidate = target.replace('*', matches);
|
|
439
|
+
const resolved = resolveFile(path.resolve(configBase, candidate));
|
|
440
|
+
|
|
441
|
+
if (resolved) {
|
|
442
|
+
return resolved;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
return resolveFile(path.resolve(configBase, specifier));
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function matchPathPattern(specifier, pattern) {
|
|
452
|
+
if (!pattern.includes('*')) {
|
|
453
|
+
return specifier === pattern ? '' : null;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const [prefix, suffix] = pattern.split('*');
|
|
457
|
+
|
|
458
|
+
if (!specifier.startsWith(prefix) || !specifier.endsWith(suffix)) {
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return specifier.slice(prefix.length, specifier.length - suffix.length);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function parseConstObjects(filePath, resolver, cache = new Map()) {
|
|
466
|
+
if (cache.has(filePath)) {
|
|
467
|
+
return cache.get(filePath);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const source = stripComments(readText(filePath));
|
|
471
|
+
const objects = new Map();
|
|
472
|
+
const exportedRe = /export\s+const\s+(\w+)\s*=\s*\{/g;
|
|
473
|
+
let match;
|
|
474
|
+
|
|
475
|
+
while ((match = exportedRe.exec(source))) {
|
|
476
|
+
const name = match[1];
|
|
477
|
+
const start = source.indexOf('{', match.index);
|
|
478
|
+
const end = findMatching(source, start, '{', '}');
|
|
479
|
+
|
|
480
|
+
if (start < 0 || end < 0) {
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const parsed = parseSimpleValueExpression(source.slice(start, end + 1));
|
|
485
|
+
|
|
486
|
+
if (parsed && typeof parsed === 'object') {
|
|
487
|
+
objects.set(name, parsed);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const exportedArrayRe = /export\s+const\s+(\w+)\s*=\s*\[/g;
|
|
492
|
+
|
|
493
|
+
while ((match = exportedArrayRe.exec(source))) {
|
|
494
|
+
const name = match[1];
|
|
495
|
+
const start = source.indexOf('[', match.index);
|
|
496
|
+
const end = findMatching(source, start, '[', ']');
|
|
497
|
+
|
|
498
|
+
if (start < 0 || end < 0) {
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const parsed = parseSimpleValueExpression(source.slice(start, end + 1));
|
|
503
|
+
|
|
504
|
+
if (Array.isArray(parsed)) {
|
|
505
|
+
objects.set(name, parsed);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const imports = parseImports(source);
|
|
510
|
+
|
|
511
|
+
for (const [localName, imported] of imports) {
|
|
512
|
+
const resolved = resolver(imported.specifier, filePath);
|
|
513
|
+
|
|
514
|
+
if (!resolved) {
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const nestedObjects = parseConstObjects(resolved, resolver, cache);
|
|
519
|
+
const importedValue = nestedObjects.get(imported.imported);
|
|
520
|
+
|
|
521
|
+
if (importedValue) {
|
|
522
|
+
objects.set(localName, importedValue);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
cache.set(filePath, objects);
|
|
527
|
+
|
|
528
|
+
return objects;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function parseSimpleObjectLiteral(source) {
|
|
532
|
+
const object = {};
|
|
533
|
+
const inner = source.trim().replace(/^\{/, '').replace(/\}$/, '');
|
|
534
|
+
|
|
535
|
+
for (const entry of splitTopLevelItems(`[${inner}]`)) {
|
|
536
|
+
const idx = entry.indexOf(':');
|
|
537
|
+
|
|
538
|
+
if (idx < 0) {
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const key = entry.slice(0, idx).trim().replace(/^['"]|['"]$/g, '');
|
|
543
|
+
const valueExpr = entry.slice(idx + 1).trim();
|
|
544
|
+
const value = parseSimpleValueExpression(valueExpr);
|
|
545
|
+
|
|
546
|
+
if (key && value !== null) {
|
|
547
|
+
object[key] = value;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
return object;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function parseSimpleValueExpression(value) {
|
|
555
|
+
const trimmed = value.trim();
|
|
556
|
+
const stringLiteral = parseStringLiteral(trimmed);
|
|
557
|
+
|
|
558
|
+
if (stringLiteral !== null) {
|
|
559
|
+
return stringLiteral;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
|
|
563
|
+
const items = splitTopLevelItems(trimmed)
|
|
564
|
+
.map((item) => parseSimpleValueExpression(item))
|
|
565
|
+
.filter((item) => item !== null);
|
|
566
|
+
|
|
567
|
+
return items.every((item) => typeof item === 'string') ? items : null;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
if (trimmed.startsWith('{') && trimmed.endsWith('}')) {
|
|
571
|
+
return parseSimpleObjectLiteral(trimmed);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
return null;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function parseStringLiteral(value) {
|
|
578
|
+
const trimmed = value.trim();
|
|
579
|
+
|
|
580
|
+
if ((trimmed.startsWith("'") && trimmed.endsWith("'")) || (trimmed.startsWith('"') && trimmed.endsWith('"'))) {
|
|
581
|
+
return trimmed.slice(1, -1);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
return null;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function resolvePathExpression(expr, constObjects) {
|
|
588
|
+
const trimmed = expr.trim();
|
|
589
|
+
const literal = parseStringLiteral(trimmed);
|
|
590
|
+
|
|
591
|
+
if (literal !== null) {
|
|
592
|
+
return literal;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const refMatch = /^(\w+)\.(\w+)$/.exec(trimmed);
|
|
596
|
+
|
|
597
|
+
if (!refMatch) {
|
|
598
|
+
return null;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const object = constObjects.get(refMatch[1]);
|
|
602
|
+
|
|
603
|
+
if (!object) {
|
|
604
|
+
return null;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
return object[refMatch[2]] ?? null;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function resolveConstExpression(expr, constObjects) {
|
|
611
|
+
const trimmed = expr.trim();
|
|
612
|
+
const literal = parseSimpleValueExpression(trimmed);
|
|
613
|
+
|
|
614
|
+
if (literal !== null) {
|
|
615
|
+
return literal;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
if (constObjects.has(trimmed)) {
|
|
619
|
+
return constObjects.get(trimmed);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
const refMatch = /^(\w+)\.(\w+)$/.exec(trimmed);
|
|
623
|
+
|
|
624
|
+
if (!refMatch) {
|
|
625
|
+
return null;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const object = constObjects.get(refMatch[1]);
|
|
629
|
+
|
|
630
|
+
if (!object || typeof object !== 'object') {
|
|
631
|
+
return null;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
return object[refMatch[2]] ?? null;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function normalizePathPath(value) {
|
|
638
|
+
const trimmed = (value ?? '').trim().replace(/^\/+|\/+$/g, '');
|
|
639
|
+
|
|
640
|
+
return trimmed;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function joinRoutePath(parentPath, childPath) {
|
|
644
|
+
const normalizedParent = normalizePathPath(parentPath);
|
|
645
|
+
const normalizedChild = normalizePathPath(childPath);
|
|
646
|
+
|
|
647
|
+
if (!normalizedParent && !normalizedChild) {
|
|
648
|
+
return '/';
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (!normalizedParent) {
|
|
652
|
+
return `/${normalizedChild}`;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
if (!normalizedChild) {
|
|
656
|
+
return `/${normalizedParent}`;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
return `/${normalizedParent}/${normalizedChild}`;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function extractFieldExpression(objectText, fieldName) {
|
|
663
|
+
const inner = objectText.trim().replace(/^\{/, '').replace(/\}$/, '');
|
|
664
|
+
|
|
665
|
+
for (const entry of splitTopLevelItems(`[${inner}]`)) {
|
|
666
|
+
const idx = entry.indexOf(':');
|
|
667
|
+
|
|
668
|
+
if (idx < 0) {
|
|
669
|
+
continue;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
const key = entry.slice(0, idx).trim().replace(/^['"]|['"]$/g, '');
|
|
673
|
+
|
|
674
|
+
if (key === fieldName) {
|
|
675
|
+
return entry.slice(idx + 1).trim();
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
return null;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function extractChildrenArray(objectText) {
|
|
683
|
+
const expr = extractFieldExpression(objectText, 'children');
|
|
684
|
+
|
|
685
|
+
if (!expr || !expr.startsWith('[')) {
|
|
686
|
+
return null;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
return expr;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function extractRouteDataNamespaces(objectText, constObjects, reportContext) {
|
|
693
|
+
const dataExpr = extractFieldExpression(objectText, 'data');
|
|
694
|
+
|
|
695
|
+
if (!dataExpr || !dataExpr.startsWith('{')) {
|
|
696
|
+
return [];
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const namespaces = new Set(extractKeyLiterals(dataExpr).filter(isAcceptedKey).map(namespaceFromKey).filter(Boolean));
|
|
700
|
+
const explicitNamespaces = extractExplicitNamespaceArray(dataExpr, 'presentiaNamespaces', constObjects);
|
|
701
|
+
|
|
702
|
+
if (extractFieldExpression(dataExpr, 'presentiaNamespaces') && !explicitNamespaces.length) {
|
|
703
|
+
generatorReport?.addIssue({
|
|
704
|
+
type: 'unsupported-data-namespaces',
|
|
705
|
+
filePath: reportContext.routesFile,
|
|
706
|
+
routePath: reportContext.fullPath,
|
|
707
|
+
message: 'Could not statically resolve data.presentiaNamespaces',
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
for (const ns of explicitNamespaces) {
|
|
712
|
+
namespaces.add(ns);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
return [...namespaces];
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function extractExplicitNamespaceArray(objectText, fieldName, constObjects) {
|
|
719
|
+
const expr = extractFieldExpression(objectText, fieldName);
|
|
720
|
+
|
|
721
|
+
if (!expr) {
|
|
722
|
+
return [];
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
const resolved = resolveConstExpression(expr, constObjects);
|
|
726
|
+
|
|
727
|
+
if (!Array.isArray(resolved)) {
|
|
728
|
+
return [];
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
return resolved.map((value) => String(value).trim()).filter(isAcceptedNamespace);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function parseLoadComponent(objectText) {
|
|
735
|
+
return parseLazyImportExpression(extractFieldExpression(objectText, 'loadComponent'));
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function parseLoadChildren(objectText) {
|
|
739
|
+
return parseLazyImportExpression(extractFieldExpression(objectText, 'loadChildren'));
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function parseLazyImportExpression(expr) {
|
|
743
|
+
|
|
744
|
+
if (!expr) {
|
|
745
|
+
return null;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
const importMatch = /import\(\s*['"]([^'"]+)['"]\s*\)/.exec(expr);
|
|
749
|
+
const exportMatch = /\.([A-Za-z0-9_]+)\s*\)?\s*$/.exec(expr);
|
|
750
|
+
|
|
751
|
+
if (!importMatch || !exportMatch) {
|
|
752
|
+
return null;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
return { specifier: importMatch[1], exportName: exportMatch[1] };
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function parseRoutes(arrayLiteral, context) {
|
|
759
|
+
const routes = [];
|
|
760
|
+
|
|
761
|
+
for (const objectText of extractObjectLiterals(arrayLiteral)) {
|
|
762
|
+
const pathExpr = extractFieldExpression(objectText, 'path');
|
|
763
|
+
const redirectExpr = extractFieldExpression(objectText, 'redirectTo');
|
|
764
|
+
const pathValue = pathExpr ? resolvePathExpression(pathExpr, context.constObjects) : null;
|
|
765
|
+
const ownPath = normalizePathPath(pathValue ?? '');
|
|
766
|
+
const fullPath = joinRoutePath(context.parentPath, ownPath);
|
|
767
|
+
const routeDataNamespaces = extractRouteDataNamespaces(objectText, context.constObjects, {
|
|
768
|
+
routesFile: context.routesFile,
|
|
769
|
+
fullPath,
|
|
770
|
+
});
|
|
771
|
+
const componentRefs = [];
|
|
772
|
+
const componentExpr = extractFieldExpression(objectText, 'component');
|
|
773
|
+
|
|
774
|
+
if (componentExpr) {
|
|
775
|
+
const localName = componentExpr.trim();
|
|
776
|
+
const imported = context.imports.get(localName);
|
|
777
|
+
|
|
778
|
+
if (imported) {
|
|
779
|
+
const resolved = context.resolver(imported.specifier, context.routesFile);
|
|
780
|
+
|
|
781
|
+
if (!resolved) {
|
|
782
|
+
generatorReport?.addIssue({
|
|
783
|
+
type: 'unresolved-component',
|
|
784
|
+
filePath: context.routesFile,
|
|
785
|
+
routePath: fullPath,
|
|
786
|
+
message: `Could not resolve component import "${imported.specifier}"`,
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
componentRefs.push({
|
|
791
|
+
filePath: resolved,
|
|
792
|
+
exportName: imported.imported,
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
const lazyComponent = parseLoadComponent(objectText);
|
|
798
|
+
if (lazyComponent) {
|
|
799
|
+
const resolved = context.resolver(lazyComponent.specifier, context.routesFile);
|
|
800
|
+
|
|
801
|
+
if (!resolved) {
|
|
802
|
+
generatorReport?.addIssue({
|
|
803
|
+
type: 'unresolved-load-component',
|
|
804
|
+
filePath: context.routesFile,
|
|
805
|
+
routePath: fullPath,
|
|
806
|
+
message: `Could not resolve loadComponent import "${lazyComponent.specifier}"`,
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
componentRefs.push({
|
|
811
|
+
filePath: resolved,
|
|
812
|
+
exportName: lazyComponent.exportName,
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
const lazyChildren = parseLoadChildren(objectText);
|
|
817
|
+
|
|
818
|
+
const componentNamespaces = componentRefs.flatMap((componentRef) =>
|
|
819
|
+
componentRef.filePath ? collectComponentNamespaces(componentRef.filePath, componentRef.exportName, context) : [],
|
|
820
|
+
);
|
|
821
|
+
const inheritedNamespaces = unique([...context.inheritedNamespaces, ...componentNamespaces, ...routeDataNamespaces]);
|
|
822
|
+
const childrenArray = extractChildrenArray(objectText);
|
|
823
|
+
|
|
824
|
+
if (childrenArray) {
|
|
825
|
+
routes.push(
|
|
826
|
+
...parseRoutes(childrenArray, {
|
|
827
|
+
...context,
|
|
828
|
+
parentPath: fullPath,
|
|
829
|
+
inheritedNamespaces,
|
|
830
|
+
}),
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
if (lazyChildren) {
|
|
835
|
+
const childRoutesFile = context.resolver(lazyChildren.specifier, context.routesFile);
|
|
836
|
+
|
|
837
|
+
if (childRoutesFile) {
|
|
838
|
+
routes.push(
|
|
839
|
+
...parseRoutesFromExport(childRoutesFile, lazyChildren.exportName, {
|
|
840
|
+
...context,
|
|
841
|
+
inheritedNamespaces,
|
|
842
|
+
parentPath: fullPath,
|
|
843
|
+
routesFile: childRoutesFile,
|
|
844
|
+
}),
|
|
845
|
+
);
|
|
846
|
+
} else {
|
|
847
|
+
generatorReport?.addIssue({
|
|
848
|
+
type: 'unresolved-load-children',
|
|
849
|
+
filePath: context.routesFile,
|
|
850
|
+
routePath: fullPath,
|
|
851
|
+
message: `Could not resolve loadChildren import "${lazyChildren.specifier}"`,
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
const isConcreteRoute = !redirectExpr && (componentRefs.length > 0 || routeDataNamespaces.length > 0);
|
|
857
|
+
|
|
858
|
+
if (isConcreteRoute) {
|
|
859
|
+
routes.push({
|
|
860
|
+
path: fullPath,
|
|
861
|
+
namespaces: inheritedNamespaces,
|
|
862
|
+
});
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
return routes;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
function parseRoutesFromExport(filePath, exportName, context) {
|
|
870
|
+
const visitKey = `${filePath}#${exportName}#${context.parentPath}`;
|
|
871
|
+
|
|
872
|
+
if (visitedRouteExports.has(visitKey)) {
|
|
873
|
+
return [];
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
visitedRouteExports.add(visitKey);
|
|
877
|
+
|
|
878
|
+
const source = stripComments(readText(filePath));
|
|
879
|
+
const arrayLiteral =
|
|
880
|
+
extractArrayLiteral(source, new RegExp(`\\bexport\\s+const\\s+${exportName}\\s*:\\s*[^=]+=\\s*\\[`, 'g')) ??
|
|
881
|
+
extractArrayLiteral(source, new RegExp(`\\bexport\\s+const\\s+${exportName}\\s*=\\s*\\[`, 'g'));
|
|
882
|
+
|
|
883
|
+
if (!arrayLiteral) {
|
|
884
|
+
return [];
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
return parseRoutes(arrayLiteral, {
|
|
888
|
+
...context,
|
|
889
|
+
constObjects: parseConstObjects(filePath, context.resolver),
|
|
890
|
+
imports: parseImports(source),
|
|
891
|
+
routesFile: filePath,
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
function collectComponentNamespaces(filePath, exportName, context) {
|
|
896
|
+
const key = `${filePath}#${exportName}`;
|
|
897
|
+
|
|
898
|
+
if (visitedFiles.has(key) || !filePath || !fs.existsSync(filePath)) {
|
|
899
|
+
return [];
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
visitedFiles.add(key);
|
|
903
|
+
|
|
904
|
+
const source = stripComments(readText(filePath));
|
|
905
|
+
const imports = parseImports(source);
|
|
906
|
+
const namespaces = new Set(collectNamespacesFromCode(source));
|
|
907
|
+
|
|
908
|
+
for (const htmlContent of extractTemplateContents(source, filePath)) {
|
|
909
|
+
for (const ns of collectNamespacesFromTemplate(htmlContent)) {
|
|
910
|
+
namespaces.add(ns);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
for (const importedName of extractStandaloneImports(source)) {
|
|
915
|
+
const imported = imports.get(importedName);
|
|
916
|
+
|
|
917
|
+
if (!imported) {
|
|
918
|
+
generatorReport?.addIssue({
|
|
919
|
+
type: 'unsupported-standalone-import',
|
|
920
|
+
filePath,
|
|
921
|
+
exportName,
|
|
922
|
+
message: `Could not resolve standalone import "${importedName}" from component imports`,
|
|
923
|
+
});
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
const resolved = context.resolver(imported.specifier, filePath);
|
|
928
|
+
|
|
929
|
+
if (!resolved || !resolved.startsWith(projectRoot)) {
|
|
930
|
+
continue;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
for (const ns of collectComponentNamespaces(resolved, imported.imported, context)) {
|
|
934
|
+
namespaces.add(ns);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
return [...namespaces];
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
function extractTemplateContents(source, filePath) {
|
|
942
|
+
const contents = [];
|
|
943
|
+
const templateUrlRe = /templateUrl\s*:\s*['"]([^'"]+)['"]/g;
|
|
944
|
+
let match;
|
|
945
|
+
|
|
946
|
+
while ((match = templateUrlRe.exec(source))) {
|
|
947
|
+
const resolved = resolveFile(path.resolve(path.dirname(filePath), match[1]));
|
|
948
|
+
|
|
949
|
+
if (resolved) {
|
|
950
|
+
contents.push(readText(resolved));
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
const templateRe = /template\s*:\s*(`[\s\S]*?`|'(?:\\.|[^'])*'|"(?:\\.|[^"])*")/g;
|
|
955
|
+
|
|
956
|
+
while ((match = templateRe.exec(source))) {
|
|
957
|
+
const raw = match[1];
|
|
958
|
+
|
|
959
|
+
if (raw.startsWith('`')) {
|
|
960
|
+
contents.push(raw.slice(1, -1));
|
|
961
|
+
} else {
|
|
962
|
+
contents.push(raw.slice(1, -1));
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
return contents;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
function extractStandaloneImports(source) {
|
|
970
|
+
const importsArray = extractArrayLiteral(source, /\bimports\s*:/g);
|
|
971
|
+
|
|
972
|
+
if (!importsArray) {
|
|
973
|
+
return [];
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
return splitTopLevelItems(importsArray)
|
|
977
|
+
.map((item) => item.replace(/\s/g, ''))
|
|
978
|
+
.filter((item) => /^[A-Za-z_][A-Za-z0-9_]*$/.test(item));
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
function collectNamespacesFromTemplate(content) {
|
|
982
|
+
const namespaces = new Set();
|
|
983
|
+
const langPipeRe = /['"`]([A-Za-z0-9_.-]+)['"`]\s*\|\s*lang\b/g;
|
|
984
|
+
const methodLikeRe = /\[\s*reLang(?:Key)?\s*\]\s*=\s*['"`]([^'"`]+)['"`]/g;
|
|
985
|
+
const tagWithReLangRe = /<[^>]*\breLang\b[^>]*>/g;
|
|
986
|
+
const reLangAttrsBindings = extractBindingExpressions(content, 'reLangAttrs');
|
|
987
|
+
const reLangConfigBindings = extractBindingExpressions(content, 'reLang');
|
|
988
|
+
let match;
|
|
989
|
+
|
|
990
|
+
while ((match = langPipeRe.exec(content))) {
|
|
991
|
+
const ns = acceptedNamespaceFromKey(match[1]);
|
|
992
|
+
if (ns) namespaces.add(ns);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
while ((match = methodLikeRe.exec(content))) {
|
|
996
|
+
const ns = acceptedNamespaceFromKey(match[1]);
|
|
997
|
+
if (ns) namespaces.add(ns);
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
while ((match = tagWithReLangRe.exec(content))) {
|
|
1001
|
+
for (const key of extractKeyLiterals(match[0])) {
|
|
1002
|
+
const ns = acceptedNamespaceFromKey(key);
|
|
1003
|
+
if (ns) namespaces.add(ns);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
for (const expr of reLangAttrsBindings) {
|
|
1008
|
+
const parsed = parseSimpleValueExpression(expr);
|
|
1009
|
+
|
|
1010
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
1011
|
+
continue;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
for (const value of Object.values(parsed)) {
|
|
1015
|
+
if (typeof value !== 'string') {
|
|
1016
|
+
continue;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
const ns = acceptedNamespaceFromKey(value);
|
|
1020
|
+
if (ns) namespaces.add(ns);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
for (const expr of reLangConfigBindings) {
|
|
1025
|
+
const parsed = parseSimpleValueExpression(expr);
|
|
1026
|
+
|
|
1027
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
1028
|
+
continue;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
if (typeof parsed.textKey === 'string') {
|
|
1032
|
+
const ns = acceptedNamespaceFromKey(parsed.textKey);
|
|
1033
|
+
if (ns) namespaces.add(ns);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
if (parsed.attrs && typeof parsed.attrs === 'object') {
|
|
1037
|
+
for (const value of Object.values(parsed.attrs)) {
|
|
1038
|
+
if (typeof value !== 'string') {
|
|
1039
|
+
continue;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
const ns = acceptedNamespaceFromKey(value);
|
|
1043
|
+
if (ns) namespaces.add(ns);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
return [...namespaces];
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
function extractBindingExpressions(content, bindingName) {
|
|
1052
|
+
const out = [];
|
|
1053
|
+
const re = new RegExp(`\\[\\s*${bindingName}\\s*\\]\\s*=\\s*([\"'])([\\s\\S]*?)\\1`, 'g');
|
|
1054
|
+
let match;
|
|
1055
|
+
|
|
1056
|
+
while ((match = re.exec(content))) {
|
|
1057
|
+
out.push(match[2]);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
return out;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
function collectNamespacesFromCode(content) {
|
|
1064
|
+
const namespaces = new Set();
|
|
1065
|
+
const methodRe = /\.\s*(?:get|observe|translate)\s*\(\s*['"`]([^'"`]+)['"`]/g;
|
|
1066
|
+
let match;
|
|
1067
|
+
|
|
1068
|
+
while ((match = methodRe.exec(content))) {
|
|
1069
|
+
const ns = acceptedNamespaceFromKey(match[1]);
|
|
1070
|
+
if (ns) namespaces.add(ns);
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
return [...namespaces];
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
function extractKeyLiterals(content) {
|
|
1077
|
+
const out = [];
|
|
1078
|
+
const re = /['"`]([A-Za-z0-9_.-]+)['"`]/g;
|
|
1079
|
+
let match;
|
|
1080
|
+
|
|
1081
|
+
while ((match = re.exec(content))) {
|
|
1082
|
+
if (KEY_RE.test(match[1])) {
|
|
1083
|
+
out.push(match[1]);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
return out;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
function namespaceFromKey(key) {
|
|
1091
|
+
if (!KEY_RE.test(key)) {
|
|
1092
|
+
return null;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
const [ns] = key.split('.', 1);
|
|
1096
|
+
|
|
1097
|
+
return ns || null;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
function acceptedNamespaceFromKey(key) {
|
|
1101
|
+
if (!isAcceptedKey(key)) {
|
|
1102
|
+
return null;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
return namespaceFromKey(key);
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
function isAcceptedKey(key) {
|
|
1109
|
+
if (!KEY_RE.test(key)) {
|
|
1110
|
+
return false;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
if (!localeValidator) {
|
|
1114
|
+
return true;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
return localeValidator.hasKey(key);
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
function isAcceptedNamespace(ns) {
|
|
1121
|
+
if (!ns) {
|
|
1122
|
+
return false;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
if (!localeValidator) {
|
|
1126
|
+
return true;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
return localeValidator.hasNamespace(ns);
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
function unique(values) {
|
|
1133
|
+
return [...new Set(values.filter(Boolean))];
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
function toManifest(routes) {
|
|
1137
|
+
const manifest = new Map();
|
|
1138
|
+
|
|
1139
|
+
for (const route of routes) {
|
|
1140
|
+
if (!route.namespaces.length) {
|
|
1141
|
+
continue;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
const existing = manifest.get(route.path) ?? [];
|
|
1145
|
+
|
|
1146
|
+
manifest.set(route.path, unique([...existing, ...route.namespaces]).sort());
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
return manifest;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
function formatManifest(manifest, name) {
|
|
1153
|
+
const entries = [...manifest.entries()].sort(([a], [b]) => a.localeCompare(b));
|
|
1154
|
+
const lines = entries.map(([routePath, namespaces]) => {
|
|
1155
|
+
const nsList = namespaces.map((ns) => `'${ns}'`).join(', ');
|
|
1156
|
+
|
|
1157
|
+
return ` '${routePath}': [${nsList}],`;
|
|
1158
|
+
});
|
|
1159
|
+
|
|
1160
|
+
return `/* auto-generated by presentia-gen-namespaces */\nexport const ${name} = {\n${lines.join('\n')}\n} as const;\n`;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
function formatReport(report) {
|
|
1164
|
+
return JSON.stringify(report, null, 2);
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
export function generateNamespaceManifest(cliArgs = process.argv.slice(2), currentWorkingDir = process.cwd()) {
|
|
1168
|
+
const routesFile = path.resolve(currentWorkingDir, getArg(cliArgs, '--routes', 'src/app/app.routes.ts'));
|
|
1169
|
+
const outputFile = path.resolve(currentWorkingDir, getArg(cliArgs, '--out', 'src/app/presentia-route-namespaces.ts'));
|
|
1170
|
+
const tsconfigFile = path.resolve(currentWorkingDir, getArg(cliArgs, '--tsconfig', 'tsconfig.json'));
|
|
1171
|
+
const localesDirArg = getArg(cliArgs, '--locales', '');
|
|
1172
|
+
const exportName = getArg(cliArgs, '--name', 'PRESENTIA_ROUTE_NAMESPACES');
|
|
1173
|
+
const reportFileArg = getArg(cliArgs, '--report', '');
|
|
1174
|
+
const printReport = cliArgs.includes('--print-report');
|
|
1175
|
+
|
|
1176
|
+
projectRoot = path.resolve(currentWorkingDir, getArg(cliArgs, '--project', path.dirname(routesFile)));
|
|
1177
|
+
localeValidator = createLocaleValidator(localesDirArg ? path.resolve(currentWorkingDir, localesDirArg) : null);
|
|
1178
|
+
generatorReport = createReport();
|
|
1179
|
+
|
|
1180
|
+
visitedFiles.clear();
|
|
1181
|
+
visitedRouteExports.clear();
|
|
1182
|
+
|
|
1183
|
+
if (!fs.existsSync(routesFile)) {
|
|
1184
|
+
fail(`routes file not found: ${routesFile}`);
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
const tsconfig = loadTsconfig(tsconfigFile);
|
|
1188
|
+
const resolver = createPathResolver(routesFile, tsconfig);
|
|
1189
|
+
const routesSource = stripComments(readText(routesFile));
|
|
1190
|
+
const routesArray = extractArrayLiteral(routesSource, /\b(?:const|let|var)\s+\w+\s*:\s*[^=]+=\s*\[/) ??
|
|
1191
|
+
extractArrayLiteral(routesSource, /\b(?:const|let|var)\s+\w+\s*=\s*\[/);
|
|
1192
|
+
|
|
1193
|
+
if (!routesArray) {
|
|
1194
|
+
fail(`unable to find routes array in: ${routesFile}`);
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
const constObjects = parseConstObjects(routesFile, resolver);
|
|
1198
|
+
const imports = parseImports(routesSource);
|
|
1199
|
+
const routes = parseRoutes(routesArray, {
|
|
1200
|
+
constObjects,
|
|
1201
|
+
imports,
|
|
1202
|
+
inheritedNamespaces: [],
|
|
1203
|
+
parentPath: '',
|
|
1204
|
+
resolver,
|
|
1205
|
+
routesFile,
|
|
1206
|
+
});
|
|
1207
|
+
const manifest = toManifest(routes);
|
|
1208
|
+
const content = formatManifest(manifest, exportName);
|
|
1209
|
+
const report = {
|
|
1210
|
+
routesFile,
|
|
1211
|
+
outputFile,
|
|
1212
|
+
routeCount: manifest.size,
|
|
1213
|
+
issueCount: generatorReport.issues.length,
|
|
1214
|
+
issues: generatorReport.issues,
|
|
1215
|
+
};
|
|
1216
|
+
|
|
1217
|
+
fs.mkdirSync(path.dirname(outputFile), { recursive: true });
|
|
1218
|
+
fs.writeFileSync(outputFile, content, 'utf8');
|
|
1219
|
+
|
|
1220
|
+
if (reportFileArg) {
|
|
1221
|
+
const reportFile = path.resolve(currentWorkingDir, reportFileArg);
|
|
1222
|
+
|
|
1223
|
+
fs.mkdirSync(path.dirname(reportFile), { recursive: true });
|
|
1224
|
+
fs.writeFileSync(reportFile, formatReport(report), 'utf8');
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
if (printReport && report.issueCount) {
|
|
1228
|
+
console.warn(formatReport(report));
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
return { manifest, content, outputFile, report };
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
const isCliEntrypoint = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
1235
|
+
|
|
1236
|
+
if (isCliEntrypoint) {
|
|
1237
|
+
try {
|
|
1238
|
+
const { manifest, outputFile, report } = generateNamespaceManifest();
|
|
1239
|
+
|
|
1240
|
+
console.log(`[presentia-gen-namespaces] Generated ${manifest.size} routes -> ${outputFile}`);
|
|
1241
|
+
if (report.issueCount) {
|
|
1242
|
+
console.warn(`[presentia-gen-namespaces] Reported ${report.issueCount} static-analysis issue(s)`);
|
|
1243
|
+
}
|
|
1244
|
+
} catch (error) {
|
|
1245
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
1246
|
+
process.exit(1);
|
|
1247
|
+
}
|
|
1248
|
+
}
|