@beignet/cli 0.0.7 → 0.0.9
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 +21 -0
- package/README.md +90 -68
- package/dist/create.js +1 -1
- package/dist/create.js.map +1 -1
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +482 -56
- package/dist/inspect.js.map +1 -1
- package/dist/make.d.ts.map +1 -1
- package/dist/make.js +269 -98
- package/dist/make.js.map +1 -1
- package/dist/registry-edits.d.ts +77 -0
- package/dist/registry-edits.d.ts.map +1 -0
- package/dist/registry-edits.js +255 -0
- package/dist/registry-edits.js.map +1 -0
- package/dist/templates/base.js +4 -4
- package/dist/templates/base.js.map +1 -1
- package/dist/templates/db.js +16 -16
- package/dist/templates/server.js +10 -10
- package/dist/templates/server.js.map +1 -1
- package/package.json +2 -2
- package/src/create.ts +1 -1
- package/src/inspect.ts +785 -77
- package/src/make.ts +435 -123
- package/src/registry-edits.ts +352 -0
- package/src/templates/base.ts +4 -4
- package/src/templates/db.ts +16 -16
- package/src/templates/server.ts +10 -10
package/src/inspect.ts
CHANGED
|
@@ -18,6 +18,17 @@ import {
|
|
|
18
18
|
formatGithubAnnotation,
|
|
19
19
|
type GithubAnnotationSeverity,
|
|
20
20
|
} from "./github-annotations.js";
|
|
21
|
+
import {
|
|
22
|
+
type AppendResult,
|
|
23
|
+
appendToArrayExpression,
|
|
24
|
+
appendToNamedArray,
|
|
25
|
+
appendToOutboxRegistryArray,
|
|
26
|
+
arrayInitializerInfo,
|
|
27
|
+
identifiersFromArrayExpression,
|
|
28
|
+
insertAfterImports,
|
|
29
|
+
matchingDelimiterIndex,
|
|
30
|
+
outboxRegistryValueInfo,
|
|
31
|
+
} from "./registry-edits.js";
|
|
21
32
|
import { getCliVersion } from "./version.js";
|
|
22
33
|
|
|
23
34
|
type InspectAppOptions = {
|
|
@@ -225,6 +236,23 @@ export async function applyDoctorFixes(
|
|
|
225
236
|
);
|
|
226
237
|
if (routeGroupFix) fixes.push(routeGroupFix);
|
|
227
238
|
|
|
239
|
+
const drift = await workflowRegistrationDrift(targetDir, files, config);
|
|
240
|
+
|
|
241
|
+
const scheduleFix = await fixUnregisteredSchedules(
|
|
242
|
+
targetDir,
|
|
243
|
+
files,
|
|
244
|
+
drift,
|
|
245
|
+
config,
|
|
246
|
+
);
|
|
247
|
+
if (scheduleFix) fixes.push(scheduleFix);
|
|
248
|
+
|
|
249
|
+
const taskFix = await fixUnregisteredTasks(targetDir, files, drift, config);
|
|
250
|
+
if (taskFix) fixes.push(taskFix);
|
|
251
|
+
|
|
252
|
+
fixes.push(
|
|
253
|
+
...(await fixUnregisteredOutboxEntries(targetDir, files, drift, config)),
|
|
254
|
+
);
|
|
255
|
+
|
|
228
256
|
const openApiFix = await fixDirectOpenApiArrayDrift(
|
|
229
257
|
targetDir,
|
|
230
258
|
files,
|
|
@@ -1300,6 +1328,12 @@ async function inspectDiagnostics(
|
|
|
1300
1328
|
config,
|
|
1301
1329
|
convention,
|
|
1302
1330
|
)),
|
|
1331
|
+
...(await inspectWorkflowRegistrationDrift(
|
|
1332
|
+
targetDir,
|
|
1333
|
+
files,
|
|
1334
|
+
config,
|
|
1335
|
+
convention,
|
|
1336
|
+
)),
|
|
1303
1337
|
...(await inspectResourceSlices(
|
|
1304
1338
|
targetDir,
|
|
1305
1339
|
files,
|
|
@@ -1831,6 +1865,14 @@ async function inspectRouteErrorCatalogDrift(
|
|
|
1831
1865
|
return diagnostics;
|
|
1832
1866
|
}
|
|
1833
1867
|
|
|
1868
|
+
type ProviderDoctorVariantRule = {
|
|
1869
|
+
name: string;
|
|
1870
|
+
displayName: string;
|
|
1871
|
+
tokens: readonly string[];
|
|
1872
|
+
requiredEnv: readonly string[];
|
|
1873
|
+
registrationSeverity?: "warning" | "hint";
|
|
1874
|
+
};
|
|
1875
|
+
|
|
1834
1876
|
type ProviderDoctorRule = {
|
|
1835
1877
|
packageName: string;
|
|
1836
1878
|
displayName: string;
|
|
@@ -1838,6 +1880,7 @@ type ProviderDoctorRule = {
|
|
|
1838
1880
|
appPorts?: readonly { name: string; type: string }[];
|
|
1839
1881
|
requiredEnv?: readonly string[];
|
|
1840
1882
|
registrationSeverity?: "warning" | "hint";
|
|
1883
|
+
variants?: readonly ProviderDoctorVariantRule[];
|
|
1841
1884
|
};
|
|
1842
1885
|
|
|
1843
1886
|
type ProviderPackageMetadataSource = {
|
|
@@ -1926,8 +1969,40 @@ async function inspectProductionReadiness(
|
|
|
1926
1969
|
}
|
|
1927
1970
|
}
|
|
1928
1971
|
|
|
1972
|
+
const providerEntries = await readProviderListEntries(
|
|
1973
|
+
targetDir,
|
|
1974
|
+
files,
|
|
1975
|
+
config,
|
|
1976
|
+
sourceCache,
|
|
1977
|
+
);
|
|
1978
|
+
|
|
1929
1979
|
for (const provider of providerDoctorRules) {
|
|
1930
1980
|
if (!installedPackages.has(provider.packageName)) continue;
|
|
1981
|
+
|
|
1982
|
+
if (provider.variants !== undefined) {
|
|
1983
|
+
// Multi-variant packages only require the env vars of the variants the
|
|
1984
|
+
// app actually registers; an unused variant must not warn.
|
|
1985
|
+
for (const variant of detectedProviderVariants(
|
|
1986
|
+
provider.variants,
|
|
1987
|
+
providerEntries,
|
|
1988
|
+
)) {
|
|
1989
|
+
for (const envVar of variant.requiredEnv) {
|
|
1990
|
+
if (
|
|
1991
|
+
await anyFileContains(targetDir, configFiles, envVar, sourceCache)
|
|
1992
|
+
) {
|
|
1993
|
+
continue;
|
|
1994
|
+
}
|
|
1995
|
+
diagnostics.push({
|
|
1996
|
+
severity: "warning",
|
|
1997
|
+
code: "BEIGNET_PROVIDER_ENV_MISSING",
|
|
1998
|
+
file: "package.json",
|
|
1999
|
+
message: `${provider.packageName} is installed and ${variant.displayName} is registered, but ${envVar} is not mentioned in app env/config files. ${variant.displayName} requires ${envVar}; add the environment variable or remove the provider registration.`,
|
|
2000
|
+
});
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
continue;
|
|
2004
|
+
}
|
|
2005
|
+
|
|
1931
2006
|
for (const envVar of provider.requiredEnv ?? []) {
|
|
1932
2007
|
if (await anyFileContains(targetDir, configFiles, envVar, sourceCache)) {
|
|
1933
2008
|
continue;
|
|
@@ -2119,23 +2194,38 @@ async function inspectProviderRegistrationDrift(
|
|
|
2119
2194
|
);
|
|
2120
2195
|
const providerFiles = serverProviderFiles(files, config);
|
|
2121
2196
|
const sourceCache = new Map<string, string>();
|
|
2122
|
-
const
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
).join("\n");
|
|
2129
|
-
const providerListSource = extractProviderListSource(providerSource);
|
|
2130
|
-
const providerEntries =
|
|
2131
|
-
providerListSource === undefined
|
|
2132
|
-
? []
|
|
2133
|
-
: providerListEntries(providerListSource);
|
|
2197
|
+
const providerEntries = await readProviderListEntries(
|
|
2198
|
+
targetDir,
|
|
2199
|
+
files,
|
|
2200
|
+
config,
|
|
2201
|
+
sourceCache,
|
|
2202
|
+
);
|
|
2134
2203
|
|
|
2135
2204
|
for (const provider of providerDoctorRules) {
|
|
2205
|
+
if (!installedPackages.has(provider.packageName)) continue;
|
|
2206
|
+
|
|
2207
|
+
if (provider.variants !== undefined) {
|
|
2208
|
+
if (
|
|
2209
|
+
detectedProviderVariants(provider.variants, providerEntries).length > 0
|
|
2210
|
+
) {
|
|
2211
|
+
continue;
|
|
2212
|
+
}
|
|
2213
|
+
const severity = providerVariantRegistrationSeverity(provider.variants);
|
|
2214
|
+
if (severity === undefined) continue;
|
|
2215
|
+
const hints = provider.variants
|
|
2216
|
+
.filter((variant) => variant.tokens.length > 0)
|
|
2217
|
+
.map((variant) => registrationTokenHint(variant.tokens));
|
|
2218
|
+
diagnostics.push({
|
|
2219
|
+
severity,
|
|
2220
|
+
code: "BEIGNET_PROVIDER_REGISTRATION_MISSING",
|
|
2221
|
+
file: providerFiles[0] ?? config.paths.server,
|
|
2222
|
+
message: `${provider.packageName} is installed, but no ${provider.displayName} variant is registered in server/providers.ts. Register one of the available providers (${hints.join(", ")}) in the exported providers array or remove the unused package.`,
|
|
2223
|
+
});
|
|
2224
|
+
continue;
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2136
2227
|
const severity = provider.registrationSeverity;
|
|
2137
2228
|
if (severity === undefined) continue;
|
|
2138
|
-
if (!installedPackages.has(provider.packageName)) continue;
|
|
2139
2229
|
if (
|
|
2140
2230
|
provider.tokens.some((token) =>
|
|
2141
2231
|
providerEntries.some((entry) => entry.includes(token)),
|
|
@@ -2156,8 +2246,12 @@ async function inspectProviderRegistrationDrift(
|
|
|
2156
2246
|
}
|
|
2157
2247
|
|
|
2158
2248
|
const drizzleIndex = findProviderEntryIndex(providerEntries, [
|
|
2159
|
-
"
|
|
2160
|
-
"
|
|
2249
|
+
"drizzleSqliteProvider",
|
|
2250
|
+
"createDrizzleSqliteProvider",
|
|
2251
|
+
"drizzlePostgresProvider",
|
|
2252
|
+
"createDrizzlePostgresProvider",
|
|
2253
|
+
"drizzleMysqlProvider",
|
|
2254
|
+
"createDrizzleMysqlProvider",
|
|
2161
2255
|
]);
|
|
2162
2256
|
const appDatabaseProviderIndex = findProviderEntryIndex(providerEntries, [
|
|
2163
2257
|
"starterDatabaseProvider",
|
|
@@ -2175,7 +2269,7 @@ async function inspectProviderRegistrationDrift(
|
|
|
2175
2269
|
code: "BEIGNET_PROVIDER_ORDER",
|
|
2176
2270
|
file: providerFiles[0] ?? config.paths.server,
|
|
2177
2271
|
message:
|
|
2178
|
-
"An app database provider is registered before
|
|
2272
|
+
"An app database provider is registered before the Drizzle database provider. Register the Drizzle database provider first so app-owned database wiring can read ctx.ports.db during setup.",
|
|
2179
2273
|
});
|
|
2180
2274
|
}
|
|
2181
2275
|
|
|
@@ -2327,12 +2421,70 @@ function providerDoctorRuleFromMetadata(
|
|
|
2327
2421
|
tokens: registration?.tokens ?? [],
|
|
2328
2422
|
appPorts: metadata.appPorts ?? [],
|
|
2329
2423
|
requiredEnv: metadata.requiredEnv ?? [],
|
|
2330
|
-
registrationSeverity:
|
|
2331
|
-
|
|
2332
|
-
|
|
2424
|
+
registrationSeverity: registrationSeverityFromMetadata(registration),
|
|
2425
|
+
variants: metadata.variants?.map((variant) => ({
|
|
2426
|
+
name: variant.name,
|
|
2427
|
+
displayName: variant.displayName ?? variant.name,
|
|
2428
|
+
tokens: variant.registration?.tokens ?? [],
|
|
2429
|
+
requiredEnv: variant.requiredEnv ?? [],
|
|
2430
|
+
registrationSeverity: registrationSeverityFromMetadata(
|
|
2431
|
+
variant.registration,
|
|
2432
|
+
),
|
|
2433
|
+
})),
|
|
2333
2434
|
};
|
|
2334
2435
|
}
|
|
2335
2436
|
|
|
2437
|
+
function registrationSeverityFromMetadata(
|
|
2438
|
+
registration:
|
|
2439
|
+
| { required?: boolean; severity?: "warning" | "hint" }
|
|
2440
|
+
| undefined,
|
|
2441
|
+
): "warning" | "hint" | undefined {
|
|
2442
|
+
return (
|
|
2443
|
+
registration?.severity ??
|
|
2444
|
+
(registration?.required === true ? "warning" : undefined)
|
|
2445
|
+
);
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
function detectedProviderVariants(
|
|
2449
|
+
variants: readonly ProviderDoctorVariantRule[],
|
|
2450
|
+
providerEntries: readonly string[],
|
|
2451
|
+
): ProviderDoctorVariantRule[] {
|
|
2452
|
+
return variants.filter((variant) =>
|
|
2453
|
+
variant.tokens.some((token) =>
|
|
2454
|
+
providerEntries.some((entry) => entry.includes(token)),
|
|
2455
|
+
),
|
|
2456
|
+
);
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
function providerVariantRegistrationSeverity(
|
|
2460
|
+
variants: readonly ProviderDoctorVariantRule[],
|
|
2461
|
+
): "warning" | "hint" | undefined {
|
|
2462
|
+
const severities = variants.map((variant) => variant.registrationSeverity);
|
|
2463
|
+
if (severities.includes("warning")) return "warning";
|
|
2464
|
+
if (severities.includes("hint")) return "hint";
|
|
2465
|
+
return undefined;
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
async function readProviderListEntries(
|
|
2469
|
+
targetDir: string,
|
|
2470
|
+
files: string[],
|
|
2471
|
+
config: ResolvedBeignetConfig,
|
|
2472
|
+
sourceCache: Map<string, string>,
|
|
2473
|
+
): Promise<string[]> {
|
|
2474
|
+
const providerFiles = serverProviderFiles(files, config);
|
|
2475
|
+
const providerSource = (
|
|
2476
|
+
await Promise.all(
|
|
2477
|
+
providerFiles.map((file) =>
|
|
2478
|
+
readCachedSource(targetDir, file, sourceCache),
|
|
2479
|
+
),
|
|
2480
|
+
)
|
|
2481
|
+
).join("\n");
|
|
2482
|
+
const providerListSource = extractProviderListSource(providerSource);
|
|
2483
|
+
return providerListSource === undefined
|
|
2484
|
+
? []
|
|
2485
|
+
: providerListEntries(providerListSource);
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2336
2488
|
function registrationTokenHint(tokens: readonly string[]): string {
|
|
2337
2489
|
const token = tokens[0];
|
|
2338
2490
|
if (!token) return "the provider";
|
|
@@ -2847,6 +2999,463 @@ function containsCallExpression(source: string, name: string): boolean {
|
|
|
2847
2999
|
).test(source);
|
|
2848
3000
|
}
|
|
2849
3001
|
|
|
3002
|
+
type WorkflowRegistryKind =
|
|
3003
|
+
| "schedules"
|
|
3004
|
+
| "tasks"
|
|
3005
|
+
| "events"
|
|
3006
|
+
| "jobs"
|
|
3007
|
+
| "listeners";
|
|
3008
|
+
|
|
3009
|
+
type FeatureWorkflowRegistry = {
|
|
3010
|
+
kind: WorkflowRegistryKind;
|
|
3011
|
+
registryName: string;
|
|
3012
|
+
indexFile: string;
|
|
3013
|
+
members: string[];
|
|
3014
|
+
memberFiles: Map<string, string>;
|
|
3015
|
+
};
|
|
3016
|
+
|
|
3017
|
+
type UnregisteredWorkflowRegistry = {
|
|
3018
|
+
registry: FeatureWorkflowRegistry;
|
|
3019
|
+
missingMembers: string[];
|
|
3020
|
+
/** True when no member is individually registered either. */
|
|
3021
|
+
fullyUnregistered: boolean;
|
|
3022
|
+
};
|
|
3023
|
+
|
|
3024
|
+
type WorkflowRegistrationDrift = {
|
|
3025
|
+
schedules: {
|
|
3026
|
+
centralExists: boolean;
|
|
3027
|
+
unregistered: UnregisteredWorkflowRegistry[];
|
|
3028
|
+
};
|
|
3029
|
+
tasks: {
|
|
3030
|
+
centralExists: boolean;
|
|
3031
|
+
unregistered: UnregisteredWorkflowRegistry[];
|
|
3032
|
+
};
|
|
3033
|
+
outbox: {
|
|
3034
|
+
exists: boolean;
|
|
3035
|
+
events: UnregisteredWorkflowRegistry[];
|
|
3036
|
+
jobs: UnregisteredWorkflowRegistry[];
|
|
3037
|
+
};
|
|
3038
|
+
listeners: {
|
|
3039
|
+
unregistered: UnregisteredWorkflowRegistry[];
|
|
3040
|
+
wiringFile?: string;
|
|
3041
|
+
eventBusFile?: string;
|
|
3042
|
+
};
|
|
3043
|
+
};
|
|
3044
|
+
|
|
3045
|
+
const workflowRegistrySuffixes: Record<WorkflowRegistryKind, string> = {
|
|
3046
|
+
schedules: "Schedules",
|
|
3047
|
+
tasks: "Tasks",
|
|
3048
|
+
events: "Events",
|
|
3049
|
+
jobs: "Jobs",
|
|
3050
|
+
listeners: "Listeners",
|
|
3051
|
+
};
|
|
3052
|
+
|
|
3053
|
+
async function inspectWorkflowRegistrationDrift(
|
|
3054
|
+
targetDir: string,
|
|
3055
|
+
files: string[],
|
|
3056
|
+
config: ResolvedBeignetConfig,
|
|
3057
|
+
convention: InspectConvention,
|
|
3058
|
+
): Promise<InspectDiagnostic[]> {
|
|
3059
|
+
if (!convention.resourceGenerator) return [];
|
|
3060
|
+
|
|
3061
|
+
const drift = await workflowRegistrationDrift(targetDir, files, config);
|
|
3062
|
+
const diagnostics: InspectDiagnostic[] = [];
|
|
3063
|
+
|
|
3064
|
+
for (const kind of ["schedules", "tasks"] as const) {
|
|
3065
|
+
const { centralExists, unregistered } = drift[kind];
|
|
3066
|
+
const centralFile =
|
|
3067
|
+
kind === "schedules" ? config.paths.schedules : config.paths.tasks;
|
|
3068
|
+
const code =
|
|
3069
|
+
kind === "schedules"
|
|
3070
|
+
? "BEIGNET_SCHEDULE_UNREGISTERED"
|
|
3071
|
+
: "BEIGNET_TASK_UNREGISTERED";
|
|
3072
|
+
const registrationTarget =
|
|
3073
|
+
kind === "schedules" ? "the schedules array" : "defineTasks([...])";
|
|
3074
|
+
const makeCommand =
|
|
3075
|
+
kind === "schedules" ? "beignet make schedule" : "beignet make task";
|
|
3076
|
+
const noun = kind === "schedules" ? "schedule" : "task";
|
|
3077
|
+
|
|
3078
|
+
for (const { registry, missingMembers } of unregistered) {
|
|
3079
|
+
for (const member of missingMembers) {
|
|
3080
|
+
diagnostics.push({
|
|
3081
|
+
severity: "error",
|
|
3082
|
+
code,
|
|
3083
|
+
file: centralExists ? centralFile : registry.indexFile,
|
|
3084
|
+
message: centralExists
|
|
3085
|
+
? `${workflowArtifactOrigin(registry, member)}, but it is not registered in ${registrationTarget} in ${centralFile}, so the ${noun} never runs. Import ${registry.registryName} and spread it into the ${kind} list.`
|
|
3086
|
+
: `${workflowArtifactOrigin(registry, member)}, but ${centralFile} does not exist, so the ${noun} never runs. Register ${registry.registryName} in ${centralFile} (or run \`${makeCommand}\` to create it).`,
|
|
3087
|
+
});
|
|
3088
|
+
}
|
|
3089
|
+
}
|
|
3090
|
+
}
|
|
3091
|
+
|
|
3092
|
+
if (drift.outbox.exists) {
|
|
3093
|
+
for (const { registry, missingMembers } of drift.outbox.events) {
|
|
3094
|
+
for (const member of missingMembers) {
|
|
3095
|
+
diagnostics.push({
|
|
3096
|
+
severity: "warning",
|
|
3097
|
+
code: "BEIGNET_OUTBOX_EVENT_UNREGISTERED",
|
|
3098
|
+
file: config.paths.outbox,
|
|
3099
|
+
message: `${workflowArtifactOrigin(registry, member)} and listeners react to it, but it is not registered in the events list of defineOutboxRegistry({...}) in ${config.paths.outbox}, so outbox drain cannot deliver it. Import ${registry.registryName} and spread it into the events list.`,
|
|
3100
|
+
});
|
|
3101
|
+
}
|
|
3102
|
+
}
|
|
3103
|
+
|
|
3104
|
+
for (const { registry, missingMembers } of drift.outbox.jobs) {
|
|
3105
|
+
for (const member of missingMembers) {
|
|
3106
|
+
diagnostics.push({
|
|
3107
|
+
severity: "warning",
|
|
3108
|
+
code: "BEIGNET_OUTBOX_JOB_UNREGISTERED",
|
|
3109
|
+
file: config.paths.outbox,
|
|
3110
|
+
message: `${workflowArtifactOrigin(registry, member)}, but it is not registered in the jobs list of defineOutboxRegistry({...}) in ${config.paths.outbox}, so outbox drain cannot deliver it. Import ${registry.registryName} and spread it into the jobs list.`,
|
|
3111
|
+
});
|
|
3112
|
+
}
|
|
3113
|
+
}
|
|
3114
|
+
}
|
|
3115
|
+
|
|
3116
|
+
const infraDir = directoryPath(
|
|
3117
|
+
path.dirname(config.paths.infrastructurePorts),
|
|
3118
|
+
);
|
|
3119
|
+
const listenerTarget = drift.listeners.wiringFile
|
|
3120
|
+
? `${drift.listeners.wiringFile}, which already calls registerListeners(...)`
|
|
3121
|
+
: drift.listeners.eventBusFile
|
|
3122
|
+
? `${drift.listeners.eventBusFile}, which creates the event bus`
|
|
3123
|
+
: `an infra provider under ${infraDir}/`;
|
|
3124
|
+
for (const { registry, missingMembers } of drift.listeners.unregistered) {
|
|
3125
|
+
for (const member of missingMembers) {
|
|
3126
|
+
diagnostics.push({
|
|
3127
|
+
severity: "warning",
|
|
3128
|
+
code: "BEIGNET_LISTENER_UNREGISTERED",
|
|
3129
|
+
file:
|
|
3130
|
+
drift.listeners.wiringFile ??
|
|
3131
|
+
drift.listeners.eventBusFile ??
|
|
3132
|
+
registry.indexFile,
|
|
3133
|
+
message: `${workflowArtifactOrigin(registry, member)}, but no registerListeners(...) call references it, so the listener never receives events. Register ${registry.registryName} with registerListeners(...) in ${listenerTarget}.`,
|
|
3134
|
+
});
|
|
3135
|
+
}
|
|
3136
|
+
}
|
|
3137
|
+
|
|
3138
|
+
return diagnostics;
|
|
3139
|
+
}
|
|
3140
|
+
|
|
3141
|
+
function workflowArtifactOrigin(
|
|
3142
|
+
registry: FeatureWorkflowRegistry,
|
|
3143
|
+
member: string,
|
|
3144
|
+
): string {
|
|
3145
|
+
const memberFile = registry.memberFiles.get(member) ?? registry.indexFile;
|
|
3146
|
+
if (memberFile === registry.indexFile) {
|
|
3147
|
+
return `${registry.indexFile} declares ${member} in ${registry.registryName}`;
|
|
3148
|
+
}
|
|
3149
|
+
return `${memberFile} declares ${member} (exported through ${registry.registryName} in ${registry.indexFile})`;
|
|
3150
|
+
}
|
|
3151
|
+
|
|
3152
|
+
async function workflowRegistrationDrift(
|
|
3153
|
+
targetDir: string,
|
|
3154
|
+
files: string[],
|
|
3155
|
+
config: ResolvedBeignetConfig,
|
|
3156
|
+
): Promise<WorkflowRegistrationDrift> {
|
|
3157
|
+
const registries = await readFeatureWorkflowRegistries(
|
|
3158
|
+
targetDir,
|
|
3159
|
+
files,
|
|
3160
|
+
config,
|
|
3161
|
+
);
|
|
3162
|
+
const byKind = (kind: WorkflowRegistryKind) =>
|
|
3163
|
+
registries.filter((registry) => registry.kind === kind);
|
|
3164
|
+
|
|
3165
|
+
const drift: WorkflowRegistrationDrift = {
|
|
3166
|
+
schedules: {
|
|
3167
|
+
centralExists: files.includes(config.paths.schedules),
|
|
3168
|
+
unregistered: [],
|
|
3169
|
+
},
|
|
3170
|
+
tasks: {
|
|
3171
|
+
centralExists: files.includes(config.paths.tasks),
|
|
3172
|
+
unregistered: [],
|
|
3173
|
+
},
|
|
3174
|
+
outbox: {
|
|
3175
|
+
exists: files.includes(config.paths.outbox),
|
|
3176
|
+
events: [],
|
|
3177
|
+
jobs: [],
|
|
3178
|
+
},
|
|
3179
|
+
listeners: { unregistered: [] },
|
|
3180
|
+
};
|
|
3181
|
+
if (registries.length === 0) return drift;
|
|
3182
|
+
|
|
3183
|
+
for (const kind of ["schedules", "tasks"] as const) {
|
|
3184
|
+
const kindRegistries = byKind(kind);
|
|
3185
|
+
if (kindRegistries.length === 0) continue;
|
|
3186
|
+
|
|
3187
|
+
const central = drift[kind];
|
|
3188
|
+
const registered = central.centralExists
|
|
3189
|
+
? registeredWorkflowIdentifiers(
|
|
3190
|
+
await readFile(
|
|
3191
|
+
path.join(
|
|
3192
|
+
targetDir,
|
|
3193
|
+
kind === "schedules"
|
|
3194
|
+
? config.paths.schedules
|
|
3195
|
+
: config.paths.tasks,
|
|
3196
|
+
),
|
|
3197
|
+
"utf8",
|
|
3198
|
+
),
|
|
3199
|
+
kind,
|
|
3200
|
+
)
|
|
3201
|
+
: new Set<string>();
|
|
3202
|
+
central.unregistered = unregisteredWorkflowRegistries(
|
|
3203
|
+
kindRegistries,
|
|
3204
|
+
registered,
|
|
3205
|
+
);
|
|
3206
|
+
}
|
|
3207
|
+
|
|
3208
|
+
if (drift.outbox.exists) {
|
|
3209
|
+
const eventRegistries = byKind("events");
|
|
3210
|
+
const jobRegistries = byKind("jobs");
|
|
3211
|
+
if (eventRegistries.length > 0 || jobRegistries.length > 0) {
|
|
3212
|
+
const outboxSource = await readFile(
|
|
3213
|
+
path.join(targetDir, config.paths.outbox),
|
|
3214
|
+
"utf8",
|
|
3215
|
+
);
|
|
3216
|
+
|
|
3217
|
+
if (eventRegistries.length > 0) {
|
|
3218
|
+
// Events without listeners have nothing to deliver on drain, so only
|
|
3219
|
+
// record-only events stay out of the registry without a warning.
|
|
3220
|
+
const listened = await listenedEventNames(targetDir, files, config);
|
|
3221
|
+
const value = outboxRegistryValueInfo(outboxSource, "events");
|
|
3222
|
+
const registered = value
|
|
3223
|
+
? identifiersFromArrayExpression(value.text)
|
|
3224
|
+
: new Set<string>();
|
|
3225
|
+
drift.outbox.events = unregisteredWorkflowRegistries(
|
|
3226
|
+
eventRegistries,
|
|
3227
|
+
registered,
|
|
3228
|
+
(member) => listened.has(member),
|
|
3229
|
+
);
|
|
3230
|
+
}
|
|
3231
|
+
|
|
3232
|
+
if (jobRegistries.length > 0) {
|
|
3233
|
+
const value = outboxRegistryValueInfo(outboxSource, "jobs");
|
|
3234
|
+
const registered = value
|
|
3235
|
+
? identifiersFromArrayExpression(value.text)
|
|
3236
|
+
: new Set<string>();
|
|
3237
|
+
drift.outbox.jobs = unregisteredWorkflowRegistries(
|
|
3238
|
+
jobRegistries,
|
|
3239
|
+
registered,
|
|
3240
|
+
);
|
|
3241
|
+
}
|
|
3242
|
+
}
|
|
3243
|
+
}
|
|
3244
|
+
|
|
3245
|
+
const listenerRegistries = byKind("listeners");
|
|
3246
|
+
if (listenerRegistries.length > 0) {
|
|
3247
|
+
const wiring = await listenerWiringReferences(targetDir, files);
|
|
3248
|
+
drift.listeners = {
|
|
3249
|
+
unregistered: unregisteredWorkflowRegistries(
|
|
3250
|
+
listenerRegistries,
|
|
3251
|
+
wiring.identifiers,
|
|
3252
|
+
),
|
|
3253
|
+
wiringFile: wiring.wiringFile,
|
|
3254
|
+
eventBusFile: wiring.eventBusFile,
|
|
3255
|
+
};
|
|
3256
|
+
}
|
|
3257
|
+
|
|
3258
|
+
return drift;
|
|
3259
|
+
}
|
|
3260
|
+
|
|
3261
|
+
function unregisteredWorkflowRegistries(
|
|
3262
|
+
registries: FeatureWorkflowRegistry[],
|
|
3263
|
+
registered: Set<string>,
|
|
3264
|
+
memberApplies: (member: string) => boolean = () => true,
|
|
3265
|
+
): UnregisteredWorkflowRegistry[] {
|
|
3266
|
+
const unregistered: UnregisteredWorkflowRegistry[] = [];
|
|
3267
|
+
|
|
3268
|
+
for (const registry of registries) {
|
|
3269
|
+
if (registered.has(registry.registryName)) continue;
|
|
3270
|
+
|
|
3271
|
+
const missingMembers = registry.members.filter(
|
|
3272
|
+
(member) => !registered.has(member) && memberApplies(member),
|
|
3273
|
+
);
|
|
3274
|
+
if (missingMembers.length === 0) continue;
|
|
3275
|
+
|
|
3276
|
+
unregistered.push({
|
|
3277
|
+
registry,
|
|
3278
|
+
missingMembers,
|
|
3279
|
+
fullyUnregistered: registry.members.every(
|
|
3280
|
+
(member) => !registered.has(member),
|
|
3281
|
+
),
|
|
3282
|
+
});
|
|
3283
|
+
}
|
|
3284
|
+
|
|
3285
|
+
return unregistered;
|
|
3286
|
+
}
|
|
3287
|
+
|
|
3288
|
+
async function readFeatureWorkflowRegistries(
|
|
3289
|
+
targetDir: string,
|
|
3290
|
+
files: string[],
|
|
3291
|
+
config: ResolvedBeignetConfig,
|
|
3292
|
+
): Promise<FeatureWorkflowRegistry[]> {
|
|
3293
|
+
const registries: FeatureWorkflowRegistry[] = [];
|
|
3294
|
+
const featuresPath = directoryPath(config.paths.features);
|
|
3295
|
+
const kinds: WorkflowRegistryKind[] = [
|
|
3296
|
+
"schedules",
|
|
3297
|
+
"tasks",
|
|
3298
|
+
"events",
|
|
3299
|
+
"jobs",
|
|
3300
|
+
"listeners",
|
|
3301
|
+
];
|
|
3302
|
+
|
|
3303
|
+
for (const kind of kinds) {
|
|
3304
|
+
const folder = kind === "events" ? "domain/events" : kind;
|
|
3305
|
+
const pattern = new RegExp(
|
|
3306
|
+
`^${escapeRegExp(featuresPath)}/[^/]+/${escapeRegExp(folder)}/index\\.ts$`,
|
|
3307
|
+
);
|
|
3308
|
+
|
|
3309
|
+
for (const file of files) {
|
|
3310
|
+
if (!pattern.test(file)) continue;
|
|
3311
|
+
|
|
3312
|
+
const source = await readFile(path.join(targetDir, file), "utf8");
|
|
3313
|
+
const memberFiles = exportedMemberFiles(source, file, files);
|
|
3314
|
+
const exportRegex = new RegExp(
|
|
3315
|
+
`(?:^|\\n)export const\\s+([A-Za-z_$][\\w$]*${workflowRegistrySuffixes[kind]})\\s*=`,
|
|
3316
|
+
"g",
|
|
3317
|
+
);
|
|
3318
|
+
|
|
3319
|
+
for (const exportMatch of source.matchAll(exportRegex)) {
|
|
3320
|
+
const registryName = exportMatch[1];
|
|
3321
|
+
const info = arrayInitializerInfo(source, registryName);
|
|
3322
|
+
if (!info) continue;
|
|
3323
|
+
|
|
3324
|
+
const members = [...identifiersFromArrayExpression(info.text)];
|
|
3325
|
+
if (members.length === 0) continue;
|
|
3326
|
+
|
|
3327
|
+
registries.push({
|
|
3328
|
+
kind,
|
|
3329
|
+
registryName,
|
|
3330
|
+
indexFile: file,
|
|
3331
|
+
members,
|
|
3332
|
+
memberFiles,
|
|
3333
|
+
});
|
|
3334
|
+
}
|
|
3335
|
+
}
|
|
3336
|
+
}
|
|
3337
|
+
|
|
3338
|
+
return registries;
|
|
3339
|
+
}
|
|
3340
|
+
|
|
3341
|
+
function exportedMemberFiles(
|
|
3342
|
+
source: string,
|
|
3343
|
+
indexFile: string,
|
|
3344
|
+
files: string[],
|
|
3345
|
+
): Map<string, string> {
|
|
3346
|
+
const memberFiles = new Map<string, string>();
|
|
3347
|
+
const referenceRegex =
|
|
3348
|
+
/(?:import|export)\s+\{([^}]+)\}\s+from\s+["']([^"']+)["']/g;
|
|
3349
|
+
|
|
3350
|
+
for (const match of source.matchAll(referenceRegex)) {
|
|
3351
|
+
const resolved = sourceFileFromImport(match[2], indexFile, files);
|
|
3352
|
+
if (!resolved || !files.includes(resolved)) continue;
|
|
3353
|
+
|
|
3354
|
+
for (const member of match[1].split(",")) {
|
|
3355
|
+
const parsed = parseImportMember(member);
|
|
3356
|
+
if (parsed) memberFiles.set(parsed.localName, resolved);
|
|
3357
|
+
}
|
|
3358
|
+
}
|
|
3359
|
+
|
|
3360
|
+
return memberFiles;
|
|
3361
|
+
}
|
|
3362
|
+
|
|
3363
|
+
function registeredWorkflowIdentifiers(
|
|
3364
|
+
source: string,
|
|
3365
|
+
kind: "schedules" | "tasks",
|
|
3366
|
+
): Set<string> {
|
|
3367
|
+
if (kind === "tasks") {
|
|
3368
|
+
const call = /\bdefineTasks\s*(?:<[^>]*>\s*)?\(/.exec(source);
|
|
3369
|
+
if (call) {
|
|
3370
|
+
const firstArg = firstCallArgInfo(source, call.index + call[0].length);
|
|
3371
|
+
if (firstArg) return identifiersFromArrayExpression(firstArg.text);
|
|
3372
|
+
}
|
|
3373
|
+
}
|
|
3374
|
+
|
|
3375
|
+
const info = arrayInitializerInfo(
|
|
3376
|
+
source,
|
|
3377
|
+
kind === "tasks" ? "tasks" : "schedules",
|
|
3378
|
+
);
|
|
3379
|
+
return info ? identifiersFromArrayExpression(info.text) : new Set<string>();
|
|
3380
|
+
}
|
|
3381
|
+
|
|
3382
|
+
async function listenedEventNames(
|
|
3383
|
+
targetDir: string,
|
|
3384
|
+
files: string[],
|
|
3385
|
+
config: ResolvedBeignetConfig,
|
|
3386
|
+
): Promise<Set<string>> {
|
|
3387
|
+
const featuresPath = directoryPath(config.paths.features);
|
|
3388
|
+
const listenerPattern = new RegExp(
|
|
3389
|
+
`^${escapeRegExp(featuresPath)}/[^/]+/listeners/[^/]+\\.ts$`,
|
|
3390
|
+
);
|
|
3391
|
+
const listened = new Set<string>();
|
|
3392
|
+
|
|
3393
|
+
for (const file of files) {
|
|
3394
|
+
if (!listenerPattern.test(file) || isWorkflowTestFile(file)) continue;
|
|
3395
|
+
|
|
3396
|
+
const source = await readFile(path.join(targetDir, file), "utf8");
|
|
3397
|
+
for (const match of source.matchAll(
|
|
3398
|
+
/\bdefineListener\s*(?:<[^>]*>\s*)?\(\s*([A-Za-z_$][\w$]*)/g,
|
|
3399
|
+
)) {
|
|
3400
|
+
listened.add(match[1]);
|
|
3401
|
+
}
|
|
3402
|
+
}
|
|
3403
|
+
|
|
3404
|
+
return listened;
|
|
3405
|
+
}
|
|
3406
|
+
|
|
3407
|
+
async function listenerWiringReferences(
|
|
3408
|
+
targetDir: string,
|
|
3409
|
+
files: string[],
|
|
3410
|
+
): Promise<{
|
|
3411
|
+
identifiers: Set<string>;
|
|
3412
|
+
wiringFile?: string;
|
|
3413
|
+
eventBusFile?: string;
|
|
3414
|
+
}> {
|
|
3415
|
+
const identifiers = new Set<string>();
|
|
3416
|
+
let wiringFile: string | undefined;
|
|
3417
|
+
let eventBusFile: string | undefined;
|
|
3418
|
+
|
|
3419
|
+
for (const file of files) {
|
|
3420
|
+
if (!file.endsWith(".ts") && !file.endsWith(".tsx")) continue;
|
|
3421
|
+
if (file.endsWith(".d.ts") || isWorkflowTestFile(file)) continue;
|
|
3422
|
+
|
|
3423
|
+
const source = await readFile(path.join(targetDir, file), "utf8");
|
|
3424
|
+
let foundCall = false;
|
|
3425
|
+
|
|
3426
|
+
for (const match of source.matchAll(/\bregisterListeners\s*\(/g)) {
|
|
3427
|
+
const openParen = (match.index ?? 0) + match[0].length - 1;
|
|
3428
|
+
const closeParen = matchingDelimiterIndex(source, openParen, "(", ")");
|
|
3429
|
+
const argsText =
|
|
3430
|
+
closeParen === -1
|
|
3431
|
+
? source.slice(openParen)
|
|
3432
|
+
: source.slice(openParen + 1, closeParen);
|
|
3433
|
+
|
|
3434
|
+
foundCall = true;
|
|
3435
|
+
for (const identifier of identifiersFromArrayExpression(argsText)) {
|
|
3436
|
+
identifiers.add(identifier);
|
|
3437
|
+
}
|
|
3438
|
+
}
|
|
3439
|
+
|
|
3440
|
+
if (foundCall) {
|
|
3441
|
+
wiringFile ??= file;
|
|
3442
|
+
} else if (!eventBusFile && /\bcreate\w*EventBus\s*\(/.test(source)) {
|
|
3443
|
+
eventBusFile = file;
|
|
3444
|
+
}
|
|
3445
|
+
}
|
|
3446
|
+
|
|
3447
|
+
return { identifiers, wiringFile, eventBusFile };
|
|
3448
|
+
}
|
|
3449
|
+
|
|
3450
|
+
function isWorkflowTestFile(file: string): boolean {
|
|
3451
|
+
return (
|
|
3452
|
+
file.endsWith(".test.ts") ||
|
|
3453
|
+
file.endsWith(".test.tsx") ||
|
|
3454
|
+
file.startsWith("tests/") ||
|
|
3455
|
+
file.includes("/tests/")
|
|
3456
|
+
);
|
|
3457
|
+
}
|
|
3458
|
+
|
|
2850
3459
|
async function inspectServerlessFootguns(
|
|
2851
3460
|
targetDir: string,
|
|
2852
3461
|
files: string[],
|
|
@@ -4057,6 +4666,162 @@ async function fixUnregisteredRouteGroups(
|
|
|
4057
4666
|
};
|
|
4058
4667
|
}
|
|
4059
4668
|
|
|
4669
|
+
async function fixUnregisteredSchedules(
|
|
4670
|
+
targetDir: string,
|
|
4671
|
+
files: string[],
|
|
4672
|
+
drift: WorkflowRegistrationDrift,
|
|
4673
|
+
config: ResolvedBeignetConfig,
|
|
4674
|
+
): Promise<InspectFix | undefined> {
|
|
4675
|
+
if (!drift.schedules.centralExists) return undefined;
|
|
4676
|
+
|
|
4677
|
+
return applyWorkflowRegistryFix({
|
|
4678
|
+
targetDir,
|
|
4679
|
+
files,
|
|
4680
|
+
centralFile: config.paths.schedules,
|
|
4681
|
+
unregistered: drift.schedules.unregistered,
|
|
4682
|
+
code: "BEIGNET_SCHEDULE_UNREGISTERED",
|
|
4683
|
+
listName: "the schedules array",
|
|
4684
|
+
importSpecifier: (indexFile) => `@/${modulePath(indexFile)}`,
|
|
4685
|
+
append: (source, entry, importLine) =>
|
|
4686
|
+
appendToNamedArray(source, "schedules", entry, importLine),
|
|
4687
|
+
});
|
|
4688
|
+
}
|
|
4689
|
+
|
|
4690
|
+
async function fixUnregisteredTasks(
|
|
4691
|
+
targetDir: string,
|
|
4692
|
+
files: string[],
|
|
4693
|
+
drift: WorkflowRegistrationDrift,
|
|
4694
|
+
config: ResolvedBeignetConfig,
|
|
4695
|
+
): Promise<InspectFix | undefined> {
|
|
4696
|
+
if (!drift.tasks.centralExists) return undefined;
|
|
4697
|
+
|
|
4698
|
+
return applyWorkflowRegistryFix({
|
|
4699
|
+
targetDir,
|
|
4700
|
+
files,
|
|
4701
|
+
centralFile: config.paths.tasks,
|
|
4702
|
+
unregistered: drift.tasks.unregistered,
|
|
4703
|
+
code: "BEIGNET_TASK_UNREGISTERED",
|
|
4704
|
+
listName: "defineTasks([...])",
|
|
4705
|
+
importSpecifier: (indexFile) =>
|
|
4706
|
+
relativeModule(config.paths.tasks, indexFile),
|
|
4707
|
+
append: (source, entry, importLine) =>
|
|
4708
|
+
appendToNamedArray(source, "tasks", entry, importLine),
|
|
4709
|
+
});
|
|
4710
|
+
}
|
|
4711
|
+
|
|
4712
|
+
async function fixUnregisteredOutboxEntries(
|
|
4713
|
+
targetDir: string,
|
|
4714
|
+
files: string[],
|
|
4715
|
+
drift: WorkflowRegistrationDrift,
|
|
4716
|
+
config: ResolvedBeignetConfig,
|
|
4717
|
+
): Promise<InspectFix[]> {
|
|
4718
|
+
if (!drift.outbox.exists) return [];
|
|
4719
|
+
|
|
4720
|
+
const fixes: InspectFix[] = [];
|
|
4721
|
+
|
|
4722
|
+
const eventFix = await applyWorkflowRegistryFix({
|
|
4723
|
+
targetDir,
|
|
4724
|
+
files,
|
|
4725
|
+
centralFile: config.paths.outbox,
|
|
4726
|
+
unregistered: drift.outbox.events,
|
|
4727
|
+
code: "BEIGNET_OUTBOX_EVENT_UNREGISTERED",
|
|
4728
|
+
listName: "the outbox events list",
|
|
4729
|
+
importSpecifier: (indexFile) => `@/${modulePath(indexFile)}`,
|
|
4730
|
+
append: (source, entry, importLine) =>
|
|
4731
|
+
appendToOutboxRegistryArray(source, "events", entry, importLine),
|
|
4732
|
+
});
|
|
4733
|
+
if (eventFix) fixes.push(eventFix);
|
|
4734
|
+
|
|
4735
|
+
const jobFix = await applyWorkflowRegistryFix({
|
|
4736
|
+
targetDir,
|
|
4737
|
+
files,
|
|
4738
|
+
centralFile: config.paths.outbox,
|
|
4739
|
+
unregistered: drift.outbox.jobs,
|
|
4740
|
+
code: "BEIGNET_OUTBOX_JOB_UNREGISTERED",
|
|
4741
|
+
listName: "the outbox jobs list",
|
|
4742
|
+
importSpecifier: (indexFile) => `@/${modulePath(indexFile)}`,
|
|
4743
|
+
append: (source, entry, importLine) =>
|
|
4744
|
+
appendToOutboxRegistryArray(source, "jobs", entry, importLine),
|
|
4745
|
+
});
|
|
4746
|
+
if (jobFix) fixes.push(jobFix);
|
|
4747
|
+
|
|
4748
|
+
return fixes;
|
|
4749
|
+
}
|
|
4750
|
+
|
|
4751
|
+
/**
|
|
4752
|
+
* Append `...<feature><Kind>` registry spreads to an existing central
|
|
4753
|
+
* registry file.
|
|
4754
|
+
*
|
|
4755
|
+
* Bails without writing when an import for the registry name resolves to a
|
|
4756
|
+
* different file, when the registry anchor is missing or unparseable, or when
|
|
4757
|
+
* some members are already individually registered (appending the registry
|
|
4758
|
+
* spread would run those members twice). The diagnostic stays in that case.
|
|
4759
|
+
*/
|
|
4760
|
+
async function applyWorkflowRegistryFix(options: {
|
|
4761
|
+
targetDir: string;
|
|
4762
|
+
files: string[];
|
|
4763
|
+
centralFile: string;
|
|
4764
|
+
unregistered: UnregisteredWorkflowRegistry[];
|
|
4765
|
+
code: string;
|
|
4766
|
+
listName: string;
|
|
4767
|
+
importSpecifier: (indexFile: string) => string;
|
|
4768
|
+
append: (source: string, entry: string, importLine?: string) => AppendResult;
|
|
4769
|
+
}): Promise<InspectFix | undefined> {
|
|
4770
|
+
const candidates = options.unregistered.filter(
|
|
4771
|
+
(entry) => entry.fullyUnregistered,
|
|
4772
|
+
);
|
|
4773
|
+
if (candidates.length === 0) return undefined;
|
|
4774
|
+
|
|
4775
|
+
const centralPath = path.join(options.targetDir, options.centralFile);
|
|
4776
|
+
let original: string;
|
|
4777
|
+
try {
|
|
4778
|
+
original = await readFile(centralPath, "utf8");
|
|
4779
|
+
} catch {
|
|
4780
|
+
return undefined;
|
|
4781
|
+
}
|
|
4782
|
+
|
|
4783
|
+
let next = original;
|
|
4784
|
+
const registeredNames: string[] = [];
|
|
4785
|
+
|
|
4786
|
+
for (const { registry } of candidates) {
|
|
4787
|
+
const imported = parseNamedImportSources(next).get(registry.registryName);
|
|
4788
|
+
let importLine: string | undefined;
|
|
4789
|
+
|
|
4790
|
+
if (imported) {
|
|
4791
|
+
const importedFile = sourceFileFromImport(
|
|
4792
|
+
imported.sourcePath,
|
|
4793
|
+
options.centralFile,
|
|
4794
|
+
options.files,
|
|
4795
|
+
);
|
|
4796
|
+
if (importedFile !== registry.indexFile) return undefined;
|
|
4797
|
+
} else {
|
|
4798
|
+
importLine = `import { ${registry.registryName} } from "${options.importSpecifier(
|
|
4799
|
+
registry.indexFile,
|
|
4800
|
+
)}";`;
|
|
4801
|
+
}
|
|
4802
|
+
|
|
4803
|
+
const result = options.append(
|
|
4804
|
+
next,
|
|
4805
|
+
`...${registry.registryName}`,
|
|
4806
|
+
importLine,
|
|
4807
|
+
);
|
|
4808
|
+
if (result.kind === "missing") return undefined;
|
|
4809
|
+
if (result.kind === "updated") {
|
|
4810
|
+
next = result.source;
|
|
4811
|
+
registeredNames.push(registry.registryName);
|
|
4812
|
+
}
|
|
4813
|
+
}
|
|
4814
|
+
|
|
4815
|
+
if (next === original || registeredNames.length === 0) return undefined;
|
|
4816
|
+
|
|
4817
|
+
await writeFile(centralPath, next);
|
|
4818
|
+
return {
|
|
4819
|
+
code: options.code,
|
|
4820
|
+
file: options.centralFile,
|
|
4821
|
+
message: `Registered ${registeredNames.join(", ")} in ${options.listName}.`,
|
|
4822
|
+
};
|
|
4823
|
+
}
|
|
4824
|
+
|
|
4060
4825
|
function routeRegistryFileFromServerSource(
|
|
4061
4826
|
source: string,
|
|
4062
4827
|
serverFile: string,
|
|
@@ -4315,64 +5080,7 @@ function trimmedSlice(
|
|
|
4315
5080
|
};
|
|
4316
5081
|
}
|
|
4317
5082
|
|
|
4318
|
-
|
|
4319
|
-
const withoutBrackets = expression.replace(/^\[/, "").replace(/\]$/, "");
|
|
4320
|
-
|
|
4321
|
-
return new Set(
|
|
4322
|
-
Array.from(
|
|
4323
|
-
withoutBrackets.matchAll(/\b[A-Za-z_$][\w$]*\b/g),
|
|
4324
|
-
([name]) => name,
|
|
4325
|
-
),
|
|
4326
|
-
);
|
|
4327
|
-
}
|
|
4328
|
-
|
|
4329
|
-
function appendToArrayExpression(expression: string, names: string[]): string {
|
|
4330
|
-
const closingBracket = /\]\s*$/.exec(expression);
|
|
4331
|
-
if (!closingBracket) return expression;
|
|
4332
|
-
|
|
4333
|
-
const beforeClosingBracket = expression.slice(0, closingBracket.index);
|
|
4334
|
-
const closingBracketText = expression.slice(closingBracket.index);
|
|
4335
|
-
const inner = beforeClosingBracket.replace(/^\[/, "");
|
|
4336
|
-
if (!inner.trim()) return `[${names.join(", ")}]`;
|
|
4337
|
-
|
|
4338
|
-
if (!expression.includes("\n")) {
|
|
4339
|
-
const separator = /,\s*$/.test(inner) ? " " : ", ";
|
|
4340
|
-
return `${beforeClosingBracket}${separator}${names.join(", ")}${closingBracketText}`;
|
|
4341
|
-
}
|
|
4342
|
-
|
|
4343
|
-
const itemIndent =
|
|
4344
|
-
inner
|
|
4345
|
-
.split("\n")
|
|
4346
|
-
.find((line) => line.trim())
|
|
4347
|
-
?.match(/^[\t ]*/)?.[0] ?? "\t";
|
|
4348
|
-
const closingIndent = inner.match(/\n([\t ]*)$/)?.[1] ?? "";
|
|
4349
|
-
const trimmedBeforeClosingBracket = beforeClosingBracket.replace(/\s*$/, "");
|
|
4350
|
-
const appendedNames = names.join(`,\n${itemIndent}`);
|
|
4351
|
-
|
|
4352
|
-
if (/,\s*$/.test(inner)) {
|
|
4353
|
-
return `${trimmedBeforeClosingBracket}\n${itemIndent}${appendedNames},\n${closingIndent}${closingBracketText}`;
|
|
4354
|
-
}
|
|
4355
|
-
|
|
4356
|
-
return `${trimmedBeforeClosingBracket},\n${itemIndent}${appendedNames}${closingBracketText}`;
|
|
4357
|
-
}
|
|
4358
|
-
|
|
4359
|
-
function insertAfterImports(source: string, line: string): string {
|
|
4360
|
-
const lines = source.split("\n");
|
|
4361
|
-
let lastImportIndex = -1;
|
|
4362
|
-
|
|
4363
|
-
for (let index = 0; index < lines.length; index++) {
|
|
4364
|
-
if (lines[index].startsWith("import ")) {
|
|
4365
|
-
lastImportIndex = index;
|
|
4366
|
-
}
|
|
4367
|
-
}
|
|
4368
|
-
|
|
4369
|
-
if (lastImportIndex === -1) {
|
|
4370
|
-
return `${line}\n${source}`;
|
|
4371
|
-
}
|
|
4372
|
-
|
|
4373
|
-
lines.splice(lastImportIndex + 1, 0, line);
|
|
4374
|
-
return lines.join("\n");
|
|
4375
|
-
}
|
|
5083
|
+
const contractsFromArrayExpression = identifiersFromArrayExpression;
|
|
4376
5084
|
|
|
4377
5085
|
function hasImportedIdentifier(source: string, identifier: string): boolean {
|
|
4378
5086
|
const importRegex = /import\s+\{([^}]+)\}\s+from\s+["'][^"']+["']/g;
|