@beignet/cli 0.0.39 → 0.0.41
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 +18 -0
- package/README.md +110 -16
- package/dist/analysis/source-index.d.ts +38 -0
- package/dist/analysis/source-index.d.ts.map +1 -0
- package/dist/analysis/source-index.js +271 -0
- package/dist/analysis/source-index.js.map +1 -0
- package/dist/analysis/workspace.d.ts +16 -0
- package/dist/analysis/workspace.d.ts.map +1 -0
- package/dist/analysis/workspace.js +134 -0
- package/dist/analysis/workspace.js.map +1 -0
- package/dist/app-map-schema.d.ts +5 -0
- package/dist/app-map-schema.d.ts.map +1 -0
- package/dist/app-map-schema.js +26 -0
- package/dist/app-map-schema.js.map +1 -0
- package/dist/app-map.d.ts +96 -0
- package/dist/app-map.d.ts.map +1 -0
- package/dist/app-map.js +1141 -0
- package/dist/app-map.js.map +1 -0
- package/dist/check.d.ts +7 -0
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +20 -5
- package/dist/check.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +4 -2
- package/dist/config.js.map +1 -1
- package/dist/db.d.ts +32 -7
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +182 -14
- package/dist/db.js.map +1 -1
- package/dist/explain.d.ts +98 -0
- package/dist/explain.d.ts.map +1 -0
- package/dist/explain.js +512 -0
- package/dist/explain.js.map +1 -0
- package/dist/framework.d.ts +3 -0
- package/dist/framework.d.ts.map +1 -0
- package/dist/framework.js +6 -0
- package/dist/framework.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +79 -2
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts +4 -2
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +173 -36
- package/dist/inspect.js.map +1 -1
- package/dist/lib.d.ts +4 -0
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js +2 -0
- package/dist/lib.js.map +1 -1
- package/dist/make/payments.d.ts.map +1 -1
- package/dist/make/payments.js +2 -0
- package/dist/make/payments.js.map +1 -1
- package/dist/make.d.ts.map +1 -1
- package/dist/make.js +157 -3
- package/dist/make.js.map +1 -1
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +69 -1
- package/dist/mcp.js.map +1 -1
- package/dist/preflight.d.ts.map +1 -1
- package/dist/preflight.js +10 -0
- package/dist/preflight.js.map +1 -1
- package/dist/provider-add.d.ts.map +1 -1
- package/dist/provider-add.js +167 -13
- package/dist/provider-add.js.map +1 -1
- package/dist/provider-audit.d.ts +6 -0
- package/dist/provider-audit.d.ts.map +1 -1
- package/dist/provider-audit.js +63 -15
- package/dist/provider-audit.js.map +1 -1
- package/dist/templates/agents.d.ts.map +1 -1
- package/dist/templates/agents.js +17 -4
- package/dist/templates/agents.js.map +1 -1
- package/dist/templates/base.d.ts.map +1 -1
- package/dist/templates/base.js +10 -4
- package/dist/templates/base.js.map +1 -1
- package/dist/templates/index.d.ts +1 -1
- package/dist/templates/index.d.ts.map +1 -1
- package/dist/templates/index.js +8 -1
- package/dist/templates/index.js.map +1 -1
- package/dist/templates/server.d.ts +3 -0
- package/dist/templates/server.d.ts.map +1 -1
- package/dist/templates/server.js +55 -1
- package/dist/templates/server.js.map +1 -1
- package/dist/templates/shadcn.js +1 -1
- package/dist/templates/shared.js +1 -1
- package/package.json +2 -2
- package/skills/app-structure/SKILL.md +38 -5
- package/src/analysis/source-index.ts +395 -0
- package/src/analysis/workspace.ts +180 -0
- package/src/app-map-schema.ts +28 -0
- package/src/app-map.ts +1705 -0
- package/src/check.ts +27 -4
- package/src/config.ts +7 -3
- package/src/db.ts +232 -14
- package/src/explain.ts +786 -0
- package/src/framework.ts +13 -0
- package/src/index.ts +113 -2
- package/src/inspect.ts +259 -40
- package/src/lib.ts +36 -0
- package/src/make/payments.ts +7 -0
- package/src/make.ts +232 -2
- package/src/mcp.ts +107 -1
- package/src/preflight.ts +14 -0
- package/src/provider-add.ts +211 -12
- package/src/provider-audit.ts +127 -22
- package/src/templates/agents.ts +17 -4
- package/src/templates/base.ts +22 -4
- package/src/templates/index.ts +18 -1
- package/src/templates/server.ts +58 -1
- package/src/templates/shadcn.ts +1 -1
- package/src/templates/shared.ts +1 -1
- package/src/test-helpers/generated-app.ts +1 -1
package/src/framework.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ResolvedBeignetConfig } from "./config.js";
|
|
2
|
+
|
|
3
|
+
export function assertNextFramework(
|
|
4
|
+
config: ResolvedBeignetConfig,
|
|
5
|
+
command: string,
|
|
6
|
+
generatedSurface: string,
|
|
7
|
+
): void {
|
|
8
|
+
if (config.framework === "next") return;
|
|
9
|
+
|
|
10
|
+
throw new Error(
|
|
11
|
+
`${command} currently supports framework: "next" because it generates ${generatedSurface}. For framework: "web", define the framework-neutral artifact directly and mount its HTTP entrypoint through @beignet/web.`,
|
|
12
|
+
);
|
|
13
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
type StricliProcess,
|
|
14
14
|
text_en,
|
|
15
15
|
} from "@stricli/core";
|
|
16
|
+
import { type AppMapNodeKind, appMapNodeKinds } from "./app-map-schema.js";
|
|
16
17
|
import {
|
|
17
18
|
type CompletionShell,
|
|
18
19
|
type CreateProviderName,
|
|
@@ -36,6 +37,7 @@ import {
|
|
|
36
37
|
} from "./choices.js";
|
|
37
38
|
// Type-only: the runtime module stays behind the create command's lazy loader.
|
|
38
39
|
import type { CreateSelections } from "./create-prompts.js";
|
|
40
|
+
import type { ExplainTargetKind } from "./explain.js";
|
|
39
41
|
import type { AddProviderPresetResult } from "./provider-add.js";
|
|
40
42
|
import { getCliVersion } from "./version.js";
|
|
41
43
|
|
|
@@ -106,6 +108,13 @@ type RoutesFlags = {
|
|
|
106
108
|
cwd?: string;
|
|
107
109
|
};
|
|
108
110
|
|
|
111
|
+
type MapFlags = RoutesFlags & {
|
|
112
|
+
feature?: string;
|
|
113
|
+
kind?: readonly AppMapNodeKind[];
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
type ExplainFlags = RoutesFlags;
|
|
117
|
+
|
|
109
118
|
type LintFlags = {
|
|
110
119
|
json?: boolean;
|
|
111
120
|
cwd?: string;
|
|
@@ -366,6 +375,21 @@ const routesFlagParameters = {
|
|
|
366
375
|
cwd: cwdFlag,
|
|
367
376
|
} satisfies FlagParametersForType<RoutesFlags, CliContext>;
|
|
368
377
|
|
|
378
|
+
const mapFlagParameters = {
|
|
379
|
+
json: jsonFlag,
|
|
380
|
+
feature: parsedStringFlag(
|
|
381
|
+
"Project one feature and its direct relationships.",
|
|
382
|
+
),
|
|
383
|
+
kind: {
|
|
384
|
+
kind: "enum",
|
|
385
|
+
values: appMapNodeKinds,
|
|
386
|
+
optional: true,
|
|
387
|
+
variadic: ",",
|
|
388
|
+
brief: "Include only these node kinds. Accepts comma-separated values.",
|
|
389
|
+
},
|
|
390
|
+
cwd: cwdFlag,
|
|
391
|
+
} satisfies FlagParametersForType<MapFlags, CliContext>;
|
|
392
|
+
|
|
369
393
|
const lintFlagParameters = {
|
|
370
394
|
json: jsonFlag,
|
|
371
395
|
cwd: cwdFlag,
|
|
@@ -585,6 +609,19 @@ const providerPresetPositional = {
|
|
|
585
609
|
],
|
|
586
610
|
} as const;
|
|
587
611
|
|
|
612
|
+
function explainTargetPositional(kind: ExplainTargetKind) {
|
|
613
|
+
return {
|
|
614
|
+
kind: "tuple",
|
|
615
|
+
parameters: [
|
|
616
|
+
{
|
|
617
|
+
parse: parseString,
|
|
618
|
+
placeholder: kind,
|
|
619
|
+
brief: `${kind[0]?.toUpperCase()}${kind.slice(1)} to explain.`,
|
|
620
|
+
},
|
|
621
|
+
],
|
|
622
|
+
} as const;
|
|
623
|
+
}
|
|
624
|
+
|
|
588
625
|
function resolveOutputFormat(flags: {
|
|
589
626
|
json?: boolean;
|
|
590
627
|
format?: OutputFormat;
|
|
@@ -829,6 +866,77 @@ const routesCommand = buildCommand<RoutesFlags, [], CliContext>({
|
|
|
829
866
|
},
|
|
830
867
|
});
|
|
831
868
|
|
|
869
|
+
const mapCommand = buildCommand<MapFlags, [], CliContext>({
|
|
870
|
+
docs: {
|
|
871
|
+
brief: "Map the app's architecture and registered workflows.",
|
|
872
|
+
fullDescription:
|
|
873
|
+
"Build a deterministic graph of features, HTTP contracts, routes, use cases, policies, workflows, ports, providers, OpenAPI, tests, dependencies, and doctor findings.",
|
|
874
|
+
},
|
|
875
|
+
parameters: {
|
|
876
|
+
flags: mapFlagParameters,
|
|
877
|
+
},
|
|
878
|
+
loader: async () => {
|
|
879
|
+
const { formatAppMap, mapApp, projectAppMap } = await import(
|
|
880
|
+
"./app-map.js"
|
|
881
|
+
);
|
|
882
|
+
|
|
883
|
+
return async function runMap(this: CliContext, flags: MapFlags) {
|
|
884
|
+
const result = projectAppMap(
|
|
885
|
+
await mapApp({ cwd: flags.cwd, strict: true }),
|
|
886
|
+
{
|
|
887
|
+
feature: flags.feature,
|
|
888
|
+
kinds: flags.kind,
|
|
889
|
+
},
|
|
890
|
+
);
|
|
891
|
+
writeOutput(
|
|
892
|
+
this,
|
|
893
|
+
flags.json ? JSON.stringify(result, null, 2) : formatAppMap(result),
|
|
894
|
+
);
|
|
895
|
+
};
|
|
896
|
+
},
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
function explainCommand(kind: ExplainTargetKind) {
|
|
900
|
+
return buildCommand<ExplainFlags, [string], CliContext>({
|
|
901
|
+
docs: {
|
|
902
|
+
brief: `Explain one mapped Beignet ${kind}.`,
|
|
903
|
+
fullDescription:
|
|
904
|
+
"Returns source-backed relationships, conventions, findings, suggested files, and follow-up commands without generating advice or changing the app.",
|
|
905
|
+
},
|
|
906
|
+
parameters: {
|
|
907
|
+
flags: routesFlagParameters,
|
|
908
|
+
positional: explainTargetPositional(kind),
|
|
909
|
+
},
|
|
910
|
+
loader: async () => {
|
|
911
|
+
const { explainApp, formatExplain } = await import("./explain.js");
|
|
912
|
+
|
|
913
|
+
return async function runExplain(
|
|
914
|
+
this: CliContext,
|
|
915
|
+
flags: ExplainFlags,
|
|
916
|
+
target: string,
|
|
917
|
+
) {
|
|
918
|
+
const result = await explainApp({ kind, target, cwd: flags.cwd });
|
|
919
|
+
writeOutput(
|
|
920
|
+
this,
|
|
921
|
+
flags.json ? JSON.stringify(result, null, 2) : formatExplain(result),
|
|
922
|
+
);
|
|
923
|
+
};
|
|
924
|
+
},
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
const explainRoutes = buildRouteMap({
|
|
929
|
+
docs: {
|
|
930
|
+
brief: "Explain mapped Beignet app concepts with source evidence.",
|
|
931
|
+
},
|
|
932
|
+
routes: {
|
|
933
|
+
diagnostic: explainCommand("diagnostic"),
|
|
934
|
+
feature: explainCommand("feature"),
|
|
935
|
+
provider: explainCommand("provider"),
|
|
936
|
+
route: explainCommand("route"),
|
|
937
|
+
},
|
|
938
|
+
});
|
|
939
|
+
|
|
832
940
|
const doctorCommand = buildCommand<DoctorFlags, [], CliContext>({
|
|
833
941
|
docs: {
|
|
834
942
|
brief: "Inspect app wiring and framework conventions.",
|
|
@@ -1035,7 +1143,7 @@ const lintCommand = buildCommand<LintFlags, [], CliContext>({
|
|
|
1035
1143
|
const mcpCommand = buildCommand<McpFlags, [], CliContext>({
|
|
1036
1144
|
docs: {
|
|
1037
1145
|
brief:
|
|
1038
|
-
"Run a Model Context Protocol server exposing
|
|
1146
|
+
"Run a Model Context Protocol server exposing the app map, validation, and generators over stdio.",
|
|
1039
1147
|
},
|
|
1040
1148
|
parameters: {
|
|
1041
1149
|
flags: {
|
|
@@ -2109,7 +2217,7 @@ const makeRoutes = buildRouteMap({
|
|
|
2109
2217
|
const rootRoutes = buildRouteMap({
|
|
2110
2218
|
docs: {
|
|
2111
2219
|
brief: "Beignet CLI",
|
|
2112
|
-
fullDescription: `Create apps, generate framework files, inspect routes, and check Beignet conventions.
|
|
2220
|
+
fullDescription: `Create apps, generate framework files, map and explain architecture, inspect routes, and check Beignet conventions.
|
|
2113
2221
|
|
|
2114
2222
|
Run npm create beignet@latest (or bun create beignet) to scaffold a new app.`,
|
|
2115
2223
|
},
|
|
@@ -2119,7 +2227,9 @@ Run npm create beignet@latest (or bun create beignet) to scaffold a new app.`,
|
|
|
2119
2227
|
create: createCommand,
|
|
2120
2228
|
db: dbRoutes,
|
|
2121
2229
|
doctor: doctorCommand,
|
|
2230
|
+
explain: explainRoutes,
|
|
2122
2231
|
lint: lintCommand,
|
|
2232
|
+
map: mapCommand,
|
|
2123
2233
|
make: makeRoutes,
|
|
2124
2234
|
mcp: mcpCommand,
|
|
2125
2235
|
outbox: outboxRoutes,
|
|
@@ -2431,6 +2541,7 @@ ${openStep}
|
|
|
2431
2541
|
|
|
2432
2542
|
Inspect the app:
|
|
2433
2543
|
${cli} routes
|
|
2544
|
+
${cli} map
|
|
2434
2545
|
${cli} check
|
|
2435
2546
|
|
|
2436
2547
|
Generate a feature:
|
package/src/inspect.ts
CHANGED
|
@@ -105,7 +105,7 @@ type InspectedCatalogErrorRef = {
|
|
|
105
105
|
export type InspectedRoute = InspectedContract & {
|
|
106
106
|
handlerFile?: string;
|
|
107
107
|
handlerExport?: HttpMethod;
|
|
108
|
-
handlerSource: "next-route" | "route-local" | "missing";
|
|
108
|
+
handlerSource: "next-route" | "route-local" | "web-server" | "missing";
|
|
109
109
|
};
|
|
110
110
|
|
|
111
111
|
/**
|
|
@@ -132,11 +132,13 @@ export type InspectFix = {
|
|
|
132
132
|
* Detected app convention and missing convention files.
|
|
133
133
|
*/
|
|
134
134
|
export type InspectConvention = {
|
|
135
|
-
kind: "standard" | "next" | "custom";
|
|
135
|
+
kind: "standard" | "next" | "web" | "custom";
|
|
136
136
|
nextLayout: boolean;
|
|
137
|
+
webLayout: boolean;
|
|
137
138
|
resourceGenerator: boolean;
|
|
138
139
|
configFile?: string;
|
|
139
140
|
missingNextLayout: string[];
|
|
141
|
+
missingWebLayout: string[];
|
|
140
142
|
missingResourceGenerator: string[];
|
|
141
143
|
};
|
|
142
144
|
|
|
@@ -204,13 +206,12 @@ export async function inspectApp(
|
|
|
204
206
|
: await loadBeignetConfig(targetDir, files);
|
|
205
207
|
const convention = inspectConvention(files, config);
|
|
206
208
|
const contracts = await readContracts(targetDir, files, config);
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
209
|
+
const matchedRoutes = await inspectRouteSurface(
|
|
210
|
+
targetDir,
|
|
211
|
+
files,
|
|
212
|
+
config,
|
|
213
|
+
contracts,
|
|
211
214
|
);
|
|
212
|
-
const routeExports = await readRouteExports(targetDir, routeFiles, config);
|
|
213
|
-
const matchedRoutes = matchRoutes(contracts, routeExports);
|
|
214
215
|
const diagnostics = await inspectDiagnostics(
|
|
215
216
|
targetDir,
|
|
216
217
|
files,
|
|
@@ -246,13 +247,12 @@ export async function applyDoctorFixes(
|
|
|
246
247
|
: await loadBeignetConfig(targetDir, files);
|
|
247
248
|
const convention = inspectConvention(files, config);
|
|
248
249
|
const contracts = await readContracts(targetDir, files, config);
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
250
|
+
const matchedRoutes = await inspectRouteSurface(
|
|
251
|
+
targetDir,
|
|
252
|
+
files,
|
|
253
|
+
config,
|
|
254
|
+
contracts,
|
|
253
255
|
);
|
|
254
|
-
const routeExports = await readRouteExports(targetDir, routeFiles, config);
|
|
255
|
-
const matchedRoutes = matchRoutes(contracts, routeExports);
|
|
256
256
|
const fixes: InspectFix[] = [];
|
|
257
257
|
|
|
258
258
|
const packageFix = await fixPackageScripts(targetDir, files, convention);
|
|
@@ -318,7 +318,9 @@ export function formatRoutes(result: InspectAppResult): string {
|
|
|
318
318
|
handler:
|
|
319
319
|
route.handlerSource === "missing"
|
|
320
320
|
? "missing"
|
|
321
|
-
:
|
|
321
|
+
: route.handlerSource === "web-server"
|
|
322
|
+
? `${route.handlerFile ?? "unknown"}:registry`
|
|
323
|
+
: `${route.handlerFile ?? "unknown"}:${route.handlerExport ?? route.method}`,
|
|
322
324
|
}));
|
|
323
325
|
|
|
324
326
|
return table([
|
|
@@ -556,6 +558,10 @@ function inspectConvention(
|
|
|
556
558
|
`${directoryPath(config.paths.routes)}/`,
|
|
557
559
|
config.paths.server,
|
|
558
560
|
]);
|
|
561
|
+
const missingWebLayout = missingRequirements(files, [
|
|
562
|
+
`${directoryPath(config.paths.contracts)}/`,
|
|
563
|
+
config.paths.server,
|
|
564
|
+
]);
|
|
559
565
|
const missingResourceGenerator = missingRequirements(files, [
|
|
560
566
|
config.paths.appContext,
|
|
561
567
|
config.paths.portWiring,
|
|
@@ -564,14 +570,23 @@ function inspectConvention(
|
|
|
564
570
|
config.paths.useCaseBuilder,
|
|
565
571
|
]);
|
|
566
572
|
const nextLayout = missingNextLayout.length === 0;
|
|
573
|
+
const webLayout = missingWebLayout.length === 0;
|
|
567
574
|
const resourceGenerator = missingResourceGenerator.length === 0;
|
|
568
575
|
|
|
569
576
|
return {
|
|
570
|
-
kind: resourceGenerator
|
|
577
|
+
kind: resourceGenerator
|
|
578
|
+
? "standard"
|
|
579
|
+
: config.framework === "web" && webLayout
|
|
580
|
+
? "web"
|
|
581
|
+
: nextLayout
|
|
582
|
+
? "next"
|
|
583
|
+
: "custom",
|
|
571
584
|
nextLayout,
|
|
585
|
+
webLayout,
|
|
572
586
|
resourceGenerator,
|
|
573
587
|
configFile: config.configFile,
|
|
574
588
|
missingNextLayout,
|
|
589
|
+
missingWebLayout,
|
|
575
590
|
missingResourceGenerator,
|
|
576
591
|
};
|
|
577
592
|
}
|
|
@@ -603,7 +618,7 @@ function parseContracts(source: string, file: string): InspectedContract[] {
|
|
|
603
618
|
for (const exportMatch of source.matchAll(exportRegex)) {
|
|
604
619
|
const block = exportMatch[2];
|
|
605
620
|
const methodMatch =
|
|
606
|
-
/([A-Za-z_$][\w$]*)(?:\s*\.\s*[A-Za-z_$][\w$]*\([^)]*\))*\s*\.\s*(get|post|put|patch|delete|head|options)\(\s*["']([^"']+)["']
|
|
621
|
+
/([A-Za-z_$][\w$]*)(?:\s*\.\s*[A-Za-z_$][\w$]*\([^)]*\))*\s*\.\s*(get|post|put|patch|delete|head|options)\(\s*["']([^"']+)["'](?:\s*,\s*["'][^"']+["'])?\s*\)/.exec(
|
|
607
622
|
block,
|
|
608
623
|
);
|
|
609
624
|
if (methodMatch) {
|
|
@@ -1289,6 +1304,73 @@ function matchRoutes(
|
|
|
1289
1304
|
};
|
|
1290
1305
|
}
|
|
1291
1306
|
|
|
1307
|
+
async function inspectRouteSurface(
|
|
1308
|
+
targetDir: string,
|
|
1309
|
+
files: string[],
|
|
1310
|
+
config: ResolvedBeignetConfig,
|
|
1311
|
+
contracts: InspectedContract[],
|
|
1312
|
+
): Promise<MatchedRoutesResult> {
|
|
1313
|
+
if (config.framework === "next") {
|
|
1314
|
+
const routeFiles = files.filter(
|
|
1315
|
+
(file) =>
|
|
1316
|
+
file.startsWith(`${directoryPath(config.paths.routes)}/`) &&
|
|
1317
|
+
file.endsWith("/route.ts"),
|
|
1318
|
+
);
|
|
1319
|
+
const routeExports = await readRouteExports(targetDir, routeFiles, config);
|
|
1320
|
+
return matchRoutes(contracts, routeExports);
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
if (!files.includes(config.paths.server)) {
|
|
1324
|
+
return {
|
|
1325
|
+
routes: contracts.map((contract) => ({
|
|
1326
|
+
...contract,
|
|
1327
|
+
handlerSource: "missing",
|
|
1328
|
+
})),
|
|
1329
|
+
unmatchedRouteHandlers: [],
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
const serverSource = await readFile(
|
|
1334
|
+
path.join(targetDir, config.paths.server),
|
|
1335
|
+
"utf8",
|
|
1336
|
+
);
|
|
1337
|
+
const routeGroups = await readFeatureRouteGroups(targetDir, files, config);
|
|
1338
|
+
const registeredGroups = await registeredRouteGroupsForServer(
|
|
1339
|
+
targetDir,
|
|
1340
|
+
files,
|
|
1341
|
+
config,
|
|
1342
|
+
serverSource,
|
|
1343
|
+
);
|
|
1344
|
+
const registeredContracts = contractsForRegisteredRouteGroups(
|
|
1345
|
+
contracts,
|
|
1346
|
+
routeGroups,
|
|
1347
|
+
registeredGroups,
|
|
1348
|
+
);
|
|
1349
|
+
const registeredKeys = new Set(registeredContracts.map(contractKey));
|
|
1350
|
+
const handlerFile =
|
|
1351
|
+
routeRegistryFileFromServerSource(
|
|
1352
|
+
serverSource,
|
|
1353
|
+
config.paths.server,
|
|
1354
|
+
files,
|
|
1355
|
+
) ?? config.paths.server;
|
|
1356
|
+
|
|
1357
|
+
return {
|
|
1358
|
+
routes: contracts.map((contract) =>
|
|
1359
|
+
registeredKeys.has(contractKey(contract))
|
|
1360
|
+
? {
|
|
1361
|
+
...contract,
|
|
1362
|
+
handlerFile,
|
|
1363
|
+
handlerSource: "web-server" as const,
|
|
1364
|
+
}
|
|
1365
|
+
: {
|
|
1366
|
+
...contract,
|
|
1367
|
+
handlerSource: "missing" as const,
|
|
1368
|
+
},
|
|
1369
|
+
),
|
|
1370
|
+
unmatchedRouteHandlers: [],
|
|
1371
|
+
};
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1292
1374
|
function findContract(
|
|
1293
1375
|
contracts: InspectedContract[],
|
|
1294
1376
|
routeExport: RouteExport,
|
|
@@ -1349,28 +1431,53 @@ async function inspectDiagnostics(
|
|
|
1349
1431
|
strict: boolean,
|
|
1350
1432
|
): Promise<InspectDiagnostic[]> {
|
|
1351
1433
|
const diagnostics: InspectDiagnostic[] = [];
|
|
1352
|
-
|
|
1353
|
-
|
|
1434
|
+
const frameworkLayout =
|
|
1435
|
+
config.framework === "next" ? convention.nextLayout : convention.webLayout;
|
|
1436
|
+
const missingFrameworkLayout =
|
|
1437
|
+
config.framework === "next"
|
|
1438
|
+
? convention.missingNextLayout
|
|
1439
|
+
: convention.missingWebLayout;
|
|
1440
|
+
|
|
1441
|
+
if (!frameworkLayout) {
|
|
1442
|
+
const expectedLayout =
|
|
1443
|
+
config.framework === "next"
|
|
1444
|
+
? `${directoryPath(config.paths.contracts)}/, ${directoryPath(config.paths.routes)}/, and ${config.paths.server}`
|
|
1445
|
+
: `${directoryPath(config.paths.contracts)}/ and ${config.paths.server}`;
|
|
1446
|
+
const layoutName =
|
|
1447
|
+
config.framework === "next"
|
|
1448
|
+
? "standard Beignet app layout"
|
|
1449
|
+
: "standard Beignet web app layout";
|
|
1354
1450
|
diagnostics.push({
|
|
1355
1451
|
severity: "warning",
|
|
1356
1452
|
code: "BEIGNET_APP_LAYOUT_NOT_FOUND",
|
|
1357
|
-
message: `This directory does not match the
|
|
1453
|
+
message: `This directory does not match the ${layoutName}. CLI inspection expects ${expectedLayout}. Missing: ${missingFrameworkLayout.join(", ")}. Add the missing app files or configure their paths in beignet.config.ts.`,
|
|
1358
1454
|
});
|
|
1359
1455
|
}
|
|
1360
1456
|
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1457
|
+
diagnostics.push(
|
|
1458
|
+
...(await inspectFrameworkAdapter(targetDir, files, config)),
|
|
1459
|
+
);
|
|
1460
|
+
|
|
1461
|
+
if (config.framework === "next" || !convention.resourceGenerator) {
|
|
1462
|
+
for (const route of matchedRoutes.routes) {
|
|
1463
|
+
if (route.handlerSource !== "missing") continue;
|
|
1464
|
+
diagnostics.push({
|
|
1465
|
+
severity: "error",
|
|
1466
|
+
code: "BEIGNET_ROUTE_MISSING",
|
|
1467
|
+
file: route.file,
|
|
1468
|
+
contract: route.exportName,
|
|
1469
|
+
message:
|
|
1470
|
+
config.framework === "next"
|
|
1471
|
+
? `${route.exportName} (${route.method} ${route.path}) has no matching Next route handler. Add ${methodArticle(route.method)} ${route.method} export to ${directoryPath(config.paths.routes)}/[[...path]]/route.ts or a matching route file, or remove the unused contract.`
|
|
1472
|
+
: `${route.exportName} (${route.method} ${route.path}) is not registered in the route surface passed to createFetchServer(...). Register the contract through defineRoutes(...) or remove the unused contract.`,
|
|
1473
|
+
});
|
|
1474
|
+
}
|
|
1370
1475
|
}
|
|
1371
1476
|
|
|
1372
|
-
|
|
1373
|
-
|
|
1477
|
+
if (config.framework === "next") {
|
|
1478
|
+
for (const routeHandler of matchedRoutes.unmatchedRouteHandlers) {
|
|
1479
|
+
diagnostics.push(unmatchedRouteDiagnostic(routeHandler));
|
|
1480
|
+
}
|
|
1374
1481
|
}
|
|
1375
1482
|
|
|
1376
1483
|
diagnostics.push(
|
|
@@ -1414,6 +1521,7 @@ async function inspectDiagnostics(
|
|
|
1414
1521
|
config,
|
|
1415
1522
|
convention,
|
|
1416
1523
|
)),
|
|
1524
|
+
...(await inspectInngestProductionPath(targetDir, files, config)),
|
|
1417
1525
|
...(await inspectDatabaseLifecycleDrift(
|
|
1418
1526
|
targetDir,
|
|
1419
1527
|
files,
|
|
@@ -1460,6 +1568,39 @@ async function inspectDiagnostics(
|
|
|
1460
1568
|
return dedupeDiagnostics(diagnostics);
|
|
1461
1569
|
}
|
|
1462
1570
|
|
|
1571
|
+
async function inspectFrameworkAdapter(
|
|
1572
|
+
targetDir: string,
|
|
1573
|
+
files: string[],
|
|
1574
|
+
config: ResolvedBeignetConfig,
|
|
1575
|
+
): Promise<InspectDiagnostic[]> {
|
|
1576
|
+
if (!files.includes(config.paths.server)) return [];
|
|
1577
|
+
|
|
1578
|
+
const source = await readFile(
|
|
1579
|
+
path.join(targetDir, config.paths.server),
|
|
1580
|
+
"utf8",
|
|
1581
|
+
);
|
|
1582
|
+
const expectedFactory =
|
|
1583
|
+
config.framework === "next" ? "createNextServer" : "createFetchServer";
|
|
1584
|
+
const otherFactory =
|
|
1585
|
+
config.framework === "next" ? "createFetchServer" : "createNextServer";
|
|
1586
|
+
|
|
1587
|
+
if (
|
|
1588
|
+
containsCallExpression(source, expectedFactory) ||
|
|
1589
|
+
!containsCallExpression(source, otherFactory)
|
|
1590
|
+
) {
|
|
1591
|
+
return [];
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
return [
|
|
1595
|
+
{
|
|
1596
|
+
severity: "error",
|
|
1597
|
+
code: "BEIGNET_FRAMEWORK_MISMATCH",
|
|
1598
|
+
file: config.paths.server,
|
|
1599
|
+
message: `${config.configFile ?? "beignet.config.*"} selects framework: "${config.framework}", but ${config.paths.server} calls ${otherFactory}(...). Use ${expectedFactory}(...) or update the configured framework profile.`,
|
|
1600
|
+
},
|
|
1601
|
+
];
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1463
1604
|
async function inspectCanonicalConformance(
|
|
1464
1605
|
targetDir: string,
|
|
1465
1606
|
files: string[],
|
|
@@ -1626,11 +1767,13 @@ async function inspectCanonicalConformance(
|
|
|
1626
1767
|
}
|
|
1627
1768
|
|
|
1628
1769
|
if (!importsAppContext(serverSource, config.paths.server, config, files)) {
|
|
1770
|
+
const serverFactory =
|
|
1771
|
+
config.framework === "next" ? "createNextServer" : "createFetchServer";
|
|
1629
1772
|
diagnostics.push({
|
|
1630
1773
|
severity: "warning",
|
|
1631
1774
|
code: "BEIGNET_SERVER_APP_CONTEXT",
|
|
1632
1775
|
file: config.paths.server,
|
|
1633
|
-
message: `${config.paths.server} should import type { AppContext } from ${config.paths.appContext} and thread that context through
|
|
1776
|
+
message: `${config.paths.server} should import type { AppContext } from ${config.paths.appContext} and thread that context through ${serverFactory} and hooks.`,
|
|
1634
1777
|
});
|
|
1635
1778
|
}
|
|
1636
1779
|
|
|
@@ -2333,7 +2476,7 @@ async function inspectProductionReadiness(
|
|
|
2333
2476
|
}
|
|
2334
2477
|
|
|
2335
2478
|
if (installedPackages.has("@beignet/provider-auth-better-auth")) {
|
|
2336
|
-
if (!hasBetterAuthRoute(files, config)) {
|
|
2479
|
+
if (config.framework === "next" && !hasBetterAuthRoute(files, config)) {
|
|
2337
2480
|
diagnostics.push({
|
|
2338
2481
|
severity: "warning",
|
|
2339
2482
|
code: "BEIGNET_AUTH_ROUTE_MISSING",
|
|
@@ -2589,7 +2732,10 @@ async function inspectPaymentsProductionReadiness(
|
|
|
2589
2732
|
});
|
|
2590
2733
|
}
|
|
2591
2734
|
|
|
2592
|
-
if (
|
|
2735
|
+
if (
|
|
2736
|
+
config.framework === "next" &&
|
|
2737
|
+
!(await hasPaymentWebhookRoute(targetDir, files, config, sourceCache))
|
|
2738
|
+
) {
|
|
2593
2739
|
diagnostics.push({
|
|
2594
2740
|
severity: "warning",
|
|
2595
2741
|
code: "BEIGNET_PAYMENTS_WEBHOOK_ROUTE_MISSING",
|
|
@@ -2950,6 +3096,73 @@ async function inspectProviderRegistrationDrift(
|
|
|
2950
3096
|
return diagnostics;
|
|
2951
3097
|
}
|
|
2952
3098
|
|
|
3099
|
+
async function inspectInngestProductionPath(
|
|
3100
|
+
targetDir: string,
|
|
3101
|
+
files: string[],
|
|
3102
|
+
config: ResolvedBeignetConfig,
|
|
3103
|
+
): Promise<InspectDiagnostic[]> {
|
|
3104
|
+
const packageJson = await readPackageJson(targetDir, files);
|
|
3105
|
+
if (
|
|
3106
|
+
!installedPackageNames(packageJson).has("@beignet/provider-jobs-inngest")
|
|
3107
|
+
) {
|
|
3108
|
+
return [];
|
|
3109
|
+
}
|
|
3110
|
+
|
|
3111
|
+
const diagnostics: InspectDiagnostic[] = [];
|
|
3112
|
+
const serverDir = directoryPath(path.dirname(config.paths.server));
|
|
3113
|
+
const registryFile = `${serverDir}/inngest.ts`;
|
|
3114
|
+
const routeFile = `${directoryPath(config.paths.routes)}/inngest/route.ts`;
|
|
3115
|
+
|
|
3116
|
+
if (!files.includes(registryFile)) {
|
|
3117
|
+
diagnostics.push({
|
|
3118
|
+
severity: "warning",
|
|
3119
|
+
code: "BEIGNET_INNGEST_REGISTRY_MISSING",
|
|
3120
|
+
file: registryFile,
|
|
3121
|
+
message: `@beignet/provider-jobs-inngest is installed, but ${registryFile} is missing. Run beignet provider add jobs-inngest or create a central createInngestJobFunctions(...) registry so dispatched jobs can execute.`,
|
|
3122
|
+
});
|
|
3123
|
+
} else {
|
|
3124
|
+
const registrySource = await readFile(
|
|
3125
|
+
path.join(targetDir, registryFile),
|
|
3126
|
+
"utf8",
|
|
3127
|
+
);
|
|
3128
|
+
const registry = arrayInitializerInfo(registrySource, "inngestJobs");
|
|
3129
|
+
const registeredJobs = registry
|
|
3130
|
+
? identifiersFromArrayExpression(registry.text)
|
|
3131
|
+
: new Set<string>();
|
|
3132
|
+
const featureRoot = directoryPath(config.paths.features);
|
|
3133
|
+
const jobIndexes = files.filter((file) =>
|
|
3134
|
+
new RegExp(`^${escapeRegExp(featureRoot)}/[^/]+/jobs/index\\.ts$`).test(
|
|
3135
|
+
file,
|
|
3136
|
+
),
|
|
3137
|
+
);
|
|
3138
|
+
for (const jobIndex of jobIndexes) {
|
|
3139
|
+
const source = await readFile(path.join(targetDir, jobIndex), "utf8");
|
|
3140
|
+
const match = source.match(/export const\s+([A-Za-z_$][\w$]*Jobs)\s*=/);
|
|
3141
|
+
const registryName = match?.[1];
|
|
3142
|
+
if (!registryName || registeredJobs.has(registryName)) {
|
|
3143
|
+
continue;
|
|
3144
|
+
}
|
|
3145
|
+
diagnostics.push({
|
|
3146
|
+
severity: "warning",
|
|
3147
|
+
code: "BEIGNET_INNGEST_JOB_REGISTRATION_MISSING",
|
|
3148
|
+
file: registryFile,
|
|
3149
|
+
message: `${jobIndex} exports ${registryName}, but it is not registered in inngestJobs in ${registryFile}. Add ...${registryName} so Inngest can execute the job.`,
|
|
3150
|
+
});
|
|
3151
|
+
}
|
|
3152
|
+
}
|
|
3153
|
+
|
|
3154
|
+
if (config.framework === "next" && !files.includes(routeFile)) {
|
|
3155
|
+
diagnostics.push({
|
|
3156
|
+
severity: "warning",
|
|
3157
|
+
code: "BEIGNET_INNGEST_ROUTE_MISSING",
|
|
3158
|
+
file: routeFile,
|
|
3159
|
+
message: `@beignet/provider-jobs-inngest is installed in a Next.js app, but ${routeFile} is missing. Expose the shared Inngest client and function registry with serve(...) so Inngest can discover and invoke jobs.`,
|
|
3160
|
+
});
|
|
3161
|
+
}
|
|
3162
|
+
|
|
3163
|
+
return diagnostics;
|
|
3164
|
+
}
|
|
3165
|
+
|
|
2953
3166
|
function isFeatureUploadDefinition(
|
|
2954
3167
|
file: string,
|
|
2955
3168
|
config: ResolvedBeignetConfig,
|
|
@@ -4213,7 +4426,7 @@ function callReferencesCentralListenerRegistry(args: {
|
|
|
4213
4426
|
|
|
4214
4427
|
for (const identifier of args.callIdentifiers) {
|
|
4215
4428
|
const imported = args.namedImports.get(identifier);
|
|
4216
|
-
if (
|
|
4429
|
+
if (imported?.importedName !== "listeners") continue;
|
|
4217
4430
|
|
|
4218
4431
|
const importedFile = sourceFileFromImport(
|
|
4219
4432
|
imported.sourcePath,
|
|
@@ -4260,6 +4473,7 @@ async function inspectServerlessFootguns(
|
|
|
4260
4473
|
const source = await readFile(path.join(targetDir, file), "utf8");
|
|
4261
4474
|
|
|
4262
4475
|
if (
|
|
4476
|
+
config.framework === "next" &&
|
|
4263
4477
|
convention.resourceGenerator &&
|
|
4264
4478
|
isNextRouteFile(file, config) &&
|
|
4265
4479
|
importsEagerServer(source, file, files, config)
|
|
@@ -6461,15 +6675,19 @@ async function registeredRouteGroupsForServer(
|
|
|
6461
6675
|
serverSource: string,
|
|
6462
6676
|
): Promise<Set<string>> {
|
|
6463
6677
|
const registeredGroups = registeredRouteGroups(serverSource);
|
|
6464
|
-
const imports =
|
|
6678
|
+
const imports = parseNamedImportSources(serverSource);
|
|
6465
6679
|
|
|
6466
6680
|
for (const identifier of routeOptionIdentifiers(serverSource)) {
|
|
6467
6681
|
const imported = imports.get(identifier);
|
|
6468
|
-
if (!imported
|
|
6469
|
-
|
|
6470
|
-
|
|
6682
|
+
if (!imported) continue;
|
|
6683
|
+
const importedFile = sourceFileFromImport(
|
|
6684
|
+
imported.sourcePath,
|
|
6685
|
+
config.paths.server,
|
|
6686
|
+
files,
|
|
6687
|
+
);
|
|
6688
|
+
if (!importedFile || !files.includes(importedFile)) continue;
|
|
6471
6689
|
|
|
6472
|
-
const source = await readFile(path.join(targetDir,
|
|
6690
|
+
const source = await readFile(path.join(targetDir, importedFile), {
|
|
6473
6691
|
encoding: "utf8",
|
|
6474
6692
|
});
|
|
6475
6693
|
for (const routeGroup of registeredRouteGroups(source)) {
|
|
@@ -7458,6 +7676,7 @@ async function inspectResourceSlices(
|
|
|
7458
7676
|
);
|
|
7459
7677
|
}
|
|
7460
7678
|
if (
|
|
7679
|
+
config.framework === "next" &&
|
|
7461
7680
|
!hasRequirement(files, routeFile) &&
|
|
7462
7681
|
!hasRequirement(files, catchAllRouteFile)
|
|
7463
7682
|
) {
|