@open-mercato/cli 0.6.8-develop.6889.1.af4501ca97 → 0.6.8-develop.6892.1.f179677788

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 (106) hide show
  1. package/.turbo/turbo-build.log +3 -3
  2. package/AGENTS.md +6 -1
  3. package/dist/agentic/guides/module-facts.json +113 -113
  4. package/dist/agentic/guides/modules/ai_assistant.md +1 -1
  5. package/dist/agentic/guides/modules/api_docs.md +1 -1
  6. package/dist/agentic/guides/modules/api_keys.md +1 -1
  7. package/dist/agentic/guides/modules/attachments.md +1 -1
  8. package/dist/agentic/guides/modules/audit_logs.md +1 -1
  9. package/dist/agentic/guides/modules/auth.md +1 -1
  10. package/dist/agentic/guides/modules/business_rules.md +1 -1
  11. package/dist/agentic/guides/modules/catalog.md +1 -1
  12. package/dist/agentic/guides/modules/channel_gmail.md +1 -1
  13. package/dist/agentic/guides/modules/channel_imap.md +1 -1
  14. package/dist/agentic/guides/modules/checkout.md +1 -1
  15. package/dist/agentic/guides/modules/communication_channels.md +3 -3
  16. package/dist/agentic/guides/modules/configs.md +1 -1
  17. package/dist/agentic/guides/modules/content.md +1 -1
  18. package/dist/agentic/guides/modules/currencies.md +1 -1
  19. package/dist/agentic/guides/modules/customer_accounts.md +1 -1
  20. package/dist/agentic/guides/modules/customers.md +1 -1
  21. package/dist/agentic/guides/modules/dashboards.md +1 -1
  22. package/dist/agentic/guides/modules/data_sync.md +1 -1
  23. package/dist/agentic/guides/modules/design_system.md +1 -1
  24. package/dist/agentic/guides/modules/dictionaries.md +1 -1
  25. package/dist/agentic/guides/modules/directory.md +1 -1
  26. package/dist/agentic/guides/modules/entities.md +1 -1
  27. package/dist/agentic/guides/modules/events.md +1 -1
  28. package/dist/agentic/guides/modules/feature_toggles.md +1 -1
  29. package/dist/agentic/guides/modules/gateway_stripe.md +1 -1
  30. package/dist/agentic/guides/modules/generators.md +1 -1
  31. package/dist/agentic/guides/modules/inbox_ops.md +1 -1
  32. package/dist/agentic/guides/modules/integrations.md +1 -1
  33. package/dist/agentic/guides/modules/messages.md +1 -1
  34. package/dist/agentic/guides/modules/notifications.md +1 -1
  35. package/dist/agentic/guides/modules/onboarding.md +1 -1
  36. package/dist/agentic/guides/modules/payment_gateways.md +1 -1
  37. package/dist/agentic/guides/modules/perspectives.md +1 -1
  38. package/dist/agentic/guides/modules/planner.md +1 -1
  39. package/dist/agentic/guides/modules/portal.md +1 -1
  40. package/dist/agentic/guides/modules/progress.md +1 -1
  41. package/dist/agentic/guides/modules/query_index.md +1 -1
  42. package/dist/agentic/guides/modules/record_locks.md +1 -1
  43. package/dist/agentic/guides/modules/resources.md +1 -1
  44. package/dist/agentic/guides/modules/sales.md +1 -1
  45. package/dist/agentic/guides/modules/scheduler.md +1 -1
  46. package/dist/agentic/guides/modules/search.md +1 -1
  47. package/dist/agentic/guides/modules/security.md +1 -1
  48. package/dist/agentic/guides/modules/shipping_carriers.md +1 -1
  49. package/dist/agentic/guides/modules/sso.md +1 -1
  50. package/dist/agentic/guides/modules/staff.md +1 -1
  51. package/dist/agentic/guides/modules/storage_s3.md +1 -1
  52. package/dist/agentic/guides/modules/sync_akeneo.md +1 -1
  53. package/dist/agentic/guides/modules/sync_excel.md +1 -1
  54. package/dist/agentic/guides/modules/system_status_overlays.md +1 -1
  55. package/dist/agentic/guides/modules/translations.md +1 -1
  56. package/dist/agentic/guides/modules/webhooks.md +1 -1
  57. package/dist/agentic/guides/modules/wms.md +1 -1
  58. package/dist/agentic/guides/modules/workflows.md +1 -1
  59. package/dist/agentic/guides/upstream/BACKWARD_COMPATIBILITY.md +1 -0
  60. package/dist/agentic/guides/upstream/manifest.json +2 -2
  61. package/dist/bin.js +20 -34
  62. package/dist/bin.js.map +2 -2
  63. package/dist/lib/cli-bootstrap-mode.js +32 -0
  64. package/dist/lib/cli-bootstrap-mode.js.map +7 -0
  65. package/dist/lib/dev-supervisor-manifest.js +105 -0
  66. package/dist/lib/dev-supervisor-manifest.js.map +7 -0
  67. package/dist/lib/generators/entity-ids.js +11 -24
  68. package/dist/lib/generators/entity-ids.js.map +2 -2
  69. package/dist/lib/generators/module-di.js +10 -23
  70. package/dist/lib/generators/module-di.js.map +2 -2
  71. package/dist/lib/generators/module-entities.js +10 -23
  72. package/dist/lib/generators/module-entities.js.map +2 -2
  73. package/dist/lib/generators/module-registry.js +394 -31
  74. package/dist/lib/generators/module-registry.js.map +2 -2
  75. package/dist/lib/generators/route-manifest-shards.js +170 -0
  76. package/dist/lib/generators/route-manifest-shards.js.map +7 -0
  77. package/dist/lib/queue-worker-supervisor.js.map +2 -2
  78. package/dist/lib/resolver.js +26 -3
  79. package/dist/lib/resolver.js.map +2 -2
  80. package/dist/lib/utils.js +15 -8
  81. package/dist/lib/utils.js.map +2 -2
  82. package/dist/mercato.js +9 -6
  83. package/dist/mercato.js.map +2 -2
  84. package/package.json +6 -6
  85. package/src/__tests__/mercato.test.ts +144 -0
  86. package/src/bin.ts +28 -42
  87. package/src/lib/__tests__/cli-bootstrap-mode.test.ts +22 -0
  88. package/src/lib/__tests__/content-stable-writes.test.ts +74 -0
  89. package/src/lib/__tests__/dev-supervisor-manifest.test.ts +131 -0
  90. package/src/lib/__tests__/queue-worker-supervisor.test.ts +33 -0
  91. package/src/lib/__tests__/resolver.enterprise.test.ts +50 -0
  92. package/src/lib/cli-bootstrap-mode.ts +31 -0
  93. package/src/lib/dev-supervisor-manifest.ts +142 -0
  94. package/src/lib/generators/__tests__/entity-ids.test.ts +32 -0
  95. package/src/lib/generators/__tests__/module-subset.test.ts +248 -0
  96. package/src/lib/generators/__tests__/output-snapshots.test.ts +3 -3
  97. package/src/lib/generators/__tests__/route-manifest-shards.test.ts +62 -0
  98. package/src/lib/generators/entity-ids.ts +10 -26
  99. package/src/lib/generators/module-di.ts +9 -25
  100. package/src/lib/generators/module-entities.ts +9 -25
  101. package/src/lib/generators/module-registry.ts +432 -29
  102. package/src/lib/generators/route-manifest-shards.ts +146 -0
  103. package/src/lib/queue-worker-supervisor.ts +7 -2
  104. package/src/lib/resolver.ts +31 -3
  105. package/src/lib/utils.ts +24 -16
  106. package/src/mercato.ts +15 -6
