@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.
- package/dist/agentic/guides/module-facts.json +53 -53
- package/dist/agentic/guides/modules/ai_assistant.md +1 -1
- package/dist/agentic/guides/modules/api_docs.md +1 -1
- package/dist/agentic/guides/modules/api_keys.md +1 -1
- package/dist/agentic/guides/modules/attachments.md +1 -1
- package/dist/agentic/guides/modules/audit_logs.md +1 -1
- package/dist/agentic/guides/modules/auth.md +1 -1
- package/dist/agentic/guides/modules/business_rules.md +1 -1
- package/dist/agentic/guides/modules/catalog.md +1 -1
- package/dist/agentic/guides/modules/channel_gmail.md +1 -1
- package/dist/agentic/guides/modules/channel_imap.md +1 -1
- package/dist/agentic/guides/modules/checkout.md +1 -1
- package/dist/agentic/guides/modules/communication_channels.md +1 -1
- package/dist/agentic/guides/modules/configs.md +1 -1
- package/dist/agentic/guides/modules/content.md +1 -1
- package/dist/agentic/guides/modules/currencies.md +1 -1
- package/dist/agentic/guides/modules/customer_accounts.md +1 -1
- package/dist/agentic/guides/modules/customers.md +1 -1
- package/dist/agentic/guides/modules/dashboards.md +1 -1
- package/dist/agentic/guides/modules/data_sync.md +1 -1
- package/dist/agentic/guides/modules/dictionaries.md +1 -1
- package/dist/agentic/guides/modules/directory.md +1 -1
- package/dist/agentic/guides/modules/entities.md +1 -1
- package/dist/agentic/guides/modules/events.md +1 -1
- package/dist/agentic/guides/modules/feature_toggles.md +1 -1
- package/dist/agentic/guides/modules/gateway_stripe.md +1 -1
- package/dist/agentic/guides/modules/generators.md +1 -1
- package/dist/agentic/guides/modules/inbox_ops.md +1 -1
- package/dist/agentic/guides/modules/integrations.md +1 -1
- package/dist/agentic/guides/modules/messages.md +1 -1
- package/dist/agentic/guides/modules/notifications.md +1 -1
- package/dist/agentic/guides/modules/onboarding.md +1 -1
- package/dist/agentic/guides/modules/payment_gateways.md +1 -1
- package/dist/agentic/guides/modules/perspectives.md +1 -1
- package/dist/agentic/guides/modules/planner.md +1 -1
- package/dist/agentic/guides/modules/portal.md +1 -1
- package/dist/agentic/guides/modules/progress.md +1 -1
- package/dist/agentic/guides/modules/query_index.md +1 -1
- package/dist/agentic/guides/modules/record_locks.md +1 -1
- package/dist/agentic/guides/modules/resources.md +1 -1
- package/dist/agentic/guides/modules/sales.md +1 -1
- package/dist/agentic/guides/modules/scheduler.md +1 -1
- package/dist/agentic/guides/modules/search.md +1 -1
- package/dist/agentic/guides/modules/security.md +1 -1
- package/dist/agentic/guides/modules/shipping_carriers.md +1 -1
- package/dist/agentic/guides/modules/sso.md +1 -1
- package/dist/agentic/guides/modules/staff.md +1 -1
- package/dist/agentic/guides/modules/storage_s3.md +1 -1
- package/dist/agentic/guides/modules/sync_akeneo.md +1 -1
- package/dist/agentic/guides/modules/sync_excel.md +1 -1
- package/dist/agentic/guides/modules/system_status_overlays.md +1 -1
- package/dist/agentic/guides/modules/translations.md +1 -1
- package/dist/agentic/guides/modules/webhooks.md +1 -1
- package/dist/agentic/guides/modules/workflows.md +1 -1
- package/dist/lib/generators/module-registry.js +32 -6
- package/dist/lib/generators/module-registry.js.map +2 -2
- package/package.json +5 -5
- package/src/lib/generators/__tests__/route-collision-guard.test.ts +123 -0
- 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.
|
|
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.
|
|
64
|
-
"@open-mercato/shared": "0.6.7-develop.
|
|
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.
|
|
75
|
+
"@open-mercato/shared": "0.6.7-develop.6646.1.be515f3d49"
|
|
76
76
|
},
|
|
77
77
|
"devDependencies": {
|
|
78
|
-
"@open-mercato/shared": "0.6.7-develop.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|