@beignet/cli 0.0.32 → 0.0.34
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 +60 -0
- package/README.md +29 -19
- package/dist/choices.d.ts +3 -3
- package/dist/choices.d.ts.map +1 -1
- package/dist/choices.js +2 -0
- package/dist/choices.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/db.js +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +279 -1
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +800 -60
- package/dist/inspect.js.map +1 -1
- package/dist/make/inbox.d.ts.map +1 -1
- package/dist/make/inbox.js +8 -6
- package/dist/make/inbox.js.map +1 -1
- package/dist/make/payments.d.ts.map +1 -1
- package/dist/make/payments.js +61 -32
- package/dist/make/payments.js.map +1 -1
- package/dist/make/shared.d.ts +11 -2
- package/dist/make/shared.d.ts.map +1 -1
- package/dist/make/shared.js +45 -11
- package/dist/make/shared.js.map +1 -1
- package/dist/make/tenancy.d.ts.map +1 -1
- package/dist/make/tenancy.js +9 -7
- package/dist/make/tenancy.js.map +1 -1
- package/dist/make.d.ts.map +1 -1
- package/dist/make.js +134 -60
- 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.js +1 -1
- package/dist/preflight.js.map +1 -1
- package/dist/provider-add.d.ts.map +1 -1
- package/dist/provider-add.js +91 -62
- package/dist/provider-add.js.map +1 -1
- package/dist/registry-edits.js +2 -1
- package/dist/registry-edits.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.js +6 -6
- 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 +17 -10
- package/dist/templates/server.js.map +1 -1
- package/dist/templates/shared.d.ts +3 -1
- package/dist/templates/shared.d.ts.map +1 -1
- package/dist/templates/shared.js +11 -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 +4 -4
- package/package.json +3 -2
- package/skills/app-structure/SKILL.md +14 -4
- package/src/choices.ts +3 -0
- package/src/config.ts +2 -0
- package/src/db.ts +1 -1
- package/src/index.ts +437 -1
- package/src/inspect.ts +1248 -156
- package/src/make/inbox.ts +8 -6
- package/src/make/payments.ts +61 -32
- package/src/make/shared.ts +79 -12
- package/src/make/tenancy.ts +9 -7
- package/src/make.ts +177 -59
- package/src/outbox.ts +363 -0
- package/src/preflight.ts +1 -1
- package/src/provider-add.ts +92 -62
- package/src/registry-edits.ts +2 -1
- package/src/task.ts +2 -3
- package/src/templates/agents.ts +6 -3
- package/src/templates/base.ts +6 -6
- package/src/templates/index.ts +2 -0
- package/src/templates/server.ts +17 -10
- package/src/templates/shared.ts +13 -1
- package/src/templates/testing.ts +545 -0
- package/src/templates/todos.ts +4 -4
- 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);
|
|
@@ -167,6 +169,7 @@ export const productionHardeningDiagnosticCodes = new Set([
|
|
|
167
169
|
"BEIGNET_PROVIDER_ENV_MISSING",
|
|
168
170
|
"BEIGNET_PROVIDER_TABLE_MISSING",
|
|
169
171
|
"BEIGNET_READINESS_ROUTE_MISSING",
|
|
172
|
+
"BEIGNET_SECURITY_HEADERS_MISSING",
|
|
170
173
|
"BEIGNET_TENANT_HEADER_AUTHORITY",
|
|
171
174
|
"BEIGNET_UPLOAD_AUTHORIZATION_MISSING",
|
|
172
175
|
"BEIGNET_UPLOAD_SIZE_LIMIT_MISSING",
|
|
@@ -179,7 +182,7 @@ function productionHardeningChecklist(diagnostics) {
|
|
|
179
182
|
"Secrets and provider credentials are unique per environment, validated at startup, never committed, and never logged.",
|
|
180
183
|
"Auth routes, tenant resolution, authorization policies, and trusted origins derive authority from verified sessions, API keys, or trusted gateway metadata.",
|
|
181
184
|
"Devtools, OpenAPI, cron, webhooks, and operational routes have intentional exposure and app-owned authorization.",
|
|
182
|
-
"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.",
|
|
183
186
|
"Uploads and storage define max sizes, authorization or explicit public access, object visibility, safe content headers, and short direct-upload expirations.",
|
|
184
187
|
"Provider-backed dependencies have bounded readiness checks, worker shutdown behavior, webhook secrets, and least-privilege credentials.",
|
|
185
188
|
];
|
|
@@ -846,7 +849,7 @@ async function inspectDiagnostics(targetDir, files, config, convention, contract
|
|
|
846
849
|
for (const routeHandler of matchedRoutes.unmatchedRouteHandlers) {
|
|
847
850
|
diagnostics.push(unmatchedRouteDiagnostic(routeHandler));
|
|
848
851
|
}
|
|
849
|
-
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)));
|
|
850
853
|
return dedupeDiagnostics(diagnostics);
|
|
851
854
|
}
|
|
852
855
|
async function inspectCanonicalConformance(targetDir, files, config, convention, strict) {
|
|
@@ -1228,6 +1231,7 @@ const readinessRelevantProviderPackages = new Set([
|
|
|
1228
1231
|
"@beignet/provider-auth-better-auth",
|
|
1229
1232
|
"@beignet/provider-db-drizzle",
|
|
1230
1233
|
"@beignet/provider-error-reporting-sentry",
|
|
1234
|
+
"@beignet/provider-event-bus-redis",
|
|
1231
1235
|
"@beignet/provider-flags-openfeature",
|
|
1232
1236
|
"@beignet/provider-jobs-bullmq",
|
|
1233
1237
|
"@beignet/provider-jobs-inngest",
|
|
@@ -1251,10 +1255,16 @@ async function inspectProductionReadiness(targetDir, files, config, convention)
|
|
|
1251
1255
|
const configFiles = operationalConfigFiles(files, config);
|
|
1252
1256
|
const infrastructurePath = directoryPath(path.dirname(config.paths.infrastructurePorts));
|
|
1253
1257
|
const schemaDir = `${infrastructurePath}/db/schema`;
|
|
1258
|
+
let hasSecurityHeadersHook = false;
|
|
1254
1259
|
for (const file of files) {
|
|
1255
1260
|
if (!file.endsWith(".ts") || file.endsWith(".test.ts"))
|
|
1256
1261
|
continue;
|
|
1257
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
|
+
}
|
|
1258
1268
|
if (containsCallExpression(source, "createDevtoolsRoute") &&
|
|
1259
1269
|
/\benabled\s*:\s*true\b/.test(source) &&
|
|
1260
1270
|
!/\bauthorize\s*:/.test(source)) {
|
|
@@ -1297,8 +1307,7 @@ async function inspectProductionReadiness(targetDir, files, config, convention)
|
|
|
1297
1307
|
message: `${file} defines a protected upload without authorize(...). Add an authorize hook, or set access: "public" only for intentionally public uploads.`,
|
|
1298
1308
|
});
|
|
1299
1309
|
}
|
|
1300
|
-
if (
|
|
1301
|
-
containsRequestTenantHeaderAuthority(source)) {
|
|
1310
|
+
if (routeServerOrInfra && containsRequestTenantHeaderAuthority(source)) {
|
|
1302
1311
|
diagnostics.push({
|
|
1303
1312
|
severity: "warning",
|
|
1304
1313
|
code: "BEIGNET_TENANT_HEADER_AUTHORITY",
|
|
@@ -1306,7 +1315,7 @@ async function inspectProductionReadiness(targetDir, files, config, convention)
|
|
|
1306
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.`,
|
|
1307
1316
|
});
|
|
1308
1317
|
}
|
|
1309
|
-
if (
|
|
1318
|
+
if (routeServerOrInfra &&
|
|
1310
1319
|
containsCallExpression(source, "createCorsHooks") &&
|
|
1311
1320
|
/\bcredentials\s*:\s*true\b/.test(source) &&
|
|
1312
1321
|
/(?:\borigin\b|\borigins\b)\s*:\s*(?:["']\*["']|\[\s*["']\*["'])/.test(source)) {
|
|
@@ -1318,6 +1327,14 @@ async function inspectProductionReadiness(targetDir, files, config, convention)
|
|
|
1318
1327
|
});
|
|
1319
1328
|
}
|
|
1320
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
|
+
}
|
|
1321
1338
|
for (const file of configFiles) {
|
|
1322
1339
|
const source = await readCachedSource(targetDir, file, sourceCache);
|
|
1323
1340
|
if (containsBetterAuthLocalSecret(source)) {
|
|
@@ -1478,7 +1495,7 @@ async function inspectPaymentsProductionReadiness(targetDir, files, config, sour
|
|
|
1478
1495
|
severity: "warning",
|
|
1479
1496
|
code: "BEIGNET_PAYMENTS_STRIPE_MEMORY_PROVIDER",
|
|
1480
1497
|
file: serverProviderFiles(files, config)[0] ?? config.paths.server,
|
|
1481
|
-
message: "@beignet/provider-payments-stripe is installed and Stripe env/config is present, but server/providers.ts still registers memory payments. Register
|
|
1498
|
+
message: "@beignet/provider-payments-stripe is installed and Stripe env/config is present, but server/providers.ts still registers memory payments. Register createStripePaymentsProvider() before deploying Stripe-backed billing, or remove the Stripe provider/config until the app is ready to switch.",
|
|
1482
1499
|
});
|
|
1483
1500
|
}
|
|
1484
1501
|
if (!(await hasPaymentWebhookRoute(targetDir, files, config, sourceCache))) {
|
|
@@ -1534,7 +1551,8 @@ async function hasStripeMemoryProviderMismatch(targetDir, sourceCache, configFil
|
|
|
1534
1551
|
if (!installedPackages.has("@beignet/provider-payments-stripe")) {
|
|
1535
1552
|
return false;
|
|
1536
1553
|
}
|
|
1537
|
-
if (findProviderEntryIndex(providerEntries, ["
|
|
1554
|
+
if (findProviderEntryIndex(providerEntries, ["createStripePaymentsProvider"]) >=
|
|
1555
|
+
0) {
|
|
1538
1556
|
return false;
|
|
1539
1557
|
}
|
|
1540
1558
|
if (findProviderEntryIndex(providerEntries, [
|
|
@@ -2130,6 +2148,61 @@ async function inspectWorkflowRegistrationDrift(targetDir, files, config, conven
|
|
|
2130
2148
|
}
|
|
2131
2149
|
return diagnostics;
|
|
2132
2150
|
}
|
|
2151
|
+
async function inspectRuntimeManifestDrift(targetDir, files, config, convention) {
|
|
2152
|
+
if (!convention.resourceGenerator)
|
|
2153
|
+
return [];
|
|
2154
|
+
const manifest = await readRuntimeManifestIdentifiers(targetDir, files);
|
|
2155
|
+
if (!manifest)
|
|
2156
|
+
return [];
|
|
2157
|
+
const registries = await readFeatureWorkflowRegistries(targetDir, files, config);
|
|
2158
|
+
const diagnostics = [];
|
|
2159
|
+
const checks = [
|
|
2160
|
+
{
|
|
2161
|
+
kind: "listeners",
|
|
2162
|
+
registered: manifest.listeners,
|
|
2163
|
+
code: "BEIGNET_RUNTIME_MANIFEST_LISTENER_MISSING",
|
|
2164
|
+
label: "listeners",
|
|
2165
|
+
},
|
|
2166
|
+
{
|
|
2167
|
+
kind: "schedules",
|
|
2168
|
+
registered: manifest.schedules,
|
|
2169
|
+
code: "BEIGNET_RUNTIME_MANIFEST_SCHEDULE_MISSING",
|
|
2170
|
+
label: "schedules",
|
|
2171
|
+
},
|
|
2172
|
+
{
|
|
2173
|
+
kind: "tasks",
|
|
2174
|
+
registered: manifest.tasks,
|
|
2175
|
+
code: "BEIGNET_RUNTIME_MANIFEST_TASK_MISSING",
|
|
2176
|
+
label: "tasks",
|
|
2177
|
+
},
|
|
2178
|
+
{
|
|
2179
|
+
kind: "events",
|
|
2180
|
+
registered: manifest.events,
|
|
2181
|
+
code: "BEIGNET_RUNTIME_MANIFEST_OUTBOX_EVENT_MISSING",
|
|
2182
|
+
label: "outbox events",
|
|
2183
|
+
},
|
|
2184
|
+
{
|
|
2185
|
+
kind: "jobs",
|
|
2186
|
+
registered: manifest.jobs,
|
|
2187
|
+
code: "BEIGNET_RUNTIME_MANIFEST_OUTBOX_JOB_MISSING",
|
|
2188
|
+
label: "outbox jobs",
|
|
2189
|
+
},
|
|
2190
|
+
];
|
|
2191
|
+
for (const check of checks) {
|
|
2192
|
+
const missing = unregisteredWorkflowRegistries(registries.filter((registry) => registry.kind === check.kind), check.registered);
|
|
2193
|
+
for (const { registry, missingMembers } of missing) {
|
|
2194
|
+
for (const member of missingMembers) {
|
|
2195
|
+
diagnostics.push({
|
|
2196
|
+
severity: "warning",
|
|
2197
|
+
code: check.code,
|
|
2198
|
+
file: manifest.file,
|
|
2199
|
+
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.`,
|
|
2200
|
+
});
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
return diagnostics;
|
|
2205
|
+
}
|
|
2133
2206
|
function workflowArtifactOrigin(registry, member) {
|
|
2134
2207
|
const memberFile = registry.memberFiles.get(member) ?? registry.indexFile;
|
|
2135
2208
|
if (memberFile === registry.indexFile) {
|
|
@@ -2196,7 +2269,7 @@ async function workflowRegistrationDrift(targetDir, files, config) {
|
|
|
2196
2269
|
}
|
|
2197
2270
|
const listenerRegistries = byKind("listeners");
|
|
2198
2271
|
if (listenerRegistries.length > 0) {
|
|
2199
|
-
const wiring = await listenerWiringReferences(targetDir, files);
|
|
2272
|
+
const wiring = await listenerWiringReferences(targetDir, files, config);
|
|
2200
2273
|
drift.listeners = {
|
|
2201
2274
|
unregistered: unregisteredWorkflowRegistries(listenerRegistries, wiring.identifiers),
|
|
2202
2275
|
wiringFile: wiring.wiringFile,
|
|
@@ -2275,6 +2348,71 @@ function exportedMemberFiles(source, indexFile, files) {
|
|
|
2275
2348
|
}
|
|
2276
2349
|
return memberFiles;
|
|
2277
2350
|
}
|
|
2351
|
+
async function readRuntimeManifestIdentifiers(targetDir, files) {
|
|
2352
|
+
for (const file of files) {
|
|
2353
|
+
if (!file.endsWith(".ts") && !file.endsWith(".tsx"))
|
|
2354
|
+
continue;
|
|
2355
|
+
if (file.endsWith(".d.ts") || isWorkflowTestFile(file))
|
|
2356
|
+
continue;
|
|
2357
|
+
const source = await readFile(path.join(targetDir, file), "utf8");
|
|
2358
|
+
if (!source.includes("defineRuntimeManifest"))
|
|
2359
|
+
continue;
|
|
2360
|
+
const identifiers = runtimeManifestIdentifiers(source);
|
|
2361
|
+
if (identifiers) {
|
|
2362
|
+
return {
|
|
2363
|
+
file,
|
|
2364
|
+
...identifiers,
|
|
2365
|
+
};
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
return undefined;
|
|
2369
|
+
}
|
|
2370
|
+
function runtimeManifestIdentifiers(source) {
|
|
2371
|
+
const call = /\bdefineRuntimeManifest\s*(?:<[^>]*>\s*)?\(/.exec(source);
|
|
2372
|
+
if (!call)
|
|
2373
|
+
return undefined;
|
|
2374
|
+
const arg = firstCallArgInfo(source, call.index + call[0].length);
|
|
2375
|
+
if (!arg)
|
|
2376
|
+
return undefined;
|
|
2377
|
+
const outbox = objectPropertyValueInfo(arg.text, "outbox");
|
|
2378
|
+
return {
|
|
2379
|
+
listeners: identifiersFromPropertyArray(arg.text, "listeners"),
|
|
2380
|
+
schedules: identifiersFromPropertyArray(arg.text, "schedules"),
|
|
2381
|
+
tasks: identifiersFromPropertyArray(arg.text, "tasks"),
|
|
2382
|
+
events: outbox
|
|
2383
|
+
? identifiersFromPropertyArray(outbox.text, "events")
|
|
2384
|
+
: new Set(),
|
|
2385
|
+
jobs: outbox
|
|
2386
|
+
? identifiersFromPropertyArray(outbox.text, "jobs")
|
|
2387
|
+
: new Set(),
|
|
2388
|
+
};
|
|
2389
|
+
}
|
|
2390
|
+
function identifiersFromPropertyArray(source, property) {
|
|
2391
|
+
const value = objectPropertyValueInfo(source, property);
|
|
2392
|
+
if (!value?.text.startsWith("["))
|
|
2393
|
+
return new Set();
|
|
2394
|
+
return identifiersFromArrayExpression(value.text);
|
|
2395
|
+
}
|
|
2396
|
+
function objectPropertyValueInfo(source, property) {
|
|
2397
|
+
const propertyMatch = new RegExp(`\\b${property}\\s*:`).exec(source);
|
|
2398
|
+
if (!propertyMatch)
|
|
2399
|
+
return undefined;
|
|
2400
|
+
let start = propertyMatch.index + propertyMatch[0].length;
|
|
2401
|
+
while (start < source.length && /\s/.test(source[start]))
|
|
2402
|
+
start++;
|
|
2403
|
+
const open = source[start];
|
|
2404
|
+
const close = open === "[" ? "]" : open === "{" ? "}" : undefined;
|
|
2405
|
+
if (!close)
|
|
2406
|
+
return undefined;
|
|
2407
|
+
const end = matchingDelimiterIndex(source, start, open, close);
|
|
2408
|
+
if (end === -1)
|
|
2409
|
+
return undefined;
|
|
2410
|
+
return {
|
|
2411
|
+
text: source.slice(start, end + 1),
|
|
2412
|
+
start,
|
|
2413
|
+
end: end + 1,
|
|
2414
|
+
};
|
|
2415
|
+
}
|
|
2278
2416
|
function registeredWorkflowIdentifiers(source, kind) {
|
|
2279
2417
|
if (kind === "tasks") {
|
|
2280
2418
|
const call = /\bdefineTasks\s*(?:<[^>]*>\s*)?\(/.exec(source);
|
|
@@ -2301,16 +2439,19 @@ async function listenedEventNames(targetDir, files, config) {
|
|
|
2301
2439
|
}
|
|
2302
2440
|
return listened;
|
|
2303
2441
|
}
|
|
2304
|
-
async function listenerWiringReferences(targetDir, files) {
|
|
2442
|
+
async function listenerWiringReferences(targetDir, files, config) {
|
|
2305
2443
|
const identifiers = new Set();
|
|
2306
2444
|
let wiringFile;
|
|
2307
2445
|
let eventBusFile;
|
|
2446
|
+
let centralListenerRegistryReferenced = false;
|
|
2447
|
+
const listenerRegistryFile = listenersFilePath(config);
|
|
2308
2448
|
for (const file of files) {
|
|
2309
2449
|
if (!file.endsWith(".ts") && !file.endsWith(".tsx"))
|
|
2310
2450
|
continue;
|
|
2311
2451
|
if (file.endsWith(".d.ts") || isWorkflowTestFile(file))
|
|
2312
2452
|
continue;
|
|
2313
2453
|
const source = await readFile(path.join(targetDir, file), "utf8");
|
|
2454
|
+
const namedImports = parseNamedImportSources(source);
|
|
2314
2455
|
let foundCall = false;
|
|
2315
2456
|
for (const match of source.matchAll(/\bregisterListeners\s*\(/g)) {
|
|
2316
2457
|
const openParen = (match.index ?? 0) + match[0].length - 1;
|
|
@@ -2319,9 +2460,19 @@ async function listenerWiringReferences(targetDir, files) {
|
|
|
2319
2460
|
? source.slice(openParen)
|
|
2320
2461
|
: source.slice(openParen + 1, closeParen);
|
|
2321
2462
|
foundCall = true;
|
|
2322
|
-
|
|
2463
|
+
const callIdentifiers = identifiersFromArrayExpression(argsText);
|
|
2464
|
+
for (const identifier of callIdentifiers) {
|
|
2323
2465
|
identifiers.add(identifier);
|
|
2324
2466
|
}
|
|
2467
|
+
if (callReferencesCentralListenerRegistry({
|
|
2468
|
+
callIdentifiers,
|
|
2469
|
+
namedImports,
|
|
2470
|
+
importerFile: file,
|
|
2471
|
+
listenerRegistryFile,
|
|
2472
|
+
files,
|
|
2473
|
+
})) {
|
|
2474
|
+
centralListenerRegistryReferenced = true;
|
|
2475
|
+
}
|
|
2325
2476
|
}
|
|
2326
2477
|
if (foundCall) {
|
|
2327
2478
|
wiringFile ??= file;
|
|
@@ -2330,8 +2481,33 @@ async function listenerWiringReferences(targetDir, files) {
|
|
|
2330
2481
|
eventBusFile = file;
|
|
2331
2482
|
}
|
|
2332
2483
|
}
|
|
2484
|
+
if (centralListenerRegistryReferenced &&
|
|
2485
|
+
files.includes(listenerRegistryFile)) {
|
|
2486
|
+
const source = await readFile(path.join(targetDir, listenerRegistryFile), "utf8");
|
|
2487
|
+
const central = arrayInitializerInfo(source, "listeners");
|
|
2488
|
+
if (central) {
|
|
2489
|
+
for (const identifier of identifiersFromArrayExpression(central.text)) {
|
|
2490
|
+
identifiers.add(identifier);
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2333
2494
|
return { identifiers, wiringFile, eventBusFile };
|
|
2334
2495
|
}
|
|
2496
|
+
function callReferencesCentralListenerRegistry(args) {
|
|
2497
|
+
if (args.importerFile === args.listenerRegistryFile &&
|
|
2498
|
+
args.callIdentifiers.has("listeners")) {
|
|
2499
|
+
return true;
|
|
2500
|
+
}
|
|
2501
|
+
for (const identifier of args.callIdentifiers) {
|
|
2502
|
+
const imported = args.namedImports.get(identifier);
|
|
2503
|
+
if (!imported || imported.importedName !== "listeners")
|
|
2504
|
+
continue;
|
|
2505
|
+
const importedFile = sourceFileFromImport(imported.sourcePath, args.importerFile, args.files);
|
|
2506
|
+
if (importedFile === args.listenerRegistryFile)
|
|
2507
|
+
return true;
|
|
2508
|
+
}
|
|
2509
|
+
return false;
|
|
2510
|
+
}
|
|
2335
2511
|
function isWorkflowTestFile(file) {
|
|
2336
2512
|
return (file.endsWith(".test.ts") ||
|
|
2337
2513
|
file.endsWith(".test.tsx") ||
|
|
@@ -2550,7 +2726,7 @@ async function inspectDatabaseLifecycleDrift(targetDir, files, config, conventio
|
|
|
2550
2726
|
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.`,
|
|
2551
2727
|
});
|
|
2552
2728
|
}
|
|
2553
|
-
diagnostics.push(...(await inspectDurableDrizzleTableDrift(targetDir, files, config, infrastructurePath, schemaDir, sourceCache)));
|
|
2729
|
+
diagnostics.push(...(await inspectDurableDrizzleTableDrift(targetDir, files, config, infrastructurePath, schemaDir, sourceCache)), ...(await inspectTenantScopeRepositoryDrift(targetDir, files, resources, config, infrastructurePath, schemaDir, sourceCache)));
|
|
2554
2730
|
}
|
|
2555
2731
|
const schemaFilePath = `${infrastructurePath}/db/schema.ts`;
|
|
2556
2732
|
if (files.includes(schemaFilePath)) {
|
|
@@ -2675,6 +2851,387 @@ async function inspectDatabaseLifecycleDrift(targetDir, files, config, conventio
|
|
|
2675
2851
|
}
|
|
2676
2852
|
return diagnostics;
|
|
2677
2853
|
}
|
|
2854
|
+
async function inspectTenantScopeRepositoryDrift(targetDir, files, resources, config, infrastructurePath, schemaDir, sourceCache) {
|
|
2855
|
+
const diagnostics = [];
|
|
2856
|
+
const schemaIndexFile = `${schemaDir}/index.ts`;
|
|
2857
|
+
const schemaFiles = files.filter((file) => file.startsWith(`${schemaDir}/`) &&
|
|
2858
|
+
file.endsWith(".ts") &&
|
|
2859
|
+
file !== schemaIndexFile);
|
|
2860
|
+
const tenantScopedTables = await collectTenantScopedDrizzleTables(targetDir, schemaFiles, sourceCache);
|
|
2861
|
+
const inspectedRepositories = new Set();
|
|
2862
|
+
for (const resource of resources) {
|
|
2863
|
+
const singular = singularize(resource);
|
|
2864
|
+
const singularPascal = pascalCase(singular);
|
|
2865
|
+
const pluralCamel = camelCase(resource);
|
|
2866
|
+
const schemaFile = `${schemaDir}/${resource}.ts`;
|
|
2867
|
+
const portFile = resourcePortFile(resource, singular, config);
|
|
2868
|
+
const drizzleRepositoryFile = drizzleRepositoryAdapterFile(files, `${infrastructurePath}/${resource}/`, singular);
|
|
2869
|
+
if (!drizzleRepositoryFile || !files.includes(schemaFile))
|
|
2870
|
+
continue;
|
|
2871
|
+
const schemaSource = await readCachedSource(targetDir, schemaFile, sourceCache);
|
|
2872
|
+
const schemaTenantColumns = tenantScopeColumnsFromSource(schemaSource);
|
|
2873
|
+
if (schemaTenantColumns.length === 0)
|
|
2874
|
+
continue;
|
|
2875
|
+
inspectedRepositories.add(drizzleRepositoryFile);
|
|
2876
|
+
if (files.includes(portFile)) {
|
|
2877
|
+
const portSource = await readCachedSource(targetDir, portFile, sourceCache);
|
|
2878
|
+
if (!/\b[A-Za-z_$][\w$]*\s*:\s*TenantScope\b/.test(portSource)) {
|
|
2879
|
+
diagnostics.push({
|
|
2880
|
+
severity: "warning",
|
|
2881
|
+
code: "BEIGNET_TENANT_SCOPE_PORT_MISSING",
|
|
2882
|
+
file: portFile,
|
|
2883
|
+
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.`,
|
|
2884
|
+
});
|
|
2885
|
+
}
|
|
2886
|
+
}
|
|
2887
|
+
const repositorySource = await readCachedSource(targetDir, drizzleRepositoryFile, sourceCache);
|
|
2888
|
+
const tenantPredicate = `eq(schema.${pluralCamel}.tenantId, tenantScopeId(scope))`;
|
|
2889
|
+
const tenantPredicateCount = countSourceOccurrences(repositorySource, tenantPredicate);
|
|
2890
|
+
const setsTenantFromScope = repositorySource.includes("tenantId: tenantScopeId(scope)");
|
|
2891
|
+
if (!repositorySource.includes(`schema.${pluralCamel}`) ||
|
|
2892
|
+
(tenantPredicateCount >= 4 && setsTenantFromScope) ||
|
|
2893
|
+
repositoryHasScopedTenantCoverage(repositorySource, [
|
|
2894
|
+
{
|
|
2895
|
+
name: pluralCamel,
|
|
2896
|
+
file: schemaFile,
|
|
2897
|
+
columns: schemaTenantColumns,
|
|
2898
|
+
},
|
|
2899
|
+
])) {
|
|
2900
|
+
continue;
|
|
2901
|
+
}
|
|
2902
|
+
diagnostics.push({
|
|
2903
|
+
severity: "warning",
|
|
2904
|
+
code: "BEIGNET_TENANT_SCOPE_PREDICATE_MISSING",
|
|
2905
|
+
file: drizzleRepositoryFile,
|
|
2906
|
+
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.`,
|
|
2907
|
+
});
|
|
2908
|
+
}
|
|
2909
|
+
const repositoryFiles = files.filter((file) => file.startsWith(`${infrastructurePath}/`) &&
|
|
2910
|
+
file.includes("/drizzle-") &&
|
|
2911
|
+
file.endsWith("-repository.ts") &&
|
|
2912
|
+
!file.endsWith(".test.ts"));
|
|
2913
|
+
const featuresPath = directoryPath(config.paths.features);
|
|
2914
|
+
for (const repositoryFile of repositoryFiles) {
|
|
2915
|
+
if (inspectedRepositories.has(repositoryFile))
|
|
2916
|
+
continue;
|
|
2917
|
+
const repositorySource = await readCachedSource(targetDir, repositoryFile, sourceCache);
|
|
2918
|
+
const referencedTables = tenantScopedTables.filter((table) => repositorySource.includes(`schema.${table.name}`));
|
|
2919
|
+
if (referencedTables.length === 0)
|
|
2920
|
+
continue;
|
|
2921
|
+
const repositoryFeature = repositoryFeatureFromPath(repositoryFile, infrastructurePath);
|
|
2922
|
+
if (!repositoryFeature || isTenantAuthorityFeature(repositoryFeature)) {
|
|
2923
|
+
continue;
|
|
2924
|
+
}
|
|
2925
|
+
const portFile = `${featuresPath}/${repositoryFeature}/ports.ts`;
|
|
2926
|
+
if (!files.includes(portFile))
|
|
2927
|
+
continue;
|
|
2928
|
+
const portSource = await readCachedSource(targetDir, portFile, sourceCache);
|
|
2929
|
+
const repositoryBlocks = repositoryInterfaceBlocks(portSource);
|
|
2930
|
+
if (repositoryBlocks.length === 0)
|
|
2931
|
+
continue;
|
|
2932
|
+
const hasTenantScopeBoundary = repositoryBlocks.some((block) => repositoryInterfaceAcceptsTenantScope(block));
|
|
2933
|
+
const rawBoundaries = repositoryBlocks.flatMap((block) => rawTenantRepositoryMethodBoundaries(block, referencedTables));
|
|
2934
|
+
if (rawBoundaries.length > 0) {
|
|
2935
|
+
diagnostics.push({
|
|
2936
|
+
severity: "warning",
|
|
2937
|
+
code: "BEIGNET_TENANT_SCOPE_RAW_ID_BOUNDARY",
|
|
2938
|
+
file: portFile,
|
|
2939
|
+
message: `${portFile} declares tenant-owned repository methods (${rawBoundaries.join(", ")}) with raw tenant or workspace IDs. Use TenantScope at app-facing repository boundaries and unwrap it with tenantScopeId(scope) inside ${repositoryFile}; keep provider-correlation lookups separate from authorization.`,
|
|
2940
|
+
});
|
|
2941
|
+
continue;
|
|
2942
|
+
}
|
|
2943
|
+
if (hasTenantScopeBoundary &&
|
|
2944
|
+
!repositoryHasScopedTenantCoverage(repositorySource, referencedTables)) {
|
|
2945
|
+
diagnostics.push({
|
|
2946
|
+
severity: "warning",
|
|
2947
|
+
code: "BEIGNET_TENANT_SCOPE_PREDICATE_MISSING",
|
|
2948
|
+
file: repositoryFile,
|
|
2949
|
+
message: `${repositoryFile} adapts tenant-owned tables (${referencedTables
|
|
2950
|
+
.map((table) => table.name)
|
|
2951
|
+
.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.`,
|
|
2952
|
+
});
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
return diagnostics;
|
|
2956
|
+
}
|
|
2957
|
+
const tenantScopeColumnDefinitions = [
|
|
2958
|
+
{
|
|
2959
|
+
property: "tenantId",
|
|
2960
|
+
sqlName: "tenant_id",
|
|
2961
|
+
label: "tenant",
|
|
2962
|
+
},
|
|
2963
|
+
{
|
|
2964
|
+
property: "workspaceId",
|
|
2965
|
+
sqlName: "workspace_id",
|
|
2966
|
+
label: "workspace",
|
|
2967
|
+
},
|
|
2968
|
+
];
|
|
2969
|
+
async function collectTenantScopedDrizzleTables(targetDir, schemaFiles, sourceCache) {
|
|
2970
|
+
const tables = [];
|
|
2971
|
+
for (const file of schemaFiles) {
|
|
2972
|
+
const source = await readCachedSource(targetDir, file, sourceCache);
|
|
2973
|
+
for (const table of drizzleTablesFromSource(source, file)) {
|
|
2974
|
+
if (table.columns.length > 0)
|
|
2975
|
+
tables.push(table);
|
|
2976
|
+
}
|
|
2977
|
+
}
|
|
2978
|
+
return tables;
|
|
2979
|
+
}
|
|
2980
|
+
function drizzleTablesFromSource(source, file) {
|
|
2981
|
+
const tables = [];
|
|
2982
|
+
const tablePattern = /\bexport\s+const\s+([A-Za-z_$][\w$]*)\s*=\s*(?:sqliteTable|pgTable|mysqlTable)\s*\(/g;
|
|
2983
|
+
for (const match of source.matchAll(tablePattern)) {
|
|
2984
|
+
const tableName = match[1];
|
|
2985
|
+
const openIndex = source.indexOf("(", match.index ?? 0);
|
|
2986
|
+
if (!tableName || openIndex === -1)
|
|
2987
|
+
continue;
|
|
2988
|
+
const closeIndex = matchingDelimiterIndex(source, openIndex, "(", ")");
|
|
2989
|
+
if (closeIndex === -1)
|
|
2990
|
+
continue;
|
|
2991
|
+
const tableSource = source.slice(openIndex, closeIndex + 1);
|
|
2992
|
+
const columns = tenantScopeColumnsFromSource(tableSource);
|
|
2993
|
+
tables.push({ name: tableName, file, columns });
|
|
2994
|
+
}
|
|
2995
|
+
return tables;
|
|
2996
|
+
}
|
|
2997
|
+
function tenantScopeColumnsFromSource(source) {
|
|
2998
|
+
return tenantScopeColumnDefinitions.filter((column) => {
|
|
2999
|
+
const propertyPattern = new RegExp(`\\b${column.property}\\s*:`);
|
|
3000
|
+
return (propertyPattern.test(source) ||
|
|
3001
|
+
source.includes(`"${column.sqlName}"`) ||
|
|
3002
|
+
source.includes(`'${column.sqlName}'`));
|
|
3003
|
+
});
|
|
3004
|
+
}
|
|
3005
|
+
function repositoryFeatureFromPath(repositoryFile, infrastructurePath) {
|
|
3006
|
+
const match = new RegExp(`^${escapeRegExp(infrastructurePath)}/([^/]+)/`).exec(repositoryFile);
|
|
3007
|
+
return match?.[1];
|
|
3008
|
+
}
|
|
3009
|
+
function isTenantAuthorityFeature(feature) {
|
|
3010
|
+
return feature === "workspaces";
|
|
3011
|
+
}
|
|
3012
|
+
function repositoryInterfaceBlocks(source) {
|
|
3013
|
+
const blocks = [];
|
|
3014
|
+
const interfacePattern = /\bexport\s+interface\s+[A-Za-z_$][\w$]*Repository\s*\{/g;
|
|
3015
|
+
for (const match of source.matchAll(interfacePattern)) {
|
|
3016
|
+
const openIndex = source.indexOf("{", match.index ?? 0);
|
|
3017
|
+
if (openIndex === -1)
|
|
3018
|
+
continue;
|
|
3019
|
+
const closeIndex = matchingDelimiterIndex(source, openIndex, "{", "}");
|
|
3020
|
+
if (closeIndex === -1)
|
|
3021
|
+
continue;
|
|
3022
|
+
blocks.push(source.slice(openIndex + 1, closeIndex));
|
|
3023
|
+
}
|
|
3024
|
+
return blocks;
|
|
3025
|
+
}
|
|
3026
|
+
function repositoryInterfaceAcceptsTenantScope(block) {
|
|
3027
|
+
return /\b[A-Za-z_$][\w$]*\s*:\s*TenantScope\b/.test(block);
|
|
3028
|
+
}
|
|
3029
|
+
function rawTenantRepositoryMethodBoundaries(block, tables) {
|
|
3030
|
+
const rawMethods = [];
|
|
3031
|
+
for (const method of repositoryMethodSignatures(block)) {
|
|
3032
|
+
if (isProviderCorrelationMethod(method.name))
|
|
3033
|
+
continue;
|
|
3034
|
+
const rawColumns = tenantColumnsUsedInSource(method.params, tables);
|
|
3035
|
+
if (rawColumns.length === 0)
|
|
3036
|
+
continue;
|
|
3037
|
+
rawMethods.push(method.name);
|
|
3038
|
+
}
|
|
3039
|
+
return uniqueStringValues(rawMethods);
|
|
3040
|
+
}
|
|
3041
|
+
function repositoryMethodSignatures(block) {
|
|
3042
|
+
const signatures = [];
|
|
3043
|
+
const methodPattern = /\b([A-Za-z_$][\w$]*)\s*\(/g;
|
|
3044
|
+
for (const match of block.matchAll(methodPattern)) {
|
|
3045
|
+
const name = match[1];
|
|
3046
|
+
const openIndex = block.indexOf("(", match.index ?? 0);
|
|
3047
|
+
if (!name || openIndex === -1)
|
|
3048
|
+
continue;
|
|
3049
|
+
const closeIndex = matchingDelimiterIndex(block, openIndex, "(", ")");
|
|
3050
|
+
if (closeIndex === -1)
|
|
3051
|
+
continue;
|
|
3052
|
+
const afterParams = block.slice(closeIndex + 1).trimStart();
|
|
3053
|
+
if (!afterParams.startsWith(":"))
|
|
3054
|
+
continue;
|
|
3055
|
+
signatures.push({
|
|
3056
|
+
name,
|
|
3057
|
+
params: block.slice(openIndex + 1, closeIndex),
|
|
3058
|
+
});
|
|
3059
|
+
}
|
|
3060
|
+
return signatures;
|
|
3061
|
+
}
|
|
3062
|
+
function isProviderCorrelationMethod(name) {
|
|
3063
|
+
return /(?:Customer|Provider|Subscription|Checkout|Event|Delivery|External|Token)[A-Za-z0-9]*(?:Id)?$/i.test(name);
|
|
3064
|
+
}
|
|
3065
|
+
function tenantColumnsUsedInSource(source, tables) {
|
|
3066
|
+
const columns = new Map();
|
|
3067
|
+
for (const table of tables) {
|
|
3068
|
+
for (const column of table.columns) {
|
|
3069
|
+
const propertyPattern = new RegExp(`\\b${column.property}\\s*:`);
|
|
3070
|
+
if (propertyPattern.test(source)) {
|
|
3071
|
+
columns.set(column.property, column);
|
|
3072
|
+
}
|
|
3073
|
+
}
|
|
3074
|
+
}
|
|
3075
|
+
return [...columns.values()];
|
|
3076
|
+
}
|
|
3077
|
+
function repositoryHasScopedTenantCoverage(source, tables) {
|
|
3078
|
+
if (!source.includes("tenantScopeId(scope)"))
|
|
3079
|
+
return false;
|
|
3080
|
+
const allFunctions = functionLikeBlocks(source);
|
|
3081
|
+
const scopedFunctions = allFunctions.filter((block) => /\bscope\b/.test(block.params));
|
|
3082
|
+
if (scopedFunctions.length === 0)
|
|
3083
|
+
return false;
|
|
3084
|
+
const functionsByName = new Map(allFunctions.map((block) => [block.name, block]));
|
|
3085
|
+
return scopedFunctions.every((block) => scopedFunctionUsesTenantScope(block, tables, functionsByName));
|
|
3086
|
+
}
|
|
3087
|
+
function functionLikeBlocks(source) {
|
|
3088
|
+
const blocks = [];
|
|
3089
|
+
const pattern = /(?:\basync\s+)?(?:function\s+)?([A-Za-z_$][\w$]*)\s*\(/g;
|
|
3090
|
+
for (const match of source.matchAll(pattern)) {
|
|
3091
|
+
const name = match[1];
|
|
3092
|
+
const openParamsIndex = source.indexOf("(", match.index ?? 0);
|
|
3093
|
+
if (!name || openParamsIndex === -1)
|
|
3094
|
+
continue;
|
|
3095
|
+
const closeParamsIndex = matchingDelimiterIndex(source, openParamsIndex, "(", ")");
|
|
3096
|
+
if (closeParamsIndex === -1)
|
|
3097
|
+
continue;
|
|
3098
|
+
const afterParams = source.slice(closeParamsIndex + 1);
|
|
3099
|
+
const bodyOpenOffset = afterParams.search(/\{/);
|
|
3100
|
+
if (bodyOpenOffset === -1)
|
|
3101
|
+
continue;
|
|
3102
|
+
const betweenParamsAndBody = afterParams.slice(0, bodyOpenOffset);
|
|
3103
|
+
if (!/^\s*(?::[^{;=]+)?\s*(?:=>\s*)?$/.test(betweenParamsAndBody) &&
|
|
3104
|
+
!/^\s*$/.test(betweenParamsAndBody)) {
|
|
3105
|
+
continue;
|
|
3106
|
+
}
|
|
3107
|
+
const openBodyIndex = closeParamsIndex + 1 + bodyOpenOffset;
|
|
3108
|
+
const closeBodyIndex = matchingDelimiterIndex(source, openBodyIndex, "{", "}");
|
|
3109
|
+
if (closeBodyIndex === -1)
|
|
3110
|
+
continue;
|
|
3111
|
+
blocks.push({
|
|
3112
|
+
name,
|
|
3113
|
+
params: source.slice(openParamsIndex + 1, closeParamsIndex),
|
|
3114
|
+
body: source.slice(openBodyIndex + 1, closeBodyIndex),
|
|
3115
|
+
});
|
|
3116
|
+
}
|
|
3117
|
+
return blocks;
|
|
3118
|
+
}
|
|
3119
|
+
function scopedFunctionUsesTenantScope(block, tables, functionsByName) {
|
|
3120
|
+
if (bodyUsesTenantScopePredicate(block.body, tables))
|
|
3121
|
+
return true;
|
|
3122
|
+
for (const variable of tenantScopeVariables(block.body)) {
|
|
3123
|
+
if (bodyUsesTenantScopePredicate(block.body, tables, variable))
|
|
3124
|
+
return true;
|
|
3125
|
+
for (const callee of functionsCalledWithVariable(block.body, variable)) {
|
|
3126
|
+
const helper = functionsByName.get(callee.name);
|
|
3127
|
+
if (helper &&
|
|
3128
|
+
helperUsesTenantVariable(helper, tables, callee.argumentIndex)) {
|
|
3129
|
+
return true;
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
}
|
|
3133
|
+
return false;
|
|
3134
|
+
}
|
|
3135
|
+
function tenantScopeVariables(body) {
|
|
3136
|
+
const variables = new Set(["tenantScopeId(scope)"]);
|
|
3137
|
+
const assignmentPattern = /\b(?:const|let)\s+([A-Za-z_$][\w$]*)\s*=\s*tenantScopeId\(scope\)/g;
|
|
3138
|
+
for (const match of body.matchAll(assignmentPattern)) {
|
|
3139
|
+
if (match[1])
|
|
3140
|
+
variables.add(match[1]);
|
|
3141
|
+
}
|
|
3142
|
+
return [...variables];
|
|
3143
|
+
}
|
|
3144
|
+
function bodyUsesTenantScopePredicate(body, tables, variable = "tenantScopeId(scope)") {
|
|
3145
|
+
for (const table of tables) {
|
|
3146
|
+
for (const column of table.columns) {
|
|
3147
|
+
if (body.includes(`eq(schema.${table.name}.${column.property}, ${variable})`)) {
|
|
3148
|
+
return true;
|
|
3149
|
+
}
|
|
3150
|
+
if (body.includes(`${column.property}: ${variable}`)) {
|
|
3151
|
+
return true;
|
|
3152
|
+
}
|
|
3153
|
+
if (variable === column.property &&
|
|
3154
|
+
new RegExp(`\\b${column.property}\\s*(?:,|})`).test(body)) {
|
|
3155
|
+
return true;
|
|
3156
|
+
}
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
3159
|
+
return false;
|
|
3160
|
+
}
|
|
3161
|
+
function functionsCalledWithVariable(body, variable) {
|
|
3162
|
+
if (variable.includes("("))
|
|
3163
|
+
return [];
|
|
3164
|
+
const calls = [];
|
|
3165
|
+
const callPattern = /\b([A-Za-z_$][\w$]*)\s*\(/g;
|
|
3166
|
+
for (const match of body.matchAll(callPattern)) {
|
|
3167
|
+
const name = match[1];
|
|
3168
|
+
const openIndex = body.indexOf("(", match.index ?? 0);
|
|
3169
|
+
if (!name || openIndex === -1)
|
|
3170
|
+
continue;
|
|
3171
|
+
const closeIndex = matchingDelimiterIndex(body, openIndex, "(", ")");
|
|
3172
|
+
if (closeIndex === -1)
|
|
3173
|
+
continue;
|
|
3174
|
+
const args = splitTopLevelArguments(body.slice(openIndex + 1, closeIndex));
|
|
3175
|
+
const argumentIndex = args.findIndex((argument) => argument.trim() === variable);
|
|
3176
|
+
if (argumentIndex !== -1) {
|
|
3177
|
+
calls.push({ name, argumentIndex });
|
|
3178
|
+
}
|
|
3179
|
+
}
|
|
3180
|
+
return calls;
|
|
3181
|
+
}
|
|
3182
|
+
function helperUsesTenantVariable(helper, tables, argumentIndex) {
|
|
3183
|
+
const params = splitTopLevelArguments(helper.params);
|
|
3184
|
+
const parameter = params[argumentIndex]?.trim();
|
|
3185
|
+
if (!parameter)
|
|
3186
|
+
return false;
|
|
3187
|
+
const parameterName = /^([A-Za-z_$][\w$]*)/.exec(parameter)?.[1];
|
|
3188
|
+
return parameterName
|
|
3189
|
+
? bodyUsesTenantScopePredicate(helper.body, tables, parameterName)
|
|
3190
|
+
: false;
|
|
3191
|
+
}
|
|
3192
|
+
function splitTopLevelArguments(source) {
|
|
3193
|
+
const args = [];
|
|
3194
|
+
let start = 0;
|
|
3195
|
+
let depth = 0;
|
|
3196
|
+
let inString;
|
|
3197
|
+
let escaped = false;
|
|
3198
|
+
for (let index = 0; index < source.length; index++) {
|
|
3199
|
+
const char = source[index];
|
|
3200
|
+
if (inString) {
|
|
3201
|
+
if (escaped) {
|
|
3202
|
+
escaped = false;
|
|
3203
|
+
}
|
|
3204
|
+
else if (char === "\\") {
|
|
3205
|
+
escaped = true;
|
|
3206
|
+
}
|
|
3207
|
+
else if (char === inString) {
|
|
3208
|
+
inString = undefined;
|
|
3209
|
+
}
|
|
3210
|
+
continue;
|
|
3211
|
+
}
|
|
3212
|
+
if (char === '"' || char === "'" || char === "`") {
|
|
3213
|
+
inString = char;
|
|
3214
|
+
continue;
|
|
3215
|
+
}
|
|
3216
|
+
if (char === "(" || char === "{" || char === "[") {
|
|
3217
|
+
depth += 1;
|
|
3218
|
+
continue;
|
|
3219
|
+
}
|
|
3220
|
+
if (char === ")" || char === "}" || char === "]") {
|
|
3221
|
+
depth -= 1;
|
|
3222
|
+
continue;
|
|
3223
|
+
}
|
|
3224
|
+
if (char === "," && depth === 0) {
|
|
3225
|
+
args.push(source.slice(start, index));
|
|
3226
|
+
start = index + 1;
|
|
3227
|
+
}
|
|
3228
|
+
}
|
|
3229
|
+
args.push(source.slice(start));
|
|
3230
|
+
return args;
|
|
3231
|
+
}
|
|
3232
|
+
function uniqueStringValues(values) {
|
|
3233
|
+
return [...new Set(values)];
|
|
3234
|
+
}
|
|
2678
3235
|
const durableDrizzleTableRequirements = [
|
|
2679
3236
|
{
|
|
2680
3237
|
capability: "idempotency",
|
|
@@ -2699,15 +3256,18 @@ const durableDrizzleTableRequirements = [
|
|
|
2699
3256
|
code: "BEIGNET_OUTBOX_TABLE_MISSING",
|
|
2700
3257
|
factoryNames: [
|
|
2701
3258
|
"createDrizzleSqliteOutboxPort",
|
|
3259
|
+
"createDrizzleSqliteOutboxAdminPort",
|
|
2702
3260
|
"createDrizzlePostgresOutboxPort",
|
|
3261
|
+
"createDrizzlePostgresOutboxAdminPort",
|
|
2703
3262
|
"createDrizzleMysqlOutboxPort",
|
|
3263
|
+
"createDrizzleMysqlOutboxAdminPort",
|
|
2704
3264
|
],
|
|
2705
3265
|
setupNames: [
|
|
2706
3266
|
"createDrizzleSqliteOutboxSetupStatements",
|
|
2707
3267
|
"createDrizzlePostgresOutboxSetupStatements",
|
|
2708
3268
|
"createDrizzleMysqlOutboxSetupStatements",
|
|
2709
3269
|
],
|
|
2710
|
-
explicitFactoryPattern: "createDrizzle[A-Za-z]*Outbox(?:Port)?",
|
|
3270
|
+
explicitFactoryPattern: "createDrizzle[A-Za-z]*Outbox(?:Admin)?(?:Port)?",
|
|
2711
3271
|
explicitSetupPattern: "createDrizzle[A-Za-z]*OutboxSetupStatements",
|
|
2712
3272
|
},
|
|
2713
3273
|
{
|
|
@@ -2929,18 +3489,52 @@ async function inspectPackageScripts(targetDir, files, convention) {
|
|
|
2929
3489
|
return [];
|
|
2930
3490
|
}
|
|
2931
3491
|
const packageJson = JSON.parse(await readFile(path.join(targetDir, "package.json"), "utf8"));
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
3492
|
+
const diagnostics = [];
|
|
3493
|
+
const scripts = packageJson.scripts ?? {};
|
|
3494
|
+
const hasDirectBunTestImports = await appUsesDirectBunTestImports(targetDir, files);
|
|
3495
|
+
if (!scripts.test) {
|
|
3496
|
+
diagnostics.push({
|
|
2936
3497
|
severity: "warning",
|
|
2937
3498
|
code: "BEIGNET_PACKAGE_TEST_SCRIPT_MISSING",
|
|
2938
3499
|
file: "package.json",
|
|
2939
|
-
message: 'package.json does not define a test script. Add "test": "
|
|
2940
|
-
}
|
|
2941
|
-
|
|
3500
|
+
message: 'package.json does not define a test script. Add "test": "tsx lib/beignet-test-runner.ts" or run beignet doctor --fix.',
|
|
3501
|
+
});
|
|
3502
|
+
}
|
|
3503
|
+
const legacyScripts = hasDirectBunTestImports
|
|
3504
|
+
? []
|
|
3505
|
+
: legacyGeneratedScriptNames(scripts);
|
|
3506
|
+
if (legacyScripts.length > 0) {
|
|
3507
|
+
diagnostics.push({
|
|
3508
|
+
severity: "warning",
|
|
3509
|
+
code: "BEIGNET_PACKAGE_SCRIPT_LEGACY",
|
|
3510
|
+
file: "package.json",
|
|
3511
|
+
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.`,
|
|
3512
|
+
});
|
|
3513
|
+
}
|
|
3514
|
+
const needsTsx = hasCurrentGeneratedTsxScript(scripts) &&
|
|
3515
|
+
!packageJson.dependencies?.tsx &&
|
|
3516
|
+
!packageJson.devDependencies?.tsx;
|
|
3517
|
+
if (needsTsx) {
|
|
3518
|
+
diagnostics.push({
|
|
3519
|
+
severity: "warning",
|
|
3520
|
+
code: "BEIGNET_PACKAGE_TSX_DEPENDENCY_MISSING",
|
|
3521
|
+
file: "package.json",
|
|
3522
|
+
message: 'package.json uses generated tsx scripts but does not depend on tsx. Add "tsx" to devDependencies or run beignet doctor --fix.',
|
|
3523
|
+
});
|
|
3524
|
+
}
|
|
3525
|
+
const missingSupportFiles = missingTestSupportFiles(files);
|
|
3526
|
+
if (scripts.test === currentGeneratedPackageScripts.test &&
|
|
3527
|
+
missingSupportFiles.length > 0) {
|
|
3528
|
+
diagnostics.push({
|
|
3529
|
+
severity: "warning",
|
|
3530
|
+
code: "BEIGNET_PACKAGE_TEST_SUPPORT_MISSING",
|
|
3531
|
+
file: "package.json",
|
|
3532
|
+
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.`,
|
|
3533
|
+
});
|
|
3534
|
+
}
|
|
3535
|
+
return diagnostics;
|
|
2942
3536
|
}
|
|
2943
|
-
async function
|
|
3537
|
+
async function fixPackageScripts(targetDir, files, convention) {
|
|
2944
3538
|
if (!convention.resourceGenerator || !files.includes("package.json")) {
|
|
2945
3539
|
return undefined;
|
|
2946
3540
|
}
|
|
@@ -2948,18 +3542,118 @@ async function fixMissingTestScript(targetDir, files, convention) {
|
|
|
2948
3542
|
const original = await readFile(filePath, "utf8");
|
|
2949
3543
|
const packageJson = JSON.parse(original);
|
|
2950
3544
|
packageJson.scripts = packageJson.scripts ?? {};
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
3545
|
+
const updatedScripts = [];
|
|
3546
|
+
const addedTestScript = !packageJson.scripts.test;
|
|
3547
|
+
const hasDirectBunTestImports = await appUsesDirectBunTestImports(targetDir, files);
|
|
3548
|
+
if (addedTestScript) {
|
|
3549
|
+
packageJson.scripts.test = currentGeneratedPackageScripts.test;
|
|
3550
|
+
updatedScripts.push("test");
|
|
3551
|
+
}
|
|
3552
|
+
if (!hasDirectBunTestImports) {
|
|
3553
|
+
for (const [name, legacyCommand] of Object.entries(legacyGeneratedPackageScripts)) {
|
|
3554
|
+
if (packageJson.scripts[name] === legacyCommand) {
|
|
3555
|
+
packageJson.scripts[name] = currentGeneratedPackageScripts[name];
|
|
3556
|
+
if (!updatedScripts.includes(name))
|
|
3557
|
+
updatedScripts.push(name);
|
|
3558
|
+
}
|
|
3559
|
+
}
|
|
3560
|
+
}
|
|
3561
|
+
const needsTsx = hasCurrentGeneratedTsxScript(packageJson.scripts);
|
|
3562
|
+
const addedTsx = needsTsx &&
|
|
3563
|
+
!packageJson.dependencies?.tsx &&
|
|
3564
|
+
!packageJson.devDependencies?.tsx;
|
|
3565
|
+
if (addedTsx) {
|
|
3566
|
+
packageJson.devDependencies = packageJson.devDependencies ?? {};
|
|
3567
|
+
packageJson.devDependencies.tsx = externalVersions.tsx;
|
|
3568
|
+
}
|
|
2954
3569
|
const next = `${JSON.stringify(packageJson, null, "\t")}\n`;
|
|
2955
|
-
|
|
3570
|
+
const createdSupportFiles = await createMissingTestSupportFiles(targetDir, files, packageJson.scripts.test === currentGeneratedPackageScripts.test);
|
|
3571
|
+
if (next === original && createdSupportFiles.length === 0)
|
|
2956
3572
|
return undefined;
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
3573
|
+
if (next !== original)
|
|
3574
|
+
await writeFile(filePath, next);
|
|
3575
|
+
if (addedTestScript) {
|
|
3576
|
+
return {
|
|
3577
|
+
code: "BEIGNET_PACKAGE_TEST_SCRIPT_MISSING",
|
|
3578
|
+
file: "package.json",
|
|
3579
|
+
message: createdSupportFiles.length > 0
|
|
3580
|
+
? `Added "test": "tsx lib/beignet-test-runner.ts" and ${createdSupportFiles.join(", ")}.`
|
|
3581
|
+
: 'Added "test": "tsx lib/beignet-test-runner.ts".',
|
|
3582
|
+
};
|
|
3583
|
+
}
|
|
3584
|
+
if (updatedScripts.length > 0) {
|
|
3585
|
+
return {
|
|
3586
|
+
code: "BEIGNET_PACKAGE_SCRIPT_LEGACY",
|
|
3587
|
+
file: "package.json",
|
|
3588
|
+
message: packageScriptFixMessage(updatedScripts, createdSupportFiles, addedTsx),
|
|
3589
|
+
};
|
|
3590
|
+
}
|
|
3591
|
+
if (createdSupportFiles.length > 0) {
|
|
3592
|
+
return {
|
|
3593
|
+
code: "BEIGNET_PACKAGE_TEST_SUPPORT_MISSING",
|
|
3594
|
+
file: "package.json",
|
|
3595
|
+
message: `Restored ${createdSupportFiles.join(", ")}.`,
|
|
3596
|
+
};
|
|
3597
|
+
}
|
|
3598
|
+
if (addedTsx) {
|
|
3599
|
+
return {
|
|
3600
|
+
code: "BEIGNET_PACKAGE_TSX_DEPENDENCY_MISSING",
|
|
3601
|
+
file: "package.json",
|
|
3602
|
+
message: 'Added "tsx" to devDependencies.',
|
|
3603
|
+
};
|
|
3604
|
+
}
|
|
3605
|
+
return undefined;
|
|
3606
|
+
}
|
|
3607
|
+
function legacyGeneratedScriptNames(scripts) {
|
|
3608
|
+
return Object.entries(legacyGeneratedPackageScripts)
|
|
3609
|
+
.filter(([name, command]) => scripts[name] === command)
|
|
3610
|
+
.map(([name]) => name);
|
|
3611
|
+
}
|
|
3612
|
+
async function appUsesDirectBunTestImports(targetDir, files) {
|
|
3613
|
+
for (const file of files) {
|
|
3614
|
+
if (!isTestSourceFile(file))
|
|
3615
|
+
continue;
|
|
3616
|
+
const source = await readFile(path.join(targetDir, file), "utf8");
|
|
3617
|
+
if (/\bfrom\s*["']bun:test["']/.test(source))
|
|
3618
|
+
return true;
|
|
3619
|
+
if (/\bimport\s*\(\s*["']bun:test["']\s*\)/.test(source))
|
|
3620
|
+
return true;
|
|
3621
|
+
}
|
|
3622
|
+
return false;
|
|
3623
|
+
}
|
|
3624
|
+
function hasCurrentGeneratedTsxScript(scripts) {
|
|
3625
|
+
return Object.values(currentGeneratedPackageScripts).some((command) => command.startsWith("tsx ") && Object.values(scripts).includes(command));
|
|
3626
|
+
}
|
|
3627
|
+
function missingTestSupportFiles(files) {
|
|
3628
|
+
return testSupportTemplateFiles
|
|
3629
|
+
.map((file) => file.path)
|
|
3630
|
+
.filter((file) => !files.includes(file));
|
|
3631
|
+
}
|
|
3632
|
+
async function createMissingTestSupportFiles(targetDir, files, shouldCreate) {
|
|
3633
|
+
if (!shouldCreate)
|
|
3634
|
+
return [];
|
|
3635
|
+
const createdSupportFiles = [];
|
|
3636
|
+
for (const file of testSupportTemplateFiles) {
|
|
3637
|
+
if (files.includes(file.path))
|
|
3638
|
+
continue;
|
|
3639
|
+
const destination = path.join(targetDir, file.path);
|
|
3640
|
+
await mkdir(path.dirname(destination), { recursive: true });
|
|
3641
|
+
await writeFile(destination, file.content);
|
|
3642
|
+
createdSupportFiles.push(file.path);
|
|
3643
|
+
}
|
|
3644
|
+
return createdSupportFiles;
|
|
3645
|
+
}
|
|
3646
|
+
function packageScriptFixMessage(updatedScripts, createdSupportFiles, addedTsx) {
|
|
3647
|
+
const parts = [
|
|
3648
|
+
`Updated generated package scripts: ${updatedScripts.join(", ")}.`,
|
|
3649
|
+
];
|
|
3650
|
+
if (createdSupportFiles.length > 0) {
|
|
3651
|
+
parts.push(`Restored ${createdSupportFiles.join(", ")}.`);
|
|
3652
|
+
}
|
|
3653
|
+
if (addedTsx) {
|
|
3654
|
+
parts.push('Added "tsx" to devDependencies.');
|
|
3655
|
+
}
|
|
3656
|
+
return parts.join(" ");
|
|
2963
3657
|
}
|
|
2964
3658
|
function unmatchedRouteDiagnostic(routeHandler) {
|
|
2965
3659
|
if (routeHandler.source === "next-route") {
|
|
@@ -3398,14 +4092,15 @@ async function fixUnregisteredTasks(targetDir, files, drift, config) {
|
|
|
3398
4092
|
});
|
|
3399
4093
|
}
|
|
3400
4094
|
/**
|
|
3401
|
-
* Wire fully unregistered feature listener registries into the
|
|
3402
|
-
*
|
|
3403
|
-
* generates, so both
|
|
4095
|
+
* Wire fully unregistered feature listener registries into the central
|
|
4096
|
+
* listener registry and ensure the providers file registers that central
|
|
4097
|
+
* registry with the same block `beignet make listener` generates, so both
|
|
4098
|
+
* write paths stay byte-compatible.
|
|
3404
4099
|
*
|
|
3405
|
-
* Bails without writing when the providers file or
|
|
3406
|
-
*
|
|
3407
|
-
* against, or when the registry name is already imported from another
|
|
3408
|
-
* The diagnostic stays in every bail case.
|
|
4100
|
+
* Bails without writing when the providers file or central listener registry
|
|
4101
|
+
* is customized beyond the known anchors, when the app has no eventBus port to
|
|
4102
|
+
* register against, or when the registry name is already imported from another
|
|
4103
|
+
* file. The diagnostic stays in every bail case.
|
|
3409
4104
|
*/
|
|
3410
4105
|
async function fixUnregisteredListeners(targetDir, files, drift, config) {
|
|
3411
4106
|
const candidates = drift.listeners.unregistered.filter((entry) => entry.fullyUnregistered);
|
|
@@ -3428,41 +4123,72 @@ async function fixUnregisteredListeners(targetDir, files, drift, config) {
|
|
|
3428
4123
|
if (!/\beventBus\s*[:?]/.test(portsSource))
|
|
3429
4124
|
return undefined;
|
|
3430
4125
|
const providersPath = path.join(targetDir, providersFile);
|
|
3431
|
-
const
|
|
3432
|
-
|
|
3433
|
-
const
|
|
4126
|
+
const originalProviders = await readFile(providersPath, "utf8");
|
|
4127
|
+
const listenersFile = listenersFilePath(config);
|
|
4128
|
+
const listenersPath = path.join(targetDir, listenersFile);
|
|
4129
|
+
const originalListeners = files.includes(listenersFile)
|
|
4130
|
+
? await readFile(listenersPath, "utf8")
|
|
4131
|
+
: undefined;
|
|
4132
|
+
let nextListeners = originalListeners;
|
|
4133
|
+
const candidateNames = [];
|
|
3434
4134
|
for (const { registry } of candidates) {
|
|
3435
|
-
|
|
4135
|
+
candidateNames.push(registry.registryName);
|
|
4136
|
+
const imported = nextListeners === undefined
|
|
4137
|
+
? undefined
|
|
4138
|
+
: parseNamedImportSources(nextListeners).get(registry.registryName);
|
|
3436
4139
|
if (imported) {
|
|
3437
|
-
const importedFile = sourceFileFromImport(imported.sourcePath,
|
|
4140
|
+
const importedFile = sourceFileFromImport(imported.sourcePath, listenersFile, files);
|
|
3438
4141
|
if (importedFile !== registry.indexFile)
|
|
3439
4142
|
return undefined;
|
|
3440
4143
|
}
|
|
3441
|
-
const
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
4144
|
+
const listenerModule = `@/${modulePath(registry.indexFile)}`;
|
|
4145
|
+
const result = nextListeners === undefined
|
|
4146
|
+
? {
|
|
4147
|
+
kind: "updated",
|
|
4148
|
+
source: createListenersRegistrySource({
|
|
4149
|
+
registryName: registry.registryName,
|
|
4150
|
+
listenerModule,
|
|
4151
|
+
}),
|
|
4152
|
+
}
|
|
4153
|
+
: updateListenersRegistrySource(nextListeners, {
|
|
4154
|
+
registryName: registry.registryName,
|
|
4155
|
+
listenerModule,
|
|
4156
|
+
});
|
|
3447
4157
|
if (result.kind === "missing")
|
|
3448
4158
|
return undefined;
|
|
3449
4159
|
if (result.kind === "updated") {
|
|
3450
|
-
|
|
3451
|
-
registeredNames.push(registry.registryName);
|
|
4160
|
+
nextListeners = result.source;
|
|
3452
4161
|
}
|
|
3453
4162
|
}
|
|
3454
|
-
if (
|
|
4163
|
+
if (nextListeners === undefined || candidateNames.length === 0) {
|
|
3455
4164
|
return undefined;
|
|
3456
|
-
|
|
4165
|
+
}
|
|
4166
|
+
const providerResult = wireListenersProviderSource(originalProviders, {
|
|
4167
|
+
registryName: candidateNames[0] ?? "listeners",
|
|
4168
|
+
featureKebab: "app",
|
|
4169
|
+
listenerModule: `@/${modulePath(listenersFile)}`,
|
|
4170
|
+
config,
|
|
4171
|
+
});
|
|
4172
|
+
if (providerResult.kind === "missing")
|
|
4173
|
+
return undefined;
|
|
4174
|
+
const nextProviders = providerResult.kind === "updated"
|
|
4175
|
+
? providerResult.source
|
|
4176
|
+
: originalProviders;
|
|
4177
|
+
if (nextListeners === originalListeners &&
|
|
4178
|
+
nextProviders === originalProviders) {
|
|
4179
|
+
return undefined;
|
|
4180
|
+
}
|
|
4181
|
+
await mkdir(path.dirname(listenersPath), { recursive: true });
|
|
4182
|
+
await writeFile(listenersPath, nextListeners);
|
|
4183
|
+
if (nextProviders !== originalProviders) {
|
|
4184
|
+
await writeFile(providersPath, nextProviders);
|
|
4185
|
+
}
|
|
3457
4186
|
return {
|
|
3458
4187
|
code: "BEIGNET_LISTENER_UNREGISTERED",
|
|
3459
|
-
file:
|
|
3460
|
-
message: `Registered ${
|
|
4188
|
+
file: listenersFile,
|
|
4189
|
+
message: `Registered ${candidateNames.join(", ")} in ${listenersFile} and wired listeners with registerListeners(...) in ${providersFile}.`,
|
|
3461
4190
|
};
|
|
3462
4191
|
}
|
|
3463
|
-
function listenerFeatureKebab(indexFile) {
|
|
3464
|
-
return path.posix.basename(path.posix.dirname(path.posix.dirname(normalizePath(indexFile))));
|
|
3465
|
-
}
|
|
3466
4192
|
async function fixUnregisteredOutboxEntries(targetDir, files, drift, config) {
|
|
3467
4193
|
if (!drift.outbox.exists)
|
|
3468
4194
|
return [];
|
|
@@ -3873,7 +4599,9 @@ async function inspectResourceSlices(targetDir, files, config, convention, stric
|
|
|
3873
4599
|
sourceIncludesAny(portSource, [
|
|
3874
4600
|
`update(input: Update${singularPascal}Input)`,
|
|
3875
4601
|
`update(input: Update${singularPascal}RepositoryInput)`,
|
|
4602
|
+
`update(input: Update${singularPascal}Input, scope: TenantScope)`,
|
|
3876
4603
|
"delete(id: string): Promise<boolean>",
|
|
4604
|
+
"delete(id: string, scope: TenantScope): Promise<boolean>",
|
|
3877
4605
|
]);
|
|
3878
4606
|
if (!hasCrudSignals)
|
|
3879
4607
|
continue;
|
|
@@ -3908,6 +4636,7 @@ async function inspectResourceSlices(targetDir, files, config, convention, stric
|
|
|
3908
4636
|
alternatives: [
|
|
3909
4637
|
`findById(id: string): Promise<${singularPascal} | null>`,
|
|
3910
4638
|
`findById(id: string, filter: ${singularPascal}TenantFilter): Promise<${singularPascal} | null>`,
|
|
4639
|
+
`findById(id: string, scope: TenantScope): Promise<${singularPascal} | null>`,
|
|
3911
4640
|
],
|
|
3912
4641
|
},
|
|
3913
4642
|
{
|
|
@@ -3915,6 +4644,7 @@ async function inspectResourceSlices(targetDir, files, config, convention, stric
|
|
|
3915
4644
|
alternatives: [
|
|
3916
4645
|
`update(input: Update${singularPascal}Input)`,
|
|
3917
4646
|
`update(input: Update${singularPascal}RepositoryInput)`,
|
|
4647
|
+
`update(input: Update${singularPascal}Input, scope: TenantScope)`,
|
|
3918
4648
|
],
|
|
3919
4649
|
},
|
|
3920
4650
|
{
|
|
@@ -3922,6 +4652,7 @@ async function inspectResourceSlices(targetDir, files, config, convention, stric
|
|
|
3922
4652
|
alternatives: [
|
|
3923
4653
|
"delete(id: string): Promise<boolean>",
|
|
3924
4654
|
`delete(id: string, filter: ${singularPascal}TenantFilter): Promise<boolean>`,
|
|
4655
|
+
"delete(id: string, scope: TenantScope): Promise<boolean>",
|
|
3925
4656
|
],
|
|
3926
4657
|
},
|
|
3927
4658
|
];
|
|
@@ -4036,6 +4767,15 @@ async function readSourceIfPresent(targetDir, files, file) {
|
|
|
4036
4767
|
function sourceIncludesAny(source, snippets) {
|
|
4037
4768
|
return source ? snippets.some((snippet) => source.includes(snippet)) : false;
|
|
4038
4769
|
}
|
|
4770
|
+
function countSourceOccurrences(source, snippet) {
|
|
4771
|
+
let count = 0;
|
|
4772
|
+
let index = source.indexOf(snippet);
|
|
4773
|
+
while (index !== -1) {
|
|
4774
|
+
count += 1;
|
|
4775
|
+
index = source.indexOf(snippet, index + snippet.length);
|
|
4776
|
+
}
|
|
4777
|
+
return count;
|
|
4778
|
+
}
|
|
4039
4779
|
function sourceIncludesAll(source, snippets) {
|
|
4040
4780
|
return snippets.every((snippet) => source.includes(snippet));
|
|
4041
4781
|
}
|