@vobs/cli 0.1.0 → 0.3.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/dist/ai-server.d.ts +11 -0
- package/dist/ai-server.js +212 -0
- package/dist/context.d.ts +12 -0
- package/dist/context.js +301 -0
- package/dist/diagnose.d.ts +12 -0
- package/dist/diagnose.js +122 -0
- package/dist/enterprise-template.d.ts +4 -4
- package/dist/enterprise-template.js +31 -49
- package/dist/index.d.ts +2 -1
- package/dist/index.js +245 -3
- package/dist/inspect.d.ts +11 -0
- package/dist/inspect.js +576 -0
- package/dist/scaffold.js +16 -17
- package/package.json +13 -11
package/dist/inspect.js
ADDED
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
/** @license MIT
|
|
2
|
+
* Copyright (c) 2026 vobsjs
|
|
3
|
+
* @vobs/cli
|
|
4
|
+
*/
|
|
5
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
6
|
+
import * as path from 'node:path';
|
|
7
|
+
import * as ts from 'typescript';
|
|
8
|
+
import { analyzeView, checkViewProject } from '@vobs/compiler-dom';
|
|
9
|
+
const ignoredDirectories = new Set(['.git', '.cache', '.tmp', 'coverage', 'dist', 'node_modules']);
|
|
10
|
+
export function inspectVobsProject(options) {
|
|
11
|
+
const tsconfigPath = path.resolve(options.tsconfigPath);
|
|
12
|
+
const projectRoot = path.dirname(tsconfigPath);
|
|
13
|
+
const workspaceRoot = findWorkspaceRoot(projectRoot);
|
|
14
|
+
const rootPackage = readJsonFile(path.join(workspaceRoot, 'package.json'));
|
|
15
|
+
const projectPackage = readJsonFile(path.join(projectRoot, 'package.json'));
|
|
16
|
+
const config = readTsConfig(tsconfigPath, projectRoot);
|
|
17
|
+
const files = collectProjectFiles(projectRoot);
|
|
18
|
+
const routeRoot = path.join(projectRoot, 'src', 'pages');
|
|
19
|
+
const routes = collectRoutes(routeRoot, projectRoot);
|
|
20
|
+
const checkResult = checkViewProject({ tsconfigPath });
|
|
21
|
+
return {
|
|
22
|
+
schemaVersion: '0.1',
|
|
23
|
+
generatedBy: 'vobs inspect',
|
|
24
|
+
workspace: {
|
|
25
|
+
root: manifestPath(workspaceRoot, workspaceRoot),
|
|
26
|
+
...(typeof rootPackage?.packageManager === 'string'
|
|
27
|
+
? { packageManager: rootPackage.packageManager }
|
|
28
|
+
: {}),
|
|
29
|
+
},
|
|
30
|
+
project: {
|
|
31
|
+
root: manifestPath(projectRoot, workspaceRoot),
|
|
32
|
+
...(typeof projectPackage?.name === 'string' ? { name: projectPackage.name } : {}),
|
|
33
|
+
...(typeof projectPackage?.name === 'string' ? { packageName: projectPackage.name } : {}),
|
|
34
|
+
...(typeof projectPackage?.version === 'string'
|
|
35
|
+
? { packageVersion: projectPackage.version }
|
|
36
|
+
: {}),
|
|
37
|
+
},
|
|
38
|
+
tsconfig: {
|
|
39
|
+
path: manifestPath(tsconfigPath, workspaceRoot),
|
|
40
|
+
rootNames: config.fileNames.map((fileName) => manifestPath(fileName, workspaceRoot)),
|
|
41
|
+
references: (config.projectReferences ?? []).map((reference) => manifestPath(reference.path, workspaceRoot)),
|
|
42
|
+
},
|
|
43
|
+
packages: collectPackages(workspaceRoot),
|
|
44
|
+
files: {
|
|
45
|
+
views: files.views.map((view) => manifestPath(view.file, workspaceRoot)),
|
|
46
|
+
routes: routes.map((route) => toManifestRoute(route, workspaceRoot)),
|
|
47
|
+
components: files.components.map((component) => toManifestComponent(component, workspaceRoot)),
|
|
48
|
+
globalComponents: checkResult.globalComponentTags ?? [],
|
|
49
|
+
viewContracts: files.views.map((view) => toManifestView(view, workspaceRoot)),
|
|
50
|
+
authorization: files.authorization.map((file) => toManifestPermission(file, workspaceRoot)),
|
|
51
|
+
},
|
|
52
|
+
diagnostics: checkResult.diagnostics.map((diagnostic) => toManifestDiagnostic(diagnostic, workspaceRoot)),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export function writeInspectOutput(manifest, outputPath) {
|
|
56
|
+
const workingDirectory = path.resolve(process.cwd());
|
|
57
|
+
const target = path.resolve(workingDirectory, outputPath);
|
|
58
|
+
const relativeTarget = path.relative(workingDirectory, target);
|
|
59
|
+
if (relativeTarget === '' ||
|
|
60
|
+
relativeTarget.startsWith(`..${path.sep}`) ||
|
|
61
|
+
path.isAbsolute(relativeTarget)) {
|
|
62
|
+
throw new Error(`Refusing to write inspect output outside the current directory: ${target}`);
|
|
63
|
+
}
|
|
64
|
+
const fileName = path.basename(target).toLowerCase();
|
|
65
|
+
if (fileName === '' || /(?:^|[._-])(?:env|secret|token|credential|password)/u.test(fileName)) {
|
|
66
|
+
throw new Error(`Refusing to write inspect output to a sensitive filename: ${target}`);
|
|
67
|
+
}
|
|
68
|
+
mkdirSync(path.dirname(target), { recursive: true });
|
|
69
|
+
writeFileSync(target, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
|
|
70
|
+
}
|
|
71
|
+
function collectProjectFiles(root) {
|
|
72
|
+
const views = [];
|
|
73
|
+
const components = [];
|
|
74
|
+
const authorization = [];
|
|
75
|
+
for (const fileName of walkFiles(root)) {
|
|
76
|
+
if (fileName.endsWith('.html')) {
|
|
77
|
+
views.push({ file: fileName, ...analyzeView(readFileSync(fileName, 'utf8'), fileName) });
|
|
78
|
+
}
|
|
79
|
+
if (!isTypeScriptFile(fileName))
|
|
80
|
+
continue;
|
|
81
|
+
const source = readFileSync(fileName, 'utf8');
|
|
82
|
+
const component = readComponentFile(fileName, source);
|
|
83
|
+
if (component !== undefined)
|
|
84
|
+
components.push(component);
|
|
85
|
+
const permission = readPermissionFile(fileName, source);
|
|
86
|
+
if (permission !== undefined)
|
|
87
|
+
authorization.push(permission);
|
|
88
|
+
}
|
|
89
|
+
views.sort((left, right) => left.file.localeCompare(right.file));
|
|
90
|
+
components.sort((left, right) => left.file.localeCompare(right.file));
|
|
91
|
+
authorization.sort((left, right) => left.file.localeCompare(right.file));
|
|
92
|
+
return { views, components, authorization };
|
|
93
|
+
}
|
|
94
|
+
function readComponentFile(fileName, source) {
|
|
95
|
+
const sourceFile = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true);
|
|
96
|
+
const definition = findDefinitionCall(sourceFile);
|
|
97
|
+
if (definition === undefined)
|
|
98
|
+
return undefined;
|
|
99
|
+
const viewRequest = readHtmlImport(sourceFile);
|
|
100
|
+
const view = viewRequest === undefined ? undefined : path.resolve(path.dirname(fileName), viewRequest);
|
|
101
|
+
return {
|
|
102
|
+
kind: definition.kind,
|
|
103
|
+
file: fileName,
|
|
104
|
+
...(view === undefined ? {} : { view }),
|
|
105
|
+
exports: readExportNames(source),
|
|
106
|
+
props: readStaticArrayProperty(definition.options, 'props'),
|
|
107
|
+
emits: readStaticArrayProperty(definition.options, 'emits'),
|
|
108
|
+
exposed: readSetupExposedNames(definition.options, sourceFile),
|
|
109
|
+
registeredComponents: readStaticObjectKeys(definition.options, 'components'),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function findDefinitionCall(sourceFile) {
|
|
113
|
+
let result;
|
|
114
|
+
const visit = (node) => {
|
|
115
|
+
if (result === undefined &&
|
|
116
|
+
ts.isCallExpression(node) &&
|
|
117
|
+
ts.isIdentifier(node.expression) &&
|
|
118
|
+
(node.expression.text === 'definePage' || node.expression.text === 'defineComponent')) {
|
|
119
|
+
const options = node.arguments[0];
|
|
120
|
+
if (options !== undefined && ts.isObjectLiteralExpression(options)) {
|
|
121
|
+
result = {
|
|
122
|
+
kind: node.expression.text === 'definePage' ? 'page' : 'component',
|
|
123
|
+
options,
|
|
124
|
+
};
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
ts.forEachChild(node, visit);
|
|
129
|
+
};
|
|
130
|
+
visit(sourceFile);
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
function readHtmlImport(sourceFile) {
|
|
134
|
+
for (const statement of sourceFile.statements) {
|
|
135
|
+
if (ts.isImportDeclaration(statement) &&
|
|
136
|
+
ts.isStringLiteral(statement.moduleSpecifier) &&
|
|
137
|
+
statement.moduleSpecifier.text.endsWith('.html')) {
|
|
138
|
+
return statement.moduleSpecifier.text;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
function readStaticArrayProperty(options, name) {
|
|
144
|
+
const expression = readPropertyExpression(options, name);
|
|
145
|
+
const array = expression === undefined ? undefined : unwrapExpression(expression);
|
|
146
|
+
if (array === undefined || !ts.isArrayLiteralExpression(array))
|
|
147
|
+
return [];
|
|
148
|
+
return array.elements
|
|
149
|
+
.map((element) => (ts.isStringLiteral(element) ? element.text : undefined))
|
|
150
|
+
.filter((value) => value !== undefined)
|
|
151
|
+
.sort();
|
|
152
|
+
}
|
|
153
|
+
function readStaticObjectKeys(options, name) {
|
|
154
|
+
const expression = readPropertyExpression(options, name);
|
|
155
|
+
const object = expression === undefined ? undefined : unwrapExpression(expression);
|
|
156
|
+
if (object === undefined || !ts.isObjectLiteralExpression(object))
|
|
157
|
+
return [];
|
|
158
|
+
return object.properties
|
|
159
|
+
.map(readObjectElementName)
|
|
160
|
+
.filter((value) => value !== undefined)
|
|
161
|
+
.sort();
|
|
162
|
+
}
|
|
163
|
+
function readSetupExposedNames(options, sourceFile) {
|
|
164
|
+
const setup = findSetupFunction(options, sourceFile);
|
|
165
|
+
const body = setup !== undefined && 'body' in setup ? setup.body : undefined;
|
|
166
|
+
if (body === undefined)
|
|
167
|
+
return [];
|
|
168
|
+
const names = new Set();
|
|
169
|
+
if (!ts.isBlock(body)) {
|
|
170
|
+
const expression = unwrapExpression(body);
|
|
171
|
+
if (ts.isObjectLiteralExpression(expression))
|
|
172
|
+
collectObjectKeys(expression, names);
|
|
173
|
+
}
|
|
174
|
+
const visit = (node) => {
|
|
175
|
+
if (node !== body && ts.isFunctionLike(node))
|
|
176
|
+
return;
|
|
177
|
+
if (ts.isReturnStatement(node) && node.expression !== undefined) {
|
|
178
|
+
const expression = unwrapExpression(node.expression);
|
|
179
|
+
if (ts.isObjectLiteralExpression(expression))
|
|
180
|
+
collectObjectKeys(expression, names);
|
|
181
|
+
}
|
|
182
|
+
if (ts.isCallExpression(node) && isExposeCall(node.expression)) {
|
|
183
|
+
const argument = node.arguments[0];
|
|
184
|
+
if (argument !== undefined) {
|
|
185
|
+
const expression = unwrapExpression(argument);
|
|
186
|
+
if (ts.isObjectLiteralExpression(expression))
|
|
187
|
+
collectObjectKeys(expression, names);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
ts.forEachChild(node, visit);
|
|
191
|
+
};
|
|
192
|
+
if (ts.isBlock(body))
|
|
193
|
+
visit(body);
|
|
194
|
+
return [...names].sort();
|
|
195
|
+
}
|
|
196
|
+
function findSetupFunction(options, sourceFile) {
|
|
197
|
+
for (const property of options.properties) {
|
|
198
|
+
if (readPropertyName(property.name) !== 'setup')
|
|
199
|
+
continue;
|
|
200
|
+
if (ts.isMethodDeclaration(property))
|
|
201
|
+
return property;
|
|
202
|
+
if (ts.isShorthandPropertyAssignment(property)) {
|
|
203
|
+
return findTopLevelFunction(sourceFile, property.name.text);
|
|
204
|
+
}
|
|
205
|
+
if (ts.isPropertyAssignment(property)) {
|
|
206
|
+
const initializer = unwrapExpression(property.initializer);
|
|
207
|
+
if (ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer)) {
|
|
208
|
+
return initializer;
|
|
209
|
+
}
|
|
210
|
+
if (ts.isIdentifier(initializer))
|
|
211
|
+
return findTopLevelFunction(sourceFile, initializer.text);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
function findTopLevelFunction(sourceFile, name) {
|
|
217
|
+
for (const statement of sourceFile.statements) {
|
|
218
|
+
if (ts.isFunctionDeclaration(statement) && statement.name?.text === name)
|
|
219
|
+
return statement;
|
|
220
|
+
if (!ts.isVariableStatement(statement))
|
|
221
|
+
continue;
|
|
222
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
223
|
+
if (!ts.isIdentifier(declaration.name) || declaration.name.text !== name)
|
|
224
|
+
continue;
|
|
225
|
+
const initializer = declaration.initializer === undefined
|
|
226
|
+
? undefined
|
|
227
|
+
: unwrapExpression(declaration.initializer);
|
|
228
|
+
if (initializer !== undefined &&
|
|
229
|
+
(ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer))) {
|
|
230
|
+
return initializer;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
function isExposeCall(expression) {
|
|
237
|
+
return ((ts.isIdentifier(expression) && expression.text === 'expose') ||
|
|
238
|
+
(ts.isPropertyAccessExpression(expression) && expression.name.text === 'expose'));
|
|
239
|
+
}
|
|
240
|
+
function collectObjectKeys(object, names) {
|
|
241
|
+
for (const property of object.properties) {
|
|
242
|
+
const name = readObjectElementName(property);
|
|
243
|
+
if (name !== undefined)
|
|
244
|
+
names.add(name);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function readPropertyExpression(object, name) {
|
|
248
|
+
for (const property of object.properties) {
|
|
249
|
+
if (readPropertyName(property.name) !== name)
|
|
250
|
+
continue;
|
|
251
|
+
if (ts.isPropertyAssignment(property))
|
|
252
|
+
return property.initializer;
|
|
253
|
+
}
|
|
254
|
+
return undefined;
|
|
255
|
+
}
|
|
256
|
+
function readPropertyName(name) {
|
|
257
|
+
if (name === undefined)
|
|
258
|
+
return undefined;
|
|
259
|
+
if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) {
|
|
260
|
+
return name.text;
|
|
261
|
+
}
|
|
262
|
+
return undefined;
|
|
263
|
+
}
|
|
264
|
+
function readObjectElementName(property) {
|
|
265
|
+
return ts.isSpreadAssignment(property) ? undefined : readPropertyName(property.name);
|
|
266
|
+
}
|
|
267
|
+
function unwrapExpression(expression) {
|
|
268
|
+
let current = expression;
|
|
269
|
+
while (ts.isParenthesizedExpression(current) ||
|
|
270
|
+
ts.isAsExpression(current) ||
|
|
271
|
+
ts.isSatisfiesExpression(current) ||
|
|
272
|
+
ts.isNonNullExpression(current)) {
|
|
273
|
+
current = current.expression;
|
|
274
|
+
}
|
|
275
|
+
return current;
|
|
276
|
+
}
|
|
277
|
+
function readPermissionFile(fileName, source) {
|
|
278
|
+
const exportedSymbols = readExportNames(source);
|
|
279
|
+
const kinds = [];
|
|
280
|
+
const authorizerOptions = readAuthorizerOptionNames(fileName, source);
|
|
281
|
+
if (authorizerOptions !== undefined ||
|
|
282
|
+
exportedSymbols.some((name) => /authorization|authorizer|permission/iu.test(name))) {
|
|
283
|
+
kinds.push('authorization');
|
|
284
|
+
}
|
|
285
|
+
if (authorizerOptions?.has('roles') === true ||
|
|
286
|
+
exportedSymbols.some((name) => /^roles?$/iu.test(name) || /^AuthorizationRole/u.test(name))) {
|
|
287
|
+
kinds.push('role');
|
|
288
|
+
}
|
|
289
|
+
if (authorizerOptions?.has('policies') === true ||
|
|
290
|
+
exportedSymbols.some((name) => /^polic(?:y|ies)$/iu.test(name) || /^AuthorizationPolicy/u.test(name))) {
|
|
291
|
+
kinds.push('policy');
|
|
292
|
+
}
|
|
293
|
+
if (kinds.length === 0)
|
|
294
|
+
return undefined;
|
|
295
|
+
return {
|
|
296
|
+
file: fileName,
|
|
297
|
+
kinds,
|
|
298
|
+
symbols: exportedSymbols.filter((name) => /authorization|authorizer|permission|roles?|polic(?:y|ies)/iu.test(name)),
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
function readAuthorizerOptionNames(fileName, source) {
|
|
302
|
+
const sourceFile = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true);
|
|
303
|
+
let options;
|
|
304
|
+
const visit = (node) => {
|
|
305
|
+
if (options === undefined &&
|
|
306
|
+
ts.isCallExpression(node) &&
|
|
307
|
+
ts.isIdentifier(node.expression) &&
|
|
308
|
+
node.expression.text === 'createAuthorizer') {
|
|
309
|
+
options = new Set();
|
|
310
|
+
const argument = node.arguments[0];
|
|
311
|
+
if (argument !== undefined && ts.isObjectLiteralExpression(argument)) {
|
|
312
|
+
for (const property of argument.properties) {
|
|
313
|
+
if ((ts.isPropertyAssignment(property) || ts.isShorthandPropertyAssignment(property)) &&
|
|
314
|
+
ts.isIdentifier(property.name)) {
|
|
315
|
+
options.add(property.name.text);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
ts.forEachChild(node, visit);
|
|
321
|
+
};
|
|
322
|
+
visit(sourceFile);
|
|
323
|
+
return options;
|
|
324
|
+
}
|
|
325
|
+
function readExportNames(source) {
|
|
326
|
+
const names = new Set();
|
|
327
|
+
if (/\bexport\s+default\b/u.test(source))
|
|
328
|
+
names.add('default');
|
|
329
|
+
for (const match of source.matchAll(/\bexport\s+(?:const|let|var|function|class|interface|type)\s+([\w$]+)/gu)) {
|
|
330
|
+
const name = match[1];
|
|
331
|
+
if (name !== undefined)
|
|
332
|
+
names.add(name);
|
|
333
|
+
}
|
|
334
|
+
for (const match of source.matchAll(/\bexport\s*\{([^}]*)\}/gu)) {
|
|
335
|
+
for (const entry of (match[1] ?? '').split(',')) {
|
|
336
|
+
const exported = /\bas\s+([\w$]+)/u.exec(entry)?.[1] ?? entry.trim();
|
|
337
|
+
if (exported !== '')
|
|
338
|
+
names.add(exported);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return [...names].sort();
|
|
342
|
+
}
|
|
343
|
+
function collectRoutes(root, projectRoot) {
|
|
344
|
+
if (!existsSync(root) || !statSync(root).isDirectory())
|
|
345
|
+
return [];
|
|
346
|
+
const routes = [];
|
|
347
|
+
walkRouteDirectory(root, root, [], undefined, routes);
|
|
348
|
+
const notFound = path.join(root, '_404.ts');
|
|
349
|
+
if (existsSync(notFound)) {
|
|
350
|
+
routes.push({
|
|
351
|
+
kind: 'not-found',
|
|
352
|
+
path: '*',
|
|
353
|
+
file: notFound,
|
|
354
|
+
layouts: [],
|
|
355
|
+
hasGuard: false,
|
|
356
|
+
hasLoader: false,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
const error = path.join(root, '_error.ts');
|
|
360
|
+
if (existsSync(error)) {
|
|
361
|
+
routes.push({
|
|
362
|
+
kind: 'error',
|
|
363
|
+
path: 'error',
|
|
364
|
+
file: error,
|
|
365
|
+
layouts: [],
|
|
366
|
+
hasGuard: false,
|
|
367
|
+
hasLoader: false,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
return routes
|
|
371
|
+
.filter((route) => route.file.startsWith(projectRoot))
|
|
372
|
+
.sort((left, right) => left.path.localeCompare(right.path) || left.file.localeCompare(right.file));
|
|
373
|
+
}
|
|
374
|
+
function walkRouteDirectory(directory, pagesRoot, parentLayouts, parentError, routes) {
|
|
375
|
+
const layout = path.join(directory, '_layout.ts');
|
|
376
|
+
const layouts = existsSync(layout) ? [...parentLayouts, layout] : parentLayouts;
|
|
377
|
+
const localError = path.join(directory, '_error.ts');
|
|
378
|
+
const error = existsSync(localError) ? localError : parentError;
|
|
379
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
380
|
+
const entryPath = path.join(directory, entry.name);
|
|
381
|
+
if (entry.isDirectory()) {
|
|
382
|
+
walkRouteDirectory(entryPath, pagesRoot, layouts, error, routes);
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
if (!entry.name.endsWith('.ts') || entry.name.endsWith('.d.ts') || entry.name.startsWith('_')) {
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
const source = readFileSync(entryPath, 'utf8');
|
|
389
|
+
routes.push({
|
|
390
|
+
kind: 'page',
|
|
391
|
+
path: routePathForFile(entryPath, pagesRoot),
|
|
392
|
+
file: entryPath,
|
|
393
|
+
layouts,
|
|
394
|
+
...(error === undefined ? {} : { error }),
|
|
395
|
+
hasGuard: hasExportedRouteMember(source, 'guard'),
|
|
396
|
+
hasLoader: hasExportedRouteMember(source, 'loader'),
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
function routePathForFile(fileName, pagesRoot) {
|
|
401
|
+
const relative = path.relative(pagesRoot, fileName).replaceAll(path.sep, '/');
|
|
402
|
+
const segments = relative
|
|
403
|
+
.slice(0, -'.ts'.length)
|
|
404
|
+
.split('/')
|
|
405
|
+
.flatMap((segment) => {
|
|
406
|
+
if (segment === 'index')
|
|
407
|
+
return [];
|
|
408
|
+
if (segment.startsWith('(') && segment.endsWith(')'))
|
|
409
|
+
return [];
|
|
410
|
+
if (segment.startsWith('[...') && segment.endsWith(']'))
|
|
411
|
+
return [`*${segment.slice(4, -1)}`];
|
|
412
|
+
if (segment.startsWith('[') && segment.endsWith(']'))
|
|
413
|
+
return [`:${segment.slice(1, -1)}`];
|
|
414
|
+
return [segment];
|
|
415
|
+
});
|
|
416
|
+
return segments.length === 0 ? '/' : `/${segments.join('/')}`;
|
|
417
|
+
}
|
|
418
|
+
function hasExportedRouteMember(source, name) {
|
|
419
|
+
return new RegExp(`^\\s*export\\s+(?:(?:async\\s+)?function|const|let|var)\\s+${name}\\b`, 'mu').test(source);
|
|
420
|
+
}
|
|
421
|
+
function walkFiles(root) {
|
|
422
|
+
const files = [];
|
|
423
|
+
walk(root, files);
|
|
424
|
+
return files;
|
|
425
|
+
}
|
|
426
|
+
function walk(directory, files) {
|
|
427
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
428
|
+
if (entry.isDirectory()) {
|
|
429
|
+
if (!ignoredDirectories.has(entry.name) && entry.name !== '__tests__') {
|
|
430
|
+
walk(path.join(directory, entry.name), files);
|
|
431
|
+
}
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
const fileName = path.join(directory, entry.name);
|
|
435
|
+
if (!entry.name.endsWith('.test.ts') &&
|
|
436
|
+
!entry.name.endsWith('.spec.ts') &&
|
|
437
|
+
(entry.name.endsWith('.html') || isTypeScriptFile(fileName))) {
|
|
438
|
+
files.push(fileName);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
function isTypeScriptFile(fileName) {
|
|
443
|
+
return (fileName.endsWith('.ts') || fileName.endsWith('.tsx')) && !fileName.endsWith('.d.ts');
|
|
444
|
+
}
|
|
445
|
+
function collectPackages(workspaceRoot) {
|
|
446
|
+
const roots = [];
|
|
447
|
+
for (const groupName of ['packages', 'plugins']) {
|
|
448
|
+
const group = path.join(workspaceRoot, groupName);
|
|
449
|
+
if (!existsSync(group) || !statSync(group).isDirectory())
|
|
450
|
+
continue;
|
|
451
|
+
for (const entry of readdirSync(group, { withFileTypes: true })) {
|
|
452
|
+
if (!entry.isDirectory())
|
|
453
|
+
continue;
|
|
454
|
+
const direct = path.join(group, entry.name);
|
|
455
|
+
if (existsSync(path.join(direct, 'package.json'))) {
|
|
456
|
+
roots.push(direct);
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
for (const nested of readdirSync(direct, { withFileTypes: true })) {
|
|
460
|
+
if (nested.isDirectory() && existsSync(path.join(direct, nested.name, 'package.json'))) {
|
|
461
|
+
roots.push(path.join(direct, nested.name));
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return roots
|
|
467
|
+
.map((root) => {
|
|
468
|
+
const packageJson = readJsonFile(path.join(root, 'package.json'));
|
|
469
|
+
return {
|
|
470
|
+
name: typeof packageJson?.name === 'string'
|
|
471
|
+
? packageJson.name
|
|
472
|
+
: manifestPath(root, workspaceRoot),
|
|
473
|
+
...(typeof packageJson?.version === 'string' ? { version: packageJson.version } : {}),
|
|
474
|
+
path: manifestPath(root, workspaceRoot),
|
|
475
|
+
...(typeof packageJson?.private === 'boolean' ? { private: packageJson.private } : {}),
|
|
476
|
+
};
|
|
477
|
+
})
|
|
478
|
+
.sort((left, right) => left.path.localeCompare(right.path));
|
|
479
|
+
}
|
|
480
|
+
function readTsConfig(tsconfigPath, projectRoot) {
|
|
481
|
+
const read = ts.readConfigFile(tsconfigPath, (fileName) => ts.sys.readFile(fileName));
|
|
482
|
+
if (read.error !== undefined)
|
|
483
|
+
throw new Error(formatTsDiagnostic(read.error));
|
|
484
|
+
return ts.parseJsonConfigFileContent(read.config, ts.sys, projectRoot, undefined, tsconfigPath);
|
|
485
|
+
}
|
|
486
|
+
function readJsonFile(fileName) {
|
|
487
|
+
try {
|
|
488
|
+
const value = JSON.parse(readFileSync(fileName, 'utf8'));
|
|
489
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
490
|
+
? value
|
|
491
|
+
: undefined;
|
|
492
|
+
}
|
|
493
|
+
catch {
|
|
494
|
+
return undefined;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
function findWorkspaceRoot(start) {
|
|
498
|
+
let current = path.resolve(start);
|
|
499
|
+
while (true) {
|
|
500
|
+
if (existsSync(path.join(current, 'pnpm-workspace.yaml')))
|
|
501
|
+
return current;
|
|
502
|
+
const parent = path.dirname(current);
|
|
503
|
+
if (parent === current)
|
|
504
|
+
return path.resolve(start);
|
|
505
|
+
current = parent;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
function manifestPath(fileName, workspaceRoot) {
|
|
509
|
+
const relative = path.relative(workspaceRoot, fileName).replaceAll(path.sep, '/');
|
|
510
|
+
return relative === '' ? '.' : relative;
|
|
511
|
+
}
|
|
512
|
+
function toManifestRoute(route, workspaceRoot) {
|
|
513
|
+
return {
|
|
514
|
+
kind: route.kind,
|
|
515
|
+
path: route.path,
|
|
516
|
+
file: manifestPath(route.file, workspaceRoot),
|
|
517
|
+
layouts: route.layouts.map((fileName) => manifestPath(fileName, workspaceRoot)),
|
|
518
|
+
...(route.error === undefined ? {} : { error: manifestPath(route.error, workspaceRoot) }),
|
|
519
|
+
hasGuard: route.hasGuard,
|
|
520
|
+
hasLoader: route.hasLoader,
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
function toManifestComponent(component, workspaceRoot) {
|
|
524
|
+
return {
|
|
525
|
+
kind: component.kind,
|
|
526
|
+
file: manifestPath(component.file, workspaceRoot),
|
|
527
|
+
...(component.view === undefined ? {} : { view: manifestPath(component.view, workspaceRoot) }),
|
|
528
|
+
exports: component.exports,
|
|
529
|
+
props: component.props,
|
|
530
|
+
emits: component.emits,
|
|
531
|
+
exposed: component.exposed,
|
|
532
|
+
registeredComponents: component.registeredComponents,
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
function toManifestView(view, workspaceRoot) {
|
|
536
|
+
return {
|
|
537
|
+
file: manifestPath(view.file, workspaceRoot),
|
|
538
|
+
bindings: view.bindings,
|
|
539
|
+
events: view.events,
|
|
540
|
+
directives: view.directives,
|
|
541
|
+
components: view.components,
|
|
542
|
+
includes: view.includes
|
|
543
|
+
.map((request) => path.resolve(path.dirname(view.file), request))
|
|
544
|
+
.filter((fileName) => isPathInside(workspaceRoot, fileName))
|
|
545
|
+
.map((fileName) => manifestPath(fileName, workspaceRoot)),
|
|
546
|
+
refs: view.refs,
|
|
547
|
+
slots: view.slots,
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
function toManifestPermission(file, workspaceRoot) {
|
|
551
|
+
return {
|
|
552
|
+
file: manifestPath(file.file, workspaceRoot),
|
|
553
|
+
kinds: file.kinds,
|
|
554
|
+
symbols: file.symbols,
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
function toManifestDiagnostic(diagnostic, workspaceRoot) {
|
|
558
|
+
return {
|
|
559
|
+
code: diagnostic.code,
|
|
560
|
+
message: diagnostic.message,
|
|
561
|
+
severity: diagnostic.severity,
|
|
562
|
+
phase: diagnostic.phase,
|
|
563
|
+
...(diagnostic.sourceId === undefined
|
|
564
|
+
? {}
|
|
565
|
+
: { source: manifestPath(diagnostic.sourceId, workspaceRoot) }),
|
|
566
|
+
...(diagnostic.start === undefined ? {} : { start: diagnostic.start }),
|
|
567
|
+
...(diagnostic.end === undefined ? {} : { end: diagnostic.end }),
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
function formatTsDiagnostic(diagnostic) {
|
|
571
|
+
return ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
|
|
572
|
+
}
|
|
573
|
+
function isPathInside(root, fileName) {
|
|
574
|
+
const relative = path.relative(root, fileName);
|
|
575
|
+
return relative === '' || (!relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));
|
|
576
|
+
}
|
package/dist/scaffold.js
CHANGED
|
@@ -43,7 +43,7 @@ export function isScaffoldTemplate(value) {
|
|
|
43
43
|
value === 'lib-di');
|
|
44
44
|
}
|
|
45
45
|
export function scaffoldSuccessText(result) {
|
|
46
|
-
const command = isAppTemplate(result.template) ? '
|
|
46
|
+
const command = isAppTemplate(result.template) ? 'pnpm run dev' : 'pnpm run build';
|
|
47
47
|
const diLine = result.template === 'app-di' || result.template === 'enterprise' || result.template === 'lib-di'
|
|
48
48
|
? '\nDI integration included.\n'
|
|
49
49
|
: '\n';
|
|
@@ -51,7 +51,7 @@ export function scaffoldSuccessText(result) {
|
|
|
51
51
|
`Created ${result.name}`,
|
|
52
52
|
'',
|
|
53
53
|
` cd ${result.name}`,
|
|
54
|
-
'
|
|
54
|
+
' pnpm install',
|
|
55
55
|
` ${command}`,
|
|
56
56
|
diLine,
|
|
57
57
|
].join('\n');
|
|
@@ -136,12 +136,15 @@ function libFiles(withDi) {
|
|
|
136
136
|
{ path: 'README.md', content: withDi ? libReadmeWithDi : libReadme },
|
|
137
137
|
];
|
|
138
138
|
}
|
|
139
|
-
function appPackageJson(
|
|
139
|
+
function appPackageJson(_withDi) {
|
|
140
140
|
return `{
|
|
141
141
|
"name": "{{packageName}}",
|
|
142
142
|
"version": "0.1.0",
|
|
143
143
|
"private": true,
|
|
144
144
|
"type": "module",
|
|
145
|
+
"engines": {
|
|
146
|
+
"node": ">=22.12.0"
|
|
147
|
+
},
|
|
145
148
|
"scripts": {
|
|
146
149
|
"dev": "vite",
|
|
147
150
|
"build": "tsc --noEmit && vite build",
|
|
@@ -149,13 +152,12 @@ function appPackageJson(withDi) {
|
|
|
149
152
|
"typecheck": "tsc --noEmit"
|
|
150
153
|
},
|
|
151
154
|
"dependencies": {
|
|
152
|
-
|
|
153
|
-
"@vobs/runtime-dom": "latest"
|
|
155
|
+
"@vobs/vobs": "0.2.0"
|
|
154
156
|
},
|
|
155
157
|
"devDependencies": {
|
|
156
|
-
"@vobs/vite-plugin": "
|
|
158
|
+
"@vobs/vite-plugin": "0.1.0",
|
|
157
159
|
"typescript": "^5.7.0",
|
|
158
|
-
"vite": "^
|
|
160
|
+
"vite": "^8.0.0"
|
|
159
161
|
}
|
|
160
162
|
}
|
|
161
163
|
`;
|
|
@@ -192,7 +194,7 @@ import './styles/global.css';
|
|
|
192
194
|
|
|
193
195
|
await app.mount('#app');
|
|
194
196
|
`;
|
|
195
|
-
const appModule = `import { createVobsApp } from '@vobs/
|
|
197
|
+
const appModule = `import { createVobsApp } from '@vobs/vobs';
|
|
196
198
|
import router from 'vobs:routes';
|
|
197
199
|
|
|
198
200
|
const reportAppError = (error: unknown): void => {
|
|
@@ -210,8 +212,7 @@ const app = createVobsApp({
|
|
|
210
212
|
|
|
211
213
|
export { app };
|
|
212
214
|
`;
|
|
213
|
-
const appModuleWithDi = `import { createInjectionScope } from '@vobs/
|
|
214
|
-
import { createVobsApp } from '@vobs/runtime-dom';
|
|
215
|
+
const appModuleWithDi = `import { createInjectionScope, createVobsApp } from '@vobs/vobs';
|
|
215
216
|
import router from 'vobs:routes';
|
|
216
217
|
import { registerServices } from './app/di.js';
|
|
217
218
|
|
|
@@ -237,7 +238,7 @@ export { app };
|
|
|
237
238
|
const appDiFiles = [
|
|
238
239
|
{
|
|
239
240
|
path: 'src/app/di.ts',
|
|
240
|
-
content: `import type { InjectionScope } from '@vobs/
|
|
241
|
+
content: `import type { InjectionScope } from '@vobs/vobs';
|
|
241
242
|
import { createToastService, ToastServiceKey } from '../services/toast.js';
|
|
242
243
|
|
|
243
244
|
export function registerServices(scope: InjectionScope): void {
|
|
@@ -252,7 +253,7 @@ export function registerServices(scope: InjectionScope): void {
|
|
|
252
253
|
},
|
|
253
254
|
{
|
|
254
255
|
path: 'src/services/toast.ts',
|
|
255
|
-
content: `import { createInjectionKey, inject } from '@vobs/
|
|
256
|
+
content: `import { createInjectionKey, inject } from '@vobs/vobs';
|
|
256
257
|
|
|
257
258
|
export interface ToastService {
|
|
258
259
|
success(message: string): void;
|
|
@@ -274,8 +275,7 @@ export function useToast(): ToastService {
|
|
|
274
275
|
`,
|
|
275
276
|
},
|
|
276
277
|
];
|
|
277
|
-
const indexPage = `import { signal } from '@vobs/
|
|
278
|
-
import { definePage } from '@vobs/runtime-dom';
|
|
278
|
+
const indexPage = `import { definePage, signal } from '@vobs/vobs';
|
|
279
279
|
import template from './index.html';
|
|
280
280
|
|
|
281
281
|
export default definePage({
|
|
@@ -291,8 +291,7 @@ export default definePage({
|
|
|
291
291
|
},
|
|
292
292
|
});
|
|
293
293
|
`;
|
|
294
|
-
const indexPageWithDi = `import { signal } from '@vobs/
|
|
295
|
-
import { definePage } from '@vobs/runtime-dom';
|
|
294
|
+
const indexPageWithDi = `import { definePage, signal } from '@vobs/vobs';
|
|
296
295
|
import { useToast } from '../services/toast.js';
|
|
297
296
|
import template from './index.html';
|
|
298
297
|
|
|
@@ -350,7 +349,7 @@ function libPackageJson(withDi) {
|
|
|
350
349
|
},
|
|
351
350
|
${withDi ? ' "peerDependencies": {\n "@vobs/di": "^0.1.0"\n },\n' : ''} "files": ["dist"],
|
|
352
351
|
"devDependencies": {
|
|
353
|
-
${withDi ? ' "@vobs/di": "
|
|
352
|
+
${withDi ? ' "@vobs/di": "0.1.0",\n' : ''} "typescript": "^5.7.0"
|
|
354
353
|
}
|
|
355
354
|
}
|
|
356
355
|
`;
|