@@ -0,0 +1,142 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import type { ModuleWorker } from '@open-mercato/shared/modules/registry'
4
+
5
+ export const DEV_SUPERVISOR_MANIFEST_FILE = 'dev-supervisor.generated.json'
6
+ export const DEV_SUPERVISOR_MANIFEST_VERSION = 1
7
+
8
+ export type DevSupervisorWorkerDescriptor = Pick<
9
+ ModuleWorker,
10
+ 'id' | 'moduleId' | 'queue' | 'concurrency'
11
+ >
12
+
13
+ export type DevSupervisorSchedulerStartStatus =
14
+ | 'ok'
15
+ | 'missing-module'
16
+ | 'missing-cli'
17
+ | 'missing-command'
18
+
19
+ export type DevSupervisorManifest = {
20
+ version: typeof DEV_SUPERVISOR_MANIFEST_VERSION
21
+ workers: DevSupervisorWorkerDescriptor[]
22
+ schedulerStartStatus: DevSupervisorSchedulerStartStatus
23
+ requiresFullBootstrap: boolean
24
+ }
25
+
26
+ let registeredManifest: DevSupervisorManifest | null = null
27
+
28
+ function manifestError(filePath: string, detail: string): Error {
29
+ return new Error(
30
+ `Invalid generated dev supervisor manifest at ${filePath}: ${detail}. ` +
31
+ 'Run `yarn generate` and retry.',
32
+ )
33
+ }
34
+
35
+ function isNonEmptyString(value: unknown): value is string {
36
+ return typeof value === 'string' && value.trim().length > 0
37
+ }
38
+
39
+ function parseWorkerDescriptor(
40
+ value: unknown,
41
+ index: number,
42
+ filePath: string,
43
+ ): DevSupervisorWorkerDescriptor {
44
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
45
+ throw manifestError(filePath, `workers[${index}] must be an object`)
46
+ }
47
+
48
+ const worker = value as Record<string, unknown>
49
+ if (!isNonEmptyString(worker.id)) {
50
+ throw manifestError(filePath, `workers[${index}].id must be a non-empty string`)
51
+ }
52
+ if (!isNonEmptyString(worker.queue)) {
53
+ throw manifestError(filePath, `workers[${index}].queue must be a non-empty string`)
54
+ }
55
+ if (typeof worker.concurrency !== 'number' || !Number.isFinite(worker.concurrency)) {
56
+ throw manifestError(filePath, `workers[${index}].concurrency must be a finite number`)
57
+ }
58
+ if (worker.moduleId !== undefined && !isNonEmptyString(worker.moduleId)) {
59
+ throw manifestError(filePath, `workers[${index}].moduleId must be a non-empty string when present`)
60
+ }
61
+
62
+ return {
63
+ id: worker.id,
64
+ ...(worker.moduleId === undefined ? {} : { moduleId: worker.moduleId }),
65
+ queue: worker.queue,
66
+ concurrency: worker.concurrency,
67
+ }
68
+ }
69
+
70
+ function parseManifest(value: unknown, filePath: string): DevSupervisorManifest {
71
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
72
+ throw manifestError(filePath, 'root value must be an object')
73
+ }
74
+
75
+ const manifest = value as Record<string, unknown>
76
+ if (manifest.version !== DEV_SUPERVISOR_MANIFEST_VERSION) {
77
+ throw manifestError(
78
+ filePath,
79
+ `unsupported version ${String(manifest.version)} (expected ${DEV_SUPERVISOR_MANIFEST_VERSION})`,
80
+ )
81
+ }
82
+ if (!Array.isArray(manifest.workers)) {
83
+ throw manifestError(filePath, 'workers must be an array')
84
+ }
85
+
86
+ const schedulerStartStatus = manifest.schedulerStartStatus
87
+ if (
88
+ schedulerStartStatus !== 'ok' &&
89
+ schedulerStartStatus !== 'missing-module' &&
90
+ schedulerStartStatus !== 'missing-cli' &&
91
+ schedulerStartStatus !== 'missing-command'
92
+ ) {
93
+ throw manifestError(filePath, 'schedulerStartStatus is invalid')
94
+ }
95
+
96
+ return {
97
+ version: DEV_SUPERVISOR_MANIFEST_VERSION,
98
+ workers: manifest.workers.map((worker, index) => parseWorkerDescriptor(worker, index, filePath)),
99
+ schedulerStartStatus,
100
+ // Version-1 manifests produced before this safety marker existed cannot
101
+ // prove that worker/CLI overrides were accounted for. Preserve the old
102
+ // full-bootstrap behavior until `yarn generate` writes an explicit false.
103
+ requiresFullBootstrap: manifest.requiresFullBootstrap !== false,
104
+ }
105
+ }
106
+
107
+ export function canUseLightweightDevSupervisor(manifest: DevSupervisorManifest): boolean {
108
+ return manifest.requiresFullBootstrap === false
109
+ }
110
+
111
+ export function loadDevSupervisorManifest(appDir: string): DevSupervisorManifest {
112
+ const filePath = path.join(appDir, '.mercato', 'generated', DEV_SUPERVISOR_MANIFEST_FILE)
113
+ let source: string
114
+ try {
115
+ source = fs.readFileSync(filePath, 'utf8')
116
+ } catch (error) {
117
+ const detail = error instanceof Error ? error.message : String(error)
118
+ throw manifestError(filePath, `file could not be read (${detail})`)
119
+ }
120
+
121
+ let parsed: unknown
122
+ try {
123
+ parsed = JSON.parse(source)
124
+ } catch (error) {
125
+ const detail = error instanceof Error ? error.message : String(error)
126
+ throw manifestError(filePath, `JSON could not be parsed (${detail})`)
127
+ }
128
+
129
+ return parseManifest(parsed, filePath)
130
+ }
131
+
132
+ export function registerDevSupervisorManifest(manifest: DevSupervisorManifest): void {
133
+ registeredManifest = manifest
134
+ }
135
+
136
+ export function getRegisteredDevSupervisorManifest(): DevSupervisorManifest | null {
137
+ return registeredManifest
138
+ }
139
+
140
+ export function resetDevSupervisorManifestForTests(): void {
141
+ registeredManifest = null
142
+ }
@@ -201,6 +201,38 @@ describe('generateEntityIds', () => {
201
201
  expect(registry).not.toContain("import * as sales_order from './entities/sales_order/index'")
202
202
  })
