@open-mercato/cli 0.6.7-develop.6631.1.9d64b49ddc → 0.6.7-develop.6646.1.be515f3d49

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 (59) hide show
  1. package/dist/agentic/guides/module-facts.json +53 -53
  2. package/dist/agentic/guides/modules/ai_assistant.md +1 -1
  3. package/dist/agentic/guides/modules/api_docs.md +1 -1
  4. package/dist/agentic/guides/modules/api_keys.md +1 -1
  5. package/dist/agentic/guides/modules/attachments.md +1 -1
  6. package/dist/agentic/guides/modules/audit_logs.md +1 -1
  7. package/dist/agentic/guides/modules/auth.md +1 -1
  8. package/dist/agentic/guides/modules/business_rules.md +1 -1
  9. package/dist/agentic/guides/modules/catalog.md +1 -1
  10. package/dist/agentic/guides/modules/channel_gmail.md +1 -1
  11. package/dist/agentic/guides/modules/channel_imap.md +1 -1
  12. package/dist/agentic/guides/modules/checkout.md +1 -1
  13. package/dist/agentic/guides/modules/communication_channels.md +1 -1
  14. package/dist/agentic/guides/modules/configs.md +1 -1
  15. package/dist/agentic/guides/modules/content.md +1 -1
  16. package/dist/agentic/guides/modules/currencies.md +1 -1
  17. package/dist/agentic/guides/modules/customer_accounts.md +1 -1
  18. package/dist/agentic/guides/modules/customers.md +1 -1
  19. package/dist/agentic/guides/modules/dashboards.md +1 -1
  20. package/dist/agentic/guides/modules/data_sync.md +1 -1
  21. package/dist/agentic/guides/modules/dictionaries.md +1 -1
  22. package/dist/agentic/guides/modules/directory.md +1 -1
  23. package/dist/agentic/guides/modules/entities.md +1 -1
  24. package/dist/agentic/guides/modules/events.md +1 -1
  25. package/dist/agentic/guides/modules/feature_toggles.md +1 -1
  26. package/dist/agentic/guides/modules/gateway_stripe.md +1 -1
  27. package/dist/agentic/guides/modules/generators.md +1 -1
  28. package/dist/agentic/guides/modules/inbox_ops.md +1 -1
  29. package/dist/agentic/guides/modules/integrations.md +1 -1
  30. package/dist/agentic/guides/modules/messages.md +1 -1
  31. package/dist/agentic/guides/modules/notifications.md +1 -1
  32. package/dist/agentic/guides/modules/onboarding.md +1 -1
  33. package/dist/agentic/guides/modules/payment_gateways.md +1 -1
  34. package/dist/agentic/guides/modules/perspectives.md +1 -1
  35. package/dist/agentic/guides/modules/planner.md +1 -1
  36. package/dist/agentic/guides/modules/portal.md +1 -1
  37. package/dist/agentic/guides/modules/progress.md +1 -1
  38. package/dist/agentic/guides/modules/query_index.md +1 -1
  39. package/dist/agentic/guides/modules/record_locks.md +1 -1
  40. package/dist/agentic/guides/modules/resources.md +1 -1
  41. package/dist/agentic/guides/modules/sales.md +1 -1
  42. package/dist/agentic/guides/modules/scheduler.md +1 -1
  43. package/dist/agentic/guides/modules/search.md +1 -1
  44. package/dist/agentic/guides/modules/security.md +1 -1
  45. package/dist/agentic/guides/modules/shipping_carriers.md +1 -1
  46. package/dist/agentic/guides/modules/sso.md +1 -1
  47. package/dist/agentic/guides/modules/staff.md +1 -1
  48. package/dist/agentic/guides/modules/storage_s3.md +1 -1
  49. package/dist/agentic/guides/modules/sync_akeneo.md +1 -1
  50. package/dist/agentic/guides/modules/sync_excel.md +1 -1
  51. package/dist/agentic/guides/modules/system_status_overlays.md +1 -1
  52. package/dist/agentic/guides/modules/translations.md +1 -1
  53. package/dist/agentic/guides/modules/webhooks.md +1 -1
  54. package/dist/agentic/guides/modules/workflows.md +1 -1
  55. package/dist/lib/generators/module-registry.js +32 -6
  56. package/dist/lib/generators/module-registry.js.map +2 -2
  57. package/package.json +5 -5
  58. package/src/lib/generators/__tests__/route-collision-guard.test.ts +123 -0
  59. package/src/lib/generators/module-registry.ts +46 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/cli",
