@beignet/cli 0.0.44 → 0.0.46
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 +25 -0
- package/README.md +78 -16
- package/dist/choices.d.ts +5 -1
- package/dist/choices.d.ts.map +1 -1
- package/dist/choices.js +12 -0
- package/dist/choices.js.map +1 -1
- package/dist/db.d.ts +4 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +12 -6
- package/dist/db.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -13
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +598 -62
- package/dist/inspect.js.map +1 -1
- package/dist/lib.d.ts +4 -1
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js +1 -1
- package/dist/lib.js.map +1 -1
- package/dist/mcp.d.ts +2 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +356 -5
- package/dist/mcp.js.map +1 -1
- package/dist/operational-lifecycle.d.ts +12 -0
- package/dist/operational-lifecycle.d.ts.map +1 -0
- package/dist/operational-lifecycle.js +39 -0
- package/dist/operational-lifecycle.js.map +1 -0
- package/dist/operational-path.d.ts +3 -0
- package/dist/operational-path.d.ts.map +1 -0
- package/dist/operational-path.js +26 -0
- package/dist/operational-path.js.map +1 -0
- package/dist/operational-process.d.ts +78 -0
- package/dist/operational-process.d.ts.map +1 -0
- package/dist/operational-process.js +228 -0
- package/dist/operational-process.js.map +1 -0
- package/dist/operational-runner.d.ts +2 -0
- package/dist/operational-runner.d.ts.map +1 -0
- package/dist/operational-runner.js +168 -0
- package/dist/operational-runner.js.map +1 -0
- package/dist/outbox.d.ts +14 -6
- package/dist/outbox.d.ts.map +1 -1
- package/dist/outbox.js +140 -124
- package/dist/outbox.js.map +1 -1
- package/dist/schedule.d.ts +2 -1
- package/dist/schedule.d.ts.map +1 -1
- package/dist/schedule.js +79 -81
- package/dist/schedule.js.map +1 -1
- package/dist/task.d.ts +2 -1
- package/dist/task.d.ts.map +1 -1
- package/dist/task.js +57 -61
- package/dist/task.js.map +1 -1
- package/dist/templates/agents.d.ts.map +1 -1
- package/dist/templates/agents.js +32 -8
- package/dist/templates/agents.js.map +1 -1
- package/dist/templates/server.d.ts.map +1 -1
- package/dist/templates/server.js +9 -2
- package/dist/templates/server.js.map +1 -1
- package/dist/templates/shadcn.d.ts.map +1 -1
- package/dist/templates/shadcn.js +3 -1
- package/dist/templates/shadcn.js.map +1 -1
- package/dist/templates/shared.js +1 -1
- package/package.json +2 -2
- package/skills/app-structure/SKILL.md +26 -7
- package/src/choices.ts +22 -0
- package/src/db.ts +12 -6
- package/src/index.ts +19 -16
- package/src/inspect.ts +844 -67
- package/src/lib.ts +27 -1
- package/src/mcp.ts +459 -4
- package/src/operational-lifecycle.ts +56 -0
- package/src/operational-path.ts +35 -0
- package/src/operational-process.ts +454 -0
- package/src/operational-runner.ts +221 -0
- package/src/outbox.ts +154 -128
- package/src/schedule.ts +84 -89
- package/src/task.ts +58 -62
- package/src/templates/agents.ts +32 -8
- package/src/templates/server.ts +9 -2
- package/src/templates/shadcn.ts +3 -1
- package/src/templates/shared.ts +1 -1
package/src/inspect.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { readdir, readFile, stat } from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { parseProviderPackageMetadata } from "@beignet/core/providers";
|
|
4
4
|
import { createPainter } from "./ansi.js";
|
|
5
|
+
import type { DatabaseSchemaDialect, DatabaseSchemaTable } from "./choices.js";
|
|
5
6
|
import {
|
|
6
7
|
type BeignetConfig,
|
|
7
8
|
type BeignetDatabaseTables,
|
|
@@ -15,6 +16,7 @@ import {
|
|
|
15
16
|
type ResolvedBeignetConfig,
|
|
16
17
|
resolveConfig,
|
|
17
18
|
} from "./config.js";
|
|
19
|
+
import { renderDatabaseSchema, resolveDatabaseSchemaDialect } from "./db.js";
|
|
18
20
|
import {
|
|
19
21
|
applyPlannedDoctorFixes,
|
|
20
22
|
createDoctorFixPlan,
|
|
@@ -390,6 +392,36 @@ async function buildDoctorFixPlan(
|
|
|
390
392
|
);
|
|
391
393
|
if (listenerFix) operations.push(listenerFix);
|
|
392
394
|
|
|
395
|
+
const databaseFix = await planMissingProviderTables(targetDir, files, config);
|
|
396
|
+
if (databaseFix) operations.push(databaseFix);
|
|
397
|
+
|
|
398
|
+
const inngestFix = await planUnregisteredInngestJobs(
|
|
399
|
+
targetDir,
|
|
400
|
+
files,
|
|
401
|
+
config,
|
|
402
|
+
);
|
|
403
|
+
if (inngestFix) operations.push(inngestFix);
|
|
404
|
+
|
|
405
|
+
const nonOverlappingOperations =
|
|
406
|
+
nonOverlappingDoctorFixOperations(operations);
|
|
407
|
+
operations.splice(0, operations.length, ...nonOverlappingOperations);
|
|
408
|
+
const sourceOverrides = new Map<string, string>();
|
|
409
|
+
for (const operation of operations) {
|
|
410
|
+
for (const change of operation.changes) {
|
|
411
|
+
sourceOverrides.set(change.file, change.after);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
const runtimeManifestFix = await planRuntimeManifestRegistries(
|
|
415
|
+
targetDir,
|
|
416
|
+
files,
|
|
417
|
+
config,
|
|
418
|
+
convention,
|
|
419
|
+
sourceOverrides,
|
|
420
|
+
);
|
|
421
|
+
if (runtimeManifestFix) {
|
|
422
|
+
addRuntimeManifestFix(operations, runtimeManifestFix);
|
|
423
|
+
}
|
|
424
|
+
|
|
393
425
|
const openApiFix = await planDirectOpenApiArrayDrift(
|
|
394
426
|
targetDir,
|
|
395
427
|
files,
|
|
@@ -403,10 +435,77 @@ async function buildDoctorFixPlan(
|
|
|
403
435
|
return createDoctorFixPlan({
|
|
404
436
|
targetDir,
|
|
405
437
|
strict: Boolean(options.strict),
|
|
406
|
-
operations,
|
|
438
|
+
operations: nonOverlappingDoctorFixOperations(operations),
|
|
407
439
|
});
|
|
408
440
|
}
|
|
409
441
|
|
|
442
|
+
function nonOverlappingDoctorFixOperations(
|
|
443
|
+
operations: PlannedDoctorFixOperation[],
|
|
444
|
+
): PlannedDoctorFixOperation[] {
|
|
445
|
+
const claimedFiles = new Set<string>();
|
|
446
|
+
const safeOperations: PlannedDoctorFixOperation[] = [];
|
|
447
|
+
|
|
448
|
+
for (const operation of operations) {
|
|
449
|
+
if (operation.changes.some((change) => claimedFiles.has(change.file))) {
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
safeOperations.push(operation);
|
|
453
|
+
for (const change of operation.changes) claimedFiles.add(change.file);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
return safeOperations;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const workflowDoctorFixOperationIds = new Set<DoctorFixOperationId>([
|
|
460
|
+
"schedules.register-missing",
|
|
461
|
+
"tasks.register-missing",
|
|
462
|
+
"workflows.register-missing",
|
|
463
|
+
"outbox.register-missing",
|
|
464
|
+
"listeners.register-missing",
|
|
465
|
+
"inngest.register-missing",
|
|
466
|
+
"runtime-manifest.register-missing",
|
|
467
|
+
]);
|
|
468
|
+
|
|
469
|
+
function addRuntimeManifestFix(
|
|
470
|
+
operations: PlannedDoctorFixOperation[],
|
|
471
|
+
runtimeManifestFix: PlannedDoctorFixOperation,
|
|
472
|
+
): void {
|
|
473
|
+
const runtimeChange = runtimeManifestFix.changes[0];
|
|
474
|
+
if (!runtimeChange) return;
|
|
475
|
+
|
|
476
|
+
const overlappingOperation = operations.find((operation) =>
|
|
477
|
+
operation.changes.some((change) => change.file === runtimeChange.file),
|
|
478
|
+
);
|
|
479
|
+
if (!overlappingOperation) {
|
|
480
|
+
operations.push(runtimeManifestFix);
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
if (!workflowDoctorFixOperationIds.has(overlappingOperation.id)) return;
|
|
484
|
+
|
|
485
|
+
overlappingOperation.id = "workflows.register-missing";
|
|
486
|
+
overlappingOperation.fixes.push(...runtimeManifestFix.fixes);
|
|
487
|
+
overlappingOperation.changes = overlappingOperation.changes.map((change) =>
|
|
488
|
+
change.file === runtimeChange.file
|
|
489
|
+
? planDoctorFixFileChange({
|
|
490
|
+
file: change.file,
|
|
491
|
+
before: change.before,
|
|
492
|
+
after: runtimeChange.after,
|
|
493
|
+
})
|
|
494
|
+
: change,
|
|
495
|
+
);
|
|
496
|
+
|
|
497
|
+
const existingWorkflowOperation = operations.find(
|
|
498
|
+
(operation) =>
|
|
499
|
+
operation !== overlappingOperation &&
|
|
500
|
+
operation.id === "workflows.register-missing",
|
|
501
|
+
);
|
|
502
|
+
if (!existingWorkflowOperation) return;
|
|
503
|
+
|
|
504
|
+
existingWorkflowOperation.fixes.push(...overlappingOperation.fixes);
|
|
505
|
+
existingWorkflowOperation.changes.push(...overlappingOperation.changes);
|
|
506
|
+
operations.splice(operations.indexOf(overlappingOperation), 1);
|
|
507
|
+
}
|
|
508
|
+
|
|
410
509
|
/**
|
|
411
510
|
* Format inspected routes as a CLI table.
|
|
412
511
|
*/
|
|
@@ -2347,6 +2446,10 @@ async function inspectProductionReadiness(
|
|
|
2347
2446
|
packageJson,
|
|
2348
2447
|
);
|
|
2349
2448
|
const configFiles = operationalConfigFiles(files, config);
|
|
2449
|
+
const cronDirectory = path.posix.join(
|
|
2450
|
+
directoryPath(config.paths.routes),
|
|
2451
|
+
"cron",
|
|
2452
|
+
);
|
|
2350
2453
|
const infrastructurePath = directoryPath(
|
|
2351
2454
|
path.dirname(config.paths.portWiring),
|
|
2352
2455
|
);
|
|
@@ -2380,16 +2483,17 @@ async function inspectProductionReadiness(
|
|
|
2380
2483
|
}
|
|
2381
2484
|
|
|
2382
2485
|
if (
|
|
2383
|
-
file
|
|
2486
|
+
fileWithinDirectory(file, cronDirectory) &&
|
|
2384
2487
|
file.endsWith("/route.ts") &&
|
|
2385
2488
|
!/\bCRON_SECRET\b/.test(source) &&
|
|
2386
|
-
!/\bcreateOutboxDrainRoute\s*\(/.test(source)
|
|
2489
|
+
!/\bcreateOutboxDrainRoute\s*\(/.test(source) &&
|
|
2490
|
+
!/\bcreateScheduleRoute\s*\(/.test(source)
|
|
2387
2491
|
) {
|
|
2388
2492
|
diagnostics.push({
|
|
2389
2493
|
severity: "warning",
|
|
2390
2494
|
code: "BEIGNET_CRON_SECRET_MISSING",
|
|
2391
2495
|
file,
|
|
2392
|
-
message: `${file} looks like a cron route that does not reference CRON_SECRET. Protect cron entrypoints with a shared secret or a framework helper such as createOutboxDrainRoute(...).`,
|
|
2496
|
+
message: `${file} looks like a cron route that does not reference CRON_SECRET. Protect cron entrypoints with a shared secret or a framework helper such as createOutboxDrainRoute(...) or createScheduleRoute(...).`,
|
|
2393
2497
|
});
|
|
2394
2498
|
}
|
|
2395
2499
|
|
|
@@ -3264,27 +3368,28 @@ async function inspectInngestProductionPath(
|
|
|
3264
3368
|
"utf8",
|
|
3265
3369
|
);
|
|
3266
3370
|
const registry = arrayInitializerInfo(registrySource, "inngestJobs");
|
|
3267
|
-
const
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
const
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3371
|
+
const featureJobs = (
|
|
3372
|
+
await readFeatureWorkflowRegistries(targetDir, files, config)
|
|
3373
|
+
).filter((entry) => entry.kind === "jobs");
|
|
3374
|
+
const registeredJobs = canonicalRegisteredWorkflowIdentifiers({
|
|
3375
|
+
source: registrySource,
|
|
3376
|
+
registered: registry
|
|
3377
|
+
? identifiersFromArrayExpression(registry.text)
|
|
3378
|
+
: new Set<string>(),
|
|
3379
|
+
centralFile: registryFile,
|
|
3380
|
+
files,
|
|
3381
|
+
registries: featureJobs,
|
|
3382
|
+
});
|
|
3383
|
+
const unregisteredJobs = unregisteredWorkflowRegistries(
|
|
3384
|
+
featureJobs,
|
|
3385
|
+
registeredJobs,
|
|
3275
3386
|
);
|
|
3276
|
-
for (const
|
|
3277
|
-
const source = await readFile(path.join(targetDir, jobIndex), "utf8");
|
|
3278
|
-
const match = source.match(/export const\s+([A-Za-z_$][\w$]*Jobs)\s*=/);
|
|
3279
|
-
const registryName = match?.[1];
|
|
3280
|
-
if (!registryName || registeredJobs.has(registryName)) {
|
|
3281
|
-
continue;
|
|
3282
|
-
}
|
|
3387
|
+
for (const { registry: featureRegistry } of unregisteredJobs) {
|
|
3283
3388
|
diagnostics.push({
|
|
3284
3389
|
severity: "warning",
|
|
3285
3390
|
code: "BEIGNET_INNGEST_JOB_REGISTRATION_MISSING",
|
|
3286
3391
|
file: registryFile,
|
|
3287
|
-
message: `${
|
|
3392
|
+
message: `${featureRegistry.indexFile} exports ${featureRegistry.registryName}, but it is not registered in inngestJobs in ${registryFile}. Add ...${featureRegistry.registryName} so Inngest can execute the job.`,
|
|
3288
3393
|
});
|
|
3289
3394
|
}
|
|
3290
3395
|
}
|
|
@@ -3882,9 +3987,15 @@ function isInfraOrServerFile(
|
|
|
3882
3987
|
}
|
|
3883
3988
|
|
|
3884
3989
|
function containsCallExpression(source: string, name: string): boolean {
|
|
3885
|
-
|
|
3990
|
+
const codeIndexes = new Set(codeCharacterIndexes(source));
|
|
3991
|
+
const pattern = new RegExp(
|
|
3886
3992
|
`\\b${escapeRegExp(name)}\\s*(?:<[^;=(){}]*>\\s*)?\\(`,
|
|
3887
|
-
|
|
3993
|
+
"g",
|
|
3994
|
+
);
|
|
3995
|
+
|
|
3996
|
+
return [...source.matchAll(pattern)].some(
|
|
3997
|
+
(match) => match.index !== undefined && codeIndexes.has(match.index),
|
|
3998
|
+
);
|
|
3888
3999
|
}
|
|
3889
4000
|
|
|
3890
4001
|
type WorkflowRegistryKind =
|
|
@@ -4053,6 +4164,10 @@ async function inspectRuntimeManifestDrift(
|
|
|
4053
4164
|
files,
|
|
4054
4165
|
config,
|
|
4055
4166
|
);
|
|
4167
|
+
const manifestSource = await readFile(
|
|
4168
|
+
path.join(targetDir, manifest.file),
|
|
4169
|
+
"utf8",
|
|
4170
|
+
);
|
|
4056
4171
|
const diagnostics: InspectDiagnostic[] = [];
|
|
4057
4172
|
|
|
4058
4173
|
const checks: readonly {
|
|
@@ -4094,9 +4209,18 @@ async function inspectRuntimeManifestDrift(
|
|
|
4094
4209
|
];
|
|
4095
4210
|
|
|
4096
4211
|
for (const check of checks) {
|
|
4212
|
+
const kindRegistries = registries.filter(
|
|
4213
|
+
(registry) => registry.kind === check.kind,
|
|
4214
|
+
);
|
|
4097
4215
|
const missing = unregisteredWorkflowRegistries(
|
|
4098
|
-
|
|
4099
|
-
|
|
4216
|
+
kindRegistries,
|
|
4217
|
+
canonicalRegisteredWorkflowIdentifiers({
|
|
4218
|
+
source: manifestSource,
|
|
4219
|
+
registered: check.registered,
|
|
4220
|
+
centralFile: manifest.file,
|
|
4221
|
+
files,
|
|
4222
|
+
registries: kindRegistries,
|
|
4223
|
+
}),
|
|
4100
4224
|
);
|
|
4101
4225
|
|
|
4102
4226
|
for (const { registry, missingMembers } of missing) {
|
|
@@ -4271,6 +4395,48 @@ function unregisteredWorkflowRegistries(
|
|
|
4271
4395
|
return unregistered;
|
|
4272
4396
|
}
|
|
4273
4397
|
|
|
4398
|
+
function canonicalRegisteredWorkflowIdentifiers(options: {
|
|
4399
|
+
source: string;
|
|
4400
|
+
registered: Set<string>;
|
|
4401
|
+
centralFile: string;
|
|
4402
|
+
files: string[];
|
|
4403
|
+
registries: FeatureWorkflowRegistry[];
|
|
4404
|
+
}): Set<string> {
|
|
4405
|
+
const canonical = new Set(options.registered);
|
|
4406
|
+
const imports = parseNamedImportSources(options.source);
|
|
4407
|
+
|
|
4408
|
+
for (const identifier of options.registered) {
|
|
4409
|
+
const imported = imports.get(identifier);
|
|
4410
|
+
if (!imported) continue;
|
|
4411
|
+
|
|
4412
|
+
const importedFile = sourceFileFromImport(
|
|
4413
|
+
imported.sourcePath,
|
|
4414
|
+
options.centralFile,
|
|
4415
|
+
options.files,
|
|
4416
|
+
);
|
|
4417
|
+
if (!importedFile) continue;
|
|
4418
|
+
|
|
4419
|
+
for (const registry of options.registries) {
|
|
4420
|
+
if (
|
|
4421
|
+
imported.importedName === registry.registryName &&
|
|
4422
|
+
importedFile === registry.indexFile
|
|
4423
|
+
) {
|
|
4424
|
+
canonical.add(registry.registryName);
|
|
4425
|
+
}
|
|
4426
|
+
|
|
4427
|
+
if (
|
|
4428
|
+
registry.members.includes(imported.importedName) &&
|
|
4429
|
+
(importedFile === registry.indexFile ||
|
|
4430
|
+
registry.memberFiles.get(imported.importedName) === importedFile)
|
|
4431
|
+
) {
|
|
4432
|
+
canonical.add(imported.importedName);
|
|
4433
|
+
}
|
|
4434
|
+
}
|
|
4435
|
+
}
|
|
4436
|
+
|
|
4437
|
+
return canonical;
|
|
4438
|
+
}
|
|
4439
|
+
|
|
4274
4440
|
async function readFeatureWorkflowRegistries(
|
|
4275
4441
|
targetDir: string,
|
|
4276
4442
|
files: string[],
|
|
@@ -4350,6 +4516,8 @@ async function readRuntimeManifestIdentifiers(
|
|
|
4350
4516
|
targetDir: string,
|
|
4351
4517
|
files: string[],
|
|
4352
4518
|
): Promise<RuntimeManifestIdentifierSets | undefined> {
|
|
4519
|
+
const manifests: RuntimeManifestIdentifierSets[] = [];
|
|
4520
|
+
|
|
4353
4521
|
for (const file of files) {
|
|
4354
4522
|
if (!file.endsWith(".ts") && !file.endsWith(".tsx")) continue;
|
|
4355
4523
|
if (file.endsWith(".d.ts") || isWorkflowTestFile(file)) continue;
|
|
@@ -4357,25 +4525,29 @@ async function readRuntimeManifestIdentifiers(
|
|
|
4357
4525
|
const source = await readFile(path.join(targetDir, file), "utf8");
|
|
4358
4526
|
if (!source.includes("defineRuntimeManifest")) continue;
|
|
4359
4527
|
|
|
4528
|
+
const calls = runtimeManifestCallMatches(source);
|
|
4529
|
+
if (calls.length === 0) continue;
|
|
4530
|
+
if (calls.length !== 1) return undefined;
|
|
4360
4531
|
const identifiers = runtimeManifestIdentifiers(source);
|
|
4361
4532
|
if (identifiers) {
|
|
4362
|
-
|
|
4533
|
+
manifests.push({
|
|
4363
4534
|
file,
|
|
4364
4535
|
...identifiers,
|
|
4365
|
-
};
|
|
4536
|
+
});
|
|
4366
4537
|
}
|
|
4367
4538
|
}
|
|
4368
4539
|
|
|
4369
|
-
return undefined;
|
|
4540
|
+
return manifests.length === 1 ? manifests[0] : undefined;
|
|
4370
4541
|
}
|
|
4371
4542
|
|
|
4372
4543
|
function runtimeManifestIdentifiers(
|
|
4373
4544
|
source: string,
|
|
4374
4545
|
): Omit<RuntimeManifestIdentifierSets, "file"> | undefined {
|
|
4375
|
-
const
|
|
4376
|
-
if (
|
|
4546
|
+
const calls = runtimeManifestCallMatches(source);
|
|
4547
|
+
if (calls.length !== 1) return undefined;
|
|
4548
|
+
const call = calls[0];
|
|
4377
4549
|
|
|
4378
|
-
const arg = firstCallArgInfo(source, call.index + call[0].length);
|
|
4550
|
+
const arg = firstCallArgInfo(source, (call.index ?? 0) + call[0].length);
|
|
4379
4551
|
if (!arg) return undefined;
|
|
4380
4552
|
|
|
4381
4553
|
const outbox = objectPropertyValueInfo(arg.text, "outbox");
|
|
@@ -4393,6 +4565,15 @@ function runtimeManifestIdentifiers(
|
|
|
4393
4565
|
};
|
|
4394
4566
|
}
|
|
4395
4567
|
|
|
4568
|
+
function runtimeManifestCallMatches(source: string): RegExpMatchArray[] {
|
|
4569
|
+
const codeIndexes = new Set(codeCharacterIndexes(source));
|
|
4570
|
+
return [
|
|
4571
|
+
...source.matchAll(/\bdefineRuntimeManifest\s*(?:<[^>]*>\s*)?\(/g),
|
|
4572
|
+
].filter(
|
|
4573
|
+
(match) => match.index !== undefined && codeIndexes.has(match.index),
|
|
4574
|
+
);
|
|
4575
|
+
}
|
|
4576
|
+
|
|
4396
4577
|
function identifiersFromPropertyArray(
|
|
4397
4578
|
source: string,
|
|
4398
4579
|
property: string,
|
|
@@ -4406,24 +4587,48 @@ function objectPropertyValueInfo(
|
|
|
4406
4587
|
source: string,
|
|
4407
4588
|
property: string,
|
|
4408
4589
|
): { text: string; start: number; end: number } | undefined {
|
|
4409
|
-
|
|
4410
|
-
|
|
4590
|
+
let depth = 0;
|
|
4591
|
+
|
|
4592
|
+
for (const index of codeCharacterIndexes(source)) {
|
|
4593
|
+
const char = source[index];
|
|
4594
|
+
if (char === "{" || char === "[" || char === "(") {
|
|
4595
|
+
depth++;
|
|
4596
|
+
continue;
|
|
4597
|
+
}
|
|
4598
|
+
if (char === "}" || char === "]" || char === ")") {
|
|
4599
|
+
depth--;
|
|
4600
|
+
continue;
|
|
4601
|
+
}
|
|
4602
|
+
if (
|
|
4603
|
+
depth !== 1 ||
|
|
4604
|
+
source.slice(index, index + property.length) !== property ||
|
|
4605
|
+
/[A-Za-z0-9_$]/.test(source[index - 1] ?? "")
|
|
4606
|
+
) {
|
|
4607
|
+
continue;
|
|
4608
|
+
}
|
|
4411
4609
|
|
|
4412
|
-
|
|
4413
|
-
|
|
4610
|
+
let colon = index + property.length;
|
|
4611
|
+
if (/[A-Za-z0-9_$]/.test(source[colon] ?? "")) continue;
|
|
4612
|
+
while (colon < source.length && /\s/.test(source[colon])) colon++;
|
|
4613
|
+
if (source[colon] !== ":") continue;
|
|
4414
4614
|
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4615
|
+
let start = colon + 1;
|
|
4616
|
+
while (start < source.length && /\s/.test(source[start])) start++;
|
|
4617
|
+
const open = source[start];
|
|
4618
|
+
const close = open === "[" ? "]" : open === "{" ? "}" : undefined;
|
|
4619
|
+
if (!close) return undefined;
|
|
4418
4620
|
|
|
4419
|
-
|
|
4420
|
-
|
|
4621
|
+
const end = matchingDelimiterIndex(source, start, open, close);
|
|
4622
|
+
if (end === -1) return undefined;
|
|
4421
4623
|
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
|
|
4426
|
-
|
|
4624
|
+
return {
|
|
4625
|
+
text: source.slice(start, end + 1),
|
|
4626
|
+
start,
|
|
4627
|
+
end: end + 1,
|
|
4628
|
+
};
|
|
4629
|
+
}
|
|
4630
|
+
|
|
4631
|
+
return undefined;
|
|
4427
4632
|
}
|
|
4428
4633
|
|
|
4429
4634
|
function registeredWorkflowIdentifiers(
|
|
@@ -4607,6 +4812,8 @@ async function inspectServerlessFootguns(
|
|
|
4607
4812
|
convention: InspectConvention,
|
|
4608
4813
|
): Promise<InspectDiagnostic[]> {
|
|
4609
4814
|
const diagnostics: InspectDiagnostic[] = [];
|
|
4815
|
+
const routeDirectory = directoryPath(config.paths.routes);
|
|
4816
|
+
const cronDirectory = path.posix.join(routeDirectory, "cron");
|
|
4610
4817
|
|
|
4611
4818
|
for (const file of files) {
|
|
4612
4819
|
if (
|
|
@@ -4617,12 +4824,9 @@ async function inspectServerlessFootguns(
|
|
|
4617
4824
|
continue;
|
|
4618
4825
|
}
|
|
4619
4826
|
|
|
4620
|
-
const isInfraOrServerFile =
|
|
4621
|
-
file.startsWith("infra/") || file.startsWith("server/");
|
|
4622
|
-
const isProviderFile =
|
|
4623
|
-
/(^|\/)provider(s)?\.ts$/.test(file) ||
|
|
4624
|
-
/(^|\/)[^/]*provider[^/]*\.ts$/.test(file);
|
|
4625
4827
|
const source = await readFile(path.join(targetDir, file), "utf8");
|
|
4828
|
+
const isInfraOrServerFile = isRuntimeFile(file, config);
|
|
4829
|
+
const isProviderFile = isProviderLifecycleFile(file, source);
|
|
4626
4830
|
|
|
4627
4831
|
if (
|
|
4628
4832
|
config.framework === "next" &&
|
|
@@ -4666,7 +4870,7 @@ async function inspectServerlessFootguns(
|
|
|
4666
4870
|
}
|
|
4667
4871
|
|
|
4668
4872
|
if (
|
|
4669
|
-
file
|
|
4873
|
+
fileWithinDirectory(file, cronDirectory) &&
|
|
4670
4874
|
file.endsWith("/route.ts") &&
|
|
4671
4875
|
!/\bauthorization\b/i.test(source) &&
|
|
4672
4876
|
!/\bcreateOutboxDrainRoute\s*\(/.test(source) &&
|
|
@@ -4682,8 +4886,8 @@ async function inspectServerlessFootguns(
|
|
|
4682
4886
|
}
|
|
4683
4887
|
|
|
4684
4888
|
if (
|
|
4685
|
-
hasOutboxRegistry(files) &&
|
|
4686
|
-
!(await appHasOutboxDrainEntrypoint(targetDir, files))
|
|
4889
|
+
hasOutboxRegistry(files, config) &&
|
|
4890
|
+
!(await appHasOutboxDrainEntrypoint(targetDir, files, config))
|
|
4687
4891
|
) {
|
|
4688
4892
|
diagnostics.push({
|
|
4689
4893
|
severity: "warning",
|
|
@@ -4696,6 +4900,82 @@ async function inspectServerlessFootguns(
|
|
|
4696
4900
|
return diagnostics;
|
|
4697
4901
|
}
|
|
4698
4902
|
|
|
4903
|
+
function configuredRuntimeFiles(config: ResolvedBeignetConfig): string[] {
|
|
4904
|
+
return [
|
|
4905
|
+
config.paths.server,
|
|
4906
|
+
config.paths.listeners,
|
|
4907
|
+
config.paths.tasks,
|
|
4908
|
+
config.paths.outbox,
|
|
4909
|
+
config.paths.schedules,
|
|
4910
|
+
config.paths.portWiring,
|
|
4911
|
+
];
|
|
4912
|
+
}
|
|
4913
|
+
|
|
4914
|
+
function configuredRuntimeDirectories(config: ResolvedBeignetConfig): string[] {
|
|
4915
|
+
const applicationDirectories = configuredApplicationDirectories(config);
|
|
4916
|
+
|
|
4917
|
+
return configuredRuntimeFiles(config)
|
|
4918
|
+
.map((file) => directoryPath(path.posix.dirname(file)))
|
|
4919
|
+
.filter((directory) => directory !== ".")
|
|
4920
|
+
.filter(
|
|
4921
|
+
(directory) =>
|
|
4922
|
+
!applicationDirectories.some(
|
|
4923
|
+
(applicationDirectory) =>
|
|
4924
|
+
applicationDirectory !== directory &&
|
|
4925
|
+
fileWithinDirectory(applicationDirectory, directory),
|
|
4926
|
+
),
|
|
4927
|
+
)
|
|
4928
|
+
.filter((directory, index, all) => all.indexOf(directory) === index);
|
|
4929
|
+
}
|
|
4930
|
+
|
|
4931
|
+
function configuredApplicationDirectories(
|
|
4932
|
+
config: ResolvedBeignetConfig,
|
|
4933
|
+
): string[] {
|
|
4934
|
+
return [
|
|
4935
|
+
config.paths.contracts,
|
|
4936
|
+
config.paths.routes,
|
|
4937
|
+
config.paths.features,
|
|
4938
|
+
config.paths.policies,
|
|
4939
|
+
config.paths.useCases,
|
|
4940
|
+
config.paths.tests,
|
|
4941
|
+
]
|
|
4942
|
+
.map(directoryPath)
|
|
4943
|
+
.filter((directory) => directory !== ".")
|
|
4944
|
+
.filter((directory, index, all) => all.indexOf(directory) === index);
|
|
4945
|
+
}
|
|
4946
|
+
|
|
4947
|
+
function isRuntimeFile(file: string, config: ResolvedBeignetConfig): boolean {
|
|
4948
|
+
if (configuredRuntimeFiles(config).includes(file)) return true;
|
|
4949
|
+
if (
|
|
4950
|
+
configuredApplicationDirectories(config).some((directory) =>
|
|
4951
|
+
fileWithinDirectory(file, directory),
|
|
4952
|
+
)
|
|
4953
|
+
) {
|
|
4954
|
+
return false;
|
|
4955
|
+
}
|
|
4956
|
+
|
|
4957
|
+
return (
|
|
4958
|
+
fileWithinDirectory(file, "infra") ||
|
|
4959
|
+
fileWithinDirectory(file, "server") ||
|
|
4960
|
+
configuredRuntimeDirectories(config).some((directory) =>
|
|
4961
|
+
fileWithinDirectory(file, directory),
|
|
4962
|
+
)
|
|
4963
|
+
);
|
|
4964
|
+
}
|
|
4965
|
+
|
|
4966
|
+
function fileWithinDirectory(file: string, directory: string): boolean {
|
|
4967
|
+
return directory === "." || file.startsWith(`${directory}/`);
|
|
4968
|
+
}
|
|
4969
|
+
|
|
4970
|
+
function isProviderLifecycleFile(file: string, source?: string): boolean {
|
|
4971
|
+
return (
|
|
4972
|
+
/(^|\/)provider(s)?\.ts$/.test(file) ||
|
|
4973
|
+
/(^|\/)[^/]*provider[^/]*\.ts$/.test(file) ||
|
|
4974
|
+
/(^|\/)providers?\//.test(file) ||
|
|
4975
|
+
(source !== undefined && containsCallExpression(source, "createProvider"))
|
|
4976
|
+
);
|
|
4977
|
+
}
|
|
4978
|
+
|
|
4699
4979
|
function isNextRouteFile(file: string, config: ResolvedBeignetConfig): boolean {
|
|
4700
4980
|
const routesPath = directoryPath(config.paths.routes);
|
|
4701
4981
|
|
|
@@ -4727,33 +5007,35 @@ function importsEagerServer(
|
|
|
4727
5007
|
return false;
|
|
4728
5008
|
}
|
|
4729
5009
|
|
|
4730
|
-
function hasOutboxRegistry(
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
return file === "server/outbox.ts";
|
|
5010
|
+
function hasOutboxRegistry(
|
|
5011
|
+
files: string[],
|
|
5012
|
+
config: ResolvedBeignetConfig,
|
|
5013
|
+
): boolean {
|
|
5014
|
+
return files.includes(config.paths.outbox);
|
|
4736
5015
|
}
|
|
4737
5016
|
|
|
4738
5017
|
async function appHasOutboxDrainEntrypoint(
|
|
4739
5018
|
targetDir: string,
|
|
4740
5019
|
files: string[],
|
|
5020
|
+
config: ResolvedBeignetConfig,
|
|
4741
5021
|
): Promise<boolean> {
|
|
5022
|
+
const routesDirectory = directoryPath(config.paths.routes);
|
|
5023
|
+
|
|
4742
5024
|
for (const file of files) {
|
|
4743
5025
|
if (!file.endsWith(".ts") || file.endsWith(".test.ts")) continue;
|
|
4744
5026
|
if (
|
|
4745
|
-
!file
|
|
4746
|
-
!file
|
|
4747
|
-
!file.startsWith("infra/")
|
|
5027
|
+
!fileWithinDirectory(file, routesDirectory) &&
|
|
5028
|
+
!isRuntimeFile(file, config)
|
|
4748
5029
|
) {
|
|
4749
5030
|
continue;
|
|
4750
5031
|
}
|
|
4751
5032
|
|
|
4752
5033
|
const source = await readFile(path.join(targetDir, file), "utf8");
|
|
4753
|
-
if (
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
)
|
|
5034
|
+
if (containsCallExpression(source, "createOutboxDrainRoute")) {
|
|
5035
|
+
return true;
|
|
5036
|
+
}
|
|
5037
|
+
if (isProviderLifecycleFile(file, source)) continue;
|
|
5038
|
+
if (containsCallExpression(source, "drainOutbox")) {
|
|
4757
5039
|
return true;
|
|
4758
5040
|
}
|
|
4759
5041
|
}
|
|
@@ -4948,7 +5230,9 @@ async function inspectDatabaseLifecycleDrift(
|
|
|
4948
5230
|
);
|
|
4949
5231
|
for (const schemaFile of schemaFiles) {
|
|
4950
5232
|
const moduleName = path.basename(schemaFile, ".ts");
|
|
4951
|
-
if (schemaIndexSource
|
|
5233
|
+
if (schemaIndexExportKind(schemaIndexSource, `./${moduleName}`)) {
|
|
5234
|
+
continue;
|
|
5235
|
+
}
|
|
4952
5236
|
diagnostics.push({
|
|
4953
5237
|
severity: "warning",
|
|
4954
5238
|
code: "BEIGNET_DB_SCHEMA_EXPORT_MISSING",
|
|
@@ -6815,6 +7099,467 @@ function findRouteGroupContract(
|
|
|
6815
7099
|
);
|
|
6816
7100
|
}
|
|
6817
7101
|
|
|
7102
|
+
const managedDatabaseSchemaMarker = "// Managed by beignet db schema sync.";
|
|
7103
|
+
const databaseSchemaExports: Record<DatabaseSchemaTable, string> = {
|
|
7104
|
+
audit: "auditLog",
|
|
7105
|
+
idempotency: "idempotencyRecords",
|
|
7106
|
+
outbox: "outboxMessages",
|
|
7107
|
+
};
|
|
7108
|
+
|
|
7109
|
+
async function planMissingProviderTables(
|
|
7110
|
+
targetDir: string,
|
|
7111
|
+
files: string[],
|
|
7112
|
+
config: ResolvedBeignetConfig,
|
|
7113
|
+
): Promise<PlannedDoctorFixOperation | undefined> {
|
|
7114
|
+
const infrastructurePath = directoryPath(
|
|
7115
|
+
path.dirname(config.paths.portWiring),
|
|
7116
|
+
);
|
|
7117
|
+
const schemaDir = `${infrastructurePath}/db/schema`;
|
|
7118
|
+
const output = `${schemaDir}/beignet.ts`;
|
|
7119
|
+
const indexFile = `${schemaDir}/index.ts`;
|
|
7120
|
+
if (!files.includes(indexFile)) return undefined;
|
|
7121
|
+
|
|
7122
|
+
const sourceCache = new Map<string, string>();
|
|
7123
|
+
const usageFiles = durableDrizzleUsageFiles(files, config);
|
|
7124
|
+
const requiredDefaultTables: DatabaseSchemaTable[] = [];
|
|
7125
|
+
const missingTables: DatabaseSchemaTable[] = [];
|
|
7126
|
+
const fixes: InspectFix[] = [];
|
|
7127
|
+
|
|
7128
|
+
for (const requirement of durableDrizzleTableRequirements) {
|
|
7129
|
+
const defaultTableName =
|
|
7130
|
+
defaultBeignetConfig.database.tables[requirement.table];
|
|
7131
|
+
if (config.database.tables[requirement.table] !== defaultTableName) {
|
|
7132
|
+
continue;
|
|
7133
|
+
}
|
|
7134
|
+
|
|
7135
|
+
const configuredTables = await configuredDurableDrizzleTableNames(
|
|
7136
|
+
targetDir,
|
|
7137
|
+
usageFiles,
|
|
7138
|
+
config,
|
|
7139
|
+
requirement,
|
|
7140
|
+
sourceCache,
|
|
7141
|
+
);
|
|
7142
|
+
if (!configuredTables.has(defaultTableName)) continue;
|
|
7143
|
+
requiredDefaultTables.push(requirement.table);
|
|
7144
|
+
|
|
7145
|
+
if (
|
|
7146
|
+
await durableDrizzleTableExists(
|
|
7147
|
+
targetDir,
|
|
7148
|
+
files,
|
|
7149
|
+
config,
|
|
7150
|
+
infrastructurePath,
|
|
7151
|
+
schemaDir,
|
|
7152
|
+
requirement,
|
|
7153
|
+
defaultTableName,
|
|
7154
|
+
sourceCache,
|
|
7155
|
+
)
|
|
7156
|
+
) {
|
|
7157
|
+
continue;
|
|
7158
|
+
}
|
|
7159
|
+
|
|
7160
|
+
missingTables.push(requirement.table);
|
|
7161
|
+
fixes.push({
|
|
7162
|
+
code: requirement.code,
|
|
7163
|
+
file: output,
|
|
7164
|
+
message: `Added the default ${defaultTableName} provider table export to ${output}.`,
|
|
7165
|
+
});
|
|
7166
|
+
}
|
|
7167
|
+
|
|
7168
|
+
const outputPath = path.join(targetDir, output);
|
|
7169
|
+
let originalOutput: string | undefined;
|
|
7170
|
+
try {
|
|
7171
|
+
originalOutput = await readFile(outputPath, "utf8");
|
|
7172
|
+
} catch (error) {
|
|
7173
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
7174
|
+
}
|
|
7175
|
+
const existingTables =
|
|
7176
|
+
originalOutput === undefined
|
|
7177
|
+
? []
|
|
7178
|
+
: managedDatabaseSchemaTables(originalOutput);
|
|
7179
|
+
if (existingTables === undefined) return undefined;
|
|
7180
|
+
const originalIndex = await readFile(path.join(targetDir, indexFile), "utf8");
|
|
7181
|
+
const managedExportKind = schemaIndexExportKind(originalIndex, "./beignet");
|
|
7182
|
+
if (managedExportKind === "named") return undefined;
|
|
7183
|
+
const needsManagedExport =
|
|
7184
|
+
originalOutput !== undefined &&
|
|
7185
|
+
existingTables.some((table) => requiredDefaultTables.includes(table)) &&
|
|
7186
|
+
managedExportKind === undefined;
|
|
7187
|
+
if (missingTables.length === 0 && !needsManagedExport) return undefined;
|
|
7188
|
+
if (needsManagedExport) {
|
|
7189
|
+
fixes.push({
|
|
7190
|
+
code: "BEIGNET_DB_SCHEMA_EXPORT_MISSING",
|
|
7191
|
+
file: indexFile,
|
|
7192
|
+
message: `Exported ${output} from ${indexFile}.`,
|
|
7193
|
+
});
|
|
7194
|
+
}
|
|
7195
|
+
|
|
7196
|
+
const selectedTables = (
|
|
7197
|
+
Object.keys(databaseSchemaExports) as DatabaseSchemaTable[]
|
|
7198
|
+
).filter(
|
|
7199
|
+
(table) => existingTables.includes(table) || missingTables.includes(table),
|
|
7200
|
+
);
|
|
7201
|
+
|
|
7202
|
+
let nextOutput = originalOutput;
|
|
7203
|
+
if (missingTables.length > 0) {
|
|
7204
|
+
let dialect: Awaited<ReturnType<typeof resolveDatabaseSchemaDialect>>;
|
|
7205
|
+
try {
|
|
7206
|
+
dialect = await resolveDatabaseSchemaDialect(targetDir, undefined);
|
|
7207
|
+
} catch {
|
|
7208
|
+
return undefined;
|
|
7209
|
+
}
|
|
7210
|
+
nextOutput = renderDatabaseSchema(dialect, selectedTables);
|
|
7211
|
+
}
|
|
7212
|
+
if (nextOutput === undefined) return undefined;
|
|
7213
|
+
|
|
7214
|
+
const addedExportNames = missingTables.map(
|
|
7215
|
+
(table) => databaseSchemaExports[table],
|
|
7216
|
+
);
|
|
7217
|
+
const otherSchemaFiles = files.filter(
|
|
7218
|
+
(file) =>
|
|
7219
|
+
file.startsWith(`${schemaDir}/`) &&
|
|
7220
|
+
file.endsWith(".ts") &&
|
|
7221
|
+
file !== indexFile &&
|
|
7222
|
+
file !== output,
|
|
7223
|
+
);
|
|
7224
|
+
for (const schemaFile of [indexFile, ...otherSchemaFiles]) {
|
|
7225
|
+
const source =
|
|
7226
|
+
schemaFile === indexFile
|
|
7227
|
+
? originalIndex
|
|
7228
|
+
: await readFile(path.join(targetDir, schemaFile), "utf8");
|
|
7229
|
+
if (addedExportNames.some((name) => exportsIdentifier(source, name))) {
|
|
7230
|
+
return undefined;
|
|
7231
|
+
}
|
|
7232
|
+
}
|
|
7233
|
+
const nextIndex =
|
|
7234
|
+
managedExportKind === "all"
|
|
7235
|
+
? originalIndex
|
|
7236
|
+
: `${originalIndex.trimEnd()}\nexport * from "./beignet";\n`;
|
|
7237
|
+
|
|
7238
|
+
const changes = [
|
|
7239
|
+
...(originalOutput === nextOutput
|
|
7240
|
+
? []
|
|
7241
|
+
: [
|
|
7242
|
+
planDoctorFixFileChange({
|
|
7243
|
+
file: output,
|
|
7244
|
+
before: originalOutput,
|
|
7245
|
+
after: nextOutput,
|
|
7246
|
+
}),
|
|
7247
|
+
]),
|
|
7248
|
+
...(originalIndex === nextIndex
|
|
7249
|
+
? []
|
|
7250
|
+
: [
|
|
7251
|
+
planDoctorFixFileChange({
|
|
7252
|
+
file: indexFile,
|
|
7253
|
+
before: originalIndex,
|
|
7254
|
+
after: nextIndex,
|
|
7255
|
+
}),
|
|
7256
|
+
]),
|
|
7257
|
+
];
|
|
7258
|
+
if (changes.length === 0) return undefined;
|
|
7259
|
+
|
|
7260
|
+
return {
|
|
7261
|
+
id: "database.sync-provider-tables",
|
|
7262
|
+
fixes,
|
|
7263
|
+
changes,
|
|
7264
|
+
};
|
|
7265
|
+
}
|
|
7266
|
+
|
|
7267
|
+
function schemaIndexExportKind(
|
|
7268
|
+
source: string,
|
|
7269
|
+
moduleSpecifier: string,
|
|
7270
|
+
): "all" | "named" | undefined {
|
|
7271
|
+
const codeIndexes = new Set(codeCharacterIndexes(source));
|
|
7272
|
+
const escaped = escapeRegExp(moduleSpecifier);
|
|
7273
|
+
const allPattern = new RegExp(
|
|
7274
|
+
`\\bexport\\s*\\*\\s*from\\s*["']${escaped}["']`,
|
|
7275
|
+
"g",
|
|
7276
|
+
);
|
|
7277
|
+
if (
|
|
7278
|
+
[...source.matchAll(allPattern)].some(
|
|
7279
|
+
(match) => match.index !== undefined && codeIndexes.has(match.index),
|
|
7280
|
+
)
|
|
7281
|
+
) {
|
|
7282
|
+
return "all";
|
|
7283
|
+
}
|
|
7284
|
+
|
|
7285
|
+
const customPatterns = [
|
|
7286
|
+
new RegExp(
|
|
7287
|
+
`\\bexport\\s*\\{[\\s\\S]*?\\}\\s*from\\s*["']${escaped}["']`,
|
|
7288
|
+
"g",
|
|
7289
|
+
),
|
|
7290
|
+
new RegExp(
|
|
7291
|
+
`\\bexport\\s*\\*\\s+as\\s+[A-Za-z_$][\\w$]*\\s+from\\s*["']${escaped}["']`,
|
|
7292
|
+
"g",
|
|
7293
|
+
),
|
|
7294
|
+
];
|
|
7295
|
+
return customPatterns.some((pattern) =>
|
|
7296
|
+
[...source.matchAll(pattern)].some(
|
|
7297
|
+
(match) => match.index !== undefined && codeIndexes.has(match.index),
|
|
7298
|
+
),
|
|
7299
|
+
)
|
|
7300
|
+
? "named"
|
|
7301
|
+
: undefined;
|
|
7302
|
+
}
|
|
7303
|
+
|
|
7304
|
+
function exportsIdentifier(source: string, identifier: string): boolean {
|
|
7305
|
+
const executableSource = Array.from({ length: source.length }, () => " ");
|
|
7306
|
+
for (const index of codeCharacterIndexes(source)) {
|
|
7307
|
+
executableSource[index] = source.charAt(index);
|
|
7308
|
+
}
|
|
7309
|
+
|
|
7310
|
+
const escaped = escapeRegExp(identifier);
|
|
7311
|
+
const patterns = [
|
|
7312
|
+
new RegExp(
|
|
7313
|
+
`\\bexport\\s+(?:const|let|var|class|function)\\s+${escaped}\\b`,
|
|
7314
|
+
),
|
|
7315
|
+
new RegExp(`\\bexport\\s*\\{[^}]*\\b${escaped}\\b[^}]*\\}`),
|
|
7316
|
+
];
|
|
7317
|
+
|
|
7318
|
+
const code = executableSource.join("");
|
|
7319
|
+
return patterns.some((pattern) => pattern.test(code));
|
|
7320
|
+
}
|
|
7321
|
+
|
|
7322
|
+
function managedDatabaseSchemaTables(
|
|
7323
|
+
source: string,
|
|
7324
|
+
): DatabaseSchemaTable[] | undefined {
|
|
7325
|
+
if (!source.startsWith(managedDatabaseSchemaMarker)) return undefined;
|
|
7326
|
+
|
|
7327
|
+
const dialect =
|
|
7328
|
+
/@beignet\/provider-db-drizzle\/(sqlite|postgres|mysql)\/schema/.exec(
|
|
7329
|
+
source,
|
|
7330
|
+
)?.[1] as DatabaseSchemaDialect | undefined;
|
|
7331
|
+
if (!dialect) return undefined;
|
|
7332
|
+
|
|
7333
|
+
const tables = (
|
|
7334
|
+
Object.entries(databaseSchemaExports) as Array<
|
|
7335
|
+
[DatabaseSchemaTable, string]
|
|
7336
|
+
>
|
|
7337
|
+
)
|
|
7338
|
+
.filter(([, exportName]) =>
|
|
7339
|
+
new RegExp(`\\b${escapeRegExp(exportName)}\\b`).test(source),
|
|
7340
|
+
)
|
|
7341
|
+
.map(([table]) => table);
|
|
7342
|
+
if (tables.length === 0) return undefined;
|
|
7343
|
+
|
|
7344
|
+
return renderDatabaseSchema(dialect, tables) === source ? tables : undefined;
|
|
7345
|
+
}
|
|
7346
|
+
|
|
7347
|
+
async function planUnregisteredInngestJobs(
|
|
7348
|
+
targetDir: string,
|
|
7349
|
+
files: string[],
|
|
7350
|
+
config: ResolvedBeignetConfig,
|
|
7351
|
+
): Promise<PlannedDoctorFixOperation | undefined> {
|
|
7352
|
+
const packageJson = await readPackageJson(targetDir, files);
|
|
7353
|
+
if (
|
|
7354
|
+
!installedPackageNames(packageJson).has("@beignet/provider-jobs-inngest")
|
|
7355
|
+
) {
|
|
7356
|
+
return undefined;
|
|
7357
|
+
}
|
|
7358
|
+
|
|
7359
|
+
const serverDir = directoryPath(path.dirname(config.paths.server));
|
|
7360
|
+
const registryFile = `${serverDir}/inngest.ts`;
|
|
7361
|
+
if (!files.includes(registryFile)) return undefined;
|
|
7362
|
+
|
|
7363
|
+
const source = await readFile(path.join(targetDir, registryFile), "utf8");
|
|
7364
|
+
const registry = arrayInitializerInfo(source, "inngestJobs");
|
|
7365
|
+
if (!registry) return undefined;
|
|
7366
|
+
|
|
7367
|
+
const featureJobs = (
|
|
7368
|
+
await readFeatureWorkflowRegistries(targetDir, files, config)
|
|
7369
|
+
).filter((entry) => entry.kind === "jobs");
|
|
7370
|
+
const unregistered = unregisteredWorkflowRegistries(
|
|
7371
|
+
featureJobs,
|
|
7372
|
+
canonicalRegisteredWorkflowIdentifiers({
|
|
7373
|
+
source,
|
|
7374
|
+
registered: identifiersFromArrayExpression(registry.text),
|
|
7375
|
+
centralFile: registryFile,
|
|
7376
|
+
files,
|
|
7377
|
+
registries: featureJobs,
|
|
7378
|
+
}),
|
|
7379
|
+
);
|
|
7380
|
+
|
|
7381
|
+
return planWorkflowRegistryOperation("inngest.register-missing", {
|
|
7382
|
+
targetDir,
|
|
7383
|
+
files,
|
|
7384
|
+
centralFile: registryFile,
|
|
7385
|
+
unregistered,
|
|
7386
|
+
code: "BEIGNET_INNGEST_JOB_REGISTRATION_MISSING",
|
|
7387
|
+
listName: "the Inngest jobs array",
|
|
7388
|
+
importSpecifier: (indexFile) => relativeModule(registryFile, indexFile),
|
|
7389
|
+
append: (nextSource, entry, importLine) =>
|
|
7390
|
+
appendToNamedArray(nextSource, "inngestJobs", entry, importLine),
|
|
7391
|
+
});
|
|
7392
|
+
}
|
|
7393
|
+
|
|
7394
|
+
const runtimeManifestRepairChecks: readonly {
|
|
7395
|
+
kind: WorkflowRegistryKind;
|
|
7396
|
+
code: string;
|
|
7397
|
+
listName: string;
|
|
7398
|
+
}[] = [
|
|
7399
|
+
{
|
|
7400
|
+
kind: "listeners",
|
|
7401
|
+
code: "BEIGNET_RUNTIME_MANIFEST_LISTENER_MISSING",
|
|
7402
|
+
listName: "the runtime manifest listeners array",
|
|
7403
|
+
},
|
|
7404
|
+
{
|
|
7405
|
+
kind: "schedules",
|
|
7406
|
+
code: "BEIGNET_RUNTIME_MANIFEST_SCHEDULE_MISSING",
|
|
7407
|
+
listName: "the runtime manifest schedules array",
|
|
7408
|
+
},
|
|
7409
|
+
{
|
|
7410
|
+
kind: "tasks",
|
|
7411
|
+
code: "BEIGNET_RUNTIME_MANIFEST_TASK_MISSING",
|
|
7412
|
+
listName: "the runtime manifest tasks array",
|
|
7413
|
+
},
|
|
7414
|
+
{
|
|
7415
|
+
kind: "events",
|
|
7416
|
+
code: "BEIGNET_RUNTIME_MANIFEST_OUTBOX_EVENT_MISSING",
|
|
7417
|
+
listName: "the runtime manifest outbox events array",
|
|
7418
|
+
},
|
|
7419
|
+
{
|
|
7420
|
+
kind: "jobs",
|
|
7421
|
+
code: "BEIGNET_RUNTIME_MANIFEST_OUTBOX_JOB_MISSING",
|
|
7422
|
+
listName: "the runtime manifest outbox jobs array",
|
|
7423
|
+
},
|
|
7424
|
+
];
|
|
7425
|
+
|
|
7426
|
+
async function planRuntimeManifestRegistries(
|
|
7427
|
+
targetDir: string,
|
|
7428
|
+
files: string[],
|
|
7429
|
+
config: ResolvedBeignetConfig,
|
|
7430
|
+
convention: InspectConvention,
|
|
7431
|
+
sourceOverrides: ReadonlyMap<string, string> = new Map(),
|
|
7432
|
+
): Promise<PlannedDoctorFixOperation | undefined> {
|
|
7433
|
+
if (!convention.resourceGenerator) return undefined;
|
|
7434
|
+
|
|
7435
|
+
const manifest = await readRuntimeManifestIdentifiers(targetDir, files);
|
|
7436
|
+
if (!manifest) return undefined;
|
|
7437
|
+
|
|
7438
|
+
const original =
|
|
7439
|
+
sourceOverrides.get(manifest.file) ??
|
|
7440
|
+
(await readFile(path.join(targetDir, manifest.file), "utf8"));
|
|
7441
|
+
const registries = await readFeatureWorkflowRegistries(
|
|
7442
|
+
targetDir,
|
|
7443
|
+
files,
|
|
7444
|
+
config,
|
|
7445
|
+
);
|
|
7446
|
+
let next = original;
|
|
7447
|
+
const fixes: InspectFix[] = [];
|
|
7448
|
+
|
|
7449
|
+
for (const check of runtimeManifestRepairChecks) {
|
|
7450
|
+
const parsedRegistered =
|
|
7451
|
+
runtimeManifestIdentifiers(next)?.[
|
|
7452
|
+
runtimeManifestIdentifierKey(check.kind)
|
|
7453
|
+
];
|
|
7454
|
+
if (!parsedRegistered) continue;
|
|
7455
|
+
const kindRegistries = registries.filter(
|
|
7456
|
+
(registry) => registry.kind === check.kind,
|
|
7457
|
+
);
|
|
7458
|
+
const registered = canonicalRegisteredWorkflowIdentifiers({
|
|
7459
|
+
source: next,
|
|
7460
|
+
registered: parsedRegistered,
|
|
7461
|
+
centralFile: manifest.file,
|
|
7462
|
+
files,
|
|
7463
|
+
registries: kindRegistries,
|
|
7464
|
+
});
|
|
7465
|
+
|
|
7466
|
+
const edit = planWorkflowRegistryEdit({
|
|
7467
|
+
files,
|
|
7468
|
+
centralFile: manifest.file,
|
|
7469
|
+
source: next,
|
|
7470
|
+
unregistered: unregisteredWorkflowRegistries(kindRegistries, registered),
|
|
7471
|
+
code: check.code,
|
|
7472
|
+
listName: check.listName,
|
|
7473
|
+
importSpecifier: (indexFile) => relativeModule(manifest.file, indexFile),
|
|
7474
|
+
append: (nextSource, entry, importLine) =>
|
|
7475
|
+
appendToRuntimeManifestArray(nextSource, check.kind, entry, importLine),
|
|
7476
|
+
});
|
|
7477
|
+
if (!edit) continue;
|
|
7478
|
+
next = edit.after;
|
|
7479
|
+
fixes.push(edit.fix);
|
|
7480
|
+
}
|
|
7481
|
+
|
|
7482
|
+
if (fixes.length === 0 || next === original) return undefined;
|
|
7483
|
+
|
|
7484
|
+
return {
|
|
7485
|
+
id: "runtime-manifest.register-missing",
|
|
7486
|
+
fixes,
|
|
7487
|
+
changes: [
|
|
7488
|
+
planDoctorFixFileChange({
|
|
7489
|
+
file: manifest.file,
|
|
7490
|
+
before: original,
|
|
7491
|
+
after: next,
|
|
7492
|
+
}),
|
|
7493
|
+
],
|
|
7494
|
+
};
|
|
7495
|
+
}
|
|
7496
|
+
|
|
7497
|
+
function runtimeManifestIdentifierKey(
|
|
7498
|
+
kind: WorkflowRegistryKind,
|
|
7499
|
+
): keyof Omit<RuntimeManifestIdentifierSets, "file"> {
|
|
7500
|
+
return kind;
|
|
7501
|
+
}
|
|
7502
|
+
|
|
7503
|
+
function runtimeManifestArrayInfo(
|
|
7504
|
+
source: string,
|
|
7505
|
+
kind: WorkflowRegistryKind,
|
|
7506
|
+
): { text: string; start: number; end: number } | undefined {
|
|
7507
|
+
const calls = runtimeManifestCallMatches(source);
|
|
7508
|
+
if (calls.length !== 1) return undefined;
|
|
7509
|
+
const call = calls[0];
|
|
7510
|
+
|
|
7511
|
+
const arg = firstCallArgInfo(source, (call.index ?? 0) + call[0].length);
|
|
7512
|
+
if (!arg) return undefined;
|
|
7513
|
+
|
|
7514
|
+
if (kind === "listeners" || kind === "schedules" || kind === "tasks") {
|
|
7515
|
+
const value = objectPropertyValueInfo(arg.text, kind);
|
|
7516
|
+
return value
|
|
7517
|
+
? {
|
|
7518
|
+
text: value.text,
|
|
7519
|
+
start: arg.start + value.start,
|
|
7520
|
+
end: arg.start + value.end,
|
|
7521
|
+
}
|
|
7522
|
+
: undefined;
|
|
7523
|
+
}
|
|
7524
|
+
|
|
7525
|
+
const outbox = objectPropertyValueInfo(arg.text, "outbox");
|
|
7526
|
+
if (!outbox) return undefined;
|
|
7527
|
+
const value = objectPropertyValueInfo(outbox.text, kind);
|
|
7528
|
+
return value
|
|
7529
|
+
? {
|
|
7530
|
+
text: value.text,
|
|
7531
|
+
start: arg.start + outbox.start + value.start,
|
|
7532
|
+
end: arg.start + outbox.start + value.end,
|
|
7533
|
+
}
|
|
7534
|
+
: undefined;
|
|
7535
|
+
}
|
|
7536
|
+
|
|
7537
|
+
function appendToRuntimeManifestArray(
|
|
7538
|
+
source: string,
|
|
7539
|
+
kind: WorkflowRegistryKind,
|
|
7540
|
+
entry: string,
|
|
7541
|
+
importLine?: string,
|
|
7542
|
+
): AppendResult {
|
|
7543
|
+
const value = runtimeManifestArrayInfo(source, kind);
|
|
7544
|
+
if (!value?.text.startsWith("[")) return { kind: "missing" };
|
|
7545
|
+
|
|
7546
|
+
const entryName = entry.replace(/^\.{3}/, "").trim();
|
|
7547
|
+
if (identifiersFromArrayExpression(value.text).has(entryName)) {
|
|
7548
|
+
return { kind: "unchanged" };
|
|
7549
|
+
}
|
|
7550
|
+
|
|
7551
|
+
const nextArray = appendToArrayExpression(value.text, [entry]);
|
|
7552
|
+
let next = `${source.slice(0, value.start)}${nextArray}${source.slice(
|
|
7553
|
+
value.end,
|
|
7554
|
+
)}`;
|
|
7555
|
+
if (importLine && !next.includes(importLine)) {
|
|
7556
|
+
next = insertAfterImports(next, importLine);
|
|
7557
|
+
}
|
|
7558
|
+
return next === source
|
|
7559
|
+
? { kind: "unchanged" }
|
|
7560
|
+
: { kind: "updated", source: next };
|
|
7561
|
+
}
|
|
7562
|
+
|
|
6818
7563
|
async function planDirectOpenApiArrayDrift(
|
|
6819
7564
|
targetDir: string,
|
|
6820
7565
|
files: string[],
|
|
@@ -7372,7 +8117,8 @@ function planWorkflowRegistryEdit(
|
|
|
7372
8117
|
const registeredNames: string[] = [];
|
|
7373
8118
|
|
|
7374
8119
|
for (const { registry } of candidates) {
|
|
7375
|
-
const
|
|
8120
|
+
const imports = parseNamedImportSources(next);
|
|
8121
|
+
const imported = imports.get(registry.registryName);
|
|
7376
8122
|
let importLine: string | undefined;
|
|
7377
8123
|
|
|
7378
8124
|
if (imported) {
|
|
@@ -7383,6 +8129,21 @@ function planWorkflowRegistryEdit(
|
|
|
7383
8129
|
);
|
|
7384
8130
|
if (importedFile !== registry.indexFile) return undefined;
|
|
7385
8131
|
} else {
|
|
8132
|
+
const aliasesRegistry = [...imports.entries()].some(
|
|
8133
|
+
([localName, candidate]) =>
|
|
8134
|
+
localName !== registry.registryName &&
|
|
8135
|
+
candidate.importedName === registry.registryName &&
|
|
8136
|
+
sourceFileFromImport(
|
|
8137
|
+
candidate.sourcePath,
|
|
8138
|
+
options.centralFile,
|
|
8139
|
+
options.files,
|
|
8140
|
+
) === registry.indexFile,
|
|
8141
|
+
);
|
|
8142
|
+
if (aliasesRegistry) return undefined;
|
|
8143
|
+
if (sourceContainsCodeIdentifier(next, registry.registryName)) {
|
|
8144
|
+
return undefined;
|
|
8145
|
+
}
|
|
8146
|
+
|
|
7386
8147
|
importLine = `import { ${registry.registryName} } from "${options.importSpecifier(
|
|
7387
8148
|
registry.indexFile,
|
|
7388
8149
|
)}";`;
|
|
@@ -7412,6 +8173,22 @@ function planWorkflowRegistryEdit(
|
|
|
7412
8173
|
};
|
|
7413
8174
|
}
|
|
7414
8175
|
|
|
8176
|
+
function sourceContainsCodeIdentifier(
|
|
8177
|
+
source: string,
|
|
8178
|
+
identifier: string,
|
|
8179
|
+
): boolean {
|
|
8180
|
+
const codeIndexes = new Set(codeCharacterIndexes(source));
|
|
8181
|
+
const pattern = new RegExp(
|
|
8182
|
+
`(^|[^A-Za-z0-9_$])${escapeRegExp(identifier)}(?![A-Za-z0-9_$])`,
|
|
8183
|
+
"g",
|
|
8184
|
+
);
|
|
8185
|
+
return [...source.matchAll(pattern)].some(
|
|
8186
|
+
(match) =>
|
|
8187
|
+
match.index !== undefined &&
|
|
8188
|
+
codeIndexes.has(match.index + (match[1]?.length ?? 0)),
|
|
8189
|
+
);
|
|
8190
|
+
}
|
|
8191
|
+
|
|
7415
8192
|
function routeRegistryFileFromServerSource(
|
|
7416
8193
|
source: string,
|
|
7417
8194
|
serverFile: string,
|