@beignet/cli 0.0.6 → 0.0.8
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 +16 -0
- package/README.md +40 -3
- package/dist/inspect.d.ts.map +1 -1
- package/dist/inspect.js +404 -44
- package/dist/inspect.js.map +1 -1
- package/dist/make.d.ts.map +1 -1
- package/dist/make.js +267 -96
- package/dist/make.js.map +1 -1
- package/dist/registry-edits.d.ts +77 -0
- package/dist/registry-edits.d.ts.map +1 -0
- package/dist/registry-edits.js +255 -0
- package/dist/registry-edits.js.map +1 -0
- package/package.json +2 -2
- package/src/inspect.ts +648 -58
- package/src/make.ts +433 -121
- package/src/registry-edits.ts +352 -0
package/src/make.ts
CHANGED
|
@@ -10,6 +10,15 @@ import {
|
|
|
10
10
|
type ResolvedBeignetConfig,
|
|
11
11
|
resolveConfig,
|
|
12
12
|
} from "./config.js";
|
|
13
|
+
import {
|
|
14
|
+
appendToArrayExpression,
|
|
15
|
+
appendToNamedArray,
|
|
16
|
+
appendToOutboxRegistryArray,
|
|
17
|
+
arrayInitializerInfo,
|
|
18
|
+
identifiersFromArrayExpression,
|
|
19
|
+
insertAfterImports,
|
|
20
|
+
matchingDelimiterIndex,
|
|
21
|
+
} from "./registry-edits.js";
|
|
13
22
|
|
|
14
23
|
type MakeResourceOptions = {
|
|
15
24
|
name: string;
|
|
@@ -450,6 +459,21 @@ async function makeResourceSlice(
|
|
|
450
459
|
generationOptions,
|
|
451
460
|
},
|
|
452
461
|
);
|
|
462
|
+
// Generated event-publishing use cases depend on ctx.ports.eventBus, so
|
|
463
|
+
// --events also wires the port, a deferred key, the in-memory dev provider,
|
|
464
|
+
// and the provider dependency. Plan the wiring before writing any files so
|
|
465
|
+
// an anchor miss aborts the generator without leaving partial output.
|
|
466
|
+
const eventBusWirings = generationOptions.events
|
|
467
|
+
? [eventBusPortWiring("make resource --events")]
|
|
468
|
+
: [];
|
|
469
|
+
const plannedEventBusUpdates =
|
|
470
|
+
eventBusWirings.length > 0
|
|
471
|
+
? (
|
|
472
|
+
await wirePortProviders(targetDir, config, eventBusWirings, {
|
|
473
|
+
dryRun: true,
|
|
474
|
+
})
|
|
475
|
+
).updatedFiles
|
|
476
|
+
: [];
|
|
453
477
|
const createdFiles = plannedFiles
|
|
454
478
|
.filter((file) => file.result === "created")
|
|
455
479
|
.map((file) => file.path);
|
|
@@ -461,7 +485,7 @@ async function makeResourceSlice(
|
|
|
461
485
|
.map((file) => file.path);
|
|
462
486
|
|
|
463
487
|
if (options.dryRun) {
|
|
464
|
-
updatedFiles.push(...plannedWiringUpdates);
|
|
488
|
+
updatedFiles.push(...plannedWiringUpdates, ...plannedEventBusUpdates);
|
|
465
489
|
} else {
|
|
466
490
|
for (const file of plannedFiles) {
|
|
467
491
|
await writePlannedGeneratedFile(targetDir, file);
|
|
@@ -475,6 +499,16 @@ async function makeResourceSlice(
|
|
|
475
499
|
generationOptions,
|
|
476
500
|
})),
|
|
477
501
|
);
|
|
502
|
+
|
|
503
|
+
if (eventBusWirings.length > 0) {
|
|
504
|
+
updatedFiles.push(
|
|
505
|
+
...(
|
|
506
|
+
await wirePortProviders(targetDir, config, eventBusWirings, {
|
|
507
|
+
dryRun: false,
|
|
508
|
+
})
|
|
509
|
+
).updatedFiles,
|
|
510
|
+
);
|
|
511
|
+
}
|
|
478
512
|
}
|
|
479
513
|
|
|
480
514
|
return {
|
|
@@ -484,7 +518,7 @@ async function makeResourceSlice(
|
|
|
484
518
|
dryRun: Boolean(options.dryRun),
|
|
485
519
|
files: generatedFiles.map((file) => file.path),
|
|
486
520
|
createdFiles,
|
|
487
|
-
updatedFiles,
|
|
521
|
+
updatedFiles: uniqueStrings(updatedFiles),
|
|
488
522
|
skippedFiles,
|
|
489
523
|
};
|
|
490
524
|
}
|
|
@@ -881,7 +915,16 @@ export async function makeEvent(
|
|
|
881
915
|
|
|
882
916
|
await assertFeatureArtifactApp(targetDir, config, "event");
|
|
883
917
|
|
|
884
|
-
|
|
918
|
+
// Events are published through ctx.ports.eventBus, so the generator wires
|
|
919
|
+
// the port and an in-memory dev provider when the app has no bus yet. Plan
|
|
920
|
+
// the wiring before writing any files so an anchor miss aborts the
|
|
921
|
+
// generator without leaving partial output.
|
|
922
|
+
const eventBusWirings = [eventBusPortWiring("make event")];
|
|
923
|
+
await wirePortProviders(targetDir, config, eventBusWirings, {
|
|
924
|
+
dryRun: true,
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
const result = await makeFeatureArtifact({
|
|
885
928
|
targetDir,
|
|
886
929
|
config,
|
|
887
930
|
force: Boolean(options.force),
|
|
@@ -893,6 +936,16 @@ export async function makeEvent(
|
|
|
893
936
|
"@beignet/core": beignetDependencyVersion,
|
|
894
937
|
},
|
|
895
938
|
});
|
|
939
|
+
|
|
940
|
+
await applyOutboxRegistryUpdate(result, targetDir, names, "events", config, {
|
|
941
|
+
dryRun: Boolean(options.dryRun),
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
await applyPortProviderWiring(result, targetDir, config, eventBusWirings, {
|
|
945
|
+
dryRun: Boolean(options.dryRun),
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
return result;
|
|
896
949
|
}
|
|
897
950
|
|
|
898
951
|
export async function makeJob(options: MakeJobOptions): Promise<MakeJobResult> {
|
|
@@ -904,7 +957,7 @@ export async function makeJob(options: MakeJobOptions): Promise<MakeJobResult> {
|
|
|
904
957
|
|
|
905
958
|
await assertFeatureArtifactApp(targetDir, config, "job");
|
|
906
959
|
|
|
907
|
-
|
|
960
|
+
const result = await makeFeatureArtifact({
|
|
908
961
|
targetDir,
|
|
909
962
|
config,
|
|
910
963
|
force: Boolean(options.force),
|
|
@@ -919,6 +972,12 @@ export async function makeJob(options: MakeJobOptions): Promise<MakeJobResult> {
|
|
|
919
972
|
"@beignet/core": beignetDependencyVersion,
|
|
920
973
|
},
|
|
921
974
|
});
|
|
975
|
+
|
|
976
|
+
await applyOutboxRegistryUpdate(result, targetDir, names, "jobs", config, {
|
|
977
|
+
dryRun: Boolean(options.dryRun),
|
|
978
|
+
});
|
|
979
|
+
|
|
980
|
+
return result;
|
|
922
981
|
}
|
|
923
982
|
|
|
924
983
|
export async function makeTask(
|
|
@@ -1025,7 +1084,19 @@ export async function makeNotification(
|
|
|
1025
1084
|
|
|
1026
1085
|
await assertFeatureArtifactApp(targetDir, config, "notification");
|
|
1027
1086
|
|
|
1028
|
-
|
|
1087
|
+
// The generated mail channel sends through ctx.ports.mailer and sending
|
|
1088
|
+
// notifications uses ctx.ports.notifications, so the generator wires both
|
|
1089
|
+
// ports with dev-default providers. Each port is wired independently and
|
|
1090
|
+
// skipped when the app already has it, for example when the resend
|
|
1091
|
+
// integration contributed the mailer. Plan the wiring before writing any
|
|
1092
|
+
// files so an anchor miss aborts the generator without leaving partial
|
|
1093
|
+
// output.
|
|
1094
|
+
const notificationWirings = notificationPortWirings("make notification");
|
|
1095
|
+
await wirePortProviders(targetDir, config, notificationWirings, {
|
|
1096
|
+
dryRun: true,
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
const result = await makeFeatureArtifact({
|
|
1029
1100
|
targetDir,
|
|
1030
1101
|
config,
|
|
1031
1102
|
force: Boolean(options.force),
|
|
@@ -1044,6 +1115,16 @@ export async function makeNotification(
|
|
|
1044
1115
|
"@beignet/core": beignetDependencyVersion,
|
|
1045
1116
|
},
|
|
1046
1117
|
});
|
|
1118
|
+
|
|
1119
|
+
await applyPortProviderWiring(
|
|
1120
|
+
result,
|
|
1121
|
+
targetDir,
|
|
1122
|
+
config,
|
|
1123
|
+
notificationWirings,
|
|
1124
|
+
{ dryRun: Boolean(options.dryRun) },
|
|
1125
|
+
);
|
|
1126
|
+
|
|
1127
|
+
return result;
|
|
1047
1128
|
}
|
|
1048
1129
|
|
|
1049
1130
|
export async function makeListener(
|
|
@@ -1265,6 +1346,236 @@ async function applyCronSecretWiring(
|
|
|
1265
1346
|
};
|
|
1266
1347
|
}
|
|
1267
1348
|
|
|
1349
|
+
/**
|
|
1350
|
+
* A provider-backed app port a generator depends on.
|
|
1351
|
+
*
|
|
1352
|
+
* Generated code that publishes events or sends notifications needs the
|
|
1353
|
+
* matching port in `AppPorts`, the key deferred in `infra/app-ports.ts`, and a
|
|
1354
|
+
* dev-default provider registered in `server/providers.ts`. Wiring is
|
|
1355
|
+
* generation-time by design: the starter stays lean and each generator wires
|
|
1356
|
+
* its own dependencies.
|
|
1357
|
+
*/
|
|
1358
|
+
type PortProviderWiring = {
|
|
1359
|
+
/** Generator command named in manual instructions, e.g. "make event". */
|
|
1360
|
+
command: string;
|
|
1361
|
+
/** AppPorts key the provider contributes. */
|
|
1362
|
+
portKey: string;
|
|
1363
|
+
/** Port interface name added to the ports file type imports. */
|
|
1364
|
+
portType: string;
|
|
1365
|
+
/** Module the port interface is imported from. */
|
|
1366
|
+
portTypeModule: string;
|
|
1367
|
+
/** Provider factory registered in the providers array. */
|
|
1368
|
+
providerFactory: string;
|
|
1369
|
+
/** Module the provider factory is imported from. */
|
|
1370
|
+
providerModule: string;
|
|
1371
|
+
/** package.json dependency for providers that live outside @beignet/core. */
|
|
1372
|
+
dependency?: string;
|
|
1373
|
+
};
|
|
1374
|
+
|
|
1375
|
+
function eventBusPortWiring(command: string): PortProviderWiring {
|
|
1376
|
+
return {
|
|
1377
|
+
command,
|
|
1378
|
+
portKey: "eventBus",
|
|
1379
|
+
portType: "EventBusPort",
|
|
1380
|
+
portTypeModule: "@beignet/core/ports",
|
|
1381
|
+
providerFactory: "createInMemoryEventBusProvider",
|
|
1382
|
+
providerModule: "@beignet/provider-event-bus-memory",
|
|
1383
|
+
dependency: "@beignet/provider-event-bus-memory",
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
function notificationPortWirings(command: string): PortProviderWiring[] {
|
|
1388
|
+
return [
|
|
1389
|
+
{
|
|
1390
|
+
command,
|
|
1391
|
+
portKey: "mailer",
|
|
1392
|
+
portType: "MailerPort",
|
|
1393
|
+
portTypeModule: "@beignet/core/mail",
|
|
1394
|
+
providerFactory: "createMemoryMailerProvider",
|
|
1395
|
+
providerModule: "@beignet/core/mail",
|
|
1396
|
+
},
|
|
1397
|
+
{
|
|
1398
|
+
command,
|
|
1399
|
+
portKey: "notifications",
|
|
1400
|
+
portType: "NotificationPort",
|
|
1401
|
+
portTypeModule: "@beignet/core/notifications",
|
|
1402
|
+
providerFactory: "createInlineNotificationsProvider",
|
|
1403
|
+
providerModule: "@beignet/core/notifications",
|
|
1404
|
+
},
|
|
1405
|
+
];
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
function providersFilePath(config: ResolvedBeignetConfig): string {
|
|
1409
|
+
return `${directoryPath(path.dirname(config.paths.server))}/providers.ts`;
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
/**
|
|
1413
|
+
* Wire provider-backed app ports a generator depends on.
|
|
1414
|
+
*
|
|
1415
|
+
* Each wiring is independent and skipped silently when its port key already
|
|
1416
|
+
* appears in the ports file, so apps that wired their own adapter (or scaffolds
|
|
1417
|
+
* whose integrations already contribute the port) are left untouched. All
|
|
1418
|
+
* anchors are computed before any file is written; an anchor miss aborts with a
|
|
1419
|
+
* copy-pasteable manual instruction so the generator never leaves partial
|
|
1420
|
+
* wiring behind. Callers validate anchors with `dryRun: true` before writing
|
|
1421
|
+
* artifact files.
|
|
1422
|
+
*/
|
|
1423
|
+
async function wirePortProviders(
|
|
1424
|
+
targetDir: string,
|
|
1425
|
+
config: ResolvedBeignetConfig,
|
|
1426
|
+
wirings: readonly PortProviderWiring[],
|
|
1427
|
+
options: { dryRun: boolean },
|
|
1428
|
+
): Promise<{ updatedFiles: string[] }> {
|
|
1429
|
+
const portsFile = config.paths.ports;
|
|
1430
|
+
const infrastructureFile = config.paths.infrastructurePorts;
|
|
1431
|
+
const providersFile = providersFilePath(config);
|
|
1432
|
+
const manualFor = (wiring: PortProviderWiring) =>
|
|
1433
|
+
`Wire the ${wiring.portKey} port manually: add ${wiring.portKey}: ${wiring.portType}; to AppPorts in ${portsFile}, defer "${wiring.portKey}" in ${infrastructureFile}, and register ${wiring.providerFactory}() from ${wiring.providerModule} in ${providersFile}.`;
|
|
1434
|
+
|
|
1435
|
+
const portsOriginal = await readOptionalFile(path.join(targetDir, portsFile));
|
|
1436
|
+
if (portsOriginal === undefined) {
|
|
1437
|
+
if (wirings.length === 0) return { updatedFiles: [] };
|
|
1438
|
+
throw new Error(
|
|
1439
|
+
`Could not find the generated ports file ${portsFile}. ${manualFor(wirings[0])}`,
|
|
1440
|
+
);
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
// A port key that already appears in the ports file means the app wired its
|
|
1444
|
+
// own adapter or an integration already contributes the port.
|
|
1445
|
+
const active = wirings.filter(
|
|
1446
|
+
(wiring) => !new RegExp(`\\b${wiring.portKey}\\b`).test(portsOriginal),
|
|
1447
|
+
);
|
|
1448
|
+
if (active.length === 0) return { updatedFiles: [] };
|
|
1449
|
+
|
|
1450
|
+
let portsNext = portsOriginal;
|
|
1451
|
+
for (const wiring of active) {
|
|
1452
|
+
portsNext = addNamedTypeImport(
|
|
1453
|
+
portsNext,
|
|
1454
|
+
wiring.portType,
|
|
1455
|
+
wiring.portTypeModule,
|
|
1456
|
+
);
|
|
1457
|
+
portsNext = insertTypeProperty(
|
|
1458
|
+
portsNext,
|
|
1459
|
+
"AppPorts",
|
|
1460
|
+
`${wiring.portKey}: ${wiring.portType};`,
|
|
1461
|
+
`Could not find AppPorts in ${portsFile}. ${manualFor(wiring)} Or restore the generated ports file before running beignet ${wiring.command}.`,
|
|
1462
|
+
);
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
const infrastructureOriginal = await readOptionalFile(
|
|
1466
|
+
path.join(targetDir, infrastructureFile),
|
|
1467
|
+
);
|
|
1468
|
+
if (infrastructureOriginal === undefined) {
|
|
1469
|
+
throw new Error(
|
|
1470
|
+
`Could not find the generated infrastructure ports file ${infrastructureFile}. ${manualFor(active[0])}`,
|
|
1471
|
+
);
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
let infrastructureNext = infrastructureOriginal;
|
|
1475
|
+
for (const wiring of active) {
|
|
1476
|
+
const appended = appendDeferredPortKey(infrastructureNext, wiring.portKey);
|
|
1477
|
+
if (
|
|
1478
|
+
appended === infrastructureNext &&
|
|
1479
|
+
!new RegExp(`["']${wiring.portKey}["']`).test(infrastructureNext)
|
|
1480
|
+
) {
|
|
1481
|
+
throw new Error(
|
|
1482
|
+
`Could not find the deferred port list in ${infrastructureFile}. ${manualFor(wiring)} Or restore the generated infrastructure ports file before running beignet ${wiring.command}.`,
|
|
1483
|
+
);
|
|
1484
|
+
}
|
|
1485
|
+
infrastructureNext = appended;
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
const providersOriginal = await readOptionalFile(
|
|
1489
|
+
path.join(targetDir, providersFile),
|
|
1490
|
+
);
|
|
1491
|
+
if (providersOriginal === undefined) {
|
|
1492
|
+
throw new Error(
|
|
1493
|
+
`Could not find the generated providers file ${providersFile}. ${manualFor(active[0])}`,
|
|
1494
|
+
);
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
let providersNext = providersOriginal;
|
|
1498
|
+
for (const wiring of active) {
|
|
1499
|
+
if (new RegExp(`\\b${wiring.providerFactory}\\b`).test(providersNext)) {
|
|
1500
|
+
continue;
|
|
1501
|
+
}
|
|
1502
|
+
const appended = appendToNamedArray(
|
|
1503
|
+
providersNext,
|
|
1504
|
+
"providers",
|
|
1505
|
+
`${wiring.providerFactory}()`,
|
|
1506
|
+
`import { ${wiring.providerFactory} } from "${wiring.providerModule}";`,
|
|
1507
|
+
);
|
|
1508
|
+
if (appended.kind === "missing") {
|
|
1509
|
+
throw new Error(
|
|
1510
|
+
`Could not find the exported providers array in ${providersFile}. ${manualFor(wiring)} Or restore the generated providers file before running beignet ${wiring.command}.`,
|
|
1511
|
+
);
|
|
1512
|
+
}
|
|
1513
|
+
if (appended.kind === "updated") providersNext = appended.source;
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
const dependencies = active.filter(
|
|
1517
|
+
(wiring): wiring is PortProviderWiring & { dependency: string } =>
|
|
1518
|
+
wiring.dependency !== undefined,
|
|
1519
|
+
);
|
|
1520
|
+
let packageJsonNext: string | undefined;
|
|
1521
|
+
if (dependencies.length > 0) {
|
|
1522
|
+
const packageJsonOriginal = await readFile(
|
|
1523
|
+
path.join(targetDir, "package.json"),
|
|
1524
|
+
"utf8",
|
|
1525
|
+
);
|
|
1526
|
+
const packageJson = JSON.parse(packageJsonOriginal) as PackageJsonLike;
|
|
1527
|
+
packageJson.dependencies = packageJson.dependencies ?? {};
|
|
1528
|
+
for (const wiring of dependencies) {
|
|
1529
|
+
packageJson.dependencies[wiring.dependency] ??=
|
|
1530
|
+
beignetDependencyVersion(packageJson);
|
|
1531
|
+
}
|
|
1532
|
+
const next = `${JSON.stringify(packageJson, null, "\t")}\n`;
|
|
1533
|
+
if (next !== packageJsonOriginal) packageJsonNext = next;
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
const plannedFiles: Array<{ path: string; content: string }> = [];
|
|
1537
|
+
if (portsNext !== portsOriginal) {
|
|
1538
|
+
plannedFiles.push({ path: portsFile, content: portsNext });
|
|
1539
|
+
}
|
|
1540
|
+
if (infrastructureNext !== infrastructureOriginal) {
|
|
1541
|
+
plannedFiles.push({
|
|
1542
|
+
path: infrastructureFile,
|
|
1543
|
+
content: infrastructureNext,
|
|
1544
|
+
});
|
|
1545
|
+
}
|
|
1546
|
+
if (providersNext !== providersOriginal) {
|
|
1547
|
+
plannedFiles.push({ path: providersFile, content: providersNext });
|
|
1548
|
+
}
|
|
1549
|
+
if (packageJsonNext !== undefined) {
|
|
1550
|
+
plannedFiles.push({ path: "package.json", content: packageJsonNext });
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
if (!options.dryRun) {
|
|
1554
|
+
for (const file of plannedFiles) {
|
|
1555
|
+
await writeFile(path.join(targetDir, file.path), file.content);
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
return { updatedFiles: plannedFiles.map((file) => file.path) };
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
/**
|
|
1563
|
+
* Record port provider wiring updates on a make result.
|
|
1564
|
+
*/
|
|
1565
|
+
async function applyPortProviderWiring(
|
|
1566
|
+
result: { files: string[]; updatedFiles: string[] },
|
|
1567
|
+
targetDir: string,
|
|
1568
|
+
config: ResolvedBeignetConfig,
|
|
1569
|
+
wirings: readonly PortProviderWiring[],
|
|
1570
|
+
options: { dryRun: boolean },
|
|
1571
|
+
): Promise<void> {
|
|
1572
|
+
const wired = await wirePortProviders(targetDir, config, wirings, options);
|
|
1573
|
+
for (const file of wired.updatedFiles) {
|
|
1574
|
+
if (!result.updatedFiles.includes(file)) result.updatedFiles.push(file);
|
|
1575
|
+
if (!result.files.includes(file)) result.files.push(file);
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1268
1579
|
export async function makeUpload(
|
|
1269
1580
|
options: MakeUploadOptions,
|
|
1270
1581
|
): Promise<MakeUploadResult> {
|
|
@@ -2720,65 +3031,6 @@ function trimmedSlice(
|
|
|
2720
3031
|
};
|
|
2721
3032
|
}
|
|
2722
3033
|
|
|
2723
|
-
function identifiersFromArrayExpression(expression: string): Set<string> {
|
|
2724
|
-
const withoutBrackets = expression.replace(/^\[/, "").replace(/\]$/, "");
|
|
2725
|
-
|
|
2726
|
-
return new Set(
|
|
2727
|
-
Array.from(
|
|
2728
|
-
withoutBrackets.matchAll(/\b[A-Za-z_$][\w$]*\b/g),
|
|
2729
|
-
([name]) => name,
|
|
2730
|
-
),
|
|
2731
|
-
);
|
|
2732
|
-
}
|
|
2733
|
-
|
|
2734
|
-
function appendToArrayExpression(expression: string, names: string[]): string {
|
|
2735
|
-
const closingBracket = /\]\s*$/.exec(expression);
|
|
2736
|
-
if (!closingBracket) return expression;
|
|
2737
|
-
|
|
2738
|
-
const beforeClosingBracket = expression.slice(0, closingBracket.index);
|
|
2739
|
-
const closingBracketText = expression.slice(closingBracket.index);
|
|
2740
|
-
const inner = beforeClosingBracket.replace(/^\[/, "");
|
|
2741
|
-
if (!inner.trim()) return `[${names.join(", ")}]`;
|
|
2742
|
-
|
|
2743
|
-
if (!expression.includes("\n")) {
|
|
2744
|
-
const separator = /,\s*$/.test(inner) ? " " : ", ";
|
|
2745
|
-
return `${beforeClosingBracket}${separator}${names.join(", ")}${closingBracketText}`;
|
|
2746
|
-
}
|
|
2747
|
-
|
|
2748
|
-
const itemIndent =
|
|
2749
|
-
inner
|
|
2750
|
-
.split("\n")
|
|
2751
|
-
.find((line) => line.trim())
|
|
2752
|
-
?.match(/^[\t ]*/)?.[0] ?? "\t";
|
|
2753
|
-
const closingIndent = inner.match(/\n([\t ]*)$/)?.[1] ?? "";
|
|
2754
|
-
const trimmedBeforeClosingBracket = beforeClosingBracket.replace(/\s*$/, "");
|
|
2755
|
-
const appendedNames = names.join(`,\n${itemIndent}`);
|
|
2756
|
-
|
|
2757
|
-
if (/,\s*$/.test(inner)) {
|
|
2758
|
-
return `${trimmedBeforeClosingBracket}\n${itemIndent}${appendedNames},\n${closingIndent}${closingBracketText}`;
|
|
2759
|
-
}
|
|
2760
|
-
|
|
2761
|
-
return `${trimmedBeforeClosingBracket},\n${itemIndent}${appendedNames}${closingBracketText}`;
|
|
2762
|
-
}
|
|
2763
|
-
|
|
2764
|
-
function insertAfterImports(source: string, line: string): string {
|
|
2765
|
-
const lines = source.split("\n");
|
|
2766
|
-
let lastImportIndex = -1;
|
|
2767
|
-
|
|
2768
|
-
for (let index = 0; index < lines.length; index++) {
|
|
2769
|
-
if (lines[index].startsWith("import ")) {
|
|
2770
|
-
lastImportIndex = index;
|
|
2771
|
-
}
|
|
2772
|
-
}
|
|
2773
|
-
|
|
2774
|
-
if (lastImportIndex === -1) {
|
|
2775
|
-
return `${line}\n${source}`;
|
|
2776
|
-
}
|
|
2777
|
-
|
|
2778
|
-
lines.splice(lastImportIndex + 1, 0, line);
|
|
2779
|
-
return lines.join("\n");
|
|
2780
|
-
}
|
|
2781
|
-
|
|
2782
3034
|
function replaceRequired(
|
|
2783
3035
|
source: string,
|
|
2784
3036
|
searchValue: RegExp,
|
|
@@ -3059,6 +3311,50 @@ function appendDeferredPortKey(source: string, portKey: string): string {
|
|
|
3059
3311
|
)}`;
|
|
3060
3312
|
}
|
|
3061
3313
|
|
|
3314
|
+
/**
|
|
3315
|
+
* Add a name to an `import type { ... } from "<module>";` statement, creating
|
|
3316
|
+
* the import when the module is not imported yet. Names are inserted in sorted
|
|
3317
|
+
* position and single-line versus multiline formatting is preserved.
|
|
3318
|
+
*/
|
|
3319
|
+
function addNamedTypeImport(
|
|
3320
|
+
source: string,
|
|
3321
|
+
name: string,
|
|
3322
|
+
moduleName: string,
|
|
3323
|
+
): string {
|
|
3324
|
+
const escapedModule = moduleName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3325
|
+
const importPattern = new RegExp(
|
|
3326
|
+
`import type \\{([^}]*)\\} from "${escapedModule}";`,
|
|
3327
|
+
);
|
|
3328
|
+
const match = importPattern.exec(source);
|
|
3329
|
+
if (!match) {
|
|
3330
|
+
return insertAfterImports(
|
|
3331
|
+
source,
|
|
3332
|
+
`import type { ${name} } from "${moduleName}";`,
|
|
3333
|
+
);
|
|
3334
|
+
}
|
|
3335
|
+
|
|
3336
|
+
const names = match[1]
|
|
3337
|
+
.split(",")
|
|
3338
|
+
.map((entry) => entry.trim())
|
|
3339
|
+
.filter(Boolean);
|
|
3340
|
+
if (names.includes(name)) return source;
|
|
3341
|
+
|
|
3342
|
+
const insertIndex = names.findIndex(
|
|
3343
|
+
(existing) => existing.localeCompare(name) > 0,
|
|
3344
|
+
);
|
|
3345
|
+
if (insertIndex === -1) names.push(name);
|
|
3346
|
+
else names.splice(insertIndex, 0, name);
|
|
3347
|
+
|
|
3348
|
+
const indent = match[1].match(/\n([\t ]+)/)?.[1] ?? "\t";
|
|
3349
|
+
const replacement = match[1].includes("\n")
|
|
3350
|
+
? `import type {\n${indent}${names.join(`,\n${indent}`)},\n} from "${moduleName}";`
|
|
3351
|
+
: `import type { ${names.join(", ")} } from "${moduleName}";`;
|
|
3352
|
+
|
|
3353
|
+
return `${source.slice(0, match.index)}${replacement}${source.slice(
|
|
3354
|
+
match.index + match[0].length,
|
|
3355
|
+
)}`;
|
|
3356
|
+
}
|
|
3357
|
+
|
|
3062
3358
|
function topLevelBoundObjectRange(
|
|
3063
3359
|
source: string,
|
|
3064
3360
|
openBrace: number,
|
|
@@ -3280,48 +3576,6 @@ function matchingBraceIndex(source: string, openBrace: number): number {
|
|
|
3280
3576
|
return matchingDelimiterIndex(source, openBrace, "{", "}");
|
|
3281
3577
|
}
|
|
3282
3578
|
|
|
3283
|
-
function matchingDelimiterIndex(
|
|
3284
|
-
source: string,
|
|
3285
|
-
openIndex: number,
|
|
3286
|
-
open: string,
|
|
3287
|
-
close: string,
|
|
3288
|
-
): number {
|
|
3289
|
-
let depth = 0;
|
|
3290
|
-
let inString: string | undefined;
|
|
3291
|
-
let escaped = false;
|
|
3292
|
-
|
|
3293
|
-
for (let index = openIndex; index < source.length; index++) {
|
|
3294
|
-
const char = source[index];
|
|
3295
|
-
if (inString) {
|
|
3296
|
-
if (escaped) {
|
|
3297
|
-
escaped = false;
|
|
3298
|
-
} else if (char === "\\") {
|
|
3299
|
-
escaped = true;
|
|
3300
|
-
} else if (char === inString) {
|
|
3301
|
-
inString = undefined;
|
|
3302
|
-
}
|
|
3303
|
-
continue;
|
|
3304
|
-
}
|
|
3305
|
-
|
|
3306
|
-
if (char === '"' || char === "'" || char === "`") {
|
|
3307
|
-
inString = char;
|
|
3308
|
-
continue;
|
|
3309
|
-
}
|
|
3310
|
-
|
|
3311
|
-
if (char === open) {
|
|
3312
|
-
depth++;
|
|
3313
|
-
continue;
|
|
3314
|
-
}
|
|
3315
|
-
|
|
3316
|
-
if (char === close) {
|
|
3317
|
-
depth--;
|
|
3318
|
-
if (depth === 0) return index;
|
|
3319
|
-
}
|
|
3320
|
-
}
|
|
3321
|
-
|
|
3322
|
-
return -1;
|
|
3323
|
-
}
|
|
3324
|
-
|
|
3325
3579
|
function replaceTransactionPortsRequired(
|
|
3326
3580
|
source: string,
|
|
3327
3581
|
entry: string,
|
|
@@ -4932,24 +5186,82 @@ export async function stopScheduleContext(): Promise<void> {
|
|
|
4932
5186
|
return "updated";
|
|
4933
5187
|
}
|
|
4934
5188
|
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
5189
|
+
/**
|
|
5190
|
+
* Append a feature event or job registry to `server/outbox.ts`.
|
|
5191
|
+
*
|
|
5192
|
+
* The outbox registry is opt-in, so this never creates the file; it returns
|
|
5193
|
+
* `undefined` when the app has no outbox module. It also bails to "skipped"
|
|
5194
|
+
* instead of writing when the `defineOutboxRegistry({...})` anchor is missing
|
|
5195
|
+
* or the matching array is not editable.
|
|
5196
|
+
*/
|
|
5197
|
+
async function updateOutboxRegistry(
|
|
5198
|
+
targetDir: string,
|
|
5199
|
+
names: EventNames | JobNames,
|
|
5200
|
+
kind: "events" | "jobs",
|
|
5201
|
+
config: ResolvedBeignetConfig,
|
|
5202
|
+
options: { dryRun: boolean },
|
|
5203
|
+
): Promise<WriteGeneratedFileResult | undefined> {
|
|
5204
|
+
const destination = path.join(targetDir, config.paths.outbox);
|
|
5205
|
+
const existing = await readOptionalFile(destination);
|
|
5206
|
+
if (existing === undefined) return undefined;
|
|
4941
5207
|
|
|
4942
|
-
const
|
|
4943
|
-
|
|
5208
|
+
const featureIndexPath = path.join(
|
|
5209
|
+
featureArtifactDir(names, kind, config),
|
|
5210
|
+
"index.ts",
|
|
5211
|
+
);
|
|
5212
|
+
const registryName = `${names.featureSingularCamel}${
|
|
5213
|
+
kind === "events" ? "Events" : "Jobs"
|
|
5214
|
+
}`;
|
|
5215
|
+
const importLine = `import { ${registryName} } from "${aliasModule(featureIndexPath)}";`;
|
|
5216
|
+
const appended = appendToOutboxRegistryArray(
|
|
5217
|
+
existing,
|
|
5218
|
+
kind,
|
|
5219
|
+
`...${registryName}`,
|
|
5220
|
+
importLine,
|
|
5221
|
+
);
|
|
4944
5222
|
|
|
4945
|
-
|
|
4946
|
-
|
|
5223
|
+
if (appended.kind === "missing" || appended.kind === "unchanged") {
|
|
5224
|
+
return "skipped";
|
|
5225
|
+
}
|
|
4947
5226
|
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
5227
|
+
if (!options.dryRun) await writeFile(destination, appended.source);
|
|
5228
|
+
return "updated";
|
|
5229
|
+
}
|
|
5230
|
+
|
|
5231
|
+
/**
|
|
5232
|
+
* Record an outbox registry update on a make result using the same
|
|
5233
|
+
* bookkeeping as the task and schedule registries. No-op when the app has no
|
|
5234
|
+
* outbox module.
|
|
5235
|
+
*/
|
|
5236
|
+
async function applyOutboxRegistryUpdate(
|
|
5237
|
+
result: {
|
|
5238
|
+
files: string[];
|
|
5239
|
+
createdFiles: string[];
|
|
5240
|
+
updatedFiles: string[];
|
|
5241
|
+
skippedFiles: string[];
|
|
5242
|
+
},
|
|
5243
|
+
targetDir: string,
|
|
5244
|
+
names: EventNames | JobNames,
|
|
5245
|
+
kind: "events" | "jobs",
|
|
5246
|
+
config: ResolvedBeignetConfig,
|
|
5247
|
+
options: { dryRun: boolean },
|
|
5248
|
+
): Promise<void> {
|
|
5249
|
+
const registryResult = await updateOutboxRegistry(
|
|
5250
|
+
targetDir,
|
|
5251
|
+
names,
|
|
5252
|
+
kind,
|
|
5253
|
+
config,
|
|
5254
|
+
options,
|
|
5255
|
+
);
|
|
5256
|
+
if (registryResult === undefined) return;
|
|
5257
|
+
|
|
5258
|
+
if (registryResult === "updated")
|
|
5259
|
+
result.updatedFiles.push(config.paths.outbox);
|
|
5260
|
+
if (registryResult === "skipped")
|
|
5261
|
+
result.skippedFiles.push(config.paths.outbox);
|
|
5262
|
+
if (!result.files.includes(config.paths.outbox)) {
|
|
5263
|
+
result.files.push(config.paths.outbox);
|
|
5264
|
+
}
|
|
4953
5265
|
}
|
|
4954
5266
|
|
|
4955
5267
|
function defineUploadsInitializerInfo(
|