203
203
 
204
+ it('preserves every generated entity output mtime when the output bytes are unchanged', async () => {
205
+ const moduleEntry: ModuleEntry = { id: 'orders', from: '@open-mercato/core' }
206
+ const entitiesFile = path.join(tmpDir, 'packages', 'core', 'src', 'modules', 'orders', 'data', 'entities.ts')
207
+ fs.mkdirSync(path.dirname(entitiesFile), { recursive: true })
208
+ fs.writeFileSync(entitiesFile, 'export class SalesOrder { id!: string; totalGross!: number }\n')
209
+
210
+ const resolver = createMockResolver(tmpDir, [moduleEntry])
211
+ const firstResult = await generateEntityIds({ resolver, quiet: true })
212
+ const outputDir = resolver.getOutputDir()
213
+ const generatedFiles = [
214
+ path.join(outputDir, 'entities.ids.generated.ts'),
215
+ path.join(outputDir, 'entity-fields-registry.ts'),
216
+ path.join(outputDir, 'entities', 'sales_order', 'index.ts'),
217
+ ]
218
+ const oldTime = new Date(Date.now() - 60_000)
219
+ for (const filePath of generatedFiles) {
220
+ fs.utimesSync(filePath, oldTime, oldTime)
221
+ }
222
+ const mtimesBefore = new Map(
223
+ generatedFiles.map((filePath) => [filePath, fs.statSync(filePath).mtimeMs]),
224
+ )
225
+
226
+ const secondResult = await generateEntityIds({ resolver, quiet: true })
227
+
228
+ expect(firstResult.filesWritten).toEqual(expect.arrayContaining(generatedFiles))
229
+ expect(secondResult.filesWritten).toEqual([])
230
+ expect(secondResult.filesUnchanged).toEqual(expect.arrayContaining(generatedFiles))
231
+ for (const filePath of generatedFiles) {
232
+ expect(fs.statSync(filePath).mtimeMs).toBe(mtimesBefore.get(filePath))
233
+ }
234
+ })
235
+
204
236
  it('reuses package generated entity metadata in standalone mode', async () => {
205
237
  const moduleEntry: ModuleEntry = { id: 'directory', from: '@open-mercato/core' }
206
238
  const packageRoot = path.join(tmpDir, 'node_modules', '@open-mercato', 'core')
@@ -578,6 +578,66 @@ export { POST, PUT, DELETE }
578
578
  expect(apiRoutes).toContain('methods: ["GET", "POST", "PUT", "DELETE"]')
579
579
  })