3
- "version": "0.6.7-develop.6631.1.9d64b49ddc",
3
+ "version": "0.6.7-develop.6646.1.be515f3d49",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -60,8 +60,8 @@
60
60
  "@mikro-orm/decorators": "^7.1.5",
61
61
  "@mikro-orm/migrations": "^7.1.5",
62
62
  "@mikro-orm/postgresql": "^7.1.5",
63
- "@open-mercato/queue": "0.6.7-develop.6631.1.9d64b49ddc",
64
- "@open-mercato/shared": "0.6.7-develop.6631.1.9d64b49ddc",
63
+ "@open-mercato/queue": "0.6.7-develop.6646.1.be515f3d49",
64
+ "@open-mercato/shared": "0.6.7-develop.6646.1.be515f3d49",
65
65
  "cross-spawn": "^7.0.6",
66
66
  "pg": "8.22.0",
67
67
  "semver": "^7.8.5",
@@ -72,10 +72,10 @@
72
72
  "typescript-js": "npm:typescript@6.0.3"
73
73
  },
74
74
  "peerDependencies": {
75
- "@open-mercato/shared": "0.6.7-develop.6631.1.9d64b49ddc"
75
+ "@open-mercato/shared": "0.6.7-develop.6646.1.be515f3d49"
76
76
  },
