@beignet/cli 0.0.31 → 0.0.33
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 +107 -0
- package/README.md +70 -17
- package/dist/check.d.ts +2 -0
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +16 -0
- package/dist/check.js.map +1 -1
- package/dist/choices.d.ts +22 -2
- package/dist/choices.d.ts.map +1 -1
- package/dist/choices.js +62 -0
- package/dist/choices.js.map +1 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -1
- package/dist/create-prompts.d.ts +6 -2
- package/dist/create-prompts.d.ts.map +1 -1
- package/dist/create-prompts.js +1 -1
- package/dist/create-prompts.js.map +1 -1
- package/dist/db.d.ts +5 -5
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +5 -5
- package/dist/db.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +386 -27
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts +5 -0
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +805 -62
- package/dist/inspect.js.map +1 -1
- package/dist/make/inbox.d.ts.map +1 -1
- package/dist/make/inbox.js +5 -3
- package/dist/make/inbox.js.map +1 -1
- package/dist/make/payments.d.ts.map +1 -1
- package/dist/make/payments.js +58 -29
- package/dist/make/payments.js.map +1 -1
- package/dist/make/shared.d.ts +12 -3
- package/dist/make/shared.d.ts.map +1 -1
- package/dist/make/shared.js +52 -11
- package/dist/make/shared.js.map +1 -1
- package/dist/make/tenancy.d.ts.map +1 -1
- package/dist/make/tenancy.js +6 -4
- package/dist/make/tenancy.js.map +1 -1
- package/dist/make.d.ts.map +1 -1
- package/dist/make.js +136 -65
- package/dist/make.js.map +1 -1
- package/dist/outbox.d.ts +114 -1
- package/dist/outbox.d.ts.map +1 -1
- package/dist/outbox.js +190 -0
- package/dist/outbox.js.map +1 -1
- package/dist/preflight.d.ts +90 -0
- package/dist/preflight.d.ts.map +1 -0
- package/dist/preflight.js +423 -0
- package/dist/preflight.js.map +1 -0
- package/dist/provider-add.d.ts +10 -1
- package/dist/provider-add.d.ts.map +1 -1
- package/dist/provider-add.js +255 -16
- package/dist/provider-add.js.map +1 -1
- package/dist/task.d.ts.map +1 -1
- package/dist/task.js +2 -3
- package/dist/task.js.map +1 -1
- package/dist/templates/agents.d.ts.map +1 -1
- package/dist/templates/agents.js +6 -3
- package/dist/templates/agents.js.map +1 -1
- package/dist/templates/base.d.ts.map +1 -1
- package/dist/templates/base.js +5 -11
- package/dist/templates/base.js.map +1 -1
- package/dist/templates/index.d.ts.map +1 -1
- package/dist/templates/index.js +2 -0
- package/dist/templates/index.js.map +1 -1
- package/dist/templates/server.d.ts.map +1 -1
- package/dist/templates/server.js +10 -5
- package/dist/templates/server.js.map +1 -1
- package/dist/templates/shared.d.ts +5 -1
- package/dist/templates/shared.d.ts.map +1 -1
- package/dist/templates/shared.js +13 -1
- package/dist/templates/shared.js.map +1 -1
- package/dist/templates/testing.d.ts +5 -0
- package/dist/templates/testing.d.ts.map +1 -0
- package/dist/templates/testing.js +542 -0
- package/dist/templates/testing.js.map +1 -0
- package/dist/templates/todos.js +2 -2
- package/package.json +3 -2
- package/skills/app-structure/SKILL.md +12 -4
- package/src/check.ts +23 -0
- package/src/choices.ts +84 -0
- package/src/config.ts +4 -0
- package/src/create-prompts.ts +8 -2
- package/src/db.ts +12 -12
- package/src/index.ts +592 -37
- package/src/inspect.ts +1253 -156
- package/src/make/inbox.ts +5 -3
- package/src/make/payments.ts +58 -29
- package/src/make/shared.ts +87 -13
- package/src/make/tenancy.ts +6 -4
- package/src/make.ts +179 -65
- package/src/outbox.ts +363 -0
- package/src/preflight.ts +596 -0
- package/src/provider-add.ts +270 -16
- package/src/task.ts +2 -3
- package/src/templates/agents.ts +6 -3
- package/src/templates/base.ts +5 -13
- package/src/templates/index.ts +2 -0
- package/src/templates/server.ts +10 -5
- package/src/templates/shared.ts +15 -1
- package/src/templates/testing.ts +545 -0
- package/src/templates/todos.ts +2 -2
- package/src/test-helpers/generated-app.ts +10 -6
package/dist/inspect.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import { readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
1
|
+
import { mkdir, readdir, readFile, stat, writeFile } 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
5
|
import { clientDirPath, clientFormsPath, clientIndexPath, defaultBeignetConfig, directoryPath, loadBeignetConfig, normalizePath, resolveConfig, } from "./config.js";
|
|
6
6
|
import { formatGithubAnnotation, } from "./github-annotations.js";
|
|
7
|
-
import { providersFilePath, wireListenersProviderSource, } from "./make/shared.js";
|
|
7
|
+
import { createListenersRegistrySource, listenersFilePath, providersFilePath, updateListenersRegistrySource, wireListenersProviderSource, } from "./make/shared.js";
|
|
8
8
|
import { detectedProviderVariants, diagnosticPackageJsonFile, formatProviderMetadataIssues, installedPackageNames, providerDoctorRulesForInstalledPackages, providerOperationalConfigFiles, providerRequiredEnvExists, providerRequiredTableExists, providerVariantRegistrationSeverity, readProviderListEntries, readProviderPackageMetadataSource, registrationTokenHint, serverProviderFiles, } from "./provider-audit.js";
|
|
9
9
|
import { appendToArrayExpression, appendToNamedArray, appendToOutboxRegistryArray, arrayInitializerInfo, identifiersFromArrayExpression, insertAfterImports, matchingDelimiterIndex, outboxRegistryValueInfo, } from "./registry-edits.js";
|
|
10
|
+
import { currentGeneratedPackageScripts, externalVersions, legacyGeneratedPackageScripts, } from "./templates/shared.js";
|
|
11
|
+
import { testSupportTemplateFiles } from "./templates/testing.js";
|
|
10
12
|
import { getCliVersion } from "./version.js";
|
|
11
13
|
export async function inspectApp(options = {}) {
|
|
12
14
|
const targetDir = path.resolve(options.cwd ?? process.cwd());
|
|
@@ -48,7 +50,7 @@ export async function applyDoctorFixes(options = {}) {
|
|
|
48
50
|
const routeExports = await readRouteExports(targetDir, routeFiles, config);
|
|
49
51
|
const matchedRoutes = matchRoutes(contracts, routeExports);
|
|
50
52
|
const fixes = [];
|
|
51
|
-
const packageFix = await
|
|
53
|
+
const packageFix = await fixPackageScripts(targetDir, files, convention);
|
|
52
54
|
if (packageFix)
|
|
53
55
|
fixes.push(packageFix);
|
|
54
56
|
const routeGroupFix = await fixUnregisteredRouteGroups(targetDir, files, config);
|
|
@@ -147,7 +149,11 @@ export function formatDoctor(result, options = {}) {
|
|
|
147
149
|
lines.push("", formatDoctorFooter(result));
|
|
148
150
|
return lines.join("\n");
|
|
149
151
|
}
|
|
150
|
-
|
|
152
|
+
/**
|
|
153
|
+
* Doctor diagnostic codes that describe production hardening concerns.
|
|
154
|
+
* `beignet preflight` promotes these to gate failures.
|
|
155
|
+
*/
|
|
156
|
+
export const productionHardeningDiagnosticCodes = new Set([
|
|
151
157
|
"BEIGNET_AUTH_ROUTE_MISSING",
|
|
152
158
|
"BEIGNET_AUTH_TRUSTED_ORIGINS_MISSING",
|
|
153
159
|
"BEIGNET_BETTER_AUTH_SECRET_PLACEHOLDER",
|
|
@@ -163,6 +169,7 @@ const productionHardeningDiagnosticCodes = new Set([
|
|
|
163
169
|
"BEIGNET_PROVIDER_ENV_MISSING",
|
|
164
170
|
"BEIGNET_PROVIDER_TABLE_MISSING",
|
|
165
171
|
"BEIGNET_READINESS_ROUTE_MISSING",
|
|
172
|
+
"BEIGNET_SECURITY_HEADERS_MISSING",
|
|
166
173
|
"BEIGNET_TENANT_HEADER_AUTHORITY",
|
|
167
174
|
"BEIGNET_UPLOAD_AUTHORIZATION_MISSING",
|
|
168
175
|
"BEIGNET_UPLOAD_SIZE_LIMIT_MISSING",
|
|
@@ -175,7 +182,7 @@ function productionHardeningChecklist(diagnostics) {
|
|
|
175
182
|
"Secrets and provider credentials are unique per environment, validated at startup, never committed, and never logged.",
|
|
176
183
|
"Auth routes, tenant resolution, authorization policies, and trusted origins derive authority from verified sessions, API keys, or trusted gateway metadata.",
|
|
177
184
|
"Devtools, OpenAPI, cron, webhooks, and operational routes have intentional exposure and app-owned authorization.",
|
|
178
|
-
"CORS origins, proxy IP trust, rate-limit keys, and request body limits match the production host topology.",
|
|
185
|
+
"Security headers, CORS origins, proxy IP trust, rate-limit keys, and request body limits match the production host topology.",
|
|
179
186
|
"Uploads and storage define max sizes, authorization or explicit public access, object visibility, safe content headers, and short direct-upload expirations.",
|
|
180
187
|
"Provider-backed dependencies have bounded readiness checks, worker shutdown behavior, webhook secrets, and least-privilege credentials.",
|
|
181
188
|
];
|
|
@@ -842,7 +849,7 @@ async function inspectDiagnostics(targetDir, files, config, convention, contract
|
|
|
842
849
|
for (const routeHandler of matchedRoutes.unmatchedRouteHandlers) {
|
|
843
850
|
diagnostics.push(unmatchedRouteDiagnostic(routeHandler));
|
|
844
851
|
}
|
|
845
|
-
diagnostics.push(...(await inspectPackageScripts(targetDir, files, convention)), ...(await inspectOpenApiDrift(targetDir, files, config, convention, contracts, matchedRoutes)), ...(await inspectFeatureRouteRegistration(targetDir, files, config, convention, contracts)), ...(await inspectRouteErrorCatalogDrift(targetDir, files, config, convention, contracts, strict)), ...(await inspectCanonicalConformance(targetDir, files, config, convention, strict)), ...(await inspectVersionSkew(targetDir, files)), ...(await inspectProviderMetadataDrift(targetDir, files, convention)), ...(await inspectPortProviderDrift(targetDir, files, config, convention)), ...(await inspectProviderRegistrationDrift(targetDir, files, config, convention)), ...(await inspectDatabaseLifecycleDrift(targetDir, files, config, convention, strict)), ...(await inspectAuthzAuditDrift(targetDir, files, config, convention, contracts)), ...(await inspectServerlessFootguns(targetDir, files, config, convention)), ...(await inspectProductionReadiness(targetDir, files, config, convention)), ...(await inspectFeatureArtifactDrift(targetDir, files, config, convention)), ...(await inspectWorkflowRegistrationDrift(targetDir, files, config, convention)), ...(await inspectResourceSlices(targetDir, files, config, convention, strict)));
|
|
852
|
+
diagnostics.push(...(await inspectPackageScripts(targetDir, files, convention)), ...(await inspectOpenApiDrift(targetDir, files, config, convention, contracts, matchedRoutes)), ...(await inspectFeatureRouteRegistration(targetDir, files, config, convention, contracts)), ...(await inspectRouteErrorCatalogDrift(targetDir, files, config, convention, contracts, strict)), ...(await inspectCanonicalConformance(targetDir, files, config, convention, strict)), ...(await inspectVersionSkew(targetDir, files)), ...(await inspectProviderMetadataDrift(targetDir, files, convention)), ...(await inspectPortProviderDrift(targetDir, files, config, convention)), ...(await inspectProviderRegistrationDrift(targetDir, files, config, convention)), ...(await inspectDatabaseLifecycleDrift(targetDir, files, config, convention, strict)), ...(await inspectAuthzAuditDrift(targetDir, files, config, convention, contracts)), ...(await inspectServerlessFootguns(targetDir, files, config, convention)), ...(await inspectProductionReadiness(targetDir, files, config, convention)), ...(await inspectFeatureArtifactDrift(targetDir, files, config, convention)), ...(await inspectWorkflowRegistrationDrift(targetDir, files, config, convention)), ...(await inspectRuntimeManifestDrift(targetDir, files, config, convention)), ...(await inspectResourceSlices(targetDir, files, config, convention, strict)));
|
|
846
853
|
return dedupeDiagnostics(diagnostics);
|
|
847
854
|
}
|
|
848
855
|
async function inspectCanonicalConformance(targetDir, files, config, convention, strict) {
|
|
@@ -1224,6 +1231,7 @@ const readinessRelevantProviderPackages = new Set([
|
|
|
1224
1231
|
"@beignet/provider-auth-better-auth",
|
|
1225
1232
|
"@beignet/provider-db-drizzle",
|
|
1226
1233
|
"@beignet/provider-error-reporting-sentry",
|
|
1234
|
+
"@beignet/provider-event-bus-redis",
|
|
1227
1235
|
"@beignet/provider-flags-openfeature",
|
|
1228
1236
|
"@beignet/provider-jobs-bullmq",
|
|
1229
1237
|
"@beignet/provider-jobs-inngest",
|
|
@@ -1232,7 +1240,7 @@ const readinessRelevantProviderPackages = new Set([
|
|
|
1232
1240
|
"@beignet/provider-mail-smtp",
|
|
1233
1241
|
"@beignet/provider-payments-stripe",
|
|
1234
1242
|
"@beignet/provider-rate-limit-upstash",
|
|
1235
|
-
"@beignet/provider-redis",
|
|
1243
|
+
"@beignet/provider-cache-redis",
|
|
1236
1244
|
"@beignet/provider-search-meilisearch",
|
|
1237
1245
|
"@beignet/provider-storage-s3",
|
|
1238
1246
|
]);
|
|
@@ -1247,10 +1255,16 @@ async function inspectProductionReadiness(targetDir, files, config, convention)
|
|
|
1247
1255
|
const configFiles = operationalConfigFiles(files, config);
|
|
1248
1256
|
const infrastructurePath = directoryPath(path.dirname(config.paths.infrastructurePorts));
|
|
1249
1257
|
const schemaDir = `${infrastructurePath}/db/schema`;
|
|
1258
|
+
let hasSecurityHeadersHook = false;
|
|
1250
1259
|
for (const file of files) {
|
|
1251
1260
|
if (!file.endsWith(".ts") || file.endsWith(".test.ts"))
|
|
1252
1261
|
continue;
|
|
1253
1262
|
const source = await readCachedSource(targetDir, file, sourceCache);
|
|
1263
|
+
const routeServerOrInfra = isRouteServerOrInfraFile(file, config);
|
|
1264
|
+
if (routeServerOrInfra &&
|
|
1265
|
+
containsCallExpression(source, "createSecurityHeadersHooks")) {
|
|
1266
|
+
hasSecurityHeadersHook = true;
|
|
1267
|
+
}
|
|
1254
1268
|
if (containsCallExpression(source, "createDevtoolsRoute") &&
|
|
1255
1269
|
/\benabled\s*:\s*true\b/.test(source) &&
|
|
1256
1270
|
!/\bauthorize\s*:/.test(source)) {
|
|
@@ -1293,8 +1307,7 @@ async function inspectProductionReadiness(targetDir, files, config, convention)
|
|
|
1293
1307
|
message: `${file} defines a protected upload without authorize(...). Add an authorize hook, or set access: "public" only for intentionally public uploads.`,
|
|
1294
1308
|
});
|
|
1295
1309
|
}
|
|
1296
|
-
if (
|
|
1297
|
-
containsRequestTenantHeaderAuthority(source)) {
|
|
1310
|
+
if (routeServerOrInfra && containsRequestTenantHeaderAuthority(source)) {
|
|
1298
1311
|
diagnostics.push({
|
|
1299
1312
|
severity: "warning",
|
|
1300
1313
|
code: "BEIGNET_TENANT_HEADER_AUTHORITY",
|
|
@@ -1302,7 +1315,7 @@ async function inspectProductionReadiness(targetDir, files, config, convention)
|
|
|
1302
1315
|
message: `${file} reads x-tenant-id and turns it into tenant authority. Resolve request tenants from verified auth/session claims or trusted gateway metadata instead of a caller-controlled header.`,
|
|
1303
1316
|
});
|
|
1304
1317
|
}
|
|
1305
|
-
if (
|
|
1318
|
+
if (routeServerOrInfra &&
|
|
1306
1319
|
containsCallExpression(source, "createCorsHooks") &&
|
|
1307
1320
|
/\bcredentials\s*:\s*true\b/.test(source) &&
|
|
1308
1321
|
/(?:\borigin\b|\borigins\b)\s*:\s*(?:["']\*["']|\[\s*["']\*["'])/.test(source)) {
|
|
@@ -1314,6 +1327,14 @@ async function inspectProductionReadiness(targetDir, files, config, convention)
|
|
|
1314
1327
|
});
|
|
1315
1328
|
}
|
|
1316
1329
|
}
|
|
1330
|
+
if (files.includes(config.paths.server) && !hasSecurityHeadersHook) {
|
|
1331
|
+
diagnostics.push({
|
|
1332
|
+
severity: "warning",
|
|
1333
|
+
code: "BEIGNET_SECURITY_HEADERS_MISSING",
|
|
1334
|
+
file: config.paths.server,
|
|
1335
|
+
message: `${config.paths.server} does not install createSecurityHeadersHooks(...). Add the hook or equivalent platform-managed headers before exposing browser traffic.`,
|
|
1336
|
+
});
|
|
1337
|
+
}
|
|
1317
1338
|
for (const file of configFiles) {
|
|
1318
1339
|
const source = await readCachedSource(targetDir, file, sourceCache);
|
|
1319
1340
|
if (containsBetterAuthLocalSecret(source)) {
|
|
@@ -1354,7 +1375,7 @@ async function inspectProductionReadiness(targetDir, files, config, convention)
|
|
|
1354
1375
|
severity: "warning",
|
|
1355
1376
|
code: "BEIGNET_PROVIDER_TABLE_MISSING",
|
|
1356
1377
|
file: schemaDir,
|
|
1357
|
-
message: `${provider.packageName} is installed and ${variant.displayName} is registered, but required table ${tableName} was not found in ${schemaDir}/, drizzle/, or app database setup files. Add it with beignet db schema
|
|
1378
|
+
message: `${provider.packageName} is installed and ${variant.displayName} is registered, but required table ${tableName} was not found in ${schemaDir}/, drizzle/, or app database setup files. Add it with beignet db schema sync, include it in your Drizzle schema/migrations, or remove the provider registration.`,
|
|
1358
1379
|
});
|
|
1359
1380
|
}
|
|
1360
1381
|
}
|
|
@@ -1379,7 +1400,7 @@ async function inspectProductionReadiness(targetDir, files, config, convention)
|
|
|
1379
1400
|
severity: "warning",
|
|
1380
1401
|
code: "BEIGNET_PROVIDER_TABLE_MISSING",
|
|
1381
1402
|
file: schemaDir,
|
|
1382
|
-
message: `${provider.packageName} is installed, but required table ${tableName} was not found in ${schemaDir}/, drizzle/, or app database setup files. Add it with beignet db schema
|
|
1403
|
+
message: `${provider.packageName} is installed, but required table ${tableName} was not found in ${schemaDir}/, drizzle/, or app database setup files. Add it with beignet db schema sync, include it in your Drizzle schema/migrations, or remove the unused provider package.`,
|
|
1383
1404
|
});
|
|
1384
1405
|
}
|
|
1385
1406
|
}
|
|
@@ -2126,6 +2147,61 @@ async function inspectWorkflowRegistrationDrift(targetDir, files, config, conven
|
|
|
2126
2147
|
}
|
|
2127
2148
|
return diagnostics;
|
|
2128
2149
|
}
|
|
2150
|
+
async function inspectRuntimeManifestDrift(targetDir, files, config, convention) {
|
|
2151
|
+
if (!convention.resourceGenerator)
|
|
2152
|
+
return [];
|
|
2153
|
+
const manifest = await readRuntimeManifestIdentifiers(targetDir, files);
|
|
2154
|
+
if (!manifest)
|
|
2155
|
+
return [];
|
|
2156
|
+
const registries = await readFeatureWorkflowRegistries(targetDir, files, config);
|
|
2157
|
+
const diagnostics = [];
|
|
2158
|
+
const checks = [
|
|
2159
|
+
{
|
|
2160
|
+
kind: "listeners",
|
|
2161
|
+
registered: manifest.listeners,
|
|
2162
|
+
code: "BEIGNET_RUNTIME_MANIFEST_LISTENER_MISSING",
|
|
2163
|
+
label: "listeners",
|
|
2164
|
+
},
|
|
2165
|
+
{
|
|
2166
|
+
kind: "schedules",
|
|
2167
|
+
registered: manifest.schedules,
|
|
2168
|
+
code: "BEIGNET_RUNTIME_MANIFEST_SCHEDULE_MISSING",
|
|
2169
|
+
label: "schedules",
|
|
2170
|
+
},
|
|
2171
|
+
{
|
|
2172
|
+
kind: "tasks",
|
|
2173
|
+
registered: manifest.tasks,
|
|
2174
|
+
code: "BEIGNET_RUNTIME_MANIFEST_TASK_MISSING",
|
|
2175
|
+
label: "tasks",
|
|
2176
|
+
},
|
|
2177
|
+
{
|
|
2178
|
+
kind: "events",
|
|
2179
|
+
registered: manifest.events,
|
|
2180
|
+
code: "BEIGNET_RUNTIME_MANIFEST_OUTBOX_EVENT_MISSING",
|
|
2181
|
+
label: "outbox events",
|
|
2182
|
+
},
|
|
2183
|
+
{
|
|
2184
|
+
kind: "jobs",
|
|
2185
|
+
registered: manifest.jobs,
|
|
2186
|
+
code: "BEIGNET_RUNTIME_MANIFEST_OUTBOX_JOB_MISSING",
|
|
2187
|
+
label: "outbox jobs",
|
|
2188
|
+
},
|
|
2189
|
+
];
|
|
2190
|
+
for (const check of checks) {
|
|
2191
|
+
const missing = unregisteredWorkflowRegistries(registries.filter((registry) => registry.kind === check.kind), check.registered);
|
|
2192
|
+
for (const { registry, missingMembers } of missing) {
|
|
2193
|
+
for (const member of missingMembers) {
|
|
2194
|
+
diagnostics.push({
|
|
2195
|
+
severity: "warning",
|
|
2196
|
+
code: check.code,
|
|
2197
|
+
file: manifest.file,
|
|
2198
|
+
message: `${workflowArtifactOrigin(registry, member)}, but it is not listed in defineRuntimeManifest(...) ${check.label} in ${manifest.file}, so runtime integrity cannot fail fast if it is omitted from a runtime registry.`,
|
|
2199
|
+
});
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
return diagnostics;
|
|
2204
|
+
}
|
|
2129
2205
|
function workflowArtifactOrigin(registry, member) {
|
|
2130
2206
|
const memberFile = registry.memberFiles.get(member) ?? registry.indexFile;
|
|
2131
2207
|
if (memberFile === registry.indexFile) {
|
|
@@ -2192,7 +2268,7 @@ async function workflowRegistrationDrift(targetDir, files, config) {
|
|
|
2192
2268
|
}
|
|
2193
2269
|
const listenerRegistries = byKind("listeners");
|
|
2194
2270
|
if (listenerRegistries.length > 0) {
|
|
2195
|
-
const wiring = await listenerWiringReferences(targetDir, files);
|
|
2271
|
+
const wiring = await listenerWiringReferences(targetDir, files, config);
|
|
2196
2272
|
drift.listeners = {
|
|
2197
2273
|
unregistered: unregisteredWorkflowRegistries(listenerRegistries, wiring.identifiers),
|
|
2198
2274
|
wiringFile: wiring.wiringFile,
|
|
@@ -2271,6 +2347,71 @@ function exportedMemberFiles(source, indexFile, files) {
|
|
|
2271
2347
|
}
|
|
2272
2348
|
return memberFiles;
|
|
2273
2349
|
}
|
|
2350
|
+
async function readRuntimeManifestIdentifiers(targetDir, files) {
|
|
2351
|
+
for (const file of files) {
|
|
2352
|
+
if (!file.endsWith(".ts") && !file.endsWith(".tsx"))
|
|
2353
|
+
continue;
|
|
2354
|
+
if (file.endsWith(".d.ts") || isWorkflowTestFile(file))
|
|
2355
|
+
continue;
|
|
2356
|
+
const source = await readFile(path.join(targetDir, file), "utf8");
|
|
2357
|
+
if (!source.includes("defineRuntimeManifest"))
|
|
2358
|
+
continue;
|
|
2359
|
+
const identifiers = runtimeManifestIdentifiers(source);
|
|
2360
|
+
if (identifiers) {
|
|
2361
|
+
return {
|
|
2362
|
+
file,
|
|
2363
|
+
...identifiers,
|
|
2364
|
+
};
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
return undefined;
|
|
2368
|
+
}
|
|
2369
|
+
function runtimeManifestIdentifiers(source) {
|
|
2370
|
+
const call = /\bdefineRuntimeManifest\s*(?:<[^>]*>\s*)?\(/.exec(source);
|
|
2371
|
+
if (!call)
|
|
2372
|
+
return undefined;
|
|
2373
|
+
const arg = firstCallArgInfo(source, call.index + call[0].length);
|
|
2374
|
+
if (!arg)
|
|
2375
|
+
return undefined;
|
|
2376
|
+
const outbox = objectPropertyValueInfo(arg.text, "outbox");
|
|
2377
|
+
return {
|
|
2378
|
+
listeners: identifiersFromPropertyArray(arg.text, "listeners"),
|
|
2379
|
+
schedules: identifiersFromPropertyArray(arg.text, "schedules"),
|
|
2380
|
+
tasks: identifiersFromPropertyArray(arg.text, "tasks"),
|
|
2381
|
+
events: outbox
|
|
2382
|
+
? identifiersFromPropertyArray(outbox.text, "events")
|
|
2383
|
+
: new Set(),
|
|
2384
|
+
jobs: outbox
|
|
2385
|
+
? identifiersFromPropertyArray(outbox.text, "jobs")
|
|
2386
|
+
: new Set(),
|
|
2387
|
+
};
|
|
2388
|
+
}
|
|
2389
|
+
function identifiersFromPropertyArray(source, property) {
|
|
2390
|
+
const value = objectPropertyValueInfo(source, property);
|
|
2391
|
+
if (!value?.text.startsWith("["))
|
|
2392
|
+
return new Set();
|
|
2393
|
+
return identifiersFromArrayExpression(value.text);
|
|
2394
|
+
}
|
|
2395
|
+
function objectPropertyValueInfo(source, property) {
|
|
2396
|
+
const propertyMatch = new RegExp(`\\b${property}\\s*:`).exec(source);
|
|
2397
|
+
if (!propertyMatch)
|
|
2398
|
+
return undefined;
|
|
2399
|
+
let start = propertyMatch.index + propertyMatch[0].length;
|
|
2400
|
+
while (start < source.length && /\s/.test(source[start]))
|
|
2401
|
+
start++;
|
|
2402
|
+
const open = source[start];
|
|
2403
|
+
const close = open === "[" ? "]" : open === "{" ? "}" : undefined;
|
|
2404
|
+
if (!close)
|
|
2405
|
+
return undefined;
|
|
2406
|
+
const end = matchingDelimiterIndex(source, start, open, close);
|
|
2407
|
+
if (end === -1)
|
|
2408
|
+
return undefined;
|
|
2409
|
+
return {
|
|
2410
|
+
text: source.slice(start, end + 1),
|
|
2411
|
+
start,
|
|
2412
|
+
end: end + 1,
|
|
2413
|
+
};
|
|
2414
|
+
}
|
|
2274
2415
|
function registeredWorkflowIdentifiers(source, kind) {
|
|
2275
2416
|
if (kind === "tasks") {
|
|
2276
2417
|
const call = /\bdefineTasks\s*(?:<[^>]*>\s*)?\(/.exec(source);
|
|
@@ -2297,16 +2438,19 @@ async function listenedEventNames(targetDir, files, config) {
|
|
|
2297
2438
|
}
|
|
2298
2439
|
return listened;
|
|
2299
2440
|
}
|
|
2300
|
-
async function listenerWiringReferences(targetDir, files) {
|
|
2441
|
+
async function listenerWiringReferences(targetDir, files, config) {
|
|
2301
2442
|
const identifiers = new Set();
|
|
2302
2443
|
let wiringFile;
|
|
2303
2444
|
let eventBusFile;
|
|
2445
|
+
let centralListenerRegistryReferenced = false;
|
|
2446
|
+
const listenerRegistryFile = listenersFilePath(config);
|
|
2304
2447
|
for (const file of files) {
|
|
2305
2448
|
if (!file.endsWith(".ts") && !file.endsWith(".tsx"))
|
|
2306
2449
|
continue;
|
|
2307
2450
|
if (file.endsWith(".d.ts") || isWorkflowTestFile(file))
|
|
2308
2451
|
continue;
|
|
2309
2452
|
const source = await readFile(path.join(targetDir, file), "utf8");
|
|
2453
|
+
const namedImports = parseNamedImportSources(source);
|
|
2310
2454
|
let foundCall = false;
|
|
2311
2455
|
for (const match of source.matchAll(/\bregisterListeners\s*\(/g)) {
|
|
2312
2456
|
const openParen = (match.index ?? 0) + match[0].length - 1;
|
|
@@ -2315,9 +2459,19 @@ async function listenerWiringReferences(targetDir, files) {
|
|
|
2315
2459
|
? source.slice(openParen)
|
|
2316
2460
|
: source.slice(openParen + 1, closeParen);
|
|
2317
2461
|
foundCall = true;
|
|
2318
|
-
|
|
2462
|
+
const callIdentifiers = identifiersFromArrayExpression(argsText);
|
|
2463
|
+
for (const identifier of callIdentifiers) {
|
|
2319
2464
|
identifiers.add(identifier);
|
|
2320
2465
|
}
|
|
2466
|
+
if (callReferencesCentralListenerRegistry({
|
|
2467
|
+
callIdentifiers,
|
|
2468
|
+
namedImports,
|
|
2469
|
+
importerFile: file,
|
|
2470
|
+
listenerRegistryFile,
|
|
2471
|
+
files,
|
|
2472
|
+
})) {
|
|
2473
|
+
centralListenerRegistryReferenced = true;
|
|
2474
|
+
}
|
|
2321
2475
|
}
|
|
2322
2476
|
if (foundCall) {
|
|
2323
2477
|
wiringFile ??= file;
|
|
@@ -2326,8 +2480,33 @@ async function listenerWiringReferences(targetDir, files) {
|
|
|
2326
2480
|
eventBusFile = file;
|
|
2327
2481
|
}
|
|
2328
2482
|
}
|
|
2483
|
+
if (centralListenerRegistryReferenced &&
|
|
2484
|
+
files.includes(listenerRegistryFile)) {
|
|
2485
|
+
const source = await readFile(path.join(targetDir, listenerRegistryFile), "utf8");
|
|
2486
|
+
const central = arrayInitializerInfo(source, "listeners");
|
|
2487
|
+
if (central) {
|
|
2488
|
+
for (const identifier of identifiersFromArrayExpression(central.text)) {
|
|
2489
|
+
identifiers.add(identifier);
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2329
2493
|
return { identifiers, wiringFile, eventBusFile };
|
|
2330
2494
|
}
|
|
2495
|
+
function callReferencesCentralListenerRegistry(args) {
|
|
2496
|
+
if (args.importerFile === args.listenerRegistryFile &&
|
|
2497
|
+
args.callIdentifiers.has("listeners")) {
|
|
2498
|
+
return true;
|
|
2499
|
+
}
|
|
2500
|
+
for (const identifier of args.callIdentifiers) {
|
|
2501
|
+
const imported = args.namedImports.get(identifier);
|
|
2502
|
+
if (!imported || imported.importedName !== "listeners")
|
|
2503
|
+
continue;
|
|
2504
|
+
const importedFile = sourceFileFromImport(imported.sourcePath, args.importerFile, args.files);
|
|
2505
|
+
if (importedFile === args.listenerRegistryFile)
|
|
2506
|
+
return true;
|
|
2507
|
+
}
|
|
2508
|
+
return false;
|
|
2509
|
+
}
|
|
2331
2510
|
function isWorkflowTestFile(file) {
|
|
2332
2511
|
return (file.endsWith(".test.ts") ||
|
|
2333
2512
|
file.endsWith(".test.tsx") ||
|
|
@@ -2546,7 +2725,7 @@ async function inspectDatabaseLifecycleDrift(targetDir, files, config, conventio
|
|
|
2546
2725
|
message: `This app has Drizzle database artifacts, but package.json does not define "${script}". Add an app-owned ${script} script so beignet db ${script.replace("db:", "")} can run the database lifecycle command.`,
|
|
2547
2726
|
});
|
|
2548
2727
|
}
|
|
2549
|
-
diagnostics.push(...(await inspectDurableDrizzleTableDrift(targetDir, files, config, infrastructurePath, schemaDir, sourceCache)));
|
|
2728
|
+
diagnostics.push(...(await inspectDurableDrizzleTableDrift(targetDir, files, config, infrastructurePath, schemaDir, sourceCache)), ...(await inspectTenantScopeRepositoryDrift(targetDir, files, resources, config, infrastructurePath, schemaDir, sourceCache)));
|
|
2550
2729
|
}
|
|
2551
2730
|
const schemaFilePath = `${infrastructurePath}/db/schema.ts`;
|
|
2552
2731
|
if (files.includes(schemaFilePath)) {
|
|
@@ -2671,6 +2850,387 @@ async function inspectDatabaseLifecycleDrift(targetDir, files, config, conventio
|
|
|
2671
2850
|
}
|
|
2672
2851
|
return diagnostics;
|
|
2673
2852
|
}
|
|
2853
|
+
async function inspectTenantScopeRepositoryDrift(targetDir, files, resources, config, infrastructurePath, schemaDir, sourceCache) {
|
|
2854
|
+
const diagnostics = [];
|
|
2855
|
+
const schemaIndexFile = `${schemaDir}/index.ts`;
|
|
2856
|
+
const schemaFiles = files.filter((file) => file.startsWith(`${schemaDir}/`) &&
|
|
2857
|
+
file.endsWith(".ts") &&
|
|
2858
|
+
file !== schemaIndexFile);
|
|
2859
|
+
const tenantScopedTables = await collectTenantScopedDrizzleTables(targetDir, schemaFiles, sourceCache);
|
|
2860
|
+
const inspectedRepositories = new Set();
|
|
2861
|
+
for (const resource of resources) {
|
|
2862
|
+
const singular = singularize(resource);
|
|
2863
|
+
const singularPascal = pascalCase(singular);
|
|
2864
|
+
const pluralCamel = camelCase(resource);
|
|
2865
|
+
const schemaFile = `${schemaDir}/${resource}.ts`;
|
|
2866
|
+
const portFile = resourcePortFile(resource, singular, config);
|
|
2867
|
+
const drizzleRepositoryFile = drizzleRepositoryAdapterFile(files, `${infrastructurePath}/${resource}/`, singular);
|
|
2868
|
+
if (!drizzleRepositoryFile || !files.includes(schemaFile))
|
|
2869
|
+
continue;
|
|
2870
|
+
const schemaSource = await readCachedSource(targetDir, schemaFile, sourceCache);
|
|
2871
|
+
const schemaTenantColumns = tenantScopeColumnsFromSource(schemaSource);
|
|
2872
|
+
if (schemaTenantColumns.length === 0)
|
|
2873
|
+
continue;
|
|
2874
|
+
inspectedRepositories.add(drizzleRepositoryFile);
|
|
2875
|
+
if (files.includes(portFile)) {
|
|
2876
|
+
const portSource = await readCachedSource(targetDir, portFile, sourceCache);
|
|
2877
|
+
if (!/\b[A-Za-z_$][\w$]*\s*:\s*TenantScope\b/.test(portSource)) {
|
|
2878
|
+
diagnostics.push({
|
|
2879
|
+
severity: "warning",
|
|
2880
|
+
code: "BEIGNET_TENANT_SCOPE_PORT_MISSING",
|
|
2881
|
+
file: portFile,
|
|
2882
|
+
message: `${resource} has a tenant-scoped Drizzle schema (${schemaFile}), but ${portFile} does not accept TenantScope in its ${singularPascal}Repository methods. Tenant-owned repository methods should require a branded scope argument instead of raw tenant or workspace IDs.`,
|
|
2883
|
+
});
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
const repositorySource = await readCachedSource(targetDir, drizzleRepositoryFile, sourceCache);
|
|
2887
|
+
const tenantPredicate = `eq(schema.${pluralCamel}.tenantId, tenantScopeId(scope))`;
|
|
2888
|
+
const tenantPredicateCount = countSourceOccurrences(repositorySource, tenantPredicate);
|
|
2889
|
+
const setsTenantFromScope = repositorySource.includes("tenantId: tenantScopeId(scope)");
|
|
2890
|
+
if (!repositorySource.includes(`schema.${pluralCamel}`) ||
|
|
2891
|
+
(tenantPredicateCount >= 4 && setsTenantFromScope) ||
|
|
2892
|
+
repositoryHasScopedTenantCoverage(repositorySource, [
|
|
2893
|
+
{
|
|
2894
|
+
name: pluralCamel,
|
|
2895
|
+
file: schemaFile,
|
|
2896
|
+
columns: schemaTenantColumns,
|
|
2897
|
+
},
|
|
2898
|
+
])) {
|
|
2899
|
+
continue;
|
|
2900
|
+
}
|
|
2901
|
+
diagnostics.push({
|
|
2902
|
+
severity: "warning",
|
|
2903
|
+
code: "BEIGNET_TENANT_SCOPE_PREDICATE_MISSING",
|
|
2904
|
+
file: drizzleRepositoryFile,
|
|
2905
|
+
message: `${resource} has a tenant-scoped Drizzle schema (${schemaFile}), but ${drizzleRepositoryFile} does not use TenantScope consistently. Tenant-owned repository adapters should derive tenant or workspace IDs from tenantScopeId(scope) and include them in create values and read, update, and delete predicates.`,
|
|
2906
|
+
});
|
|
2907
|
+
}
|
|
2908
|
+
const repositoryFiles = files.filter((file) => file.startsWith(`${infrastructurePath}/`) &&
|
|
2909
|
+
file.includes("/drizzle-") &&
|
|
2910
|
+
file.endsWith("-repository.ts") &&
|
|
2911
|
+
!file.endsWith(".test.ts"));
|
|
2912
|
+
const featuresPath = directoryPath(config.paths.features);
|
|
2913
|
+
for (const repositoryFile of repositoryFiles) {
|
|
2914
|
+
if (inspectedRepositories.has(repositoryFile))
|
|
2915
|
+
continue;
|
|
2916
|
+
const repositorySource = await readCachedSource(targetDir, repositoryFile, sourceCache);
|
|
2917
|
+
const referencedTables = tenantScopedTables.filter((table) => repositorySource.includes(`schema.${table.name}`));
|
|
2918
|
+
if (referencedTables.length === 0)
|
|
2919
|
+
continue;
|
|
2920
|
+
const repositoryFeature = repositoryFeatureFromPath(repositoryFile, infrastructurePath);
|
|
2921
|
+
if (!repositoryFeature || isTenantAuthorityFeature(repositoryFeature)) {
|
|
2922
|
+
continue;
|
|
2923
|
+
}
|
|
2924
|
+
const portFile = `${featuresPath}/${repositoryFeature}/ports.ts`;
|
|
2925
|
+
if (!files.includes(portFile))
|
|
2926
|
+
continue;
|
|
2927
|
+
const portSource = await readCachedSource(targetDir, portFile, sourceCache);
|
|
2928
|
+
const repositoryBlocks = repositoryInterfaceBlocks(portSource);
|
|
2929
|
+
if (repositoryBlocks.length === 0)
|
|
2930
|
+
continue;
|
|
2931
|
+
const hasTenantScopeBoundary = repositoryBlocks.some((block) => repositoryInterfaceAcceptsTenantScope(block));
|
|
2932
|
+
const rawBoundaries = repositoryBlocks.flatMap((block) => rawTenantRepositoryMethodBoundaries(block, referencedTables));
|
|
2933
|
+
if (rawBoundaries.length > 0) {
|
|
2934
|
+
diagnostics.push({
|
|
2935
|
+
severity: "warning",
|
|
2936
|
+
code: "BEIGNET_TENANT_SCOPE_RAW_ID_BOUNDARY",
|
|
2937
|
+
file: portFile,
|
|
2938
|
+
message: `${portFile} declares tenant-owned repository methods (${rawBoundaries.join(", ")}) with raw tenant IDs. Use TenantScope at app-facing repository boundaries and unwrap it with tenantScopeId(scope) inside ${repositoryFile}; keep provider-correlation lookups separate from authorization.`,
|
|
2939
|
+
});
|
|
2940
|
+
continue;
|
|
2941
|
+
}
|
|
2942
|
+
if (hasTenantScopeBoundary &&
|
|
2943
|
+
!repositoryHasScopedTenantCoverage(repositorySource, referencedTables)) {
|
|
2944
|
+
diagnostics.push({
|
|
2945
|
+
severity: "warning",
|
|
2946
|
+
code: "BEIGNET_TENANT_SCOPE_PREDICATE_MISSING",
|
|
2947
|
+
file: repositoryFile,
|
|
2948
|
+
message: `${repositoryFile} adapts tenant-owned tables (${referencedTables
|
|
2949
|
+
.map((table) => table.name)
|
|
2950
|
+
.join(", ")}), but its scoped methods do not consistently derive tenant or workspace IDs from tenantScopeId(scope). Include that derived ID in create values and read, update, and delete predicates.`,
|
|
2951
|
+
});
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2954
|
+
return diagnostics;
|
|
2955
|
+
}
|
|
2956
|
+
const tenantScopeColumnDefinitions = [
|
|
2957
|
+
{
|
|
2958
|
+
property: "tenantId",
|
|
2959
|
+
sqlName: "tenant_id",
|
|
2960
|
+
label: "tenant",
|
|
2961
|
+
},
|
|
2962
|
+
{
|
|
2963
|
+
property: "workspaceId",
|
|
2964
|
+
sqlName: "workspace_id",
|
|
2965
|
+
label: "workspace",
|
|
2966
|
+
},
|
|
2967
|
+
];
|
|
2968
|
+
async function collectTenantScopedDrizzleTables(targetDir, schemaFiles, sourceCache) {
|
|
2969
|
+
const tables = [];
|
|
2970
|
+
for (const file of schemaFiles) {
|
|
2971
|
+
const source = await readCachedSource(targetDir, file, sourceCache);
|
|
2972
|
+
for (const table of drizzleTablesFromSource(source, file)) {
|
|
2973
|
+
if (table.columns.length > 0)
|
|
2974
|
+
tables.push(table);
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
2977
|
+
return tables;
|
|
2978
|
+
}
|
|
2979
|
+
function drizzleTablesFromSource(source, file) {
|
|
2980
|
+
const tables = [];
|
|
2981
|
+
const tablePattern = /\bexport\s+const\s+([A-Za-z_$][\w$]*)\s*=\s*(?:sqliteTable|pgTable|mysqlTable)\s*\(/g;
|
|
2982
|
+
for (const match of source.matchAll(tablePattern)) {
|
|
2983
|
+
const tableName = match[1];
|
|
2984
|
+
const openIndex = source.indexOf("(", match.index ?? 0);
|
|
2985
|
+
if (!tableName || openIndex === -1)
|
|
2986
|
+
continue;
|
|
2987
|
+
const closeIndex = matchingDelimiterIndex(source, openIndex, "(", ")");
|
|
2988
|
+
if (closeIndex === -1)
|
|
2989
|
+
continue;
|
|
2990
|
+
const tableSource = source.slice(openIndex, closeIndex + 1);
|
|
2991
|
+
const columns = tenantScopeColumnsFromSource(tableSource);
|
|
2992
|
+
tables.push({ name: tableName, file, columns });
|
|
2993
|
+
}
|
|
2994
|
+
return tables;
|
|
2995
|
+
}
|
|
2996
|
+
function tenantScopeColumnsFromSource(source) {
|
|
2997
|
+
return tenantScopeColumnDefinitions.filter((column) => {
|
|
2998
|
+
const propertyPattern = new RegExp(`\\b${column.property}\\s*:`);
|
|
2999
|
+
return (propertyPattern.test(source) ||
|
|
3000
|
+
source.includes(`"${column.sqlName}"`) ||
|
|
3001
|
+
source.includes(`'${column.sqlName}'`));
|
|
3002
|
+
});
|
|
3003
|
+
}
|
|
3004
|
+
function repositoryFeatureFromPath(repositoryFile, infrastructurePath) {
|
|
3005
|
+
const match = new RegExp(`^${escapeRegExp(infrastructurePath)}/([^/]+)/`).exec(repositoryFile);
|
|
3006
|
+
return match?.[1];
|
|
3007
|
+
}
|
|
3008
|
+
function isTenantAuthorityFeature(feature) {
|
|
3009
|
+
return feature === "workspaces";
|
|
3010
|
+
}
|
|
3011
|
+
function repositoryInterfaceBlocks(source) {
|
|
3012
|
+
const blocks = [];
|
|
3013
|
+
const interfacePattern = /\bexport\s+interface\s+[A-Za-z_$][\w$]*Repository\s*\{/g;
|
|
3014
|
+
for (const match of source.matchAll(interfacePattern)) {
|
|
3015
|
+
const openIndex = source.indexOf("{", match.index ?? 0);
|
|
3016
|
+
if (openIndex === -1)
|
|
3017
|
+
continue;
|
|
3018
|
+
const closeIndex = matchingDelimiterIndex(source, openIndex, "{", "}");
|
|
3019
|
+
if (closeIndex === -1)
|
|
3020
|
+
continue;
|
|
3021
|
+
blocks.push(source.slice(openIndex + 1, closeIndex));
|
|
3022
|
+
}
|
|
3023
|
+
return blocks;
|
|
3024
|
+
}
|
|
3025
|
+
function repositoryInterfaceAcceptsTenantScope(block) {
|
|
3026
|
+
return /\b[A-Za-z_$][\w$]*\s*:\s*TenantScope\b/.test(block);
|
|
3027
|
+
}
|
|
3028
|
+
function rawTenantRepositoryMethodBoundaries(block, tables) {
|
|
3029
|
+
const rawMethods = [];
|
|
3030
|
+
for (const method of repositoryMethodSignatures(block)) {
|
|
3031
|
+
if (isProviderCorrelationMethod(method.name))
|
|
3032
|
+
continue;
|
|
3033
|
+
const rawColumns = tenantColumnsUsedInSource(method.params, tables).filter((column) => column.property === "tenantId");
|
|
3034
|
+
if (rawColumns.length === 0)
|
|
3035
|
+
continue;
|
|
3036
|
+
rawMethods.push(method.name);
|
|
3037
|
+
}
|
|
3038
|
+
return uniqueStringValues(rawMethods);
|
|
3039
|
+
}
|
|
3040
|
+
function repositoryMethodSignatures(block) {
|
|
3041
|
+
const signatures = [];
|
|
3042
|
+
const methodPattern = /\b([A-Za-z_$][\w$]*)\s*\(/g;
|
|
3043
|
+
for (const match of block.matchAll(methodPattern)) {
|
|
3044
|
+
const name = match[1];
|
|
3045
|
+
const openIndex = block.indexOf("(", match.index ?? 0);
|
|
3046
|
+
if (!name || openIndex === -1)
|
|
3047
|
+
continue;
|
|
3048
|
+
const closeIndex = matchingDelimiterIndex(block, openIndex, "(", ")");
|
|
3049
|
+
if (closeIndex === -1)
|
|
3050
|
+
continue;
|
|
3051
|
+
const afterParams = block.slice(closeIndex + 1).trimStart();
|
|
3052
|
+
if (!afterParams.startsWith(":"))
|
|
3053
|
+
continue;
|
|
3054
|
+
signatures.push({
|
|
3055
|
+
name,
|
|
3056
|
+
params: block.slice(openIndex + 1, closeIndex),
|
|
3057
|
+
});
|
|
3058
|
+
}
|
|
3059
|
+
return signatures;
|
|
3060
|
+
}
|
|
3061
|
+
function isProviderCorrelationMethod(name) {
|
|
3062
|
+
return /(?:Customer|Provider|Subscription|Checkout|Event|Delivery|External|Token)[A-Za-z0-9]*(?:Id)?$/i.test(name);
|
|
3063
|
+
}
|
|
3064
|
+
function tenantColumnsUsedInSource(source, tables) {
|
|
3065
|
+
const columns = new Map();
|
|
3066
|
+
for (const table of tables) {
|
|
3067
|
+
for (const column of table.columns) {
|
|
3068
|
+
const propertyPattern = new RegExp(`\\b${column.property}\\s*:`);
|
|
3069
|
+
if (propertyPattern.test(source)) {
|
|
3070
|
+
columns.set(column.property, column);
|
|
3071
|
+
}
|
|
3072
|
+
}
|
|
3073
|
+
}
|
|
3074
|
+
return [...columns.values()];
|
|
3075
|
+
}
|
|
3076
|
+
function repositoryHasScopedTenantCoverage(source, tables) {
|
|
3077
|
+
if (!source.includes("tenantScopeId(scope)"))
|
|
3078
|
+
return false;
|
|
3079
|
+
const allFunctions = functionLikeBlocks(source);
|
|
3080
|
+
const scopedFunctions = allFunctions.filter((block) => /\bscope\b/.test(block.params));
|
|
3081
|
+
if (scopedFunctions.length === 0)
|
|
3082
|
+
return false;
|
|
3083
|
+
const functionsByName = new Map(allFunctions.map((block) => [block.name, block]));
|
|
3084
|
+
return scopedFunctions.every((block) => scopedFunctionUsesTenantScope(block, tables, functionsByName));
|
|
3085
|
+
}
|
|
3086
|
+
function functionLikeBlocks(source) {
|
|
3087
|
+
const blocks = [];
|
|
3088
|
+
const pattern = /(?:\basync\s+)?(?:function\s+)?([A-Za-z_$][\w$]*)\s*\(/g;
|
|
3089
|
+
for (const match of source.matchAll(pattern)) {
|
|
3090
|
+
const name = match[1];
|
|
3091
|
+
const openParamsIndex = source.indexOf("(", match.index ?? 0);
|
|
3092
|
+
if (!name || openParamsIndex === -1)
|
|
3093
|
+
continue;
|
|
3094
|
+
const closeParamsIndex = matchingDelimiterIndex(source, openParamsIndex, "(", ")");
|
|
3095
|
+
if (closeParamsIndex === -1)
|
|
3096
|
+
continue;
|
|
3097
|
+
const afterParams = source.slice(closeParamsIndex + 1);
|
|
3098
|
+
const bodyOpenOffset = afterParams.search(/\{/);
|
|
3099
|
+
if (bodyOpenOffset === -1)
|
|
3100
|
+
continue;
|
|
3101
|
+
const betweenParamsAndBody = afterParams.slice(0, bodyOpenOffset);
|
|
3102
|
+
if (!/^\s*(?::[^{;=]+)?\s*(?:=>\s*)?$/.test(betweenParamsAndBody) &&
|
|
3103
|
+
!/^\s*$/.test(betweenParamsAndBody)) {
|
|
3104
|
+
continue;
|
|
3105
|
+
}
|
|
3106
|
+
const openBodyIndex = closeParamsIndex + 1 + bodyOpenOffset;
|
|
3107
|
+
const closeBodyIndex = matchingDelimiterIndex(source, openBodyIndex, "{", "}");
|
|
3108
|
+
if (closeBodyIndex === -1)
|
|
3109
|
+
continue;
|
|
3110
|
+
blocks.push({
|
|
3111
|
+
name,
|
|
3112
|
+
params: source.slice(openParamsIndex + 1, closeParamsIndex),
|
|
3113
|
+
body: source.slice(openBodyIndex + 1, closeBodyIndex),
|
|
3114
|
+
});
|
|
3115
|
+
}
|
|
3116
|
+
return blocks;
|
|
3117
|
+
}
|
|
3118
|
+
function scopedFunctionUsesTenantScope(block, tables, functionsByName) {
|
|
3119
|
+
if (bodyUsesTenantScopePredicate(block.body, tables))
|
|
3120
|
+
return true;
|
|
3121
|
+
for (const variable of tenantScopeVariables(block.body)) {
|
|
3122
|
+
if (bodyUsesTenantScopePredicate(block.body, tables, variable))
|
|
3123
|
+
return true;
|
|
3124
|
+
for (const callee of functionsCalledWithVariable(block.body, variable)) {
|
|
3125
|
+
const helper = functionsByName.get(callee.name);
|
|
3126
|
+
if (helper &&
|
|
3127
|
+
helperUsesTenantVariable(helper, tables, callee.argumentIndex)) {
|
|
3128
|
+
return true;
|
|
3129
|
+
}
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
return false;
|
|
3133
|
+
}
|
|
3134
|
+
function tenantScopeVariables(body) {
|
|
3135
|
+
const variables = new Set(["tenantScopeId(scope)"]);
|
|
3136
|
+
const assignmentPattern = /\b(?:const|let)\s+([A-Za-z_$][\w$]*)\s*=\s*tenantScopeId\(scope\)/g;
|
|
3137
|
+
for (const match of body.matchAll(assignmentPattern)) {
|
|
3138
|
+
if (match[1])
|
|
3139
|
+
variables.add(match[1]);
|
|
3140
|
+
}
|
|
3141
|
+
return [...variables];
|
|
3142
|
+
}
|
|
3143
|
+
function bodyUsesTenantScopePredicate(body, tables, variable = "tenantScopeId(scope)") {
|
|
3144
|
+
for (const table of tables) {
|
|
3145
|
+
for (const column of table.columns) {
|
|
3146
|
+
if (body.includes(`eq(schema.${table.name}.${column.property}, ${variable})`)) {
|
|
3147
|
+
return true;
|
|
3148
|
+
}
|
|
3149
|
+
if (body.includes(`${column.property}: ${variable}`)) {
|
|
3150
|
+
return true;
|
|
3151
|
+
}
|
|
3152
|
+
if (variable === column.property &&
|
|
3153
|
+
new RegExp(`\\b${column.property}\\s*(?:,|})`).test(body)) {
|
|
3154
|
+
return true;
|
|
3155
|
+
}
|
|
3156
|
+
}
|
|
3157
|
+
}
|
|
3158
|
+
return false;
|
|
3159
|
+
}
|
|
3160
|
+
function functionsCalledWithVariable(body, variable) {
|
|
3161
|
+
if (variable.includes("("))
|
|
3162
|
+
return [];
|
|
3163
|
+
const calls = [];
|
|
3164
|
+
const callPattern = /\b([A-Za-z_$][\w$]*)\s*\(/g;
|
|
3165
|
+
for (const match of body.matchAll(callPattern)) {
|
|
3166
|
+
const name = match[1];
|
|
3167
|
+
const openIndex = body.indexOf("(", match.index ?? 0);
|
|
3168
|
+
if (!name || openIndex === -1)
|
|
3169
|
+
continue;
|
|
3170
|
+
const closeIndex = matchingDelimiterIndex(body, openIndex, "(", ")");
|
|
3171
|
+
if (closeIndex === -1)
|
|
3172
|
+
continue;
|
|
3173
|
+
const args = splitTopLevelArguments(body.slice(openIndex + 1, closeIndex));
|
|
3174
|
+
const argumentIndex = args.findIndex((argument) => argument.trim() === variable);
|
|
3175
|
+
if (argumentIndex !== -1) {
|
|
3176
|
+
calls.push({ name, argumentIndex });
|
|
3177
|
+
}
|
|
3178
|
+
}
|
|
3179
|
+
return calls;
|
|
3180
|
+
}
|
|
3181
|
+
function helperUsesTenantVariable(helper, tables, argumentIndex) {
|
|
3182
|
+
const params = splitTopLevelArguments(helper.params);
|
|
3183
|
+
const parameter = params[argumentIndex]?.trim();
|
|
3184
|
+
if (!parameter)
|
|
3185
|
+
return false;
|
|
3186
|
+
const parameterName = /^([A-Za-z_$][\w$]*)/.exec(parameter)?.[1];
|
|
3187
|
+
return parameterName
|
|
3188
|
+
? bodyUsesTenantScopePredicate(helper.body, tables, parameterName)
|
|
3189
|
+
: false;
|
|
3190
|
+
}
|
|
3191
|
+
function splitTopLevelArguments(source) {
|
|
3192
|
+
const args = [];
|
|
3193
|
+
let start = 0;
|
|
3194
|
+
let depth = 0;
|
|
3195
|
+
let inString;
|
|
3196
|
+
let escaped = false;
|
|
3197
|
+
for (let index = 0; index < source.length; index++) {
|
|
3198
|
+
const char = source[index];
|
|
3199
|
+
if (inString) {
|
|
3200
|
+
if (escaped) {
|
|
3201
|
+
escaped = false;
|
|
3202
|
+
}
|
|
3203
|
+
else if (char === "\\") {
|
|
3204
|
+
escaped = true;
|
|
3205
|
+
}
|
|
3206
|
+
else if (char === inString) {
|
|
3207
|
+
inString = undefined;
|
|
3208
|
+
}
|
|
3209
|
+
continue;
|
|
3210
|
+
}
|
|
3211
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
3212
|
+
inString = char;
|
|
3213
|
+
continue;
|
|
3214
|
+
}
|
|
3215
|
+
if (char === "(" || char === "{" || char === "[") {
|
|
3216
|
+
depth += 1;
|
|
3217
|
+
continue;
|
|
3218
|
+
}
|
|
3219
|
+
if (char === ")" || char === "}" || char === "]") {
|
|
3220
|
+
depth -= 1;
|
|
3221
|
+
continue;
|
|
3222
|
+
}
|
|
3223
|
+
if (char === "," && depth === 0) {
|
|
3224
|
+
args.push(source.slice(start, index));
|
|
3225
|
+
start = index + 1;
|
|
3226
|
+
}
|
|
3227
|
+
}
|
|
3228
|
+
args.push(source.slice(start));
|
|
3229
|
+
return args;
|
|
3230
|
+
}
|
|
3231
|
+
function uniqueStringValues(values) {
|
|
3232
|
+
return [...new Set(values)];
|
|
3233
|
+
}
|
|
2674
3234
|
const durableDrizzleTableRequirements = [
|
|
2675
3235
|
{
|
|
2676
3236
|
capability: "idempotency",
|
|
@@ -2695,15 +3255,18 @@ const durableDrizzleTableRequirements = [
|
|
|
2695
3255
|
code: "BEIGNET_OUTBOX_TABLE_MISSING",
|
|
2696
3256
|
factoryNames: [
|
|
2697
3257
|
"createDrizzleSqliteOutboxPort",
|
|
3258
|
+
"createDrizzleSqliteOutboxAdminPort",
|
|
2698
3259
|
"createDrizzlePostgresOutboxPort",
|
|
3260
|
+
"createDrizzlePostgresOutboxAdminPort",
|
|
2699
3261
|
"createDrizzleMysqlOutboxPort",
|
|
3262
|
+
"createDrizzleMysqlOutboxAdminPort",
|
|
2700
3263
|
],
|
|
2701
3264
|
setupNames: [
|
|
2702
3265
|
"createDrizzleSqliteOutboxSetupStatements",
|
|
2703
3266
|
"createDrizzlePostgresOutboxSetupStatements",
|
|
2704
3267
|
"createDrizzleMysqlOutboxSetupStatements",
|
|
2705
3268
|
],
|
|
2706
|
-
explicitFactoryPattern: "createDrizzle[A-Za-z]*Outbox(?:Port)?",
|
|
3269
|
+
explicitFactoryPattern: "createDrizzle[A-Za-z]*Outbox(?:Admin)?(?:Port)?",
|
|
2707
3270
|
explicitSetupPattern: "createDrizzle[A-Za-z]*OutboxSetupStatements",
|
|
2708
3271
|
},
|
|
2709
3272
|
{
|
|
@@ -2925,18 +3488,52 @@ async function inspectPackageScripts(targetDir, files, convention) {
|
|
|
2925
3488
|
return [];
|
|
2926
3489
|
}
|
|
2927
3490
|
const packageJson = JSON.parse(await readFile(path.join(targetDir, "package.json"), "utf8"));
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
3491
|
+
const diagnostics = [];
|
|
3492
|
+
const scripts = packageJson.scripts ?? {};
|
|
3493
|
+
const hasDirectBunTestImports = await appUsesDirectBunTestImports(targetDir, files);
|
|
3494
|
+
if (!scripts.test) {
|
|
3495
|
+
diagnostics.push({
|
|
2932
3496
|
severity: "warning",
|
|
2933
3497
|
code: "BEIGNET_PACKAGE_TEST_SCRIPT_MISSING",
|
|
2934
3498
|
file: "package.json",
|
|
2935
|
-
message: 'package.json does not define a test script. Add "test": "
|
|
2936
|
-
}
|
|
2937
|
-
|
|
3499
|
+
message: 'package.json does not define a test script. Add "test": "tsx lib/beignet-test-runner.ts" or run beignet doctor --fix.',
|
|
3500
|
+
});
|
|
3501
|
+
}
|
|
3502
|
+
const legacyScripts = hasDirectBunTestImports
|
|
3503
|
+
? []
|
|
3504
|
+
: legacyGeneratedScriptNames(scripts);
|
|
3505
|
+
if (legacyScripts.length > 0) {
|
|
3506
|
+
diagnostics.push({
|
|
3507
|
+
severity: "warning",
|
|
3508
|
+
code: "BEIGNET_PACKAGE_SCRIPT_LEGACY",
|
|
3509
|
+
file: "package.json",
|
|
3510
|
+
message: `package.json still uses legacy generated Bun scripts for ${legacyScripts.join(", ")}. Run beignet doctor --fix to migrate them to package-manager-neutral tsx scripts.`,
|
|
3511
|
+
});
|
|
3512
|
+
}
|
|
3513
|
+
const needsTsx = hasCurrentGeneratedTsxScript(scripts) &&
|
|
3514
|
+
!packageJson.dependencies?.tsx &&
|
|
3515
|
+
!packageJson.devDependencies?.tsx;
|
|
3516
|
+
if (needsTsx) {
|
|
3517
|
+
diagnostics.push({
|
|
3518
|
+
severity: "warning",
|
|
3519
|
+
code: "BEIGNET_PACKAGE_TSX_DEPENDENCY_MISSING",
|
|
3520
|
+
file: "package.json",
|
|
3521
|
+
message: 'package.json uses generated tsx scripts but does not depend on tsx. Add "tsx" to devDependencies or run beignet doctor --fix.',
|
|
3522
|
+
});
|
|
3523
|
+
}
|
|
3524
|
+
const missingSupportFiles = missingTestSupportFiles(files);
|
|
3525
|
+
if (scripts.test === currentGeneratedPackageScripts.test &&
|
|
3526
|
+
missingSupportFiles.length > 0) {
|
|
3527
|
+
diagnostics.push({
|
|
3528
|
+
severity: "warning",
|
|
3529
|
+
code: "BEIGNET_PACKAGE_TEST_SUPPORT_MISSING",
|
|
3530
|
+
file: "package.json",
|
|
3531
|
+
message: `package.json uses the generated test runner, but ${missingSupportFiles.join(", ")} ${missingSupportFiles.length === 1 ? "is" : "are"} missing. Run beignet doctor --fix to restore test support files.`,
|
|
3532
|
+
});
|
|
3533
|
+
}
|
|
3534
|
+
return diagnostics;
|
|
2938
3535
|
}
|
|
2939
|
-
async function
|
|
3536
|
+
async function fixPackageScripts(targetDir, files, convention) {
|
|
2940
3537
|
if (!convention.resourceGenerator || !files.includes("package.json")) {
|
|
2941
3538
|
return undefined;
|
|
2942
3539
|
}
|
|
@@ -2944,18 +3541,118 @@ async function fixMissingTestScript(targetDir, files, convention) {
|
|
|
2944
3541
|
const original = await readFile(filePath, "utf8");
|
|
2945
3542
|
const packageJson = JSON.parse(original);
|
|
2946
3543
|
packageJson.scripts = packageJson.scripts ?? {};
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
3544
|
+
const updatedScripts = [];
|
|
3545
|
+
const addedTestScript = !packageJson.scripts.test;
|
|
3546
|
+
const hasDirectBunTestImports = await appUsesDirectBunTestImports(targetDir, files);
|
|
3547
|
+
if (addedTestScript) {
|
|
3548
|
+
packageJson.scripts.test = currentGeneratedPackageScripts.test;
|
|
3549
|
+
updatedScripts.push("test");
|
|
3550
|
+
}
|
|
3551
|
+
if (!hasDirectBunTestImports) {
|
|
3552
|
+
for (const [name, legacyCommand] of Object.entries(legacyGeneratedPackageScripts)) {
|
|
3553
|
+
if (packageJson.scripts[name] === legacyCommand) {
|
|
3554
|
+
packageJson.scripts[name] = currentGeneratedPackageScripts[name];
|
|
3555
|
+
if (!updatedScripts.includes(name))
|
|
3556
|
+
updatedScripts.push(name);
|
|
3557
|
+
}
|
|
3558
|
+
}
|
|
3559
|
+
}
|
|
3560
|
+
const needsTsx = hasCurrentGeneratedTsxScript(packageJson.scripts);
|
|
3561
|
+
const addedTsx = needsTsx &&
|
|
3562
|
+
!packageJson.dependencies?.tsx &&
|
|
3563
|
+
!packageJson.devDependencies?.tsx;
|
|
3564
|
+
if (addedTsx) {
|
|
3565
|
+
packageJson.devDependencies = packageJson.devDependencies ?? {};
|
|
3566
|
+
packageJson.devDependencies.tsx = externalVersions.tsx;
|
|
3567
|
+
}
|
|
2950
3568
|
const next = `${JSON.stringify(packageJson, null, "\t")}\n`;
|
|
2951
|
-
|
|
3569
|
+
const createdSupportFiles = await createMissingTestSupportFiles(targetDir, files, packageJson.scripts.test === currentGeneratedPackageScripts.test);
|
|
3570
|
+
if (next === original && createdSupportFiles.length === 0)
|
|
2952
3571
|
return undefined;
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
3572
|
+
if (next !== original)
|
|
3573
|
+
await writeFile(filePath, next);
|
|
3574
|
+
if (addedTestScript) {
|
|
3575
|
+
return {
|
|
3576
|
+
code: "BEIGNET_PACKAGE_TEST_SCRIPT_MISSING",
|
|
3577
|
+
file: "package.json",
|
|
3578
|
+
message: createdSupportFiles.length > 0
|
|
3579
|
+
? `Added "test": "tsx lib/beignet-test-runner.ts" and ${createdSupportFiles.join(", ")}.`
|
|
3580
|
+
: 'Added "test": "tsx lib/beignet-test-runner.ts".',
|
|
3581
|
+
};
|
|
3582
|
+
}
|
|
3583
|
+
if (updatedScripts.length > 0) {
|
|
3584
|
+
return {
|
|
3585
|
+
code: "BEIGNET_PACKAGE_SCRIPT_LEGACY",
|
|
3586
|
+
file: "package.json",
|
|
3587
|
+
message: packageScriptFixMessage(updatedScripts, createdSupportFiles, addedTsx),
|
|
3588
|
+
};
|
|
3589
|
+
}
|
|
3590
|
+
if (createdSupportFiles.length > 0) {
|
|
3591
|
+
return {
|
|
3592
|
+
code: "BEIGNET_PACKAGE_TEST_SUPPORT_MISSING",
|
|
3593
|
+
file: "package.json",
|
|
3594
|
+
message: `Restored ${createdSupportFiles.join(", ")}.`,
|
|
3595
|
+
};
|
|
3596
|
+
}
|
|
3597
|
+
if (addedTsx) {
|
|
3598
|
+
return {
|
|
3599
|
+
code: "BEIGNET_PACKAGE_TSX_DEPENDENCY_MISSING",
|
|
3600
|
+
file: "package.json",
|
|
3601
|
+
message: 'Added "tsx" to devDependencies.',
|
|
3602
|
+
};
|
|
3603
|
+
}
|
|
3604
|
+
return undefined;
|
|
3605
|
+
}
|
|
3606
|
+
function legacyGeneratedScriptNames(scripts) {
|
|
3607
|
+
return Object.entries(legacyGeneratedPackageScripts)
|
|
3608
|
+
.filter(([name, command]) => scripts[name] === command)
|
|
3609
|
+
.map(([name]) => name);
|
|
3610
|
+
}
|
|
3611
|
+
async function appUsesDirectBunTestImports(targetDir, files) {
|
|
3612
|
+
for (const file of files) {
|
|
3613
|
+
if (!isTestSourceFile(file))
|
|
3614
|
+
continue;
|
|
3615
|
+
const source = await readFile(path.join(targetDir, file), "utf8");
|
|
3616
|
+
if (/\bfrom\s*["']bun:test["']/.test(source))
|
|
3617
|
+
return true;
|
|
3618
|
+
if (/\bimport\s*\(\s*["']bun:test["']\s*\)/.test(source))
|
|
3619
|
+
return true;
|
|
3620
|
+
}
|
|
3621
|
+
return false;
|
|
3622
|
+
}
|
|
3623
|
+
function hasCurrentGeneratedTsxScript(scripts) {
|
|
3624
|
+
return Object.values(currentGeneratedPackageScripts).some((command) => command.startsWith("tsx ") && Object.values(scripts).includes(command));
|
|
3625
|
+
}
|
|
3626
|
+
function missingTestSupportFiles(files) {
|
|
3627
|
+
return testSupportTemplateFiles
|
|
3628
|
+
.map((file) => file.path)
|
|
3629
|
+
.filter((file) => !files.includes(file));
|
|
3630
|
+
}
|
|
3631
|
+
async function createMissingTestSupportFiles(targetDir, files, shouldCreate) {
|
|
3632
|
+
if (!shouldCreate)
|
|
3633
|
+
return [];
|
|
3634
|
+
const createdSupportFiles = [];
|
|
3635
|
+
for (const file of testSupportTemplateFiles) {
|
|
3636
|
+
if (files.includes(file.path))
|
|
3637
|
+
continue;
|
|
3638
|
+
const destination = path.join(targetDir, file.path);
|
|
3639
|
+
await mkdir(path.dirname(destination), { recursive: true });
|
|
3640
|
+
await writeFile(destination, file.content);
|
|
3641
|
+
createdSupportFiles.push(file.path);
|
|
3642
|
+
}
|
|
3643
|
+
return createdSupportFiles;
|
|
3644
|
+
}
|
|
3645
|
+
function packageScriptFixMessage(updatedScripts, createdSupportFiles, addedTsx) {
|
|
3646
|
+
const parts = [
|
|
3647
|
+
`Updated generated package scripts: ${updatedScripts.join(", ")}.`,
|
|
3648
|
+
];
|
|
3649
|
+
if (createdSupportFiles.length > 0) {
|
|
3650
|
+
parts.push(`Restored ${createdSupportFiles.join(", ")}.`);
|
|
3651
|
+
}
|
|
3652
|
+
if (addedTsx) {
|
|
3653
|
+
parts.push('Added "tsx" to devDependencies.');
|
|
3654
|
+
}
|
|
3655
|
+
return parts.join(" ");
|
|
2959
3656
|
}
|
|
2960
3657
|
function unmatchedRouteDiagnostic(routeHandler) {
|
|
2961
3658
|
if (routeHandler.source === "next-route") {
|
|
@@ -3394,14 +4091,15 @@ async function fixUnregisteredTasks(targetDir, files, drift, config) {
|
|
|
3394
4091
|
});
|
|
3395
4092
|
}
|
|
3396
4093
|
/**
|
|
3397
|
-
* Wire fully unregistered feature listener registries into the
|
|
3398
|
-
*
|
|
3399
|
-
* generates, so both
|
|
4094
|
+
* Wire fully unregistered feature listener registries into the central
|
|
4095
|
+
* listener registry and ensure the providers file registers that central
|
|
4096
|
+
* registry with the same block `beignet make listener` generates, so both
|
|
4097
|
+
* write paths stay byte-compatible.
|
|
3400
4098
|
*
|
|
3401
|
-
* Bails without writing when the providers file or
|
|
3402
|
-
*
|
|
3403
|
-
* against, or when the registry name is already imported from another
|
|
3404
|
-
* The diagnostic stays in every bail case.
|
|
4099
|
+
* Bails without writing when the providers file or central listener registry
|
|
4100
|
+
* is customized beyond the known anchors, when the app has no eventBus port to
|
|
4101
|
+
* register against, or when the registry name is already imported from another
|
|
4102
|
+
* file. The diagnostic stays in every bail case.
|
|
3405
4103
|
*/
|
|
3406
4104
|
async function fixUnregisteredListeners(targetDir, files, drift, config) {
|
|
3407
4105
|
const candidates = drift.listeners.unregistered.filter((entry) => entry.fullyUnregistered);
|
|
@@ -3424,41 +4122,72 @@ async function fixUnregisteredListeners(targetDir, files, drift, config) {
|
|
|
3424
4122
|
if (!/\beventBus\s*[:?]/.test(portsSource))
|
|
3425
4123
|
return undefined;
|
|
3426
4124
|
const providersPath = path.join(targetDir, providersFile);
|
|
3427
|
-
const
|
|
3428
|
-
|
|
3429
|
-
const
|
|
4125
|
+
const originalProviders = await readFile(providersPath, "utf8");
|
|
4126
|
+
const listenersFile = listenersFilePath(config);
|
|
4127
|
+
const listenersPath = path.join(targetDir, listenersFile);
|
|
4128
|
+
const originalListeners = files.includes(listenersFile)
|
|
4129
|
+
? await readFile(listenersPath, "utf8")
|
|
4130
|
+
: undefined;
|
|
4131
|
+
let nextListeners = originalListeners;
|
|
4132
|
+
const candidateNames = [];
|
|
3430
4133
|
for (const { registry } of candidates) {
|
|
3431
|
-
|
|
4134
|
+
candidateNames.push(registry.registryName);
|
|
4135
|
+
const imported = nextListeners === undefined
|
|
4136
|
+
? undefined
|
|
4137
|
+
: parseNamedImportSources(nextListeners).get(registry.registryName);
|
|
3432
4138
|
if (imported) {
|
|
3433
|
-
const importedFile = sourceFileFromImport(imported.sourcePath,
|
|
4139
|
+
const importedFile = sourceFileFromImport(imported.sourcePath, listenersFile, files);
|
|
3434
4140
|
if (importedFile !== registry.indexFile)
|
|
3435
4141
|
return undefined;
|
|
3436
4142
|
}
|
|
3437
|
-
const
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
4143
|
+
const listenerModule = `@/${modulePath(registry.indexFile)}`;
|
|
4144
|
+
const result = nextListeners === undefined
|
|
4145
|
+
? {
|
|
4146
|
+
kind: "updated",
|
|
4147
|
+
source: createListenersRegistrySource({
|
|
4148
|
+
registryName: registry.registryName,
|
|
4149
|
+
listenerModule,
|
|
4150
|
+
}),
|
|
4151
|
+
}
|
|
4152
|
+
: updateListenersRegistrySource(nextListeners, {
|
|
4153
|
+
registryName: registry.registryName,
|
|
4154
|
+
listenerModule,
|
|
4155
|
+
});
|
|
3443
4156
|
if (result.kind === "missing")
|
|
3444
4157
|
return undefined;
|
|
3445
4158
|
if (result.kind === "updated") {
|
|
3446
|
-
|
|
3447
|
-
registeredNames.push(registry.registryName);
|
|
4159
|
+
nextListeners = result.source;
|
|
3448
4160
|
}
|
|
3449
4161
|
}
|
|
3450
|
-
if (
|
|
4162
|
+
if (nextListeners === undefined || candidateNames.length === 0) {
|
|
3451
4163
|
return undefined;
|
|
3452
|
-
|
|
4164
|
+
}
|
|
4165
|
+
const providerResult = wireListenersProviderSource(originalProviders, {
|
|
4166
|
+
registryName: candidateNames[0] ?? "listeners",
|
|
4167
|
+
featureKebab: "app",
|
|
4168
|
+
listenerModule: `@/${modulePath(listenersFile)}`,
|
|
4169
|
+
config,
|
|
4170
|
+
});
|
|
4171
|
+
if (providerResult.kind === "missing")
|
|
4172
|
+
return undefined;
|
|
4173
|
+
const nextProviders = providerResult.kind === "updated"
|
|
4174
|
+
? providerResult.source
|
|
4175
|
+
: originalProviders;
|
|
4176
|
+
if (nextListeners === originalListeners &&
|
|
4177
|
+
nextProviders === originalProviders) {
|
|
4178
|
+
return undefined;
|
|
4179
|
+
}
|
|
4180
|
+
await mkdir(path.dirname(listenersPath), { recursive: true });
|
|
4181
|
+
await writeFile(listenersPath, nextListeners);
|
|
4182
|
+
if (nextProviders !== originalProviders) {
|
|
4183
|
+
await writeFile(providersPath, nextProviders);
|
|
4184
|
+
}
|
|
3453
4185
|
return {
|
|
3454
4186
|
code: "BEIGNET_LISTENER_UNREGISTERED",
|
|
3455
|
-
file:
|
|
3456
|
-
message: `Registered ${
|
|
4187
|
+
file: listenersFile,
|
|
4188
|
+
message: `Registered ${candidateNames.join(", ")} in ${listenersFile} and wired listeners with registerListeners(...) in ${providersFile}.`,
|
|
3457
4189
|
};
|
|
3458
4190
|
}
|
|
3459
|
-
function listenerFeatureKebab(indexFile) {
|
|
3460
|
-
return path.posix.basename(path.posix.dirname(path.posix.dirname(normalizePath(indexFile))));
|
|
3461
|
-
}
|
|
3462
4191
|
async function fixUnregisteredOutboxEntries(targetDir, files, drift, config) {
|
|
3463
4192
|
if (!drift.outbox.exists)
|
|
3464
4193
|
return [];
|
|
@@ -3869,7 +4598,9 @@ async function inspectResourceSlices(targetDir, files, config, convention, stric
|
|
|
3869
4598
|
sourceIncludesAny(portSource, [
|
|
3870
4599
|
`update(input: Update${singularPascal}Input)`,
|
|
3871
4600
|
`update(input: Update${singularPascal}RepositoryInput)`,
|
|
4601
|
+
`update(input: Update${singularPascal}Input, scope: TenantScope)`,
|
|
3872
4602
|
"delete(id: string): Promise<boolean>",
|
|
4603
|
+
"delete(id: string, scope: TenantScope): Promise<boolean>",
|
|
3873
4604
|
]);
|
|
3874
4605
|
if (!hasCrudSignals)
|
|
3875
4606
|
continue;
|
|
@@ -3904,6 +4635,7 @@ async function inspectResourceSlices(targetDir, files, config, convention, stric
|
|
|
3904
4635
|
alternatives: [
|
|
3905
4636
|
`findById(id: string): Promise<${singularPascal} | null>`,
|
|
3906
4637
|
`findById(id: string, filter: ${singularPascal}TenantFilter): Promise<${singularPascal} | null>`,
|
|
4638
|
+
`findById(id: string, scope: TenantScope): Promise<${singularPascal} | null>`,
|
|
3907
4639
|
],
|
|
3908
4640
|
},
|
|
3909
4641
|
{
|
|
@@ -3911,6 +4643,7 @@ async function inspectResourceSlices(targetDir, files, config, convention, stric
|
|
|
3911
4643
|
alternatives: [
|
|
3912
4644
|
`update(input: Update${singularPascal}Input)`,
|
|
3913
4645
|
`update(input: Update${singularPascal}RepositoryInput)`,
|
|
4646
|
+
`update(input: Update${singularPascal}Input, scope: TenantScope)`,
|
|
3914
4647
|
],
|
|
3915
4648
|
},
|
|
3916
4649
|
{
|
|
@@ -3918,6 +4651,7 @@ async function inspectResourceSlices(targetDir, files, config, convention, stric
|
|
|
3918
4651
|
alternatives: [
|
|
3919
4652
|
"delete(id: string): Promise<boolean>",
|
|
3920
4653
|
`delete(id: string, filter: ${singularPascal}TenantFilter): Promise<boolean>`,
|
|
4654
|
+
"delete(id: string, scope: TenantScope): Promise<boolean>",
|
|
3921
4655
|
],
|
|
3922
4656
|
},
|
|
3923
4657
|
];
|
|
@@ -4032,6 +4766,15 @@ async function readSourceIfPresent(targetDir, files, file) {
|
|
|
4032
4766
|
function sourceIncludesAny(source, snippets) {
|
|
4033
4767
|
return source ? snippets.some((snippet) => source.includes(snippet)) : false;
|
|
4034
4768
|
}
|
|
4769
|
+
function countSourceOccurrences(source, snippet) {
|
|
4770
|
+
let count = 0;
|
|
4771
|
+
let index = source.indexOf(snippet);
|
|
4772
|
+
while (index !== -1) {
|
|
4773
|
+
count += 1;
|
|
4774
|
+
index = source.indexOf(snippet, index + snippet.length);
|
|
4775
|
+
}
|
|
4776
|
+
return count;
|
|
4777
|
+
}
|
|
4035
4778
|
function sourceIncludesAll(source, snippets) {
|
|
4036
4779
|
return snippets.every((snippet) => source.includes(snippet));
|
|
4037
4780
|
}
|