580
580
 
581
+ it('partitions API and backend loader graphs while preserving legacy manifests', async () => {
582
+ scaffoldModule(tmpDir, 'customers', 'pkg', ['api/people/route.ts', 'backend/people/page.tsx'])
583
+ scaffoldModule(tmpDir, 'sales', 'pkg', ['api/orders/route.ts', 'backend/orders/page.tsx'])
584
+ touchFile(
585
+ path.join(tmpDir, 'packages', 'core', 'src', 'modules', 'customers', 'api', 'people', 'route.ts'),
586
+ `export const metadata = { path: '/customers/people' }\nexport function GET() { return null }\n`,
587
+ )
588
+ touchFile(
589
+ path.join(tmpDir, 'packages', 'core', 'src', 'modules', 'customers', 'backend', 'people', 'page.tsx'),
590
+ `export const metadata = { pageTitle: 'People' }\nexport default function Page() { return null }\n`,
591
+ )
592
+ touchFile(
593
+ path.join(tmpDir, 'packages', 'core', 'src', 'modules', 'sales', 'api', 'orders', 'route.ts'),
594
+ `export const metadata = { path: '/sales/orders' }\nexport function POST() { return null }\n`,
595
+ )
596
+ touchFile(
597
+ path.join(tmpDir, 'packages', 'core', 'src', 'modules', 'sales', 'backend', 'orders', 'page.tsx'),
598
+ `export const metadata = { pageTitle: 'Orders' }\nexport default function Page() { return null }\n`,
599
+ )
600
+
601
+ const resolver = createMockResolver(tmpDir, [
602
+ { id: 'customers', from: '@open-mercato/core' },
603
+ { id: 'sales', from: '@open-mercato/core' },
604
+ ])
605
+ await generateModuleRegistry({ resolver, quiet: true })
606
+
607
+ const outputDir = path.join(tmpDir, 'output', 'generated')
608
+ const generatedNames = fs.readdirSync(outputDir)
609
+ const apiShardNames = generatedNames.filter((name) => /^api-route-shard\..+\.generated\.ts$/.test(name))
610
+ const backendShardNames = generatedNames.filter((name) => /^backend-route-shard\..+\.generated\.ts$/.test(name))
611
+ const apiShards = apiShardNames.map((name) => fs.readFileSync(path.join(outputDir, name), 'utf8'))
612
+ const backendShards = backendShardNames.map((name) => fs.readFileSync(path.join(outputDir, name), 'utf8'))
613
+
614
+ expect(apiShardNames).toHaveLength(2)
615
+ expect(backendShardNames).toHaveLength(2)
616
+ expect(apiShards.filter((source) => source.includes('/customers/people'))).toHaveLength(1)
617
+ expect(apiShards.filter((source) => source.includes('/sales/orders'))).toHaveLength(1)
618
+ expect(backendShards.filter((source) => source.includes('/backend/people'))).toHaveLength(1)
619
+ expect(backendShards.filter((source) => source.includes('/backend/orders'))).toHaveLength(1)
620
+
621
+ const apiLoader = readGenerated(tmpDir, 'api-route-shards.generated.ts')!
622
+ const backendLoader = readGenerated(tmpDir, 'backend-route-shards.generated.ts')!
623
+ expect(apiLoader).not.toContain('@open-mercato/core/modules/customers/api')
624
+ expect(backendLoader).not.toContain('@open-mercato/core/modules/customers/backend')
625
+
626
+ const apiMetadata = readGenerated(tmpDir, 'api-route-metadata.generated.ts')!
627
+ const backendMetadata = readGenerated(tmpDir, 'backend-route-metadata.generated.ts')!
628
+ expect(apiMetadata).not.toContain('load: async')
629
+ expect(backendMetadata).not.toContain('load: async')
630
+ expect(readGenerated(tmpDir, 'api-routes.generated.ts')).toContain('/customers/people')
631
+ expect(readGenerated(tmpDir, 'backend-routes.generated.ts')).toContain('/backend/orders')
632
+
633
+ const customersOnlyResolver = createMockResolver(tmpDir, [
634
+ { id: 'customers', from: '@open-mercato/core' },
635
+ ])
636
+ await generateModuleRegistry({ resolver: customersOnlyResolver, quiet: true })
637
+ const remainingApiShards = fs.readdirSync(outputDir).filter((name) => /^api-route-shard\..+\.generated\.ts$/.test(name))
638
+ expect(remainingApiShards).toHaveLength(1)
639
+ })
640
+
581
641
  it('keeps all HTTP methods for route.ts files that destructure-export crud handlers', async () => {
582
642
  scaffoldModule(tmpDir, 'example', 'pkg', ['api/todos/route.ts'])
583
643
  touchFile(
@@ -1062,6 +1122,85 @@ export default async function handle(): Promise<void> {}
1062
1122
  expect(output).toContain("workers:")
1063
1123
  })
1064
1124
 
1125
+ it('writes a content-stable primitive dev supervisor manifest', async () => {
1126
+ scaffoldModule(tmpDir, 'events', 'pkg', ['workers/dispatch.ts'])
1127
+ scaffoldModule(tmpDir, 'scheduler', 'pkg', ['cli.ts'])
1128
+ touchFile(
1129
+ path.join(tmpDir, 'packages', 'core', 'src', 'modules', 'events', 'workers', 'dispatch.ts'),
1130
+ `export const metadata = { id: 'events:dispatch', queue: 'events', concurrency: 3 }\nexport default async function dispatch() {}\n`,
1131
+ )
1132
+ touchFile(
1133
+ path.join(tmpDir, 'packages', 'core', 'src', 'modules', 'scheduler', 'cli.ts'),
1134
+ `export default [{ command: 'start', run() {} }]\n`,
1135
+ )
1136
+ const resolver = createMockResolver(tmpDir, [
1137
+ { id: 'events', from: '@open-mercato/core' },
1138
+ { id: 'scheduler', from: '@open-mercato/core' },
1139
+ ])
1140
+
1141
+ const first = await generateModuleRegistryCli({ resolver, quiet: true })
1142
+ const manifestPath = path.join(tmpDir, 'output', 'generated', 'dev-supervisor.generated.json')
1143
+ expect(JSON.parse(fs.readFileSync(manifestPath, 'utf8'))).toEqual({
1144
+ version: 1,
1145
+ workers: [{
1146
+ id: 'events:dispatch',
1147
+ moduleId: 'events',
1148
+ queue: 'events',
1149
+ concurrency: 3,
1150
+ }],
1151
+ schedulerStartStatus: 'ok',
1152
+ requiresFullBootstrap: false,
1153
+ })
1154
+ expect(first.filesWritten).toContain(manifestPath)
1155
+
1156
+ const second = await generateModuleRegistryCli({ resolver, quiet: true })
1157
+ expect(second.filesUnchanged).toContain(manifestPath)
1158
+ expect(second.filesWritten).not.toContain(manifestPath)
1159
+ })
1160
+
1161
+ it('falls back to full bootstrap for worker overrides and computed scheduler commands', async () => {
1162
+ scaffoldModule(tmpDir, 'events', 'pkg', ['workers/dispatch.ts'])
1163
+ scaffoldModule(tmpDir, 'scheduler', 'pkg', ['cli.ts'])
1164
+ touchFile(
1165
+ path.join(tmpDir, 'packages', 'core', 'src', 'modules', 'events', 'workers', 'dispatch.ts'),
1166
+ `export const metadata = { id: 'events:dispatch', queue: 'events', concurrency: 3 }\nexport default async function dispatch() {}\n`,
1167
+ )
1168
+ touchFile(
1169
+ path.join(tmpDir, 'packages', 'core', 'src', 'modules', 'scheduler', 'cli.ts'),
1170
+ `const command = 'start'\nexport default [{ command, run() {} }]\n`,
1171
+ )
1172
+ const resolver = createMockResolver(tmpDir, [
1173
+ { id: 'events', from: '@open-mercato/core', devSupervisorRequiresFullBootstrap: true },
1174
+ { id: 'scheduler', from: '@open-mercato/core' },
1175
+ ])
1176
+
1177
+ await generateModuleRegistryCli({ resolver, quiet: true })
1178
+ const manifestPath = path.join(tmpDir, 'output', 'generated', 'dev-supervisor.generated.json')
1179
+ expect(JSON.parse(fs.readFileSync(manifestPath, 'utf8'))).toMatchObject({
1180
+ schedulerStartStatus: 'missing-command',
1181
+ requiresFullBootstrap: true,
1182
+ })
1183
+ })
1184
+
1185
+ it('falls back to full bootstrap when app code registers programmatic worker overrides', async () => {
1186
+ scaffoldModule(tmpDir, 'events', 'pkg', ['workers/dispatch.ts'])
1187
+ touchFile(
1188
+ path.join(tmpDir, 'packages', 'core', 'src', 'modules', 'events', 'workers', 'dispatch.ts'),
1189
+ `export const metadata = { id: 'events:dispatch', queue: 'events', concurrency: 3 }\nexport default async function dispatch() {}\n`,
1190
+ )
1191
+ touchFile(
1192
+ path.join(tmpDir, 'app', 'src', 'bootstrap-overrides.ts'),
1193
+ `import { applyWorkerOverrides as overrideWorkers } from '@open-mercato/shared/modules/overrides'\noverrideWorkers({ 'events:dispatch': null })\n`,
1194
+ )
1195
+ const resolver = createMockResolver(tmpDir, [
1196
+ { id: 'events', from: '@open-mercato/core' },
1197
+ ])
1198
+
1199
+ await generateModuleRegistryCli({ resolver, quiet: true })
1200
+ const manifestPath = path.join(tmpDir, 'output', 'generated', 'dev-supervisor.generated.json')
1201
+ expect(JSON.parse(fs.readFileSync(manifestPath, 'utf8')).requiresFullBootstrap).toBe(true)
1202
+ })
1203
+
1065
1204
  it('handles disabling a module that was previously enabled', async () => {
1066
1205
  scaffoldModule(tmpDir, 'keep_mod', 'pkg', [
1067
1206
  'subscribers/handler.ts',
@@ -1480,6 +1619,115 @@ describe('generateModuleRegistryApp with module subsets', () => {
1480
1619
  const bootstrapOutput = readGenerated(tmpDir, 'modules.bootstrap.generated.ts')
1481
1620
  expect(bootstrapOutput).not.toBeNull()
1482
1621
  expect(bootstrapOutput).toContain('export const modules: Module[] = [')
1622
+
1623
+ const i18nLoadersOutput = readGenerated(tmpDir, 'modules.i18n.loaders.generated.ts')
1624
+ expect(i18nLoadersOutput).toContain('export async function loadI18nModules')
1625
+ expect(i18nLoadersOutput).toContain('default:')
1626
+ expect(i18nLoadersOutput).toContain('return []')
1627
+ })
1628
+
1629
+ it('keeps the legacy i18n registry while emitting deterministic locale-specific shards', async () => {
1630
+ touchFile(
1631
+ path.join(tmpDir, 'packages', 'core', 'src', 'modules', 'localized', 'i18n', 'en.json'),
1632
+ JSON.stringify({ localized: { title: 'Core title' } }),
1633
+ )
1634
+ touchFile(
1635
+ path.join(tmpDir, 'packages', 'core', 'src', 'modules', 'localized', 'i18n', 'pl.json'),
1636
+ JSON.stringify({ localized: { title: 'Tytuł' } }),
1637
+ )
1638
+ touchFile(
1639
+ path.join(tmpDir, 'app', 'src', 'modules', 'localized', 'i18n', 'en.json'),
1640
+ JSON.stringify({ localized: { title: 'App title' } }),
1641
+ )
1642
+ touchFile(
1643
+ path.join(tmpDir, 'packages', 'core', 'src', 'modules', 'localized', 'i18n', '.hardcoded-allowlist.json'),
1644
+ JSON.stringify({ allow: ['framework chrome'] }),
1645
+ )
1646
+
1647
+ const resolver = createMockResolver(tmpDir, [
1648
+ { id: 'localized', from: '@open-mercato/core' },
1649
+ ])
1650
+ const result = await generateModuleRegistryApp({ resolver, quiet: true })
1651
+
1652
+ expect(result.errors).toEqual([])
1653
+
1654
+ const legacyI18nOutput = readGenerated(tmpDir, 'modules.i18n.generated.ts')!
1655
+ expect(legacyI18nOutput).toContain('@open-mercato/core/modules/localized/i18n/en.json')
1656
+ expect(legacyI18nOutput).toContain('@/modules/localized/i18n/en.json')
1657
+ expect(legacyI18nOutput).toContain('@open-mercato/core/modules/localized/i18n/pl.json')
1658
+ expect(legacyI18nOutput).toContain('export const modules: Module[] = [')
1659
+
1660
+ const bootstrapOutput = readGenerated(tmpDir, 'modules.bootstrap.generated.ts')!
1661
+ expect(bootstrapOutput).not.toContain('/i18n/')
1662
+ expect(bootstrapOutput).not.toContain('translations:')
1663
+
1664
+ const loadersOutput = readGenerated(tmpDir, 'modules.i18n.loaders.generated.ts')!
1665
+ expect(loadersOutput).not.toContain('.json')
1666
+ expect(loadersOutput).toContain('case "en":')
1667
+ expect(loadersOutput).toContain('import("./modules.i18n.en.generated")')
1668
+ expect(loadersOutput).toContain('case "pl":')
1669
+ expect(loadersOutput).not.toContain('hardcoded-allowlist')
1670
+ expect(loadersOutput.indexOf('case "en":')).toBeLessThan(loadersOutput.indexOf('case "pl":'))
1671
+
1672
+ const enShard = readGenerated(tmpDir, 'modules.i18n.en.generated.ts')!
1673
+ expect(enShard).toContain('@open-mercato/core/modules/localized/i18n/en.json')
1674
+ expect(enShard).toContain('@/modules/localized/i18n/en.json')
1675
+ expect(enShard).not.toContain('/i18n/pl.json')
1676
+ expect(enShard).toContain('id: "localized"')
1677
+ expect(enShard).toContain('translations:')
1678
+
1679
+ const plShard = readGenerated(tmpDir, 'modules.i18n.pl.generated.ts')!
1680
+ expect(plShard).toContain('@open-mercato/core/modules/localized/i18n/pl.json')
1681
+ expect(plShard).not.toContain('/i18n/en.json')
1682
+ expect(plShard).not.toContain('@/modules/localized/i18n/pl.json')
1683
+ expect(readGenerated(tmpDir, 'modules.i18n._hardcoded-allowlist.generated.ts')).toBeNull()
1684
+
1685
+ const secondResult = await generateModuleRegistryApp({ resolver, quiet: true })
1686
+ expect(secondResult.filesWritten).toEqual([])
1687
+
1688
+ fs.rmSync(path.join(
1689
+ tmpDir,
1690
+ 'packages',
1691
+ 'core',
1692
+ 'src',
1693
+ 'modules',
1694
+ 'localized',
1695
+ 'i18n',
1696
+ 'pl.json',
1697
+ ))
1698
+ await generateModuleRegistryApp({ resolver, quiet: true })
1699
+ expect(readGenerated(tmpDir, 'modules.i18n.pl.generated.ts')).toBeNull()
1700
+ expect(fs.existsSync(path.join(
1701
+ tmpDir,
1702
+ 'output',
1703
+ 'generated',
1704
+ 'modules.i18n.pl.generated.checksum',
1705
+ ))).toBe(false)
1706
+ expect(readGenerated(tmpDir, 'modules.i18n.loaders.generated.ts')).not.toContain('case "pl":')
1707
+ })
1708
+
1709
+ it('uses standalone source-mirror translations in locale-specific shards', async () => {
1710
+ touchFile(
1711
+ path.join(tmpDir, 'node_modules', 'pkg', 'src', 'modules', 'customers', 'i18n', 'en.json'),
1712
+ JSON.stringify({ customers: { people: { title: 'People' } } }),
1713
+ )
1714
+ touchFile(
1715
+ path.join(tmpDir, 'node_modules', 'pkg', 'dist', 'modules', 'customers', 'index.js'),
1716
+ 'export const metadata = { id: "customers" }\n',
1717
+ )
1718
+
1719
+ const resolver = createStandaloneMockResolver(tmpDir, [
1720
+ { id: 'customers', from: '@open-mercato/core' },
1721
+ ])
1722
+ const result = await generateModuleRegistryApp({ resolver, quiet: true })
1723
+
1724
+ expect(result.errors).toEqual([])
1725
+ const shard = fs.readFileSync(
1726
+ path.join(tmpDir, '.mercato', 'generated', 'modules.i18n.en.generated.ts'),
1727
+ 'utf8',
1728
+ )
1729
+ expect(shard).toContain('@open-mercato/core/modules/customers/i18n/en.json')
1730
+ expect(shard).toContain('id: "customers"')
1483
1731
  })
1484
1732
 
1485
1733
  it('app output excludes CLI commands and keeps lazy workers/subscribers', async () => {
@@ -829,12 +829,12 @@ describe('generator output compatibility', () => {
829
829
 
830
830
  const content = readGenerated('bootstrap-registrations.generated.ts')
831
831
  expect(content).not.toBeNull()
832
- expect(content).toContain(`import { backendRoutes } from "./backend-routes.generated"`)
832
+ expect(content).toContain(`import { backendRouteFacades } from "./backend-route-shards.generated"`)
833
833
  expect(content).toContain(`import { frontendRoutes } from "./frontend-routes.generated"`)
834
834
  expect(content).toContain(
835
835
  `import { registerBackendRouteManifests, registerFrontendRouteManifests } from '@open-mercato/shared/modules/registry'`,
836
836
  )
837
- expect(content).toMatch(/export function runBootstrapRegistrations\(\): void \{[\s\S]*registerBackendRouteManifests\(backendRoutes\)[\s\S]*\}/)
837
+ expect(content).toMatch(/export function runBootstrapRegistrations\(\): void \{[\s\S]*registerBackendRouteManifests\(backendRouteFacades\)[\s\S]*\}/)
838
838
  expect(content).toMatch(/export function runBootstrapRegistrations\(\): void \{[\s\S]*registerFrontendRouteManifests\(frontendRoutes\)[\s\S]*\}/)
839
839
  })
840
840
 
@@ -846,6 +846,6 @@ describe('generator output compatibility', () => {
846
846
 
847
847
  const content = readGenerated('bootstrap-registrations.generated.ts')
848
848
  expect(content).not.toBeNull()
849
- expect(content).toContain(`registerBackendRouteManifests(backendRoutes)`)
849
+ expect(content).toContain(`registerBackendRouteManifests(backendRouteFacades)`)
850
850
  })
851
851
  })
@@ -0,0 +1,62 @@
1
+ import {
2
+ getRouteManifestShardKey,
3
+ renderRouteManifestShardOutputs,
4
+ renderRouteMetadataOutput,
5
+ } from '../route-manifest-shards'
6
+
7
+ describe('route manifest shards', () => {
8
+ it('groups static request prefixes and preserves dynamic-prefix fallbacks', () => {
9
+ expect(getRouteManifestShardKey('api', '/customers/[id]')).toBe('customers')
10
+ expect(getRouteManifestShardKey('backend', '/backend/sales/orders')).toBe('sales')
11
+ expect(getRouteManifestShardKey('backend', '/backend/[tenant]/settings')).toBe('__dynamic')
12
+ expect(getRouteManifestShardKey('api', '/')).toBe('__root')
13
+ })
14
+
15
+ it('renders a small loader graph with exact and dynamic shards', () => {
16
+ const outputs = renderRouteManifestShardOutputs('api', [
17
+ { path: '/customers', declaration: `{ moduleId: 'customers' } as any` },
18
+ { path: '/sales/orders', declaration: `{ moduleId: 'sales' } as any` },
19
+ { path: '/[module]/health', declaration: `{ moduleId: 'dynamic' } as any` },
20
+ ])
21
+ const loader = outputs.find((entry) => entry.fileName === 'api-route-shards.generated.ts')?.content ?? ''
22
+
23
+ expect(outputs.filter((entry) => entry.fileName.includes('route-shard.'))).toHaveLength(3)
24
+ expect(loader).toContain(`case "customers":`)
25
+ expect(loader).toContain(`case "sales":`)
26
+ expect(loader).toContain(`const dynamicShard = await loadShard("__dynamic")`)
27
+ expect(loader).toContain(`export const apiRouteFacades`)
28
+ expect(loader).toContain(`loadApiRouteManifestByOrdinal(ordinal)`)
29
+ expect(loader).not.toContain("moduleId: 'customers'")
30
+ })
31
+
32
+ it('retains global route order when exact and dynamic entries interleave', () => {
33
+ const outputs = renderRouteManifestShardOutputs('api', [
34
+ { path: '/customers/new', declaration: `{ path: '/customers/new' } as any` },
35
+ { path: '/[module]/health', declaration: `{ path: '/[module]/health' } as any` },
36
+ { path: '/customers/[id]', declaration: `{ path: '/customers/[id]' } as any` },
37
+ { path: '/', declaration: `{ path: '/' } as any` },
38
+ ])
39
+ const customerShard = outputs.find((entry) => entry.content.includes(`/customers/new`))?.content ?? ''
40
+ const dynamicShard = outputs.find((entry) => entry.content.includes(`/[module]/health`))?.content ?? ''
41
+ const loader = outputs.find((entry) => entry.fileName === 'api-route-shards.generated.ts')?.content ?? ''
42
+
43
+ expect(customerShard).toContain('export const routeOrders = [0, 2]')
44
+ expect(dynamicShard).toContain('export const routeOrders = [1]')
45
+ expect(loader).toContain(`case "__root":`)
46
+ expect(loader).toContain(`.sort((a, b) => a.order - b.order)`)
47
+ expect(loader).toContain(`const ROUTE_SHARD_KEYS = ["customers","__dynamic","customers","__root"]`)
48
+ })
49
+
50
+ it('renders metadata without load functions while retaining metadata imports', () => {
51
+ const output = renderRouteMetadataOutput('backend', [{
52
+ path: '/backend/settings',
53
+ declaration: `{ moduleId: 'settings', ...resolvePageRouteMetadata('/backend/settings', META.metadata) }`,
54
+ imports: [`import * as META from '@app/settings/meta'`],
55
+ }])
56
+
57
+ expect(output.fileName).toBe('backend-route-metadata.generated.ts')
58
+ expect(output.content).toContain(`import * as META from '@app/settings/meta'`)
59
+ expect(output.content).toContain(`Omit<BackendRouteManifestEntry, 'load'>`)
60
+ expect(output.content).not.toContain('load: async')
61
+ })
62
+ })
@@ -5,17 +5,13 @@ import { VariableDeclarationKind } from 'ts-morph'
5
5
  import type { PackageResolver, ModuleEntry } from '../resolver'
6
6
  import { MODULE_CODE_EXTENSIONS } from './scanner'
7
7
  import {
8
- calculateChecksum,
9
- readChecksumRecord,
10
- writeChecksumRecord,
11
- ensureDir,
12
8
  toVar,
13
9
  toSnake,
14
10
  rimrafDir,
15
- logGenerationResult,
16
- writeIfChanged,
17
11
  type GeneratorResult,
18
12
  createGeneratorResult,
13
+ writeGeneratedFile,
14
+ writeIfChanged,
19
15
  } from '../utils'
20
16
  import { addAutoGeneratedComment, createGeneratedSourceFile, elementAccess, getSourceText, identifier, returnStatement } from './ast'
21
17
 
@@ -438,26 +434,14 @@ export async function generateEntityIds(options: EntityIdsOptions): Promise<Gene
438
434
  })
439
435
  const consolidatedSrc = getSourceText(consolidatedSourceFile)
440
436
 
441
- // Check if content has changed
442
- const newChecksum = calculateChecksum(consolidatedSrc)
443
- let shouldWrite = true
444
-
445
- const existingRecord = readChecksumRecord(checksumFile)
446
- if (existingRecord && existingRecord.content === newChecksum) {
447
- shouldWrite = false
448
- }
449
-
450
- if (shouldWrite) {
451
- ensureDir(outFile)
452
- fs.writeFileSync(outFile, consolidatedSrc)
453
- writeChecksumRecord(checksumFile, { content: newChecksum, structure: '' })
454
- result.filesWritten.push(outFile)
455
- if (!quiet) {
456
- logGenerationResult(path.relative(process.cwd(), outFile), true)
457
- }
458
- } else {
459
- result.filesUnchanged.push(outFile)
460
- }
437
+ writeGeneratedFile({
438
+ outFile,
439
+ checksumFile,
440
+ content: consolidatedSrc,
441
+ structureChecksum: '',
442
+ result,
443
+ quiet,
444
+ })
461
445
 
462
446
  // Write per-group outputs
463
447
  const groups = Object.keys(grouped) as GroupKey[]
@@ -4,14 +4,10 @@ import { StructureKind, VariableDeclarationKind } from 'ts-morph'
4
4
  import type { PackageResolver } from '../resolver'
5
5
  import { MODULE_CODE_EXTENSIONS } from './scanner'
6
6
  import {
7
- calculateChecksum,
8
- readChecksumRecord,
9
- writeChecksumRecord,
10
- ensureDir,
11
7
  toVar,
12
- logGenerationResult,
13
8
  type GeneratorResult,
14
9
  createGeneratorResult,
10
+ writeGeneratedFile,
15
11
  } from '../utils'
16
12
  import {
17
13
  addAutoGeneratedComment,
@@ -96,26 +92,14 @@ export async function generateModuleDi(options: ModuleDiOptions): Promise<Genera
96
92
  })
97
93
  const output = getSourceText(sourceFile)
98
94
 
99
- // Check if content has changed
100
- const newChecksum = calculateChecksum(output)
101
- let shouldWrite = true
102
-
103
- const existingRecord = readChecksumRecord(checksumFile)
104
- if (existingRecord && existingRecord.content === newChecksum) {
105
- shouldWrite = false
106
- }
107
-
108
- if (shouldWrite) {
109
- ensureDir(outFile)
110
- fs.writeFileSync(outFile, output)
111
- writeChecksumRecord(checksumFile, { content: newChecksum, structure: '' })
112
- result.filesWritten.push(outFile)
113
- if (!quiet) {
114
- logGenerationResult(path.relative(process.cwd(), outFile), true)
115
- }
116
- } else {
117
- result.filesUnchanged.push(outFile)
118
- }
95
+ writeGeneratedFile({
96
+ outFile,
97
+ checksumFile,
98
+ content: output,
99
+ structureChecksum: '',
100
+ result,
101
+ quiet,
102
+ })
119
103
 
120
104
  return result
121
105
  }