@open-mercato/cli 0.10.1-develop.7990.1.82d0a183c5 → 0.10.1-develop.7994.1.c9f31972aa

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 (162) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/agentic/guides/module-facts.json +162 -162
  3. package/dist/agentic/guides/module-facts.v2.json +162 -162
  4. package/dist/agentic/guides/module-tool.md +21 -0
  5. package/dist/agentic/guides/modules/agent_orchestrator/index.md +1 -1
  6. package/dist/agentic/guides/modules/ai_assistant/index.md +1 -1
  7. package/dist/agentic/guides/modules/api_docs/index.md +1 -1
  8. package/dist/agentic/guides/modules/api_keys/index.md +1 -1
  9. package/dist/agentic/guides/modules/attachments/index.md +1 -1
  10. package/dist/agentic/guides/modules/audit_logs/index.md +1 -1
  11. package/dist/agentic/guides/modules/auth/index.md +1 -1
  12. package/dist/agentic/guides/modules/availability/index.md +1 -1
  13. package/dist/agentic/guides/modules/business_rules/index.md +1 -1
  14. package/dist/agentic/guides/modules/carrier_inpost/index.md +1 -1
  15. package/dist/agentic/guides/modules/cart/index.md +1 -1
  16. package/dist/agentic/guides/modules/catalog/index.md +1 -1
  17. package/dist/agentic/guides/modules/channel_apns/index.md +1 -1
  18. package/dist/agentic/guides/modules/channel_discord/index.md +1 -1
  19. package/dist/agentic/guides/modules/channel_expo/index.md +1 -1
  20. package/dist/agentic/guides/modules/channel_fcm/index.md +1 -1
  21. package/dist/agentic/guides/modules/channel_gmail/index.md +1 -1
  22. package/dist/agentic/guides/modules/channel_imap/index.md +1 -1
  23. package/dist/agentic/guides/modules/channel_resend/index.md +1 -1
  24. package/dist/agentic/guides/modules/channel_ses/index.md +1 -1
  25. package/dist/agentic/guides/modules/channel_smtp/index.md +1 -1
  26. package/dist/agentic/guides/modules/checkout/index.md +1 -1
  27. package/dist/agentic/guides/modules/communication_channels/index.md +1 -1
  28. package/dist/agentic/guides/modules/configs/index.md +1 -1
  29. package/dist/agentic/guides/modules/content/index.md +1 -1
  30. package/dist/agentic/guides/modules/currencies/index.md +1 -1
  31. package/dist/agentic/guides/modules/customer_accounts/index.md +1 -1
  32. package/dist/agentic/guides/modules/customer_groups/index.md +1 -1
  33. package/dist/agentic/guides/modules/customers/index.md +1 -1
  34. package/dist/agentic/guides/modules/dashboards/index.md +1 -1
  35. package/dist/agentic/guides/modules/data_sync/index.md +1 -1
  36. package/dist/agentic/guides/modules/design_system/index.md +1 -1
  37. package/dist/agentic/guides/modules/devices/index.md +1 -1
  38. package/dist/agentic/guides/modules/dictionaries/index.md +1 -1
  39. package/dist/agentic/guides/modules/directory/index.md +1 -1
  40. package/dist/agentic/guides/modules/documents/index.md +1 -1
  41. package/dist/agentic/guides/modules/ecommerce/index.md +1 -1
  42. package/dist/agentic/guides/modules/entities/index.md +1 -1
  43. package/dist/agentic/guides/modules/eudr/index.md +1 -1
  44. package/dist/agentic/guides/modules/events/index.md +1 -1
  45. package/dist/agentic/guides/modules/feature_toggles/index.md +1 -1
  46. package/dist/agentic/guides/modules/forms/index.md +1 -1
  47. package/dist/agentic/guides/modules/gateway_autopay/index.md +1 -1
  48. package/dist/agentic/guides/modules/gateway_stripe/index.md +1 -1
  49. package/dist/agentic/guides/modules/gateway_tpay/index.md +1 -1
  50. package/dist/agentic/guides/modules/generators/index.md +1 -1
  51. package/dist/agentic/guides/modules/inbox_ops/index.md +1 -1
  52. package/dist/agentic/guides/modules/integrations/index.md +1 -1
  53. package/dist/agentic/guides/modules/messages/index.md +1 -1
  54. package/dist/agentic/guides/modules/notifications/index.md +1 -1
  55. package/dist/agentic/guides/modules/onboarding/index.md +1 -1
  56. package/dist/agentic/guides/modules/payment_gateways/index.md +1 -1
  57. package/dist/agentic/guides/modules/perspectives/index.md +1 -1
  58. package/dist/agentic/guides/modules/phone_calls/index.md +1 -1
  59. package/dist/agentic/guides/modules/planner/index.md +1 -1
  60. package/dist/agentic/guides/modules/portal/index.md +1 -1
  61. package/dist/agentic/guides/modules/progress/index.md +1 -1
  62. package/dist/agentic/guides/modules/promotions/index.md +1 -1
  63. package/dist/agentic/guides/modules/push_notifications/index.md +1 -1
  64. package/dist/agentic/guides/modules/query_index/index.md +1 -1
  65. package/dist/agentic/guides/modules/record_locks/index.md +1 -1
  66. package/dist/agentic/guides/modules/resources/index.md +1 -1
  67. package/dist/agentic/guides/modules/sales/index.md +1 -1
  68. package/dist/agentic/guides/modules/scheduler/index.md +1 -1
  69. package/dist/agentic/guides/modules/search/index.md +1 -1
  70. package/dist/agentic/guides/modules/security/index.md +1 -1
  71. package/dist/agentic/guides/modules/seeds/index.md +1 -1
  72. package/dist/agentic/guides/modules/shipping_carriers/index.md +1 -1
  73. package/dist/agentic/guides/modules/sso/index.md +1 -1
  74. package/dist/agentic/guides/modules/staff/index.md +1 -1
  75. package/dist/agentic/guides/modules/storage_s3/index.md +1 -1
  76. package/dist/agentic/guides/modules/sync_akeneo/index.md +1 -1
  77. package/dist/agentic/guides/modules/sync_excel/index.md +1 -1
  78. package/dist/agentic/guides/modules/system_status_overlays/index.md +1 -1
  79. package/dist/agentic/guides/modules/telemetry/index.md +1 -1
  80. package/dist/agentic/guides/modules/tillio/index.md +1 -1
  81. package/dist/agentic/guides/modules/translations/index.md +1 -1
  82. package/dist/agentic/guides/modules/warranty_claims/index.md +1 -1
  83. package/dist/agentic/guides/modules/webhooks/index.md +1 -1
  84. package/dist/agentic/guides/modules/wms/index.md +1 -1
  85. package/dist/agentic/guides/modules/workflows/index.md +1 -1
  86. package/dist/agentic/guides/reference-module-facts.json +1 -1
  87. package/dist/agentic/guides/upstream/manifest.json +1 -1
  88. package/dist/agentic/shared/ai/harness/cases.json +23 -9
  89. package/dist/agentic/shared/ai/harness/source-link-inventory.json +90 -4
  90. package/dist/lib/__integration__/TC-INT-007.spec.js +207 -19
  91. package/dist/lib/__integration__/TC-INT-007.spec.js.map +2 -2
  92. package/dist/lib/__integration__/TC-INT-009.spec.js +267 -0
  93. package/dist/lib/__integration__/TC-INT-009.spec.js.map +7 -0
  94. package/dist/lib/eject.js +25 -9
  95. package/dist/lib/eject.js.map +2 -2
  96. package/dist/lib/generators/entity-ids.js +2 -0
  97. package/dist/lib/generators/entity-ids.js.map +2 -2
  98. package/dist/lib/generators/module-di.js +2 -0
  99. package/dist/lib/generators/module-di.js.map +2 -2
  100. package/dist/lib/generators/module-entities.js +2 -0
  101. package/dist/lib/generators/module-entities.js.map +2 -2
  102. package/dist/lib/generators/module-package-sources.js +6 -4
  103. package/dist/lib/generators/module-package-sources.js.map +2 -2
  104. package/dist/lib/generators/module-registry.js +32 -86
  105. package/dist/lib/generators/module-registry.js.map +2 -2
  106. package/dist/lib/generators/openapi.js +2 -0
  107. package/dist/lib/generators/openapi.js.map +2 -2
  108. package/dist/lib/generators/source-exports.js +73 -0
  109. package/dist/lib/generators/source-exports.js.map +7 -0
  110. package/dist/lib/init-summary.js +44 -0
  111. package/dist/lib/init-summary.js.map +7 -0
  112. package/dist/lib/module-github.js +98 -0
  113. package/dist/lib/module-github.js.map +7 -0
  114. package/dist/lib/module-identity.js +131 -0
  115. package/dist/lib/module-identity.js.map +7 -0
  116. package/dist/lib/module-inspection.js +124 -0
  117. package/dist/lib/module-inspection.js.map +7 -0
  118. package/dist/lib/module-install.js +150 -21
  119. package/dist/lib/module-install.js.map +3 -3
  120. package/dist/lib/module-package.js +205 -15
  121. package/dist/lib/module-package.js.map +2 -2
  122. package/dist/lib/modules-config.js +69 -30
  123. package/dist/lib/modules-config.js.map +2 -2
  124. package/dist/lib/resolver.js +33 -34
  125. package/dist/lib/resolver.js.map +2 -2
  126. package/dist/mercato.js +14 -23
  127. package/dist/mercato.js.map +2 -2
  128. package/package.json +7 -7
  129. package/src/__tests__/mercato.test.ts +14 -4
  130. package/src/lib/__fixtures__/official-module-package/dist/modules/test_package/widgets/injection/test/widget.js +1 -0
  131. package/src/lib/__integration__/TC-INT-007.spec.ts +219 -19
  132. package/src/lib/__integration__/TC-INT-009.spec.ts +279 -0
  133. package/src/lib/__tests__/eject.test.ts +98 -4
  134. package/src/lib/__tests__/init-summary.test.ts +50 -0
  135. package/src/lib/__tests__/module-github.test.ts +108 -0
  136. package/src/lib/__tests__/module-identity.test.ts +144 -0
  137. package/src/lib/__tests__/module-inspection.test.ts +195 -0
  138. package/src/lib/__tests__/module-install.test.ts +399 -10
  139. package/src/lib/__tests__/module-package.test.ts +154 -0
  140. package/src/lib/__tests__/modules-config.test.ts +55 -0
  141. package/src/lib/__tests__/resolver.packages.test.ts +29 -0
  142. package/src/lib/eject.ts +28 -10
  143. package/src/lib/generators/__tests__/module-id-collisions.test.ts +146 -0
  144. package/src/lib/generators/__tests__/module-package-sources.test.ts +19 -2
  145. package/src/lib/generators/__tests__/module-requirements.test.ts +56 -0
  146. package/src/lib/generators/entity-ids.ts +2 -0
  147. package/src/lib/generators/module-di.ts +2 -0
  148. package/src/lib/generators/module-entities.ts +2 -0
  149. package/src/lib/generators/module-package-sources.ts +7 -5
  150. package/src/lib/generators/module-registry.ts +33 -115
  151. package/src/lib/generators/openapi.ts +3 -0
  152. package/src/lib/generators/source-exports.ts +93 -0
  153. package/src/lib/init-summary.ts +54 -0
  154. package/src/lib/module-github.ts +109 -0
  155. package/src/lib/module-identity.ts +145 -0
  156. package/src/lib/module-inspection.ts +133 -0
  157. package/src/lib/module-install.ts +170 -22
  158. package/src/lib/module-package.ts +243 -16
  159. package/src/lib/modules-config.ts +80 -32
  160. package/src/lib/resolver.ts +35 -39
  161. package/src/mercato.ts +14 -24
  162. package/src/types/cross-spawn.d.ts +2 -2
