@open-mercato/cli 0.6.7-develop.6569.1.c06dee4866 → 0.6.7-develop.6572.1.3d8e83062f

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 (71) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/dist/agentic/guides/module-facts.json +53 -53
  3. package/dist/agentic/guides/modules/ai_assistant.md +1 -1
  4. package/dist/agentic/guides/modules/api_docs.md +1 -1
  5. package/dist/agentic/guides/modules/api_keys.md +1 -1
  6. package/dist/agentic/guides/modules/attachments.md +1 -1
  7. package/dist/agentic/guides/modules/audit_logs.md +1 -1
  8. package/dist/agentic/guides/modules/auth.md +1 -1
  9. package/dist/agentic/guides/modules/business_rules.md +1 -1
  10. package/dist/agentic/guides/modules/catalog.md +1 -1
  11. package/dist/agentic/guides/modules/channel_gmail.md +1 -1
  12. package/dist/agentic/guides/modules/channel_imap.md +1 -1
  13. package/dist/agentic/guides/modules/checkout.md +1 -1
  14. package/dist/agentic/guides/modules/communication_channels.md +1 -1
  15. package/dist/agentic/guides/modules/configs.md +1 -1
  16. package/dist/agentic/guides/modules/content.md +1 -1
  17. package/dist/agentic/guides/modules/currencies.md +1 -1
  18. package/dist/agentic/guides/modules/customer_accounts.md +1 -1
  19. package/dist/agentic/guides/modules/customers.md +1 -1
  20. package/dist/agentic/guides/modules/dashboards.md +1 -1
  21. package/dist/agentic/guides/modules/data_sync.md +1 -1
  22. package/dist/agentic/guides/modules/dictionaries.md +1 -1
  23. package/dist/agentic/guides/modules/directory.md +1 -1
  24. package/dist/agentic/guides/modules/entities.md +1 -1
  25. package/dist/agentic/guides/modules/events.md +1 -1
  26. package/dist/agentic/guides/modules/feature_toggles.md +1 -1
  27. package/dist/agentic/guides/modules/gateway_stripe.md +1 -1
  28. package/dist/agentic/guides/modules/generators.md +1 -1
  29. package/dist/agentic/guides/modules/inbox_ops.md +1 -1
  30. package/dist/agentic/guides/modules/integrations.md +1 -1
  31. package/dist/agentic/guides/modules/messages.md +1 -1
  32. package/dist/agentic/guides/modules/notifications.md +1 -1
  33. package/dist/agentic/guides/modules/onboarding.md +1 -1
  34. package/dist/agentic/guides/modules/payment_gateways.md +1 -1
  35. package/dist/agentic/guides/modules/perspectives.md +1 -1
  36. package/dist/agentic/guides/modules/planner.md +1 -1
  37. package/dist/agentic/guides/modules/portal.md +1 -1
  38. package/dist/agentic/guides/modules/progress.md +1 -1
  39. package/dist/agentic/guides/modules/query_index.md +1 -1
  40. package/dist/agentic/guides/modules/record_locks.md +1 -1
  41. package/dist/agentic/guides/modules/resources.md +1 -1
  42. package/dist/agentic/guides/modules/sales.md +1 -1
  43. package/dist/agentic/guides/modules/scheduler.md +1 -1
  44. package/dist/agentic/guides/modules/search.md +1 -1
  45. package/dist/agentic/guides/modules/security.md +1 -1
  46. package/dist/agentic/guides/modules/shipping_carriers.md +1 -1
  47. package/dist/agentic/guides/modules/sso.md +1 -1
  48. package/dist/agentic/guides/modules/staff.md +1 -1
  49. package/dist/agentic/guides/modules/storage_s3.md +1 -1
  50. package/dist/agentic/guides/modules/sync_akeneo.md +1 -1
  51. package/dist/agentic/guides/modules/sync_excel.md +1 -1
  52. package/dist/agentic/guides/modules/system_status_overlays.md +1 -1
  53. package/dist/agentic/guides/modules/translations.md +1 -1
  54. package/dist/agentic/guides/modules/webhooks.md +1 -1
  55. package/dist/agentic/guides/modules/workflows.md +1 -1
  56. package/dist/lib/generators/index.js +7 -1
  57. package/dist/lib/generators/index.js.map +2 -2
  58. package/dist/lib/generators/module-registry.js +224 -266
  59. package/dist/lib/generators/module-registry.js.map +3 -3
  60. package/dist/lib/module-install.js +2 -6
  61. package/dist/lib/module-install.js.map +2 -2
  62. package/dist/mercato.js +6 -14
  63. package/dist/mercato.js.map +2 -2
  64. package/package.json +5 -5
  65. package/src/__tests__/mercato.test.ts +13 -27
  66. package/src/lib/generators/__tests__/generators.test.ts +5 -0
  67. package/src/lib/generators/__tests__/output-snapshots.test.ts +37 -1
  68. package/src/lib/generators/index.ts +7 -1
  69. package/src/lib/generators/module-registry.ts +291 -293
  70. package/src/lib/module-install.ts +2 -6
  71. package/src/mercato.ts +6 -14
@@ -3,7 +3,7 @@ import path from 'node:path'
3
3
  import { pathToFileURL } from 'node:url'
4
4
  import { VariableDeclarationKind, type WriterFunction } from 'ts-morph'
5
5
  import ts from 'typescript-js'
