@beignet/cli 0.0.39 → 0.0.40
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 +9 -0
- package/README.md +34 -6
- 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/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/inspect.d.ts +4 -2
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +120 -33
- package/dist/inspect.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 +94 -1
- package/dist/make.js.map +1 -1
- package/dist/templates/shadcn.js +1 -1
- package/package.json +2 -2
- package/skills/app-structure/SKILL.md +16 -1
- package/src/config.ts +7 -3
- package/src/framework.ts +13 -0
- package/src/inspect.ts +189 -38
- package/src/make/payments.ts +7 -0
- package/src/make.ts +142 -0
- package/src/templates/shadcn.ts +1 -1
- package/src/test-helpers/generated-app.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@beignet/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.40",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "CLI for creating and maintaining Beignet apps.",
|
|
6
6
|
"main": "./dist/lib.js",
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"tsx": "^4.22.4"
|
|
70
70
|
},
|
|
71
71
|
"dependencies": {
|
|
72
|
-
"@beignet/core": "^0.0.
|
|
72
|
+
"@beignet/core": "^0.0.40",
|
|
73
73
|
"@clack/prompts": "^1.5.1",
|
|
74
74
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
75
75
|
"@stricli/core": "^1.2.7",
|
|
@@ -41,6 +41,7 @@ beignet make job billing.sync
|
|
|
41
41
|
beignet make schedule billing.nightly --cron "0 2 * * *"
|
|
42
42
|
beignet make task admin.backfill
|
|
43
43
|
beignet make seed demo.users
|
|
44
|
+
beignet make outbox
|
|
44
45
|
beignet make upload issues.attachment
|
|
45
46
|
beignet make upload issues.attachment --ui
|
|
46
47
|
```
|
|
@@ -75,6 +76,12 @@ Hand-written files do not run until registered:
|
|
|
75
76
|
every job worker or outbox registry that can receive it
|
|
76
77
|
- seeds -> the app-owned seed entrypoint, usually `server/seed.ts`
|
|
77
78
|
|
|
79
|
+
For generated Next.js 15.1+ apps, `make outbox` also adds a server-local
|
|
80
|
+
provider that wraps the database Unit of Work with a push-assisted `after()`
|
|
81
|
+
trigger. Keep the generated cron route: it is the recovery path for missed
|
|
82
|
+
callbacks, delayed messages, retries, and additional batches. Older Next.js
|
|
83
|
+
apps remain cron-only.
|
|
84
|
+
|
|
78
85
|
When an app opts into runtime integrity, keep `server/runtime-integrity.ts`
|
|
79
86
|
aligned with those central registries. The boot check is pure and
|
|
80
87
|
serverless-safe: it compares declared listeners, schedules, tasks, and outbox
|
|
@@ -89,7 +96,11 @@ low-risk route-group, schedule, task, outbox, and listener registry repair.
|
|
|
89
96
|
|
|
90
97
|
## Path Config
|
|
91
98
|
|
|
92
|
-
`beignet.config.*` moves paths; it
|
|
99
|
+
`beignet.config.*` selects the inspected server adapter and moves paths; it
|
|
100
|
+
does not change the application architecture. `framework: "next"` is the
|
|
101
|
+
default. Use `framework: "web"` for `@beignet/web` Fetch servers so `routes`
|
|
102
|
+
and `doctor` inspect the central `defineRoutes(...)` registry instead of
|
|
103
|
+
requiring Next.js `app/api/` handlers.
|
|
93
104
|
|
|
94
105
|
Common `src/` apps configure:
|
|
95
106
|
|
|
@@ -107,6 +118,10 @@ export default defineConfig({
|
|
|
107
118
|
});
|
|
108
119
|
```
|
|
109
120
|
|
|
121
|
+
Framework-neutral generators work in either profile. `make payments`,
|
|
122
|
+
`make upload`, `make outbox`, and `make schedule --route` still generate
|
|
123
|
+
Next.js handlers and reject the web profile before writing files.
|
|
124
|
+
|
|
110
125
|
When `appContext` moves under `src/`, root client setup follows that source
|
|
111
126
|
root at `src/client/index.ts`. Other registries and app-bound builders are not
|
|
112
127
|
relocated implicitly; override each path the app has moved, such as
|
package/src/config.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { createJiti } from "jiti";
|
|
|
6
6
|
/**
|
|
7
7
|
* Supported application framework targets for Beignet config.
|
|
8
8
|
*/
|
|
9
|
-
export type BeignetFramework = "next";
|
|
9
|
+
export type BeignetFramework = "next" | "web";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Configurable app paths used by CLI generators, inspection, linting, and
|
|
@@ -306,11 +306,15 @@ function assertConfigObject(
|
|
|
306
306
|
throw new Error(`${configFile} must export a Beignet config object.`);
|
|
307
307
|
}
|
|
308
308
|
|
|
309
|
-
if (
|
|
309
|
+
if (
|
|
310
|
+
config.framework !== undefined &&
|
|
311
|
+
config.framework !== "next" &&
|
|
312
|
+
config.framework !== "web"
|
|
313
|
+
) {
|
|
310
314
|
throw new Error(
|
|
311
315
|
`${configFile} uses unsupported framework "${String(
|
|
312
316
|
config.framework,
|
|
313
|
-
)}". Supported
|
|
317
|
+
)}". Supported frameworks: "next", "web".`,
|
|
314
318
|
);
|
|
315
319
|
}
|
|
316
320
|
|
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/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
|
}
|
|
@@ -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(
|
|
@@ -1460,6 +1567,39 @@ async function inspectDiagnostics(
|
|
|
1460
1567
|
return dedupeDiagnostics(diagnostics);
|
|
1461
1568
|
}
|
|
1462
1569
|
|
|
1570
|
+
async function inspectFrameworkAdapter(
|
|
1571
|
+
targetDir: string,
|
|
1572
|
+
files: string[],
|
|
1573
|
+
config: ResolvedBeignetConfig,
|
|
1574
|
+
): Promise<InspectDiagnostic[]> {
|
|
1575
|
+
if (!files.includes(config.paths.server)) return [];
|
|
1576
|
+
|
|
1577
|
+
const source = await readFile(
|
|
1578
|
+
path.join(targetDir, config.paths.server),
|
|
1579
|
+
"utf8",
|
|
1580
|
+
);
|
|
1581
|
+
const expectedFactory =
|
|
1582
|
+
config.framework === "next" ? "createNextServer" : "createFetchServer";
|
|
1583
|
+
const otherFactory =
|
|
1584
|
+
config.framework === "next" ? "createFetchServer" : "createNextServer";
|
|
1585
|
+
|
|
1586
|
+
if (
|
|
1587
|
+
containsCallExpression(source, expectedFactory) ||
|
|
1588
|
+
!containsCallExpression(source, otherFactory)
|
|
1589
|
+
) {
|
|
1590
|
+
return [];
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
return [
|
|
1594
|
+
{
|
|
1595
|
+
severity: "error",
|
|
1596
|
+
code: "BEIGNET_FRAMEWORK_MISMATCH",
|
|
1597
|
+
file: config.paths.server,
|
|
1598
|
+
message: `${config.configFile ?? "beignet.config.*"} selects framework: "${config.framework}", but ${config.paths.server} calls ${otherFactory}(...). Use ${expectedFactory}(...) or update the configured framework profile.`,
|
|
1599
|
+
},
|
|
1600
|
+
];
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1463
1603
|
async function inspectCanonicalConformance(
|
|
1464
1604
|
targetDir: string,
|
|
1465
1605
|
files: string[],
|
|
@@ -1626,11 +1766,13 @@ async function inspectCanonicalConformance(
|
|
|
1626
1766
|
}
|
|
1627
1767
|
|
|
1628
1768
|
if (!importsAppContext(serverSource, config.paths.server, config, files)) {
|
|
1769
|
+
const serverFactory =
|
|
1770
|
+
config.framework === "next" ? "createNextServer" : "createFetchServer";
|
|
1629
1771
|
diagnostics.push({
|
|
1630
1772
|
severity: "warning",
|
|
1631
1773
|
code: "BEIGNET_SERVER_APP_CONTEXT",
|
|
1632
1774
|
file: config.paths.server,
|
|
1633
|
-
message: `${config.paths.server} should import type { AppContext } from ${config.paths.appContext} and thread that context through
|
|
1775
|
+
message: `${config.paths.server} should import type { AppContext } from ${config.paths.appContext} and thread that context through ${serverFactory} and hooks.`,
|
|
1634
1776
|
});
|
|
1635
1777
|
}
|
|
1636
1778
|
|
|
@@ -2333,7 +2475,7 @@ async function inspectProductionReadiness(
|
|
|
2333
2475
|
}
|
|
2334
2476
|
|
|
2335
2477
|
if (installedPackages.has("@beignet/provider-auth-better-auth")) {
|
|
2336
|
-
if (!hasBetterAuthRoute(files, config)) {
|
|
2478
|
+
if (config.framework === "next" && !hasBetterAuthRoute(files, config)) {
|
|
2337
2479
|
diagnostics.push({
|
|
2338
2480
|
severity: "warning",
|
|
2339
2481
|
code: "BEIGNET_AUTH_ROUTE_MISSING",
|
|
@@ -2589,7 +2731,10 @@ async function inspectPaymentsProductionReadiness(
|
|
|
2589
2731
|
});
|
|
2590
2732
|
}
|
|
2591
2733
|
|
|
2592
|
-
if (
|
|
2734
|
+
if (
|
|
2735
|
+
config.framework === "next" &&
|
|
2736
|
+
!(await hasPaymentWebhookRoute(targetDir, files, config, sourceCache))
|
|
2737
|
+
) {
|
|
2593
2738
|
diagnostics.push({
|
|
2594
2739
|
severity: "warning",
|
|
2595
2740
|
code: "BEIGNET_PAYMENTS_WEBHOOK_ROUTE_MISSING",
|
|
@@ -4260,6 +4405,7 @@ async function inspectServerlessFootguns(
|
|
|
4260
4405
|
const source = await readFile(path.join(targetDir, file), "utf8");
|
|
4261
4406
|
|
|
4262
4407
|
if (
|
|
4408
|
+
config.framework === "next" &&
|
|
4263
4409
|
convention.resourceGenerator &&
|
|
4264
4410
|
isNextRouteFile(file, config) &&
|
|
4265
4411
|
importsEagerServer(source, file, files, config)
|
|
@@ -6461,15 +6607,19 @@ async function registeredRouteGroupsForServer(
|
|
|
6461
6607
|
serverSource: string,
|
|
6462
6608
|
): Promise<Set<string>> {
|
|
6463
6609
|
const registeredGroups = registeredRouteGroups(serverSource);
|
|
6464
|
-
const imports =
|
|
6610
|
+
const imports = parseNamedImportSources(serverSource);
|
|
6465
6611
|
|
|
6466
6612
|
for (const identifier of routeOptionIdentifiers(serverSource)) {
|
|
6467
6613
|
const imported = imports.get(identifier);
|
|
6468
|
-
if (!imported
|
|
6469
|
-
|
|
6470
|
-
|
|
6614
|
+
if (!imported) continue;
|
|
6615
|
+
const importedFile = sourceFileFromImport(
|
|
6616
|
+
imported.sourcePath,
|
|
6617
|
+
config.paths.server,
|
|
6618
|
+
files,
|
|
6619
|
+
);
|
|
6620
|
+
if (!importedFile || !files.includes(importedFile)) continue;
|
|
6471
6621
|
|
|
6472
|
-
const source = await readFile(path.join(targetDir,
|
|
6622
|
+
const source = await readFile(path.join(targetDir, importedFile), {
|
|
6473
6623
|
encoding: "utf8",
|
|
6474
6624
|
});
|
|
6475
6625
|
for (const routeGroup of registeredRouteGroups(source)) {
|
|
@@ -7458,6 +7608,7 @@ async function inspectResourceSlices(
|
|
|
7458
7608
|
);
|
|
7459
7609
|
}
|
|
7460
7610
|
if (
|
|
7611
|
+
config.framework === "next" &&
|
|
7461
7612
|
!hasRequirement(files, routeFile) &&
|
|
7462
7613
|
!hasRequirement(files, catchAllRouteFile)
|
|
7463
7614
|
) {
|
package/src/make/payments.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
type ResolvedBeignetConfig,
|
|
7
7
|
resolveConfig,
|
|
8
8
|
} from "../config.js";
|
|
9
|
+
import { assertNextFramework } from "../framework.js";
|
|
9
10
|
import { appendToNamedArray, insertAfterImports } from "../registry-edits.js";
|
|
10
11
|
import { testSupportTemplateFiles } from "../templates/testing.js";
|
|
11
12
|
import {
|
|
@@ -71,6 +72,12 @@ export async function makePayments(
|
|
|
71
72
|
? resolveConfig(options.config)
|
|
72
73
|
: await loadBeignetConfig(targetDir);
|
|
73
74
|
|
|
75
|
+
assertNextFramework(
|
|
76
|
+
config,
|
|
77
|
+
"beignet make payments",
|
|
78
|
+
"Next.js App Router billing and webhook handlers",
|
|
79
|
+
);
|
|
80
|
+
|
|
74
81
|
await assertStandardApp(targetDir, config);
|
|
75
82
|
|
|
76
83
|
const persistence = await detectResourcePersistence(targetDir, config);
|
package/src/make.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
type ResolvedBeignetConfig,
|
|
12
12
|
resolveConfig,
|
|
13
13
|
} from "./config.js";
|
|
14
|
+
import { assertNextFramework } from "./framework.js";
|
|
14
15
|
import {
|
|
15
16
|
adapterFilePath,
|
|
16
17
|
addNamedImport,
|
|
@@ -143,6 +144,7 @@ import {
|
|
|
143
144
|
} from "./make/shared.js";
|
|
144
145
|
import {
|
|
145
146
|
appendToArrayExpression,
|
|
147
|
+
appendToNamedArray,
|
|
146
148
|
appendToOutboxRegistryArray,
|
|
147
149
|
arrayInitializerInfo,
|
|
148
150
|
identifiersFromArrayExpression,
|
|
@@ -1499,6 +1501,14 @@ export async function makeSchedule(
|
|
|
1499
1501
|
? resolveConfig(options.config)
|
|
1500
1502
|
: await loadBeignetConfig(targetDir);
|
|
1501
1503
|
|
|
1504
|
+
if (options.route) {
|
|
1505
|
+
assertNextFramework(
|
|
1506
|
+
config,
|
|
1507
|
+
"beignet make schedule --route",
|
|
1508
|
+
"a Next.js App Router cron handler",
|
|
1509
|
+
);
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1502
1512
|
await assertFeatureArtifactApp(targetDir, config, "schedule", {
|
|
1503
1513
|
requireServer: Boolean(options.route),
|
|
1504
1514
|
});
|
|
@@ -1714,6 +1724,9 @@ async function updateOutboxPortWiring(
|
|
|
1714
1724
|
path.join(infrastructureDir(config), "db/provider.ts"),
|
|
1715
1725
|
);
|
|
1716
1726
|
}
|
|
1727
|
+
if (await updateOutboxDrainProvider(targetDir, config, options)) {
|
|
1728
|
+
changes.updatedFiles.push(providersFilePath(config));
|
|
1729
|
+
}
|
|
1717
1730
|
if (await updateOutboxRepositoriesReturnType(targetDir, config, options)) {
|
|
1718
1731
|
changes.updatedFiles.push(drizzleRepositoriesPath(config));
|
|
1719
1732
|
}
|
|
@@ -1825,6 +1838,31 @@ async function updateOutboxInfrastructurePorts(
|
|
|
1825
1838
|
return true;
|
|
1826
1839
|
}
|
|
1827
1840
|
|
|
1841
|
+
async function projectSupportsNextAfter(targetDir: string): Promise<boolean> {
|
|
1842
|
+
const packageJson = JSON.parse(
|
|
1843
|
+
await readFile(path.join(targetDir, "package.json"), "utf8"),
|
|
1844
|
+
) as PackageJsonLike;
|
|
1845
|
+
const version =
|
|
1846
|
+
packageJson.dependencies?.next ?? packageJson.devDependencies?.next;
|
|
1847
|
+
if (!version) return false;
|
|
1848
|
+
|
|
1849
|
+
return version.split("||").every((range) => {
|
|
1850
|
+
const lowerBound =
|
|
1851
|
+
/^\s*(\^|~|>=|>|=|<=|<)?\s*v?(\d+)(?:\.(\d+|x|\*))?(?:\.(\d+|x|\*))?([+-][0-9A-Za-z.-]+)?(?=\s|$)/.exec(
|
|
1852
|
+
range,
|
|
1853
|
+
);
|
|
1854
|
+
if (!lowerBound) return false;
|
|
1855
|
+
|
|
1856
|
+
const operator = lowerBound[1];
|
|
1857
|
+
if (operator === "<" || operator === "<=") return false;
|
|
1858
|
+
if (lowerBound[5]?.startsWith("-")) return false;
|
|
1859
|
+
|
|
1860
|
+
const major = Number(lowerBound[2]);
|
|
1861
|
+
const minor = /^\d+$/.test(lowerBound[3] ?? "") ? Number(lowerBound[3]) : 0;
|
|
1862
|
+
return major > 15 || (major === 15 && minor >= 1);
|
|
1863
|
+
});
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1828
1866
|
async function updateOutboxDatabaseProvider(
|
|
1829
1867
|
targetDir: string,
|
|
1830
1868
|
config: ResolvedBeignetConfig,
|
|
@@ -1923,6 +1961,95 @@ async function updateOutboxDatabaseProvider(
|
|
|
1923
1961
|
return true;
|
|
1924
1962
|
}
|
|
1925
1963
|
|
|
1964
|
+
async function updateOutboxDrainProvider(
|
|
1965
|
+
targetDir: string,
|
|
1966
|
+
config: ResolvedBeignetConfig,
|
|
1967
|
+
options: { dryRun: boolean },
|
|
1968
|
+
): Promise<boolean> {
|
|
1969
|
+
if (!(await projectSupportsNextAfter(targetDir))) return false;
|
|
1970
|
+
|
|
1971
|
+
const file = providersFilePath(config);
|
|
1972
|
+
const filePath = path.join(targetDir, file);
|
|
1973
|
+
const original = await readOptionalFile(filePath);
|
|
1974
|
+
if (original === undefined) {
|
|
1975
|
+
throw new Error(
|
|
1976
|
+
`Could not find the generated providers file ${file}. Register the outbox drain trigger manually, or restore the generated providers file before running make outbox.`,
|
|
1977
|
+
);
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
let next = addNamedImport(
|
|
1981
|
+
original,
|
|
1982
|
+
"createObservedUnitOfWork",
|
|
1983
|
+
"@beignet/core/ports",
|
|
1984
|
+
);
|
|
1985
|
+
next = addNamedImport(next, "createProvider", "@beignet/core/providers");
|
|
1986
|
+
next = addNamedImport(next, "createNextOutboxDrainTrigger", "@beignet/next");
|
|
1987
|
+
next = addNamedImport(next, "after", "next/server");
|
|
1988
|
+
next = addNamedTypeImport(
|
|
1989
|
+
next,
|
|
1990
|
+
"AppContext",
|
|
1991
|
+
aliasModule(config.paths.appContext),
|
|
1992
|
+
);
|
|
1993
|
+
next = addNamedTypeImport(next, "AppPorts", aliasModule(config.paths.ports));
|
|
1994
|
+
next = addNamedTypeImport(
|
|
1995
|
+
next,
|
|
1996
|
+
"AppServiceContextInput",
|
|
1997
|
+
aliasModule(path.join(path.dirname(config.paths.server), "context.ts")),
|
|
1998
|
+
);
|
|
1999
|
+
|
|
2000
|
+
const providerName = "outboxDrainProvider";
|
|
2001
|
+
if (!new RegExp(`\\bconst\\s+${providerName}\\b`).test(next)) {
|
|
2002
|
+
const outboxModule = aliasModule(config.paths.outbox);
|
|
2003
|
+
const providerDefinition = `const ${providerName} = createProvider<
|
|
2004
|
+
\tPick<AppPorts, "uow">,
|
|
2005
|
+
\tAppContext,
|
|
2006
|
+
\tAppServiceContextInput
|
|
2007
|
+
>()({
|
|
2008
|
+
\tname: "outbox-drain-trigger",
|
|
2009
|
+
\tsetup({ ports, createServiceContext }): { ports: Pick<AppPorts, "uow"> } {
|
|
2010
|
+
\t\tconst triggerOutboxDrain: () => void = createNextOutboxDrainTrigger({
|
|
2011
|
+
\t\t\tdefer: after,
|
|
2012
|
+
\t\t\tcreateContext: () => createServiceContext(undefined),
|
|
2013
|
+
\t\t\tregistry: async () => (await import("${outboxModule}")).outboxRegistry,
|
|
2014
|
+
\t\t\tbatchSize: 100,
|
|
2015
|
+
\t\t});
|
|
2016
|
+
|
|
2017
|
+
\t\treturn {
|
|
2018
|
+
\t\t\tports: {
|
|
2019
|
+
\t\t\t\tuow: createObservedUnitOfWork({
|
|
2020
|
+
\t\t\t\t\tunitOfWork: ports.uow,
|
|
2021
|
+
\t\t\t\t\tafterCommit: triggerOutboxDrain,
|
|
2022
|
+
\t\t\t\t}),
|
|
2023
|
+
\t\t\t},
|
|
2024
|
+
\t\t};
|
|
2025
|
+
\t},
|
|
2026
|
+
});
|
|
2027
|
+
`;
|
|
2028
|
+
const withProvider = next.replace(
|
|
2029
|
+
/\nexport const providers = \[/,
|
|
2030
|
+
`\n${providerDefinition}\nexport const providers = [`,
|
|
2031
|
+
);
|
|
2032
|
+
if (withProvider === next) {
|
|
2033
|
+
throw new Error(
|
|
2034
|
+
`Could not find the exported providers array in ${file}. Register outboxDrainProvider manually, or restore the generated providers file before running make outbox.`,
|
|
2035
|
+
);
|
|
2036
|
+
}
|
|
2037
|
+
next = withProvider;
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2040
|
+
const appended = appendToNamedArray(next, "providers", providerName);
|
|
2041
|
+
if (appended.kind === "missing") {
|
|
2042
|
+
throw new Error(
|
|
2043
|
+
`Could not find the exported providers array in ${file}. Register outboxDrainProvider manually, or restore the generated providers file before running make outbox.`,
|
|
2044
|
+
);
|
|
2045
|
+
}
|
|
2046
|
+
if (appended.kind === "updated") next = appended.source;
|
|
2047
|
+
|
|
2048
|
+
if (next === original) return false;
|
|
2049
|
+
if (!options.dryRun) await writeFile(filePath, next);
|
|
2050
|
+
return true;
|
|
2051
|
+
}
|
|
2052
|
+
|
|
1926
2053
|
async function updateOutboxRepositoriesReturnType(
|
|
1927
2054
|
targetDir: string,
|
|
1928
2055
|
config: ResolvedBeignetConfig,
|
|
@@ -2068,6 +2195,12 @@ export async function makeUpload(
|
|
|
2068
2195
|
? resolveConfig(options.config)
|
|
2069
2196
|
: await loadBeignetConfig(targetDir);
|
|
2070
2197
|
|
|
2198
|
+
assertNextFramework(
|
|
2199
|
+
config,
|
|
2200
|
+
"beignet make upload",
|
|
2201
|
+
"a Next.js App Router upload handler",
|
|
2202
|
+
);
|
|
2203
|
+
|
|
2071
2204
|
await assertFeatureArtifactApp(targetDir, config, "upload");
|
|
2072
2205
|
if (options.ui) {
|
|
2073
2206
|
await assertUploadUiApp(targetDir, config);
|
|
@@ -2168,6 +2301,12 @@ export async function makeOutbox(
|
|
|
2168
2301
|
? resolveConfig(options.config)
|
|
2169
2302
|
: await loadBeignetConfig(targetDir);
|
|
2170
2303
|
|
|
2304
|
+
assertNextFramework(
|
|
2305
|
+
config,
|
|
2306
|
+
"beignet make outbox",
|
|
2307
|
+
"a Next.js App Router recovery drain handler",
|
|
2308
|
+
);
|
|
2309
|
+
|
|
2171
2310
|
await assertServerRuntimeApp(targetDir, config, "outbox");
|
|
2172
2311
|
await updateOutboxPortWiring(targetDir, config, { dryRun: true });
|
|
2173
2312
|
|
|
@@ -2219,6 +2358,9 @@ export async function makeOutbox(
|
|
|
2219
2358
|
config.paths.ports,
|
|
2220
2359
|
config.paths.portWiring,
|
|
2221
2360
|
path.join(infrastructureDir(config), "db/provider.ts"),
|
|
2361
|
+
...((await projectSupportsNextAfter(targetDir))
|
|
2362
|
+
? [providersFilePath(config)]
|
|
2363
|
+
: []),
|
|
2222
2364
|
drizzleRepositoriesPath(config),
|
|
2223
2365
|
outboxDrizzleSchemaFilePath(config),
|
|
2224
2366
|
drizzleSchemaIndexPath(config),
|