@@ -1,13 +1,18 @@
1
1
  import fs from 'node:fs'
2
2
  import path from 'node:path'
3
3
  import { createRequire } from 'node:module'
4
+ import ts from 'typescript-js'
4
5
  import type { PackageResolver } from './resolver'
6
+ import { normalizeModuleLocator } from '@open-mercato/shared/modules/module-locators'
7
+ import { sourceHasDefaultExport, sourceExportedHttpMethods } from './generators/source-exports'
8
+ import { MODULE_CODE_EXTENSIONS, SCAN_CONFIGS, stripModuleCodeExtension, type ScanConfig } from './generators/scanner'
5
9
 
6
10
  type PackageJsonRecord = {
7
11
  name?: string
8
12
  version?: string
9
13
  dependencies?: Record<string, string>
10
14
  peerDependencies?: Record<string, string>
15
+ exports?: unknown
11
16
  }
12
17
 
13
18
  export type ModulePackageMetadata = {
@@ -18,6 +23,8 @@ export type ModulePackageMetadata = {
18
23
  export type ModuleInfoSnapshot = {
19
24
  title?: string
20
25
  description?: string
26
+ requires?: string[]
27
+ locator?: string
21
28
  }
22
29
 
23
30
  export type ValidatedOfficialModulePackage = {
@@ -54,20 +61,94 @@ function readPackageJson(packageJsonPath: string): PackageJsonRecord {
54
61
  }
55
62
  }
56
63
 
57
- function parseModuleInfo(indexPath: string): { title?: string; description?: string; ejectable?: boolean } {
58
- if (!fs.existsSync(indexPath)) return {}
64
+ function readPropertyName(property: ts.ObjectLiteralElementLike): string | null {
65
+ if (!('name' in property) || !property.name) return null
66
+ return ts.isIdentifier(property.name) || ts.isStringLiteralLike(property.name)
67
+ ? property.name.text
68
+ : null
69
+ }
70
+
71
+ function unwrapExpression(expression: ts.Expression): ts.Expression {
72
+ if (
73
+ ts.isAsExpression(expression)
74
+ || ts.isTypeAssertionExpression(expression)
75
+ || ts.isParenthesizedExpression(expression)
76
+ || ts.isSatisfiesExpression(expression)
77
+ ) {
78
+ return unwrapExpression(expression.expression)
79
+ }
80
+ return expression
81
+ }
82
+
83
+ function findMetadataLiteral(source: string, indexPath: string): ts.ObjectLiteralExpression | null {
84
+ const sourceFile = ts.createSourceFile(indexPath, source, ts.ScriptTarget.Latest, true)
85
+ for (const statement of sourceFile.statements) {
86
+ if (!ts.isVariableStatement(statement)) continue
87
+ for (const declaration of statement.declarationList.declarations) {
88
+ if (
89
+ !ts.isIdentifier(declaration.name)
90
+ || declaration.name.text !== 'metadata'
91
+ || !declaration.initializer
92
+ ) continue
93
+ const initializer = unwrapExpression(declaration.initializer)
94
+ if (ts.isObjectLiteralExpression(initializer)) return initializer
95
+ }
96
+ }
97
+ return null
98
+ }
99
+
100
+ function parseModuleInfo(indexPath: string, validateRequires = false): {
101
+ title?: string
102
+ description?: string
103
+ ejectable?: boolean
104
+ locator?: string
105
+ requires: string[]
106
+ } {
107
+ if (!fs.existsSync(indexPath)) return { requires: [] }
59
108
 
60
109
  const source = fs.readFileSync(indexPath, 'utf8')
61
- const result: { title?: string; description?: string; ejectable?: boolean } = {}
110
+ const metadata = findMetadataLiteral(source, indexPath)
111
+ const result: { title?: string; description?: string; ejectable?: boolean; locator?: string; requires: string[] } = {
112
+ requires: [],
113
+ }
114
+ if (!metadata) return result
62
115
 
63
- const titleMatch = source.match(/\btitle\s*:\s*['"]([^'"]+)['"]/)
64
- if (titleMatch) result.title = titleMatch[1]
116
+ for (const property of metadata.properties) {
117
+ const propertyName = readPropertyName(property)
118
+ if (!ts.isPropertyAssignment(property)) {
119
+ if (validateRequires && propertyName === 'requires') {
120
+ throw new Error(`Module descriptor at ${indexPath} must declare "requires" as a static string array.`)
121
+ }
122
+ continue
123
+ }
124
+ const initializer = unwrapExpression(property.initializer)
65
125
 
66
- const descriptionMatch = source.match(/\bdescription\s*:\s*['"]([^'"]+)['"]/)
67
- if (descriptionMatch) result.description = descriptionMatch[1]
126
+ if ((propertyName === 'title' || propertyName === 'description' || propertyName === 'locator') && ts.isStringLiteralLike(initializer)) {
127
+ result[propertyName] = initializer.text
128
+ continue
129
+ }
68
130
 
69
- const ejectableMatch = source.match(/\bejectable\s*:\s*(true|false)/)
70
- if (ejectableMatch) result.ejectable = ejectableMatch[1] === 'true'
131
+ if (propertyName === 'ejectable') {
132
+ if (initializer.kind === ts.SyntaxKind.TrueKeyword) result.ejectable = true
133
+ if (initializer.kind === ts.SyntaxKind.FalseKeyword) result.ejectable = false
134
+ continue
135
+ }
136
+
137
+ if (propertyName !== 'requires') continue
138
+ if (!ts.isArrayLiteralExpression(initializer)) {
139
+ if (validateRequires) {
140
+ throw new Error(`Module descriptor at ${indexPath} must declare "requires" as a static string array.`)
141
+ }
142
+ continue
143
+ }
144
+ if (initializer.elements.some((element) => !ts.isStringLiteralLike(element))) {
145
+ if (validateRequires) {
146
+ throw new Error(`Module descriptor at ${indexPath} must contain only string module ids in "requires".`)
147
+ }
148
+ continue
149
+ }
150
+ result.requires = initializer.elements.map((element) => (element as ts.StringLiteralLike).text)
151
+ }
71
152
 
72
153
  return result
73
154
  }
@@ -175,7 +256,6 @@ function collectBoundaryViolations(moduleDir: string): string[] {
175
256
 
176
257
  function findResolvedPackageRoot(
177
258
  resolvedPath: string,
178
- packageName: string,
179
259
  ): string | null {
180
260
  let currentPath = fs.statSync(resolvedPath).isDirectory()
181
261
  ? resolvedPath
@@ -185,7 +265,7 @@ function findResolvedPackageRoot(
185
265
  const packageJsonPath = path.join(currentPath, 'package.json')
186
266
  if (fs.existsSync(packageJsonPath)) {
187
267
  const packageJson = readPackageJson(packageJsonPath)
188
- if (packageJson.name === packageName) {
268
+ if (typeof packageJson.name === 'string' && normalizeModuleLocator(`npm:${packageJson.name}`) === `npm:${packageJson.name}`) {
189
269
  return currentPath
190
270
  }
191
271
  }
@@ -195,7 +275,7 @@ function findResolvedPackageRoot(
195
275
  return null
196
276
  }
197
277
 
198
- function resolveInstalledPackageRootWithRequire(packageName: string, appDir: string): string | null {
278
+ export function resolveInstalledPackageRootWithRequire(packageName: string, appDir: string): string | null {
199
279
  const specifiers = [`${packageName}/package.json`, packageName]
200
280
 
201
281
  for (const specifier of specifiers) {
@@ -205,7 +285,7 @@ function resolveInstalledPackageRootWithRequire(packageName: string, appDir: str
205
285
  })
206
286
  const packageRoot =
207
287
  specifier === packageName
208
- ? findResolvedPackageRoot(resolvedPath, packageName)
288
+ ? findResolvedPackageRoot(resolvedPath)
209
289
  : path.dirname(resolvedPath)
210
290
 
211
291
  if (packageRoot) {
@@ -258,6 +338,130 @@ export function resolveInstalledPackageRoot(
258
338
  throw new Error(`Package "${packageName}" is not installed in ${resolver.getAppDir()}.`)
259
339
  }
260
340
 
341
+ function resolveEsmExportTarget(value: unknown, substitution?: string): string | null | undefined {
342
+ if (typeof value === 'string') {
343
+ const target = substitution === undefined ? value : value.replaceAll('*', substitution)
344
+ if (!target.startsWith('./') || target.slice(2).split('/').some((segment) => ['..', '.', 'node_modules'].includes(segment))) return undefined
345
+ return target
346
+ }
347
+ if (value === null) return null
348
+ if (Array.isArray(value)) {
349
+ for (const candidate of value) {
350
+ const target = resolveEsmExportTarget(candidate, substitution)
351
+ if (target !== undefined && target !== null) return target
352
+ }
353
+ return null
354
+ }
355
+ if (value && typeof value === 'object') {
356
+ for (const [condition, candidate] of Object.entries(value)) {
357
+ if (!['import', 'node', 'node-addons', 'module-sync', 'default'].includes(condition)) continue
358
+ const target = resolveEsmExportTarget(candidate, substitution)
359
+ if (target !== undefined) return target
360
+ }
361
+ }
362
+ return undefined
363
+ }
364
+
365
+ function resolvePackageSubpathExport(exports: unknown, subpath: string): string | null | undefined {
366
+ if (!exports || typeof exports !== 'object' || Array.isArray(exports)) return undefined
367
+ const entries = Object.entries(exports)
368
+ const direct = entries.find(([key]) => key === subpath)
369
+ if (direct) return resolveEsmExportTarget(direct[1])
370
+ const patterns = entries.filter(([key]) => key.startsWith('./') && key.includes('*'))
371
+ .sort(([left], [right]) => right.indexOf('*') - left.indexOf('*') || right.length - left.length)
372
+ for (const [pattern, target] of patterns) {
373
+ const wildcard = pattern.indexOf('*')
374
+ const prefix = pattern.slice(0, wildcard)
375
+ const suffix = pattern.slice(wildcard + 1)
376
+ if (subpath.startsWith(prefix) && subpath.endsWith(suffix) && subpath.length >= prefix.length + suffix.length) {
377
+ return resolveEsmExportTarget(target, subpath.slice(prefix.length, subpath.length - suffix.length))
378
+ }
379
+ }
380
+ return undefined
381
+ }
382
+
383
+ function isGeneratedModuleImport(relativePath: string, moduleFiles: readonly string[], readSource?: (relativePath: string) => string): boolean {
384
+ const segments = relativePath.split('/')
385
+ if (segments.some((segment) => segment === '__tests__' || segment === '__mocks__')) return false
386
+ const fileName = segments[segments.length - 1]
387
+ if (fileName.endsWith('.d.ts') || /\.(?:test|spec)\.[jt]sx?$/.test(fileName)) return false
388
+ const basename = stripModuleCodeExtension(fileName)
389
+ if (segments.length === 1) {
390
+ return new Set(['index', 'acl', 'setup', 'cli', 'runtime', 'ce', 'search', 'events', 'notifications', 'notifications.client', 'notifications.handlers', 'translations', 'encryption', 'ai-tools', 'ai-agents', 'di', 'integration', 'vector']).has(basename)
391
+ }
392
+ if (['widgets/components', 'widgets/injection-table', 'api/interceptors', 'data/extensions', 'data/fields'].includes(stripModuleCodeExtension(relativePath))) return true
393
+ if (/^commands\//.test(relativePath)) {
394
+ return MODULE_CODE_EXTENSIONS.some((extension) => fileName.endsWith(extension)) && !['index', 'shared', 'factory'].includes(basename)
395
+ }
396
+ const scanConfigs: readonly ScanConfig[] = Object.values(SCAN_CONFIGS)
397
+ for (const config of scanConfigs) {
398
+ if (!relativePath.startsWith(`${config.folder}/`)) continue
399
+ const relativeSegments = relativePath.slice(config.folder.length + 1).split('/')
400
+ if (relativeSegments.slice(0, -1).some((segment) => config.skipDirs?.(segment))) continue
401
+ if (!config.include(fileName)) continue
402
+ if (config.folder === 'frontend' || config.folder === 'backend') return readSource ? sourceHasDefaultExport(relativePath, readSource(relativePath)) : false
403
+ if (config.folder === 'api') return readSource ? sourceExportedHttpMethods(readSource(relativePath)).length > 0 : false
404
+ return true
405
+ }
406
+ if (/^(?:frontend|backend)\//.test(relativePath) && (basename === 'meta' || basename.endsWith('.meta'))) {
407
+ const folder = segments.slice(0, -1).join('/')
408
+ const pageConfig = segments[0] === 'frontend' ? SCAN_CONFIGS.frontendPages : SCAN_CONFIGS.backendPages
409
+ const candidates = moduleFiles.filter((file) => file.startsWith(`${folder}/`) && !file.slice(folder.length + 1).includes('/') && pageConfig.include(path.posix.basename(file)))
410
+ return candidates.some((candidate) => (basename === 'meta' || basename === `${stripModuleCodeExtension(path.posix.basename(candidate))}.meta`) && Boolean(readSource && sourceHasDefaultExport(candidate, readSource(candidate))))
411
+ }
412
+ if (/^(?:data|db)\/(?:entities\.override|entities|schema)\.[jt]sx?$/.test(relativePath)) {
413
+ const selected = ['data', 'db'].flatMap((folder) => ['entities.override', 'entities', 'schema'].flatMap((candidate) => MODULE_CODE_EXTENSIONS.map((extension) => `${folder}/${candidate}${extension}`)))
414
+ .find((candidate) => moduleFiles.includes(candidate))
415
+ return relativePath === selected
416
+ }
417
+ return /^i18n\/[^/]+\.json$/.test(relativePath)
418
+ }
419
+
420
+ export function isModulePackageExportError(error: unknown): boolean {
421
+ return error instanceof Error && 'code' in error && error.code === 'MODULE_PACKAGE_EXPORT_UNAVAILABLE'
422
+ }
423
+
424
+ export function assertModulePackageExports(
425
+ manifest: { exports?: unknown },
426
+ binding: string,
427
+ moduleId: string,
428
+ packageFiles: readonly string[],
429
+ readSource?: (relativePath: string) => string,
430
+ ): void {
431
+ const normalizedFiles = new Set(packageFiles.map((file) => file.replaceAll('\\', '/').replace(/^\.\//, '')))
432
+ const sourcePrefix = `src/modules/${moduleId}/`
433
+ const distPrefix = `dist/modules/${moduleId}/`
434
+ const sourceFiles = [...normalizedFiles].filter((file) => file.startsWith(sourcePrefix)).map((file) => file.slice(sourcePrefix.length))
435
+ const distFiles = [...normalizedFiles].filter((file) => file.startsWith(distPrefix)).map((file) => file.slice(distPrefix.length))
436
+ const moduleFiles = sourceFiles.length > 0 ? sourceFiles : distFiles
437
+ const cachedSources = new Map<string, string>()
438
+ const readModuleSource = readSource ? (relativePath: string): string => {
439
+ let source = cachedSources.get(relativePath)
440
+ if (source === undefined) {
441
+ source = readSource(`${sourceFiles.length > 0 ? sourcePrefix : distPrefix}${relativePath}`)
442
+ cachedSources.set(relativePath, source)
443
+ }
444
+ return source
445
+ } : undefined
446
+ const generatedCandidates = sourceFiles.length > 0 ? sourceFiles : distFiles
447
+ for (const sourcePath of generatedCandidates) {
448
+ if (!isGeneratedModuleImport(sourcePath, moduleFiles, readModuleSource)) continue
449
+ const isCode = /\.[jt]sx?$/.test(sourcePath)
450
+ const runtimePath = sourceFiles.length > 0 && isCode
451
+ ? distFiles.find((file) => /\.jsx?$/.test(file) && stripModuleCodeExtension(file) === stripModuleCodeExtension(sourcePath))
452
+ : sourcePath
453
+ const publicPath = `./modules/${moduleId}/${stripModuleCodeExtension(sourcePath)}`
454
+ if (!runtimePath) {
455
+ throw Object.assign(new Error(`Package binding "${binding}" cannot import "${binding}/${publicPath.slice(2)}": the package artifact is missing the compiled runtime entrypoint. Build the package before enabling it.`), { code: 'MODULE_PACKAGE_EXPORT_UNAVAILABLE' })
456
+ }
457
+ if (manifest.exports === undefined) continue
458
+ const target = resolvePackageSubpathExport(manifest.exports, publicPath)
459
+ if (!target || !normalizedFiles.has(target.slice(2))) {
460
+ throw Object.assign(new Error(`Package binding "${binding}" cannot import "${binding}/${publicPath.slice(2)}": the package exports do not provide an available ESM target. Keep the current provider and use a package exposing this module subpath before enabling it.`), { code: 'MODULE_PACKAGE_EXPORT_UNAVAILABLE' })
461
+ }
462
+ }
463
+ }
464
+
261
465
  export function readOfficialModulePackageFromRoot(
262
466
  packageRoot: string,
263
467
  expectedPackageName?: string,
@@ -274,6 +478,10 @@ export function readOfficialModulePackageFromRoot(
274
478
  throw new Error(`Package manifest at ${packageJsonPath} is missing the "name" field.`)
275
479
  }
276
480
 
481
+ if (typeof packageName !== 'string' || normalizeModuleLocator(`npm:${packageName}`) !== `npm:${packageName}`) {
482
+ throw new Error(`Package manifest at ${packageJsonPath} has an invalid "name" field.`)
483
+ }
484
+
277
485
  if (expectedPackageName && packageName !== expectedPackageName) {
278
486
  throw new Error(`Resolved package "${packageName}" does not match requested package "${expectedPackageName}".`)
279
487
  }
@@ -317,26 +525,45 @@ export function readOfficialModulePackageFromRoot(
317
525
  throw new Error(`Package "${packageName}" is missing dist/modules/${moduleId}.`)
318
526
  }
319
527
 
320
- const info = parseModuleInfo(path.join(sourceModuleDir, 'index.ts'))
528
+ const info = parseModuleInfo(path.join(sourceModuleDir, 'index.ts'), true)
321
529
 
322
530
  return {
323
531
  packageName,
324
532
  packageRoot,
325
533
  packageJson,
326
534
  metadata: { moduleId, ejectable: selected.ejectable },
327
- moduleInfo: { title: info.title, description: info.description },
535
+ moduleInfo: { title: info.title, description: info.description, requires: info.requires, ...(info.locator ? { locator: info.locator } : {}) },
328
536
  sourceModuleDir,
329
537
  distModuleDir,
330
538
  }
331
539
  }
332
540
 
541
+ function listPackageFiles(packageRoot: string, directory = packageRoot): string[] {
542
+ return fs.readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
543
+ if (shouldSkipEntryName(entry.name)) return []
544
+ const fullPath = path.join(directory, entry.name)
545
+ return entry.isDirectory() ? listPackageFiles(packageRoot, fullPath) : [path.relative(packageRoot, fullPath)]
546
+ })
547
+ }
548
+
333
549
  export function resolveInstalledOfficialModulePackage(
334
550
  resolver: PackageResolver,
335
551
  packageName: string,
336
552
  moduleId?: string,
337
553
  ): ValidatedOfficialModulePackage {
338
554
  const packageRoot = resolveInstalledPackageRoot(resolver, packageName)
339
- return readOfficialModulePackageFromRoot(packageRoot, packageName, moduleId)
555
+ const modulePackage = readOfficialModulePackageFromRoot(packageRoot, undefined, moduleId)
556
+ const packageFiles = listPackageFiles(packageRoot)
557
+ assertModulePackageExports(modulePackage.packageJson, packageName, modulePackage.metadata.moduleId, packageFiles, (relativePath) => fs.readFileSync(path.join(packageRoot, relativePath), 'utf8'))
558
+ if (modulePackage.packageName !== packageName) {
559
+ const resolvedBindingRoot = resolveInstalledPackageRootWithRequire(packageName, resolver.getAppDir())
560
+ const installedBindingPath = path.join('node_modules', packageName)
561
+ const matchesBindingPath = path.normalize(packageRoot).endsWith(`${path.sep}${installedBindingPath}`)
562
+ if (!matchesBindingPath && (!resolvedBindingRoot || fs.realpathSync(resolvedBindingRoot) !== fs.realpathSync(packageRoot))) {
563
+ throw new Error(`Resolved package "${modulePackage.packageName}" does not match requested package binding "${packageName}".`)
564
+ }
565
+ }
566
+ return modulePackage
340
567
  }
341
568
 
342
569
  export function validateEjectBoundaries(modulePackage: ValidatedOfficialModulePackage): void {
@@ -5,6 +5,8 @@ import { parseBooleanWithDefault } from '@open-mercato/shared/lib/boolean'
5
5
  export type ModuleEntry = {
6
6
  id: string
7
7
  from?: '@open-mercato/core' | '@app' | string
8
+ locator?: string
9
+ packageName?: string
8
10
  }
9
11
 
10
12
  type ModuleOccurrence = {
@@ -24,14 +26,24 @@ type ModuleConfigShape = {
24
26
  function parseModuleEntryFromObjectLiteral(node: ts.ObjectLiteralExpression): ModuleEntry | null {
25
27
  let id: string | null = null
26
28
  let from: string | null = null
29
+ let locator: string | undefined
30
+ let packageName: string | undefined
27
31
 
28
32
  for (const property of node.properties) {
29
- if (!ts.isPropertyAssignment(property) || !ts.isIdentifier(property.name)) continue
33
+ if (!ts.isPropertyAssignment(property) || (!ts.isIdentifier(property.name) && !ts.isStringLiteralLike(property.name))) continue
30
34
 
31
35
  if (property.name.text === 'id' && ts.isStringLiteralLike(property.initializer)) {
32
36
  id = property.initializer.text
33
37
  }
34
38
 
39
+ if (property.name.text === 'packageName' && ts.isStringLiteralLike(property.initializer)) {
40
+ packageName = property.initializer.text
41
+ }
42
+
43
+ if (property.name.text === 'locator' && ts.isStringLiteralLike(property.initializer)) {
44
+ locator = property.initializer.text
45
+ }
46
+
35
47
  if (property.name.text === 'from' && ts.isStringLiteralLike(property.initializer)) {
36
48
  from = property.initializer.text
37
49
  }
@@ -42,6 +54,8 @@ function parseModuleEntryFromObjectLiteral(node: ts.ObjectLiteralExpression): Mo
42
54
  return {
43
55
  id,
44
56
  from: from ?? '@open-mercato/core',
57
+ ...(locator ? { locator } : {}),
58
+ ...(packageName ? { packageName } : {}),
45
59
  }
46
60
  }
47
61
 
@@ -368,6 +382,8 @@ function normalizeModuleEntry(entry: ModuleEntry): ModuleEntry {
368
382
  return {
369
383
  id: entry.id,
370
384
  from: entry.from ?? '@open-mercato/core',
385
+ ...(entry.locator ? { locator: entry.locator } : {}),
386
+ ...(entry.packageName ? { packageName: entry.packageName } : {}),
371
387
  }
372
388
  }
373
389
 
@@ -377,7 +393,7 @@ function escapeSingleQuotedString(value: string): string {
377
393
 
378
394
  function buildModuleEntryLiteral(entry: ModuleEntry): string {
379
395
  const normalized = normalizeModuleEntry(entry)
380
- return `{ id: '${escapeSingleQuotedString(normalized.id)}', from: '${escapeSingleQuotedString(normalized.from ?? '@open-mercato/core')}' }`
396
+ return `{ id: '${escapeSingleQuotedString(normalized.id)}', from: '${escapeSingleQuotedString(normalized.from ?? '@open-mercato/core')}'${normalized.locator ? `, locator: '${escapeSingleQuotedString(normalized.locator)}'` : ''}${normalized.packageName ? `, packageName: '${escapeSingleQuotedString(normalized.packageName)}'` : ''} }`
381
397
  }
382
398
 
383
399
  function lineIndentAt(source: string, position: number): string {
@@ -443,16 +459,29 @@ function insertArrayElement(
443
459
  return head + insertion + closingLine + tail
444
460
  }
445
461
 
446
- function upsertModuleSource(objectLiteral: string, from: string): string {
447
- if (/from\s*:\s*'[^']*'/.test(objectLiteral)) {
448
- return objectLiteral.replace(/from\s*:\s*'[^']*'/, `from: '${escapeSingleQuotedString(from)}'`)
449
- }
450
-
451
- if (/from\s*:\s*"[^"]*"/.test(objectLiteral)) {
452
- return objectLiteral.replace(/from\s*:\s*"[^"]*"/, `from: "${from.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`)
453
- }
454
-
455
- return objectLiteral.replace(/\}\s*$/, `, from: '${escapeSingleQuotedString(from)}' }`)
462
+ function upsertModuleProperty(objectLiteral: string, propertyName: 'from' | 'locator' | 'packageName', value: string): string {
463
+ const prefix = 'const entry = '
464
+ const sourceFile = ts.createSourceFile('entry.ts', prefix + objectLiteral, ts.ScriptTarget.Latest, true)
465
+ const statement = sourceFile.statements[0]
466
+ const initializer = ts.isVariableStatement(statement) ? statement.declarationList.declarations[0]?.initializer : undefined
467
+ if (!initializer || !ts.isObjectLiteralExpression(initializer)) {
468
+ throw new Error('Cannot update module registration: expected an object literal.')
469
+ }
470
+ const property = initializer.properties.find((candidate) =>
471
+ 'name' in candidate && candidate.name && (ts.isIdentifier(candidate.name) || ts.isStringLiteralLike(candidate.name)) && candidate.name.text === propertyName,
472
+ )
473
+ const quotedValue = `'${escapeSingleQuotedString(value)}'`
474
+ if (property) {
475
+ const target = ts.isPropertyAssignment(property) ? property.initializer : property
476
+ const existingQuote = ts.isPropertyAssignment(property) ? property.initializer.getText(sourceFile)[0] : undefined
477
+ const replacementValue = existingQuote === '"' ? JSON.stringify(value) : quotedValue
478
+ const replacement = ts.isPropertyAssignment(property) ? replacementValue : `${propertyName}: ${replacementValue}`
479
+ return objectLiteral.slice(0, target.getStart(sourceFile) - prefix.length) + replacement + objectLiteral.slice(target.end - prefix.length)
480
+ }
481
+ const lastProperty = initializer.properties[initializer.properties.length - 1]
482
+ const insertPosition = lastProperty ? lastProperty.end - prefix.length : initializer.end - prefix.length - 1
483
+ const separator = lastProperty ? ', ' : ' '
484
+ return objectLiteral.slice(0, insertPosition) + `${separator}${propertyName}: ${quotedValue}` + objectLiteral.slice(insertPosition)
456
485
  }
457
486
 
458
487
  export function readEnabledModules(modulesPath: string, env: NodeJS.ProcessEnv = process.env): ModuleEntry[] {
@@ -515,6 +544,16 @@ export function ensureModuleRegistration(
515
544
  }
516
545
 
517
546
  const source = fs.readFileSync(modulesPath, 'utf8')
547
+ const result = prepareModuleRegistration(source, entry, modulesPath)
548
+ if (result.changed) fs.writeFileSync(modulesPath, result.source)
549
+ return { changed: result.changed, registeredAs: result.registeredAs }
550
+ }
551
+
552
+ export function prepareModuleRegistration(
553
+ source: string,
554
+ entry: ModuleEntry,
555
+ modulesPath = 'src/modules.ts',
556
+ ): { source: string; changed: boolean; registeredAs: string } {
518
557
  const sourceFile = ts.createSourceFile('modules.ts', source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS)
519
558
  const config = parseModuleConfigShape(source)
520
559
  const normalized = normalizeModuleEntry(entry)
@@ -526,67 +565,76 @@ export function ensureModuleRegistration(
526
565
  }
527
566
 
528
567
  if (matches.length > 1) {
529
- throw new Error(
530
- `Module "${normalized.id}" is registered multiple times in ${modulesPath}. Resolve duplicates manually before running this command.`,
531
- )
568
+ throw new Error(`Module "${normalized.id}" is registered multiple times in src/modules.ts. Resolve duplicates manually before running this command.`)
532
569
  }
533
570
 
534
571
  if (matches.length === 1) {
535
572
  const existing = normalizeModuleEntry(matches[0].entry)
536
573
  if (existing.from === normalized.from) {
537
- return { changed: false, registeredAs: normalized.from ?? '@open-mercato/core' }
574
+ return { source, changed: false, registeredAs: normalized.from ?? '@open-mercato/core' }
538
575
  }
539
-
540
- throw new Error(
541
- `Module "${normalized.id}" is already registered from "${existing.from}". Remove or eject the existing registration first.`,
542
- )
576
+ throw new Error(`Module "${normalized.id}" is already registered from "${existing.from}". Remove or eject the existing registration first.`)
543
577
  }
544
578
 
545
- const updated = insertArrayElement(source, config.arrayNode, normalized, sourceFile)
546
- fs.writeFileSync(modulesPath, updated)
547
-
548
- return { changed: true, registeredAs: normalized.from ?? '@open-mercato/core' }
579
+ return {
580
+ source: insertArrayElement(source, config.arrayNode, normalized, sourceFile),
581
+ changed: true,
582
+ registeredAs: normalized.from ?? '@open-mercato/core',
583
+ }
549
584
  }
550
585
 
551
586
  export function setModuleRegistrationSource(
552
587
  modulesPath: string,
553
588
  moduleId: string,
554
589
  from: string,
590
+ provenance?: Pick<ModuleEntry, 'locator' | 'packageName'>,
555
591
  ): { changed: boolean } {
556
592
  if (!fs.existsSync(modulesPath)) {
557
593
  throw new Error(`modules.ts not found at ${modulesPath}`)
558
594
  }
559
595
 
560
596
  const source = fs.readFileSync(modulesPath, 'utf8')
597
+ const updated = prepareModuleRegistrationSource(source, moduleId, from, provenance)
598
+ if (updated === source) return { changed: false }
599
+ fs.writeFileSync(modulesPath, updated)
600
+ return { changed: true }
601
+ }
602
+
603
+ export function prepareModuleRegistrationSource(
604
+ source: string,
605
+ moduleId: string,
606
+ from: string,
607
+ provenance?: Pick<ModuleEntry, 'locator' | 'packageName'>,
608
+ ): string {
561
609
  const config = parseModuleConfigShape(source)
562
610
  const matches = config.occurrences.filter((occurrence) => occurrence.active && occurrence.entry.id === moduleId)
563
611
 
564
612
  if (matches.length === 0) {
565
613
  throw new Error(
566
- `Could not find module entry for "${moduleId}" in ${modulesPath}. Expected a pattern like: { id: '${moduleId}', from: '...' } or { id: '${moduleId}' }`,
614
+ `Could not find module entry for "${moduleId}" in src/modules.ts. Expected a pattern like: { id: '${moduleId}', from: '...' } or { id: '${moduleId}' }`,
567
615
  )
568
616
  }
569
617
 
570
618
  if (matches.length > 1) {
571
619
  throw new Error(
572
- `Module "${moduleId}" is registered multiple times in ${modulesPath}. Resolve duplicates manually before running this command.`,
620
+ `Module "${moduleId}" is registered multiple times in src/modules.ts. Resolve duplicates manually before running this command.`,
573
621
  )
574
622
  }
575
623
 
576
624
  const match = matches[0]
577
625
  const current = normalizeModuleEntry(match.entry)
578
- if (current.from === from) {
579
- return { changed: false }
626
+ if (current.from === from && (!provenance?.locator || current.locator === provenance.locator) && (!provenance?.packageName || current.packageName === provenance.packageName)) {
627
+ return source
580
628
  }
581
629
 
582
630
  const objectLiteral = source.slice(match.node.getStart(), match.node.end)
583
- const updatedObjectLiteral = upsertModuleSource(objectLiteral, from)
631
+ let updatedObjectLiteral = upsertModuleProperty(objectLiteral, 'from', from)
632
+ if (provenance?.locator) updatedObjectLiteral = upsertModuleProperty(updatedObjectLiteral, 'locator', provenance.locator)
633
+ if (provenance?.packageName) updatedObjectLiteral = upsertModuleProperty(updatedObjectLiteral, 'packageName', provenance.packageName)
584
634
  const updated =
585
635
  source.slice(0, match.node.getStart()) +
586
636
  updatedObjectLiteral +
587
637
  source.slice(match.node.end)
588
638
 
589
- fs.writeFileSync(modulesPath, updated)
590
-
591
- return { changed: true }
639
+ return updated
592
640
  }