@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.
Files changed (68) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +131 -1
  3. package/dist/analysis/layers.d.ts +2 -0
  4. package/dist/analysis/layers.d.ts.map +1 -1
  5. package/dist/analysis/layers.js +5 -0
  6. package/dist/analysis/layers.js.map +1 -1
  7. package/dist/analysis/port-wiring.d.ts +4 -2
  8. package/dist/analysis/port-wiring.d.ts.map +1 -1
  9. package/dist/analysis/port-wiring.js +39 -10
  10. package/dist/analysis/port-wiring.js.map +1 -1
  11. package/dist/analysis/source-index.d.ts +2 -1
  12. package/dist/analysis/source-index.d.ts.map +1 -1
  13. package/dist/analysis/source-index.js +34 -5
  14. package/dist/analysis/source-index.js.map +1 -1
  15. package/dist/analysis/workspace-routes.d.ts +8 -0
  16. package/dist/analysis/workspace-routes.d.ts.map +1 -0
  17. package/dist/analysis/workspace-routes.js +291 -0
  18. package/dist/analysis/workspace-routes.js.map +1 -0
  19. package/dist/analysis/workspace.d.ts +24 -3
  20. package/dist/analysis/workspace.d.ts.map +1 -1
  21. package/dist/analysis/workspace.js +166 -21
  22. package/dist/analysis/workspace.js.map +1 -1
  23. package/dist/app-map-changes.d.ts.map +1 -1
  24. package/dist/app-map-changes.js +19 -7
  25. package/dist/app-map-changes.js.map +1 -1
  26. package/dist/app-map.d.ts.map +1 -1
  27. package/dist/app-map.js +58 -55
  28. package/dist/app-map.js.map +1 -1
  29. package/dist/config.d.ts +7 -0
  30. package/dist/config.d.ts.map +1 -1
  31. package/dist/config.js +19 -0
  32. package/dist/config.js.map +1 -1
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +20 -0
  35. package/dist/index.js.map +1 -1
  36. package/dist/inspect.d.ts.map +1 -1
  37. package/dist/inspect.js +89 -21
  38. package/dist/inspect.js.map +1 -1
  39. package/dist/lint.d.ts.map +1 -1
  40. package/dist/lint.js +106 -38
  41. package/dist/lint.js.map +1 -1
  42. package/dist/make/shared.js +1 -1
  43. package/dist/make.js +18 -1
  44. package/dist/make.js.map +1 -1
  45. package/dist/provider-add.js +2 -2
  46. package/dist/provider-add.js.map +1 -1
  47. package/dist/provider-audit.d.ts.map +1 -1
  48. package/dist/provider-audit.js +22 -6
  49. package/dist/provider-audit.js.map +1 -1
  50. package/dist/templates/shared.js +3 -3
  51. package/package.json +3 -2
  52. package/skills/app-structure/SKILL.md +42 -0
  53. package/src/analysis/layers.ts +9 -0
  54. package/src/analysis/port-wiring.ts +66 -10
  55. package/src/analysis/source-index.ts +56 -4
  56. package/src/analysis/workspace-routes.ts +385 -0
  57. package/src/analysis/workspace.ts +281 -34
  58. package/src/app-map-changes.ts +31 -5
  59. package/src/app-map.ts +75 -75
  60. package/src/config.ts +33 -0
  61. package/src/index.ts +25 -0
  62. package/src/inspect.ts +123 -16
  63. package/src/lint.ts +146 -43
  64. package/src/make/shared.ts +1 -1
  65. package/src/make.ts +31 -1
  66. package/src/provider-add.ts +2 -2
  67. package/src/provider-audit.ts +30 -10
  68. 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
+ }