@jskit-ai/architecture-inspector 0.1.0 → 0.1.2
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/README.md +135 -13
- package/package.json +4 -2
- package/src/calls/cache.js +97 -0
- package/src/calls/evaluate.js +474 -0
- package/src/calls/external.js +123 -0
- package/src/calls/features.js +371 -0
- package/src/calls/functions.js +277 -0
- package/src/calls/index.js +343 -0
- package/src/calls/program.js +143 -0
- package/src/calls/resolve.js +658 -0
- package/src/calls/symbols.js +201 -0
- package/src/calls/transports.js +194 -0
- package/src/calls/types.js +9 -0
- package/src/cli.js +93 -14
- package/src/inspectVueArchitecture.js +146 -1035
- package/src/previewLocator.js +30 -0
- package/src/vue/modules.js +293 -0
- package/src/vue/pages.js +551 -0
- package/src/vue/routes.js +589 -0
- package/src/vue/syntax.js +222 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import { isFunctionLike, safeAliased, unwrapExpression } from "./functions.js";
|
|
3
|
+
|
|
4
|
+
const MAX_DEPTH = 8;
|
|
5
|
+
|
|
6
|
+
function isClassLike(node) {
|
|
7
|
+
return Boolean(node) && (ts.isClassDeclaration(node) || ts.isClassExpression(node));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function isBareSpecifier(specifier) {
|
|
11
|
+
return typeof specifier === "string" && !specifier.startsWith(".") && !specifier.startsWith("/");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Symbol and declaration helpers over one loaded call program.
|
|
15
|
+
function createSymbols(context) {
|
|
16
|
+
const { checker, fileOf, resolveImport } = context;
|
|
17
|
+
|
|
18
|
+
function symbolAt(node) {
|
|
19
|
+
if (!node) {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
const parent = node.parent;
|
|
23
|
+
if (parent && ts.isShorthandPropertyAssignment(parent) && parent.name === node) {
|
|
24
|
+
return checker.getShorthandAssignmentValueSymbol(parent);
|
|
25
|
+
}
|
|
26
|
+
return checker.getSymbolAtLocation(node);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function resolved(symbol) {
|
|
30
|
+
return symbol && symbol.flags & ts.SymbolFlags.Alias ? safeAliased(checker, symbol) : symbol;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function declarationOf(node) {
|
|
34
|
+
const symbol = resolved(symbolAt(node));
|
|
35
|
+
return symbol?.valueDeclaration || symbol?.declarations?.[0] || null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// The import an identifier is bound to: { specifier, imported }, or null.
|
|
39
|
+
function importOf(identifier) {
|
|
40
|
+
const symbol = symbolAt(identifier);
|
|
41
|
+
if (!symbol || !(symbol.flags & ts.SymbolFlags.Alias)) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
const declaration = symbol.declarations?.[0];
|
|
45
|
+
if (!declaration) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
let importDeclaration = declaration;
|
|
49
|
+
while (importDeclaration && !ts.isImportDeclaration(importDeclaration)) {
|
|
50
|
+
importDeclaration = importDeclaration.parent;
|
|
51
|
+
}
|
|
52
|
+
if (!importDeclaration || !ts.isStringLiteralLike(importDeclaration.moduleSpecifier)) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
let imported = "default";
|
|
56
|
+
if (ts.isImportSpecifier(declaration)) {
|
|
57
|
+
imported = (declaration.propertyName || declaration.name).text;
|
|
58
|
+
} else if (ts.isNamespaceImport(declaration)) {
|
|
59
|
+
imported = "*";
|
|
60
|
+
}
|
|
61
|
+
return { specifier: importDeclaration.moduleSpecifier.text, imported, file: fileOf(declaration) };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// A package import that does not resolve to declared project source.
|
|
65
|
+
function isPackageImport(identifier) {
|
|
66
|
+
const binding = importOf(identifier);
|
|
67
|
+
return Boolean(binding) && isBareSpecifier(binding.specifier) && !resolveImport(binding.file, binding.specifier);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function isImportOf(identifier, packagePrefix, names) {
|
|
71
|
+
const binding = importOf(identifier);
|
|
72
|
+
return Boolean(binding) &&
|
|
73
|
+
(binding.specifier === packagePrefix || binding.specifier.startsWith(`${packagePrefix}/`)) &&
|
|
74
|
+
names.includes(binding.imported);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// The project function (or class) a declaration ultimately names.
|
|
78
|
+
function declarationImplementations(declaration, depth = 0) {
|
|
79
|
+
if (!declaration || depth > MAX_DEPTH) {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
if (isFunctionLike(declaration) || isClassLike(declaration)) {
|
|
83
|
+
return fileOf(declaration) ? [declaration] : [];
|
|
84
|
+
}
|
|
85
|
+
if (ts.isVariableDeclaration(declaration) || ts.isPropertyAssignment(declaration) || ts.isPropertyDeclaration(declaration)) {
|
|
86
|
+
return expressionImplementations(declaration.initializer, depth + 1);
|
|
87
|
+
}
|
|
88
|
+
if (ts.isShorthandPropertyAssignment(declaration)) {
|
|
89
|
+
return symbolImplementations(checker.getShorthandAssignmentValueSymbol(declaration), depth + 1);
|
|
90
|
+
}
|
|
91
|
+
if (ts.isBindingElement(declaration)) {
|
|
92
|
+
return bindingElementImplementations(declaration, depth + 1);
|
|
93
|
+
}
|
|
94
|
+
if (ts.isExportSpecifier(declaration) || ts.isImportSpecifier(declaration) || ts.isImportClause(declaration)) {
|
|
95
|
+
return symbolImplementations(checker.getSymbolAtLocation(declaration.name), depth + 1);
|
|
96
|
+
}
|
|
97
|
+
if (ts.isExportAssignment(declaration)) {
|
|
98
|
+
return expressionImplementations(declaration.expression, depth + 1);
|
|
99
|
+
}
|
|
100
|
+
if (ts.isBinaryExpression(declaration) && declaration.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
|
|
101
|
+
return expressionImplementations(declaration.right, depth + 1);
|
|
102
|
+
}
|
|
103
|
+
if ((ts.isPropertyAccessExpression(declaration) || ts.isElementAccessExpression(declaration)) &&
|
|
104
|
+
ts.isBinaryExpression(declaration.parent) && declaration.parent.left === declaration) {
|
|
105
|
+
return expressionImplementations(declaration.parent.right, depth + 1);
|
|
106
|
+
}
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// A name an object pattern binds (`const { a: b } = source`, a parameter's
|
|
111
|
+
// pattern, or one nested in another) holds the property it reads from the
|
|
112
|
+
// type of what the pattern destructures.
|
|
113
|
+
function bindingElementImplementations(element, depth) {
|
|
114
|
+
const key = element.propertyName || element.name;
|
|
115
|
+
if (!ts.isObjectBindingPattern(element.parent) || element.dotDotDotToken || !(ts.isIdentifier(key) || ts.isStringLiteralLike(key))) {
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
const property = checker.getTypeAtLocation(element.parent).getProperty(key.text);
|
|
119
|
+
const found = new Set();
|
|
120
|
+
for (const declaration of property?.declarations || []) {
|
|
121
|
+
if (declaration !== element) {
|
|
122
|
+
for (const implementation of declarationImplementations(declaration, depth)) {
|
|
123
|
+
found.add(implementation);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return [...found];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function expressionImplementations(expression, depth) {
|
|
131
|
+
const value = unwrapExpression(expression);
|
|
132
|
+
if (!value || depth > MAX_DEPTH) {
|
|
133
|
+
return [];
|
|
134
|
+
}
|
|
135
|
+
if (isFunctionLike(value) || isClassLike(value)) {
|
|
136
|
+
return fileOf(value) ? [value] : [];
|
|
137
|
+
}
|
|
138
|
+
if (ts.isIdentifier(value)) {
|
|
139
|
+
return symbolImplementations(symbolAt(value), depth + 1);
|
|
140
|
+
}
|
|
141
|
+
if (ts.isPropertyAccessExpression(value)) {
|
|
142
|
+
return symbolImplementations(checker.getSymbolAtLocation(value.name), depth + 1);
|
|
143
|
+
}
|
|
144
|
+
if (ts.isCallExpression(value)) {
|
|
145
|
+
// A factory-returned closure: `fileArea: createFileArea(service)`.
|
|
146
|
+
const declaration = checker.getTypeAtLocation(value).getSymbol()?.valueDeclaration;
|
|
147
|
+
return declaration && isFunctionLike(declaration) && fileOf(declaration) ? [declaration] : [];
|
|
148
|
+
}
|
|
149
|
+
return [];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function symbolImplementations(symbol, depth = 0) {
|
|
153
|
+
const target = resolved(symbol);
|
|
154
|
+
const found = new Set();
|
|
155
|
+
for (const declaration of target?.declarations || []) {
|
|
156
|
+
for (const implementation of declarationImplementations(declaration, depth)) {
|
|
157
|
+
found.add(implementation);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return [...found];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Whether a symbol's declarations all live in TypeScript's default libraries.
|
|
164
|
+
function isLibrarySymbol(symbol) {
|
|
165
|
+
const target = resolved(symbol);
|
|
166
|
+
const declarations = target?.declarations || [];
|
|
167
|
+
return declarations.length > 0 && declarations.every((declaration) => context.isLibrary(declaration));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// The leftmost identifier of a receiver chain, through calls and member access.
|
|
171
|
+
function receiverRoot(expression) {
|
|
172
|
+
let current = unwrapExpression(expression);
|
|
173
|
+
while (current) {
|
|
174
|
+
if (ts.isPropertyAccessExpression(current) || ts.isElementAccessExpression(current)) {
|
|
175
|
+
current = unwrapExpression(current.expression);
|
|
176
|
+
} else if (ts.isCallExpression(current) || ts.isNewExpression(current)) {
|
|
177
|
+
current = unwrapExpression(current.expression);
|
|
178
|
+
} else if (ts.isAwaitExpression(current)) {
|
|
179
|
+
current = unwrapExpression(current.expression);
|
|
180
|
+
} else {
|
|
181
|
+
return current;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
symbolAt,
|
|
189
|
+
resolved,
|
|
190
|
+
declarationOf,
|
|
191
|
+
importOf,
|
|
192
|
+
isPackageImport,
|
|
193
|
+
isImportOf,
|
|
194
|
+
symbolImplementations,
|
|
195
|
+
expressionImplementations,
|
|
196
|
+
isLibrarySymbol,
|
|
197
|
+
receiverRoot
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export { createSymbols, isClassLike };
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import { resolveScopedApiBasePath } from "@jskit-ai/kernel/shared/surface";
|
|
3
|
+
import { isFunctionLike, unwrapExpression } from "./functions.js";
|
|
4
|
+
import { ANY, SEGMENT, isLiteralPattern } from "./evaluate.js";
|
|
5
|
+
import { codeSide } from "./resolve.js";
|
|
6
|
+
|
|
7
|
+
const HTTP_WEB = "@jskit-ai/http-web";
|
|
8
|
+
const METHOD_PATTERN = /^[A-Za-z]+$/u;
|
|
9
|
+
const MAX_PROBABLE_ROUTES = 3;
|
|
10
|
+
|
|
11
|
+
function literalMethods(evaluator, node) {
|
|
12
|
+
if (!node) {
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
const values = evaluator.evaluate(node);
|
|
16
|
+
return values.every((value) => isLiteralPattern(value) && METHOD_PATTERN.test(value))
|
|
17
|
+
? [...new Set(values.map((value) => value.toUpperCase()))]
|
|
18
|
+
: null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function isFalse(node) {
|
|
22
|
+
return unwrapExpression(node)?.kind === ts.SyntaxKind.FalseKeyword;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// JSKIT client transport call sites with their partially evaluated paths and
|
|
26
|
+
// methods (`methods: null` means any method).
|
|
27
|
+
function collectTransportSites({ calls, symbols, evaluator, fileOf }) {
|
|
28
|
+
const sites = [];
|
|
29
|
+
const option = (call, name, index = 0) => {
|
|
30
|
+
const options = unwrapExpression(call.arguments[index]);
|
|
31
|
+
return options && ts.isObjectLiteralExpression(options) ? evaluator.propertyInitializer(options, name) : undefined;
|
|
32
|
+
};
|
|
33
|
+
const isHttpClient = (expression) => {
|
|
34
|
+
let value = unwrapExpression(expression);
|
|
35
|
+
if (value && ts.isIdentifier(value)) {
|
|
36
|
+
const declaration = symbols.declarationOf(value);
|
|
37
|
+
value = declaration && ts.isVariableDeclaration(declaration) ? unwrapExpression(declaration.initializer) : null;
|
|
38
|
+
}
|
|
39
|
+
return Boolean(value) && ts.isCallExpression(value) && ts.isIdentifier(value.expression) &&
|
|
40
|
+
symbols.isImportOf(value.expression, HTTP_WEB, ["getHttpWebClient"]);
|
|
41
|
+
};
|
|
42
|
+
for (const call of calls) {
|
|
43
|
+
if (!ts.isCallExpression(call)) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const callee = unwrapExpression(call.expression);
|
|
47
|
+
const file = fileOf(call);
|
|
48
|
+
if (ts.isIdentifier(callee) && symbols.isImportOf(callee, HTTP_WEB, ["useEndpointResource"])) {
|
|
49
|
+
const reads = !isFalse(option(call, "enabled"));
|
|
50
|
+
const writeMethod = option(call, "writeMethod");
|
|
51
|
+
const writes = writeMethod ? literalMethods(evaluator, writeMethod) : reads ? [] : ["PATCH"];
|
|
52
|
+
sites.push({ call, file, kind: "useEndpointResource", path: option(call, "path"), methods: writes === null ? null : [...(reads ? ["GET"] : []), ...writes] });
|
|
53
|
+
} else if (ts.isIdentifier(callee) && symbols.isImportOf(callee, HTTP_WEB, ["useCommand"])) {
|
|
54
|
+
const build = unwrapExpression(option(call, "buildCommandOptions"));
|
|
55
|
+
const built = build && isFunctionLike(build) ? unwrapExpression(evaluator.functionReturns(build)[0]) : null;
|
|
56
|
+
const builtObject = built && ts.isObjectLiteralExpression(built) ? built : null;
|
|
57
|
+
const builtPath = builtObject && evaluator.propertyInitializer(builtObject, "path");
|
|
58
|
+
const builtMethod = builtObject && literalMethods(evaluator, evaluator.propertyInitializer(builtObject, "method"));
|
|
59
|
+
const writeMethod = literalMethods(evaluator, option(call, "writeMethod"));
|
|
60
|
+
sites.push({
|
|
61
|
+
call,
|
|
62
|
+
file,
|
|
63
|
+
kind: "useCommand",
|
|
64
|
+
path: builtPath || null,
|
|
65
|
+
apiSuffix: builtPath ? null : option(call, "apiSuffix"),
|
|
66
|
+
methods: builtMethod || writeMethod || (writeMethod === null ? null : ["POST"])
|
|
67
|
+
});
|
|
68
|
+
} else if (ts.isPropertyAccessExpression(callee) && callee.name.text === "request" && isHttpClient(callee.expression)) {
|
|
69
|
+
const method = option(call, "method", 1);
|
|
70
|
+
sites.push({ call, file, kind: "request", path: call.arguments[0], methods: method ? literalMethods(evaluator, method) : ["GET"] });
|
|
71
|
+
} else if (ts.isIdentifier(callee) && callee.text === "fetch" && codeSide(file) !== "server") {
|
|
72
|
+
const symbol = symbols.symbolAt(callee);
|
|
73
|
+
if (symbol && symbols.isLibrarySymbol(symbol)) {
|
|
74
|
+
const method = option(call, "method", 1);
|
|
75
|
+
sites.push({ call, file, kind: "fetch", path: call.arguments[0], methods: method ? literalMethods(evaluator, method) : ["GET"] });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return sites;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function sitePatterns(site, evaluator) {
|
|
83
|
+
if (site.path) {
|
|
84
|
+
return evaluator.evaluate(site.path);
|
|
85
|
+
}
|
|
86
|
+
if (site.apiSuffix) {
|
|
87
|
+
return evaluator.evaluate(site.apiSuffix).map((suffix) =>
|
|
88
|
+
resolveScopedApiBasePath({ relativePath: suffix.startsWith("/") || suffix.startsWith(ANY) ? suffix : `/${suffix}`, strictParams: false }));
|
|
89
|
+
}
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Client path pattern -> path segments, after the declared prefix aliases.
|
|
94
|
+
function clientSegments(pattern, prefixAliases) {
|
|
95
|
+
let value = pattern.replace(/^https?:\/\/[^/]+/u, "").split("?")[0].split("#")[0];
|
|
96
|
+
for (const { client, route } of prefixAliases) {
|
|
97
|
+
if (value === client || value.startsWith(`${client}/`)) {
|
|
98
|
+
value = `${route}${value.slice(client.length)}`;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return value.split("/").filter(Boolean);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Aligns client segments with a route template. ANY spans one or more route
|
|
106
|
+
// segments; SEGMENT and alias parameters (`:slug`) fill one. The score counts
|
|
107
|
+
// route literals matched by client literals and by wildcards.
|
|
108
|
+
function matchSegments(client, route, clientIndex = 0, routeIndex = 0, score = { literal: 0, wildcard: 0 }) {
|
|
109
|
+
if (clientIndex === client.length) {
|
|
110
|
+
return routeIndex === route.length ? score : null;
|
|
111
|
+
}
|
|
112
|
+
const segment = client[clientIndex];
|
|
113
|
+
if (segment.includes(ANY)) {
|
|
114
|
+
let best = null;
|
|
115
|
+
for (let take = 1; routeIndex + take <= route.length; take += 1) {
|
|
116
|
+
const covered = route.slice(routeIndex, routeIndex + take).filter((part) => !part.startsWith(":")).length;
|
|
117
|
+
const result = matchSegments(client, route, clientIndex + 1, routeIndex + take, { literal: score.literal, wildcard: score.wildcard + covered });
|
|
118
|
+
if (result && (!best || result.literal > best.literal || (result.literal === best.literal && result.wildcard < best.wildcard))) {
|
|
119
|
+
best = result;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return best;
|
|
123
|
+
}
|
|
124
|
+
if (routeIndex >= route.length) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
const target = route[routeIndex];
|
|
128
|
+
if (target.startsWith(":")) {
|
|
129
|
+
return matchSegments(client, route, clientIndex + 1, routeIndex + 1, score);
|
|
130
|
+
}
|
|
131
|
+
if (segment.includes(SEGMENT) || segment.startsWith(":")) {
|
|
132
|
+
const [prefix, suffix = ""] = segment.startsWith(":") ? ["", ""] : segment.split(SEGMENT);
|
|
133
|
+
if (!target.startsWith(prefix) || !target.endsWith(suffix)) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
return matchSegments(client, route, clientIndex + 1, routeIndex + 1, { literal: score.literal, wildcard: score.wildcard + 1 });
|
|
137
|
+
}
|
|
138
|
+
return segment === target
|
|
139
|
+
? matchSegments(client, route, clientIndex + 1, routeIndex + 1, { literal: score.literal + 1, wildcard: score.wildcard })
|
|
140
|
+
: null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function routeLiterals(route) {
|
|
144
|
+
return route.segments.filter((part) => !part.startsWith(":")).length;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Matches one site against the statically discovered HTTP entry operations.
|
|
148
|
+
// Returns, per method, the matched routes and whether each key was evaluated
|
|
149
|
+
// exactly: one best route (or one per evaluated alternative) whose every
|
|
150
|
+
// literal segment aligns with a client literal.
|
|
151
|
+
function matchSite({ patterns, methods }, routes, prefixAliases) {
|
|
152
|
+
const literal = patterns.map((pattern) => clientSegments(pattern, prefixAliases))
|
|
153
|
+
.filter((segments) => segments.some((segment) => isLiteralPattern(segment) && !segment.startsWith(":") && segment !== "api"));
|
|
154
|
+
const methodsToMatch = methods || [...new Set(routes.map((route) => route.key.method))];
|
|
155
|
+
return methodsToMatch.map((method) => {
|
|
156
|
+
if (!literal.length) {
|
|
157
|
+
return { method, matches: [] };
|
|
158
|
+
}
|
|
159
|
+
const candidates = new Map();
|
|
160
|
+
for (const segments of literal) {
|
|
161
|
+
for (const route of routes) {
|
|
162
|
+
if (route.key.method !== method) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
const score = matchSegments(segments, route.segments);
|
|
166
|
+
const previous = candidates.get(route);
|
|
167
|
+
if (score && (!previous || previous.literal < score.literal)) {
|
|
168
|
+
candidates.set(route, score);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const ranked = [...candidates.entries()].sort((left, right) => right[1].literal - left[1].literal || left[1].wildcard - right[1].wildcard);
|
|
173
|
+
if (!ranked.length) {
|
|
174
|
+
return { method, matches: [] };
|
|
175
|
+
}
|
|
176
|
+
const top = ranked.filter(([, score]) => score.literal === ranked[0][1].literal);
|
|
177
|
+
const exact = top.length <= literal.length && top.every(([route, score]) => score.wildcard === 0 && score.literal === routeLiterals(route));
|
|
178
|
+
if (exact) {
|
|
179
|
+
return { method, matches: top.map(([route]) => ({ route, keyCertainty: "exact" })) };
|
|
180
|
+
}
|
|
181
|
+
if (top.length <= MAX_PROBABLE_ROUTES) {
|
|
182
|
+
return { method, matches: top.map(([route]) => ({ route, keyCertainty: "probable" })) };
|
|
183
|
+
}
|
|
184
|
+
return { method, matches: [] };
|
|
185
|
+
}).filter((entry) => methods || entry.matches.length);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function routeTable(entryOperations) {
|
|
189
|
+
return entryOperations
|
|
190
|
+
.filter((entry) => entry.resolution === "resolved" && entry.key?.protocol === "http" && typeof entry.key.operation === "string")
|
|
191
|
+
.map((entry) => ({ key: entry.key, segments: entry.key.operation.split("/").filter(Boolean) }));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export { collectTransportSites, matchSite, routeTable, sitePatterns };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// The collaborators the call layer's factories hand each other over one
|
|
2
|
+
// loaded call program.
|
|
3
|
+
|
|
4
|
+
/** @typedef {ReturnType<typeof import("./program.js").loadCallProgram>["fileOf"]} FileOf */
|
|
5
|
+
/** @typedef {ReturnType<typeof import("./symbols.js").createSymbols>} CallSymbols */
|
|
6
|
+
/** @typedef {ReturnType<typeof import("./evaluate.js").createEvaluator>} CallEvaluator */
|
|
7
|
+
/** @typedef {ReturnType<typeof import("./features.js").bindFeatures>} FeatureBindings */
|
|
8
|
+
|
|
9
|
+
export {};
|
package/src/cli.js
CHANGED
|
@@ -1,33 +1,112 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { once } from "node:events";
|
|
4
|
+
import { lstat } from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
2
7
|
import { parseArgs } from "node:util";
|
|
3
|
-
import { inspectVueArchitecture } from "./inspectVueArchitecture.js";
|
|
4
8
|
|
|
5
|
-
|
|
9
|
+
const MAX_INPUT_BYTES = 4 * 1024 * 1024;
|
|
10
|
+
const SCRIPT_FILE_PATTERN = /\.(?:[cm]?js|jsx|tsx?|vue)$/u;
|
|
11
|
+
// The inspection's old generation is bounded by the size of the declared
|
|
12
|
+
// scripts. It holds about 45 MiB per MiB of script; the bound leaves roughly a
|
|
13
|
+
// third more, so garbage is collected before memory grows far past what the
|
|
14
|
+
// inspection really holds.
|
|
15
|
+
const HEAP_BASE_MB = 192;
|
|
16
|
+
const HEAP_MB_PER_SCRIPT_MB = 56;
|
|
17
|
+
|
|
18
|
+
function pairs(entries, usage) {
|
|
19
|
+
return entries.map((entry) => {
|
|
20
|
+
const separator = entry.indexOf("=");
|
|
21
|
+
if (separator < 1 || separator === entry.length - 1) {
|
|
22
|
+
throw new Error(usage);
|
|
23
|
+
}
|
|
24
|
+
return [entry.slice(0, separator), entry.slice(separator + 1)];
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function readOptions() {
|
|
6
29
|
const { values } = parseArgs({ options: {
|
|
7
30
|
"http-scope": { type: "string", default: "" },
|
|
8
31
|
"api-base-path": { type: "string", default: "" },
|
|
9
|
-
alias: { type: "string", multiple: true, default: [] }
|
|
32
|
+
alias: { type: "string", multiple: true, default: [] },
|
|
33
|
+
"api-prefix-alias": { type: "string", multiple: true, default: [] },
|
|
34
|
+
calls: { type: "boolean", default: false }
|
|
10
35
|
} });
|
|
36
|
+
const aliases = {};
|
|
37
|
+
for (const [name, directory] of pairs(values.alias, "Use --alias name=project-directory.")) {
|
|
38
|
+
if (Object.hasOwn(aliases, name)) throw new Error("Duplicate architecture alias.");
|
|
39
|
+
aliases[name] = directory;
|
|
40
|
+
}
|
|
41
|
+
const apiPrefixAliases = pairs(values["api-prefix-alias"], "Use --api-prefix-alias client-prefix=route-prefix.")
|
|
42
|
+
.map(([client, route]) => ({ client, route }));
|
|
43
|
+
if (new Set(apiPrefixAliases.map(({ client }) => client)).size !== apiPrefixAliases.length) {
|
|
44
|
+
throw new Error("Duplicate API prefix alias.");
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
projectRoot: process.cwd(),
|
|
48
|
+
aliases,
|
|
49
|
+
httpScope: values["http-scope"],
|
|
50
|
+
apiBasePath: values["api-base-path"],
|
|
51
|
+
calls: values.calls,
|
|
52
|
+
apiPrefixAliases
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function readInput() {
|
|
11
57
|
const chunks = [];
|
|
12
58
|
let length = 0;
|
|
13
59
|
for await (const chunk of process.stdin) {
|
|
14
60
|
length += chunk.length;
|
|
15
|
-
if (length >
|
|
61
|
+
if (length > MAX_INPUT_BYTES) throw new Error("Architecture inspection input exceeds 4 MiB.");
|
|
16
62
|
chunks.push(chunk);
|
|
17
63
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
64
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// File sizes only: the inspection itself validates every declared path.
|
|
68
|
+
async function heapLimitMb(projectRoot, input) {
|
|
69
|
+
let bytes = 0;
|
|
70
|
+
for (const file of Array.isArray(input?.files) ? input.files : []) {
|
|
71
|
+
const absolute = typeof file?.path === "string" && SCRIPT_FILE_PATTERN.test(file.path) &&
|
|
72
|
+
path.resolve(projectRoot, file.path);
|
|
73
|
+
if (absolute && absolute.startsWith(`${projectRoot}${path.sep}`)) {
|
|
74
|
+
bytes += (await lstat(absolute).catch(() => null))?.size || 0;
|
|
75
|
+
}
|
|
25
76
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
77
|
+
return Math.ceil(HEAP_BASE_MB + HEAP_MB_PER_SCRIPT_MB * bytes / (1024 * 1024));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// The bounded inspection runs in a child Node process started with the old
|
|
81
|
+
// generation bound; a bound given by the caller is kept as it is.
|
|
82
|
+
async function launch(inputText) {
|
|
83
|
+
const limit = await heapLimitMb(process.cwd(), JSON.parse(inputText));
|
|
84
|
+
const child = spawn(process.execPath, [`--max-old-space-size=${limit}`, fileURLToPath(import.meta.url), ...process.argv.slice(2)], {
|
|
85
|
+
stdio: ["pipe", "inherit", "pipe"]
|
|
29
86
|
});
|
|
87
|
+
const errors = [];
|
|
88
|
+
child.stderr.on("data", (chunk) => errors.push(chunk));
|
|
89
|
+
child.stdin.end(inputText);
|
|
90
|
+
const [code, signal] = await once(child, "close");
|
|
91
|
+
const message = Buffer.concat(errors).toString("utf8");
|
|
92
|
+
if (code !== 0) {
|
|
93
|
+
throw new Error(signal || /heap out of memory/u.test(message)
|
|
94
|
+
? `Architecture inspection needs more than its ${limit} MB memory bound.`
|
|
95
|
+
: message.trim() || "Architecture inspection failed.");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function inspect(inputText) {
|
|
100
|
+
// Loaded here, so the launching process stays small.
|
|
101
|
+
const { inspectVueArchitecture } = await import("./inspectVueArchitecture.js");
|
|
102
|
+
const result = await inspectVueArchitecture({ ...readOptions(), input: JSON.parse(inputText) });
|
|
30
103
|
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
readOptions();
|
|
108
|
+
const inputText = await readInput();
|
|
109
|
+
await (process.execArgv.some((argument) => argument.startsWith("--max-old-space-size")) ? inspect(inputText) : launch(inputText));
|
|
31
110
|
} catch (error) {
|
|
32
111
|
process.stderr.write(`${error.message}\n`);
|
|
33
112
|
process.exitCode = 1;
|