@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/dist/inspect.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { parseProviderPackageMetadata } from "@beignet/core/providers";
|
|
4
4
|
import { createPainter } from "./ansi.js";
|
|
5
5
|
import { clientDirPath, clientFormsPath, clientIndexPath, defaultBeignetConfig, directoryPath, loadBeignetConfig, normalizePath, resolveConfig, } from "./config.js";
|
|
6
|
+
import { renderDatabaseSchema, resolveDatabaseSchemaDialect } from "./db.js";
|
|
6
7
|
import { applyPlannedDoctorFixes, createDoctorFixPlan, planDoctorFixFileChange, publicDoctorFixPlan, } from "./doctor-fixes.js";
|
|
7
8
|
import { formatGithubAnnotation, } from "./github-annotations.js";
|
|
8
9
|
import { createListenersRegistrySource, listenersFilePath, providersFilePath, updateListenersRegistrySource, wireListenersProviderSource, } from "./make/shared.js";
|
|
@@ -104,15 +105,83 @@ async function buildDoctorFixPlan(options = {}) {
|
|
|
104
105
|
const listenerFix = await planUnregisteredListeners(targetDir, files, drift, config);
|
|
105
106
|
if (listenerFix)
|
|
106
107
|
operations.push(listenerFix);
|
|
108
|
+
const databaseFix = await planMissingProviderTables(targetDir, files, config);
|
|
109
|
+
if (databaseFix)
|
|
110
|
+
operations.push(databaseFix);
|
|
111
|
+
const inngestFix = await planUnregisteredInngestJobs(targetDir, files, config);
|
|
112
|
+
if (inngestFix)
|
|
113
|
+
operations.push(inngestFix);
|
|
114
|
+
const nonOverlappingOperations = nonOverlappingDoctorFixOperations(operations);
|
|
115
|
+
operations.splice(0, operations.length, ...nonOverlappingOperations);
|
|
116
|
+
const sourceOverrides = new Map();
|
|
117
|
+
for (const operation of operations) {
|
|
118
|
+
for (const change of operation.changes) {
|
|
119
|
+
sourceOverrides.set(change.file, change.after);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const runtimeManifestFix = await planRuntimeManifestRegistries(targetDir, files, config, convention, sourceOverrides);
|
|
123
|
+
if (runtimeManifestFix) {
|
|
124
|
+
addRuntimeManifestFix(operations, runtimeManifestFix);
|
|
125
|
+
}
|
|
107
126
|
const openApiFix = await planDirectOpenApiArrayDrift(targetDir, files, config, convention, contracts, matchedRoutes);
|
|
108
127
|
if (openApiFix)
|
|
109
128
|
operations.push(openApiFix);
|
|
110
129
|
return createDoctorFixPlan({
|
|
111
130
|
targetDir,
|
|
112
131
|
strict: Boolean(options.strict),
|
|
113
|
-
operations,
|
|
132
|
+
operations: nonOverlappingDoctorFixOperations(operations),
|
|
114
133
|
});
|
|
115
134
|
}
|
|
135
|
+
function nonOverlappingDoctorFixOperations(operations) {
|
|
136
|
+
const claimedFiles = new Set();
|
|
137
|
+
const safeOperations = [];
|
|
138
|
+
for (const operation of operations) {
|
|
139
|
+
if (operation.changes.some((change) => claimedFiles.has(change.file))) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
safeOperations.push(operation);
|
|
143
|
+
for (const change of operation.changes)
|
|
144
|
+
claimedFiles.add(change.file);
|
|
145
|
+
}
|
|
146
|
+
return safeOperations;
|
|
147
|
+
}
|
|
148
|
+
const workflowDoctorFixOperationIds = new Set([
|
|
149
|
+
"schedules.register-missing",
|
|
150
|
+
"tasks.register-missing",
|
|
151
|
+
"workflows.register-missing",
|
|
152
|
+
"outbox.register-missing",
|
|
153
|
+
"listeners.register-missing",
|
|
154
|
+
"inngest.register-missing",
|
|
155
|
+
"runtime-manifest.register-missing",
|
|
156
|
+
]);
|
|
157
|
+
function addRuntimeManifestFix(operations, runtimeManifestFix) {
|
|
158
|
+
const runtimeChange = runtimeManifestFix.changes[0];
|
|
159
|
+
if (!runtimeChange)
|
|
160
|
+
return;
|
|
161
|
+
const overlappingOperation = operations.find((operation) => operation.changes.some((change) => change.file === runtimeChange.file));
|
|
162
|
+
if (!overlappingOperation) {
|
|
163
|
+
operations.push(runtimeManifestFix);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (!workflowDoctorFixOperationIds.has(overlappingOperation.id))
|
|
167
|
+
return;
|
|
168
|
+
overlappingOperation.id = "workflows.register-missing";
|
|
169
|
+
overlappingOperation.fixes.push(...runtimeManifestFix.fixes);
|
|
170
|
+
overlappingOperation.changes = overlappingOperation.changes.map((change) => change.file === runtimeChange.file
|
|
171
|
+
? planDoctorFixFileChange({
|
|
172
|
+
file: change.file,
|
|
173
|
+
before: change.before,
|
|
174
|
+
after: runtimeChange.after,
|
|
175
|
+
})
|
|
176
|
+
: change);
|
|
177
|
+
const existingWorkflowOperation = operations.find((operation) => operation !== overlappingOperation &&
|
|
178
|
+
operation.id === "workflows.register-missing");
|
|
179
|
+
if (!existingWorkflowOperation)
|
|
180
|
+
return;
|
|
181
|
+
existingWorkflowOperation.fixes.push(...overlappingOperation.fixes);
|
|
182
|
+
existingWorkflowOperation.changes.push(...overlappingOperation.changes);
|
|
183
|
+
operations.splice(operations.indexOf(overlappingOperation), 1);
|
|
184
|
+
}
|
|
116
185
|
/**
|
|
117
186
|
* Format inspected routes as a CLI table.
|
|
118
187
|
*/
|
|
@@ -1402,6 +1471,7 @@ async function inspectProductionReadiness(targetDir, files, config, convention)
|
|
|
1402
1471
|
const installedPackages = installedPackageNames(packageJson);
|
|
1403
1472
|
const providerDoctorRules = await providerDoctorRulesForInstalledPackages(targetDir, packageJson);
|
|
1404
1473
|
const configFiles = operationalConfigFiles(files, config);
|
|
1474
|
+
const cronDirectory = path.posix.join(directoryPath(config.paths.routes), "cron");
|
|
1405
1475
|
const infrastructurePath = directoryPath(path.dirname(config.paths.portWiring));
|
|
1406
1476
|
const schemaDir = `${infrastructurePath}/db/schema`;
|
|
1407
1477
|
let hasSecurityHeadersHook = false;
|
|
@@ -1424,15 +1494,16 @@ async function inspectProductionReadiness(targetDir, files, config, convention)
|
|
|
1424
1494
|
message: `${file} enables devtools with enabled: true and no authorize callback. Keep devtools disabled in production or add app-owned authorization before exposing diagnostic data.`,
|
|
1425
1495
|
});
|
|
1426
1496
|
}
|
|
1427
|
-
if (file
|
|
1497
|
+
if (fileWithinDirectory(file, cronDirectory) &&
|
|
1428
1498
|
file.endsWith("/route.ts") &&
|
|
1429
1499
|
!/\bCRON_SECRET\b/.test(source) &&
|
|
1430
|
-
!/\bcreateOutboxDrainRoute\s*\(/.test(source)
|
|
1500
|
+
!/\bcreateOutboxDrainRoute\s*\(/.test(source) &&
|
|
1501
|
+
!/\bcreateScheduleRoute\s*\(/.test(source)) {
|
|
1431
1502
|
diagnostics.push({
|
|
1432
1503
|
severity: "warning",
|
|
1433
1504
|
code: "BEIGNET_CRON_SECRET_MISSING",
|
|
1434
1505
|
file,
|
|
1435
|
-
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(...).`,
|
|
1506
|
+
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(...).`,
|
|
1436
1507
|
});
|
|
1437
1508
|
}
|
|
1438
1509
|
if (isFeatureUploadDefinition(file, config) &&
|
|
@@ -1965,23 +2036,23 @@ async function inspectInngestProductionPath(targetDir, files, config) {
|
|
|
1965
2036
|
else {
|
|
1966
2037
|
const registrySource = await readFile(path.join(targetDir, registryFile), "utf8");
|
|
1967
2038
|
const registry = arrayInitializerInfo(registrySource, "inngestJobs");
|
|
1968
|
-
const
|
|
1969
|
-
|
|
1970
|
-
:
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
2039
|
+
const featureJobs = (await readFeatureWorkflowRegistries(targetDir, files, config)).filter((entry) => entry.kind === "jobs");
|
|
2040
|
+
const registeredJobs = canonicalRegisteredWorkflowIdentifiers({
|
|
2041
|
+
source: registrySource,
|
|
2042
|
+
registered: registry
|
|
2043
|
+
? identifiersFromArrayExpression(registry.text)
|
|
2044
|
+
: new Set(),
|
|
2045
|
+
centralFile: registryFile,
|
|
2046
|
+
files,
|
|
2047
|
+
registries: featureJobs,
|
|
2048
|
+
});
|
|
2049
|
+
const unregisteredJobs = unregisteredWorkflowRegistries(featureJobs, registeredJobs);
|
|
2050
|
+
for (const { registry: featureRegistry } of unregisteredJobs) {
|
|
1980
2051
|
diagnostics.push({
|
|
1981
2052
|
severity: "warning",
|
|
1982
2053
|
code: "BEIGNET_INNGEST_JOB_REGISTRATION_MISSING",
|
|
1983
2054
|
file: registryFile,
|
|
1984
|
-
message: `${
|
|
2055
|
+
message: `${featureRegistry.indexFile} exports ${featureRegistry.registryName}, but it is not registered in inngestJobs in ${registryFile}. Add ...${featureRegistry.registryName} so Inngest can execute the job.`,
|
|
1985
2056
|
});
|
|
1986
2057
|
}
|
|
1987
2058
|
}
|
|
@@ -2323,7 +2394,9 @@ function isInfraOrServerFile(file, config) {
|
|
|
2323
2394
|
file.startsWith(`${infrastructureDir}/`));
|
|
2324
2395
|
}
|
|
2325
2396
|
function containsCallExpression(source, name) {
|
|
2326
|
-
|
|
2397
|
+
const codeIndexes = new Set(codeCharacterIndexes(source));
|
|
2398
|
+
const pattern = new RegExp(`\\b${escapeRegExp(name)}\\s*(?:<[^;=(){}]*>\\s*)?\\(`, "g");
|
|
2399
|
+
return [...source.matchAll(pattern)].some((match) => match.index !== undefined && codeIndexes.has(match.index));
|
|
2327
2400
|
}
|
|
2328
2401
|
const workflowRegistrySuffixes = {
|
|
2329
2402
|
schedules: "Schedules",
|
|
@@ -2412,6 +2485,7 @@ async function inspectRuntimeManifestDrift(targetDir, files, config, convention)
|
|
|
2412
2485
|
if (!manifest)
|
|
2413
2486
|
return [];
|
|
2414
2487
|
const registries = await readFeatureWorkflowRegistries(targetDir, files, config);
|
|
2488
|
+
const manifestSource = await readFile(path.join(targetDir, manifest.file), "utf8");
|
|
2415
2489
|
const diagnostics = [];
|
|
2416
2490
|
const checks = [
|
|
2417
2491
|
{
|
|
@@ -2446,7 +2520,14 @@ async function inspectRuntimeManifestDrift(targetDir, files, config, convention)
|
|
|
2446
2520
|
},
|
|
2447
2521
|
];
|
|
2448
2522
|
for (const check of checks) {
|
|
2449
|
-
const
|
|
2523
|
+
const kindRegistries = registries.filter((registry) => registry.kind === check.kind);
|
|
2524
|
+
const missing = unregisteredWorkflowRegistries(kindRegistries, canonicalRegisteredWorkflowIdentifiers({
|
|
2525
|
+
source: manifestSource,
|
|
2526
|
+
registered: check.registered,
|
|
2527
|
+
centralFile: manifest.file,
|
|
2528
|
+
files,
|
|
2529
|
+
registries: kindRegistries,
|
|
2530
|
+
}));
|
|
2450
2531
|
for (const { registry, missingMembers } of missing) {
|
|
2451
2532
|
for (const member of missingMembers) {
|
|
2452
2533
|
diagnostics.push({
|
|
@@ -2557,6 +2638,30 @@ function unregisteredWorkflowRegistries(registries, registered, memberApplies =
|
|
|
2557
2638
|
}
|
|
2558
2639
|
return unregistered;
|
|
2559
2640
|
}
|
|
2641
|
+
function canonicalRegisteredWorkflowIdentifiers(options) {
|
|
2642
|
+
const canonical = new Set(options.registered);
|
|
2643
|
+
const imports = parseNamedImportSources(options.source);
|
|
2644
|
+
for (const identifier of options.registered) {
|
|
2645
|
+
const imported = imports.get(identifier);
|
|
2646
|
+
if (!imported)
|
|
2647
|
+
continue;
|
|
2648
|
+
const importedFile = sourceFileFromImport(imported.sourcePath, options.centralFile, options.files);
|
|
2649
|
+
if (!importedFile)
|
|
2650
|
+
continue;
|
|
2651
|
+
for (const registry of options.registries) {
|
|
2652
|
+
if (imported.importedName === registry.registryName &&
|
|
2653
|
+
importedFile === registry.indexFile) {
|
|
2654
|
+
canonical.add(registry.registryName);
|
|
2655
|
+
}
|
|
2656
|
+
if (registry.members.includes(imported.importedName) &&
|
|
2657
|
+
(importedFile === registry.indexFile ||
|
|
2658
|
+
registry.memberFiles.get(imported.importedName) === importedFile)) {
|
|
2659
|
+
canonical.add(imported.importedName);
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
return canonical;
|
|
2664
|
+
}
|
|
2560
2665
|
async function readFeatureWorkflowRegistries(targetDir, files, config) {
|
|
2561
2666
|
const registries = [];
|
|
2562
2667
|
const featuresPath = directoryPath(config.paths.features);
|
|
@@ -2612,6 +2717,7 @@ function exportedMemberFiles(source, indexFile, files) {
|
|
|
2612
2717
|
return memberFiles;
|
|
2613
2718
|
}
|
|
2614
2719
|
async function readRuntimeManifestIdentifiers(targetDir, files) {
|
|
2720
|
+
const manifests = [];
|
|
2615
2721
|
for (const file of files) {
|
|
2616
2722
|
if (!file.endsWith(".ts") && !file.endsWith(".tsx"))
|
|
2617
2723
|
continue;
|
|
@@ -2620,21 +2726,27 @@ async function readRuntimeManifestIdentifiers(targetDir, files) {
|
|
|
2620
2726
|
const source = await readFile(path.join(targetDir, file), "utf8");
|
|
2621
2727
|
if (!source.includes("defineRuntimeManifest"))
|
|
2622
2728
|
continue;
|
|
2729
|
+
const calls = runtimeManifestCallMatches(source);
|
|
2730
|
+
if (calls.length === 0)
|
|
2731
|
+
continue;
|
|
2732
|
+
if (calls.length !== 1)
|
|
2733
|
+
return undefined;
|
|
2623
2734
|
const identifiers = runtimeManifestIdentifiers(source);
|
|
2624
2735
|
if (identifiers) {
|
|
2625
|
-
|
|
2736
|
+
manifests.push({
|
|
2626
2737
|
file,
|
|
2627
2738
|
...identifiers,
|
|
2628
|
-
};
|
|
2739
|
+
});
|
|
2629
2740
|
}
|
|
2630
2741
|
}
|
|
2631
|
-
return undefined;
|
|
2742
|
+
return manifests.length === 1 ? manifests[0] : undefined;
|
|
2632
2743
|
}
|
|
2633
2744
|
function runtimeManifestIdentifiers(source) {
|
|
2634
|
-
const
|
|
2635
|
-
if (
|
|
2745
|
+
const calls = runtimeManifestCallMatches(source);
|
|
2746
|
+
if (calls.length !== 1)
|
|
2636
2747
|
return undefined;
|
|
2637
|
-
const
|
|
2748
|
+
const call = calls[0];
|
|
2749
|
+
const arg = firstCallArgInfo(source, (call.index ?? 0) + call[0].length);
|
|
2638
2750
|
if (!arg)
|
|
2639
2751
|
return undefined;
|
|
2640
2752
|
const outbox = objectPropertyValueInfo(arg.text, "outbox");
|
|
@@ -2650,6 +2762,12 @@ function runtimeManifestIdentifiers(source) {
|
|
|
2650
2762
|
: new Set(),
|
|
2651
2763
|
};
|
|
2652
2764
|
}
|
|
2765
|
+
function runtimeManifestCallMatches(source) {
|
|
2766
|
+
const codeIndexes = new Set(codeCharacterIndexes(source));
|
|
2767
|
+
return [
|
|
2768
|
+
...source.matchAll(/\bdefineRuntimeManifest\s*(?:<[^>]*>\s*)?\(/g),
|
|
2769
|
+
].filter((match) => match.index !== undefined && codeIndexes.has(match.index));
|
|
2770
|
+
}
|
|
2653
2771
|
function identifiersFromPropertyArray(source, property) {
|
|
2654
2772
|
const value = objectPropertyValueInfo(source, property);
|
|
2655
2773
|
if (!value?.text.startsWith("["))
|
|
@@ -2657,24 +2775,46 @@ function identifiersFromPropertyArray(source, property) {
|
|
|
2657
2775
|
return identifiersFromArrayExpression(value.text);
|
|
2658
2776
|
}
|
|
2659
2777
|
function objectPropertyValueInfo(source, property) {
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2778
|
+
let depth = 0;
|
|
2779
|
+
for (const index of codeCharacterIndexes(source)) {
|
|
2780
|
+
const char = source[index];
|
|
2781
|
+
if (char === "{" || char === "[" || char === "(") {
|
|
2782
|
+
depth++;
|
|
2783
|
+
continue;
|
|
2784
|
+
}
|
|
2785
|
+
if (char === "}" || char === "]" || char === ")") {
|
|
2786
|
+
depth--;
|
|
2787
|
+
continue;
|
|
2788
|
+
}
|
|
2789
|
+
if (depth !== 1 ||
|
|
2790
|
+
source.slice(index, index + property.length) !== property ||
|
|
2791
|
+
/[A-Za-z0-9_$]/.test(source[index - 1] ?? "")) {
|
|
2792
|
+
continue;
|
|
2793
|
+
}
|
|
2794
|
+
let colon = index + property.length;
|
|
2795
|
+
if (/[A-Za-z0-9_$]/.test(source[colon] ?? ""))
|
|
2796
|
+
continue;
|
|
2797
|
+
while (colon < source.length && /\s/.test(source[colon]))
|
|
2798
|
+
colon++;
|
|
2799
|
+
if (source[colon] !== ":")
|
|
2800
|
+
continue;
|
|
2801
|
+
let start = colon + 1;
|
|
2802
|
+
while (start < source.length && /\s/.test(source[start]))
|
|
2803
|
+
start++;
|
|
2804
|
+
const open = source[start];
|
|
2805
|
+
const close = open === "[" ? "]" : open === "{" ? "}" : undefined;
|
|
2806
|
+
if (!close)
|
|
2807
|
+
return undefined;
|
|
2808
|
+
const end = matchingDelimiterIndex(source, start, open, close);
|
|
2809
|
+
if (end === -1)
|
|
2810
|
+
return undefined;
|
|
2811
|
+
return {
|
|
2812
|
+
text: source.slice(start, end + 1),
|
|
2813
|
+
start,
|
|
2814
|
+
end: end + 1,
|
|
2815
|
+
};
|
|
2816
|
+
}
|
|
2817
|
+
return undefined;
|
|
2678
2818
|
}
|
|
2679
2819
|
function registeredWorkflowIdentifiers(source, kind) {
|
|
2680
2820
|
if (kind === "tasks") {
|
|
@@ -2793,16 +2933,17 @@ function isWorkflowTestFile(file) {
|
|
|
2793
2933
|
}
|
|
2794
2934
|
async function inspectServerlessFootguns(targetDir, files, config, convention) {
|
|
2795
2935
|
const diagnostics = [];
|
|
2936
|
+
const routeDirectory = directoryPath(config.paths.routes);
|
|
2937
|
+
const cronDirectory = path.posix.join(routeDirectory, "cron");
|
|
2796
2938
|
for (const file of files) {
|
|
2797
2939
|
if (!file.endsWith(".ts") ||
|
|
2798
2940
|
file.endsWith(".test.ts") ||
|
|
2799
2941
|
file.endsWith(".d.ts")) {
|
|
2800
2942
|
continue;
|
|
2801
2943
|
}
|
|
2802
|
-
const isInfraOrServerFile = file.startsWith("infra/") || file.startsWith("server/");
|
|
2803
|
-
const isProviderFile = /(^|\/)provider(s)?\.ts$/.test(file) ||
|
|
2804
|
-
/(^|\/)[^/]*provider[^/]*\.ts$/.test(file);
|
|
2805
2944
|
const source = await readFile(path.join(targetDir, file), "utf8");
|
|
2945
|
+
const isInfraOrServerFile = isRuntimeFile(file, config);
|
|
2946
|
+
const isProviderFile = isProviderLifecycleFile(file, source);
|
|
2806
2947
|
if (config.framework === "next" &&
|
|
2807
2948
|
convention.resourceGenerator &&
|
|
2808
2949
|
isNextRouteFile(file, config) &&
|
|
@@ -2835,7 +2976,7 @@ async function inspectServerlessFootguns(targetDir, files, config, convention) {
|
|
|
2835
2976
|
message: `${file} drains the outbox from provider lifecycle code. Move outbox delivery to createOutboxDrainRoute(...) or another bounded runtime entrypoint so imports and serverless cold starts do not run background work.`,
|
|
2836
2977
|
});
|
|
2837
2978
|
}
|
|
2838
|
-
if (file
|
|
2979
|
+
if (fileWithinDirectory(file, cronDirectory) &&
|
|
2839
2980
|
file.endsWith("/route.ts") &&
|
|
2840
2981
|
!/\bauthorization\b/i.test(source) &&
|
|
2841
2982
|
!/\bcreateOutboxDrainRoute\s*\(/.test(source) &&
|
|
@@ -2848,8 +2989,8 @@ async function inspectServerlessFootguns(targetDir, files, config, convention) {
|
|
|
2848
2989
|
});
|
|
2849
2990
|
}
|
|
2850
2991
|
}
|
|
2851
|
-
if (hasOutboxRegistry(files) &&
|
|
2852
|
-
!(await appHasOutboxDrainEntrypoint(targetDir, files))) {
|
|
2992
|
+
if (hasOutboxRegistry(files, config) &&
|
|
2993
|
+
!(await appHasOutboxDrainEntrypoint(targetDir, files, config))) {
|
|
2853
2994
|
diagnostics.push({
|
|
2854
2995
|
severity: "warning",
|
|
2855
2996
|
code: "BEIGNET_OUTBOX_DRAIN_MISSING",
|
|
@@ -2858,6 +2999,57 @@ async function inspectServerlessFootguns(targetDir, files, config, convention) {
|
|
|
2858
2999
|
}
|
|
2859
3000
|
return diagnostics;
|
|
2860
3001
|
}
|
|
3002
|
+
function configuredRuntimeFiles(config) {
|
|
3003
|
+
return [
|
|
3004
|
+
config.paths.server,
|
|
3005
|
+
config.paths.listeners,
|
|
3006
|
+
config.paths.tasks,
|
|
3007
|
+
config.paths.outbox,
|
|
3008
|
+
config.paths.schedules,
|
|
3009
|
+
config.paths.portWiring,
|
|
3010
|
+
];
|
|
3011
|
+
}
|
|
3012
|
+
function configuredRuntimeDirectories(config) {
|
|
3013
|
+
const applicationDirectories = configuredApplicationDirectories(config);
|
|
3014
|
+
return configuredRuntimeFiles(config)
|
|
3015
|
+
.map((file) => directoryPath(path.posix.dirname(file)))
|
|
3016
|
+
.filter((directory) => directory !== ".")
|
|
3017
|
+
.filter((directory) => !applicationDirectories.some((applicationDirectory) => applicationDirectory !== directory &&
|
|
3018
|
+
fileWithinDirectory(applicationDirectory, directory)))
|
|
3019
|
+
.filter((directory, index, all) => all.indexOf(directory) === index);
|
|
3020
|
+
}
|
|
3021
|
+
function configuredApplicationDirectories(config) {
|
|
3022
|
+
return [
|
|
3023
|
+
config.paths.contracts,
|
|
3024
|
+
config.paths.routes,
|
|
3025
|
+
config.paths.features,
|
|
3026
|
+
config.paths.policies,
|
|
3027
|
+
config.paths.useCases,
|
|
3028
|
+
config.paths.tests,
|
|
3029
|
+
]
|
|
3030
|
+
.map(directoryPath)
|
|
3031
|
+
.filter((directory) => directory !== ".")
|
|
3032
|
+
.filter((directory, index, all) => all.indexOf(directory) === index);
|
|
3033
|
+
}
|
|
3034
|
+
function isRuntimeFile(file, config) {
|
|
3035
|
+
if (configuredRuntimeFiles(config).includes(file))
|
|
3036
|
+
return true;
|
|
3037
|
+
if (configuredApplicationDirectories(config).some((directory) => fileWithinDirectory(file, directory))) {
|
|
3038
|
+
return false;
|
|
3039
|
+
}
|
|
3040
|
+
return (fileWithinDirectory(file, "infra") ||
|
|
3041
|
+
fileWithinDirectory(file, "server") ||
|
|
3042
|
+
configuredRuntimeDirectories(config).some((directory) => fileWithinDirectory(file, directory)));
|
|
3043
|
+
}
|
|
3044
|
+
function fileWithinDirectory(file, directory) {
|
|
3045
|
+
return directory === "." || file.startsWith(`${directory}/`);
|
|
3046
|
+
}
|
|
3047
|
+
function isProviderLifecycleFile(file, source) {
|
|
3048
|
+
return (/(^|\/)provider(s)?\.ts$/.test(file) ||
|
|
3049
|
+
/(^|\/)[^/]*provider[^/]*\.ts$/.test(file) ||
|
|
3050
|
+
/(^|\/)providers?\//.test(file) ||
|
|
3051
|
+
(source !== undefined && containsCallExpression(source, "createProvider")));
|
|
3052
|
+
}
|
|
2861
3053
|
function isNextRouteFile(file, config) {
|
|
2862
3054
|
const routesPath = directoryPath(config.paths.routes);
|
|
2863
3055
|
return (file.startsWith(`${routesPath}/`) &&
|
|
@@ -2878,24 +3070,25 @@ function importsEagerServer(source, importerFile, files, config) {
|
|
|
2878
3070
|
}
|
|
2879
3071
|
return false;
|
|
2880
3072
|
}
|
|
2881
|
-
function hasOutboxRegistry(files) {
|
|
2882
|
-
return files.
|
|
2883
|
-
}
|
|
2884
|
-
function isOutboxRegistryFile(file) {
|
|
2885
|
-
return file === "server/outbox.ts";
|
|
3073
|
+
function hasOutboxRegistry(files, config) {
|
|
3074
|
+
return files.includes(config.paths.outbox);
|
|
2886
3075
|
}
|
|
2887
|
-
async function appHasOutboxDrainEntrypoint(targetDir, files) {
|
|
3076
|
+
async function appHasOutboxDrainEntrypoint(targetDir, files, config) {
|
|
3077
|
+
const routesDirectory = directoryPath(config.paths.routes);
|
|
2888
3078
|
for (const file of files) {
|
|
2889
3079
|
if (!file.endsWith(".ts") || file.endsWith(".test.ts"))
|
|
2890
3080
|
continue;
|
|
2891
|
-
if (!file
|
|
2892
|
-
!file
|
|
2893
|
-
!file.startsWith("infra/")) {
|
|
3081
|
+
if (!fileWithinDirectory(file, routesDirectory) &&
|
|
3082
|
+
!isRuntimeFile(file, config)) {
|
|
2894
3083
|
continue;
|
|
2895
3084
|
}
|
|
2896
3085
|
const source = await readFile(path.join(targetDir, file), "utf8");
|
|
2897
|
-
if (
|
|
2898
|
-
|
|
3086
|
+
if (containsCallExpression(source, "createOutboxDrainRoute")) {
|
|
3087
|
+
return true;
|
|
3088
|
+
}
|
|
3089
|
+
if (isProviderLifecycleFile(file, source))
|
|
3090
|
+
continue;
|
|
3091
|
+
if (containsCallExpression(source, "drainOutbox")) {
|
|
2899
3092
|
return true;
|
|
2900
3093
|
}
|
|
2901
3094
|
}
|
|
@@ -3027,8 +3220,9 @@ async function inspectDatabaseLifecycleDrift(targetDir, files, config, conventio
|
|
|
3027
3220
|
const schemaIndexSource = await readFile(path.join(targetDir, schemaIndexFile), "utf8");
|
|
3028
3221
|
for (const schemaFile of schemaFiles) {
|
|
3029
3222
|
const moduleName = path.basename(schemaFile, ".ts");
|
|
3030
|
-
if (schemaIndexSource
|
|
3223
|
+
if (schemaIndexExportKind(schemaIndexSource, `./${moduleName}`)) {
|
|
3031
3224
|
continue;
|
|
3225
|
+
}
|
|
3032
3226
|
diagnostics.push({
|
|
3033
3227
|
severity: "warning",
|
|
3034
3228
|
code: "BEIGNET_DB_SCHEMA_EXPORT_MISSING",
|
|
@@ -4219,6 +4413,333 @@ function findRouteGroupContract(contracts, routeGroupContract) {
|
|
|
4219
4413
|
contract.file ===
|
|
4220
4414
|
routeGroupContract.file.replace(/\.ts$/, "/index.ts")));
|
|
4221
4415
|
}
|
|
4416
|
+
const managedDatabaseSchemaMarker = "// Managed by beignet db schema sync.";
|
|
4417
|
+
const databaseSchemaExports = {
|
|
4418
|
+
audit: "auditLog",
|
|
4419
|
+
idempotency: "idempotencyRecords",
|
|
4420
|
+
outbox: "outboxMessages",
|
|
4421
|
+
};
|
|
4422
|
+
async function planMissingProviderTables(targetDir, files, config) {
|
|
4423
|
+
const infrastructurePath = directoryPath(path.dirname(config.paths.portWiring));
|
|
4424
|
+
const schemaDir = `${infrastructurePath}/db/schema`;
|
|
4425
|
+
const output = `${schemaDir}/beignet.ts`;
|
|
4426
|
+
const indexFile = `${schemaDir}/index.ts`;
|
|
4427
|
+
if (!files.includes(indexFile))
|
|
4428
|
+
return undefined;
|
|
4429
|
+
const sourceCache = new Map();
|
|
4430
|
+
const usageFiles = durableDrizzleUsageFiles(files, config);
|
|
4431
|
+
const requiredDefaultTables = [];
|
|
4432
|
+
const missingTables = [];
|
|
4433
|
+
const fixes = [];
|
|
4434
|
+
for (const requirement of durableDrizzleTableRequirements) {
|
|
4435
|
+
const defaultTableName = defaultBeignetConfig.database.tables[requirement.table];
|
|
4436
|
+
if (config.database.tables[requirement.table] !== defaultTableName) {
|
|
4437
|
+
continue;
|
|
4438
|
+
}
|
|
4439
|
+
const configuredTables = await configuredDurableDrizzleTableNames(targetDir, usageFiles, config, requirement, sourceCache);
|
|
4440
|
+
if (!configuredTables.has(defaultTableName))
|
|
4441
|
+
continue;
|
|
4442
|
+
requiredDefaultTables.push(requirement.table);
|
|
4443
|
+
if (await durableDrizzleTableExists(targetDir, files, config, infrastructurePath, schemaDir, requirement, defaultTableName, sourceCache)) {
|
|
4444
|
+
continue;
|
|
4445
|
+
}
|
|
4446
|
+
missingTables.push(requirement.table);
|
|
4447
|
+
fixes.push({
|
|
4448
|
+
code: requirement.code,
|
|
4449
|
+
file: output,
|
|
4450
|
+
message: `Added the default ${defaultTableName} provider table export to ${output}.`,
|
|
4451
|
+
});
|
|
4452
|
+
}
|
|
4453
|
+
const outputPath = path.join(targetDir, output);
|
|
4454
|
+
let originalOutput;
|
|
4455
|
+
try {
|
|
4456
|
+
originalOutput = await readFile(outputPath, "utf8");
|
|
4457
|
+
}
|
|
4458
|
+
catch (error) {
|
|
4459
|
+
if (error.code !== "ENOENT")
|
|
4460
|
+
throw error;
|
|
4461
|
+
}
|
|
4462
|
+
const existingTables = originalOutput === undefined
|
|
4463
|
+
? []
|
|
4464
|
+
: managedDatabaseSchemaTables(originalOutput);
|
|
4465
|
+
if (existingTables === undefined)
|
|
4466
|
+
return undefined;
|
|
4467
|
+
const originalIndex = await readFile(path.join(targetDir, indexFile), "utf8");
|
|
4468
|
+
const managedExportKind = schemaIndexExportKind(originalIndex, "./beignet");
|
|
4469
|
+
if (managedExportKind === "named")
|
|
4470
|
+
return undefined;
|
|
4471
|
+
const needsManagedExport = originalOutput !== undefined &&
|
|
4472
|
+
existingTables.some((table) => requiredDefaultTables.includes(table)) &&
|
|
4473
|
+
managedExportKind === undefined;
|
|
4474
|
+
if (missingTables.length === 0 && !needsManagedExport)
|
|
4475
|
+
return undefined;
|
|
4476
|
+
if (needsManagedExport) {
|
|
4477
|
+
fixes.push({
|
|
4478
|
+
code: "BEIGNET_DB_SCHEMA_EXPORT_MISSING",
|
|
4479
|
+
file: indexFile,
|
|
4480
|
+
message: `Exported ${output} from ${indexFile}.`,
|
|
4481
|
+
});
|
|
4482
|
+
}
|
|
4483
|
+
const selectedTables = Object.keys(databaseSchemaExports).filter((table) => existingTables.includes(table) || missingTables.includes(table));
|
|
4484
|
+
let nextOutput = originalOutput;
|
|
4485
|
+
if (missingTables.length > 0) {
|
|
4486
|
+
let dialect;
|
|
4487
|
+
try {
|
|
4488
|
+
dialect = await resolveDatabaseSchemaDialect(targetDir, undefined);
|
|
4489
|
+
}
|
|
4490
|
+
catch {
|
|
4491
|
+
return undefined;
|
|
4492
|
+
}
|
|
4493
|
+
nextOutput = renderDatabaseSchema(dialect, selectedTables);
|
|
4494
|
+
}
|
|
4495
|
+
if (nextOutput === undefined)
|
|
4496
|
+
return undefined;
|
|
4497
|
+
const addedExportNames = missingTables.map((table) => databaseSchemaExports[table]);
|
|
4498
|
+
const otherSchemaFiles = files.filter((file) => file.startsWith(`${schemaDir}/`) &&
|
|
4499
|
+
file.endsWith(".ts") &&
|
|
4500
|
+
file !== indexFile &&
|
|
4501
|
+
file !== output);
|
|
4502
|
+
for (const schemaFile of [indexFile, ...otherSchemaFiles]) {
|
|
4503
|
+
const source = schemaFile === indexFile
|
|
4504
|
+
? originalIndex
|
|
4505
|
+
: await readFile(path.join(targetDir, schemaFile), "utf8");
|
|
4506
|
+
if (addedExportNames.some((name) => exportsIdentifier(source, name))) {
|
|
4507
|
+
return undefined;
|
|
4508
|
+
}
|
|
4509
|
+
}
|
|
4510
|
+
const nextIndex = managedExportKind === "all"
|
|
4511
|
+
? originalIndex
|
|
4512
|
+
: `${originalIndex.trimEnd()}\nexport * from "./beignet";\n`;
|
|
4513
|
+
const changes = [
|
|
4514
|
+
...(originalOutput === nextOutput
|
|
4515
|
+
? []
|
|
4516
|
+
: [
|
|
4517
|
+
planDoctorFixFileChange({
|
|
4518
|
+
file: output,
|
|
4519
|
+
before: originalOutput,
|
|
4520
|
+
after: nextOutput,
|
|
4521
|
+
}),
|
|
4522
|
+
]),
|
|
4523
|
+
...(originalIndex === nextIndex
|
|
4524
|
+
? []
|
|
4525
|
+
: [
|
|
4526
|
+
planDoctorFixFileChange({
|
|
4527
|
+
file: indexFile,
|
|
4528
|
+
before: originalIndex,
|
|
4529
|
+
after: nextIndex,
|
|
4530
|
+
}),
|
|
4531
|
+
]),
|
|
4532
|
+
];
|
|
4533
|
+
if (changes.length === 0)
|
|
4534
|
+
return undefined;
|
|
4535
|
+
return {
|
|
4536
|
+
id: "database.sync-provider-tables",
|
|
4537
|
+
fixes,
|
|
4538
|
+
changes,
|
|
4539
|
+
};
|
|
4540
|
+
}
|
|
4541
|
+
function schemaIndexExportKind(source, moduleSpecifier) {
|
|
4542
|
+
const codeIndexes = new Set(codeCharacterIndexes(source));
|
|
4543
|
+
const escaped = escapeRegExp(moduleSpecifier);
|
|
4544
|
+
const allPattern = new RegExp(`\\bexport\\s*\\*\\s*from\\s*["']${escaped}["']`, "g");
|
|
4545
|
+
if ([...source.matchAll(allPattern)].some((match) => match.index !== undefined && codeIndexes.has(match.index))) {
|
|
4546
|
+
return "all";
|
|
4547
|
+
}
|
|
4548
|
+
const customPatterns = [
|
|
4549
|
+
new RegExp(`\\bexport\\s*\\{[\\s\\S]*?\\}\\s*from\\s*["']${escaped}["']`, "g"),
|
|
4550
|
+
new RegExp(`\\bexport\\s*\\*\\s+as\\s+[A-Za-z_$][\\w$]*\\s+from\\s*["']${escaped}["']`, "g"),
|
|
4551
|
+
];
|
|
4552
|
+
return customPatterns.some((pattern) => [...source.matchAll(pattern)].some((match) => match.index !== undefined && codeIndexes.has(match.index)))
|
|
4553
|
+
? "named"
|
|
4554
|
+
: undefined;
|
|
4555
|
+
}
|
|
4556
|
+
function exportsIdentifier(source, identifier) {
|
|
4557
|
+
const executableSource = Array.from({ length: source.length }, () => " ");
|
|
4558
|
+
for (const index of codeCharacterIndexes(source)) {
|
|
4559
|
+
executableSource[index] = source.charAt(index);
|
|
4560
|
+
}
|
|
4561
|
+
const escaped = escapeRegExp(identifier);
|
|
4562
|
+
const patterns = [
|
|
4563
|
+
new RegExp(`\\bexport\\s+(?:const|let|var|class|function)\\s+${escaped}\\b`),
|
|
4564
|
+
new RegExp(`\\bexport\\s*\\{[^}]*\\b${escaped}\\b[^}]*\\}`),
|
|
4565
|
+
];
|
|
4566
|
+
const code = executableSource.join("");
|
|
4567
|
+
return patterns.some((pattern) => pattern.test(code));
|
|
4568
|
+
}
|
|
4569
|
+
function managedDatabaseSchemaTables(source) {
|
|
4570
|
+
if (!source.startsWith(managedDatabaseSchemaMarker))
|
|
4571
|
+
return undefined;
|
|
4572
|
+
const dialect = /@beignet\/provider-db-drizzle\/(sqlite|postgres|mysql)\/schema/.exec(source)?.[1];
|
|
4573
|
+
if (!dialect)
|
|
4574
|
+
return undefined;
|
|
4575
|
+
const tables = Object.entries(databaseSchemaExports)
|
|
4576
|
+
.filter(([, exportName]) => new RegExp(`\\b${escapeRegExp(exportName)}\\b`).test(source))
|
|
4577
|
+
.map(([table]) => table);
|
|
4578
|
+
if (tables.length === 0)
|
|
4579
|
+
return undefined;
|
|
4580
|
+
return renderDatabaseSchema(dialect, tables) === source ? tables : undefined;
|
|
4581
|
+
}
|
|
4582
|
+
async function planUnregisteredInngestJobs(targetDir, files, config) {
|
|
4583
|
+
const packageJson = await readPackageJson(targetDir, files);
|
|
4584
|
+
if (!installedPackageNames(packageJson).has("@beignet/provider-jobs-inngest")) {
|
|
4585
|
+
return undefined;
|
|
4586
|
+
}
|
|
4587
|
+
const serverDir = directoryPath(path.dirname(config.paths.server));
|
|
4588
|
+
const registryFile = `${serverDir}/inngest.ts`;
|
|
4589
|
+
if (!files.includes(registryFile))
|
|
4590
|
+
return undefined;
|
|
4591
|
+
const source = await readFile(path.join(targetDir, registryFile), "utf8");
|
|
4592
|
+
const registry = arrayInitializerInfo(source, "inngestJobs");
|
|
4593
|
+
if (!registry)
|
|
4594
|
+
return undefined;
|
|
4595
|
+
const featureJobs = (await readFeatureWorkflowRegistries(targetDir, files, config)).filter((entry) => entry.kind === "jobs");
|
|
4596
|
+
const unregistered = unregisteredWorkflowRegistries(featureJobs, canonicalRegisteredWorkflowIdentifiers({
|
|
4597
|
+
source,
|
|
4598
|
+
registered: identifiersFromArrayExpression(registry.text),
|
|
4599
|
+
centralFile: registryFile,
|
|
4600
|
+
files,
|
|
4601
|
+
registries: featureJobs,
|
|
4602
|
+
}));
|
|
4603
|
+
return planWorkflowRegistryOperation("inngest.register-missing", {
|
|
4604
|
+
targetDir,
|
|
4605
|
+
files,
|
|
4606
|
+
centralFile: registryFile,
|
|
4607
|
+
unregistered,
|
|
4608
|
+
code: "BEIGNET_INNGEST_JOB_REGISTRATION_MISSING",
|
|
4609
|
+
listName: "the Inngest jobs array",
|
|
4610
|
+
importSpecifier: (indexFile) => relativeModule(registryFile, indexFile),
|
|
4611
|
+
append: (nextSource, entry, importLine) => appendToNamedArray(nextSource, "inngestJobs", entry, importLine),
|
|
4612
|
+
});
|
|
4613
|
+
}
|
|
4614
|
+
const runtimeManifestRepairChecks = [
|
|
4615
|
+
{
|
|
4616
|
+
kind: "listeners",
|
|
4617
|
+
code: "BEIGNET_RUNTIME_MANIFEST_LISTENER_MISSING",
|
|
4618
|
+
listName: "the runtime manifest listeners array",
|
|
4619
|
+
},
|
|
4620
|
+
{
|
|
4621
|
+
kind: "schedules",
|
|
4622
|
+
code: "BEIGNET_RUNTIME_MANIFEST_SCHEDULE_MISSING",
|
|
4623
|
+
listName: "the runtime manifest schedules array",
|
|
4624
|
+
},
|
|
4625
|
+
{
|
|
4626
|
+
kind: "tasks",
|
|
4627
|
+
code: "BEIGNET_RUNTIME_MANIFEST_TASK_MISSING",
|
|
4628
|
+
listName: "the runtime manifest tasks array",
|
|
4629
|
+
},
|
|
4630
|
+
{
|
|
4631
|
+
kind: "events",
|
|
4632
|
+
code: "BEIGNET_RUNTIME_MANIFEST_OUTBOX_EVENT_MISSING",
|
|
4633
|
+
listName: "the runtime manifest outbox events array",
|
|
4634
|
+
},
|
|
4635
|
+
{
|
|
4636
|
+
kind: "jobs",
|
|
4637
|
+
code: "BEIGNET_RUNTIME_MANIFEST_OUTBOX_JOB_MISSING",
|
|
4638
|
+
listName: "the runtime manifest outbox jobs array",
|
|
4639
|
+
},
|
|
4640
|
+
];
|
|
4641
|
+
async function planRuntimeManifestRegistries(targetDir, files, config, convention, sourceOverrides = new Map()) {
|
|
4642
|
+
if (!convention.resourceGenerator)
|
|
4643
|
+
return undefined;
|
|
4644
|
+
const manifest = await readRuntimeManifestIdentifiers(targetDir, files);
|
|
4645
|
+
if (!manifest)
|
|
4646
|
+
return undefined;
|
|
4647
|
+
const original = sourceOverrides.get(manifest.file) ??
|
|
4648
|
+
(await readFile(path.join(targetDir, manifest.file), "utf8"));
|
|
4649
|
+
const registries = await readFeatureWorkflowRegistries(targetDir, files, config);
|
|
4650
|
+
let next = original;
|
|
4651
|
+
const fixes = [];
|
|
4652
|
+
for (const check of runtimeManifestRepairChecks) {
|
|
4653
|
+
const parsedRegistered = runtimeManifestIdentifiers(next)?.[runtimeManifestIdentifierKey(check.kind)];
|
|
4654
|
+
if (!parsedRegistered)
|
|
4655
|
+
continue;
|
|
4656
|
+
const kindRegistries = registries.filter((registry) => registry.kind === check.kind);
|
|
4657
|
+
const registered = canonicalRegisteredWorkflowIdentifiers({
|
|
4658
|
+
source: next,
|
|
4659
|
+
registered: parsedRegistered,
|
|
4660
|
+
centralFile: manifest.file,
|
|
4661
|
+
files,
|
|
4662
|
+
registries: kindRegistries,
|
|
4663
|
+
});
|
|
4664
|
+
const edit = planWorkflowRegistryEdit({
|
|
4665
|
+
files,
|
|
4666
|
+
centralFile: manifest.file,
|
|
4667
|
+
source: next,
|
|
4668
|
+
unregistered: unregisteredWorkflowRegistries(kindRegistries, registered),
|
|
4669
|
+
code: check.code,
|
|
4670
|
+
listName: check.listName,
|
|
4671
|
+
importSpecifier: (indexFile) => relativeModule(manifest.file, indexFile),
|
|
4672
|
+
append: (nextSource, entry, importLine) => appendToRuntimeManifestArray(nextSource, check.kind, entry, importLine),
|
|
4673
|
+
});
|
|
4674
|
+
if (!edit)
|
|
4675
|
+
continue;
|
|
4676
|
+
next = edit.after;
|
|
4677
|
+
fixes.push(edit.fix);
|
|
4678
|
+
}
|
|
4679
|
+
if (fixes.length === 0 || next === original)
|
|
4680
|
+
return undefined;
|
|
4681
|
+
return {
|
|
4682
|
+
id: "runtime-manifest.register-missing",
|
|
4683
|
+
fixes,
|
|
4684
|
+
changes: [
|
|
4685
|
+
planDoctorFixFileChange({
|
|
4686
|
+
file: manifest.file,
|
|
4687
|
+
before: original,
|
|
4688
|
+
after: next,
|
|
4689
|
+
}),
|
|
4690
|
+
],
|
|
4691
|
+
};
|
|
4692
|
+
}
|
|
4693
|
+
function runtimeManifestIdentifierKey(kind) {
|
|
4694
|
+
return kind;
|
|
4695
|
+
}
|
|
4696
|
+
function runtimeManifestArrayInfo(source, kind) {
|
|
4697
|
+
const calls = runtimeManifestCallMatches(source);
|
|
4698
|
+
if (calls.length !== 1)
|
|
4699
|
+
return undefined;
|
|
4700
|
+
const call = calls[0];
|
|
4701
|
+
const arg = firstCallArgInfo(source, (call.index ?? 0) + call[0].length);
|
|
4702
|
+
if (!arg)
|
|
4703
|
+
return undefined;
|
|
4704
|
+
if (kind === "listeners" || kind === "schedules" || kind === "tasks") {
|
|
4705
|
+
const value = objectPropertyValueInfo(arg.text, kind);
|
|
4706
|
+
return value
|
|
4707
|
+
? {
|
|
4708
|
+
text: value.text,
|
|
4709
|
+
start: arg.start + value.start,
|
|
4710
|
+
end: arg.start + value.end,
|
|
4711
|
+
}
|
|
4712
|
+
: undefined;
|
|
4713
|
+
}
|
|
4714
|
+
const outbox = objectPropertyValueInfo(arg.text, "outbox");
|
|
4715
|
+
if (!outbox)
|
|
4716
|
+
return undefined;
|
|
4717
|
+
const value = objectPropertyValueInfo(outbox.text, kind);
|
|
4718
|
+
return value
|
|
4719
|
+
? {
|
|
4720
|
+
text: value.text,
|
|
4721
|
+
start: arg.start + outbox.start + value.start,
|
|
4722
|
+
end: arg.start + outbox.start + value.end,
|
|
4723
|
+
}
|
|
4724
|
+
: undefined;
|
|
4725
|
+
}
|
|
4726
|
+
function appendToRuntimeManifestArray(source, kind, entry, importLine) {
|
|
4727
|
+
const value = runtimeManifestArrayInfo(source, kind);
|
|
4728
|
+
if (!value?.text.startsWith("["))
|
|
4729
|
+
return { kind: "missing" };
|
|
4730
|
+
const entryName = entry.replace(/^\.{3}/, "").trim();
|
|
4731
|
+
if (identifiersFromArrayExpression(value.text).has(entryName)) {
|
|
4732
|
+
return { kind: "unchanged" };
|
|
4733
|
+
}
|
|
4734
|
+
const nextArray = appendToArrayExpression(value.text, [entry]);
|
|
4735
|
+
let next = `${source.slice(0, value.start)}${nextArray}${source.slice(value.end)}`;
|
|
4736
|
+
if (importLine && !next.includes(importLine)) {
|
|
4737
|
+
next = insertAfterImports(next, importLine);
|
|
4738
|
+
}
|
|
4739
|
+
return next === source
|
|
4740
|
+
? { kind: "unchanged" }
|
|
4741
|
+
: { kind: "updated", source: next };
|
|
4742
|
+
}
|
|
4222
4743
|
async function planDirectOpenApiArrayDrift(targetDir, files, config, convention, contracts, matchedRoutes) {
|
|
4223
4744
|
const routePath = path.join(targetDir, config.paths.openapiRoute);
|
|
4224
4745
|
let source;
|
|
@@ -4622,7 +5143,8 @@ function planWorkflowRegistryEdit(options) {
|
|
|
4622
5143
|
let next = options.source;
|
|
4623
5144
|
const registeredNames = [];
|
|
4624
5145
|
for (const { registry } of candidates) {
|
|
4625
|
-
const
|
|
5146
|
+
const imports = parseNamedImportSources(next);
|
|
5147
|
+
const imported = imports.get(registry.registryName);
|
|
4626
5148
|
let importLine;
|
|
4627
5149
|
if (imported) {
|
|
4628
5150
|
const importedFile = sourceFileFromImport(imported.sourcePath, options.centralFile, options.files);
|
|
@@ -4630,6 +5152,14 @@ function planWorkflowRegistryEdit(options) {
|
|
|
4630
5152
|
return undefined;
|
|
4631
5153
|
}
|
|
4632
5154
|
else {
|
|
5155
|
+
const aliasesRegistry = [...imports.entries()].some(([localName, candidate]) => localName !== registry.registryName &&
|
|
5156
|
+
candidate.importedName === registry.registryName &&
|
|
5157
|
+
sourceFileFromImport(candidate.sourcePath, options.centralFile, options.files) === registry.indexFile);
|
|
5158
|
+
if (aliasesRegistry)
|
|
5159
|
+
return undefined;
|
|
5160
|
+
if (sourceContainsCodeIdentifier(next, registry.registryName)) {
|
|
5161
|
+
return undefined;
|
|
5162
|
+
}
|
|
4633
5163
|
importLine = `import { ${registry.registryName} } from "${options.importSpecifier(registry.indexFile)}";`;
|
|
4634
5164
|
}
|
|
4635
5165
|
const result = options.append(next, `...${registry.registryName}`, importLine);
|
|
@@ -4651,6 +5181,12 @@ function planWorkflowRegistryEdit(options) {
|
|
|
4651
5181
|
},
|
|
4652
5182
|
};
|
|
4653
5183
|
}
|
|
5184
|
+
function sourceContainsCodeIdentifier(source, identifier) {
|
|
5185
|
+
const codeIndexes = new Set(codeCharacterIndexes(source));
|
|
5186
|
+
const pattern = new RegExp(`(^|[^A-Za-z0-9_$])${escapeRegExp(identifier)}(?![A-Za-z0-9_$])`, "g");
|
|
5187
|
+
return [...source.matchAll(pattern)].some((match) => match.index !== undefined &&
|
|
5188
|
+
codeIndexes.has(match.index + (match[1]?.length ?? 0)));
|
|
5189
|
+
}
|
|
4654
5190
|
function routeRegistryFileFromServerSource(source, serverFile, files) {
|
|
4655
5191
|
const imports = parseNamedImportSources(source);
|
|
4656
5192
|
for (const identifier of routeOptionIdentifiers(source)) {
|