@beignet/cli 0.0.48 → 0.0.50
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 +54 -0
- package/README.md +117 -22
- package/dist/analysis/contract-factories.d.ts +17 -0
- package/dist/analysis/contract-factories.d.ts.map +1 -0
- package/dist/analysis/contract-factories.js +176 -0
- package/dist/analysis/contract-factories.js.map +1 -0
- package/dist/analysis/layers.d.ts +5 -0
- package/dist/analysis/layers.d.ts.map +1 -0
- package/dist/analysis/layers.js +172 -0
- package/dist/analysis/layers.js.map +1 -0
- package/dist/analysis/port-wiring.d.ts +23 -0
- package/dist/analysis/port-wiring.d.ts.map +1 -0
- package/dist/analysis/port-wiring.js +379 -0
- package/dist/analysis/port-wiring.js.map +1 -0
- package/dist/analysis/source-index.d.ts +5 -0
- package/dist/analysis/source-index.d.ts.map +1 -1
- package/dist/analysis/source-index.js +171 -42
- package/dist/analysis/source-index.js.map +1 -1
- package/dist/analysis/workspace.d.ts +11 -4
- package/dist/analysis/workspace.d.ts.map +1 -1
- package/dist/analysis/workspace.js +78 -8
- package/dist/analysis/workspace.js.map +1 -1
- package/dist/app-map-changes.d.ts +103 -0
- package/dist/app-map-changes.d.ts.map +1 -0
- package/dist/app-map-changes.js +949 -0
- package/dist/app-map-changes.js.map +1 -0
- package/dist/app-map.d.ts.map +1 -1
- package/dist/app-map.js +311 -92
- package/dist/app-map.js.map +1 -1
- package/dist/check.js +37 -3
- package/dist/check.js.map +1 -1
- package/dist/explain.js +4 -4
- package/dist/explain.js.map +1 -1
- package/dist/git-changes.d.ts +30 -0
- package/dist/git-changes.d.ts.map +1 -0
- package/dist/git-changes.js +367 -0
- package/dist/git-changes.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +26 -1
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +706 -186
- package/dist/inspect.js.map +1 -1
- package/dist/lib.d.ts +3 -0
- package/dist/lib.d.ts.map +1 -1
- package/dist/lib.js +1 -0
- package/dist/lib.js.map +1 -1
- package/dist/lint.d.ts.map +1 -1
- package/dist/lint.js +309 -487
- package/dist/lint.js.map +1 -1
- package/dist/make/inbox.d.ts.map +1 -1
- package/dist/make/inbox.js +11 -4
- package/dist/make/inbox.js.map +1 -1
- package/dist/make/payments.js +2 -2
- package/dist/make/shared.d.ts.map +1 -1
- package/dist/make/shared.js +31 -27
- package/dist/make/shared.js.map +1 -1
- package/dist/make.d.ts.map +1 -1
- package/dist/make.js +144 -8
- package/dist/make.js.map +1 -1
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +43 -21
- package/dist/mcp.js.map +1 -1
- package/dist/provider-add.d.ts.map +1 -1
- package/dist/provider-add.js +142 -7
- package/dist/provider-add.js.map +1 -1
- package/dist/registry-edits.d.ts +7 -0
- package/dist/registry-edits.d.ts.map +1 -1
- package/dist/registry-edits.js +237 -53
- package/dist/registry-edits.js.map +1 -1
- package/dist/templates/agents.d.ts.map +1 -1
- package/dist/templates/agents.js +37 -1
- package/dist/templates/agents.js.map +1 -1
- package/dist/templates/base.d.ts.map +1 -1
- package/dist/templates/base.js +13 -2
- package/dist/templates/base.js.map +1 -1
- package/dist/templates/server.d.ts.map +1 -1
- package/dist/templates/server.js +2 -0
- package/dist/templates/server.js.map +1 -1
- package/dist/templates/shared.d.ts +1 -0
- package/dist/templates/shared.d.ts.map +1 -1
- package/dist/templates/shared.js +3 -2
- package/dist/templates/shared.js.map +1 -1
- package/package.json +7 -6
- package/skills/app-structure/SKILL.md +71 -7
- package/src/analysis/contract-factories.ts +256 -0
- package/src/analysis/layers.ts +266 -0
- package/src/analysis/port-wiring.ts +506 -0
- package/src/analysis/source-index.ts +225 -57
- package/src/analysis/workspace.ts +134 -9
- package/src/app-map-changes.ts +1462 -0
- package/src/app-map.ts +418 -109
- package/src/check.ts +51 -3
- package/src/explain.ts +4 -4
- package/src/git-changes.ts +511 -0
- package/src/index.ts +39 -1
- package/src/inspect.ts +1005 -238
- package/src/lib.ts +21 -0
- package/src/lint.ts +393 -643
- package/src/make/inbox.ts +11 -4
- package/src/make/payments.ts +2 -2
- package/src/make/shared.ts +31 -27
- package/src/make.ts +225 -8
- package/src/mcp.ts +51 -24
- package/src/provider-add.ts +186 -7
- package/src/registry-edits.ts +313 -63
- package/src/templates/agents.ts +37 -1
- package/src/templates/base.ts +13 -2
- package/src/templates/server.ts +2 -0
- package/src/templates/shared.ts +3 -2
package/src/provider-add.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import ts from "typescript";
|
|
3
4
|
import {
|
|
4
5
|
type ProviderPresetName,
|
|
5
6
|
providerPresetChoices,
|
|
@@ -164,7 +165,10 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
|
|
|
164
165
|
"Add a worker entrypoint that runs createBullMQJobWorker(...) in a worker process; dispatched jobs do not execute without one. See the jobs docs page.",
|
|
165
166
|
"Run beignet provider audit and beignet doctor --strict.",
|
|
166
167
|
],
|
|
167
|
-
conflictingProviderEntries: [
|
|
168
|
+
conflictingProviderEntries: [
|
|
169
|
+
"createInngestJobsProvider",
|
|
170
|
+
"inlineJobsProvider",
|
|
171
|
+
],
|
|
168
172
|
},
|
|
169
173
|
"jobs-inngest": {
|
|
170
174
|
name: "jobs-inngest",
|
|
@@ -215,7 +219,10 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
|
|
|
215
219
|
"For non-Next runtimes, call createAppInngestFunctions(...) after creating the app server, then mount the result with Inngest's host adapter.",
|
|
216
220
|
"Run beignet provider audit and beignet doctor --strict.",
|
|
217
221
|
],
|
|
218
|
-
conflictingProviderEntries: [
|
|
222
|
+
conflictingProviderEntries: [
|
|
223
|
+
"createBullMQJobsProvider",
|
|
224
|
+
"inlineJobsProvider",
|
|
225
|
+
],
|
|
219
226
|
},
|
|
220
227
|
"mail-resend": {
|
|
221
228
|
name: "mail-resend",
|
|
@@ -276,7 +283,14 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
|
|
|
276
283
|
importName: "MailerPort",
|
|
277
284
|
importFrom: "@beignet/core/mail",
|
|
278
285
|
},
|
|
279
|
-
envVars: [
|
|
286
|
+
envVars: [
|
|
287
|
+
"MAIL_HOST",
|
|
288
|
+
"MAIL_PORT",
|
|
289
|
+
"MAIL_USER",
|
|
290
|
+
"MAIL_PASS",
|
|
291
|
+
"MAIL_FROM",
|
|
292
|
+
"MAIL_REQUIRE_TLS",
|
|
293
|
+
],
|
|
280
294
|
envExample: [
|
|
281
295
|
"# SMTP mail provider",
|
|
282
296
|
"MAIL_HOST=smtp.example.com",
|
|
@@ -284,6 +298,7 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
|
|
|
284
298
|
"MAIL_USER=dev-placeholder-change-me",
|
|
285
299
|
"MAIL_PASS=dev-placeholder-change-me",
|
|
286
300
|
"MAIL_FROM=Beignet App <noreply@example.com>",
|
|
301
|
+
"MAIL_REQUIRE_TLS=true",
|
|
287
302
|
"",
|
|
288
303
|
].join("\n"),
|
|
289
304
|
docs: [
|
|
@@ -293,12 +308,13 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
|
|
|
293
308
|
"- Peer dependency: `nodemailer`",
|
|
294
309
|
"- The preset wires `createSmtpMailProvider()` in `server/providers.ts` and defers `mailer` in `infra/port-wiring.ts`.",
|
|
295
310
|
"- `.env.example` ships dev placeholders so the app boots; set real `MAIL_*` values before sending mail.",
|
|
311
|
+
"- Encrypted transport is required by default: port 465 uses implicit TLS and other ports must negotiate STARTTLS.",
|
|
296
312
|
"- Use `ctx.ports.mailer` in app code; `ctx.ports.smtp` is available as an escape hatch.",
|
|
297
313
|
"",
|
|
298
314
|
].join("\n"),
|
|
299
315
|
nextSteps: [
|
|
300
316
|
"Run your package manager install command.",
|
|
301
|
-
"Set MAIL_HOST, MAIL_PORT, MAIL_USER, MAIL_PASS, and MAIL_FROM in .env.local or your deployment environment.",
|
|
317
|
+
"Set MAIL_HOST, MAIL_PORT, MAIL_USER, MAIL_PASS, and MAIL_FROM in .env.local or your deployment environment; keep MAIL_REQUIRE_TLS=true outside trusted local capture services.",
|
|
302
318
|
"Run beignet provider audit and beignet doctor --strict.",
|
|
303
319
|
],
|
|
304
320
|
conflictingProviderEntries: [
|
|
@@ -561,6 +577,7 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
|
|
|
561
577
|
dependencies: (packageJson) => ({
|
|
562
578
|
"@beignet/provider-storage-s3": beignetDependencyVersion(packageJson),
|
|
563
579
|
"@aws-sdk/client-s3": externalVersions.awsSdkS3,
|
|
580
|
+
"@aws-sdk/lib-storage": externalVersions.awsSdkLibStorage,
|
|
564
581
|
"@aws-sdk/s3-request-presigner": externalVersions.awsSdkS3,
|
|
565
582
|
}),
|
|
566
583
|
appPort: {
|
|
@@ -601,7 +618,7 @@ const presets: Record<ProviderPresetName, ProviderPreset> = {
|
|
|
601
618
|
"## S3-compatible storage",
|
|
602
619
|
"",
|
|
603
620
|
"- Package: `@beignet/provider-storage-s3`",
|
|
604
|
-
"- Peer dependencies: `@aws-sdk/client-s3
|
|
621
|
+
"- Peer dependencies: `@aws-sdk/client-s3`, `@aws-sdk/lib-storage`, and `@aws-sdk/s3-request-presigner`",
|
|
605
622
|
"- The preset wires `createS3StorageProvider()` in `server/providers.ts` and defers `storage` in `infra/port-wiring.ts`.",
|
|
606
623
|
"- Set `STORAGE_S3_BUCKET` before booting the app; set endpoint and credentials when using an S3-compatible host outside AWS defaults.",
|
|
607
624
|
"- Use `ctx.ports.storage` in app code; `ctx.ports.s3Storage` is available as an escape hatch.",
|
|
@@ -1179,9 +1196,13 @@ function updatePackageJson(
|
|
|
1179
1196
|
}
|
|
1180
1197
|
|
|
1181
1198
|
function updateServerProviders(source: string, preset: ProviderPreset): string {
|
|
1182
|
-
|
|
1199
|
+
const withoutGeneratedFallback =
|
|
1200
|
+
preset.appPort.key === "jobs"
|
|
1201
|
+
? removeGeneratedInlineJobsProvider(source)
|
|
1202
|
+
: source;
|
|
1203
|
+
assertNoConflictingProviders(withoutGeneratedFallback, preset);
|
|
1183
1204
|
|
|
1184
|
-
let next =
|
|
1205
|
+
let next = withoutGeneratedFallback;
|
|
1185
1206
|
for (const importLine of preset.additionalImports ?? []) {
|
|
1186
1207
|
if (!next.includes(importLine)) next = insertAfterImports(next, importLine);
|
|
1187
1208
|
}
|
|
@@ -1205,6 +1226,164 @@ function updateServerProviders(source: string, preset: ProviderPreset): string {
|
|
|
1205
1226
|
return result.kind === "updated" ? result.source : next;
|
|
1206
1227
|
}
|
|
1207
1228
|
|
|
1229
|
+
function removeGeneratedInlineJobsProvider(source: string): string {
|
|
1230
|
+
const marker = "// beignet:generated-inline-jobs-provider";
|
|
1231
|
+
const sourceFile = ts.createSourceFile(
|
|
1232
|
+
"providers.ts",
|
|
1233
|
+
source,
|
|
1234
|
+
ts.ScriptTarget.Latest,
|
|
1235
|
+
true,
|
|
1236
|
+
ts.ScriptKind.TS,
|
|
1237
|
+
);
|
|
1238
|
+
const statement = sourceFile.statements.find((candidate) => {
|
|
1239
|
+
if (!ts.isVariableStatement(candidate)) return false;
|
|
1240
|
+
if (candidate.declarationList.declarations.length !== 1) return false;
|
|
1241
|
+
const statementStart = candidate.getStart(sourceFile);
|
|
1242
|
+
const markerStart = source.lastIndexOf(marker, statementStart);
|
|
1243
|
+
if (
|
|
1244
|
+
markerStart < 0 ||
|
|
1245
|
+
source.slice(markerStart + marker.length, statementStart).trim() !== ""
|
|
1246
|
+
) {
|
|
1247
|
+
return false;
|
|
1248
|
+
}
|
|
1249
|
+
const declaration = candidate.declarationList.declarations.find(
|
|
1250
|
+
(item) =>
|
|
1251
|
+
ts.isIdentifier(item.name) && item.name.text === "inlineJobsProvider",
|
|
1252
|
+
);
|
|
1253
|
+
if (!declaration?.initializer) return false;
|
|
1254
|
+
const text = declaration.initializer.getText(sourceFile);
|
|
1255
|
+
return (
|
|
1256
|
+
/\bcreateProvider\b/.test(text) &&
|
|
1257
|
+
/\bcreateInlineJobDispatcher\b/.test(text) &&
|
|
1258
|
+
/\bname\s*:\s*["']inline-jobs["']/.test(text)
|
|
1259
|
+
);
|
|
1260
|
+
});
|
|
1261
|
+
if (!statement) return source;
|
|
1262
|
+
|
|
1263
|
+
const statementStart = statement.getStart(sourceFile);
|
|
1264
|
+
const markerStart = source.lastIndexOf(marker, statementStart);
|
|
1265
|
+
const removeStart =
|
|
1266
|
+
markerStart >= 0 &&
|
|
1267
|
+
source.slice(markerStart + marker.length, statementStart).trim() === ""
|
|
1268
|
+
? markerStart
|
|
1269
|
+
: statementStart;
|
|
1270
|
+
let next = `${source.slice(0, removeStart)}${source.slice(statement.end)}`;
|
|
1271
|
+
const registry = arrayInitializerInfo(next, "providers");
|
|
1272
|
+
if (!registry) {
|
|
1273
|
+
throw new Error(
|
|
1274
|
+
"Could not remove the generated inline jobs provider because `export const providers = [...]` is missing.",
|
|
1275
|
+
);
|
|
1276
|
+
}
|
|
1277
|
+
const lines = registry.text.split(/\r?\n/);
|
|
1278
|
+
const entryIndex = lines.findIndex(
|
|
1279
|
+
(line) =>
|
|
1280
|
+
line.trim() === "inlineJobsProvider," ||
|
|
1281
|
+
line.trim() === "inlineJobsProvider",
|
|
1282
|
+
);
|
|
1283
|
+
let nextRegistry: string | undefined;
|
|
1284
|
+
if (entryIndex >= 0) {
|
|
1285
|
+
lines.splice(entryIndex, 1);
|
|
1286
|
+
nextRegistry = lines.join(registry.text.includes("\r\n") ? "\r\n" : "\n");
|
|
1287
|
+
} else if (!registry.text.includes("\n")) {
|
|
1288
|
+
const beforeFollowingEntry = /\binlineJobsProvider\s*,\s*/;
|
|
1289
|
+
const afterPreviousEntry = /,\s*inlineJobsProvider\b/;
|
|
1290
|
+
const onlyEntry = /^\[\s*inlineJobsProvider\s*\]$/;
|
|
1291
|
+
if (onlyEntry.test(registry.text)) nextRegistry = "[]";
|
|
1292
|
+
else if (beforeFollowingEntry.test(registry.text)) {
|
|
1293
|
+
nextRegistry = registry.text.replace(beforeFollowingEntry, "");
|
|
1294
|
+
} else if (afterPreviousEntry.test(registry.text)) {
|
|
1295
|
+
nextRegistry = registry.text.replace(afterPreviousEntry, "");
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
if (nextRegistry === undefined) {
|
|
1299
|
+
throw new Error(
|
|
1300
|
+
"Could not remove the generated inline jobs provider from `export const providers = [...]`.",
|
|
1301
|
+
);
|
|
1302
|
+
}
|
|
1303
|
+
next = `${next.slice(0, registry.start)}${nextRegistry}${next.slice(registry.end)}`;
|
|
1304
|
+
|
|
1305
|
+
return removeNamedProviderImport(
|
|
1306
|
+
next,
|
|
1307
|
+
"createInlineJobDispatcher",
|
|
1308
|
+
"@beignet/core/jobs",
|
|
1309
|
+
);
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
function removeNamedProviderImport(
|
|
1313
|
+
source: string,
|
|
1314
|
+
name: string,
|
|
1315
|
+
moduleName: string,
|
|
1316
|
+
): string {
|
|
1317
|
+
const escapedModule = moduleName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1318
|
+
const pattern = new RegExp(
|
|
1319
|
+
`import \\{([^}]*)\\} from ["']${escapedModule}["'];`,
|
|
1320
|
+
);
|
|
1321
|
+
const match = pattern.exec(source);
|
|
1322
|
+
if (!match) return source;
|
|
1323
|
+
const names = match[1]
|
|
1324
|
+
.split(",")
|
|
1325
|
+
.map((entry) => entry.trim())
|
|
1326
|
+
.filter(Boolean);
|
|
1327
|
+
if (!names.includes(name)) return source;
|
|
1328
|
+
if (hasIdentifierReferenceOutsideImport(source, name, moduleName)) {
|
|
1329
|
+
return source;
|
|
1330
|
+
}
|
|
1331
|
+
const remaining = names.filter((entry) => entry !== name);
|
|
1332
|
+
if (remaining.length === 0) {
|
|
1333
|
+
const lineBreak = source.startsWith("\r\n", match.index + match[0].length)
|
|
1334
|
+
? "\r\n"
|
|
1335
|
+
: source[match.index + match[0].length] === "\n"
|
|
1336
|
+
? "\n"
|
|
1337
|
+
: "";
|
|
1338
|
+
return `${source.slice(0, match.index)}${source.slice(match.index + match[0].length + lineBreak.length)}`;
|
|
1339
|
+
}
|
|
1340
|
+
const indent = match[1].match(/\n([\t ]+)/)?.[1] ?? "\t";
|
|
1341
|
+
const replacement = match[1].includes("\n")
|
|
1342
|
+
? `import {\n${indent}${remaining.join(`,\n${indent}`)},\n} from "${moduleName}";`
|
|
1343
|
+
: `import { ${remaining.join(", ")} } from "${moduleName}";`;
|
|
1344
|
+
return `${source.slice(0, match.index)}${replacement}${source.slice(match.index + match[0].length)}`;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
function hasIdentifierReferenceOutsideImport(
|
|
1348
|
+
source: string,
|
|
1349
|
+
name: string,
|
|
1350
|
+
moduleName: string,
|
|
1351
|
+
): boolean {
|
|
1352
|
+
const sourceFile = ts.createSourceFile(
|
|
1353
|
+
"providers.ts",
|
|
1354
|
+
source,
|
|
1355
|
+
ts.ScriptTarget.Latest,
|
|
1356
|
+
true,
|
|
1357
|
+
ts.ScriptKind.TS,
|
|
1358
|
+
);
|
|
1359
|
+
const importDeclaration = sourceFile.statements.find(
|
|
1360
|
+
(statement): statement is ts.ImportDeclaration =>
|
|
1361
|
+
ts.isImportDeclaration(statement) &&
|
|
1362
|
+
ts.isStringLiteral(statement.moduleSpecifier) &&
|
|
1363
|
+
statement.moduleSpecifier.text === moduleName &&
|
|
1364
|
+
Boolean(
|
|
1365
|
+
statement.importClause?.namedBindings &&
|
|
1366
|
+
ts.isNamedImports(statement.importClause.namedBindings) &&
|
|
1367
|
+
statement.importClause.namedBindings.elements.some(
|
|
1368
|
+
(element) => !element.propertyName && element.name.text === name,
|
|
1369
|
+
),
|
|
1370
|
+
),
|
|
1371
|
+
);
|
|
1372
|
+
if (!importDeclaration) return true;
|
|
1373
|
+
|
|
1374
|
+
let referenced = false;
|
|
1375
|
+
const visit = (node: ts.Node): void => {
|
|
1376
|
+
if (node === importDeclaration || referenced) return;
|
|
1377
|
+
if (ts.isIdentifier(node) && node.text === name) {
|
|
1378
|
+
referenced = true;
|
|
1379
|
+
return;
|
|
1380
|
+
}
|
|
1381
|
+
ts.forEachChild(node, visit);
|
|
1382
|
+
};
|
|
1383
|
+
ts.forEachChild(sourceFile, visit);
|
|
1384
|
+
return referenced;
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1208
1387
|
function assertNoConflictingProviders(
|
|
1209
1388
|
source: string,
|
|
1210
1389
|
preset: ProviderPreset,
|
package/src/registry-edits.ts
CHANGED
|
@@ -308,24 +308,324 @@ export function outboxRegistryValueInfo(
|
|
|
308
308
|
source: string,
|
|
309
309
|
property: "events" | "jobs",
|
|
310
310
|
): InitializerInfo | undefined {
|
|
311
|
-
const
|
|
312
|
-
if (!
|
|
311
|
+
const match = outboxRegistryPropertyAnalysis(source, property);
|
|
312
|
+
if (!match?.value || match.indeterminate) return undefined;
|
|
313
|
+
const start = match.value.getStart(match.sourceFile);
|
|
314
|
+
return {
|
|
315
|
+
text: source.slice(start, match.value.end),
|
|
316
|
+
start,
|
|
317
|
+
end: match.value.end,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export type OutboxRegistryEntryState =
|
|
322
|
+
| "absent"
|
|
323
|
+
| "empty"
|
|
324
|
+
| "present"
|
|
325
|
+
| "indeterminate";
|
|
326
|
+
|
|
327
|
+
/** Static state of one outbox registry property. */
|
|
328
|
+
export function outboxRegistryEntryState(
|
|
329
|
+
source: string,
|
|
330
|
+
property: "events" | "jobs",
|
|
331
|
+
): OutboxRegistryEntryState {
|
|
332
|
+
const match = outboxRegistryPropertyAnalysis(source, property);
|
|
333
|
+
if (!match) return "absent";
|
|
334
|
+
if (match.indeterminate) return "indeterminate";
|
|
335
|
+
if (!match.value) return "empty";
|
|
336
|
+
return resolvedRegistryExpressions(match.value, match.sourceFile).length > 0
|
|
337
|
+
? "present"
|
|
338
|
+
: "empty";
|
|
339
|
+
}
|
|
313
340
|
|
|
314
|
-
|
|
315
|
-
|
|
341
|
+
/** Whether a registry property is non-empty or cannot be proven empty. */
|
|
342
|
+
export function outboxRegistryHasEntries(
|
|
343
|
+
source: string,
|
|
344
|
+
property: "events" | "jobs",
|
|
345
|
+
): boolean {
|
|
346
|
+
const state = outboxRegistryEntryState(source, property);
|
|
347
|
+
return state === "present" || state === "indeterminate";
|
|
348
|
+
}
|
|
316
349
|
|
|
317
|
-
|
|
318
|
-
|
|
350
|
+
/** Identifiers referenced by a registry property after resolving local array aliases. */
|
|
351
|
+
export function outboxRegistryIdentifiers(
|
|
352
|
+
source: string,
|
|
353
|
+
property: "events" | "jobs",
|
|
354
|
+
): Set<string> {
|
|
355
|
+
const match = outboxRegistryPropertyAnalysis(source, property);
|
|
356
|
+
if (!match?.value || match.indeterminate) return new Set();
|
|
357
|
+
|
|
358
|
+
const identifiers = new Set<string>();
|
|
359
|
+
for (const expression of resolvedRegistryExpressions(
|
|
360
|
+
match.value,
|
|
361
|
+
match.sourceFile,
|
|
362
|
+
)) {
|
|
363
|
+
const start = expression.getStart(match.sourceFile);
|
|
364
|
+
for (const identifier of identifiersFromArrayExpression(
|
|
365
|
+
source.slice(start, expression.end),
|
|
366
|
+
)) {
|
|
367
|
+
identifiers.add(identifier);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return identifiers;
|
|
371
|
+
}
|
|
319
372
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
373
|
+
type OutboxRegistryPropertyAnalysis = {
|
|
374
|
+
sourceFile: ts.SourceFile;
|
|
375
|
+
value?: ts.Expression;
|
|
376
|
+
indeterminate: boolean;
|
|
377
|
+
};
|
|
323
378
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
379
|
+
function outboxRegistryPropertyAnalysis(
|
|
380
|
+
source: string,
|
|
381
|
+
property: "events" | "jobs",
|
|
382
|
+
): OutboxRegistryPropertyAnalysis | undefined {
|
|
383
|
+
const sourceFile = ts.createSourceFile(
|
|
384
|
+
"outbox-registry.ts",
|
|
385
|
+
source,
|
|
386
|
+
ts.ScriptTarget.Latest,
|
|
387
|
+
true,
|
|
388
|
+
ts.ScriptKind.TS,
|
|
389
|
+
);
|
|
390
|
+
const initializers = registryInitializers(sourceFile);
|
|
391
|
+
const selected = selectOutboxRegistryCall(sourceFile);
|
|
392
|
+
if (!selected.call) {
|
|
393
|
+
return selected.indeterminate
|
|
394
|
+
? { sourceFile, indeterminate: true }
|
|
395
|
+
: undefined;
|
|
396
|
+
}
|
|
327
397
|
|
|
328
|
-
|
|
398
|
+
const firstArgument = selected.call.arguments[0];
|
|
399
|
+
const object = firstArgument
|
|
400
|
+
? resolveRegistryExpression(firstArgument, initializers)
|
|
401
|
+
: undefined;
|
|
402
|
+
if (!object || !ts.isObjectLiteralExpression(object)) {
|
|
403
|
+
return { sourceFile, indeterminate: true };
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
let value: ts.Expression | undefined;
|
|
407
|
+
let indeterminate = false;
|
|
408
|
+
for (const member of object.properties) {
|
|
409
|
+
if (ts.isSpreadAssignment(member)) {
|
|
410
|
+
value = undefined;
|
|
411
|
+
indeterminate = true;
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
const name = member.name ? registryPropertyName(member.name) : undefined;
|
|
415
|
+
if (!name) {
|
|
416
|
+
value = undefined;
|
|
417
|
+
indeterminate = true;
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
if (name !== property) continue;
|
|
421
|
+
if (
|
|
422
|
+
ts.isPropertyAssignment(member) ||
|
|
423
|
+
ts.isShorthandPropertyAssignment(member)
|
|
424
|
+
) {
|
|
425
|
+
value = ts.isPropertyAssignment(member)
|
|
426
|
+
? member.initializer
|
|
427
|
+
: member.name;
|
|
428
|
+
indeterminate = false;
|
|
429
|
+
} else {
|
|
430
|
+
value = undefined;
|
|
431
|
+
indeterminate = true;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
if (value && !isStaticRegistryValue(value, initializers)) {
|
|
435
|
+
return { sourceFile, indeterminate: true };
|
|
436
|
+
}
|
|
437
|
+
return { sourceFile, value, indeterminate };
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function isStaticRegistryValue(
|
|
441
|
+
expression: ts.Expression,
|
|
442
|
+
initializers: Map<string, ts.Expression>,
|
|
443
|
+
visited = new Set<string>(),
|
|
444
|
+
): boolean {
|
|
445
|
+
const current = unwrapRegistryExpression(expression);
|
|
446
|
+
if (ts.isArrayLiteralExpression(current)) return true;
|
|
447
|
+
if (!ts.isIdentifier(current)) return false;
|
|
448
|
+
|
|
449
|
+
const initializer = initializers.get(current.text);
|
|
450
|
+
if (!initializer) return true;
|
|
451
|
+
if (visited.has(current.text)) return false;
|
|
452
|
+
|
|
453
|
+
const nextVisited = new Set(visited);
|
|
454
|
+
nextVisited.add(current.text);
|
|
455
|
+
return isStaticRegistryValue(initializer, initializers, nextVisited);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function selectOutboxRegistryCall(sourceFile: ts.SourceFile): {
|
|
459
|
+
call?: ts.CallExpression;
|
|
460
|
+
indeterminate: boolean;
|
|
461
|
+
} {
|
|
462
|
+
const exportedNames = locallyExportedRegistryNames(sourceFile);
|
|
463
|
+
const candidates: {
|
|
464
|
+
name?: string;
|
|
465
|
+
exported: boolean;
|
|
466
|
+
calls: ts.CallExpression[];
|
|
467
|
+
}[] = [];
|
|
468
|
+
|
|
469
|
+
for (const statement of sourceFile.statements) {
|
|
470
|
+
if (!ts.isVariableStatement(statement)) continue;
|
|
471
|
+
const statementExported = Boolean(
|
|
472
|
+
statement.modifiers?.some(
|
|
473
|
+
(modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword,
|
|
474
|
+
),
|
|
475
|
+
);
|
|
476
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
477
|
+
if (!declaration.initializer) continue;
|
|
478
|
+
const calls = outboxRegistryCalls(declaration.initializer);
|
|
479
|
+
if (calls.length === 0) continue;
|
|
480
|
+
const name = ts.isIdentifier(declaration.name)
|
|
481
|
+
? declaration.name.text
|
|
482
|
+
: undefined;
|
|
483
|
+
candidates.push({
|
|
484
|
+
...(name ? { name } : {}),
|
|
485
|
+
exported: statementExported || (name ? exportedNames.has(name) : false),
|
|
486
|
+
calls,
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const canonical = candidates.filter(
|
|
492
|
+
(candidate) => candidate.name === "outboxRegistry" && candidate.exported,
|
|
493
|
+
);
|
|
494
|
+
const exported = candidates.filter((candidate) => candidate.exported);
|
|
495
|
+
const selected =
|
|
496
|
+
canonical.length > 0
|
|
497
|
+
? canonical
|
|
498
|
+
: exported.length > 0
|
|
499
|
+
? exported
|
|
500
|
+
: candidates;
|
|
501
|
+
if (selected.length === 1 && selected[0].calls.length === 1) {
|
|
502
|
+
return { call: selected[0].calls[0], indeterminate: false };
|
|
503
|
+
}
|
|
504
|
+
if (selected.length > 0) return { indeterminate: true };
|
|
505
|
+
|
|
506
|
+
const fallback = outboxRegistryCalls(sourceFile);
|
|
507
|
+
return fallback.length === 1
|
|
508
|
+
? { call: fallback[0], indeterminate: false }
|
|
509
|
+
: { indeterminate: fallback.length > 0 };
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function locallyExportedRegistryNames(sourceFile: ts.SourceFile): Set<string> {
|
|
513
|
+
const names = new Set<string>();
|
|
514
|
+
for (const statement of sourceFile.statements) {
|
|
515
|
+
if (
|
|
516
|
+
!ts.isExportDeclaration(statement) ||
|
|
517
|
+
!statement.exportClause ||
|
|
518
|
+
!ts.isNamedExports(statement.exportClause) ||
|
|
519
|
+
statement.moduleSpecifier
|
|
520
|
+
) {
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
for (const element of statement.exportClause.elements) {
|
|
524
|
+
names.add(element.propertyName?.text ?? element.name.text);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return names;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function outboxRegistryCalls(node: ts.Node): ts.CallExpression[] {
|
|
531
|
+
const calls: ts.CallExpression[] = [];
|
|
532
|
+
const visit = (child: ts.Node): void => {
|
|
533
|
+
if (
|
|
534
|
+
ts.isCallExpression(child) &&
|
|
535
|
+
((ts.isIdentifier(child.expression) &&
|
|
536
|
+
child.expression.text === "defineOutboxRegistry") ||
|
|
537
|
+
(ts.isPropertyAccessExpression(child.expression) &&
|
|
538
|
+
child.expression.name.text === "defineOutboxRegistry"))
|
|
539
|
+
) {
|
|
540
|
+
calls.push(child);
|
|
541
|
+
}
|
|
542
|
+
ts.forEachChild(child, visit);
|
|
543
|
+
};
|
|
544
|
+
visit(node);
|
|
545
|
+
return calls;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function resolvedRegistryExpressions(
|
|
549
|
+
expression: ts.Expression,
|
|
550
|
+
sourceFile: ts.SourceFile,
|
|
551
|
+
): ts.Expression[] {
|
|
552
|
+
const initializers = registryInitializers(sourceFile);
|
|
553
|
+
|
|
554
|
+
const resolve = (
|
|
555
|
+
candidate: ts.Expression,
|
|
556
|
+
visited: Set<string>,
|
|
557
|
+
): ts.Expression[] => {
|
|
558
|
+
const current = unwrapRegistryExpression(candidate);
|
|
559
|
+
if (ts.isArrayLiteralExpression(current)) {
|
|
560
|
+
return current.elements.flatMap((element) =>
|
|
561
|
+
ts.isSpreadElement(element)
|
|
562
|
+
? resolve(element.expression, new Set(visited))
|
|
563
|
+
: ts.isExpression(element)
|
|
564
|
+
? [element]
|
|
565
|
+
: [],
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
if (ts.isIdentifier(current) && !visited.has(current.text)) {
|
|
569
|
+
const initializer = initializers.get(current.text);
|
|
570
|
+
if (initializer) {
|
|
571
|
+
const nextVisited = new Set(visited);
|
|
572
|
+
nextVisited.add(current.text);
|
|
573
|
+
return resolve(initializer, nextVisited);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
return [current];
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
return resolve(expression, new Set());
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function registryInitializers(
|
|
583
|
+
sourceFile: ts.SourceFile,
|
|
584
|
+
): Map<string, ts.Expression> {
|
|
585
|
+
const initializers = new Map<string, ts.Expression>();
|
|
586
|
+
for (const statement of sourceFile.statements) {
|
|
587
|
+
if (!ts.isVariableStatement(statement)) continue;
|
|
588
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
589
|
+
if (ts.isIdentifier(declaration.name) && declaration.initializer) {
|
|
590
|
+
initializers.set(declaration.name.text, declaration.initializer);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
return initializers;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function resolveRegistryExpression(
|
|
598
|
+
expression: ts.Expression,
|
|
599
|
+
initializers: Map<string, ts.Expression>,
|
|
600
|
+
visited = new Set<string>(),
|
|
601
|
+
): ts.Expression {
|
|
602
|
+
const current = unwrapRegistryExpression(expression);
|
|
603
|
+
if (!ts.isIdentifier(current) || visited.has(current.text)) return current;
|
|
604
|
+
const initializer = initializers.get(current.text);
|
|
605
|
+
if (!initializer) return current;
|
|
606
|
+
const nextVisited = new Set(visited);
|
|
607
|
+
nextVisited.add(current.text);
|
|
608
|
+
return resolveRegistryExpression(initializer, initializers, nextVisited);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function unwrapRegistryExpression(expression: ts.Expression): ts.Expression {
|
|
612
|
+
let current = expression;
|
|
613
|
+
while (
|
|
614
|
+
ts.isParenthesizedExpression(current) ||
|
|
615
|
+
ts.isAsExpression(current) ||
|
|
616
|
+
ts.isSatisfiesExpression(current) ||
|
|
617
|
+
ts.isTypeAssertionExpression(current) ||
|
|
618
|
+
ts.isNonNullExpression(current)
|
|
619
|
+
) {
|
|
620
|
+
current = current.expression;
|
|
621
|
+
}
|
|
622
|
+
return current;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function registryPropertyName(name: ts.PropertyName): string | undefined {
|
|
626
|
+
return ts.isIdentifier(name) || ts.isStringLiteralLike(name)
|
|
627
|
+
? name.text
|
|
628
|
+
: undefined;
|
|
329
629
|
}
|
|
330
630
|
|
|
331
631
|
/**
|
|
@@ -390,53 +690,3 @@ function appendEntry(
|
|
|
390
690
|
if (next === source) return { kind: "unchanged" };
|
|
391
691
|
return { kind: "updated", source: next };
|
|
392
692
|
}
|
|
393
|
-
|
|
394
|
-
function topLevelValueSlice(
|
|
395
|
-
source: string,
|
|
396
|
-
valueStart: number,
|
|
397
|
-
hardEnd: number,
|
|
398
|
-
): InitializerInfo | undefined {
|
|
399
|
-
let start = valueStart;
|
|
400
|
-
while (start < hardEnd && /\s/.test(source[start])) start++;
|
|
401
|
-
if (start >= hardEnd) return undefined;
|
|
402
|
-
|
|
403
|
-
let depth = 0;
|
|
404
|
-
|
|
405
|
-
for (const index of codeCharacterIndexes(source, start, hardEnd + 1)) {
|
|
406
|
-
const char = source[index];
|
|
407
|
-
|
|
408
|
-
if (char === "[" || char === "(" || char === "{") {
|
|
409
|
-
depth++;
|
|
410
|
-
continue;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
if (char === "]" || char === ")" || char === "}") {
|
|
414
|
-
if (depth === 0) {
|
|
415
|
-
return trimmedInfo(source, start, index);
|
|
416
|
-
}
|
|
417
|
-
depth--;
|
|
418
|
-
continue;
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
if (char === "," && depth === 0) {
|
|
422
|
-
return trimmedInfo(source, start, index);
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
return undefined;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
function trimmedInfo(
|
|
430
|
-
source: string,
|
|
431
|
-
start: number,
|
|
432
|
-
end: number,
|
|
433
|
-
): InitializerInfo {
|
|
434
|
-
let trimmedEnd = end;
|
|
435
|
-
while (trimmedEnd > start && /\s/.test(source[trimmedEnd - 1])) trimmedEnd--;
|
|
436
|
-
|
|
437
|
-
return {
|
|
438
|
-
text: source.slice(start, trimmedEnd),
|
|
439
|
-
start,
|
|
440
|
-
end: trimmedEnd,
|
|
441
|
-
};
|
|
442
|
-
}
|