6
- import type { PackageResolver } from '../resolver'
6
+ import type { ModuleEntry, PackageResolver } from '../resolver'
7
7
  import {
8
8
  calculateStructureChecksum,
9
9
  toVar,
@@ -59,6 +59,8 @@ import {
59
59
  MODULE_CODE_EXTENSIONS,
60
60
  type ModuleRoots,
61
61
  type ModuleImports,
62
+ type ScannedFile,
63
+ type ResolvedFile,
62
64
  stripModuleCodeExtension,
63
65
  isModulePageFile,
64
66
  resolveStandaloneSourceMirrorBase,
@@ -73,6 +75,45 @@ export interface ModuleRegistryOptions {
73
75
  quiet?: boolean
74
76
  }
75
77
 
78
+ interface ModuleRegistryDiscovery {
79
+ enabled: ModuleEntry[]
80
+ modules: ModuleRegistryDiscoveryEntry[]
81
+ getStructureChecksum: () => string
82
+ }
83
+
84
+ type DiscoveredSubscriber = {
85
+ id: string
86
+ importPath: string
87
+ metadata: SerializableSubscriberMetadata | null
88
+ }
89
+
90
+ type DiscoveredWorker = {
91
+ id: string
92
+ importPath: string
93
+ metadata: SerializableWorkerMetadata
94
+ }
95
+
96
+ type DiscoveredTranslation = {
97
+ locale: string
98
+ coreHas: boolean
99
+ appHas: boolean
100
+ }
101
+
102
+ type ModuleRegistryDiscoveryEntry = {
103
+ modId: string
104
+ roots: ModuleRoots
105
+ imps: ModuleImports
106
+ appImportBase: string
107
+ resolve: (relativePath: string) => ResolvedFile | null
108
+ frontendFiles: ScannedFile[]
109
+ backendFiles: ScannedFile[]
110
+ commandLoaderEntries: CommandLoaderGenerationEntry[]
111
+ getSubscribers: () => Promise<DiscoveredSubscriber[]>
112
+ getWorkers: () => Promise<DiscoveredWorker[]>
113
+ translations: DiscoveredTranslation[]
114
+ dashboardWidgetEntries: DashboardWidgetEntry[]
115
+ }
116
+
76
117
  type DashboardWidgetEntry = {
77
118
  moduleId: string
78
119
  key: string
@@ -1504,6 +1545,135 @@ async function loadWorkerMetadata(sourceFile: string): Promise<SerializableWorke
1504
1545
  ?? normalizeWorkerMetadata(extractNamedObjectLiteralExport(sourceFile, 'metadata'))
1505
1546
  }
1506
1547
 
1548
+ async function discoverSubscribers(
1549
+ roots: ModuleRoots,
1550
+ imps: ModuleImports,
1551
+ modId: string,
1552
+ ): Promise<DiscoveredSubscriber[]> {
1553
+ const subscribers: DiscoveredSubscriber[] = []
1554
+ for (const { relPath } of scanModuleDir(roots, SCAN_CONFIGS.subscribers)) {
1555
+ const resolved = resolveModuleFile(
1556
+ roots,
1557
+ imps,
1558
+ path.posix.join('subscribers', relPath.replace(/\\/g, '/')),
1559
+ )
1560
+ if (!resolved) continue
1561
+ const segs = relPath.split('/')
1562
+ const file = segs.pop()!
1563
+ const name = stripModuleCodeExtension(file)
1564
+ subscribers.push({
1565
+ id: [modId, ...segs, name].filter(Boolean).join(':'),
1566
+ importPath: sanitizeGeneratedModuleSpecifier(resolved.importPath),
1567
+ metadata: await loadSubscriberMetadata(resolved.absolutePath),
1568
+ })
1569
+ }
1570
+ return subscribers
1571
+ }
1572
+
1573
+ async function discoverWorkers(
1574
+ roots: ModuleRoots,
1575
+ imps: ModuleImports,
1576
+ modId: string,
1577
+ ): Promise<DiscoveredWorker[]> {
1578
+ const workers: DiscoveredWorker[] = []
1579
+ for (const { relPath } of scanModuleDir(roots, SCAN_CONFIGS.workers)) {
1580
+ const resolved = resolveModuleFile(
1581
+ roots,
1582
+ imps,
1583
+ path.posix.join('workers', relPath.replace(/\\/g, '/')),
1584
+ )
1585
+ if (!resolved) continue
1586
+ const metadata = await loadWorkerMetadata(resolved.absolutePath)
1587
+ if (!metadata?.queue) continue
1588
+ const segs = relPath.split('/')
1589
+ const file = segs.pop()!
1590
+ const name = stripModuleCodeExtension(file)
1591
+ workers.push({
1592
+ id: [modId, 'workers', ...segs, name].filter(Boolean).join(':'),
1593
+ importPath: sanitizeGeneratedModuleSpecifier(resolved.importPath),
1594
+ metadata,
1595
+ })
1596
+ }
1597
+ return workers
1598
+ }
1599
+
1600
+ function discoverTranslations(roots: ModuleRoots): DiscoveredTranslation[] {
1601
+ const i18nApp = path.join(roots.appBase, 'i18n')
1602
+ const pkgI18nBase = resolveStandaloneSourceMirrorBase(roots.pkgBase) ?? roots.pkgBase
1603
+ const i18nCore = path.join(pkgI18nBase, 'i18n')
1604
+ const locales = new Set<string>()
1605
+ if (fs.existsSync(i18nCore)) {
1606
+ for (const entry of fs.readdirSync(i18nCore, { withFileTypes: true })) {
1607
+ if (entry.isFile() && entry.name.endsWith('.json')) locales.add(entry.name.replace(/\.json$/, ''))
1608
+ }
1609
+ }
1610
+ if (fs.existsSync(i18nApp)) {
1611
+ for (const entry of fs.readdirSync(i18nApp, { withFileTypes: true })) {
1612
+ if (entry.isFile() && entry.name.endsWith('.json')) locales.add(entry.name.replace(/\.json$/, ''))
1613
+ }
1614
+ }
1615
+ return Array.from(locales, (locale) => ({
1616
+ locale,
1617
+ coreHas: fs.existsSync(path.join(i18nCore, `${locale}.json`)),
1618
+ appHas: fs.existsSync(path.join(i18nApp, `${locale}.json`)),
1619
+ }))
1620
+ }
1621
+
1622
+ async function createModuleRegistryDiscovery(
1623
+ resolver: PackageResolver,
1624
+ ): Promise<ModuleRegistryDiscovery> {
1625
+ const enabled = resolver.loadEnabledModules()
1626
+ const modules: ModuleRegistryDiscoveryEntry[] = []
1627
+ const trackedRoots = new Set<string>()
1628
+
1629
+ for (const entry of enabled) {
1630
+ const modId = entry.id
1631
+ const roots = resolver.getModulePaths(entry)
1632
+ verifyThirdPartyModuleShape(entry, roots.pkgBase)
1633
+ const rawImps = resolver.getModuleImportBase(entry)
1634
+ const appImportBase = entry.from === '@app' ? `../../src/modules/${modId}` : rawImps.appBase
1635
+ const imps: ModuleImports = { appBase: appImportBase, pkgBase: rawImps.pkgBase }
1636
+ trackedRoots.add(roots.appBase)
1637
+ trackedRoots.add(roots.pkgBase)
1638
+
1639
+ let subscribers: Promise<DiscoveredSubscriber[]> | undefined
1640
+ let workers: Promise<DiscoveredWorker[]> | undefined
1641
+ const resolvedFiles = new Map<string, ResolvedFile | null>()
1642
+ const resolve = (relativePath: string): ResolvedFile | null => {
1643
+ if (!resolvedFiles.has(relativePath)) {
1644
+ resolvedFiles.set(relativePath, resolveModuleFile(roots, imps, relativePath))
1645
+ }
1646
+ return resolvedFiles.get(relativePath) ?? null
1647
+ }
1648
+ modules.push({
1649
+ modId,
1650
+ roots,
1651
+ imps,
1652
+ appImportBase,
1653
+ resolve,
1654
+ frontendFiles: scanModuleDir(roots, SCAN_CONFIGS.frontendPages),
1655
+ backendFiles: scanModuleDir(roots, SCAN_CONFIGS.backendPages),
1656
+ commandLoaderEntries: collectCommandLoaderEntries(roots, imps, modId),
1657
+ getSubscribers: () => subscribers ??= discoverSubscribers(roots, imps, modId),
1658
+ getWorkers: () => workers ??= discoverWorkers(roots, imps, modId),
1659
+ translations: discoverTranslations(roots),
1660
+ dashboardWidgetEntries: scanDashboardWidgetEntries({
1661
+ modId,
1662
+ roots,
1663
+ appImportBase,
1664
+ pkgImportBase: imps.pkgBase,
1665
+ }),
1666
+ })
1667
+ }
1668
+
1669
+ let structureChecksum: string | undefined
1670
+ return {
1671
+ enabled,
1672
+ modules,
1673
+ getStructureChecksum: () => structureChecksum ??= calculateStructureChecksum(Array.from(trackedRoots)),
1674
+ }
1675
+ }
1676
+
1507
1677
  async function loadPageMetadataForManifest(options: {
1508
1678
  sourceFile: string
1509
1679
  metaPath?: string
@@ -1842,90 +2012,39 @@ async function processApiRoutes(options: {
1842
2012
  }
1843
2013
  }
1844
2014
 
1845
- async function processSubscribers(options: {
1846
- roots: ModuleRoots
1847
- modId: string
1848
- appImportBase: string
1849
- pkgImportBase: string
1850
- }): Promise<string[]> {
1851
- const { roots, modId, appImportBase, pkgImportBase } = options
1852
- const files = scanModuleDir(roots, SCAN_CONFIGS.subscribers)
2015
+ function processSubscribers(discovered: DiscoveredSubscriber[]): string[] {
1853
2016
  const subscribers: string[] = []
1854
- for (const { relPath } of files) {
1855
- const resolved = resolveModuleFile(
1856
- roots,
1857
- { appBase: appImportBase, pkgBase: pkgImportBase },
1858
- path.posix.join('subscribers', relPath.replace(/\\/g, '/')),
1859
- )
1860
- if (!resolved) continue
1861
- const segs = relPath.split('/')
1862
- const file = segs.pop()!
1863
- const name = stripModuleCodeExtension(file)
1864
- const importPath = sanitizeGeneratedModuleSpecifier(resolved.importPath)
1865
- const sid = [modId, ...segs, name].filter(Boolean).join(':')
1866
- const sourceFile = resolved.absolutePath
1867
- const metadata = await loadSubscriberMetadata(sourceFile)
2017
+ for (const { id, importPath, metadata } of discovered) {
2018
+ const subscriberId = metadata?.id ?? id
1868
2019
  subscribers.push(
1869
- `{ id: ${toLiteral(metadata?.id ?? sid)}, 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(metadata?.id ?? sid)}) }`
2020
+ `{ 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)}) }`
1870
2021
  )
1871
2022
  }
1872
2023
  return subscribers
1873
2024
  }
1874
2025
 
1875
- async function processWorkers(options: {
1876
- roots: ModuleRoots
1877
- modId: string
1878
- appImportBase: string
1879
- pkgImportBase: string
1880
- }): Promise<string[]> {
1881
- const { roots, modId, appImportBase, pkgImportBase } = options
1882
- const files = scanModuleDir(roots, SCAN_CONFIGS.workers)
2026
+ function processWorkers(discovered: DiscoveredWorker[]): string[] {
1883
2027
  const workers: string[] = []
1884
- for (const { relPath } of files) {
1885
- const resolved = resolveModuleFile(
1886
- roots,
1887
- { appBase: appImportBase, pkgBase: pkgImportBase },
1888
- path.posix.join('workers', relPath.replace(/\\/g, '/')),
1889
- )
1890
- if (!resolved) continue
1891
- const segs = relPath.split('/')
1892
- const file = segs.pop()!
1893
- const name = stripModuleCodeExtension(file)
1894
- const importPath = sanitizeGeneratedModuleSpecifier(resolved.importPath)
1895
- const sourceFile = resolved.absolutePath
1896
- const metadata = await loadWorkerMetadata(sourceFile)
1897
- if (!metadata?.queue) continue
1898
- const wid = [modId, 'workers', ...segs, name].filter(Boolean).join(':')
2028
+ for (const { id, importPath, metadata } of discovered) {
2029
+ const workerId = metadata.id ?? id
1899
2030
  workers.push(
1900
- `{ id: ${toLiteral(metadata.id ?? wid)}, queue: ${toLiteral(metadata.queue)}, concurrency: ${toLiteral(metadata.concurrency ?? 1)}, handler: createLazyModuleWorker(() => ${buildDynamicImportExpression(importPath)}, ${toLiteral(metadata.id ?? wid)}) }`
2031
+ `{ id: ${toLiteral(workerId)}, queue: ${toLiteral(metadata.queue)}, concurrency: ${toLiteral(metadata.concurrency ?? 1)}, handler: createLazyModuleWorker(() => ${buildDynamicImportExpression(importPath)}, ${toLiteral(workerId)}) }`
1901
2032
  )
1902
2033
  }
1903
2034
  return workers
1904
2035
  }
1905
2036
 
1906
2037
  function processTranslations(options: {
1907
- roots: ModuleRoots
2038
+ discovered: DiscoveredTranslation[]
1908
2039
  modId: string
1909
2040
  appImportBase: string
1910
2041
  pkgImportBase: string
1911
2042
  imports: string[]
1912
2043
  extraImports?: string[]
1913
2044
  }): string[] {
1914
- const { roots, modId, appImportBase, pkgImportBase, imports, extraImports } = options
1915
- const i18nApp = path.join(roots.appBase, 'i18n')
1916
- const pkgI18nBase = resolveStandaloneSourceMirrorBase(roots.pkgBase) ?? roots.pkgBase
1917
- const i18nCore = path.join(pkgI18nBase, 'i18n')
1918
- const locales = new Set<string>()
1919
- if (fs.existsSync(i18nCore))
1920
- for (const e of fs.readdirSync(i18nCore, { withFileTypes: true }))
1921
- if (e.isFile() && e.name.endsWith('.json')) locales.add(e.name.replace(/\.json$/, ''))
1922
- if (fs.existsSync(i18nApp))
1923
- for (const e of fs.readdirSync(i18nApp, { withFileTypes: true }))
1924
- if (e.isFile() && e.name.endsWith('.json')) locales.add(e.name.replace(/\.json$/, ''))
2045
+ const { discovered, modId, appImportBase, pkgImportBase, imports, extraImports } = options
1925
2046
  const translations: string[] = []
1926
- for (const locale of locales) {
1927
- const coreHas = fs.existsSync(path.join(i18nCore, `${locale}.json`))
1928
- const appHas = fs.existsSync(path.join(i18nApp, `${locale}.json`))
2047
+ for (const { locale, coreHas, appHas } of discovered) {
1929
2048
  if (coreHas && appHas) {
1930
2049
  const cName = `T_${toVar(modId)}_${toVar(locale)}_C`
1931
2050
  const aName = `T_${toVar(modId)}_${toVar(locale)}_A`
@@ -2547,28 +2666,10 @@ async function processPageFilesAst(options: {
2547
2666
  return routes
2548
2667
  }
2549
2668
 
2550
- async function processSubscribersAst(options: {
2551
- roots: ModuleRoots
2552
- modId: string
2553
- appImportBase: string
2554
- pkgImportBase: string
2555
- }): Promise<WriterFunction[]> {
2556
- const { roots, modId, appImportBase, pkgImportBase } = options
2557
- const files = scanModuleDir(roots, SCAN_CONFIGS.subscribers)
2669
+ function processSubscribersAst(discovered: DiscoveredSubscriber[]): WriterFunction[] {
2558
2670
  const subscribers: WriterFunction[] = []
2559
- for (const { relPath } of files) {
2560
- const resolved = resolveModuleFile(
2561
- roots,
2562
- { appBase: appImportBase, pkgBase: pkgImportBase },
2563
- path.posix.join('subscribers', relPath.replace(/\\/g, '/')),
2564
- )
2565
- if (!resolved) continue
2566
- const segs = relPath.split('/')
2567
- const file = segs.pop()!
2568
- const name = stripModuleCodeExtension(file)
2569
- const sid = [modId, ...segs, name].filter(Boolean).join(':')
2570
- const metadata = await loadSubscriberMetadata(resolved.absolutePath)
2571
- const subscriberId = metadata?.id ?? sid
2671
+ for (const { id, importPath, metadata } of discovered) {
2672
+ const subscriberId = metadata?.id ?? id
2572
2673
  subscribers.push(
2573
2674
  objectLiteral([
2574
2675
  { name: 'id', value: subscriberId },
@@ -2580,7 +2681,7 @@ async function processSubscribersAst(options: {
2580
2681
  name: 'handler',
2581
2682
  value: callExpression(identifier('createLazyModuleSubscriber'), [
2582
2683
  arrowFunction({
2583
- body: importExpression(resolved.importPath),
2684
+ body: importExpression(importPath),
2584
2685
  }),
2585
2686
  subscriberId,
2586
2687
  ]),
@@ -2591,28 +2692,10 @@ async function processSubscribersAst(options: {
2591
2692
  return subscribers
2592
2693
  }
2593
2694
 
2594
- async function processWorkersAst(options: {
2595
- roots: ModuleRoots
2596
- modId: string
2597
- appImportBase: string
2598
- pkgImportBase: string
2599
- }): Promise<WriterFunction[]> {
2600
- const { roots, modId, appImportBase, pkgImportBase } = options
2601
- const files = scanModuleDir(roots, SCAN_CONFIGS.workers)
2695
+ function processWorkersAst(discovered: DiscoveredWorker[]): WriterFunction[] {
2602
2696
  const workers: WriterFunction[] = []
2603
- for (const { relPath } of files) {
2604
- const resolved = resolveModuleFile(
2605
- roots,
2606
- { appBase: appImportBase, pkgBase: pkgImportBase },
2607
- path.posix.join('workers', relPath.replace(/\\/g, '/')),
2608
- )
2609
- if (!resolved) continue
2610
- const segs = relPath.split('/')
2611
- const file = segs.pop()!
2612
- const name = stripModuleCodeExtension(file)
2613
- const metadata = await loadWorkerMetadata(resolved.absolutePath)
2614
- if (!metadata?.queue) continue
2615
- const workerId = metadata.id ?? [modId, 'workers', ...segs, name].filter(Boolean).join(':')
2697
+ for (const { id, importPath, metadata } of discovered) {
2698
+ const workerId = metadata.id ?? id
2616
2699
  workers.push(
2617
2700
  objectLiteral([
2618
2701
  { name: 'id', value: workerId },
@@ -2622,7 +2705,7 @@ async function processWorkersAst(options: {
2622
2705
  name: 'handler',
2623
2706
  value: callExpression(identifier('createLazyModuleWorker'), [
2624
2707
  arrowFunction({
2625
- body: importExpression(resolved.importPath),
2708
+ body: importExpression(importPath),
2626
2709
  }),
2627
2710
  workerId,
2628
2711
  ]),
@@ -2634,40 +2717,18 @@ async function processWorkersAst(options: {
2634
2717
  }
2635
2718
 
2636
2719
  function processTranslationsAst(options: {
2637
- roots: ModuleRoots
2720
+ discovered: DiscoveredTranslation[]
2638
2721
  modId: string
2639
2722
  appImportBase: string
2640
2723
  pkgImportBase: string
2641
2724
  imports: string[]
2642
2725
  extraImports?: string[]
2643
2726
  }): GeneratedObjectEntry[] {
2644
- const { roots, modId, appImportBase, pkgImportBase, imports, extraImports } = options
2645
- const i18nApp = path.join(roots.appBase, 'i18n')
2646
- const pkgI18nBase = resolveStandaloneSourceMirrorBase(roots.pkgBase) ?? roots.pkgBase
2647
- const i18nCore = path.join(pkgI18nBase, 'i18n')
2648
- const locales = new Set<string>()
2649
-
2650
- if (fs.existsSync(i18nCore)) {
2651
- for (const entry of fs.readdirSync(i18nCore, { withFileTypes: true })) {
2652
- if (entry.isFile() && entry.name.endsWith('.json')) {
2653
- locales.add(entry.name.replace(/\.json$/, ''))
2654
- }
2655
- }
2656
- }
2657
-
2658
- if (fs.existsSync(i18nApp)) {
2659
- for (const entry of fs.readdirSync(i18nApp, { withFileTypes: true })) {
2660
- if (entry.isFile() && entry.name.endsWith('.json')) {
2661
- locales.add(entry.name.replace(/\.json$/, ''))
2662
- }
2663
- }
2664
- }
2727
+ const { discovered, modId, appImportBase, pkgImportBase, imports, extraImports } = options
2665
2728
 
2666
2729
  const translations: GeneratedObjectEntry[] = []
2667
2730
 
2668
- for (const locale of locales) {
2669
- const coreHas = fs.existsSync(path.join(i18nCore, `${locale}.json`))
2670
- const appHas = fs.existsSync(path.join(i18nApp, `${locale}.json`))
2731
+ for (const { locale, coreHas, appHas } of discovered) {
2671
2732
 
2672
2733
  if (coreHas && appHas) {
2673
2734
  const coreName = `T_${toVar(modId)}_${toVar(locale)}_C`
@@ -2712,9 +2773,7 @@ function processTranslationsAst(options: {
2712
2773
  }
2713
2774
 
2714
2775
  function resolveConventionFile(
2715
- roots: ModuleRoots,
2716
- imps: ModuleImports,
2717
- relativePath: string,
2776
+ resolved: ResolvedFile | null,
2718
2777
  prefix: string,
2719
2778
  modId: string,
2720
2779
  importIdRef: { value: number },
@@ -2722,7 +2781,6 @@ function resolveConventionFile(
2722
2781
  extraImports?: string[],
2723
2782
  importStyle: 'namespace' | 'default' = 'namespace'
2724
2783
  ): { importName: string; importPath: string; fromApp: boolean } | null {
2725
- const resolved = resolveModuleFile(roots, imps, relativePath)
2726
2784
  if (!resolved) return null
2727
2785
  const importName = `${prefix}_${toVar(modId)}_${importIdRef.value++}`
2728
2786
  const importPath = sanitizeGeneratedModuleSpecifier(resolved.importPath)
@@ -2736,8 +2794,13 @@ function resolveConventionFile(
2736
2794
  return { importName, importPath, fromApp: resolved.fromApp }
2737
2795
  }
2738
2796
 
2739
- export async function generateModuleRegistry(options: ModuleRegistryOptions): Promise<GeneratorResult> {
2797
+ type ModuleRegistryRenderOptions = ModuleRegistryOptions & {
2798
+ discovery: ModuleRegistryDiscovery
2799
+ }
2800
+
2801
+ async function generateModuleRegistryFromDiscovery(options: ModuleRegistryRenderOptions): Promise<GeneratorResult> {
2740
2802
  const { resolver, quiet = false } = options
2803
+ const { discovery } = options
2741
2804
  const result = createGeneratorResult()
2742
2805
 
2743
2806
  const outputDir = resolver.getOutputDir()
@@ -2758,22 +2821,14 @@ export async function generateModuleRegistry(options: ModuleRegistryOptions): Pr
2758
2821
  const commandLoadersOutFile = path.join(outputDir, 'command-loaders.generated.ts')
2759
2822
  const commandLoadersChecksumFile = path.join(outputDir, 'command-loaders.generated.checksum')
2760
2823
 
2761
- const enabled = resolver.loadEnabledModules()
2762
- for (const entry of enabled) {
2763
- verifyThirdPartyModuleShape(entry, resolver.getModulePaths(entry).pkgBase)
2764
- }
2824
+ const enabled = discovery.enabled
2765
2825
  const extensions = loadGeneratorExtensions()
2766
2826
 
2767
2827
  // Pre-pass: collect generator plugins from each enabled module's generators.ts
2768
2828
  const pluginRegistry = new Map<string, import('@open-mercato/shared/modules/generators').GeneratorPlugin>()
2769
2829
  const pluginState = new Map<string, { imports: string[]; configs: string[] }>()
2770
- for (const entry of enabled) {
2771
- const roots = resolver.getModulePaths(entry)
2772
- const rawImps = resolver.getModuleImportBase(entry)
2773
- const isAppMod = entry.from === '@app'
2774
- const appImportBase = isAppMod ? `../../src/modules/${entry.id}` : rawImps.appBase
2775
- const imps: ModuleImports = { appBase: appImportBase, pkgBase: rawImps.pkgBase }
2776
- const resolved = resolveModuleFile(roots, imps, 'generators.ts')
2830
+ for (const discovered of discovery.modules) {
2831
+ const resolved = discovered.resolve('generators.ts')
2777
2832
  if (!resolved) continue
2778
2833
  try {
2779
2834
  const pluginMod = await import(buildCacheBustedSourceImportUrl(resolved.absolutePath))
@@ -2799,7 +2854,6 @@ export async function generateModuleRegistry(options: ModuleRegistryOptions): Pr
2799
2854
  const apiRouteManifestDecls: string[] = []
2800
2855
  // Mutable ref so extracted helper functions can increment the shared counter
2801
2856
  const importIdRef = { value: 0 }
2802
- const trackedRoots = new Set<string>()
2803
2857
  const requiresByModule = new Map<string, string[]>()
2804
2858
  const commandLoaderEntries: CommandLoaderGenerationEntry[] = []
2805
2859
  let hasRouteComponents = false
@@ -2812,17 +2866,9 @@ export async function generateModuleRegistry(options: ModuleRegistryOptions): Pr
2812
2866
  aclPath?: string
2813
2867
  }> = []
2814
2868
 
2815
- for (const entry of enabled) {
2816
- const modId = entry.id
2817
- const roots = resolver.getModulePaths(entry)
2818
- const rawImps = resolver.getModuleImportBase(entry)
2819
- trackedRoots.add(roots.appBase)
2820
- trackedRoots.add(roots.pkgBase)
2821
-
2822
- const isAppModule = entry.from === '@app'
2823
- const appImportBase = isAppModule ? `../../src/modules/${modId}` : rawImps.appBase
2824
- const imps: ModuleImports = { appBase: appImportBase, pkgBase: rawImps.pkgBase }
2825
- commandLoaderEntries.push(...collectCommandLoaderEntries(roots, imps, modId))
2869
+ for (const discovered of discovery.modules) {
2870
+ const { modId, roots, imps, appImportBase } = discovered
2871
+ commandLoaderEntries.push(...discovered.commandLoaderEntries)
2826
2872
 
2827
2873
  const frontendRoutes: string[] = []
2828
2874
  const backendRoutes: string[] = []
@@ -2848,7 +2894,7 @@ export async function generateModuleRegistry(options: ModuleRegistryOptions): Pr
2848
2894
  // === Processing order MUST match original import ID sequence ===
2849
2895
 
2850
2896
  // 1. Module metadata: index.ts (overrideable)
2851
- const indexResolved = resolveModuleFile(roots, imps, 'index.ts')
2897
+ const indexResolved = discovered.resolve('index.ts')
2852
2898
  if (indexResolved) {
2853
2899
  infoImportName = `I${importIdRef.value++}_${toVar(modId)}`
2854
2900
  const importPath = sanitizeGeneratedModuleSpecifier(indexResolved.importPath)
@@ -2867,7 +2913,7 @@ export async function generateModuleRegistry(options: ModuleRegistryOptions): Pr
2867
2913
  {
2868
2914
  const feApp = path.join(roots.appBase, 'frontend')
2869
2915
  const fePkg = path.join(roots.pkgBase, 'frontend')
2870
- const feFiles = scanModuleDir(roots, SCAN_CONFIGS.frontendPages)
2916
+ const feFiles = discovered.frontendFiles
2871
2917
  if (feFiles.length) {
2872
2918
  const generatedFrontendRoutes = await processPageFiles({
2873
2919
  files: feFiles,
@@ -2891,13 +2937,13 @@ export async function generateModuleRegistry(options: ModuleRegistryOptions): Pr
2891
2937
 
2892
2938
  // 3. Entity extensions: data/extensions.ts
2893
2939
  {
2894
- const ext = resolveConventionFile(roots, imps, 'data/extensions.ts', 'X', modId, importIdRef, imports, runtimeImports)
2940
+ const ext = resolveConventionFile(discovered.resolve('data/extensions.ts'), 'X', modId, importIdRef, imports, runtimeImports)
2895
2941
  if (ext) extensionsImportName = ext.importName
2896
2942
  }
2897
2943
 
2898
2944
  // 4. RBAC: acl.ts
2899
2945
  {
2900
- const aclResolved = resolveModuleFile(roots, imps, 'acl.ts')
2946
+ const aclResolved = discovered.resolve('acl.ts')
2901
2947
  if (aclResolved) {
2902
2948
  const importName = `ACL_${toVar(modId)}_${importIdRef.value++}`
2903
2949
  const importPath = sanitizeGeneratedModuleSpecifier(aclResolved.importPath)
@@ -2909,7 +2955,7 @@ export async function generateModuleRegistry(options: ModuleRegistryOptions): Pr
2909
2955
 
2910
2956
  // 5. Custom entities: ce.ts
2911
2957
  {
2912
- const ce = resolveConventionFile(roots, imps, 'ce.ts', 'CE', modId, importIdRef, imports, runtimeImports)
2958
+ const ce = resolveConventionFile(discovered.resolve('ce.ts'), 'CE', modId, importIdRef, imports, runtimeImports)
2913
2959
  if (ce) customEntitiesImportName = ce.importName
2914
2960
  }
2915
2961
 
@@ -2930,9 +2976,9 @@ export async function generateModuleRegistry(options: ModuleRegistryOptions): Pr
2930
2976
 
2931
2977
  // Track file paths for UMES conflict detection
2932
2978
  {
2933
- const compOverridesFile = resolveModuleFile(roots, imps, 'widgets/components.ts')
2934
- const interceptorsFile = resolveModuleFile(roots, imps, 'api/interceptors.ts')
2935
- const aclFile = resolveModuleFile(roots, imps, 'acl.ts')
2979
+ const compOverridesFile = discovered.resolve('widgets/components.ts')
2980
+ const interceptorsFile = discovered.resolve('api/interceptors.ts')
2981
+ const aclFile = discovered.resolve('acl.ts')
2936
2982
  umesConflictSources.push({
2937
2983
  moduleId: modId,
2938
2984
  componentOverridesPath: compOverridesFile?.absolutePath,
@@ -2955,19 +3001,19 @@ export async function generateModuleRegistry(options: ModuleRegistryOptions): Pr
2955
3001
 
2956
3002
  // 11. Setup: setup.ts
2957
3003
  {
2958
- const setup = resolveConventionFile(roots, imps, 'setup.ts', 'SETUP', modId, importIdRef, imports, runtimeImports)
3004
+ const setup = resolveConventionFile(discovered.resolve('setup.ts'), 'SETUP', modId, importIdRef, imports, runtimeImports)
2959
3005
  if (setup) setupImportName = setup.importName
2960
3006
  }
2961
3007
 
2962
3008
  // 11a. Encryption defaults: encryption.ts
2963
3009
  {
2964
- const encryption = resolveConventionFile(roots, imps, 'encryption.ts', 'ENCRYPTION', modId, importIdRef, imports, runtimeImports)
3010
+ const encryption = resolveConventionFile(discovered.resolve('encryption.ts'), 'ENCRYPTION', modId, importIdRef, imports, runtimeImports)
2965
3011
  if (encryption) encryptionImportName = encryption.importName
2966
3012
  }
2967
3013
 
2968
3014
  // 11b. Integration manifest: integration.ts
2969
3015
  {
2970
- const resolved = resolveModuleFile(roots, imps, 'integration.ts')
3016
+ const resolved = discovered.resolve('integration.ts')
2971
3017
  if (resolved) {
2972
3018
  const importName = `INTEGRATION_${toVar(modId)}_${importIdRef.value++}`
2973
3019
  imports.push(buildImportStatement(`* as ${importName}`, resolved.importPath))
@@ -2978,7 +3024,7 @@ export async function generateModuleRegistry(options: ModuleRegistryOptions): Pr
2978
3024
 
2979
3025
  // 12. Custom fields: data/fields.ts
2980
3026
  {
2981
- const fields = resolveConventionFile(roots, imps, 'data/fields.ts', 'F', modId, importIdRef, imports, runtimeImports)
3027
+ const fields = resolveConventionFile(discovered.resolve('data/fields.ts'), 'F', modId, importIdRef, imports, runtimeImports)
2982
3028
  if (fields) fieldsImportName = fields.importName
2983
3029
  }
2984
3030
 
@@ -2986,7 +3032,7 @@ export async function generateModuleRegistry(options: ModuleRegistryOptions): Pr
2986
3032
  {
2987
3033
  const beApp = path.join(roots.appBase, 'backend')
2988
3034
  const bePkg = path.join(roots.pkgBase, 'backend')
2989
- const beFiles = scanModuleDir(roots, SCAN_CONFIGS.backendPages)
3035
+ const beFiles = discovered.backendFiles
2990
3036
  if (beFiles.length) {
2991
3037
  const generatedBackendRoutes = await processPageFiles({
2992
3038
  files: beFiles,
@@ -3025,7 +3071,7 @@ export async function generateModuleRegistry(options: ModuleRegistryOptions): Pr
3025
3071
 
3026
3072
  // 15. CLI
3027
3073
  {
3028
- const cliResolved = resolveModuleFile(roots, imps, 'cli.ts')
3074
+ const cliResolved = discovered.resolve('cli.ts')
3029
3075
  if (cliResolved) {
3030
3076
  const importName = `CLI_${toVar(modId)}`
3031
3077
  imports.push(buildImportStatement(importName, sanitizeGeneratedModuleSpecifier(cliResolved.importPath)))
@@ -3035,7 +3081,7 @@ export async function generateModuleRegistry(options: ModuleRegistryOptions): Pr
3035
3081
 
3036
3082
  // 16. Translations
3037
3083
  translations.push(...processTranslations({
3038
- roots,
3084
+ discovered: discovered.translations,
3039
3085
  modId,
3040
3086
  appImportBase,
3041
3087
  pkgImportBase: imps.pkgBase,
@@ -3044,20 +3090,10 @@ export async function generateModuleRegistry(options: ModuleRegistryOptions): Pr
3044
3090
  }))
3045
3091
 
3046
3092
  // 17. Subscribers
3047
- subscribers.push(...await processSubscribers({
3048
- roots,
3049
- modId,
3050
- appImportBase,
3051
- pkgImportBase: imps.pkgBase,
3052
- }))
3093
+ subscribers.push(...processSubscribers(await discovered.getSubscribers()))
3053
3094
 
3054
3095
  // 18. Workers
3055
- workers.push(...await processWorkers({
3056
- roots,
3057
- modId,
3058
- appImportBase,
3059
- pkgImportBase: imps.pkgBase,
3060
- }))
3096
+ workers.push(...processWorkers(await discovered.getWorkers()))
3061
3097
 
3062
3098
  // Build combined customFieldSets expression
3063
3099
  {
@@ -3073,13 +3109,7 @@ export async function generateModuleRegistry(options: ModuleRegistryOptions): Pr
3073
3109
 
3074
3110
  // 19. Dashboard widgets
3075
3111
  {
3076
- const entries = scanDashboardWidgetEntries({
3077
- modId,
3078
- roots,
3079
- appImportBase,
3080
- pkgImportBase: imps.pkgBase,
3081
- })
3082
- for (const entry of entries) {
3112
+ for (const entry of discovered.dashboardWidgetEntries) {
3083
3113
  dashboardWidgets.push(
3084
3114
  `{ moduleId: ${toLiteral(entry.moduleId)}, key: ${toLiteral(entry.key)}, source: ${toLiteral(entry.source)}, loader: () => ${buildDynamicImportExpression(entry.importPath)}.then((mod) => mod.default ?? mod) }`
3085
3115
  )
@@ -3296,9 +3326,7 @@ export async function generateModuleRegistry(options: ModuleRegistryOptions): Pr
3296
3326
  }
3297
3327
  }
3298
3328
 
3299
- const structureChecksum = calculateStructureChecksum([
3300
- ...Array.from(trackedRoots),
3301
- ])
3329
+ const structureChecksum = discovery.getStructureChecksum()
3302
3330
 
3303
3331
  writeGeneratedFile({ outFile, checksumFile, content: output, structureChecksum, result, quiet })
3304
3332
  writeGeneratedFile({ outFile: runtimeOutFile, checksumFile: runtimeChecksumFile, content: runtimeOutput, structureChecksum, result, quiet })
@@ -3373,8 +3401,9 @@ ${body}
3373
3401
  return result
3374
3402
  }
3375
3403
 
3376
- export async function generateModuleRegistryApp(options: ModuleRegistryOptions): Promise<GeneratorResult> {
3404
+ async function generateModuleRegistryAppFromDiscovery(options: ModuleRegistryRenderOptions): Promise<GeneratorResult> {
3377
3405
  const { resolver, quiet = false } = options
3406
+ const { discovery } = options
3378
3407
  const result = createGeneratorResult()
3379
3408
 
3380
3409
  const outputDir = resolver.getOutputDir()
@@ -3391,10 +3420,7 @@ export async function generateModuleRegistryApp(options: ModuleRegistryOptions):
3391
3420
  const commandLoadersOutFile = path.join(outputDir, 'command-loaders.generated.ts')
3392
3421
  const commandLoadersChecksumFile = path.join(outputDir, 'command-loaders.generated.checksum')
3393
3422
 
3394
- const enabled = resolver.loadEnabledModules()
3395
- for (const entry of enabled) {
3396
- verifyThirdPartyModuleShape(entry, resolver.getModulePaths(entry).pkgBase)
3397
- }
3423
+ const enabled = discovery.enabled
3398
3424
  const imports: string[] = []
3399
3425
  const bootstrapImports: string[] = []
3400
3426
  const i18nImports: string[] = []
@@ -3402,22 +3428,13 @@ export async function generateModuleRegistryApp(options: ModuleRegistryOptions):
3402
3428
  const bootstrapModuleDecls: WriterFunction[] = []
3403
3429
  const i18nModuleDecls: WriterFunction[] = []
3404
3430
  const importIdRef = { value: 0 }
3405
- const trackedRoots = new Set<string>()
3406
3431
  const requiresByModule = new Map<string, string[]>()
3407
3432
  const commandLoaderEntries: CommandLoaderGenerationEntry[] = []
3408
3433
  let hasRouteComponents = false
3409
3434
 
3410
- for (const entry of enabled) {
3411
- const modId = entry.id
3412
- const roots = resolver.getModulePaths(entry)
3413
- const rawImps = resolver.getModuleImportBase(entry)
3414
- trackedRoots.add(roots.appBase)
3415
- trackedRoots.add(roots.pkgBase)
3416
-
3417
- const isAppModule = entry.from === '@app'
3418
- const appImportBase = isAppModule ? `../../src/modules/${modId}` : rawImps.appBase
3419
- const imps: ModuleImports = { appBase: appImportBase, pkgBase: rawImps.pkgBase }
3420
- commandLoaderEntries.push(...collectCommandLoaderEntries(roots, imps, modId))
3435
+ for (const discovered of discovery.modules) {
3436
+ const { modId, roots, imps, appImportBase } = discovered
3437
+ commandLoaderEntries.push(...discovered.commandLoaderEntries)
3421
3438
 
3422
3439
  const frontendRoutes: WriterFunction[] = []
3423
3440
  const backendRoutes: WriterFunction[] = []
@@ -3434,7 +3451,7 @@ export async function generateModuleRegistryApp(options: ModuleRegistryOptions):
3434
3451
  let encryptionImportName: string | null = null
3435
3452
  let integrationImportName: string | null = null
3436
3453
 
3437
- const indexResolved = resolveModuleFile(roots, imps, 'index.ts')
3454
+ const indexResolved = discovered.resolve('index.ts')
3438
3455
  if (indexResolved) {
3439
3456
  infoImportName = `I${importIdRef.value++}_${toVar(modId)}`
3440
3457
  const importPath = sanitizeGeneratedModuleSpecifier(indexResolved.importPath)
@@ -3450,19 +3467,19 @@ export async function generateModuleRegistryApp(options: ModuleRegistryOptions):
3450
3467
  }
3451
3468
 
3452
3469
  {
3453
- const setup = resolveConventionFile(roots, imps, 'setup.ts', 'SETUP', modId, importIdRef, imports)
3470
+ const setup = resolveConventionFile(discovered.resolve('setup.ts'), 'SETUP', modId, importIdRef, imports)
3454
3471
  if (setup) bootstrapImports.push(buildImportStatement(`* as ${setup.importName}`, setup.importPath))
3455
3472
  if (setup) setupImportName = setup.importName
3456
3473
  }
3457
3474
 
3458
3475
  {
3459
- const encryption = resolveConventionFile(roots, imps, 'encryption.ts', 'ENCRYPTION', modId, importIdRef, imports)
3476
+ const encryption = resolveConventionFile(discovered.resolve('encryption.ts'), 'ENCRYPTION', modId, importIdRef, imports)
3460
3477
  if (encryption) bootstrapImports.push(buildImportStatement(`* as ${encryption.importName}`, encryption.importPath))
3461
3478
  if (encryption) encryptionImportName = encryption.importName
3462
3479
  }
3463
3480
 
3464
3481
  {
3465
- const resolved = resolveModuleFile(roots, imps, 'integration.ts')
3482
+ const resolved = discovered.resolve('integration.ts')
3466
3483
  if (resolved) {
3467
3484
  const importName = `INTEGRATION_${toVar(modId)}_${importIdRef.value++}`
3468
3485
  imports.push(buildImportStatement(`* as ${importName}`, resolved.importPath))
@@ -3472,12 +3489,12 @@ export async function generateModuleRegistryApp(options: ModuleRegistryOptions):
3472
3489
  }
3473
3490
 
3474
3491
  {
3475
- const ext = resolveConventionFile(roots, imps, 'data/extensions.ts', 'X', modId, importIdRef, imports, bootstrapImports)
3492
+ const ext = resolveConventionFile(discovered.resolve('data/extensions.ts'), 'X', modId, importIdRef, imports, bootstrapImports)
3476
3493
  if (ext) extensionsImportName = ext.importName
3477
3494
  }
3478
3495
 
3479
3496
  {
3480
- const aclResolved = resolveModuleFile(roots, imps, 'acl.ts')
3497
+ const aclResolved = discovered.resolve('acl.ts')
3481
3498
  if (aclResolved) {
3482
3499
  const importName = `ACL_${toVar(modId)}_${importIdRef.value++}`
3483
3500
  const importPath = sanitizeGeneratedModuleSpecifier(aclResolved.importPath)
@@ -3488,19 +3505,19 @@ export async function generateModuleRegistryApp(options: ModuleRegistryOptions):
3488
3505
  }
3489
3506
 
3490
3507
  {
3491
- const ce = resolveConventionFile(roots, imps, 'ce.ts', 'CE', modId, importIdRef, imports, bootstrapImports)
3508
+ const ce = resolveConventionFile(discovered.resolve('ce.ts'), 'CE', modId, importIdRef, imports, bootstrapImports)
3492
3509
  if (ce) customEntitiesImportName = ce.importName
3493
3510
  }
3494
3511
 
3495
3512
  {
3496
- const fields = resolveConventionFile(roots, imps, 'data/fields.ts', 'F', modId, importIdRef, imports, bootstrapImports)
3513
+ const fields = resolveConventionFile(discovered.resolve('data/fields.ts'), 'F', modId, importIdRef, imports, bootstrapImports)
3497
3514
  if (fields) fieldsImportName = fields.importName
3498
3515
  }
3499
3516
 
3500
3517
  {
3501
3518
  const feApp = path.join(roots.appBase, 'frontend')
3502
3519
  const fePkg = path.join(roots.pkgBase, 'frontend')
3503
- const feFiles = scanModuleDir(roots, SCAN_CONFIGS.frontendPages)
3520
+ const feFiles = discovered.frontendFiles
3504
3521
  if (feFiles.length > 0) {
3505
3522
  frontendRoutes.push(...await processPageFilesAst({
3506
3523
  files: feFiles,
@@ -3520,7 +3537,7 @@ export async function generateModuleRegistryApp(options: ModuleRegistryOptions):
3520
3537
  {
3521
3538
  const beApp = path.join(roots.appBase, 'backend')
3522
3539
  const bePkg = path.join(roots.pkgBase, 'backend')
3523
- const beFiles = scanModuleDir(roots, SCAN_CONFIGS.backendPages)
3540
+ const beFiles = discovered.backendFiles
3524
3541
  if (beFiles.length > 0) {
3525
3542
  backendRoutes.push(...await processPageFilesAst({
3526
3543
  files: beFiles,
@@ -3539,7 +3556,7 @@ export async function generateModuleRegistryApp(options: ModuleRegistryOptions):
3539
3556
 
3540
3557
  const translationImports: string[] = []
3541
3558
  translations.push(...processTranslationsAst({
3542
- roots,
3559
+ discovered: discovered.translations,
3543
3560
  modId,
3544
3561
  appImportBase,
3545
3562
  pkgImportBase: imps.pkgBase,
@@ -3549,28 +3566,12 @@ export async function generateModuleRegistryApp(options: ModuleRegistryOptions):
3549
3566
  bootstrapImports.push(...translationImports)
3550
3567
  i18nImports.push(...translationImports)
3551
3568
 
3552
- subscribers.push(...await processSubscribersAst({
3553
- roots,
3554
- modId,
3555
- appImportBase,
3556
- pkgImportBase: imps.pkgBase,
3557
- }))
3569
+ subscribers.push(...processSubscribersAst(await discovered.getSubscribers()))
3558
3570
 
3559
- workers.push(...await processWorkersAst({
3560
- roots,
3561
- modId,
3562
- appImportBase,
3563
- pkgImportBase: imps.pkgBase,
3564
- }))
3571
+ workers.push(...processWorkersAst(await discovered.getWorkers()))
3565
3572
 
3566
3573
  {
3567
- const entries = scanDashboardWidgetEntries({
3568
- modId,
3569
- roots,
3570
- appImportBase,
3571
- pkgImportBase: imps.pkgBase,
3572
- })
3573
- dashboardWidgetsValue = buildModuleDashboardWidgetsValue(entries)
3574
+ dashboardWidgetsValue = buildModuleDashboardWidgetsValue(discovered.dashboardWidgetEntries)
3574
3575
  }
3575
3576
 
3576
3577
  const moduleEntries: GeneratedObjectEntry[] = [
@@ -3732,7 +3733,7 @@ export async function generateModuleRegistryApp(options: ModuleRegistryOptions):
3732
3733
  }
3733
3734
  }
3734
3735
 
3735
- const structureChecksum = calculateStructureChecksum(Array.from(trackedRoots))
3736
+ const structureChecksum = discovery.getStructureChecksum()
3736
3737
  writeGeneratedFile({ outFile, checksumFile, content: output, structureChecksum, result, quiet })
3737
3738
  writeGeneratedFile({ outFile: bootstrapOutFile, checksumFile: bootstrapChecksumFile, content: bootstrapOutput, structureChecksum, result, quiet })
3738
3739
  writeGeneratedFile({ outFile: i18nOutFile, checksumFile: i18nChecksumFile, content: i18nOutput, structureChecksum, result, quiet })
@@ -3755,8 +3756,9 @@ export async function generateModuleRegistryApp(options: ModuleRegistryOptions):
3755
3756
  * features/ACL, custom entities, vector config, custom fields, dashboard widgets
3756
3757
  * Excludes: frontend routes, backend routes, API handlers, injection widgets
3757
3758
  */
3758
- export async function generateModuleRegistryCli(options: ModuleRegistryOptions): Promise<GeneratorResult> {
3759
+ async function generateModuleRegistryCliFromDiscovery(options: ModuleRegistryRenderOptions): Promise<GeneratorResult> {
3759
3760
  const { resolver, quiet = false } = options
3761
+ const { discovery } = options
3760
3762
  const result = createGeneratorResult()
3761
3763
 
3762
3764
  const outputDir = resolver.getOutputDir()
@@ -3767,29 +3769,17 @@ export async function generateModuleRegistryCli(options: ModuleRegistryOptions):
3767
3769
  const commandLoadersOutFile = path.join(outputDir, 'command-loaders.generated.ts')
3768
3770
  const commandLoadersChecksumFile = path.join(outputDir, 'command-loaders.generated.checksum')
3769
3771
 
3770
- const enabled = resolver.loadEnabledModules()
3771
- for (const entry of enabled) {
3772
- verifyThirdPartyModuleShape(entry, resolver.getModulePaths(entry).pkgBase)
3773
- }
3772
+ const enabled = discovery.enabled
3774
3773
  const imports: string[] = []
3775
3774
  const moduleDecls: WriterFunction[] = []
3776
3775
  // Mutable ref so extracted helper functions can increment the shared counter
3777
3776
  const importIdRef = { value: 0 }
3778
- const trackedRoots = new Set<string>()
3779
3777
  const requiresByModule = new Map<string, string[]>()
3780
3778
  const commandLoaderEntries: CommandLoaderGenerationEntry[] = []
3781
3779
 
3782
- for (const entry of enabled) {
3783
- const modId = entry.id
3784
- const roots = resolver.getModulePaths(entry)
3785
- const rawImps = resolver.getModuleImportBase(entry)
3786
- trackedRoots.add(roots.appBase)
3787
- trackedRoots.add(roots.pkgBase)
3788
-
3789
- const isAppModule = entry.from === '@app'
3790
- const appImportBase = isAppModule ? `../../src/modules/${modId}` : rawImps.appBase
3791
- const imps: ModuleImports = { appBase: appImportBase, pkgBase: rawImps.pkgBase }
3792
- commandLoaderEntries.push(...collectCommandLoaderEntries(roots, imps, modId))
3780
+ for (const discovered of discovery.modules) {
3781
+ const { modId, roots, imps, appImportBase } = discovered
3782
+ commandLoaderEntries.push(...discovered.commandLoaderEntries)
3793
3783
 
3794
3784
  let cliImportName: string | null = null
3795
3785
  const translations: GeneratedObjectEntry[] = []
@@ -3807,7 +3797,7 @@ export async function generateModuleRegistryCli(options: ModuleRegistryOptions):
3807
3797
  let integrationImportName: string | null = null
3808
3798
 
3809
3799
  // Module metadata: index.ts (overrideable)
3810
- const indexResolved = resolveModuleFile(roots, imps, 'index.ts')
3800
+ const indexResolved = discovered.resolve('index.ts')
3811
3801
  if (indexResolved) {
3812
3802
  infoImportName = `I${importIdRef.value++}_${toVar(modId)}`
3813
3803
  const importPath = sanitizeGeneratedModuleSpecifier(indexResolved.importPath)
@@ -3823,19 +3813,19 @@ export async function generateModuleRegistryCli(options: ModuleRegistryOptions):
3823
3813
 
3824
3814
  // Module setup configuration: setup.ts
3825
3815
  {
3826
- const setup = resolveConventionFile(roots, imps, 'setup.ts', 'SETUP', modId, importIdRef, imports)
3816
+ const setup = resolveConventionFile(discovered.resolve('setup.ts'), 'SETUP', modId, importIdRef, imports)
3827
3817
  if (setup) setupImportName = setup.importName
3828
3818
  }
3829
3819
 
3830
3820
  // Module encryption defaults: encryption.ts
3831
3821
  {
3832
- const encryption = resolveConventionFile(roots, imps, 'encryption.ts', 'ENCRYPTION', modId, importIdRef, imports)
3822
+ const encryption = resolveConventionFile(discovered.resolve('encryption.ts'), 'ENCRYPTION', modId, importIdRef, imports)
3833
3823
  if (encryption) encryptionImportName = encryption.importName
3834
3824
  }
3835
3825
 
3836
3826
  // Integration manifest: integration.ts
3837
3827
  {
3838
- const resolved = resolveModuleFile(roots, imps, 'integration.ts')
3828
+ const resolved = discovered.resolve('integration.ts')
3839
3829
  if (resolved) {
3840
3830
  const importName = `INTEGRATION_${toVar(modId)}_${importIdRef.value++}`
3841
3831
  imports.push(buildImportStatement(`* as ${importName}`, resolved.importPath))
@@ -3845,13 +3835,13 @@ export async function generateModuleRegistryCli(options: ModuleRegistryOptions):
3845
3835
 
3846
3836
  // Entity extensions: data/extensions.ts
3847
3837
  {
3848
- const ext = resolveConventionFile(roots, imps, 'data/extensions.ts', 'X', modId, importIdRef, imports)
3838
+ const ext = resolveConventionFile(discovered.resolve('data/extensions.ts'), 'X', modId, importIdRef, imports)
3849
3839
  if (ext) extensionsImportName = ext.importName
3850
3840
  }
3851
3841
 
3852
3842
  // RBAC: acl.ts
3853
3843
  {
3854
- const aclResolved = resolveModuleFile(roots, imps, 'acl.ts')
3844
+ const aclResolved = discovered.resolve('acl.ts')
3855
3845
  if (aclResolved) {
3856
3846
  const importName = `ACL_${toVar(modId)}_${importIdRef.value++}`
3857
3847
  const importPath = sanitizeGeneratedModuleSpecifier(aclResolved.importPath)
@@ -3862,25 +3852,25 @@ export async function generateModuleRegistryCli(options: ModuleRegistryOptions):
3862
3852
 
3863
3853
  // Custom entities: ce.ts
3864
3854
  {
3865
- const ce = resolveConventionFile(roots, imps, 'ce.ts', 'CE', modId, importIdRef, imports)
3855
+ const ce = resolveConventionFile(discovered.resolve('ce.ts'), 'CE', modId, importIdRef, imports)
3866
3856
  if (ce) customEntitiesImportName = ce.importName
3867
3857
  }
3868
3858
 
3869
3859
  // Vector search configuration: vector.ts
3870
3860
  {
3871
- const vec = resolveConventionFile(roots, imps, 'vector.ts', 'VECTOR', modId, importIdRef, imports)
3861
+ const vec = resolveConventionFile(discovered.resolve('vector.ts'), 'VECTOR', modId, importIdRef, imports)
3872
3862
  if (vec) vectorImportName = vec.importName
3873
3863
  }
3874
3864
 
3875
3865
  // Custom fields: data/fields.ts
3876
3866
  {
3877
- const fields = resolveConventionFile(roots, imps, 'data/fields.ts', 'F', modId, importIdRef, imports)
3867
+ const fields = resolveConventionFile(discovered.resolve('data/fields.ts'), 'F', modId, importIdRef, imports)
3878
3868
  if (fields) fieldsImportName = fields.importName
3879
3869
  }
3880
3870
 
3881
3871
  // CLI
3882
3872
  {
3883
- const cliResolved = resolveModuleFile(roots, imps, 'cli.ts')
3873
+ const cliResolved = discovered.resolve('cli.ts')
3884
3874
  if (cliResolved) {
3885
3875
  const importName = `CLI_${toVar(modId)}`
3886
3876
  imports.push(buildImportStatement(importName, sanitizeGeneratedModuleSpecifier(cliResolved.importPath)))
@@ -3890,7 +3880,7 @@ export async function generateModuleRegistryCli(options: ModuleRegistryOptions):
3890
3880
 
3891
3881
  // Translations
3892
3882
  translations.push(...processTranslationsAst({
3893
- roots,
3883
+ discovered: discovered.translations,
3894
3884
  modId,
3895
3885
  appImportBase,
3896
3886
  pkgImportBase: imps.pkgBase,
@@ -3898,30 +3888,14 @@ export async function generateModuleRegistryCli(options: ModuleRegistryOptions):
3898
3888
  }))
3899
3889
 
3900
3890
  // Subscribers
3901
- subscribers.push(...await processSubscribersAst({
3902
- roots,
3903
- modId,
3904
- appImportBase,
3905
- pkgImportBase: imps.pkgBase,
3906
- }))
3891
+ subscribers.push(...processSubscribersAst(await discovered.getSubscribers()))
3907
3892
 
3908
3893
  // Workers
3909
- workers.push(...await processWorkersAst({
3910
- roots,
3911
- modId,
3912
- appImportBase,
3913
- pkgImportBase: imps.pkgBase,
3914
- }))
3894
+ workers.push(...processWorkersAst(await discovered.getWorkers()))
3915
3895
 
3916
3896
  // Dashboard widgets
3917
3897
  {
3918
- const entries = scanDashboardWidgetEntries({
3919
- modId,
3920
- roots,
3921
- appImportBase,
3922
- pkgImportBase: imps.pkgBase,
3923
- })
3924
- dashboardWidgetsValue = buildModuleDashboardWidgetsValue(entries)
3898
+ dashboardWidgetsValue = buildModuleDashboardWidgetsValue(discovered.dashboardWidgetEntries)
3925
3899
  }
3926
3900
 
3927
3901
  const moduleEntries: GeneratedObjectEntry[] = [
@@ -4079,7 +4053,7 @@ export async function generateModuleRegistryCli(options: ModuleRegistryOptions):
4079
4053
  }
4080
4054
  }
4081
4055
 
4082
- const structureChecksum = calculateStructureChecksum(Array.from(trackedRoots))
4056
+ const structureChecksum = discovery.getStructureChecksum()
4083
4057
  writeGeneratedFile({ outFile, checksumFile, content: output, structureChecksum, result, quiet })
4084
4058
  writeGeneratedFile({ outFile: legacyOutFile, checksumFile: legacyChecksumFile, content: legacyOutput, structureChecksum, result, quiet })
4085
4059
  const commandLoadersOutput = renderCommandLoadersFile(commandLoaderEntries)
@@ -4087,3 +4061,27 @@ export async function generateModuleRegistryCli(options: ModuleRegistryOptions):
4087
4061
 
4088
4062
  return result
4089
4063
  }
4064
+
4065
+ export async function generateModuleRegistry(options: ModuleRegistryOptions): Promise<GeneratorResult> {
4066
+ const discovery = await createModuleRegistryDiscovery(options.resolver)
4067
+ return generateModuleRegistryFromDiscovery({ ...options, discovery })
4068
+ }
4069
+
4070
+ export async function generateModuleRegistryApp(options: ModuleRegistryOptions): Promise<GeneratorResult> {
4071
+ const discovery = await createModuleRegistryDiscovery(options.resolver)
4072
+ return generateModuleRegistryAppFromDiscovery({ ...options, discovery })
4073
+ }
4074
+
4075
+ export async function generateModuleRegistryCli(options: ModuleRegistryOptions): Promise<GeneratorResult> {
4076
+ const discovery = await createModuleRegistryDiscovery(options.resolver)
4077
+ return generateModuleRegistryCliFromDiscovery({ ...options, discovery })
4078
+ }
4079
+
4080
+ export async function generateModuleRegistries(options: ModuleRegistryOptions): Promise<GeneratorResult[]> {
4081
+ const discovery = await createModuleRegistryDiscovery(options.resolver)
4082
+ return [
4083
+ await generateModuleRegistryFromDiscovery({ ...options, discovery }),
4084
+ await generateModuleRegistryAppFromDiscovery({ ...options, discovery }),
4085
+ await generateModuleRegistryCliFromDiscovery({ ...options, discovery }),
4086
+ ]
4087
+ }