77
77
  "devDependencies": {
78
- "@open-mercato/shared": "0.6.7-develop.6631.1.9d64b49ddc",
78
+ "@open-mercato/shared": "0.6.7-develop.6646.1.be515f3d49",
79
79
  "@types/jest": "^30.0.0",
80
80
  "jest": "^30.4.2",
81
81
  "ts-jest": "^29.4.11"
@@ -0,0 +1,123 @@
1
+ // Tests for the backend route-collision guard in `mercato generate`
2
+ // (`assertUniqueBackendRoutePattern`): two modules emitting the same backend URL
3
+ // must fail generation, while distinct patterns must keep generating unchanged.
4
+ import fs from 'node:fs'
5
+ import path from 'node:path'
6
+ import os from 'node:os'
7
+ import type { PackageResolver, ModuleEntry } from '../../resolver'
8
+ import { generateModuleRegistry, generateModuleRegistryApp } from '../module-registry'
9
+
10
+ let tmpDir: string
11
+ let outputDir: string
12
+
13
+ function touchFile(filePath: string, content = ''): void {
14
+ fs.mkdirSync(path.dirname(filePath), { recursive: true })
15
+ fs.writeFileSync(filePath, content)
16
+ }
17
+
18
+ function pkgModulePath(modId: string, ...segments: string[]): string {
19
+ return path.join(tmpDir, 'packages', 'core', 'src', 'modules', modId, ...segments)
20
+ }
21
+
22
+ function createMockResolver(enabled: ModuleEntry[]): PackageResolver {
23
+ return {
24
+ isMonorepo: () => true,
25
+ getRootDir: () => tmpDir,
26
+ getAppDir: () => path.join(tmpDir, 'app'),
27
+ getOutputDir: () => outputDir,
28
+ getModulesConfigPath: () => path.join(tmpDir, 'app', 'src', 'modules.ts'),
29
+ discoverPackages: () => [],
30
+ loadEnabledModules: () => enabled,
31
+ getModulePaths: (entry: ModuleEntry) => ({
32
+ appBase: path.join(tmpDir, 'app', 'src', 'modules', entry.id),
33
+ pkgBase: path.join(tmpDir, 'packages', 'core', 'src', 'modules', entry.id),
34
+ }),
35
+ getModuleImportBase: (entry: ModuleEntry) => ({
36
+ appBase: `@/modules/${entry.id}`,
37
+ pkgBase: `@open-mercato/core/modules/${entry.id}`,
38
+ }),
39
+ getPackageOutputDir: () => outputDir,
40
+ getPackageRoot: () => path.join(tmpDir, 'packages', 'core'),
41
+ }
42
+ }
43
+
44
+ // Minimal core module: index metadata + a single backend page at the given sub-path.
45
+ function makeCoreModule(modId: string, backendPageSegments: string[]): ModuleEntry {
46
+ touchFile(pkgModulePath(modId, 'index.ts'), `export const metadata = { id: '${modId}', label: '${modId}' }\n`)
47
+ touchFile(
48
+ pkgModulePath(modId, 'backend', ...backendPageSegments, 'page.tsx'),
49
+ `export default function ${modId}Page() { return null }\n`,
50
+ )
51
+ return { id: modId, from: '@open-mercato/core' }
52
+ }
53
+
54
+ beforeEach(() => {
55
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'route-collision-'))
56
+ outputDir = path.join(tmpDir, 'app', '.mercato', 'generated')
57
+ fs.mkdirSync(outputDir, { recursive: true })
58
+ })
59
+
60
+ afterEach(() => {
61
+ fs.rmSync(tmpDir, { recursive: true, force: true })
62
+ })
63
+
64
+ describe('backend route-collision guard', () => {
65
+ it('throws when two modules generate the same backend pattern (string/registry path)', async () => {
66
+ // Both leave the detail page un-namespaced → both resolve to `/backend/[id]`.
67
+ const enabled = [
68
+ makeCoreModule('alpha', ['[id]']),
69
+ makeCoreModule('beta', ['[id]']),
70
+ ]
71
+ const resolver = createMockResolver(enabled)
72
+
73
+ await expect(generateModuleRegistry({ resolver, quiet: true })).rejects.toThrow(
74
+ /Duplicate backend route pattern "\/backend\/\[id\]"/,
75
+ )
76
+ })
77
+
78
+ it('throws for the same collision in the app (AST) generator', async () => {
79
+ const enabled = [
80
+ makeCoreModule('alpha', ['[id]']),
81
+ makeCoreModule('beta', ['[id]']),
82
+ ]
83
+ const resolver = createMockResolver(enabled)
84
+
85
+ await expect(generateModuleRegistryApp({ resolver, quiet: true })).rejects.toThrow(
86
+ /Duplicate backend route pattern/,
87
+ )
88
+ })
89
+
90
+ it('names both colliding modules in the error', async () => {
91
+ const enabled = [
92
+ makeCoreModule('alpha', ['[id]']),
93
+ makeCoreModule('beta', ['[id]']),
94
+ ]
95
+ const resolver = createMockResolver(enabled)
96
+
97
+ await expect(generateModuleRegistry({ resolver, quiet: true })).rejects.toThrow(/alpha/)
98
+ const resolver2 = createMockResolver(enabled)
99
+ await expect(generateModuleRegistry({ resolver: resolver2, quiet: true })).rejects.toThrow(/beta/)
100
+ })
101
+
102
+ it('does not throw when backend pages are namespaced under their module folder', async () => {
103
+ // Distinct patterns: `/backend/alpha/[id]` and `/backend/beta/[id]`.
104
+ const enabled = [
105
+ makeCoreModule('alpha', ['alpha', '[id]']),
106
+ makeCoreModule('beta', ['beta', '[id]']),
107
+ ]
108
+ const resolver = createMockResolver(enabled)
109
+
110
+ await expect(generateModuleRegistry({ resolver, quiet: true })).resolves.toBeDefined()
111
+
112
+ const generated = fs.readFileSync(path.join(outputDir, 'backend-routes.generated.ts'), 'utf8')
113
+ expect(generated).toContain('/backend/alpha/[id]')
114
+ expect(generated).toContain('/backend/beta/[id]')
115
+ })
116
+
117
+ it('does not throw for a single module owning a pattern (no false positive)', async () => {
118
+ const enabled = [makeCoreModule('alpha', ['[id]'])]
119
+ const resolver = createMockResolver(enabled)
120
+
121
+ await expect(generateModuleRegistry({ resolver, quiet: true })).resolves.toBeDefined()
122
+ })
123
+ })
@@ -144,6 +144,29 @@ type PageRouteGenerationResult = {
144
144
  eagerRoutes: string[]
145
145
  runtimeRoutes: string[]
146
146
  manifestRoutes: string[]
147
+ routePatterns: string[]
148
+ }
149
+
150
+ // Fail loud when two modules generate the same backend URL. Folder-derived
151
+ // patterns collide only when a page is left un-namespaced (e.g. `backend/[id]`
152
+ // instead of `backend/<mod>/[id]`), silently shadowing another module at runtime.
153
+ // Mirrors the existing duplicate-command-id guard.
154
+ function assertUniqueBackendRoutePattern(
155
+ seen: Map<string, string>,
156
+ moduleId: string,
157
+ pattern: string,
158
+ ): void {
159
+ if (!pattern.startsWith('/backend/')) return
160
+ const previous = seen.get(pattern)
161
+ if (previous && previous !== moduleId) {
162
+ throw new Error(
163
+ `[generate] Duplicate backend route pattern "${pattern}" generated by modules ` +
164
+ `"${previous}" and "${moduleId}". Each backend page must resolve to a unique URL — ` +
165
+ `nest the page under a module-specific folder (e.g. backend/${moduleId}/...) so it ` +
166
+ `does not silently shadow another module.`,
167
+ )
168
+ }
169
+ seen.set(pattern, moduleId)
147
170
  }
