@open-mercato/cli 0.7.1-develop.7191.1.3c9346566b → 0.7.1-develop.7194.1.ab4fc81f82
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/.turbo/turbo-build.log +6 -2
- package/dist/agentic/github-copilot/copilot-instructions.md.template +37 -0
- package/dist/agentic/github-copilot/instructions/entity-guard.instructions.md +27 -0
- package/dist/agentic/github-copilot/instructions/generated-guard.instructions.md +10 -0
- package/dist/agentic/github-copilot/mcp.json.example +13 -0
- package/dist/agentic/guides/module-facts.json +11527 -283
- package/dist/agentic/guides/module-facts.v2.json +11527 -283
- package/dist/agentic/guides/modules/agent_orchestrator/acl-features.md +31 -0
- package/dist/agentic/guides/modules/agent_orchestrator/active-extension-bindings.md +12 -0
- package/dist/agentic/guides/modules/agent_orchestrator/ai-agents.md +12 -0
- package/dist/agentic/guides/modules/agent_orchestrator/ai-tools-mcp-capabilities.md +13 -0
- package/dist/agentic/guides/modules/agent_orchestrator/backend-pages.md +28 -0
- package/dist/agentic/guides/modules/agent_orchestrator/cli-commands.md +14 -0
- package/dist/agentic/guides/modules/agent_orchestrator/contribution-resolutions.md +49 -0
- package/dist/agentic/guides/modules/agent_orchestrator/di-registrations-rich.md +42 -0
- package/dist/agentic/guides/modules/agent_orchestrator/di-service-tokens.md +24 -0
- package/dist/agentic/guides/modules/agent_orchestrator/domain-commands.md +28 -0
- package/dist/agentic/guides/modules/agent_orchestrator/encryption.md +20 -0
- package/dist/agentic/guides/modules/agent_orchestrator/entities.md +31 -0
- package/dist/agentic/guides/modules/agent_orchestrator/events.md +38 -0
- package/dist/agentic/guides/modules/agent_orchestrator/exact-override-targets.md +122 -0
- package/dist/agentic/guides/modules/agent_orchestrator/host-extension-points.md +10 -0
- package/dist/agentic/guides/modules/agent_orchestrator/index.md +31 -0
- package/dist/agentic/guides/modules/agent_orchestrator/owned-contract-module-metadata.md +11 -0
- package/dist/agentic/guides/modules/agent_orchestrator/setup.md +11 -0
- package/dist/agentic/guides/modules/agent_orchestrator/umes-contributions.md +49 -0
- package/dist/agentic/guides/modules/agent_orchestrator/umes-hosts.md +61 -0
- package/dist/agentic/guides/modules/agent_orchestrator/workers.md +14 -0
- package/dist/agentic/guides/modules/ai_assistant/di-registrations-rich.md +3 -2
- package/dist/agentic/guides/modules/ai_assistant/di-service-tokens.md +2 -1
- package/dist/agentic/guides/modules/ai_assistant/exact-override-targets.md +3 -2
- package/dist/agentic/guides/modules/ai_assistant/index.md +1 -1
- package/dist/agentic/guides/modules/api_docs/index.md +1 -1
- package/dist/agentic/guides/modules/api_keys/index.md +1 -1
- package/dist/agentic/guides/modules/attachments/index.md +1 -1
- package/dist/agentic/guides/modules/audit_logs/index.md +1 -1
- package/dist/agentic/guides/modules/auth/index.md +1 -1
- package/dist/agentic/guides/modules/business_rules/index.md +1 -1
- package/dist/agentic/guides/modules/catalog/index.md +1 -1
- package/dist/agentic/guides/modules/channel_apns/index.md +1 -1
- package/dist/agentic/guides/modules/channel_discord/index.md +1 -1
- package/dist/agentic/guides/modules/channel_expo/index.md +1 -1
- package/dist/agentic/guides/modules/channel_fcm/index.md +1 -1
- package/dist/agentic/guides/modules/channel_gmail/index.md +1 -1
- package/dist/agentic/guides/modules/channel_imap/index.md +1 -1
- package/dist/agentic/guides/modules/channel_resend/index.md +1 -1
- package/dist/agentic/guides/modules/channel_ses/index.md +1 -1
- package/dist/agentic/guides/modules/checkout/index.md +1 -1
- package/dist/agentic/guides/modules/communication_channels/index.md +1 -1
- package/dist/agentic/guides/modules/configs/index.md +1 -1
- package/dist/agentic/guides/modules/content/index.md +1 -1
- package/dist/agentic/guides/modules/currencies/index.md +1 -1
- package/dist/agentic/guides/modules/customer_accounts/cli-commands.md +11 -0
- package/dist/agentic/guides/modules/customer_accounts/exact-override-targets.md +5 -4
- package/dist/agentic/guides/modules/customer_accounts/index.md +3 -2
- package/dist/agentic/guides/modules/customer_accounts/setup.md +1 -1
- package/dist/agentic/guides/modules/customers/contribution-resolutions.md +1 -0
- package/dist/agentic/guides/modules/customers/exact-override-targets.md +1 -0
- package/dist/agentic/guides/modules/customers/incoming-installed-contributions.md +3 -0
- package/dist/agentic/guides/modules/customers/index.md +2 -2
- package/dist/agentic/guides/modules/customers/umes-contributions.md +1 -0
- package/dist/agentic/guides/modules/dashboards/index.md +1 -1
- package/dist/agentic/guides/modules/data_sync/index.md +1 -1
- package/dist/agentic/guides/modules/design_system/index.md +1 -1
- package/dist/agentic/guides/modules/devices/index.md +1 -1
- package/dist/agentic/guides/modules/dictionaries/index.md +1 -1
- package/dist/agentic/guides/modules/directory/index.md +1 -1
- package/dist/agentic/guides/modules/documents/index.md +1 -1
- package/dist/agentic/guides/modules/entities/index.md +1 -1
- package/dist/agentic/guides/modules/eudr/index.md +1 -1
- package/dist/agentic/guides/modules/events/index.md +1 -1
- package/dist/agentic/guides/modules/feature_toggles/index.md +1 -1
- package/dist/agentic/guides/modules/gateway_stripe/index.md +1 -1
- package/dist/agentic/guides/modules/generators/index.md +1 -1
- package/dist/agentic/guides/modules/inbox_ops/index.md +1 -1
- package/dist/agentic/guides/modules/integrations/index.md +1 -1
- package/dist/agentic/guides/modules/messages/index.md +1 -1
- package/dist/agentic/guides/modules/notifications/index.md +1 -1
- package/dist/agentic/guides/modules/onboarding/index.md +1 -1
- package/dist/agentic/guides/modules/payment_gateways/index.md +1 -1
- package/dist/agentic/guides/modules/perspectives/index.md +1 -1
- package/dist/agentic/guides/modules/phone_calls/index.md +1 -1
- package/dist/agentic/guides/modules/planner/index.md +1 -1
- package/dist/agentic/guides/modules/portal/index.md +1 -1
- package/dist/agentic/guides/modules/progress/index.md +1 -1
- package/dist/agentic/guides/modules/push_notifications/index.md +1 -1
- package/dist/agentic/guides/modules/query_index/index.md +1 -1
- package/dist/agentic/guides/modules/record_locks/index.md +1 -1
- package/dist/agentic/guides/modules/resources/index.md +1 -1
- package/dist/agentic/guides/modules/sales/index.md +1 -1
- package/dist/agentic/guides/modules/scheduler/index.md +1 -1
- package/dist/agentic/guides/modules/search/index.md +1 -1
- package/dist/agentic/guides/modules/security/index.md +1 -1
- package/dist/agentic/guides/modules/seeds/cli-commands.md +14 -0
- package/dist/agentic/guides/modules/seeds/exact-override-targets.md +14 -0
- package/dist/agentic/guides/modules/seeds/index.md +14 -0
- package/dist/agentic/guides/modules/seeds/owned-contract-module-metadata.md +11 -0
- package/dist/agentic/guides/modules/shipping_carriers/index.md +1 -1
- package/dist/agentic/guides/modules/sso/index.md +1 -1
- package/dist/agentic/guides/modules/staff/index.md +1 -1
- package/dist/agentic/guides/modules/storage_s3/index.md +1 -1
- package/dist/agentic/guides/modules/sync_akeneo/index.md +1 -1
- package/dist/agentic/guides/modules/sync_excel/index.md +1 -1
- package/dist/agentic/guides/modules/system_status_overlays/index.md +1 -1
- package/dist/agentic/guides/modules/tillio/index.md +1 -1
- package/dist/agentic/guides/modules/translations/index.md +1 -1
- package/dist/agentic/guides/modules/warranty_claims/index.md +1 -1
- package/dist/agentic/guides/modules/webhooks/index.md +1 -1
- package/dist/agentic/guides/modules/wms/index.md +1 -1
- package/dist/agentic/guides/modules/workflows/acl-features.md +11 -1
- package/dist/agentic/guides/modules/workflows/active-extension-bindings.md +4 -1
- package/dist/agentic/guides/modules/workflows/ai-tools-mcp-capabilities.md +17 -0
- package/dist/agentic/guides/modules/workflows/backend-pages.md +3 -0
- package/dist/agentic/guides/modules/workflows/contribution-resolutions.md +24 -0
- package/dist/agentic/guides/modules/workflows/di-registrations-rich.md +11 -7
- package/dist/agentic/guides/modules/workflows/di-service-tokens.md +11 -7
- package/dist/agentic/guides/modules/workflows/domain-commands.md +11 -0
- package/dist/agentic/guides/modules/workflows/entities.md +2 -0
- package/dist/agentic/guides/modules/workflows/events.md +14 -8
- package/dist/agentic/guides/modules/workflows/exact-override-targets.md +45 -10
- package/dist/agentic/guides/modules/workflows/frontend-pages.md +2 -0
- package/dist/agentic/guides/modules/workflows/incoming-installed-contributions.md +17 -0
- package/dist/agentic/guides/modules/workflows/index.md +15 -12
- package/dist/agentic/guides/modules/workflows/notifications.md +2 -0
- package/dist/agentic/guides/modules/workflows/setup.md +1 -1
- package/dist/agentic/guides/modules/workflows/umes-contributions.md +24 -0
- package/dist/agentic/guides/modules/workflows/umes-hosts.md +14 -6
- package/dist/agentic/guides/modules/workflows/workers.md +4 -1
- package/dist/agentic/guides/reference-module-facts.json +2 -2
- package/dist/agentic/guides/reference-modules/example/index.md +1 -1
- package/dist/agentic/guides/upstream/AGENTS.md +3 -1
- package/dist/agentic/guides/upstream/manifest.json +2 -2
- package/dist/agentic/shared/ai/harness/design-system-inventory.json +118 -106
- package/dist/agentic/shared/ai/harness/source-link-inventory.json +8 -8
- package/dist/lib/agentic-init.js +4 -0
- package/dist/lib/agentic-init.js.map +2 -2
- package/dist/lib/agentic-setup.js +19 -5
- package/dist/lib/agentic-setup.js.map +2 -2
- package/dist/lib/generators/extensions/agent-files.js +913 -0
- package/dist/lib/generators/extensions/agent-files.js.map +7 -0
- package/dist/lib/generators/extensions/index.js +2 -0
- package/dist/lib/generators/extensions/index.js.map +2 -2
- package/dist/lib/generators/index.js +3 -1
- package/dist/lib/generators/index.js.map +2 -2
- package/dist/lib/generators/module-registry.js +40 -11
- package/dist/lib/generators/module-registry.js.map +2 -2
- package/dist/lib/generators/route-collisions.js +35 -0
- package/dist/lib/generators/route-collisions.js.map +7 -0
- package/dist/lib/generators/web-research-adapters.js +95 -0
- package/dist/lib/generators/web-research-adapters.js.map +7 -0
- package/dist/mercato.js +5 -2
- package/dist/mercato.js.map +2 -2
- package/package.json +6 -6
- package/src/__tests__/agent-files.test.ts +199 -0
- package/src/__tests__/mercato.test.ts +14 -0
- package/src/lib/__tests__/agentic-init.test.ts +11 -0
- package/src/lib/__tests__/agentic-setup.ownership.test.ts +5 -0
- package/src/lib/agentic-init.ts +4 -0
- package/src/lib/agentic-setup.ts +30 -5
- package/src/lib/generators/__tests__/agent-files-extension.test.ts +477 -0
- package/src/lib/generators/__tests__/module-facts.bc-guard.test.ts +25 -8
- package/src/lib/generators/__tests__/route-collisions.test.ts +155 -0
- package/src/lib/generators/extensions/agent-files.ts +1332 -0
- package/src/lib/generators/extensions/index.ts +2 -0
- package/src/lib/generators/index.ts +1 -0
- package/src/lib/generators/module-registry.ts +66 -17
- package/src/lib/generators/route-collisions.ts +57 -0
- package/src/lib/generators/web-research-adapters.ts +133 -0
- package/src/mercato.ts +4 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { GeneratorExtension } from '../extension'
|
|
2
|
+
import { createAgentFilesExtension } from './agent-files'
|
|
2
3
|
import { createAiAgentsExtension } from './ai-agents'
|
|
3
4
|
import { createAiToolsExtension } from './ai-tools'
|
|
4
5
|
import { createAnalyticsExtension } from './analytics'
|
|
@@ -25,6 +26,7 @@ export function loadGeneratorExtensions(): GeneratorExtension[] {
|
|
|
25
26
|
createMessagesExtension(),
|
|
26
27
|
createAiToolsExtension(),
|
|
27
28
|
createAiAgentsExtension(),
|
|
29
|
+
createAgentFilesExtension(),
|
|
28
30
|
createEventsExtension(),
|
|
29
31
|
createAnalyticsExtension(),
|
|
30
32
|
createTranslatableFieldsExtension(),
|
|
@@ -10,3 +10,4 @@ export { generateModuleEntities, type ModuleEntitiesOptions } from './module-ent
|
|
|
10
10
|
export { generateModuleDi, type ModuleDiOptions } from './module-di'
|
|
11
11
|
export { generateModulePackageSources, type ModulePackageSourcesOptions } from './module-package-sources'
|
|
12
12
|
export { generateOpenApi, type GenerateOpenApiOptions } from './openapi'
|
|
13
|
+
export { generateWebResearchAdapters, type WebResearchAdaptersOptions } from './web-research-adapters'
|
|
@@ -59,6 +59,7 @@ import {
|
|
|
59
59
|
namespaceFallback,
|
|
60
60
|
namespaceImportSpec,
|
|
61
61
|
} from './extensions/shared'
|
|
62
|
+
import { detectBackendRouteCollisions, type BackendRouteRegistration } from './route-collisions'
|
|
62
63
|
import {
|
|
63
64
|
scanModuleDir,
|
|
64
65
|
resolveModuleFile,
|
|
@@ -118,6 +119,7 @@ type ModuleRegistryDiscoveryEntry = {
|
|
|
118
119
|
roots: ModuleRoots
|
|
119
120
|
imps: ModuleImports
|
|
120
121
|
appImportBase: string
|
|
122
|
+
isAppModule: boolean
|
|
121
123
|
resolve: (relativePath: string) => ResolvedFile | null
|
|
122
124
|
frontendFiles: ScannedFile[]
|
|
123
125
|
backendFiles: ScannedFile[]
|
|
@@ -171,22 +173,39 @@ type PageRouteGenerationResult = {
|
|
|
171
173
|
// patterns collide only when a page is left un-namespaced (e.g. `backend/[id]`
|
|
172
174
|
// instead of `backend/<mod>/[id]`), silently shadowing another module at runtime.
|
|
173
175
|
// Mirrors the existing duplicate-command-id guard.
|
|
176
|
+
type BackendRoutePatternOwner = { moduleId: string; isAppModule: boolean }
|
|
177
|
+
|
|
174
178
|
function assertUniqueBackendRoutePattern(
|
|
175
|
-
seen: Map<string,
|
|
179
|
+
seen: Map<string, BackendRoutePatternOwner>,
|
|
176
180
|
moduleId: string,
|
|
177
181
|
pattern: string,
|
|
182
|
+
isAppModule: boolean,
|
|
178
183
|
): void {
|
|
179
184
|
if (!pattern.startsWith('/backend/')) return
|
|
180
185
|
const previous = seen.get(pattern)
|
|
181
|
-
if (previous && previous !== moduleId) {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
186
|
+
if (previous && previous.moduleId !== moduleId) {
|
|
187
|
+
// An app-local module shadowing a package module's page is a supported
|
|
188
|
+
// override — the app entry wins at runtime, which is the whole point of
|
|
189
|
+
// putting a module in `apps/<app>/src/modules/`. Only a collision where
|
|
190
|
+
// neither side can predictably win is fatal: package-vs-package (no
|
|
191
|
+
// precedence between them) or app-vs-app (the same app twice).
|
|
192
|
+
const isIntentionalAppShadow = previous.isAppModule !== isAppModule
|
|
193
|
+
if (!isIntentionalAppShadow) {
|
|
194
|
+
const message =
|
|
195
|
+
`[generate] Duplicate backend route pattern "${pattern}" generated by modules ` +
|
|
196
|
+
`"${previous.moduleId}" and "${moduleId}". Each backend page must resolve to a unique URL — ` +
|
|
197
|
+
`nest the page under a module-specific folder (e.g. backend/${moduleId}/...) so it ` +
|
|
198
|
+
`does not silently shadow another module.`
|
|
199
|
+
// Print before throwing: this guard short-circuits the aggregate collision
|
|
200
|
+
// sweep that would otherwise report the offending path and modules, and an
|
|
201
|
+
// operator needs the detail whichever guard fires first.
|
|
202
|
+
console.error(message)
|
|
203
|
+
throw new Error(message)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// The app module owns the pattern once it shadows, so a package module seen
|
|
207
|
+
// afterwards is measured against the app entry rather than replacing it.
|
|
208
|
+
if (!previous || isAppModule) seen.set(pattern, { moduleId, isAppModule })
|
|
190
209
|
}
|
|
191
210
|
|
|
192
211
|
type ApiRouteGenerationResult = {
|
|
@@ -2056,6 +2075,7 @@ async function createModuleRegistryDiscovery(
|
|
|
2056
2075
|
roots,
|
|
2057
2076
|
imps,
|
|
2058
2077
|
appImportBase,
|
|
2078
|
+
isAppModule: entry.from === '@app',
|
|
2059
2079
|
resolve,
|
|
2060
2080
|
frontendFiles: scanModuleDir(roots, SCAN_CONFIGS.frontendPages),
|
|
2061
2081
|
backendFiles: scanModuleDir(roots, SCAN_CONFIGS.backendPages),
|
|
@@ -2121,9 +2141,10 @@ async function processPageFiles(options: {
|
|
|
2121
2141
|
runtimeImports: string[]
|
|
2122
2142
|
manifestImports?: string[]
|
|
2123
2143
|
importIdRef: { value: number }
|
|
2144
|
+
collectRoute?: (registration: { routePath: string; fromApp: boolean }) => void
|
|
2124
2145
|
quiet?: boolean
|
|
2125
2146
|
}): Promise<PageRouteGenerationResult> {
|
|
2126
|
-
const { files, type, modId, appDir, pkgDir, appImportBase, pkgImportBase, eagerImports, runtimeImports, manifestImports, importIdRef, quiet } = options
|
|
2147
|
+
const { files, type, modId, appDir, pkgDir, appImportBase, pkgImportBase, eagerImports, runtimeImports, manifestImports, importIdRef, collectRoute, quiet } = options
|
|
2127
2148
|
const metaPrefix = type === 'frontend' ? 'M' : 'BM'
|
|
2128
2149
|
const eagerRoutes: string[] = []
|
|
2129
2150
|
const runtimeRoutes: string[] = []
|
|
@@ -2148,6 +2169,7 @@ async function processPageFiles(options: {
|
|
|
2148
2169
|
const moduleBaseDir = path.join(fromApp ? appDir : pkgDir, ...segs)
|
|
2149
2170
|
const sourceFile = findExistingModuleFile(moduleBaseDir, pageFile)
|
|
2150
2171
|
if (!sourceFile || !hasDefaultExport(sourceFile)) continue
|
|
2172
|
+
collectRoute?.({ routePath, fromApp })
|
|
2151
2173
|
const metaPath = findExistingModuleFileByBaseNames(moduleBaseDir, ['page.meta', 'meta'])
|
|
2152
2174
|
warnIfPageMetaMissingMetadataExport(metaPath, quiet)
|
|
2153
2175
|
let metaExpr = 'undefined'
|
|
@@ -2245,6 +2267,7 @@ async function processPageFiles(options: {
|
|
|
2245
2267
|
const moduleBaseDir = path.join(fromApp ? appDir : pkgDir, ...segs)
|
|
2246
2268
|
const sourceFile = findExistingModuleFile(moduleBaseDir, file)
|
|
2247
2269
|
if (!sourceFile || !hasDefaultExport(sourceFile)) continue
|
|
2270
|
+
collectRoute?.({ routePath, fromApp })
|
|
2248
2271
|
const metaPath = findExistingModuleFileByBaseNames(moduleBaseDir, [`${name}.meta`, 'meta'])
|
|
2249
2272
|
let metaExpr = 'undefined'
|
|
2250
2273
|
let runtimeMetaExpr = 'undefined'
|
|
@@ -3609,6 +3632,7 @@ async function generateModuleRegistryFromDiscovery(options: ModuleRegistryRender
|
|
|
3609
3632
|
const importIdRef = { value: 0 }
|
|
3610
3633
|
const requiresByModule = new Map<string, string[]>()
|
|
3611
3634
|
const commandLoaderEntries: CommandLoaderGenerationEntry[] = []
|
|
3635
|
+
const backendRouteRegistrations: BackendRouteRegistration[] = []
|
|
3612
3636
|
let hasRouteComponents = false
|
|
3613
3637
|
let hasAbandonHookWorkers = false
|
|
3614
3638
|
|
|
@@ -3620,10 +3644,10 @@ async function generateModuleRegistryFromDiscovery(options: ModuleRegistryRender
|
|
|
3620
3644
|
aclPath?: string
|
|
3621
3645
|
}> = []
|
|
3622
3646
|
|
|
3623
|
-
const seenBackendRoutePatterns = new Map<string,
|
|
3647
|
+
const seenBackendRoutePatterns = new Map<string, BackendRoutePatternOwner>()
|
|
3624
3648
|
|
|
3625
3649
|
for (const discovered of discovery.modules) {
|
|
3626
|
-
const { modId, roots, imps, appImportBase } = discovered
|
|
3650
|
+
const { modId, roots, imps, appImportBase, isAppModule } = discovered
|
|
3627
3651
|
commandLoaderEntries.push(...discovered.commandLoaderEntries)
|
|
3628
3652
|
|
|
3629
3653
|
const frontendRoutes: string[] = []
|
|
@@ -3814,6 +3838,13 @@ async function generateModuleRegistryFromDiscovery(options: ModuleRegistryRender
|
|
|
3814
3838
|
runtimeImports,
|
|
3815
3839
|
manifestImports: backendRouteManifestImports,
|
|
3816
3840
|
importIdRef,
|
|
3841
|
+
collectRoute: ({ routePath, fromApp }) => {
|
|
3842
|
+
backendRouteRegistrations.push({
|
|
3843
|
+
routePath,
|
|
3844
|
+
moduleId: modId,
|
|
3845
|
+
packageSourced: !fromApp && !isAppModule,
|
|
3846
|
+
})
|
|
3847
|
+
},
|
|
3817
3848
|
quiet,
|
|
3818
3849
|
})
|
|
3819
3850
|
backendRoutes.push(...generatedBackendRoutes.eagerRoutes)
|
|
@@ -3822,7 +3853,7 @@ async function generateModuleRegistryFromDiscovery(options: ModuleRegistryRender
|
|
|
3822
3853
|
backendRouteShardEntries.push(...generatedBackendRoutes.manifestShardRoutes)
|
|
3823
3854
|
backendRouteMetadataEntries.push(...generatedBackendRoutes.metadataRoutes)
|
|
3824
3855
|
for (const pattern of generatedBackendRoutes.routePatterns) {
|
|
3825
|
-
assertUniqueBackendRoutePattern(seenBackendRoutePatterns, modId, pattern)
|
|
3856
|
+
assertUniqueBackendRoutePattern(seenBackendRoutePatterns, modId, pattern, isAppModule)
|
|
3826
3857
|
}
|
|
3827
3858
|
hasRouteComponents = true
|
|
3828
3859
|
}
|
|
@@ -4111,6 +4142,24 @@ async function generateModuleRegistryFromDiscovery(options: ModuleRegistryRender
|
|
|
4111
4142
|
}
|
|
4112
4143
|
}
|
|
4113
4144
|
|
|
4145
|
+
// Backend route collision detection: duplicate paths between package modules are
|
|
4146
|
+
// always a bug (one silently shadows the other at runtime); app-module shadowing
|
|
4147
|
+
// of a package page is the supported override mechanism and only logs a note.
|
|
4148
|
+
{
|
|
4149
|
+
const collisionResult = detectBackendRouteCollisions(backendRouteRegistrations)
|
|
4150
|
+
for (const note of collisionResult.notes) {
|
|
4151
|
+
if (!quiet) console.log(`[Route Shadow] ${note}`)
|
|
4152
|
+
}
|
|
4153
|
+
if (collisionResult.errors.length > 0) {
|
|
4154
|
+
for (const error of collisionResult.errors) {
|
|
4155
|
+
console.error(`\x1b[31m[Route Collision]\x1b[0m ${error}`)
|
|
4156
|
+
}
|
|
4157
|
+
throw new Error(
|
|
4158
|
+
`Backend route collision detection found ${collisionResult.errors.length} duplicate path(s) registered by multiple package modules. Rename the colliding page directories before proceeding.`
|
|
4159
|
+
)
|
|
4160
|
+
}
|
|
4161
|
+
}
|
|
4162
|
+
|
|
4114
4163
|
const output = renderAstModuleRegistryFile({
|
|
4115
4164
|
fileName: 'modules.generated.ts',
|
|
4116
4165
|
generator: 'registry',
|
|
@@ -4326,10 +4375,10 @@ async function generateModuleRegistryAppFromDiscovery(options: ModuleRegistryRen
|
|
|
4326
4375
|
const commandLoaderEntries: CommandLoaderGenerationEntry[] = []
|
|
4327
4376
|
let hasRouteComponents = false
|
|
4328
4377
|
let hasAbandonHookWorkers = false
|
|
4329
|
-
const seenBackendRoutePatterns = new Map<string,
|
|
4378
|
+
const seenBackendRoutePatterns = new Map<string, BackendRoutePatternOwner>()
|
|
4330
4379
|
|
|
4331
4380
|
for (const discovered of discovery.modules) {
|
|
4332
|
-
const { modId, roots, imps, appImportBase } = discovered
|
|
4381
|
+
const { modId, roots, imps, appImportBase, isAppModule } = discovered
|
|
4333
4382
|
commandLoaderEntries.push(...discovered.commandLoaderEntries)
|
|
4334
4383
|
|
|
4335
4384
|
const frontendRoutes: WriterFunction[] = []
|
|
@@ -4460,7 +4509,7 @@ async function generateModuleRegistryAppFromDiscovery(options: ModuleRegistryRen
|
|
|
4460
4509
|
})
|
|
4461
4510
|
backendRoutes.push(...beAst.routes)
|
|
4462
4511
|
for (const pattern of beAst.routePatterns) {
|
|
4463
|
-
assertUniqueBackendRoutePattern(seenBackendRoutePatterns, modId, pattern)
|
|
4512
|
+
assertUniqueBackendRoutePattern(seenBackendRoutePatterns, modId, pattern, isAppModule)
|
|
4464
4513
|
}
|
|
4465
4514
|
hasRouteComponents = true
|
|
4466
4515
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export type BackendRouteRegistration = {
|
|
2
|
+
routePath: string
|
|
3
|
+
moduleId: string
|
|
4
|
+
packageSourced: boolean
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export type BackendRouteCollisionResult = {
|
|
8
|
+
errors: string[]
|
|
9
|
+
notes: string[]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function detectBackendRouteCollisions(
|
|
13
|
+
registrations: BackendRouteRegistration[],
|
|
14
|
+
): BackendRouteCollisionResult {
|
|
15
|
+
const byPath = new Map<string, BackendRouteRegistration[]>()
|
|
16
|
+
for (const registration of registrations) {
|
|
17
|
+
const existing = byPath.get(registration.routePath)
|
|
18
|
+
if (existing) existing.push(registration)
|
|
19
|
+
else byPath.set(registration.routePath, [registration])
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const errors: string[] = []
|
|
23
|
+
const notes: string[] = []
|
|
24
|
+
for (const [routePath, entries] of byPath) {
|
|
25
|
+
const moduleIds = new Set(entries.map((entry) => entry.moduleId))
|
|
26
|
+
if (moduleIds.size < 2) continue
|
|
27
|
+
|
|
28
|
+
const packageModuleIds = Array.from(
|
|
29
|
+
new Set(entries.filter((entry) => entry.packageSourced).map((entry) => entry.moduleId)),
|
|
30
|
+
).sort((a, b) => a.localeCompare(b))
|
|
31
|
+
|
|
32
|
+
if (packageModuleIds.length >= 2) {
|
|
33
|
+
errors.push(
|
|
34
|
+
`Backend route "${routePath}" is registered by ${packageModuleIds.length} package modules: ${packageModuleIds
|
|
35
|
+
.map((id) => `"${id}"`)
|
|
36
|
+
.join(', ')}. Duplicate backend paths between package modules are always a bug — rename one module's page directory.`,
|
|
37
|
+
)
|
|
38
|
+
continue
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const appModuleIds = Array.from(
|
|
42
|
+
new Set(entries.filter((entry) => !entry.packageSourced).map((entry) => entry.moduleId)),
|
|
43
|
+
).sort((a, b) => a.localeCompare(b))
|
|
44
|
+
const shadowedId = packageModuleIds[0]
|
|
45
|
+
notes.push(
|
|
46
|
+
shadowedId
|
|
47
|
+
? `Backend route "${routePath}" from package module "${shadowedId}" is shadowed by app module(s) ${appModuleIds
|
|
48
|
+
.map((id) => `"${id}"`)
|
|
49
|
+
.join(', ')} (intentional override mechanism).`
|
|
50
|
+
: `Backend route "${routePath}" is registered by multiple app modules: ${appModuleIds
|
|
51
|
+
.map((id) => `"${id}"`)
|
|
52
|
+
.join(', ')}.`,
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { errors, notes }
|
|
57
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import type { PackageResolver } from '../resolver'
|
|
4
|
+
import { calculateStructureChecksum, createGeneratorResult, type GeneratorResult, writeGeneratedFile } from '../utils'
|
|
5
|
+
|
|
6
|
+
export interface WebResearchAdaptersOptions {
|
|
7
|
+
resolver: PackageResolver
|
|
8
|
+
quiet?: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Mirrors `openMercato.webResearchAdapter` in `@open-mercato/web-research`. It is
|
|
13
|
+
* duplicated rather than imported because the CLI must not depend on the engine
|
|
14
|
+
* package to run its generators.
|
|
15
|
+
*/
|
|
16
|
+
const MANIFEST_NAMESPACE = 'openMercato'
|
|
17
|
+
const ADAPTER_MANIFEST_KEY = 'webResearchAdapter'
|
|
18
|
+
|
|
19
|
+
type DiscoveredAdapter = {
|
|
20
|
+
packageName: string
|
|
21
|
+
adapterId: string
|
|
22
|
+
sourceRoot: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function readJson(file: string): unknown {
|
|
26
|
+
try {
|
|
27
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
28
|
+
} catch {
|
|
29
|
+
return null
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function readManifest(packageJsonPath: string): DiscoveredAdapter | null {
|
|
34
|
+
const parsed = readJson(packageJsonPath)
|
|
35
|
+
if (typeof parsed !== 'object' || parsed === null) return null
|
|
36
|
+
const pkg = parsed as Record<string, unknown>
|
|
37
|
+
const namespace = pkg[MANIFEST_NAMESPACE]
|
|
38
|
+
if (typeof namespace !== 'object' || namespace === null) return null
|
|
39
|
+
const manifest = (namespace as Record<string, unknown>)[ADAPTER_MANIFEST_KEY]
|
|
40
|
+
if (typeof manifest !== 'object' || manifest === null) return null
|
|
41
|
+
const adapterId = (manifest as Record<string, unknown>).id
|
|
42
|
+
const packageName = pkg.name
|
|
43
|
+
if (typeof packageName !== 'string' || packageName.length === 0) return null
|
|
44
|
+
if (typeof adapterId !== 'string' || adapterId.length === 0) return null
|
|
45
|
+
return { packageName, adapterId, sourceRoot: path.dirname(packageJsonPath) }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function scanDirectory(root: string, results: Map<string, DiscoveredAdapter>): void {
|
|
49
|
+
if (!fs.existsSync(root)) return
|
|
50
|
+
let entries: fs.Dirent[]
|
|
51
|
+
try {
|
|
52
|
+
entries = fs.readdirSync(root, { withFileTypes: true })
|
|
53
|
+
} catch {
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
for (const entry of entries) {
|
|
57
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue
|
|
58
|
+
if (entry.name === '.bin' || entry.name === '.cache') continue
|
|
59
|
+
const child = path.join(root, entry.name)
|
|
60
|
+
// Scoped packages nest one level deeper (`@scope/name`).
|
|
61
|
+
if (entry.name.startsWith('@')) {
|
|
62
|
+
scanDirectory(child, results)
|
|
63
|
+
continue
|
|
64
|
+
}
|
|
65
|
+
const discovered = readManifest(path.join(child, 'package.json'))
|
|
66
|
+
if (discovered && !results.has(discovered.packageName)) results.set(discovered.packageName, discovered)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function identifierFor(index: number): string {
|
|
71
|
+
return `adapter${index}`
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Emits the statically imported adapter registry. Discovery happens here, at
|
|
76
|
+
* build time, because a bundler cannot follow `import()` with a runtime-computed
|
|
77
|
+
* specifier — a dynamic registry would resolve to nothing under Turbopack.
|
|
78
|
+
*/
|
|
79
|
+
export async function generateWebResearchAdapters(
|
|
80
|
+
options: WebResearchAdaptersOptions,
|
|
81
|
+
): Promise<GeneratorResult> {
|
|
82
|
+
const { resolver, quiet } = options
|
|
83
|
+
const result = createGeneratorResult()
|
|
84
|
+
const outputDir = resolver.getOutputDir()
|
|
85
|
+
const outFile = path.join(outputDir, 'web-research-adapters.generated.ts')
|
|
86
|
+
const checksumFile = path.join(outputDir, 'web-research-adapters.checksum')
|
|
87
|
+
|
|
88
|
+
const appDir = resolver.getAppDir()
|
|
89
|
+
const repoRoot = path.resolve(appDir, '..', '..')
|
|
90
|
+
const discovered = new Map<string, DiscoveredAdapter>()
|
|
91
|
+
const scanRoots = [
|
|
92
|
+
path.join(repoRoot, 'packages'),
|
|
93
|
+
path.join(repoRoot, 'node_modules'),
|
|
94
|
+
path.join(appDir, 'node_modules'),
|
|
95
|
+
]
|
|
96
|
+
for (const root of scanRoots) scanDirectory(root, discovered)
|
|
97
|
+
|
|
98
|
+
const adapters = [...discovered.values()].sort((left, right) =>
|
|
99
|
+
left.packageName.localeCompare(right.packageName),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
const imports = adapters
|
|
103
|
+
.map((adapter, index) => `import * as ${identifierFor(index)} from '${adapter.packageName}'`)
|
|
104
|
+
.join('\n')
|
|
105
|
+
const entries = adapters
|
|
106
|
+
.map(
|
|
107
|
+
(adapter, index) =>
|
|
108
|
+
` { packageName: '${adapter.packageName}', module: ${identifierFor(index)} },`,
|
|
109
|
+
)
|
|
110
|
+
.join('\n')
|
|
111
|
+
|
|
112
|
+
const content = `// AUTO-GENERATED — do not edit by hand.
|
|
113
|
+
// Source: packages declaring \`${MANIFEST_NAMESPACE}.${ADAPTER_MANIFEST_KEY}\` in package.json.
|
|
114
|
+
// Regenerate with: yarn generate
|
|
115
|
+
import type { AdapterRegistryEntry } from '@open-mercato/web-research'
|
|
116
|
+
${imports}
|
|
117
|
+
|
|
118
|
+
export const webResearchAdapterEntries: AdapterRegistryEntry[] = [
|
|
119
|
+
${entries}
|
|
120
|
+
]
|
|
121
|
+
`
|
|
122
|
+
|
|
123
|
+
writeGeneratedFile({
|
|
124
|
+
outFile,
|
|
125
|
+
checksumFile,
|
|
126
|
+
content,
|
|
127
|
+
structureChecksum: calculateStructureChecksum(adapters.map((adapter) => adapter.sourceRoot)),
|
|
128
|
+
result,
|
|
129
|
+
quiet,
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
return result
|
|
133
|
+
}
|
package/src/mercato.ts
CHANGED
|
@@ -790,6 +790,7 @@ async function runGeneratorSuite(quiet: boolean): Promise<boolean> {
|
|
|
790
790
|
generateModuleDi,
|
|
791
791
|
generateModulePackageSources,
|
|
792
792
|
generateOpenApi,
|
|
793
|
+
generateWebResearchAdapters,
|
|
793
794
|
} = await import('./lib/generators')
|
|
794
795
|
const resolver = createResolver()
|
|
795
796
|
const results = [
|
|
@@ -798,6 +799,7 @@ async function runGeneratorSuite(quiet: boolean): Promise<boolean> {
|
|
|
798
799
|
await generateModuleEntities({ resolver, quiet }),
|
|
799
800
|
await generateModuleDi({ resolver, quiet }),
|
|
800
801
|
await generateModulePackageSources({ resolver, quiet }),
|
|
802
|
+
await generateWebResearchAdapters({ resolver, quiet }),
|
|
801
803
|
await generateOpenApi({ resolver, quiet }),
|
|
802
804
|
]
|
|
803
805
|
return results.some((result) => (result?.filesWritten.length ?? 0) > 0)
|
|
@@ -1089,13 +1091,14 @@ export async function run(argv = process.argv) {
|
|
|
1089
1091
|
// Step 1: Run generators directly (no process spawn)
|
|
1090
1092
|
console.log('🔧 Preparing modules (registry, entities, DI)...')
|
|
1091
1093
|
const { createResolver } = await import('./lib/resolver')
|
|
1092
|
-
const { generateEntityIds, generateModuleRegistries, generateModuleEntities, generateModuleDi, generateModulePackageSources, generateOpenApi } = await import('./lib/generators')
|
|
1094
|
+
const { generateEntityIds, generateModuleRegistries, generateModuleEntities, generateModuleDi, generateModulePackageSources, generateOpenApi, generateWebResearchAdapters } = await import('./lib/generators')
|
|
1093
1095
|
const resolver = createResolver()
|
|
1094
1096
|
await generateEntityIds({ resolver, quiet: true })
|
|
1095
1097
|
await generateModuleRegistries({ resolver, quiet: true })
|
|
1096
1098
|
await generateModuleEntities({ resolver, quiet: true })
|
|
1097
1099
|
await generateModuleDi({ resolver, quiet: true })
|
|
1098
1100
|
await generateModulePackageSources({ resolver, quiet: true })
|
|
1101
|
+
await generateWebResearchAdapters({ resolver, quiet: true })
|
|
1099
1102
|
await generateOpenApi({ resolver, quiet: true })
|
|
1100
1103
|
console.log('✅ Modules prepared\n')
|
|
1101
1104
|
|