@open-mercato/cli 0.7.0 → 0.7.1-develop.7103.1.41ff100d93

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.
Files changed (184) hide show
  1. package/.turbo/turbo-build.log +7 -1
  2. package/dist/agentic/guides/module-facts.json +5509 -599
  3. package/dist/agentic/guides/module-facts.v2.json +5595 -599
  4. package/dist/agentic/guides/modules/ai_assistant/index.md +1 -1
  5. package/dist/agentic/guides/modules/api_docs/index.md +1 -1
  6. package/dist/agentic/guides/modules/api_keys/di-registrations-rich.md +11 -0
  7. package/dist/agentic/guides/modules/api_keys/di-service-tokens.md +11 -0
  8. package/dist/agentic/guides/modules/api_keys/exact-override-targets.md +1 -0
  9. package/dist/agentic/guides/modules/api_keys/index.md +4 -2
  10. package/dist/agentic/guides/modules/attachments/di-registrations-rich.md +6 -5
  11. package/dist/agentic/guides/modules/attachments/di-service-tokens.md +5 -4
  12. package/dist/agentic/guides/modules/attachments/exact-override-targets.md +6 -5
  13. package/dist/agentic/guides/modules/attachments/index.md +2 -2
  14. package/dist/agentic/guides/modules/audit_logs/index.md +1 -1
  15. package/dist/agentic/guides/modules/auth/di-registrations-rich.md +3 -2
  16. package/dist/agentic/guides/modules/auth/di-service-tokens.md +3 -2
  17. package/dist/agentic/guides/modules/auth/exact-override-targets.md +3 -2
  18. package/dist/agentic/guides/modules/auth/index.md +1 -1
  19. package/dist/agentic/guides/modules/business_rules/index.md +1 -1
  20. package/dist/agentic/guides/modules/catalog/incoming-installed-contributions.md +3 -0
  21. package/dist/agentic/guides/modules/catalog/index.md +2 -2
  22. package/dist/agentic/guides/modules/channel_apns/index.md +1 -1
  23. package/dist/agentic/guides/modules/channel_discord/acl-features.md +13 -0
  24. package/dist/agentic/guides/modules/channel_discord/ai-agents.md +11 -0
  25. package/dist/agentic/guides/modules/channel_discord/backend-pages.md +11 -0
  26. package/dist/agentic/guides/modules/channel_discord/cli-commands.md +13 -0
  27. package/dist/agentic/guides/modules/channel_discord/contribution-resolutions.md +14 -0
  28. package/dist/agentic/guides/modules/channel_discord/di-registrations-rich.md +12 -0
  29. package/dist/agentic/guides/modules/channel_discord/exact-override-targets.md +26 -0
  30. package/dist/agentic/guides/modules/channel_discord/index.md +22 -0
  31. package/dist/agentic/guides/modules/channel_discord/owned-contract-module-metadata.md +11 -0
  32. package/dist/agentic/guides/modules/channel_discord/setup.md +11 -0
  33. package/dist/agentic/guides/modules/channel_discord/umes-contributions.md +14 -0
  34. package/dist/agentic/guides/modules/channel_discord/workers.md +12 -0
  35. package/dist/agentic/guides/modules/channel_expo/index.md +1 -1
  36. package/dist/agentic/guides/modules/channel_fcm/index.md +1 -1
  37. package/dist/agentic/guides/modules/channel_gmail/index.md +1 -1
  38. package/dist/agentic/guides/modules/channel_imap/index.md +1 -1
  39. package/dist/agentic/guides/modules/checkout/index.md +1 -1
  40. package/dist/agentic/guides/modules/communication_channels/di-registrations-rich.md +9 -8
  41. package/dist/agentic/guides/modules/communication_channels/exact-override-targets.md +9 -8
  42. package/dist/agentic/guides/modules/communication_channels/incoming-installed-contributions.md +2 -0
  43. package/dist/agentic/guides/modules/communication_channels/index.md +3 -3
  44. package/dist/agentic/guides/modules/configs/index.md +1 -1
  45. package/dist/agentic/guides/modules/content/index.md +1 -1
  46. package/dist/agentic/guides/modules/currencies/index.md +1 -1
  47. package/dist/agentic/guides/modules/customer_accounts/index.md +1 -1
  48. package/dist/agentic/guides/modules/customers/active-extension-bindings.md +6 -3
  49. package/dist/agentic/guides/modules/customers/contribution-resolutions.md +1 -0
  50. package/dist/agentic/guides/modules/customers/events.md +2 -1
  51. package/dist/agentic/guides/modules/customers/incoming-installed-contributions.md +7 -0
  52. package/dist/agentic/guides/modules/customers/index.md +4 -4
  53. package/dist/agentic/guides/modules/customers/umes-contributions.md +1 -0
  54. package/dist/agentic/guides/modules/customers/umes-hosts.md +1 -0
  55. package/dist/agentic/guides/modules/dashboards/index.md +1 -1
  56. package/dist/agentic/guides/modules/data_sync/exact-override-targets.md +2 -2
  57. package/dist/agentic/guides/modules/data_sync/index.md +1 -1
  58. package/dist/agentic/guides/modules/data_sync/workers.md +2 -2
  59. package/dist/agentic/guides/modules/design_system/index.md +1 -1
  60. package/dist/agentic/guides/modules/devices/index.md +1 -1
  61. package/dist/agentic/guides/modules/dictionaries/index.md +1 -1
  62. package/dist/agentic/guides/modules/directory/di-registrations-rich.md +12 -0
  63. package/dist/agentic/guides/modules/directory/di-service-tokens.md +12 -0
  64. package/dist/agentic/guides/modules/directory/exact-override-targets.md +2 -0
  65. package/dist/agentic/guides/modules/directory/index.md +5 -3
  66. package/dist/agentic/guides/modules/documents/acl-features.md +17 -0
  67. package/dist/agentic/guides/modules/documents/backend-pages.md +13 -0
  68. package/dist/agentic/guides/modules/documents/contribution-resolutions.md +27 -0
  69. package/dist/agentic/guides/modules/documents/di-registrations-rich.md +21 -0
  70. package/dist/agentic/guides/modules/documents/domain-commands.md +37 -0
  71. package/dist/agentic/guides/modules/documents/encryption.md +11 -0
  72. package/dist/agentic/guides/modules/documents/entities.md +21 -0
  73. package/dist/agentic/guides/modules/documents/events.md +25 -0
  74. package/dist/agentic/guides/modules/documents/exact-override-targets.md +39 -0
  75. package/dist/agentic/guides/modules/documents/index.md +25 -0
  76. package/dist/agentic/guides/modules/documents/notifications.md +13 -0
  77. package/dist/agentic/guides/modules/documents/owned-contract-module-metadata.md +11 -0
  78. package/dist/agentic/guides/modules/documents/setup.md +11 -0
  79. package/dist/agentic/guides/modules/documents/umes-contributions.md +27 -0
  80. package/dist/agentic/guides/modules/documents/umes-hosts.md +36 -0
  81. package/dist/agentic/guides/modules/entities/index.md +1 -1
  82. package/dist/agentic/guides/modules/eudr/index.md +1 -1
  83. package/dist/agentic/guides/modules/events/index.md +1 -1
  84. package/dist/agentic/guides/modules/feature_toggles/index.md +1 -1
  85. package/dist/agentic/guides/modules/gateway_stripe/exact-override-targets.md +1 -1
  86. package/dist/agentic/guides/modules/gateway_stripe/index.md +1 -1
  87. package/dist/agentic/guides/modules/gateway_stripe/workers.md +1 -1
  88. package/dist/agentic/guides/modules/generators/index.md +1 -1
  89. package/dist/agentic/guides/modules/inbox_ops/index.md +1 -1
  90. package/dist/agentic/guides/modules/integrations/index.md +1 -1
  91. package/dist/agentic/guides/modules/messages/index.md +1 -1
  92. package/dist/agentic/guides/modules/notifications/index.md +1 -1
  93. package/dist/agentic/guides/modules/onboarding/index.md +1 -1
  94. package/dist/agentic/guides/modules/payment_gateways/index.md +1 -1
  95. package/dist/agentic/guides/modules/perspectives/index.md +1 -1
  96. package/dist/agentic/guides/modules/planner/index.md +1 -1
  97. package/dist/agentic/guides/modules/portal/index.md +1 -1
  98. package/dist/agentic/guides/modules/progress/index.md +1 -1
  99. package/dist/agentic/guides/modules/push_notifications/index.md +1 -1
  100. package/dist/agentic/guides/modules/query_index/index.md +1 -1
  101. package/dist/agentic/guides/modules/record_locks/index.md +1 -1
  102. package/dist/agentic/guides/modules/resources/index.md +1 -1
  103. package/dist/agentic/guides/modules/sales/active-extension-bindings.md +1 -0
  104. package/dist/agentic/guides/modules/sales/contribution-resolutions.md +1 -0
  105. package/dist/agentic/guides/modules/sales/exact-override-targets.md +5 -4
  106. package/dist/agentic/guides/modules/sales/host-extension-points.md +1 -1
  107. package/dist/agentic/guides/modules/sales/incoming-installed-contributions.md +2 -0
  108. package/dist/agentic/guides/modules/sales/index.md +3 -3
  109. package/dist/agentic/guides/modules/sales/setup.md +1 -1
  110. package/dist/agentic/guides/modules/sales/umes-contributions.md +1 -0
  111. package/dist/agentic/guides/modules/sales/umes-diagnostics.md +1 -0
  112. package/dist/agentic/guides/modules/sales/umes-hosts.md +9 -0
  113. package/dist/agentic/guides/modules/scheduler/cli-commands.md +1 -0
  114. package/dist/agentic/guides/modules/scheduler/exact-override-targets.md +3 -1
  115. package/dist/agentic/guides/modules/scheduler/index.md +2 -2
  116. package/dist/agentic/guides/modules/scheduler/workers.md +2 -1
  117. package/dist/agentic/guides/modules/search/index.md +1 -1
  118. package/dist/agentic/guides/modules/security/index.md +1 -1
  119. package/dist/agentic/guides/modules/security/owned-contract-module-metadata.md +1 -1
  120. package/dist/agentic/guides/modules/shipping_carriers/index.md +1 -1
  121. package/dist/agentic/guides/modules/sso/index.md +1 -1
  122. package/dist/agentic/guides/modules/staff/index.md +1 -1
  123. package/dist/agentic/guides/modules/storage_s3/index.md +1 -1
  124. package/dist/agentic/guides/modules/sync_akeneo/index.md +1 -1
  125. package/dist/agentic/guides/modules/sync_excel/index.md +1 -1
  126. package/dist/agentic/guides/modules/system_status_overlays/index.md +1 -1
  127. package/dist/agentic/guides/modules/translations/index.md +1 -1
  128. package/dist/agentic/guides/modules/warranty_claims/index.md +1 -1
  129. package/dist/agentic/guides/modules/webhooks/index.md +1 -1
  130. package/dist/agentic/guides/modules/wms/index.md +1 -1
  131. package/dist/agentic/guides/modules/workflows/index.md +1 -1
  132. package/dist/agentic/guides/reference-module-facts.json +2 -2
  133. package/dist/agentic/guides/reference-modules/example/index.md +1 -1
  134. package/dist/agentic/guides/upstream/manifest.json +1 -1
  135. package/dist/agentic/shared/AGENTS.md.template +3 -3
  136. package/dist/agentic/shared/ai/harness/README.md +2 -2
  137. package/dist/agentic/shared/ai/harness/RELEASE.md +8 -6
  138. package/dist/agentic/shared/ai/harness/cases.json +83 -0
  139. package/dist/agentic/shared/ai/harness/cases.schema.json +4 -4
  140. package/dist/agentic/shared/ai/harness/validators.json +1 -1
  141. package/dist/agentic/shared/scripts/run-agent-harness-release.mjs +37 -11
  142. package/dist/lib/generators/ast/index.js +4 -1
  143. package/dist/lib/generators/ast/index.js.map +2 -2
  144. package/dist/lib/generators/ast/source-file.js +8 -1
  145. package/dist/lib/generators/ast/source-file.js.map +2 -2
  146. package/dist/lib/generators/ast/writers.js +9 -1
  147. package/dist/lib/generators/ast/writers.js.map +2 -2
  148. package/dist/lib/generators/module-registry.js +424 -189
  149. package/dist/lib/generators/module-registry.js.map +2 -2
  150. package/dist/lib/generators/route-manifest-shards.js +4 -1
  151. package/dist/lib/generators/route-manifest-shards.js.map +2 -2
  152. package/dist/lib/spawn.js +3 -2
  153. package/dist/lib/spawn.js.map +2 -2
  154. package/dist/lib/testing/integration.js +162 -24
  155. package/dist/lib/testing/integration.js.map +2 -2
  156. package/dist/mercato.js +14 -0
  157. package/dist/mercato.js.map +2 -2
  158. package/package.json +8 -7
  159. package/src/__tests__/resolve-queue-abandon-hook.test.ts +39 -0
  160. package/src/lib/__tests__/spawn.test.ts +23 -0
  161. package/src/lib/generators/__tests__/__snapshots__/bootstrap-registrations.test.ts.snap +14 -0
  162. package/src/lib/generators/__tests__/__snapshots__/command-loaders.test.ts.snap +23 -0
  163. package/src/lib/generators/__tests__/ast-writers-escaping.test.ts +64 -0
  164. package/src/lib/generators/__tests__/bootstrap-registrations.test.ts +176 -0
  165. package/src/lib/generators/__tests__/command-loaders.test.ts +239 -0
  166. package/src/lib/generators/__tests__/generated-registry-imports.test.ts +128 -0
  167. package/src/lib/generators/__tests__/metadata-extraction.test.ts +44 -0
  168. package/src/lib/generators/__tests__/module-facts.bc-guard.test.ts +32 -8
  169. package/src/lib/generators/__tests__/module-facts.customers.fixture.test.ts +4 -1
  170. package/src/lib/generators/__tests__/module-facts.extension-hosts.test.ts +1 -1
  171. package/src/lib/generators/__tests__/module-subset.test.ts +96 -72
  172. package/src/lib/generators/__tests__/output-snapshots.test.ts +1 -1
  173. package/src/lib/generators/__tests__/registry-variant-parity.test.ts +258 -0
  174. package/src/lib/generators/__tests__/route-manifest-shards.test.ts +15 -0
  175. package/src/lib/generators/__tests__/structural-contracts.test.ts +7 -3
  176. package/src/lib/generators/ast/index.ts +2 -1
  177. package/src/lib/generators/ast/source-file.ts +13 -0
  178. package/src/lib/generators/ast/writers.ts +26 -1
  179. package/src/lib/generators/module-registry.ts +534 -227
  180. package/src/lib/generators/route-manifest-shards.ts +8 -1
  181. package/src/lib/spawn.ts +5 -4
  182. package/src/lib/testing/__tests__/integration.test.ts +435 -0
  183. package/src/lib/testing/integration.ts +254 -32
  184. package/src/mercato.ts +27 -0