148
171
 
149
172
  type ApiRouteGenerationResult = {
@@ -1721,6 +1744,7 @@ async function processPageFiles(options: {
1721
1744
  const eagerRoutes: string[] = []
1722
1745
  const runtimeRoutes: string[] = []
1723
1746
  const manifestRoutes: string[] = []
1747
+ const routePatterns: string[] = []
1724
1748
 
1725
1749
  // Next-style page.* files
1726
1750
  for (const { relPath, fromApp } of files.filter(({ relPath: f }) => isModulePageFile(path.basename(f)))) {
@@ -1792,6 +1816,7 @@ async function processPageFiles(options: {
1792
1816
  eagerRoutes.push(`{ ${baseProps}, Component: ${buildLazyRouteComponentExpression(importPath)} }`)
1793
1817
  runtimeRoutes.push(`{ ${runtimeBaseProps}, Component: ${buildLazyRouteComponentExpression(importPath)} }`)
1794
1818
  manifestRoutes.push(`{ moduleId: ${toLiteral(modId)}, ${manifestBaseProps}, load: async () => { const mod = await ${buildDynamicImportExpression(importPath)}; return (mod.default ?? mod) as any } }`)
1819
+ routePatterns.push(routePath)
1795
1820
  }
1796
1821
 
1797
1822
  // Back-compat direct files (old-style pages like login.tsx instead of login/page.tsx)
@@ -1870,12 +1895,14 @@ async function processPageFiles(options: {
1870
1895
  eagerRoutes.push(`{ ${baseProps}, Component: ${buildLazyRouteComponentExpression(importPath)} }`)
1871
1896
  runtimeRoutes.push(`{ ${runtimeBaseProps}, Component: ${buildLazyRouteComponentExpression(importPath)} }`)
1872
1897
  manifestRoutes.push(`{ moduleId: ${toLiteral(modId)}, ${manifestBaseProps}, load: async () => { const mod = await ${buildDynamicImportExpression(importPath)}; return (mod.default ?? mod) as any } }`)
1898
+ routePatterns.push(routePath)
1873
1899
  }
1874
1900
 
1875
1901
  return {
1876
1902
  eagerRoutes,
1877
1903
  runtimeRoutes,
1878
1904
  manifestRoutes,
1905
+ routePatterns,
1879
1906
  }
1880
1907
  }
1881
1908
 
@@ -2540,7 +2567,7 @@ async function processPageFilesAst(options: {
2540
2567
  pkgImportBase: string
2541
2568
  imports: string[]
2542
2569
  importIdRef: { value: number }
2543
- }): Promise<WriterFunction[]> {
2570
+ }): Promise<{ routes: WriterFunction[]; routePatterns: string[] }> {
2544
2571
  const {
2545
2572
  files,
2546
2573
  type,
@@ -2553,6 +2580,7 @@ async function processPageFilesAst(options: {
2553
2580
  importIdRef,
2554
2581
  } = options
2555
2582
  const routes: WriterFunction[] = []
2583
+ const routePatterns: string[] = []
2556
2584
  const metaPrefix = type === 'frontend' ? 'MRuntime' : 'BMRuntime'
2557
2585
 
2558
2586
  const addRoute = (params: {
@@ -2569,6 +2597,7 @@ async function processPageFilesAst(options: {
2569
2597
  },
2570
2598
  ]),
2571
2599
  )
2600
+ routePatterns.push(params.routePath)
2572
2601
  }
2573
2602
 
2574
2603
  for (const { relPath, fromApp } of files.filter(({ relPath: filePath }) => isModulePageFile(path.basename(filePath)))) {
@@ -2663,7 +2692,7 @@ async function processPageFilesAst(options: {
2663
2692
  })
2664
2693
  }
2665
2694
 
2666
- return routes
2695
+ return { routes, routePatterns }
2667
2696
  }
2668
2697
 
2669
2698
  function processSubscribersAst(discovered: DiscoveredSubscriber[]): WriterFunction[] {
@@ -2866,6 +2895,8 @@ async function generateModuleRegistryFromDiscovery(options: ModuleRegistryRender
2866
2895
  aclPath?: string
2867
2896
  }> = []
2868
2897
 
2898
+ const seenBackendRoutePatterns = new Map<string, string>()
2899
+
2869
2900
  for (const discovered of discovery.modules) {
2870
2901
  const { modId, roots, imps, appImportBase } = discovered
2871
2902
  commandLoaderEntries.push(...discovered.commandLoaderEntries)
@@ -3050,6 +3081,9 @@ async function generateModuleRegistryFromDiscovery(options: ModuleRegistryRender
3050
3081
  backendRoutes.push(...generatedBackendRoutes.eagerRoutes)
3051
3082
  runtimeBackendRoutes.push(...generatedBackendRoutes.runtimeRoutes)
3052
3083
  backendRouteManifestDecls.push(...generatedBackendRoutes.manifestRoutes)
3084
+ for (const pattern of generatedBackendRoutes.routePatterns) {
3085
+ assertUniqueBackendRoutePattern(seenBackendRoutePatterns, modId, pattern)
3086
+ }
3053
3087
  hasRouteComponents = true
3054
3088
  }
3055
3089
  }
@@ -3431,6 +3465,7 @@ async function generateModuleRegistryAppFromDiscovery(options: ModuleRegistryRen
3431
3465
  const requiresByModule = new Map<string, string[]>()
3432
3466
  const commandLoaderEntries: CommandLoaderGenerationEntry[] = []
3433
3467
  let hasRouteComponents = false
3468
+ const seenBackendRoutePatterns = new Map<string, string>()
3434
3469
 
3435
3470
  for (const discovered of discovery.modules) {
3436
3471
  const { modId, roots, imps, appImportBase } = discovered
@@ -3519,7 +3554,7 @@ async function generateModuleRegistryAppFromDiscovery(options: ModuleRegistryRen
3519
3554
  const fePkg = path.join(roots.pkgBase, 'frontend')
3520
3555
  const feFiles = discovered.frontendFiles
3521
3556
  if (feFiles.length > 0) {
3522
- frontendRoutes.push(...await processPageFilesAst({
3557
+ const feAst = await processPageFilesAst({
3523
3558
  files: feFiles,
3524
3559
  type: 'frontend',
3525
3560
  modId,
@@ -3529,7 +3564,8 @@ async function generateModuleRegistryAppFromDiscovery(options: ModuleRegistryRen
3529
3564
  pkgImportBase: imps.pkgBase,
3530
3565
  imports,
3531
3566
  importIdRef,
3532
- }))
3567
+ })
3568
+ frontendRoutes.push(...feAst.routes)
3533
3569
  hasRouteComponents = true
3534
3570
  }
3535
3571
  }
@@ -3539,7 +3575,7 @@ async function generateModuleRegistryAppFromDiscovery(options: ModuleRegistryRen
3539
3575
  const bePkg = path.join(roots.pkgBase, 'backend')
3540
3576
  const beFiles = discovered.backendFiles
3541
3577
  if (beFiles.length > 0) {
3542
- backendRoutes.push(...await processPageFilesAst({
3578
+ const beAst = await processPageFilesAst({
3543
3579
  files: beFiles,
3544
3580
  type: 'backend',
3545
3581
  modId,
@@ -3549,7 +3585,11 @@ async function generateModuleRegistryAppFromDiscovery(options: ModuleRegistryRen
3549
3585
  pkgImportBase: imps.pkgBase,
3550
3586
  imports,
3551
3587
  importIdRef,
3552
- }))
3588
+ })
3589
+ backendRoutes.push(...beAst.routes)
3590
+ for (const pattern of beAst.routePatterns) {
3591
+ assertUniqueBackendRoutePattern(seenBackendRoutePatterns, modId, pattern)
3592
+ }
3553
3593
  hasRouteComponents = true
3554
3594
  }
3555
3595
  }