@beignet/cli 0.0.52 → 0.0.53
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 +26 -0
- package/README.md +131 -1
- package/dist/analysis/layers.d.ts +2 -0
- package/dist/analysis/layers.d.ts.map +1 -1
- package/dist/analysis/layers.js +5 -0
- package/dist/analysis/layers.js.map +1 -1
- package/dist/analysis/port-wiring.d.ts +4 -2
- package/dist/analysis/port-wiring.d.ts.map +1 -1
- package/dist/analysis/port-wiring.js +39 -10
- package/dist/analysis/port-wiring.js.map +1 -1
- package/dist/analysis/source-index.d.ts +2 -1
- package/dist/analysis/source-index.d.ts.map +1 -1
- package/dist/analysis/source-index.js +34 -5
- package/dist/analysis/source-index.js.map +1 -1
- package/dist/analysis/workspace-routes.d.ts +8 -0
- package/dist/analysis/workspace-routes.d.ts.map +1 -0
- package/dist/analysis/workspace-routes.js +291 -0
- package/dist/analysis/workspace-routes.js.map +1 -0
- package/dist/analysis/workspace.d.ts +24 -3
- package/dist/analysis/workspace.d.ts.map +1 -1
- package/dist/analysis/workspace.js +166 -21
- package/dist/analysis/workspace.js.map +1 -1
- package/dist/app-map-changes.d.ts.map +1 -1
- package/dist/app-map-changes.js +19 -7
- package/dist/app-map-changes.js.map +1 -1
- package/dist/app-map.d.ts.map +1 -1
- package/dist/app-map.js +58 -55
- package/dist/app-map.js.map +1 -1
- package/dist/config.d.ts +7 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +19 -0
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +89 -21
- package/dist/inspect.js.map +1 -1
- package/dist/lint.d.ts.map +1 -1
- package/dist/lint.js +106 -38
- package/dist/lint.js.map +1 -1
- package/dist/make/shared.js +1 -1
- package/dist/make.js +18 -1
- package/dist/make.js.map +1 -1
- package/dist/provider-add.js +2 -2
- package/dist/provider-add.js.map +1 -1
- package/dist/provider-audit.d.ts.map +1 -1
- package/dist/provider-audit.js +22 -6
- package/dist/provider-audit.js.map +1 -1
- package/dist/templates/shared.js +3 -3
- package/package.json +3 -2
- package/skills/app-structure/SKILL.md +42 -0
- package/src/analysis/layers.ts +9 -0
- package/src/analysis/port-wiring.ts +66 -10
- package/src/analysis/source-index.ts +56 -4
- package/src/analysis/workspace-routes.ts +385 -0
- package/src/analysis/workspace.ts +281 -34
- package/src/app-map-changes.ts +31 -5
- package/src/app-map.ts +75 -75
- package/src/config.ts +33 -0
- package/src/index.ts +25 -0
- package/src/inspect.ts +123 -16
- package/src/lint.ts +146 -43
- package/src/make/shared.ts +1 -1
- package/src/make.ts +31 -1
- package/src/provider-add.ts +2 -2
- package/src/provider-audit.ts +30 -10
- package/src/templates/shared.ts +3 -3
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import { createSourceIndex, type SourceIndex } from "./source-index.js";
|
|
3
|
+
import type { AnalysisWorkspace } from "./workspace.js";
|
|
4
|
+
|
|
5
|
+
type LocalDeclarations = Map<string, ts.Declaration>;
|
|
6
|
+
type ServerCall = {
|
|
7
|
+
file: string;
|
|
8
|
+
call: ts.CallExpression;
|
|
9
|
+
locals: LocalDeclarations;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/** Follow declarative route composition without executing application modules. */
|
|
13
|
+
export async function workspaceRouteContracts(
|
|
14
|
+
workspace: AnalysisWorkspace,
|
|
15
|
+
): Promise<{
|
|
16
|
+
contracts: Set<string>;
|
|
17
|
+
groups: Set<string>;
|
|
18
|
+
unresolved: boolean;
|
|
19
|
+
}> {
|
|
20
|
+
const sourceIndex = createSourceIndex(workspace);
|
|
21
|
+
const contracts = new Set<string>();
|
|
22
|
+
const groups = new Set<string>();
|
|
23
|
+
const visited = new Set<ts.Node>();
|
|
24
|
+
const active = new Set<ts.Node>();
|
|
25
|
+
let unresolved = false;
|
|
26
|
+
|
|
27
|
+
async function follow(
|
|
28
|
+
file: string,
|
|
29
|
+
expression: ts.Expression,
|
|
30
|
+
locals: LocalDeclarations = new Map(),
|
|
31
|
+
): Promise<void> {
|
|
32
|
+
if (
|
|
33
|
+
ts.isAsExpression(expression) ||
|
|
34
|
+
ts.isSatisfiesExpression(expression) ||
|
|
35
|
+
ts.isParenthesizedExpression(expression)
|
|
36
|
+
) {
|
|
37
|
+
return follow(file, expression.expression, locals);
|
|
38
|
+
}
|
|
39
|
+
if (
|
|
40
|
+
ts.isIdentifier(expression) ||
|
|
41
|
+
ts.isPropertyAccessExpression(expression)
|
|
42
|
+
) {
|
|
43
|
+
const local = ts.isIdentifier(expression)
|
|
44
|
+
? locals.get(expression.text)
|
|
45
|
+
: undefined;
|
|
46
|
+
const reference = local
|
|
47
|
+
? { file, declaration: local }
|
|
48
|
+
: ts.isIdentifier(expression)
|
|
49
|
+
? await sourceIndex.resolveName(file, expression.text)
|
|
50
|
+
: await sourceIndex.resolveExpression(file, expression);
|
|
51
|
+
if (
|
|
52
|
+
!reference?.declaration ||
|
|
53
|
+
!ts.isVariableDeclaration(reference.declaration) ||
|
|
54
|
+
!reference.declaration.initializer
|
|
55
|
+
) {
|
|
56
|
+
unresolved = true;
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const key = reference.declaration;
|
|
60
|
+
if (active.has(key)) {
|
|
61
|
+
unresolved = true;
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (visited.has(key)) return;
|
|
65
|
+
visited.add(key);
|
|
66
|
+
active.add(key);
|
|
67
|
+
await follow(
|
|
68
|
+
reference.file,
|
|
69
|
+
reference.declaration.initializer,
|
|
70
|
+
local ? locals : new Map(),
|
|
71
|
+
);
|
|
72
|
+
active.delete(key);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (ts.isArrayLiteralExpression(expression)) {
|
|
76
|
+
for (const item of expression.elements) {
|
|
77
|
+
if (ts.isSpreadElement(item))
|
|
78
|
+
await follow(file, item.expression, locals);
|
|
79
|
+
else if (ts.isExpression(item)) await follow(file, item, locals);
|
|
80
|
+
else unresolved = true;
|
|
81
|
+
}
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (ts.isCallExpression(expression)) {
|
|
85
|
+
const name = ts.isIdentifier(expression.expression)
|
|
86
|
+
? expression.expression.text
|
|
87
|
+
: undefined;
|
|
88
|
+
if (
|
|
89
|
+
(name === "defineRoutes" ||
|
|
90
|
+
name === "defineRouteGroup" ||
|
|
91
|
+
name === "defineRoute") &&
|
|
92
|
+
expression.arguments[0]
|
|
93
|
+
) {
|
|
94
|
+
if (name === "defineRouteGroup") {
|
|
95
|
+
let parent = expression.parent;
|
|
96
|
+
while (
|
|
97
|
+
ts.isAsExpression(parent) ||
|
|
98
|
+
ts.isSatisfiesExpression(parent) ||
|
|
99
|
+
ts.isParenthesizedExpression(parent)
|
|
100
|
+
)
|
|
101
|
+
parent = parent.parent;
|
|
102
|
+
if (ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name))
|
|
103
|
+
groups.add(`${file}#${parent.name.text}`);
|
|
104
|
+
}
|
|
105
|
+
return follow(file, expression.arguments[0], locals);
|
|
106
|
+
}
|
|
107
|
+
unresolved = true;
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (ts.isObjectLiteralExpression(expression)) {
|
|
111
|
+
if (!explicitObject(expression)) {
|
|
112
|
+
unresolved = true;
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
let recognized = false;
|
|
116
|
+
for (const member of expression.properties) {
|
|
117
|
+
if (ts.isSpreadAssignment(member)) {
|
|
118
|
+
unresolved = true;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (
|
|
122
|
+
!ts.isPropertyAssignment(member) &&
|
|
123
|
+
!ts.isShorthandPropertyAssignment(member)
|
|
124
|
+
)
|
|
125
|
+
continue;
|
|
126
|
+
const name =
|
|
127
|
+
ts.isIdentifier(member.name) || ts.isStringLiteralLike(member.name)
|
|
128
|
+
? member.name.text
|
|
129
|
+
: undefined;
|
|
130
|
+
const value = ts.isPropertyAssignment(member)
|
|
131
|
+
? member.initializer
|
|
132
|
+
: member.name;
|
|
133
|
+
if (name === "routes") {
|
|
134
|
+
recognized = true;
|
|
135
|
+
await follow(file, value, locals);
|
|
136
|
+
}
|
|
137
|
+
if (name === "contract") {
|
|
138
|
+
recognized = true;
|
|
139
|
+
const reference = await sourceIndex.resolveValue(file, value, locals);
|
|
140
|
+
if (reference)
|
|
141
|
+
contracts.add(`${reference.file}#${reference.exportName}`);
|
|
142
|
+
else unresolved = true;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (!recognized) unresolved = true;
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
unresolved = true;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const serverFile = workspace.config.paths.server;
|
|
152
|
+
if (!workspace.fileSet.has(serverFile))
|
|
153
|
+
return { contracts, groups, unresolved: true };
|
|
154
|
+
const selected = await selectedServerCall(workspace, sourceIndex);
|
|
155
|
+
if (!selected) return { contracts, groups, unresolved: true };
|
|
156
|
+
const { file, call, locals } = selected;
|
|
157
|
+
const options = call.arguments[0];
|
|
158
|
+
if (
|
|
159
|
+
!options ||
|
|
160
|
+
!ts.isObjectLiteralExpression(options) ||
|
|
161
|
+
!explicitObject(options)
|
|
162
|
+
) {
|
|
163
|
+
return { contracts, groups, unresolved: true };
|
|
164
|
+
}
|
|
165
|
+
const routes = options.properties.find(
|
|
166
|
+
(member) =>
|
|
167
|
+
member.name &&
|
|
168
|
+
(ts.isIdentifier(member.name) || ts.isStringLiteralLike(member.name)) &&
|
|
169
|
+
member.name.text === "routes",
|
|
170
|
+
);
|
|
171
|
+
if (
|
|
172
|
+
routes &&
|
|
173
|
+
(ts.isPropertyAssignment(routes) ||
|
|
174
|
+
ts.isShorthandPropertyAssignment(routes))
|
|
175
|
+
) {
|
|
176
|
+
await follow(
|
|
177
|
+
file,
|
|
178
|
+
ts.isPropertyAssignment(routes) ? routes.initializer : routes.name,
|
|
179
|
+
locals,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
return { contracts, groups, unresolved };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Trace only the exported server or the value returned by its lazy loader. */
|
|
186
|
+
async function selectedServerCall(
|
|
187
|
+
workspace: AnalysisWorkspace,
|
|
188
|
+
sourceIndex: SourceIndex,
|
|
189
|
+
): Promise<ServerCall | undefined> {
|
|
190
|
+
const file = workspace.config.paths.server;
|
|
191
|
+
const roots = (
|
|
192
|
+
await Promise.all(
|
|
193
|
+
["server", "getServer"].map((name) =>
|
|
194
|
+
sourceIndex.resolveExport(file, name),
|
|
195
|
+
),
|
|
196
|
+
)
|
|
197
|
+
).filter((reference) => reference !== undefined);
|
|
198
|
+
if (roots.length !== 1) return undefined;
|
|
199
|
+
const root = roots[0];
|
|
200
|
+
if (
|
|
201
|
+
!root.declaration ||
|
|
202
|
+
!ts.isVariableDeclaration(root.declaration) ||
|
|
203
|
+
!root.declaration.initializer ||
|
|
204
|
+
!ts.isVariableDeclarationList(root.declaration.parent) ||
|
|
205
|
+
!(root.declaration.parent.flags & ts.NodeFlags.Const)
|
|
206
|
+
)
|
|
207
|
+
return undefined;
|
|
208
|
+
const visited = new Set<ts.Node>();
|
|
209
|
+
return follow(root.file, root.declaration.initializer, new Map());
|
|
210
|
+
|
|
211
|
+
async function follow(
|
|
212
|
+
file: string,
|
|
213
|
+
expression: ts.Expression,
|
|
214
|
+
locals: LocalDeclarations,
|
|
215
|
+
): Promise<ServerCall | undefined> {
|
|
216
|
+
if (visited.has(expression)) return undefined;
|
|
217
|
+
visited.add(expression);
|
|
218
|
+
if (
|
|
219
|
+
ts.isAsExpression(expression) ||
|
|
220
|
+
ts.isSatisfiesExpression(expression) ||
|
|
221
|
+
ts.isParenthesizedExpression(expression) ||
|
|
222
|
+
ts.isAwaitExpression(expression)
|
|
223
|
+
)
|
|
224
|
+
return follow(file, expression.expression, locals);
|
|
225
|
+
if (ts.isIdentifier(expression)) {
|
|
226
|
+
const local = locals.get(expression.text);
|
|
227
|
+
const reference = local
|
|
228
|
+
? { file, declaration: local }
|
|
229
|
+
: await sourceIndex.resolveName(file, expression.text);
|
|
230
|
+
const declaration = reference?.declaration;
|
|
231
|
+
if (
|
|
232
|
+
!reference ||
|
|
233
|
+
!declaration ||
|
|
234
|
+
!ts.isVariableDeclaration(declaration) ||
|
|
235
|
+
!declaration.initializer ||
|
|
236
|
+
!ts.isVariableDeclarationList(declaration.parent) ||
|
|
237
|
+
!(declaration.parent.flags & ts.NodeFlags.Const)
|
|
238
|
+
)
|
|
239
|
+
return undefined;
|
|
240
|
+
return follow(
|
|
241
|
+
reference.file,
|
|
242
|
+
declaration.initializer,
|
|
243
|
+
local ? locals : new Map(),
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
// The canonical loader wraps stop() while preserving the server API.
|
|
247
|
+
if (ts.isObjectLiteralExpression(expression)) {
|
|
248
|
+
const [spread, ...overrides] = expression.properties;
|
|
249
|
+
if (
|
|
250
|
+
!spread ||
|
|
251
|
+
!ts.isSpreadAssignment(spread) ||
|
|
252
|
+
overrides.some(
|
|
253
|
+
(property) =>
|
|
254
|
+
!property.name ||
|
|
255
|
+
!ts.isIdentifier(property.name) ||
|
|
256
|
+
property.name.text !== "stop",
|
|
257
|
+
)
|
|
258
|
+
)
|
|
259
|
+
return undefined;
|
|
260
|
+
return follow(file, spread.expression, locals);
|
|
261
|
+
}
|
|
262
|
+
if (!ts.isCallExpression(expression)) return undefined;
|
|
263
|
+
const factory = importedFactory(
|
|
264
|
+
await workspace.readSourceFile(file),
|
|
265
|
+
expression.expression,
|
|
266
|
+
locals,
|
|
267
|
+
);
|
|
268
|
+
if (
|
|
269
|
+
factory ===
|
|
270
|
+
(workspace.config.framework === "next"
|
|
271
|
+
? "createNextServer"
|
|
272
|
+
: "createFetchServer")
|
|
273
|
+
)
|
|
274
|
+
return { file, call: expression, locals };
|
|
275
|
+
if (
|
|
276
|
+
factory !== "createNextServerLoader" ||
|
|
277
|
+
workspace.config.framework !== "next"
|
|
278
|
+
)
|
|
279
|
+
return undefined;
|
|
280
|
+
const callback = expression.arguments[0];
|
|
281
|
+
if (
|
|
282
|
+
!callback ||
|
|
283
|
+
(!ts.isArrowFunction(callback) && !ts.isFunctionExpression(callback)) ||
|
|
284
|
+
callback.parameters.length
|
|
285
|
+
)
|
|
286
|
+
return undefined;
|
|
287
|
+
if (!ts.isBlock(callback.body)) return follow(file, callback.body, locals);
|
|
288
|
+
const scoped = new Map(locals);
|
|
289
|
+
let returned: ts.Expression | undefined;
|
|
290
|
+
for (const statement of callback.body.statements) {
|
|
291
|
+
if (returned) return undefined;
|
|
292
|
+
if (ts.isVariableStatement(statement)) {
|
|
293
|
+
for (const declaration of statement.declarationList.declarations)
|
|
294
|
+
addLocal(scoped, declaration.name, declaration);
|
|
295
|
+
} else if (ts.isFunctionDeclaration(statement) && statement.name) {
|
|
296
|
+
scoped.set(statement.name.text, statement);
|
|
297
|
+
} else if (ts.isReturnStatement(statement)) {
|
|
298
|
+
if (!statement.expression) return undefined;
|
|
299
|
+
returned = statement.expression;
|
|
300
|
+
} else if (
|
|
301
|
+
!ts.isExpressionStatement(statement) &&
|
|
302
|
+
!ts.isFunctionDeclaration(statement) &&
|
|
303
|
+
!ts.isEmptyStatement(statement)
|
|
304
|
+
)
|
|
305
|
+
return undefined;
|
|
306
|
+
}
|
|
307
|
+
return returned ? follow(file, returned, scoped) : undefined;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function importedFactory(
|
|
312
|
+
source: ts.SourceFile,
|
|
313
|
+
expression: ts.Expression,
|
|
314
|
+
locals: LocalDeclarations,
|
|
315
|
+
): string | undefined {
|
|
316
|
+
const receiver = ts.isIdentifier(expression)
|
|
317
|
+
? expression
|
|
318
|
+
: ts.isPropertyAccessExpression(expression) &&
|
|
319
|
+
ts.isIdentifier(expression.expression)
|
|
320
|
+
? expression.expression
|
|
321
|
+
: undefined;
|
|
322
|
+
if (!receiver || locals.has(receiver.text)) return undefined;
|
|
323
|
+
for (const statement of source.statements) {
|
|
324
|
+
if (
|
|
325
|
+
!ts.isImportDeclaration(statement) ||
|
|
326
|
+
!ts.isStringLiteral(statement.moduleSpecifier) ||
|
|
327
|
+
statement.importClause?.isTypeOnly
|
|
328
|
+
)
|
|
329
|
+
continue;
|
|
330
|
+
const bindings = statement.importClause?.namedBindings;
|
|
331
|
+
const name =
|
|
332
|
+
bindings && ts.isNamedImports(bindings) && ts.isIdentifier(expression)
|
|
333
|
+
? bindings.elements.find(
|
|
334
|
+
(element) =>
|
|
335
|
+
element.name.text === receiver.text && !element.isTypeOnly,
|
|
336
|
+
)
|
|
337
|
+
: undefined;
|
|
338
|
+
const imported = name
|
|
339
|
+
? (name.propertyName ?? name.name).text
|
|
340
|
+
: bindings &&
|
|
341
|
+
ts.isNamespaceImport(bindings) &&
|
|
342
|
+
bindings.name.text === receiver.text &&
|
|
343
|
+
ts.isPropertyAccessExpression(expression)
|
|
344
|
+
? expression.name.text
|
|
345
|
+
: undefined;
|
|
346
|
+
if (
|
|
347
|
+
statement.moduleSpecifier.text === "@beignet/next" &&
|
|
348
|
+
(imported === "createNextServer" || imported === "createNextServerLoader")
|
|
349
|
+
)
|
|
350
|
+
return imported;
|
|
351
|
+
if (
|
|
352
|
+
statement.moduleSpecifier.text === "@beignet/web" &&
|
|
353
|
+
imported === "createFetchServer"
|
|
354
|
+
)
|
|
355
|
+
return imported;
|
|
356
|
+
}
|
|
357
|
+
return undefined;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function addLocal(
|
|
361
|
+
locals: LocalDeclarations,
|
|
362
|
+
name: ts.BindingName,
|
|
363
|
+
declaration: ts.Declaration,
|
|
364
|
+
): void {
|
|
365
|
+
if (ts.isIdentifier(name)) locals.set(name.text, declaration);
|
|
366
|
+
else
|
|
367
|
+
for (const element of name.elements)
|
|
368
|
+
if (ts.isBindingElement(element)) addLocal(locals, element.name, element);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function explicitObject(object: ts.ObjectLiteralExpression): boolean {
|
|
372
|
+
const names = new Set<string>();
|
|
373
|
+
for (const property of object.properties) {
|
|
374
|
+
if (
|
|
375
|
+
ts.isSpreadAssignment(property) ||
|
|
376
|
+
!property.name ||
|
|
377
|
+
(!ts.isIdentifier(property.name) &&
|
|
378
|
+
!ts.isStringLiteralLike(property.name))
|
|
379
|
+
)
|
|
380
|
+
return false;
|
|
381
|
+
if (names.has(property.name.text)) return false;
|
|
382
|
+
names.add(property.name.text);
|
|
383
|
+
}
|
|
384
|
+
return true;
|
|
385
|
+
}
|