@@ -32,6 +32,7 @@ import {
32
32
  conditionalExpression,
33
33
  createGeneratedSourceFile,
34
34
  elementAccess,
35
+ escapeGeneratedJsonLiteral,
35
36
  returnStatement,
36
37
  getSourceText,
37
38
  identifier,
@@ -44,6 +45,7 @@ import {
44
45
  optionalPropertyAccess,
45
46
  parenthesized,
46
47
  propertyAccess,
48
+ renderGeneratedValue,
47
49
  spreadExpression,
48
50
  type GeneratedObjectEntry,
49
51
  variableStatement,
@@ -133,7 +135,7 @@ type DashboardWidgetEntry = {
133
135
  importPath: string
134
136
  }
135
137
 
136
- type CommandLoaderGenerationEntry = {
138
+ export type CommandLoaderGenerationEntry = {
137
139
  moduleId: string
138
140
  key: string
139
141
  importPath: string
@@ -159,7 +161,7 @@ type RuntimeApiMethodMetadata = {
159
161
  type PageRouteGenerationResult = {
160
162
  eagerRoutes: string[]
161
163
  runtimeRoutes: string[]
162
- manifestRoutes: string[]
164
+ manifestRoutes: WriterFunction[]
163
165
  manifestShardRoutes: RouteManifestShardEntry[]
164
166
  metadataRoutes: RouteManifestShardEntry[]
165
167
  routePatterns: string[]
@@ -190,7 +192,7 @@ function assertUniqueBackendRoutePattern(
190
192
  type ApiRouteGenerationResult = {
191
193
  eagerApis: string[]
192
194
  runtimeApis: string[]
193
- manifestApis: string[]
195
+ manifestApis: WriterFunction[]
194
196
  manifestShardApis: RouteManifestShardEntry[]
195
197
  metadataApis: RouteManifestShardEntry[]
196
198
  }
@@ -244,8 +246,19 @@ type SerializableWorkerMetadata = {
244
246
  id?: string
245
247
  queue?: string
246
248
  concurrency?: number
249
+ schedulerSafe?: boolean
250
+ schedulerRequiredFeatures?: string[]
247
251
  lockDuration?: number
248
252
  maxStalledCount?: number
253
+ /**
254
+ * Whether the worker's metadata declares an `onJobAbandoned` callback.
255
+ *
256
+ * A function cannot be serialized into the registry the way the scalar options are, so the flag is
257
+ * resolved here at build time and the callback itself is emitted as a lazy import beside the
258
+ * handler. Recorded only when the source module could actually be loaded — the object-literal
259
+ * fallback below cannot see whether a referenced identifier is a function.
260
+ */
261
+ hasJobAbandonedHook?: boolean
249
262
  }
250
263
 
251
264
  type PageMetadataManifestLoadResult = {
@@ -577,6 +590,55 @@ function extractObjectPropertiesFromAst(sourceFile: string, exportName: string):
577
590
  return Object.keys(result).length > 0 ? result : null
578
591
  }
579
592
 
593
+ /**
594
+ * Whether an exported object literal declares a property, regardless of what it evaluates to.
595
+ *
596
+ * Needed for callbacks: the value resolvers above can read literals and local constants, but a
597
+ * property whose value is an imported function stays unresolvable, so "did the author declare it"
598
+ * cannot be answered by inspecting the extracted value. Answering it from the syntax instead is what
599
+ * lets the generator emit a lazy accessor for a hook it can see but cannot serialize.
600
+ */
601
+ export function namedObjectLiteralDeclaresProperty(sourceFile: string, exportName: string, propertyName: string): boolean {
602
+ let source = ''
603
+ try {
604
+ source = fs.readFileSync(sourceFile, 'utf8')
605
+ } catch {
606
+ return false
607
+ }
608
+
609
+ const parsed = ts.createSourceFile(sourceFile, source, ts.ScriptTarget.Latest, true, inferScriptKind(sourceFile))
610
+ const exportedNames = new Set<string>()
611
+ for (const statement of parsed.statements) {
612
+ if (
613
+ ts.isExportDeclaration(statement)
614
+ && statement.exportClause
615
+ && ts.isNamedExports(statement.exportClause)
616
+ && !statement.moduleSpecifier
617
+ ) {
618
+ for (const element of statement.exportClause.elements) exportedNames.add(element.name.text)
619
+ }
620
+ }
621
+
622
+ for (const statement of parsed.statements) {
623
+ if (!ts.isVariableStatement(statement)) continue
624
+ const exportsDirectly = (ts.canHaveModifiers(statement) ? ts.getModifiers(statement) : undefined)
625
+ ?.some((modifier: ts.Modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword)
626
+ for (const declaration of statement.declarationList.declarations) {
627
+ if (!ts.isIdentifier(declaration.name) || declaration.name.text !== exportName) continue
628
+ if (!exportsDirectly && !exportedNames.has(exportName)) continue
629
+ const objectLiteral = unwrapObjectLiteralExpression(declaration.initializer)
630
+ if (!objectLiteral) return false
631
+ return objectLiteral.properties.some((property) => {
632
+ const name = property.name
633
+ if (!name) return false
634
+ const key = ts.isIdentifier(name) ? name.text : ts.isStringLiteral(name) ? name.text : null
635
+ return key === propertyName
636
+ })
637
+ }
638
+ }
639
+ return false
640
+ }
641
+
580
642
  export function extractNamedObjectLiteralExport(sourceFile: string, exportName: string): Record<string, unknown> | null {
581
643
  const literal = extractNamedObjectLiteralSource(sourceFile, exportName)
582
644
  if (literal) {
@@ -1136,11 +1198,7 @@ function requiresRuntimePageMetadataFromSourceFile(sourceFile: string): boolean
1136
1198
  }
1137
1199
 
1138
1200
  function toLiteral(value: unknown): string {
1139
- return JSON.stringify(value)
1140
- .replace(/</g, '\\u003c')
1141
- .replace(/>/g, '\\u003e')
1142
- .replace(/\u2028/g, '\\u2028')
1143
- .replace(/\u2029/g, '\\u2029')
1201
+ return escapeGeneratedJsonLiteral(JSON.stringify(value))
1144
1202
  }
1145
1203
 
1146
1204
  const GENERATED_MODULE_RELATIVE_PREFIXES = ['@/', '../../src/modules/', './'] as const
@@ -1382,32 +1440,81 @@ function collectCommandLoaderEntries(
1382
1440
  return entries
1383
1441
  }
1384
1442
 
1385
- function renderCommandLoadersFile(entries: CommandLoaderGenerationEntry[]): string {
1443
+ export function renderCommandLoadersFile(entries: CommandLoaderGenerationEntry[]): string {
1386
1444
  const seenCommandIds = new Map<string, string>()
1387
- const rendered: string[] = []
1445
+ const loaderEntries: WriterFunction[] = []
1388
1446
 
1389
1447
  for (const entry of entries) {
1390
- const loadExpr = `() => ${buildDynamicImportExpression(entry.importPath)}`
1448
+ const loadValue = arrowFunction({
1449
+ body: importExpression(sanitizeGeneratedModuleSpecifier(entry.importPath)),
1450
+ })
1391
1451
  for (const id of entry.ids) {
1392
1452
  const previous = seenCommandIds.get(id)
1393
1453
  if (previous && previous !== entry.key) {
1394
1454
  throw new Error(`[generate] Duplicate command id "${id}" discovered in "${previous}" and "${entry.key}"`)
1395
1455
  }
1396
1456
  seenCommandIds.set(id, entry.key)
1397
- rendered.push(` { moduleId: ${toLiteral(entry.moduleId)}, id: ${toLiteral(id)}, key: ${toLiteral(entry.key)}, load: ${loadExpr} },`)
1457
+ loaderEntries.push(objectLiteral([
1458
+ { name: 'moduleId', value: entry.moduleId },
1459
+ { name: 'id', value: id },
1460
+ { name: 'key', value: entry.key },
1461
+ { name: 'load', value: loadValue },
1462
+ ]))
1398
1463
  }
1399
- rendered.push(` { moduleId: ${toLiteral(entry.moduleId)}, key: ${toLiteral(entry.key)}, load: ${loadExpr} },`)
1464
+ loaderEntries.push(objectLiteral([
1465
+ { name: 'moduleId', value: entry.moduleId },
1466
+ { name: 'key', value: entry.key },
1467
+ { name: 'load', value: loadValue },
1468
+ ]))
1400
1469
  }
1401
1470
 
1402
- return `// AUTO-GENERATED by mercato generate command-loaders
1403
- import type { CommandLoader } from '@open-mercato/shared/lib/commands'
1471
+ const sourceFile = createGeneratedSourceFile('command-loaders.generated.ts')
1472
+ addAutoGeneratedComment(sourceFile, 'command-loaders')
1473
+
1474
+ addImportSpec(sourceFile, {
1475
+ moduleSpecifier: '@open-mercato/shared/lib/commands',
1476
+ isTypeOnly: true,
1477
+ namedImports: [{ name: 'CommandLoader' }],
1478
+ })
1404
1479
 
1405
- export const commandLoaderEntries: CommandLoader[] = [
1406
- ${rendered.join('\n')}
1407
- ]
1480
+ sourceFile.addVariableStatement({
1481
+ declarationKind: VariableDeclarationKind.Const,
1482
+ isExported: true,
1483
+ declarations: [
1484
+ {
1485
+ name: 'commandLoaderEntries',
1486
+ type: 'CommandLoader[]',
1487
+ initializer: arrayLiteral(loaderEntries, writeValue),
1488
+ },
1489
+ ],
1490
+ })
1491
+ sourceFile.addExportAssignment({
1492
+ isExportEquals: false,
1493
+ expression: 'commandLoaderEntries',
1494
+ })
1408
1495
 
1409
- export default commandLoaderEntries
1410
- `
1496
+ return getSourceText(sourceFile)
1497
+ }
1498
+
1499
+ export function renderBootstrapRegistrationsFile(options: {
1500
+ entryImports: string[]
1501
+ registrationImports: string[]
1502
+ calls: string[]
1503
+ }): string {
1504
+ const uniqueImports = [...new Set([...options.entryImports, ...options.registrationImports])]
1505
+
1506
+ const sourceFile = createGeneratedSourceFile('bootstrap-registrations.generated.ts')
1507
+ addAutoGeneratedComment(sourceFile, 'registry')
1508
+ addImportStatements(sourceFile, uniqueImports)
1509
+
1510
+ sourceFile.addFunction({
1511
+ name: 'runBootstrapRegistrations',
1512
+ isExported: true,
1513
+ returnType: 'void',
1514
+ statements: options.calls,
1515
+ })
1516
+
1517
+ return getSourceText(sourceFile)
1411
1518
  }
1412
1519
 
1413
1520
  function serializeGeneratedImport(statement: GeneratedImportStatement): string {
@@ -1663,8 +1770,38 @@ function buildPageRouteProps(metaExpr: string, routePath: string): string {
1663
1770
  return `pattern: ${toLiteral(routePath || '/')}, requireAuth: (${metaExpr})?.requireAuth, requireRoles: (${metaExpr})?.requireRoles, requireFeatures: (${metaExpr})?.requireFeatures, requireCustomerAuth: (${metaExpr})?.requireCustomerAuth, requireCustomerFeatures: (${metaExpr})?.requireCustomerFeatures, nav: (${metaExpr})?.nav, title: (${metaExpr})?.pageTitle ?? (${metaExpr})?.title, titleKey: (${metaExpr})?.pageTitleKey ?? (${metaExpr})?.titleKey, group: (${metaExpr})?.pageGroup ?? (${metaExpr})?.group, groupKey: (${metaExpr})?.pageGroupKey ?? (${metaExpr})?.groupKey, icon: (${metaExpr})?.icon, order: (${metaExpr})?.pageOrder ?? (${metaExpr})?.order, priority: (${metaExpr})?.pagePriority ?? (${metaExpr})?.priority, navHidden: (${metaExpr})?.navHidden, visible: (${metaExpr})?.visible, enabled: (${metaExpr})?.enabled, breadcrumb: (${metaExpr})?.breadcrumb, pageContext: (${metaExpr})?.pageContext, placement: (${metaExpr})?.placement`
1664
1771
  }
1665
1772
 
1666
- function buildPageRouteManifestSpread(metaExpr: string, routePath: string): string {
1667
- return `...resolvePageRouteMetadata(${toLiteral(routePath || '/')}, ${metaExpr})`
1773
+ function buildPageRouteManifestSpread(metaExpr: string, routePath: string): WriterFunction {
1774
+ return callExpression(identifier('resolvePageRouteMetadata'), [routePath || '/', identifier(metaExpr)])
1775
+ }
1776
+
1777
+ function buildManifestRouteLoader(importPath: string): WriterFunction {
1778
+ return arrowFunction({
1779
+ async: true,
1780
+ body: block((writer) => {
1781
+ writeStatements(writer, [
1782
+ variableStatement({
1783
+ name: 'mod',
1784
+ initializer: awaitExpression(importExpression(sanitizeGeneratedModuleSpecifier(importPath))),
1785
+ }),
1786
+ returnStatement(
1787
+ asExpression(
1788
+ parenthesized(nullishCoalesce([
1789
+ propertyAccess(identifier('mod'), 'default'),
1790
+ identifier('mod'),
1791
+ ])),
1792
+ 'any',
1793
+ ),
1794
+ ),
1795
+ ])
1796
+ }),
1797
+ })
1798
+ }
1799
+
1800
+ function buildManifestApiLoader(importPath: string): WriterFunction {
1801
+ return arrowFunction({
1802
+ async: true,
1803
+ body: importExpression(sanitizeGeneratedModuleSpecifier(importPath)),
1804
+ })
1668
1805
  }
1669
1806
 
1670
1807
  function normalizeBreadcrumb(raw: unknown): SerializablePageMetadata['breadcrumb'] {
@@ -1777,6 +1914,14 @@ function normalizeWorkerMetadata(raw: unknown): SerializableWorkerMetadata | nul
1777
1914
  if (typeof source.concurrency === 'number') normalized.concurrency = source.concurrency
1778
1915
  if (typeof source.lockDuration === 'number') normalized.lockDuration = source.lockDuration
1779
1916
  if (typeof source.maxStalledCount === 'number') normalized.maxStalledCount = source.maxStalledCount
1917
+ if (typeof source.onJobAbandoned === 'function') normalized.hasJobAbandonedHook = true
1918
+ if (source.schedulerSafe === true) normalized.schedulerSafe = true
1919
+ if (Array.isArray(source.schedulerRequiredFeatures)) {
1920
+ const features = source.schedulerRequiredFeatures.filter(
1921
+ (feature): feature is string => typeof feature === 'string' && feature.length > 0,
1922
+ )
1923
+ if (features.length > 0) normalized.schedulerRequiredFeatures = features
1924
+ }
1780
1925
  return Object.keys(normalized).length > 0 ? normalized : null
1781
1926
  }
1782
1927
 
@@ -1788,8 +1933,16 @@ async function loadSubscriberMetadata(sourceFile: string): Promise<SerializableS
1788
1933
 
1789
1934
  async function loadWorkerMetadata(sourceFile: string): Promise<SerializableWorkerMetadata | null> {
1790
1935
  const sourceModule = await loadModuleExportsFromSource<Record<string, unknown>>(sourceFile)
1791
- return normalizeWorkerMetadata(sourceModule?.metadata)
1936
+ const metadata = normalizeWorkerMetadata(sourceModule?.metadata)
1792
1937
  ?? normalizeWorkerMetadata(extractNamedObjectLiteralExport(sourceFile, 'metadata'))
1938
+ if (!metadata) return null
1939
+ // Resolved from the syntax, not the extracted value: a worker module that cannot be imported at
1940
+ // build time falls back to the literal extractor, which cannot tell an imported function from an
1941
+ // unresolvable identifier. Missing this is silent — the hook is simply never installed.
1942
+ if (!metadata.hasJobAbandonedHook && namedObjectLiteralDeclaresProperty(sourceFile, 'metadata', 'onJobAbandoned')) {
1943
+ metadata.hasJobAbandonedHook = true
1944
+ }
1945
+ return metadata
1793
1946
  }
1794
1947
 
1795
1948
  async function discoverSubscribers(
@@ -1974,7 +2127,7 @@ async function processPageFiles(options: {
1974
2127
  const metaPrefix = type === 'frontend' ? 'M' : 'BM'
1975
2128
  const eagerRoutes: string[] = []
1976
2129
  const runtimeRoutes: string[] = []
1977
- const manifestRoutes: string[] = []
2130
+ const manifestRoutes: WriterFunction[] = []
1978
2131
  const manifestShardRoutes: RouteManifestShardEntry[] = []
1979
2132
  const metadataRoutes: RouteManifestShardEntry[] = []
1980
2133
  const routePatterns: string[] = []
@@ -2049,18 +2202,25 @@ async function processPageFiles(options: {
2049
2202
  const manifestBaseProps = buildPageRouteManifestSpread(manifestMetaExpr, routePath)
2050
2203
  eagerRoutes.push(`{ ${baseProps}, Component: ${buildLazyRouteComponentExpression(importPath)} }`)
2051
2204
  runtimeRoutes.push(`{ ${runtimeBaseProps}, Component: ${buildLazyRouteComponentExpression(importPath)} }`)
2052
- const metadataDeclaration = `{ moduleId: ${toLiteral(modId)}, ${manifestBaseProps} }`
2053
- const manifestDeclaration = `{ moduleId: ${toLiteral(modId)}, ${manifestBaseProps}, load: async () => { const mod = await ${buildDynamicImportExpression(importPath)}; return (mod.default ?? mod) as any } }`
2054
- manifestRoutes.push(manifestDeclaration)
2205
+ const metadataEntries: GeneratedObjectEntry[] = [
2206
+ { name: 'moduleId', value: modId },
2207
+ { kind: 'spread', value: manifestBaseProps },
2208
+ ]
2209
+ const manifestEntry = objectLiteral([
2210
+ ...metadataEntries,
2211
+ { name: 'load', value: buildManifestRouteLoader(importPath) },
2212
+ ])
2213
+ const shardImports = manifestImportStatement ? [manifestImportStatement] : []
2214
+ manifestRoutes.push(manifestEntry)
2055
2215
  manifestShardRoutes.push({
2056
2216
  path: routePath,
2057
- declaration: manifestDeclaration,
2058
- imports: manifestImportStatement ? [manifestImportStatement] : [],
2217
+ declaration: renderGeneratedValue(manifestEntry),
2218
+ imports: shardImports,
2059
2219
  })
2060
2220
  metadataRoutes.push({
2061
2221
  path: routePath,
2062
- declaration: metadataDeclaration,
2063
- imports: manifestImportStatement ? [manifestImportStatement] : [],
2222
+ declaration: renderGeneratedValue(objectLiteral(metadataEntries)),
2223
+ imports: shardImports,
2064
2224
  })
2065
2225
  routePatterns.push(routePath)
2066
2226
  }
@@ -2140,18 +2300,25 @@ async function processPageFiles(options: {
2140
2300
  const manifestBaseProps = buildPageRouteManifestSpread(manifestMetaExpr, routePath)
2141
2301
  eagerRoutes.push(`{ ${baseProps}, Component: ${buildLazyRouteComponentExpression(importPath)} }`)
2142
2302
  runtimeRoutes.push(`{ ${runtimeBaseProps}, Component: ${buildLazyRouteComponentExpression(importPath)} }`)
2143
- const metadataDeclaration = `{ moduleId: ${toLiteral(modId)}, ${manifestBaseProps} }`
2144
- const manifestDeclaration = `{ moduleId: ${toLiteral(modId)}, ${manifestBaseProps}, load: async () => { const mod = await ${buildDynamicImportExpression(importPath)}; return (mod.default ?? mod) as any } }`
2145
- manifestRoutes.push(manifestDeclaration)
2303
+ const metadataEntries: GeneratedObjectEntry[] = [
2304
+ { name: 'moduleId', value: modId },
2305
+ { kind: 'spread', value: manifestBaseProps },
2306
+ ]
2307
+ const manifestEntry = objectLiteral([
2308
+ ...metadataEntries,
2309
+ { name: 'load', value: buildManifestRouteLoader(importPath) },
2310
+ ])
2311
+ const shardImports = manifestImportStatement ? [manifestImportStatement] : []
2312
+ manifestRoutes.push(manifestEntry)
2146
2313
  manifestShardRoutes.push({
2147
2314
  path: routePath,
2148
- declaration: manifestDeclaration,
2149
- imports: manifestImportStatement ? [manifestImportStatement] : [],
2315
+ declaration: renderGeneratedValue(manifestEntry),
2316
+ imports: shardImports,
2150
2317
  })
2151
2318
  metadataRoutes.push({
2152
2319
  path: routePath,
2153
- declaration: metadataDeclaration,
2154
- imports: manifestImportStatement ? [manifestImportStatement] : [],
2320
+ declaration: renderGeneratedValue(objectLiteral(metadataEntries)),
2321
+ imports: shardImports,
2155
2322
  })
2156
2323
  routePatterns.push(routePath)
2157
2324
  }
@@ -2183,7 +2350,7 @@ async function processApiRoutes(options: {
2183
2350
 
2184
2351
  const eagerApis: string[] = []
2185
2352
  const runtimeApis: string[] = []
2186
- const manifestApis: string[] = []
2353
+ const manifestApis: WriterFunction[] = []
2187
2354
  const manifestShardApis: RouteManifestShardEntry[] = []
2188
2355
  const metadataApis: RouteManifestShardEntry[] = []
2189
2356
 
@@ -2216,11 +2383,19 @@ async function processApiRoutes(options: {
2216
2383
  eagerImports.push(buildImportStatement(`* as ${importName}`, importPath))
2217
2384
  eagerApis.push(`{ path: ((${importName} as any).metadata?.path ?? ${toLiteral(routePath)}), metadata: (${importName} as any).metadata, handlers: ${importName} as any${docsPart} }`)
2218
2385
  runtimeApis.push(`{ path: ${toLiteral(resolvedPath)}, metadata: ${metadataLiteral}, handlers: { ${exportedMethods.map((method) => `${method}: async (req: Request, ctx?: any) => { const mod = await ${buildDynamicImportExpression(importPath)}; return (mod as any).${method}(req, ctx) }`).join(', ')} } }`)
2219
- const metadataDeclaration = `{ moduleId: ${toLiteral(modId)}, kind: ${toLiteral('route-file')}, path: ${toLiteral(resolvedPath)}, methods: [${exportedMethods.map((method) => toLiteral(method)).join(', ')}] }`
2220
- const manifestDeclaration = `{ moduleId: ${toLiteral(modId)}, kind: ${toLiteral('route-file')}, path: ${toLiteral(resolvedPath)}, methods: [${exportedMethods.map((method) => toLiteral(method)).join(', ')}], load: async () => ${buildDynamicImportExpression(importPath)} }`
2221
- manifestApis.push(manifestDeclaration)
2222
- manifestShardApis.push({ path: resolvedPath, declaration: manifestDeclaration })
2223
- metadataApis.push({ path: resolvedPath, declaration: metadataDeclaration })
2386
+ const metadataEntries: GeneratedObjectEntry[] = [
2387
+ { name: 'moduleId', value: modId },
2388
+ { name: 'kind', value: 'route-file' },
2389
+ { name: 'path', value: resolvedPath },
2390
+ { name: 'methods', value: arrayLiteral(exportedMethods, writeValue) },
2391
+ ]
2392
+ const manifestEntry = objectLiteral([
2393
+ ...metadataEntries,
2394
+ { name: 'load', value: buildManifestApiLoader(importPath) },
2395
+ ])
2396
+ manifestApis.push(manifestEntry)
2397
+ manifestShardApis.push({ path: resolvedPath, declaration: renderGeneratedValue(manifestEntry) })
2398
+ metadataApis.push({ path: resolvedPath, declaration: renderGeneratedValue(objectLiteral(metadataEntries)) })
2224
2399
  }
2225
2400
 
2226
2401
  // Single files (plain scripts, not route.*, not tests, skip method dirs)
@@ -2252,11 +2427,19 @@ async function processApiRoutes(options: {
2252
2427
  eagerImports.push(buildImportStatement(`* as ${importName}`, importPath))
2253
2428
  eagerApis.push(`{ path: ((${importName} as any).metadata?.path ?? ${toLiteral(routePath)}), metadata: (${importName} as any).metadata, handlers: ${importName} as any${docsPart} }`)
2254
2429
  runtimeApis.push(`{ path: ${toLiteral(resolvedPath)}, metadata: ${metadataLiteral}, handlers: { ${exportedMethods.map((entryMethod) => `${entryMethod}: async (req: Request, ctx?: any) => { const mod = await ${buildDynamicImportExpression(importPath)}; return (mod as any).${entryMethod}(req, ctx) }`).join(', ')} } }`)
2255
- const metadataDeclaration = `{ moduleId: ${toLiteral(modId)}, kind: ${toLiteral('route-file')}, path: ${toLiteral(resolvedPath)}, methods: [${exportedMethods.map((entryMethod) => toLiteral(entryMethod)).join(', ')}] }`
2256
- const manifestDeclaration = `{ moduleId: ${toLiteral(modId)}, kind: ${toLiteral('route-file')}, path: ${toLiteral(resolvedPath)}, methods: [${exportedMethods.map((entryMethod) => toLiteral(entryMethod)).join(', ')}], load: async () => ${buildDynamicImportExpression(importPath)} }`
2257
- manifestApis.push(manifestDeclaration)
2258
- manifestShardApis.push({ path: resolvedPath, declaration: manifestDeclaration })
2259
- metadataApis.push({ path: resolvedPath, declaration: metadataDeclaration })
2430
+ const metadataEntries: GeneratedObjectEntry[] = [
2431
+ { name: 'moduleId', value: modId },
2432
+ { name: 'kind', value: 'route-file' },
2433
+ { name: 'path', value: resolvedPath },
2434
+ { name: 'methods', value: arrayLiteral(exportedMethods, writeValue) },
2435
+ ]
2436
+ const manifestEntry = objectLiteral([
2437
+ ...metadataEntries,
2438
+ { name: 'load', value: buildManifestApiLoader(importPath) },
2439
+ ])
2440
+ manifestApis.push(manifestEntry)
2441
+ manifestShardApis.push({ path: resolvedPath, declaration: renderGeneratedValue(manifestEntry) })
2442
+ metadataApis.push({ path: resolvedPath, declaration: renderGeneratedValue(objectLiteral(metadataEntries)) })
2260
2443
  }
2261
2444
 
2262
2445
  // Legacy per-method
@@ -2298,11 +2481,20 @@ async function processApiRoutes(options: {
2298
2481
  eagerImports.push(buildImportStatement(`${importName}, * as ${metaName}`, importPath))
2299
2482
  eagerApis.push(`{ method: ${toLiteral(method)}, path: (${metaName}.metadata?.path ?? ${toLiteral(routePath)}), handler: ${importName}, metadata: ${metaName}.metadata${docsPart} }`)
2300
2483
  runtimeApis.push(`{ method: ${toLiteral(method)}, path: ${toLiteral(resolvedPath)}, handler: async (req: Request, ctx?: any) => { const mod = await ${buildDynamicImportExpression(importPath)}; const handler = ((mod as any).default ?? (mod as any).${method} ?? (mod as any).handler) as any; return handler(req, ctx) }, metadata: ${metadataLiteral} }`)
2301
- const metadataDeclaration = `{ moduleId: ${toLiteral(modId)}, kind: ${toLiteral('legacy')}, method: ${toLiteral(method)}, path: ${toLiteral(resolvedPath)}, methods: [${toLiteral(method)}] }`
2302
- const manifestDeclaration = `{ moduleId: ${toLiteral(modId)}, kind: ${toLiteral('legacy')}, method: ${toLiteral(method)}, path: ${toLiteral(resolvedPath)}, methods: [${toLiteral(method)}], load: async () => ${buildDynamicImportExpression(importPath)} }`
2303
- manifestApis.push(manifestDeclaration)
2304
- manifestShardApis.push({ path: resolvedPath, declaration: manifestDeclaration })
2305
- metadataApis.push({ path: resolvedPath, declaration: metadataDeclaration })
2484
+ const metadataEntries: GeneratedObjectEntry[] = [
2485
+ { name: 'moduleId', value: modId },
2486
+ { name: 'kind', value: 'legacy' },
2487
+ { name: 'method', value: method },
2488
+ { name: 'path', value: resolvedPath },
2489
+ { name: 'methods', value: arrayLiteral([method], writeValue) },
2490
+ ]
2491
+ const manifestEntry = objectLiteral([
2492
+ ...metadataEntries,
2493
+ { name: 'load', value: buildManifestApiLoader(importPath) },
2494
+ ])
2495
+ manifestApis.push(manifestEntry)
2496
+ manifestShardApis.push({ path: resolvedPath, declaration: renderGeneratedValue(manifestEntry) })
2497
+ metadataApis.push({ path: resolvedPath, declaration: renderGeneratedValue(objectLiteral(metadataEntries)) })
2306
2498
  }
2307
2499
  }
2308
2500
 
@@ -2315,62 +2507,17 @@ async function processApiRoutes(options: {
2315
2507
  }
2316
2508
  }
2317
2509
 
2318
- function processSubscribers(discovered: DiscoveredSubscriber[]): string[] {
2319
- const subscribers: string[] = []
2320
- for (const { id, importPath, metadata } of discovered) {
2321
- const subscriberId = metadata?.id ?? id
2322
- subscribers.push(
2323
- `{ id: ${toLiteral(subscriberId)}, event: ${toLiteral(metadata?.event ?? '')}, persistent: ${metadata?.persistent === undefined ? 'undefined' : toLiteral(metadata.persistent)}, sync: ${metadata?.sync === undefined ? 'undefined' : toLiteral(metadata.sync)}, priority: ${metadata?.priority === undefined ? 'undefined' : toLiteral(metadata.priority)}, handler: createLazyModuleSubscriber(() => ${buildDynamicImportExpression(importPath)}, ${toLiteral(subscriberId)}) }`
2324
- )
2325
- }
2326
- return subscribers
2327
- }
2328
-
2329
- function processWorkers(discovered: DiscoveredWorker[]): string[] {
2330
- const workers: string[] = []
2331
- for (const { id, importPath, metadata } of discovered) {
2332
- const workerId = metadata.id ?? id
2333
- workers.push(
2334
- `{ id: ${toLiteral(workerId)}, queue: ${toLiteral(metadata.queue)}, concurrency: ${toLiteral(metadata.concurrency ?? 1)}${metadata.lockDuration === undefined ? '' : `, lockDuration: ${toLiteral(metadata.lockDuration)}`}${metadata.maxStalledCount === undefined ? '' : `, maxStalledCount: ${toLiteral(metadata.maxStalledCount)}`}, handler: createLazyModuleWorker(() => ${buildDynamicImportExpression(importPath)}, ${toLiteral(workerId)}) }`
2335
- )
2336
- }
2337
- return workers
2338
- }
2510
+ /**
2511
+ * Registry helper emitted for a worker that declares `metadata.onJobAbandoned`.
2512
+ *
2513
+ * Named once because two things must stay in step: the call the worker entry renders, and the import
2514
+ * the file needs for it. A generated file that references this without importing it type-checks
2515
+ * nowhere, and only fails at `build:app` — see `generated-registry-imports.test.ts`.
2516
+ */
2517
+ const ABANDON_HOOK_FACTORY = 'createLazyModuleWorkerAbandonHook'
2339
2518
 
2340
- function processTranslations(options: {
2341
- discovered: DiscoveredTranslation[]
2342
- modId: string
2343
- appImportBase: string
2344
- pkgImportBase: string
2345
- imports: string[]
2346
- extraImports?: string[]
2347
- }): string[] {
2348
- const { discovered, modId, appImportBase, pkgImportBase, imports, extraImports } = options
2349
- const translations: string[] = []
2350
- for (const { locale, coreHas, appHas } of discovered) {
2351
- if (coreHas && appHas) {
2352
- const cName = `T_${toVar(modId)}_${toVar(locale)}_C`
2353
- const aName = `T_${toVar(modId)}_${toVar(locale)}_A`
2354
- imports.push(buildImportStatement(cName, `${pkgImportBase}/i18n/${locale}.json`))
2355
- imports.push(buildImportStatement(aName, `${appImportBase}/i18n/${locale}.json`))
2356
- extraImports?.push(buildImportStatement(cName, `${pkgImportBase}/i18n/${locale}.json`))
2357
- extraImports?.push(buildImportStatement(aName, `${appImportBase}/i18n/${locale}.json`))
2358
- translations.push(
2359
- `'${locale}': { ...( ${cName} as unknown as Record<string,string> ), ...( ${aName} as unknown as Record<string,string> ) }`
2360
- )
2361
- } else if (appHas) {
2362
- const aName = `T_${toVar(modId)}_${toVar(locale)}_A`
2363
- imports.push(buildImportStatement(aName, `${appImportBase}/i18n/${locale}.json`))
2364
- extraImports?.push(buildImportStatement(aName, `${appImportBase}/i18n/${locale}.json`))
2365
- translations.push(`'${locale}': ${aName} as unknown as Record<string,string>`)
2366
- } else if (coreHas) {
2367
- const cName = `T_${toVar(modId)}_${toVar(locale)}_C`
2368
- imports.push(buildImportStatement(cName, `${pkgImportBase}/i18n/${locale}.json`))
2369
- extraImports?.push(buildImportStatement(cName, `${pkgImportBase}/i18n/${locale}.json`))
2370
- translations.push(`'${locale}': ${cName} as unknown as Record<string,string>`)
2371
- }
2372
- }
2373
- return translations
2519
+ function workersDeclareAbandonHook(discovered: DiscoveredWorker[]): boolean {
2520
+ return discovered.some(({ metadata }) => metadata.hasJobAbandonedHook === true)
2374
2521
  }
2375
2522
 
2376
2523
  /**
@@ -2517,6 +2664,64 @@ function buildModuleDashboardWidgetsValue(entries: DashboardWidgetEntry[]): Writ
2517
2664
  )
2518
2665
  }
2519
2666
 
2667
+ /**
2668
+ * Wraps expressions that are still produced as pre-rendered strings by collaborators this
2669
+ * change does not convert (page routes, api handlers, dashboard widgets) so they can sit
2670
+ * inside an AST-built array without being re-quoted as string literals. It disappears once
2671
+ * those producers emit writers themselves — see the follow-up noted in the spec's Non-Goals.
2672
+ */
2673
+ function preRenderedExpressionList(expressions: readonly string[]): WriterFunction {
2674
+ return arrayLiteral(expressions.map((expression) => identifier(expression)), writeValue)
2675
+ }
2676
+
2677
+ /**
2678
+ * `(X.<first> ?? X.<second>)` — the member-fallback shape the main registry path emits.
2679
+ * It is deliberately not `namespaceFallback`, which the app path uses: that helper walks
2680
+ * the members in an IIFE and treats only `!= null` as present, which is a different
2681
+ * expression. Converging the two is a behaviour change, not a formatting one.
2682
+ */
2683
+ function legacyNamespaceMemberFallback(importName: string, members: readonly [string, string]): WriterFunction {
2684
+ return parenthesized(nullishCoalesce([
2685
+ propertyAccess(identifier(importName), members[0]),
2686
+ propertyAccess(identifier(importName), members[1]),
2687
+ ]))
2688
+ }
2689
+
2690
+ function buildLegacyModuleListValue(options: {
2691
+ importName: string
2692
+ members: readonly [string, string]
2693
+ castType: string
2694
+ }): WriterFunction {
2695
+ return logicalOr([
2696
+ asExpression(legacyNamespaceMemberFallback(options.importName, options.members), options.castType),
2697
+ emptyArray(),
2698
+ ])
2699
+ }
2700
+
2701
+ function buildLegacyModuleSetupValue(importName: string): WriterFunction {
2702
+ return logicalOr([
2703
+ legacyNamespaceMemberFallback(importName, ['default', 'setup']),
2704
+ identifier('undefined'),
2705
+ ])
2706
+ }
2707
+
2708
+ function buildLegacyIntegrationListValue(
2709
+ importName: string,
2710
+ pluralMember: string,
2711
+ singularMember: string,
2712
+ castType: string,
2713
+ ): WriterFunction {
2714
+ const singular = propertyAccess(identifier(importName), singularMember)
2715
+
2716
+ return asExpression(
2717
+ parenthesized(nullishCoalesce([
2718
+ propertyAccess(identifier(importName), pluralMember),
2719
+ parenthesized(conditionalExpression(singular, arrayLiteral([singular], writeValue), emptyArray())),
2720
+ ])),
2721
+ castType,
2722
+ )
2723
+ }
2724
+
2520
2725
  function buildModuleIntegrationListValue(
2521
2726
  importName: string,
2522
2727
  pluralMember: string,
@@ -2564,9 +2769,15 @@ function buildModulesInfoExpression(): WriterFunction {
2564
2769
  function renderAstModuleRegistryFile(options: {
2565
2770
  fileName: string
2566
2771
  generator: string
2567
- imports: string[]
2772
+ /**
2773
+ * Import statements, or the structured specs generator extensions push onto the shared
2774
+ * import array via `processStandaloneConfig`. Both shapes reach this emitter at runtime,
2775
+ * so both are normalised here rather than at each call site.
2776
+ */
2777
+ imports: GeneratedImportStatement[]
2568
2778
  moduleEntries: WriterFunction[]
2569
2779
  includeCreateElementImport?: boolean
2780
+ includeAbandonHookImport?: boolean
2570
2781
  }): string {
2571
2782
  const sourceFile = createGeneratedSourceFile(options.fileName)
2572
2783
  addAutoGeneratedComment(sourceFile, options.generator)
@@ -2582,10 +2793,11 @@ function renderAstModuleRegistryFile(options: {
2582
2793
  namedImports: [
2583
2794
  { name: 'createLazyModuleSubscriber' },
2584
2795
  { name: 'createLazyModuleWorker' },
2796
+ ...(options.includeAbandonHookImport ? [{ name: ABANDON_HOOK_FACTORY }] : []),
2585
2797
  { name: 'Module', isTypeOnly: true },
2586
2798
  ],
2587
2799
  })
2588
- addImportStatements(sourceFile, options.imports)
2800
+ addImportStatements(sourceFile, options.imports.map((entry) => serializeGeneratedImport(entry)))
2589
2801
 
2590
2802
  sourceFile.addVariableStatement({
2591
2803
  declarationKind: VariableDeclarationKind.Const,
@@ -2616,6 +2828,52 @@ function renderAstModuleRegistryFile(options: {
2616
2828
  return getSourceText(sourceFile)
2617
2829
  }
2618
2830
 
2831
+ function renderAstManifestFile(options: {
2832
+ fileName: string
2833
+ typeName: string
2834
+ exportName: string
2835
+ imports?: string[]
2836
+ entries: WriterFunction[]
2837
+ }): string {
2838
+ const sourceFile = createGeneratedSourceFile(options.fileName)
2839
+ addAutoGeneratedComment(sourceFile, 'registry')
2840
+
2841
+ const usesPageRouteMetadataHelper = options.typeName === 'FrontendRouteManifestEntry'
2842
+ || options.typeName === 'BackendRouteManifestEntry'
2843
+ addImportSpec(sourceFile, usesPageRouteMetadataHelper
2844
+ ? {
2845
+ moduleSpecifier: '@open-mercato/shared/modules/registry',
2846
+ namedImports: [
2847
+ { name: 'resolvePageRouteMetadata' },
2848
+ { name: options.typeName, isTypeOnly: true },
2849
+ ],
2850
+ }
2851
+ : {
2852
+ moduleSpecifier: '@open-mercato/shared/modules/registry',
2853
+ isTypeOnly: true,
2854
+ namedImports: [{ name: options.typeName }],
2855
+ })
2856
+ addImportStatements(sourceFile, options.imports ?? [])
2857
+
2858
+ sourceFile.addVariableStatement({
2859
+ declarationKind: VariableDeclarationKind.Const,
2860
+ isExported: true,
2861
+ declarations: [
2862
+ {
2863
+ name: options.exportName,
2864
+ type: `${options.typeName}[]`,
2865
+ initializer: arrayLiteral(options.entries, writeValue),
2866
+ },
2867
+ ],
2868
+ })
2869
+ sourceFile.addExportAssignment({
2870
+ isExportEquals: false,
2871
+ expression: options.exportName,
2872
+ })
2873
+
2874
+ return getSourceText(sourceFile)
2875
+ }
2876
+
2619
2877
  function renderAstLegacyAliasFile(options: {
2620
2878
  fileName: string
2621
2879
  exportName: string
@@ -2679,62 +2937,10 @@ function renderEnabledModuleIdsFile(moduleIds: readonly string[]): string {
2679
2937
  return getSourceText(sourceFile)
2680
2938
  }
2681
2939
 
2682
- function renderLegacyCompatibleArray(entries: readonly string[]): string {
2683
- return `[
2684
- ${entries.join(',\n ')}
2685
- ]`
2686
- }
2687
-
2688
2940
  function buildLazyRouteComponentExpression(importPath: string): string {
2689
2941
  return `async (props: any) => { const mod = await ${buildDynamicImportExpression(importPath)}; const Component = (mod.default ?? mod) as any; return createElement(Component, props) }`
2690
2942
  }
2691
2943
 
2692
- function renderAstLegacyModuleRegistryOutput(options: {
2693
- fileName: string
2694
- imports: GeneratedImportStatement[]
2695
- moduleEntries: string[]
2696
- includeCreateElementImport?: boolean
2697
- }): string {
2698
- const importSection = [
2699
- ...(options.includeCreateElementImport ? ["import { createElement } from 'react'"] : []),
2700
- "import { createLazyModuleSubscriber, createLazyModuleWorker, type Module } from '@open-mercato/shared/modules/registry'",
2701
- ...options.imports.map((entry) => serializeGeneratedImport(entry)),
2702
- ].join('\n')
2703
-
2704
- return `// AUTO-GENERATED by mercato generate registry
2705
- ${importSection}
2706
-
2707
- export const modules: Module[] = ${renderLegacyCompatibleArray(options.moduleEntries)}
2708
- export const modulesInfo = modules.map(m => ({ id: m.id, ...(m.info || {}) }))
2709
- export default modules
2710
- `
2711
- }
2712
-
2713
- function renderAstLegacyManifestOutput(options: {
2714
- fileName: string
2715
- typeName: string
2716
- exportName: string
2717
- imports?: GeneratedImportStatement[]
2718
- entries: string[]
2719
- }): string {
2720
- const usesPageRouteMetadataHelper = options.typeName === 'FrontendRouteManifestEntry'
2721
- || options.typeName === 'BackendRouteManifestEntry'
2722
- const importSection = [
2723
- usesPageRouteMetadataHelper
2724
- ? `import { resolvePageRouteMetadata, type ${options.typeName} } from '@open-mercato/shared/modules/registry'`
2725
- : `import type { ${options.typeName} } from '@open-mercato/shared/modules/registry'`,
2726
- ...(options.imports ?? []).map((entry) => serializeGeneratedImport(entry)),
2727
- ].join('\n')
2728
-
2729
- return `// AUTO-GENERATED by mercato generate registry
2730
- ${importSection}
2731
-
2732
- export const ${options.exportName}: ${options.typeName}[] = ${renderLegacyCompatibleArray(options.entries)}
2733
-
2734
- export default ${options.exportName}
2735
- `
2736
- }
2737
-
2738
2944
  function buildRuntimeRouteComponent(importPath: string): WriterFunction {
2739
2945
  return arrowFunction({
2740
2946
  async: true,
@@ -3009,8 +3215,21 @@ function processWorkersAst(discovered: DiscoveredWorker[]): WriterFunction[] {
3009
3215
  { name: 'id', value: workerId },
3010
3216
  { name: 'queue', value: metadata.queue },
3011
3217
  { name: 'concurrency', value: metadata.concurrency ?? 1 },
3218
+ ...(metadata.schedulerSafe === true ? [{ name: 'schedulerSafe', value: true }] : []),
3219
+ ...(metadata.schedulerRequiredFeatures
3220
+ ? [{ name: 'schedulerRequiredFeatures', value: arrayLiteral(metadata.schedulerRequiredFeatures, writeValue) }]
3221
+ : []),
3012
3222
  ...(metadata.lockDuration === undefined ? [] : [{ name: 'lockDuration', value: metadata.lockDuration }]),
3013
3223
  ...(metadata.maxStalledCount === undefined ? [] : [{ name: 'maxStalledCount', value: metadata.maxStalledCount }]),
3224
+ ...(metadata.hasJobAbandonedHook
3225
+ ? [{
3226
+ name: 'onJobAbandoned',
3227
+ value: callExpression(identifier(ABANDON_HOOK_FACTORY), [
3228
+ arrowFunction({ body: importExpression(importPath) }),
3229
+ workerId,
3230
+ ]),
3231
+ }]
3232
+ : []),
3014
3233
  {
3015
3234
  name: 'handler',
3016
3235
  value: callExpression(identifier('createLazyModuleWorker'), [
@@ -3370,11 +3589,11 @@ async function generateModuleRegistryFromDiscovery(options: ModuleRegistryRender
3370
3589
  const runtimeImports: string[] = []
3371
3590
  const frontendRouteManifestImports: string[] = []
3372
3591
  const backendRouteManifestImports: string[] = []
3373
- const moduleDecls: string[] = []
3374
- const runtimeModuleDecls: string[] = []
3375
- const frontendRouteManifestDecls: string[] = []
3376
- const backendRouteManifestDecls: string[] = []
3377
- const apiRouteManifestDecls: string[] = []
3592
+ const moduleDecls: WriterFunction[] = []
3593
+ const runtimeModuleDecls: WriterFunction[] = []
3594
+ const frontendRouteManifestDecls: WriterFunction[] = []
3595
+ const backendRouteManifestDecls: WriterFunction[] = []
3596
+ const apiRouteManifestDecls: WriterFunction[] = []
3378
3597
  const backendRouteShardEntries: RouteManifestShardEntry[] = []
3379
3598
  const backendRouteMetadataEntries: RouteManifestShardEntry[] = []
3380
3599
  const apiRouteShardEntries: RouteManifestShardEntry[] = []
@@ -3384,6 +3603,7 @@ async function generateModuleRegistryFromDiscovery(options: ModuleRegistryRender
3384
3603
  const requiresByModule = new Map<string, string[]>()
3385
3604
  const commandLoaderEntries: CommandLoaderGenerationEntry[] = []
3386
3605
  let hasRouteComponents = false
3606
+ let hasAbandonHookWorkers = false
3387
3607
 
3388
3608
  // UMES conflict detection: collect file paths during module processing
3389
3609
  const umesConflictSources: Array<{
@@ -3406,9 +3626,9 @@ async function generateModuleRegistryFromDiscovery(options: ModuleRegistryRender
3406
3626
  const runtimeBackendRoutes: string[] = []
3407
3627
  const runtimeApis: string[] = []
3408
3628
  let cliImportName: string | null = null
3409
- const translations: string[] = []
3410
- const subscribers: string[] = []
3411
- const workers: string[] = []
3629
+ const translations: GeneratedObjectEntry[] = []
3630
+ const subscribers: WriterFunction[] = []
3631
+ const workers: WriterFunction[] = []
3412
3632
  let infoImportName: string | null = null
3413
3633
  let extensionsImportName: string | null = null
3414
3634
  let fieldsImportName: string | null = null
@@ -3622,7 +3842,7 @@ async function generateModuleRegistryFromDiscovery(options: ModuleRegistryRender
3622
3842
  }
3623
3843
 
3624
3844
  // 16. Translations
3625
- translations.push(...processTranslations({
3845
+ translations.push(...processTranslationsAst({
3626
3846
  discovered: discovered.translations,
3627
3847
  modId,
3628
3848
  appImportBase,
@@ -3632,10 +3852,12 @@ async function generateModuleRegistryFromDiscovery(options: ModuleRegistryRender
3632
3852
  }))
3633
3853
 
3634
3854
  // 17. Subscribers
3635
- subscribers.push(...processSubscribers(await discovered.getSubscribers()))
3855
+ subscribers.push(...processSubscribersAst(await discovered.getSubscribers()))
3636
3856
 
3637
3857
  // 18. Workers
3638
- workers.push(...processWorkers(await discovered.getWorkers()))
3858
+ const discoveredWorkersForModule = await discovered.getWorkers()
3859
+ if (workersDeclareAbandonHook(discoveredWorkersForModule)) hasAbandonHookWorkers = true
3860
+ workers.push(...processWorkersAst(discoveredWorkersForModule))
3639
3861
 
3640
3862
  // Build combined customFieldSets expression
3641
3863
  {
@@ -3658,44 +3880,120 @@ async function generateModuleRegistryFromDiscovery(options: ModuleRegistryRender
3658
3880
  }
3659
3881
  }
3660
3882
 
3661
- moduleDecls.push(`{
3662
- id: ${toLiteral(modId)},
3663
- ${infoImportName ? `info: ${infoImportName}.metadata,` : ''}
3664
- ${frontendRoutes.length ? `frontendRoutes: [${frontendRoutes.join(', ')}],` : ''}
3665
- ${backendRoutes.length ? `backendRoutes: [${backendRoutes.join(', ')}],` : ''}
3666
- ${apis.length ? `apis: [${apis.join(', ')}],` : ''}
3667
- ${cliImportName ? `cli: ${cliImportName},` : ''}
3668
- ${translations.length ? `translations: { ${translations.join(', ')} },` : ''}
3669
- ${subscribers.length ? `subscribers: [${subscribers.join(', ')}],` : ''}
3670
- ${workers.length ? `workers: [${workers.join(', ')}],` : ''}
3671
- ${extensionsImportName ? `entityExtensions: ((${extensionsImportName}.default ?? ${extensionsImportName}.extensions) as import('@open-mercato/shared/modules/entities').EntityExtension[]) || [],` : ''}
3672
- customFieldSets: ${customFieldSetsExpr},
3673
- ${featuresImportName ? `features: ((${featuresImportName}.default ?? ${featuresImportName}.features) as any) || [],` : ''}
3674
- ${customEntitiesImportName ? `customEntities: ((${customEntitiesImportName}.default ?? ${customEntitiesImportName}.entities) as any) || [],` : ''}
3675
- ${dashboardWidgets.length ? `dashboardWidgets: [${dashboardWidgets.join(', ')}],` : ''}
3676
- ${setupImportName ? `setup: (${setupImportName}.default ?? ${setupImportName}.setup) || undefined,` : ''}
3677
- ${encryptionImportName ? `defaultEncryptionMaps: ((${encryptionImportName}.default ?? ${encryptionImportName}.defaultEncryptionMaps) as import('@open-mercato/shared/modules/encryption').ModuleEncryptionMap[]) || [],` : ''}
3678
- ${integrationExports ? `integrations: ((${integrationExports}.integrations ?? (${integrationExports}.integration ? [${integrationExports}.integration] : [])) as import('@open-mercato/shared/modules/integrations/types').IntegrationDefinition[]),` : ''}
3679
- ${integrationExports ? `bundles: ((${integrationExports}.bundles ?? (${integrationExports}.bundle ? [${integrationExports}.bundle] : [])) as import('@open-mercato/shared/modules/integrations/types').IntegrationBundle[]),` : ''}
3680
- }`)
3681
- runtimeModuleDecls.push(`{
3682
- id: ${toLiteral(modId)},
3683
- ${infoImportName ? `info: ${infoImportName}.metadata,` : ''}
3684
- ${runtimeFrontendRoutes.length ? `frontendRoutes: [${runtimeFrontendRoutes.join(', ')}],` : ''}
3685
- ${runtimeBackendRoutes.length ? `backendRoutes: [${runtimeBackendRoutes.join(', ')}],` : ''}
3686
- ${runtimeApis.length ? `apis: [${runtimeApis.join(', ')}],` : ''}
3687
- ${translations.length ? `translations: { ${translations.join(', ')} },` : ''}
3688
- ${subscribers.length ? `subscribers: [${subscribers.join(', ')}],` : ''}
3689
- ${workers.length ? `workers: [${workers.join(', ')}],` : ''}
3690
- ${extensionsImportName ? `entityExtensions: ((${extensionsImportName}.default ?? ${extensionsImportName}.extensions) as import('@open-mercato/shared/modules/entities').EntityExtension[]) || [],` : ''}
3691
- customFieldSets: ${customFieldSetsExpr},
3692
- ${featuresImportName ? `features: ((${featuresImportName}.default ?? ${featuresImportName}.features) as any) || [],` : ''}
3693
- ${customEntitiesImportName ? `customEntities: ((${customEntitiesImportName}.default ?? ${customEntitiesImportName}.entities) as any) || [],` : ''}
3694
- ${setupImportName ? `setup: (${setupImportName}.default ?? ${setupImportName}.setup) || undefined,` : ''}
3695
- ${encryptionImportName ? `defaultEncryptionMaps: ((${encryptionImportName}.default ?? ${encryptionImportName}.defaultEncryptionMaps) as import('@open-mercato/shared/modules/encryption').ModuleEncryptionMap[]) || [],` : ''}
3696
- ${integrationExports ? `integrations: ((${integrationExports}.integrations ?? (${integrationExports}.integration ? [${integrationExports}.integration] : [])) as import('@open-mercato/shared/modules/integrations/types').IntegrationDefinition[]),` : ''}
3697
- ${integrationExports ? `bundles: ((${integrationExports}.bundles ?? (${integrationExports}.bundle ? [${integrationExports}.bundle] : [])) as import('@open-mercato/shared/modules/integrations/types').IntegrationBundle[]),` : ''}
3698
- }`)
3883
+ const identityEntries: GeneratedObjectEntry[] = [{ name: 'id', value: modId }]
3884
+ if (infoImportName) {
3885
+ identityEntries.push({ name: 'info', value: propertyAccess(identifier(infoImportName), 'metadata') })
3886
+ }
3887
+
3888
+ const sharedTailEntries: GeneratedObjectEntry[] = []
3889
+ if (translations.length > 0) {
3890
+ sharedTailEntries.push({ name: 'translations', value: objectLiteral(translations) })
3891
+ }
3892
+ if (subscribers.length > 0) {
3893
+ sharedTailEntries.push({ name: 'subscribers', value: arrayLiteral(subscribers, writeValue) })
3894
+ }
3895
+ if (workers.length > 0) {
3896
+ sharedTailEntries.push({ name: 'workers', value: arrayLiteral(workers, writeValue) })
3897
+ }
3898
+ if (extensionsImportName) {
3899
+ sharedTailEntries.push({
3900
+ name: 'entityExtensions',
3901
+ value: buildLegacyModuleListValue({
3902
+ importName: extensionsImportName,
3903
+ members: ['default', 'extensions'],
3904
+ castType: "import('@open-mercato/shared/modules/entities').EntityExtension[]",
3905
+ }),
3906
+ })
3907
+ }
3908
+ sharedTailEntries.push({ name: 'customFieldSets', value: identifier(customFieldSetsExpr) })
3909
+ if (featuresImportName) {
3910
+ sharedTailEntries.push({
3911
+ name: 'features',
3912
+ value: buildLegacyModuleListValue({
3913
+ importName: featuresImportName,
3914
+ members: ['default', 'features'],
3915
+ castType: 'any',
3916
+ }),
3917
+ })
3918
+ }
3919
+ if (customEntitiesImportName) {
3920
+ sharedTailEntries.push({
3921
+ name: 'customEntities',
3922
+ value: buildLegacyModuleListValue({
3923
+ importName: customEntitiesImportName,
3924
+ members: ['default', 'entities'],
3925
+ castType: 'any',
3926
+ }),
3927
+ })
3928
+ }
3929
+
3930
+ const setupAndBeyondEntries: GeneratedObjectEntry[] = []
3931
+ if (setupImportName) {
3932
+ setupAndBeyondEntries.push({ name: 'setup', value: buildLegacyModuleSetupValue(setupImportName) })
3933
+ }
3934
+ if (encryptionImportName) {
3935
+ setupAndBeyondEntries.push({
3936
+ name: 'defaultEncryptionMaps',
3937
+ value: buildLegacyModuleListValue({
3938
+ importName: encryptionImportName,
3939
+ members: ['default', 'defaultEncryptionMaps'],
3940
+ castType: "import('@open-mercato/shared/modules/encryption').ModuleEncryptionMap[]",
3941
+ }),
3942
+ })
3943
+ }
3944
+ if (integrationExports) {
3945
+ setupAndBeyondEntries.push({
3946
+ name: 'integrations',
3947
+ value: buildLegacyIntegrationListValue(
3948
+ integrationExports,
3949
+ 'integrations',
3950
+ 'integration',
3951
+ "import('@open-mercato/shared/modules/integrations/types').IntegrationDefinition[]",
3952
+ ),
3953
+ })
3954
+ setupAndBeyondEntries.push({
3955
+ name: 'bundles',
3956
+ value: buildLegacyIntegrationListValue(
3957
+ integrationExports,
3958
+ 'bundles',
3959
+ 'bundle',
3960
+ "import('@open-mercato/shared/modules/integrations/types').IntegrationBundle[]",
3961
+ ),
3962
+ })
3963
+ }
3964
+
3965
+ const eagerEntries: GeneratedObjectEntry[] = [...identityEntries]
3966
+ if (frontendRoutes.length > 0) {
3967
+ eagerEntries.push({ name: 'frontendRoutes', value: preRenderedExpressionList(frontendRoutes) })
3968
+ }
3969
+ if (backendRoutes.length > 0) {
3970
+ eagerEntries.push({ name: 'backendRoutes', value: preRenderedExpressionList(backendRoutes) })
3971
+ }
3972
+ if (apis.length > 0) {
3973
+ eagerEntries.push({ name: 'apis', value: preRenderedExpressionList(apis) })
3974
+ }
3975
+ if (cliImportName) {
3976
+ eagerEntries.push({ name: 'cli', value: identifier(cliImportName) })
3977
+ }
3978
+ eagerEntries.push(...sharedTailEntries)
3979
+ if (dashboardWidgets.length > 0) {
3980
+ eagerEntries.push({ name: 'dashboardWidgets', value: preRenderedExpressionList(dashboardWidgets) })
3981
+ }
3982
+ eagerEntries.push(...setupAndBeyondEntries)
3983
+ moduleDecls.push(objectLiteral(eagerEntries))
3984
+
3985
+ const runtimeEntries: GeneratedObjectEntry[] = [...identityEntries]
3986
+ if (runtimeFrontendRoutes.length > 0) {
3987
+ runtimeEntries.push({ name: 'frontendRoutes', value: preRenderedExpressionList(runtimeFrontendRoutes) })
3988
+ }
3989
+ if (runtimeBackendRoutes.length > 0) {
3990
+ runtimeEntries.push({ name: 'backendRoutes', value: preRenderedExpressionList(runtimeBackendRoutes) })
3991
+ }
3992
+ if (runtimeApis.length > 0) {
3993
+ runtimeEntries.push({ name: 'apis', value: preRenderedExpressionList(runtimeApis) })
3994
+ }
3995
+ runtimeEntries.push(...sharedTailEntries, ...setupAndBeyondEntries)
3996
+ runtimeModuleDecls.push(objectLiteral(runtimeEntries))
3699
3997
  }
3700
3998
 
3701
3999
  // === UMES Conflict Detection ===
@@ -3796,33 +4094,37 @@ async function generateModuleRegistryFromDiscovery(options: ModuleRegistryRender
3796
4094
  }
3797
4095
  }
3798
4096
 
3799
- const output = renderAstLegacyModuleRegistryOutput({
4097
+ const output = renderAstModuleRegistryFile({
3800
4098
  fileName: 'modules.generated.ts',
4099
+ generator: 'registry',
3801
4100
  imports,
3802
4101
  moduleEntries: moduleDecls,
3803
4102
  includeCreateElementImport: hasRouteComponents,
4103
+ includeAbandonHookImport: hasAbandonHookWorkers,
3804
4104
  })
3805
- const runtimeOutput = renderAstLegacyModuleRegistryOutput({
4105
+ const runtimeOutput = renderAstModuleRegistryFile({
3806
4106
  fileName: 'modules.runtime.generated.ts',
4107
+ generator: 'registry',
3807
4108
  imports: runtimeImports,
3808
4109
  moduleEntries: runtimeModuleDecls,
3809
4110
  includeCreateElementImport: true,
4111
+ includeAbandonHookImport: hasAbandonHookWorkers,
3810
4112
  })
3811
- const frontendRoutesOutput = renderAstLegacyManifestOutput({
4113
+ const frontendRoutesOutput = renderAstManifestFile({
3812
4114
  fileName: 'frontend-routes.generated.ts',
3813
4115
  typeName: 'FrontendRouteManifestEntry',
3814
4116
  exportName: 'frontendRoutes',
3815
4117
  imports: frontendRouteManifestImports,
3816
4118
  entries: frontendRouteManifestDecls,
3817
4119
  })
3818
- const backendRoutesOutput = renderAstLegacyManifestOutput({
4120
+ const backendRoutesOutput = renderAstManifestFile({
3819
4121
  fileName: 'backend-routes.generated.ts',
3820
4122
  typeName: 'BackendRouteManifestEntry',
3821
4123
  exportName: 'backendRoutes',
3822
4124
  imports: backendRouteManifestImports,
3823
4125
  entries: backendRouteManifestDecls,
3824
4126
  })
3825
- const apiRoutesOutput = renderAstLegacyManifestOutput({
4127
+ const apiRoutesOutput = renderAstManifestFile({
3826
4128
  fileName: 'api-routes.generated.ts',
3827
4129
  typeName: 'ApiRouteManifestEntry',
3828
4130
  exportName: 'apiRoutes',
@@ -3962,15 +4264,11 @@ async function generateModuleRegistryFromDiscovery(options: ModuleRegistryRender
3962
4264
  allRegImports.push(...reg.registrationImports)
3963
4265
  allCalls.push(reg.buildCall(reg.entriesExportName))
3964
4266
  }
3965
- const uniqueImports = [...new Set([...allEntryImports, ...allRegImports])]
3966
- const importSection = uniqueImports.join('\n')
3967
- const body = ` ${allCalls.join('\n ')}`
3968
- const bootstrapRegsOutput = `// AUTO-GENERATED by mercato generate registry
3969
- ${importSection ? `${importSection}\n` : ''}
3970
- export function runBootstrapRegistrations(): void {
3971
- ${body}
3972
- }
3973
- `
4267
+ const bootstrapRegsOutput = renderBootstrapRegistrationsFile({
4268
+ entryImports: allEntryImports,
4269
+ registrationImports: allRegImports,
4270
+ calls: allCalls,
4271
+ })
3974
4272
  writeGeneratedFile({ outFile: bootstrapRegsOutFile, checksumFile: bootstrapRegsChecksumFile, content: bootstrapRegsOutput, structureChecksum, result, quiet })
3975
4273
  }
3976
4274
 
@@ -4010,6 +4308,7 @@ async function generateModuleRegistryAppFromDiscovery(options: ModuleRegistryRen
4010
4308
  const requiresByModule = new Map<string, string[]>()
4011
4309
  const commandLoaderEntries: CommandLoaderGenerationEntry[] = []
4012
4310
  let hasRouteComponents = false
4311
+ let hasAbandonHookWorkers = false
4013
4312
  const seenBackendRoutePatterns = new Map<string, string>()
4014
4313
 
4015
4314
  for (const discovered of discovery.modules) {
@@ -4171,7 +4470,9 @@ async function generateModuleRegistryAppFromDiscovery(options: ModuleRegistryRen
4171
4470
 
4172
4471
  subscribers.push(...processSubscribersAst(await discovered.getSubscribers()))
4173
4472
 
4174
- workers.push(...processWorkersAst(await discovered.getWorkers()))
4473
+ const discoveredWorkersForModule = await discovered.getWorkers()
4474
+ if (workersDeclareAbandonHook(discoveredWorkersForModule)) hasAbandonHookWorkers = true
4475
+ workers.push(...processWorkersAst(discoveredWorkersForModule))
4175
4476
 
4176
4477
  {
4177
4478
  dashboardWidgetsValue = buildModuleDashboardWidgetsValue(discovered.dashboardWidgetEntries)
@@ -4295,18 +4596,21 @@ async function generateModuleRegistryAppFromDiscovery(options: ModuleRegistryRen
4295
4596
  imports,
4296
4597
  moduleEntries: moduleDecls,
4297
4598
  includeCreateElementImport: hasRouteComponents,
4599
+ includeAbandonHookImport: hasAbandonHookWorkers,
4298
4600
  })
4299
4601
  const bootstrapOutput = renderAstModuleRegistryFile({
4300
4602
  fileName: 'modules.bootstrap.generated.ts',
4301
4603
  generator: 'registry (bootstrap version)',
4302
4604
  imports: bootstrapImports,
4303
4605
  moduleEntries: bootstrapModuleDecls,
4606
+ includeAbandonHookImport: hasAbandonHookWorkers,
4304
4607
  })
4305
4608
  const i18nOutput = renderAstModuleRegistryFile({
4306
4609
  fileName: 'modules.i18n.generated.ts',
4307
4610
  generator: 'registry (i18n version)',
4308
4611
  imports: i18nImports,
4309
4612
  moduleEntries: i18nModuleDecls,
4613
+ includeAbandonHookImport: hasAbandonHookWorkers,
4310
4614
  })
4311
4615
  const legacyOutput = renderAstLegacyAliasFile({
4312
4616
  fileName: 'bootstrap-modules.generated.ts',
@@ -4397,6 +4701,7 @@ async function generateModuleRegistryAppFromDiscovery(options: ModuleRegistryRen
4397
4701
  * Excludes: frontend routes, backend routes, API handlers, injection widgets
4398
4702
  */
4399
4703
  async function generateModuleRegistryCliFromDiscovery(options: ModuleRegistryRenderOptions): Promise<GeneratorResult> {
4704
+ let hasAbandonHookWorkers = false
4400
4705
  const { resolver, quiet = false } = options
4401
4706
  const { discovery } = options
4402
4707
  const result = createGeneratorResult()
@@ -4548,6 +4853,7 @@ async function generateModuleRegistryCliFromDiscovery(options: ModuleRegistryRen
4548
4853
  // Workers
4549
4854
  const discoveredWorkers = await discovered.getWorkers()
4550
4855
  const workerGenerationEntries = collectWorkerGenerationEntries(discoveredWorkers, modId)
4856
+ if (workersDeclareAbandonHook(discoveredWorkers)) hasAbandonHookWorkers = true
4551
4857
  workers.push(...processWorkersAst(discoveredWorkers))
4552
4858
  devSupervisorWorkers.push(...workerGenerationEntries.map(({ importPath: _importPath, ...worker }) => worker))
4553
4859
 
@@ -4671,6 +4977,7 @@ async function generateModuleRegistryCliFromDiscovery(options: ModuleRegistryRen
4671
4977
  generator: 'registry (CLI version)',
4672
4978
  imports,
4673
4979
  moduleEntries: moduleDecls,
4980
+ includeAbandonHookImport: hasAbandonHookWorkers,
4674
4981
  })
4675
4982
  const legacyOutput = renderAstLegacyAliasFile({
4676
4983
  fileName: 'cli-modules.generated.ts',