@open-mercato/cli 0.10.1-develop.7990.1.82d0a183c5 → 0.10.1-develop.8004.1.a8a79ee43f

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 (182) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/build.mjs +1 -0
  3. package/dist/agentic/guides/module-facts.json +162 -162
  4. package/dist/agentic/guides/module-facts.v2.json +162 -162
  5. package/dist/agentic/guides/module-tool.md +21 -0
  6. package/dist/agentic/guides/modules/agent_orchestrator/index.md +1 -1
  7. package/dist/agentic/guides/modules/ai_assistant/index.md +1 -1
  8. package/dist/agentic/guides/modules/api_docs/index.md +1 -1
  9. package/dist/agentic/guides/modules/api_keys/index.md +1 -1
  10. package/dist/agentic/guides/modules/attachments/index.md +1 -1
  11. package/dist/agentic/guides/modules/audit_logs/index.md +1 -1
  12. package/dist/agentic/guides/modules/auth/index.md +1 -1
  13. package/dist/agentic/guides/modules/availability/index.md +1 -1
  14. package/dist/agentic/guides/modules/business_rules/index.md +1 -1
  15. package/dist/agentic/guides/modules/carrier_inpost/index.md +1 -1
  16. package/dist/agentic/guides/modules/cart/index.md +1 -1
  17. package/dist/agentic/guides/modules/catalog/index.md +1 -1
  18. package/dist/agentic/guides/modules/channel_apns/index.md +1 -1
  19. package/dist/agentic/guides/modules/channel_discord/index.md +1 -1
  20. package/dist/agentic/guides/modules/channel_expo/index.md +1 -1
  21. package/dist/agentic/guides/modules/channel_fcm/index.md +1 -1
  22. package/dist/agentic/guides/modules/channel_gmail/index.md +1 -1
  23. package/dist/agentic/guides/modules/channel_imap/index.md +1 -1
  24. package/dist/agentic/guides/modules/channel_resend/index.md +1 -1
  25. package/dist/agentic/guides/modules/channel_ses/index.md +1 -1
  26. package/dist/agentic/guides/modules/channel_smtp/index.md +1 -1
  27. package/dist/agentic/guides/modules/checkout/index.md +1 -1
  28. package/dist/agentic/guides/modules/communication_channels/index.md +1 -1
  29. package/dist/agentic/guides/modules/configs/index.md +1 -1
  30. package/dist/agentic/guides/modules/content/index.md +1 -1
  31. package/dist/agentic/guides/modules/currencies/index.md +1 -1
  32. package/dist/agentic/guides/modules/customer_accounts/index.md +1 -1
  33. package/dist/agentic/guides/modules/customer_groups/index.md +1 -1
  34. package/dist/agentic/guides/modules/customers/index.md +1 -1
  35. package/dist/agentic/guides/modules/dashboards/index.md +1 -1
  36. package/dist/agentic/guides/modules/data_sync/index.md +1 -1
  37. package/dist/agentic/guides/modules/design_system/index.md +1 -1
  38. package/dist/agentic/guides/modules/devices/index.md +1 -1
  39. package/dist/agentic/guides/modules/dictionaries/index.md +1 -1
  40. package/dist/agentic/guides/modules/directory/index.md +1 -1
  41. package/dist/agentic/guides/modules/documents/index.md +1 -1
  42. package/dist/agentic/guides/modules/ecommerce/index.md +1 -1
  43. package/dist/agentic/guides/modules/entities/index.md +1 -1
  44. package/dist/agentic/guides/modules/eudr/index.md +1 -1
  45. package/dist/agentic/guides/modules/events/index.md +1 -1
  46. package/dist/agentic/guides/modules/feature_toggles/index.md +1 -1
  47. package/dist/agentic/guides/modules/forms/index.md +1 -1
  48. package/dist/agentic/guides/modules/gateway_autopay/index.md +1 -1
  49. package/dist/agentic/guides/modules/gateway_stripe/index.md +1 -1
  50. package/dist/agentic/guides/modules/gateway_tpay/index.md +1 -1
  51. package/dist/agentic/guides/modules/generators/index.md +1 -1
  52. package/dist/agentic/guides/modules/inbox_ops/index.md +1 -1
  53. package/dist/agentic/guides/modules/integrations/index.md +1 -1
  54. package/dist/agentic/guides/modules/messages/index.md +1 -1
  55. package/dist/agentic/guides/modules/notifications/index.md +1 -1
  56. package/dist/agentic/guides/modules/onboarding/index.md +1 -1
  57. package/dist/agentic/guides/modules/payment_gateways/index.md +1 -1
  58. package/dist/agentic/guides/modules/perspectives/index.md +1 -1
  59. package/dist/agentic/guides/modules/phone_calls/index.md +1 -1
  60. package/dist/agentic/guides/modules/planner/index.md +1 -1
  61. package/dist/agentic/guides/modules/portal/index.md +1 -1
  62. package/dist/agentic/guides/modules/progress/index.md +1 -1
  63. package/dist/agentic/guides/modules/promotions/index.md +1 -1
  64. package/dist/agentic/guides/modules/push_notifications/index.md +1 -1
  65. package/dist/agentic/guides/modules/query_index/index.md +1 -1
  66. package/dist/agentic/guides/modules/record_locks/index.md +1 -1
  67. package/dist/agentic/guides/modules/resources/index.md +1 -1
  68. package/dist/agentic/guides/modules/sales/index.md +1 -1
  69. package/dist/agentic/guides/modules/scheduler/index.md +1 -1
  70. package/dist/agentic/guides/modules/search/index.md +1 -1
  71. package/dist/agentic/guides/modules/security/index.md +1 -1
  72. package/dist/agentic/guides/modules/seeds/index.md +1 -1
  73. package/dist/agentic/guides/modules/shipping_carriers/index.md +1 -1
  74. package/dist/agentic/guides/modules/sso/index.md +1 -1
  75. package/dist/agentic/guides/modules/staff/index.md +1 -1
  76. package/dist/agentic/guides/modules/storage_s3/index.md +1 -1
  77. package/dist/agentic/guides/modules/sync_akeneo/index.md +1 -1
  78. package/dist/agentic/guides/modules/sync_excel/index.md +1 -1
  79. package/dist/agentic/guides/modules/system_status_overlays/index.md +1 -1
  80. package/dist/agentic/guides/modules/telemetry/index.md +1 -1
  81. package/dist/agentic/guides/modules/tillio/index.md +1 -1
  82. package/dist/agentic/guides/modules/translations/index.md +1 -1
  83. package/dist/agentic/guides/modules/warranty_claims/index.md +1 -1
  84. package/dist/agentic/guides/modules/webhooks/index.md +1 -1
  85. package/dist/agentic/guides/modules/wms/index.md +1 -1
  86. package/dist/agentic/guides/modules/workflows/index.md +1 -1
  87. package/dist/agentic/guides/reference-module-facts.json +1 -1
  88. package/dist/agentic/guides/upstream/BACKWARD_COMPATIBILITY.md +4 -0
  89. package/dist/agentic/guides/upstream/manifest.json +2 -2
  90. package/dist/agentic/shared/ai/harness/cases.json +23 -9
  91. package/dist/agentic/shared/ai/harness/source-link-inventory.json +90 -4
  92. package/dist/i18n/de.json +4 -0
  93. package/dist/i18n/en.json +4 -0
  94. package/dist/i18n/es.json +4 -0
  95. package/dist/i18n/ko.json +4 -0
  96. package/dist/i18n/pl.json +4 -0
  97. package/dist/lib/__fixtures__/module-search/built-in.json +235 -0
  98. package/dist/lib/__fixtures__/module-search/catalog-parity.json +4076 -0
  99. package/dist/lib/__fixtures__/module-search/modules-index.json +1980 -0
  100. package/dist/lib/__fixtures__/official-module-package/package.json +33 -0
  101. package/dist/lib/__integration__/TC-INT-007.spec.js +207 -19
  102. package/dist/lib/__integration__/TC-INT-007.spec.js.map +2 -2
  103. package/dist/lib/__integration__/TC-INT-009.spec.js +267 -0
  104. package/dist/lib/__integration__/TC-INT-009.spec.js.map +7 -0
  105. package/dist/lib/eject.js +25 -9
  106. package/dist/lib/eject.js.map +2 -2
  107. package/dist/lib/generators/entity-ids.js +2 -0
  108. package/dist/lib/generators/entity-ids.js.map +2 -2
  109. package/dist/lib/generators/module-di.js +2 -0
  110. package/dist/lib/generators/module-di.js.map +2 -2
  111. package/dist/lib/generators/module-entities.js +2 -0
  112. package/dist/lib/generators/module-entities.js.map +2 -2
  113. package/dist/lib/generators/module-package-sources.js +6 -4
  114. package/dist/lib/generators/module-package-sources.js.map +2 -2
  115. package/dist/lib/generators/module-registry.js +32 -86
  116. package/dist/lib/generators/module-registry.js.map +2 -2
  117. package/dist/lib/generators/openapi.js +2 -0
  118. package/dist/lib/generators/openapi.js.map +2 -2
  119. package/dist/lib/generators/source-exports.js +73 -0
  120. package/dist/lib/generators/source-exports.js.map +7 -0
  121. package/dist/lib/init-secrets.js +33 -1
  122. package/dist/lib/init-secrets.js.map +2 -2
  123. package/dist/lib/init-summary.js +44 -0
  124. package/dist/lib/init-summary.js.map +7 -0
  125. package/dist/lib/module-github.js +98 -0
  126. package/dist/lib/module-github.js.map +7 -0
  127. package/dist/lib/module-identity.js +131 -0
  128. package/dist/lib/module-identity.js.map +7 -0
  129. package/dist/lib/module-inspection.js +124 -0
  130. package/dist/lib/module-inspection.js.map +7 -0
  131. package/dist/lib/module-install.js +150 -21
  132. package/dist/lib/module-install.js.map +3 -3
  133. package/dist/lib/module-package.js +205 -15
  134. package/dist/lib/module-package.js.map +2 -2
  135. package/dist/lib/modules-config.js +69 -30
  136. package/dist/lib/modules-config.js.map +2 -2
  137. package/dist/lib/resolver.js +33 -34
  138. package/dist/lib/resolver.js.map +2 -2
  139. package/dist/mercato.js +17 -37
  140. package/dist/mercato.js.map +2 -2
  141. package/package.json +7 -7
  142. package/src/__tests__/mercato.test.ts +71 -4
  143. package/src/i18n/de.json +4 -0
  144. package/src/i18n/en.json +4 -0
  145. package/src/i18n/es.json +4 -0
  146. package/src/i18n/ko.json +4 -0
  147. package/src/i18n/pl.json +4 -0
  148. package/src/lib/__fixtures__/official-module-package/dist/modules/test_package/widgets/injection/test/widget.js +1 -0
  149. package/src/lib/__integration__/TC-INT-007.spec.ts +219 -19
  150. package/src/lib/__integration__/TC-INT-009.spec.ts +279 -0
  151. package/src/lib/__tests__/eject.test.ts +98 -4
  152. package/src/lib/__tests__/init-secrets.test.ts +85 -1
  153. package/src/lib/__tests__/init-summary.test.ts +50 -0
  154. package/src/lib/__tests__/module-github.test.ts +108 -0
  155. package/src/lib/__tests__/module-identity.test.ts +144 -0
  156. package/src/lib/__tests__/module-inspection.test.ts +195 -0
  157. package/src/lib/__tests__/module-install.test.ts +399 -10
  158. package/src/lib/__tests__/module-package.test.ts +154 -0
  159. package/src/lib/__tests__/modules-config.test.ts +55 -0
  160. package/src/lib/__tests__/resolver.packages.test.ts +29 -0
  161. package/src/lib/eject.ts +28 -10
  162. package/src/lib/generators/__tests__/module-id-collisions.test.ts +146 -0
  163. package/src/lib/generators/__tests__/module-package-sources.test.ts +19 -2
  164. package/src/lib/generators/__tests__/module-requirements.test.ts +56 -0
  165. package/src/lib/generators/entity-ids.ts +2 -0
  166. package/src/lib/generators/module-di.ts +2 -0
  167. package/src/lib/generators/module-entities.ts +2 -0
  168. package/src/lib/generators/module-package-sources.ts +7 -5
  169. package/src/lib/generators/module-registry.ts +33 -115
  170. package/src/lib/generators/openapi.ts +3 -0
  171. package/src/lib/generators/source-exports.ts +93 -0
  172. package/src/lib/init-secrets.ts +37 -0
  173. package/src/lib/init-summary.ts +54 -0
  174. package/src/lib/module-github.ts +109 -0
  175. package/src/lib/module-identity.ts +145 -0
  176. package/src/lib/module-inspection.ts +133 -0
  177. package/src/lib/module-install.ts +170 -22
  178. package/src/lib/module-package.ts +243 -16
  179. package/src/lib/modules-config.ts +80 -32
  180. package/src/lib/resolver.ts +35 -39
  181. package/src/mercato.ts +18 -44
  182. package/src/types/cross-spawn.d.ts +2 -2
@@ -3,6 +3,7 @@ import os from 'node:os'
3
3
  import path from 'node:path'
4
4
  import type { PackageResolver } from '../resolver'
5
5
  import {
6
+ assertModulePackageExports,
6
7
  discoverModulesInPackage,
7
8
  parsePackageNameFromSpec,
8
9
  readOfficialModulePackageFromRoot,
@@ -57,6 +58,39 @@ describe('module-package', () => {
57
58
  expect(modulePackage.distModuleDir).toContain(path.join('dist', 'modules', 'test_package'))
58
59
  })
59
60
 
61
+ it('reads static dependency ids from the module descriptor', () => {
62
+ const packageRoot = path.join(tmpDir, 'package-with-dependencies')
63
+ copyDir(fixturePackageRoot, packageRoot)
64
+ fs.writeFileSync(
65
+ path.join(packageRoot, 'src', 'modules', 'test_package', 'index.ts'),
66
+ [
67
+ 'export const metadata = {',
68
+ " title: 'Test Package',",
69
+ " requires: ['customers', 'catalog'],",
70
+ ' ejectable: true,',
71
+ '} as const',
72
+ '',
73
+ ].join('\n'),
74
+ )
75
+
76
+ const modulePackage = readOfficialModulePackageFromRoot(packageRoot)
77
+
78
+ expect(modulePackage.moduleInfo.requires).toEqual(['customers', 'catalog'])
79
+ })
80
+
81
+ it('rejects a dynamic dependency declaration that cannot be checked safely', () => {
82
+ const packageRoot = path.join(tmpDir, 'package-with-dynamic-dependencies')
83
+ copyDir(fixturePackageRoot, packageRoot)
84
+ fs.writeFileSync(
85
+ path.join(packageRoot, 'src', 'modules', 'test_package', 'index.ts'),
86
+ "const dependencies = ['customers']\nexport const metadata = { requires: dependencies }\n",
87
+ )
88
+
89
+ expect(() => readOfficialModulePackageFromRoot(packageRoot)).toThrow(
90
+ 'must declare "requires" as a static string array',
91
+ )
92
+ })
93
+
60
94
  it('validates a third-party (non-@open-mercato) module package by structure', () => {
61
95
  const packageRoot = path.join(tmpDir, 'node_modules', '@fast-white-cat', 'integration-ksef-direct')
62
96
  for (const base of ['src', 'dist']) {
@@ -104,6 +138,126 @@ describe('module-package', () => {
104
138
  expect(modulePackage.metadata.moduleId).toBe('test_package')
105
139
  })
106
140
 
141
+ it('resolves an installed alias while retaining the canonical manifest name', () => {
142
+ const appDir = path.join(tmpDir, 'app')
143
+ const packageRoot = path.join(appDir, 'node_modules', 'candidate_consents')
144
+ fs.mkdirSync(appDir, { recursive: true })
145
+ copyDir(fixturePackageRoot, packageRoot)
146
+ fs.writeFileSync(path.join(appDir, 'package.json'), JSON.stringify({ dependencies: { candidate_consents: 'npm:@open-mercato/test-package@1.0.0' } }))
147
+ fs.writeFileSync(path.join(packageRoot, 'package.json'), JSON.stringify({ name: '@open-mercato/test-package', exports: { '.': './dist/modules/test_package/index.js', './modules/*': { import: './dist/modules/*.js' } } }))
148
+ const resolver = { getAppDir: () => appDir, getPackageRoot: () => packageRoot } as unknown as PackageResolver
149
+
150
+ const result = resolveInstalledOfficialModulePackage(resolver, 'candidate_consents')
151
+
152
+ expect(result.packageName).toBe('@open-mercato/test-package')
153
+ expect(fs.realpathSync(result.packageRoot)).toBe(fs.realpathSync(packageRoot))
154
+ expect(result.metadata.moduleId).toBe('test_package')
155
+ })
156
+
157
+ it('rejects an unrelated package root returned for an unobserved binding', () => {
158
+ const packageRoot = path.join(tmpDir, 'unrelated-package')
159
+ copyDir(fixturePackageRoot, packageRoot)
160
+ const resolver = { getAppDir: () => tmpDir, getPackageRoot: () => packageRoot } as unknown as PackageResolver
161
+
162
+ expect(() => resolveInstalledOfficialModulePackage(resolver, 'uninstalled_binding')).toThrow('does not match requested package binding')
163
+ expect(() => readOfficialModulePackageFromRoot(packageRoot, 'uninstalled_binding')).toThrow('does not match requested package')
164
+ })
165
+
166
+ it('rejects module subpaths hidden by package exports before enabling an alias', () => {
167
+ const appDir = path.join(tmpDir, 'app')
168
+ const packageRoot = path.join(appDir, 'node_modules', 'candidate_consents')
169
+ copyDir(fixturePackageRoot, packageRoot)
170
+ fs.writeFileSync(path.join(packageRoot, 'package.json'), JSON.stringify({ name: '@open-mercato/test-package', exports: { '.': './dist/modules/test_package/index.js' } }))
171
+ const resolver = { getAppDir: () => appDir, getPackageRoot: () => packageRoot } as unknown as PackageResolver
172
+
173
+ expect(() => resolveInstalledOfficialModulePackage(resolver, 'candidate_consents')).toThrow('candidate_consents/modules/test_package/index')
174
+ })
175
+
176
+ it('rejects an installed package whose generated source entrypoint has no runtime artifact', () => {
177
+ const appDir = path.join(tmpDir, 'app')
178
+ const packageRoot = path.join(appDir, 'node_modules', 'candidate_consents')
179
+ copyDir(fixturePackageRoot, packageRoot)
180
+ fs.writeFileSync(path.join(packageRoot, 'src', 'modules', 'test_package', 'di.ts'), 'export const register = () => undefined\n')
181
+ fs.writeFileSync(path.join(packageRoot, 'package.json'), JSON.stringify({ name: '@open-mercato/test-package', exports: { './modules/test_package/*': './dist/modules/test_package/*.js' } }))
182
+ const resolver = { getAppDir: () => appDir, getPackageRoot: () => packageRoot } as unknown as PackageResolver
183
+
184
+ expect(() => resolveInstalledOfficialModulePackage(resolver, 'candidate_consents')).toThrow('candidate_consents/modules/test_package/di')
185
+ })
186
+
187
+ it('accepts ESM import-only and nested node conditions without requiring CommonJS exports', () => {
188
+ const files = ['dist/modules/consents/index.js', 'dist/modules/consents/di.js', 'esm/consents/index.mjs', 'esm/consents/di.mjs']
189
+ const manifest = { exports: { './modules/consents/*': { types: './types/*.d.ts', node: { import: './esm/consents/*.mjs', require: null }, default: './dist/modules/consents/*.js' } } }
190
+
191
+ expect(() => assertModulePackageExports(manifest, 'consents_candidate', 'consents', files)).not.toThrow()
192
+ })
193
+
194
+ it('honors an explicit blocked subpath over a wildcard and rejects missing mapped targets', () => {
195
+ const files = ['dist/modules/consents/index.js', 'dist/modules/consents/di.js']
196
+ const blocked = { exports: { './modules/*': './dist/modules/*.js', './modules/consents/di': null } }
197
+ const missing = { exports: { './modules/*': { import: './missing/*.js' } } }
198
+
199
+ expect(() => assertModulePackageExports(blocked, 'consents_candidate', 'consents', files)).toThrow('consents_candidate/modules/consents/di')
200
+ expect(() => assertModulePackageExports(missing, 'consents_candidate', 'consents', files)).toThrow('consents_candidate/modules/consents/index')
201
+ })
202
+
203
+ it('rejects a source-discovered generated entrypoint without a compiled runtime artifact', () => {
204
+ const files = [
205
+ 'src/modules/consents/index.ts',
206
+ 'src/modules/consents/di.ts',
207
+ 'dist/modules/consents/index.js',
208
+ ]
209
+ const manifest = { exports: { './modules/consents/*': './dist/modules/consents/*.js' } }
210
+
211
+ expect(() => assertModulePackageExports(manifest, 'consents_candidate', 'consents', files)).toThrow('missing the compiled runtime entrypoint')
212
+ })
213
+
214
+ it('keeps private helper files private while validating generated convention imports', () => {
215
+ const files = ['dist/modules/consents/index.js', 'dist/modules/consents/lib/private.js']
216
+ const manifest = { exports: { './modules/consents/index': './dist/modules/consents/index.js' } }
217
+
218
+ expect(() => assertModulePackageExports(manifest, 'consents_candidate', 'consents', files)).not.toThrow()
219
+ })
220
+
221
+ it('validates selective exports only for discovered entry points and leaves their private helpers private', () => {
222
+ const publicFiles = ['index', 'backend/tasks/page', 'frontend/consents/page', 'widgets/injection/consents/widget', 'commands/create']
223
+ const privateFiles = ['backend/tasks/TaskForm', 'backend/tasks/helpers', 'frontend/consents/ConsentForm', 'widgets/injection/consents/ConsentField', 'commands/index', 'commands/shared', 'commands/factory', 'commands/create.test', 'backend/__tests__/page', 'widgets/__mocks__/injection/consents/widget', 'workers/__tests__/handler', 'subscribers/__mocks__/handler']
224
+ const files = [...publicFiles, ...privateFiles].flatMap((file) => [
225
+ `src/modules/consents/${file}${file.endsWith('/page') || /TaskForm|ConsentForm|ConsentField/.test(file) ? '.tsx' : '.ts'}`,
226
+ `dist/modules/consents/${file}.js`,
227
+ ])
228
+ const exports = Object.fromEntries(publicFiles.map((file) => [`./modules/consents/${file}`, `./dist/modules/consents/${file}.js`]))
229
+
230
+ expect(() => assertModulePackageExports({ exports }, 'consents_candidate', 'consents', files)).not.toThrow()
231
+ const missingPageExport = { ...exports, './modules/consents/backend/tasks/page': null }
232
+ expect(() => assertModulePackageExports({ exports: missingPageExport }, 'consents_candidate', 'consents', files, () => 'export default function Page() {}')).toThrow('consents_candidate/modules/consents/backend/tasks/page')
233
+ })
234
+
235
+ it('uses source exports to distinguish private lowercase helpers from generated pages and HTTP handlers', () => {
236
+ const source = {
237
+ 'index.ts': 'export const metadata = {}',
238
+ 'backend/tasks/page.tsx': 'export default function Page() {}',
239
+ 'backend/tasks/form-utils.tsx': 'export function renderLabel() {}',
240
+ 'backend/tasks/form-utils.meta.ts': 'export const metadata = {}',
241
+ 'api/helpers.ts': 'export function parseInput() {}',
242
+ 'api/orders.ts': 'export { listOrders as GET }',
243
+ }
244
+ const files = Object.keys(source).flatMap((file) => [`src/modules/consents/${file}`, `dist/modules/consents/${file.replace(/\.tsx?$/, '.js')}`])
245
+ const exports = Object.fromEntries(['index', 'backend/tasks/page', 'api/orders'].map((file) => [`./modules/consents/${file}`, `./dist/modules/consents/${file}.js`]))
246
+ const readSource = jest.fn((file: string) => source[file.replace('src/modules/consents/', '') as keyof typeof source])
247
+ expect(() => assertModulePackageExports({ exports }, 'candidate', 'consents', files, readSource)).not.toThrow()
248
+ expect(() => assertModulePackageExports({ exports: { ...exports, './modules/consents/backend/tasks/page': null } }, 'candidate', 'consents', files, readSource)).toThrow('backend/tasks/page')
249
+ expect(() => assertModulePackageExports({ exports: { ...exports, './modules/consents/api/orders': null } }, 'candidate', 'consents', files, readSource)).toThrow('api/orders')
250
+ expect(() => assertModulePackageExports({ exports: { './modules/consents/index': './dist/modules/consents/index.js' } }, 'candidate', 'consents', files)).not.toThrow()
251
+ })
252
+
253
+ it('rejects invalid manifest names instead of attributing them to an import binding', () => {
254
+ const packageRoot = path.join(tmpDir, 'invalid-name')
255
+ copyDir(fixturePackageRoot, packageRoot)
256
+ fs.writeFileSync(path.join(packageRoot, 'package.json'), JSON.stringify({ name: 'invalid package' }))
257
+
258
+ expect(() => readOfficialModulePackageFromRoot(packageRoot)).toThrow('invalid "name" field')
259
+ })
260
+
107
261
  it('discovers all modules in a multi-module package', () => {
108
262
  const packageRoot = path.join(tmpDir, 'multi-module-package')
109
263
  fs.mkdirSync(path.join(packageRoot, 'src', 'modules', 'alpha'), { recursive: true })
@@ -6,7 +6,10 @@ import {
6
6
  ensureModuleRegistration,
7
7
  ModuleGatedOffError,
8
8
  setModuleRegistrationSource,
9
+ prepareModuleRegistration,
10
+ prepareModuleRegistrationSource,
9
11
  } from '../modules-config'
12
+ import { readEnabledModulesFromConfig } from '../resolver'
10
13
 
11
14
  describe('modules-config', () => {
12
15
  let tmpDir: string
@@ -19,6 +22,41 @@ describe('modules-config', () => {
19
22
  fs.rmSync(tmpDir, { recursive: true, force: true })
20
23
  })
21
24
 
25
+ it('preserves module locator when registering and ejecting', () => {
26
+ const filePath = path.join(tmpDir, 'modules.ts')
27
+ fs.writeFileSync(filePath, 'export const enabledModules = []\n')
28
+ ensureModuleRegistration(filePath, { id: 'consents', from: '@pkarw/consents', locator: 'github:pkarw/consents', packageName: '@pkarw/consents' })
29
+ expect(fs.readFileSync(filePath, 'utf8')).toContain("locator: 'github:pkarw/consents'")
30
+ setModuleRegistrationSource(filePath, 'consents', '@app')
31
+ expect(fs.readFileSync(filePath, 'utf8')).toContain("packageName: '@pkarw/consents'")
32
+ expect(fs.readFileSync(filePath, 'utf8')).toContain("locator: 'github:pkarw/consents'")
33
+ })
34
+
35
+ it('prepares add and eject config edits without writing the existing configuration', () => {
36
+ const filePath = path.join(tmpDir, 'modules.ts')
37
+ const source = 'export const enabledModules = []\n'
38
+ fs.writeFileSync(filePath, source)
39
+
40
+ const added = prepareModuleRegistration(source, { id: 'consents', from: 'consents_candidate' })
41
+ const ejected = prepareModuleRegistrationSource(added.source, 'consents', '@app', { packageName: '@publisher/consents', locator: 'npm:@publisher/consents' })
42
+
43
+ expect(added).toMatchObject({ changed: true, registeredAs: 'consents_candidate' })
44
+ expect(ejected).toContain("from: '@app'")
45
+ expect(ejected).toContain("packageName: '@publisher/consents'")
46
+ expect(ejected).toContain("locator: 'npm:@publisher/consents'")
47
+ expect(fs.readFileSync(filePath, 'utf8')).toBe(source)
48
+ expect(prepareModuleRegistration(added.source, { id: 'consents', from: 'consents_candidate' })).toEqual({ source: added.source, changed: false, registeredAs: 'consents_candidate' })
49
+ })
50
+
51
+ it('accepts quoted property names when preparing an existing registration update', () => {
52
+ const source = 'export const enabledModules = [{ "id": "consents", "from": "consents_candidate" }]\n'
53
+
54
+ const updated = prepareModuleRegistrationSource(source, 'consents', '@app')
55
+
56
+ expect(updated).toContain('"from": "@app"')
57
+ expect(updated).toContain('"id": "consents"')
58
+ })
59
+
22
60
  it('adds a package-backed module without touching conditional pushes', () => {
23
61
  const filePath = path.join(tmpDir, 'modules.ts')
24
62
  fs.writeFileSync(
@@ -530,3 +568,20 @@ describe('modules-config', () => {
530
568
  })
531
569
  })
532
570
  })
571
+
572
+
573
+ describe('module provenance updates', () => {
574
+ it('updates only top-level origin properties and preserves nested overrides', () => {
575
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'module-config-provenance-'))
576
+ const modulesPath = path.join(root, 'modules.ts')
577
+ try {
578
+ fs.writeFileSync(modulesPath, "export const enabledModules = [{ id: 'consents', overrides: { custom: { from: 'nested', locator: 'keep-locator', packageName: 'keep-package' } } }]\n")
579
+ setModuleRegistrationSource(modulesPath, 'consents', '@app', { locator: 'github:pkarw/consents', packageName: '@pkarw/consents' })
580
+ const source = fs.readFileSync(modulesPath, 'utf8')
581
+ expect(source).toContain("custom: { from: 'nested', locator: 'keep-locator', packageName: 'keep-package' }")
582
+ expect(readEnabledModulesFromConfig(modulesPath)).toEqual([{ id: 'consents', from: '@app', locator: 'github:pkarw/consents', packageName: '@pkarw/consents' }])
583
+ } finally {
584
+ fs.rmSync(root, { recursive: true, force: true })
585
+ }
586
+ })
587
+ })
@@ -0,0 +1,29 @@
1
+ import fs from 'node:fs'
2
+ import os from 'node:os'
3
+ import path from 'node:path'
4
+ import { createResolver } from '../resolver'
5
+
6
+ describe('installed package paths in monorepos', () => {
7
+ let root: string
8
+ beforeEach(() => {
9
+ root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'resolver-packages-')))
10
+ fs.mkdirSync(path.join(root, 'packages', 'core', 'src', 'modules'), { recursive: true })
11
+ fs.writeFileSync(path.join(root, 'packages', 'core', 'package.json'), JSON.stringify({ name: '@open-mercato/core' }))
12
+ fs.mkdirSync(path.join(root, 'node_modules', '@open-mercato'), { recursive: true })
13
+ fs.symlinkSync(path.join(root, 'packages', 'core'), path.join(root, 'node_modules', '@open-mercato', 'core'))
14
+ fs.mkdirSync(path.join(root, 'apps', 'mercato', 'src'), { recursive: true })
15
+ fs.writeFileSync(path.join(root, 'apps', 'mercato', 'package.json'), JSON.stringify({ name: 'app' }))
16
+ })
17
+ afterEach(() => fs.rmSync(root, { recursive: true, force: true }))
18
+
19
+ it.each(['@pkarw/consents', '@open-mercato/consents', 'consents'])('resolves installed package %s without falling back to core', (packageName) => {
20
+ const packageRoot = path.join(root, 'node_modules', packageName)
21
+ fs.mkdirSync(path.join(packageRoot, 'dist', 'modules', 'consents'), { recursive: true })
22
+ fs.writeFileSync(path.join(packageRoot, 'package.json'), JSON.stringify({ name: packageName }))
23
+ const resolver = createResolver(path.join(root, 'apps', 'mercato'))
24
+ expect(resolver.isMonorepo()).toBe(true)
25
+ expect(resolver.getPackageRoot(packageName)).toBe(packageRoot)
26
+ expect(resolver.getModulePaths({ id: 'consents', from: packageName }).pkgBase).toBe(path.join(packageRoot, 'dist', 'modules', 'consents'))
27
+ expect(resolver.getModulePaths({ id: 'customers', from: '@open-mercato/core' }).pkgBase).toBe(path.join(root, 'packages', 'core', 'src', 'modules', 'customers'))
28
+ })
29
+ })
package/src/lib/eject.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  import path from 'node:path'
2
2
  import fs from 'node:fs'
3
- import { setModuleRegistrationSource } from './modules-config'
3
+ import { prepareModuleRegistrationSource, setModuleRegistrationSource } from './modules-config'
4
4
  import type { PackageResolver, ModuleEntry } from './resolver'
5
- import { resolveInstalledOfficialModulePackage } from './module-package'
5
+ import { isModulePackageExportError, resolveInstalledOfficialModulePackage, resolveInstalledPackageRoot } from './module-package'
6
+ import { normalizeModuleLocator } from '@open-mercato/shared/modules/module-locators'
7
+ import { getEnabledModuleIdentities } from './module-identity'
6
8
 
7
9
  type ModuleMetadata = {
8
10
  ejectable?: boolean
@@ -178,8 +180,8 @@ export function copyDirRecursive(src: string, dest: string): void {
178
180
  }
179
181
  }
180
182
 
181
- export function updateModulesTs(modulesPath: string, moduleId: string): void {
182
- setModuleRegistrationSource(modulesPath, moduleId, '@app')
183
+ export function updateModulesTs(modulesPath: string, moduleId: string, provenance?: Pick<ModuleEntry, 'locator' | 'packageName'>): void {
184
+ setModuleRegistrationSource(modulesPath, moduleId, '@app', provenance)
183
185
  }
184
186
 
185
187
  export type EjectableModule = {
@@ -239,7 +241,8 @@ function resolveModuleSource(
239
241
  description: modulePackage.moduleInfo.description,
240
242
  },
241
243
  }
242
- } catch {
244
+ } catch (error) {
245
+ if (isModulePackageExportError(error)) throw error
243
246
  return { pkgBase, metadata: fallbackMetadata }
244
247
  }
245
248
  }
@@ -268,7 +271,9 @@ export function listEjectableModules(resolver: PackageResolver): EjectableModule
268
271
 
269
272
  export function ejectModule(resolver: PackageResolver, moduleId: string): void {
270
273
  const modules = resolver.loadEnabledModules()
271
- const entry = modules.find((m: ModuleEntry) => m.id === moduleId)
274
+ const identities = getEnabledModuleIdentities(resolver, modules)
275
+ const matches = modules.filter((module: ModuleEntry) => module.id === moduleId)
276
+ const entry = matches[0]
272
277
 
273
278
  if (!entry) {
274
279
  throw new Error(
@@ -302,13 +307,26 @@ export function ejectModule(resolver: PackageResolver, moduleId: string): void {
302
307
  if (fs.existsSync(appBase)) {
303
308
  throw new Error(
304
309
  `Destination directory already exists: ${appBase}. ` +
305
- `Remove it first or resolve the conflict manually.`,
310
+ `Keep the existing local source and review the destination conflict before ejecting.`,
306
311
  )
307
312
  }
308
313
 
314
+ const locators = identities.find((identity) => identity.id === moduleId)?.locators ?? []
315
+ const packageRoot = resolveInstalledPackageRoot(resolver, entry.from || '@open-mercato/core')
316
+ const manifest: unknown = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'))
317
+ const packageName = manifest && typeof manifest === 'object' && 'name' in manifest && typeof manifest.name === 'string'
318
+ ? manifest.name
319
+ : undefined
320
+ if (!packageName || normalizeModuleLocator(`npm:${packageName}`) !== `npm:${packageName}`) {
321
+ throw new Error(`Cannot retain source provenance for module "${moduleId}": the installed package manifest has no valid name.`)
322
+ }
323
+ const locator = locators.find((identity) => identity.startsWith('github:')) ?? locators.find((identity) => identity.startsWith('npm:'))
324
+ const provenance = { ...(packageName ? { packageName } : {}), ...(locator ? { locator } : {}) }
325
+ const modulesPath = resolver.getModulesConfigPath()
326
+ const originalConfig = fs.readFileSync(modulesPath, 'utf8')
327
+ const updatedConfig = prepareModuleRegistrationSource(originalConfig, moduleId, '@app', provenance)
328
+
309
329
  copyDirRecursive(pkgBase, appBase)
310
330
  rewriteCrossModuleImports(pkgBase, appBase, moduleId, entry.from || '@open-mercato/core')
311
-
312
- const modulesPath = resolver.getModulesConfigPath()
313
- updateModulesTs(modulesPath, moduleId)
331
+ fs.writeFileSync(modulesPath, updatedConfig)
314
332
  }
@@ -0,0 +1,146 @@
1
+ import fs from 'node:fs'
2
+ import os from 'node:os'
3
+ import path from 'node:path'
4
+ import { createResolver, type ModuleEntry, type PackageResolver } from '../../resolver'
5
+ import { generateEntityIds } from '../entity-ids'
6
+ import { generateModuleDi } from '../module-di'
7
+ import { generateModuleEntities } from '../module-entities'
8
+ import { generateModulePackageSources } from '../module-package-sources'
9
+ import { generateModuleRegistries, generateModuleRegistry, generateModuleRegistryApp, generateModuleRegistryCli } from '../module-registry'
10
+ import { generateOpenApi } from '../openapi'
11
+
12
+ const generators = [
13
+ generateEntityIds,
14
+ generateModuleDi,
15
+ generateModuleEntities,
16
+ generateModulePackageSources,
17
+ generateModuleRegistry,
18
+ generateModuleRegistryApp,
19
+ generateModuleRegistryCli,
20
+ generateModuleRegistries,
21
+ generateOpenApi,
22
+ ]
23
+
24
+ describe('effective module ID preflight', () => {
25
+ let root: string
26
+ beforeEach(() => { root = fs.mkdtempSync(path.join(os.tmpdir(), 'module-id-collisions-')) })
27
+ afterEach(() => { fs.rmSync(root, { recursive: true, force: true }) })
28
+
29
+ function resolverFor(entries: ModuleEntry[]): PackageResolver {
30
+ const outputDir = path.join(root, '.mercato', 'generated')
31
+ return {
32
+ isMonorepo: () => true,
33
+ getRootDir: () => root,
34
+ getAppDir: () => root,
35
+ getOutputDir: () => outputDir,
36
+ getModulesConfigPath: () => path.join(root, 'src', 'modules.ts'),
37
+ discoverPackages: () => [],
38
+ loadEnabledModules: () => entries,
39
+ getModulePaths: (entry) => ({ appBase: path.join(root, 'src', 'modules', entry.id), pkgBase: path.join(root, 'packages', 'core', 'src', 'modules', entry.id) }),
40
+ getModuleImportBase: (entry) => ({ appBase: `@/modules/${entry.id}`, pkgBase: `@open-mercato/core/modules/${entry.id}` }),
41
+ getPackageRoot: () => path.join(root, 'packages', 'core'),
42
+ getPackageOutputDir: () => path.join(root, 'packages', 'core', 'generated'),
43
+ }
44
+ }
45
+
46
+ it.each(generators)('rejects separate providers before any output write in %p', async (generate) => {
47
+ const resolver = resolverFor([
48
+ { id: 'consents', from: '@publisher/consents', locator: 'npm:@publisher/consents' },
49
+ { id: 'consents', from: '@alias/consents', locator: 'github:other/consents' },
50
+ ])
51
+ fs.mkdirSync(resolver.getOutputDir(), { recursive: true })
52
+ const sentinel = path.join(resolver.getOutputDir(), 'existing.generated.ts')
53
+ fs.writeFileSync(sentinel, 'existing output\n')
54
+ const timestamp = fs.statSync(sentinel).mtimeMs
55
+ await expect(generate({ resolver, quiet: true })).rejects.toThrow(/Duplicate enabled module ID 'consents'.*@publisher\/consents.*@alias\/consents/)
56
+ expect(fs.readdirSync(resolver.getOutputDir())).toEqual(['existing.generated.ts'])
57
+ expect(fs.readFileSync(sentinel, 'utf8')).toBe('existing output\n')
58
+ expect(fs.statSync(sentinel).mtimeMs).toBe(timestamp)
59
+ expect(fs.existsSync(resolver.getPackageOutputDir('@open-mercato/core'))).toBe(false)
60
+ })
61
+
62
+ it.each(generators)('rejects exact repeated registrations without creating output directories in %p', async (generate) => {
63
+ const resolver = resolverFor([{ id: 'consents', from: '@app' }, { id: 'consents', from: '@app' }])
64
+ await expect(generate({ resolver, quiet: true })).rejects.toThrow(/Duplicate enabled module ID 'consents'.*@app/)
65
+ expect(fs.existsSync(resolver.getOutputDir())).toBe(false)
66
+ })
67
+
68
+ it.each(generators)('rejects duplicate source registrations after runtime resolver deduplication in %p', async (generate) => {
69
+ fs.mkdirSync(path.join(root, 'src'), { recursive: true })
70
+ fs.writeFileSync(path.join(root, 'package.json'), '{"name":"module-fixture"}')
71
+ fs.writeFileSync(path.join(root, 'src', 'modules.ts'), "export const enabledModules = [{ id: 'consents', from: '@app' }, { id: 'consents', from: '@app' }]\n")
72
+ const warning = jest.spyOn(console, 'warn').mockImplementation(() => {})
73
+ try {
74
+ const resolver = createResolver(root)
75
+ expect(resolver.loadEnabledModules()).toHaveLength(1)
76
+ fs.mkdirSync(resolver.getOutputDir(), { recursive: true })
77
+ const sentinel = path.join(resolver.getOutputDir(), 'existing.generated.ts')
78
+ fs.writeFileSync(sentinel, 'existing output\n')
79
+ const timestamp = fs.statSync(sentinel).mtimeMs
80
+ await expect(generate({ resolver, quiet: true })).rejects.toThrow(/Duplicate enabled module ID 'consents'/)
81
+ expect(fs.readdirSync(resolver.getOutputDir())).toEqual(['existing.generated.ts'])
82
+ expect(fs.readFileSync(sentinel, 'utf8')).toBe('existing output\n')
83
+ expect(fs.statSync(sentinel).mtimeMs).toBe(timestamp)
84
+ } finally {
85
+ warning.mockRestore()
86
+ }
87
+ })
88
+
89
+ it.each(generators)('rejects contradictory retained origin before any output write in %p', async (generate) => {
90
+ const resolver = resolverFor([{ id: 'consents', from: '@publisher/consents', locator: 'github:publisher/consents' }])
91
+ fs.mkdirSync(resolver.getPackageRoot('@publisher/consents'), { recursive: true })
92
+ fs.writeFileSync(path.join(resolver.getPackageRoot('@publisher/consents'), 'package.json'), JSON.stringify({ name: '@publisher/consents' }))
93
+ fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify({ dependencies: { '@publisher/consents': '1.0.0' } }))
94
+ await expect(generate({ resolver, quiet: true })).rejects.toThrow(/declared origin.*disagrees with observed origin/)
95
+ expect(fs.existsSync(resolver.getOutputDir())).toBe(false)
96
+ })
97
+
98
+ it('preserves one registration with local entity and DI overlays', async () => {
99
+ const resolver = resolverFor([{ id: 'customers', from: '@open-mercato/core' }])
100
+ const roots = resolver.getModulePaths(resolver.loadEnabledModules()[0])
101
+ for (const base of [roots.appBase, roots.pkgBase]) fs.mkdirSync(path.join(base, 'data'), { recursive: true })
102
+ fs.writeFileSync(path.join(roots.appBase, 'data', 'entities.ts'), 'export class LocalCustomer {}')
103
+ fs.writeFileSync(path.join(roots.pkgBase, 'data', 'entities.ts'), 'export class PackageCustomer {}')
104
+ fs.writeFileSync(path.join(roots.appBase, 'di.ts'), 'export function register() {}')
105
+ fs.writeFileSync(path.join(roots.pkgBase, 'di.ts'), 'export function register() {}')
106
+ await generateModuleEntities({ resolver, quiet: true })
107
+ await generateModuleDi({ resolver, quiet: true })
108
+ expect(fs.readFileSync(path.join(resolver.getOutputDir(), 'entities.generated.ts'), 'utf8')).toContain('@/modules/customers/data/entities')
109
+ const di = fs.readFileSync(path.join(resolver.getOutputDir(), 'di.generated.ts'), 'utf8')
110
+ expect(di).toContain('@/modules/customers/di')
111
+ expect(di).not.toContain('@open-mercato/core/modules/customers/di')
112
+ })
113
+
114
+ it.each(['npm', 'github'])('validates only the effective %s provider in mutually exclusive branches', async (provider) => {
115
+ fs.mkdirSync(path.join(root, 'src'), { recursive: true })
116
+ fs.writeFileSync(path.join(root, 'package.json'), '{"name":"module-fixture"}')
117
+ fs.writeFileSync(path.join(root, 'src', 'modules.ts'), `
118
+ export const enabledModules = []
119
+ if (process.env.MODULE_PROVIDER === 'npm') {
120
+ enabledModules.push({ id: 'consents', from: '@app', locator: 'npm:@publisher/consents' })
121
+ } else {
122
+ enabledModules.push({ id: 'consents', from: '@app', locator: 'github:publisher/consents' })
123
+ }
124
+ `)
125
+ const previousProvider = process.env.MODULE_PROVIDER
126
+ process.env.MODULE_PROVIDER = provider
127
+ try {
128
+ const resolver = createResolver(root)
129
+ expect(resolver.loadEnabledModules()).toEqual([{ id: 'consents', from: '@app', locator: provider === 'npm' ? 'npm:@publisher/consents' : 'github:publisher/consents' }])
130
+ await expect(generateModulePackageSources({ resolver, quiet: true })).resolves.toMatchObject({ errors: [] })
131
+ } finally {
132
+ if (previousProvider === undefined) delete process.env.MODULE_PROVIDER
133
+ else process.env.MODULE_PROVIDER = previousProvider
134
+ }
135
+ })
136
+
137
+ it('keeps explicitly disabled local modules out of an empty effective configuration', async () => {
138
+ fs.mkdirSync(path.join(root, 'src', 'modules', 'parked'), { recursive: true })
139
+ fs.writeFileSync(path.join(root, 'package.json'), '{"name":"module-fixture"}')
140
+ fs.writeFileSync(path.join(root, 'src', 'modules.ts'), 'export const enabledModules = []')
141
+ const resolver = createResolver(root)
142
+ expect(resolver.loadEnabledModules()).toEqual([])
143
+ await generateModuleDi({ resolver, quiet: true })
144
+ expect(fs.readFileSync(path.join(resolver.getOutputDir(), 'di.generated.ts'), 'utf8')).not.toContain('parked')
145
+ })
146
+ })
@@ -76,6 +76,23 @@ describe('generateModulePackageSources', () => {
76
76
  expect(output).toContain('node_modules/@open-mercato/test-package/src/**/*.{ts,tsx}')
77
77
  })
78
78
 
79
+ it('includes source paths through the selected installed npm alias binding', async () => {
80
+ const packageRoot = path.join(tmpDir, 'app', 'node_modules', 'consents_candidate')
81
+ copyDir(fixturePackageRoot, packageRoot)
82
+ const packageJsonPath = path.join(packageRoot, 'package.json')
83
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')) as Record<string, unknown>
84
+ packageJson.name = '@publisher/consents'
85
+ fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson))
86
+ const resolver = createResolver(tmpDir, packageRoot, 'consents_candidate')
87
+ fs.writeFileSync(path.join(resolver.getAppDir(), 'package.json'), JSON.stringify({ dependencies: { consents_candidate: 'npm:@publisher/consents@1.0.0' } }))
88
+
89
+ await generateModulePackageSources({ resolver, quiet: true })
90
+
91
+ const output = fs.readFileSync(path.join(resolver.getOutputDir(), 'module-package-sources.css'), 'utf8')
92
+ expect(output).toContain('node_modules/consents_candidate/src/**/*.{ts,tsx}')
93
+ expect(output).not.toContain('node_modules/@publisher/consents')
94
+ })
95
+
79
96
  it('does not checksum unrelated files below the installed package root', async () => {
80
97
  const packageRoot = path.join(tmpDir, 'node_modules', '@open-mercato', 'test-package')
81
98
  copyDir(fixturePackageRoot, packageRoot)
@@ -146,12 +163,12 @@ describe('generateModulePackageSources', () => {
146
163
  expect(fs.statSync(outFile).mtimeMs).toBe(outputMtimeBefore)
147
164
  })
148
165
 
149
- it('keeps official package validation as the source inclusion gate', async () => {
166
+ it('keeps package manifest validation as the source inclusion gate', async () => {
150
167
  const packageRoot = path.join(tmpDir, 'node_modules', '@open-mercato', 'test-package')
151
168
  copyDir(fixturePackageRoot, packageRoot)
152
169
  const packageJsonPath = path.join(packageRoot, 'package.json')
153
170
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')) as Record<string, unknown>
154
- packageJson.name = '@open-mercato/different-package'
171
+ packageJson.name = 'invalid package name'
155
172
  fs.writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`)
156
173
  const resolver = createResolver(tmpDir, packageRoot, '@open-mercato/test-package')
157
174
 
@@ -0,0 +1,56 @@
1
+ import fs from 'node:fs'
2
+ import os from 'node:os'
3
+ import path from 'node:path'
4
+ import { generateModuleRegistry, generateModuleRegistryApp, generateModuleRegistryCli } from '../module-registry'
5
+ import type { ModuleEntry, PackageResolver } from '../../resolver'
6
+
7
+ describe('canonical module dependencies across registries', () => {
8
+ let root: string
9
+ beforeEach(() => { root = fs.mkdtempSync(path.join(os.tmpdir(), 'module-requires-')) })
10
+ afterEach(() => { fs.rmSync(root, { recursive: true, force: true }); jest.restoreAllMocks() })
11
+
12
+ function fixture(requirement: string): PackageResolver {
13
+ const enabled: ModuleEntry[] = [
14
+ { id: 'dependent', from: '@app' },
15
+ { id: 'consents', from: '@app', locator: 'github:pkarw/marketing-consents', packageName: '@pkarw/marketing_consents' },
16
+ ]
17
+ const moduleBase = (moduleId: string) => path.join(root, 'src', 'modules', moduleId)
18
+ for (const entry of enabled) fs.mkdirSync(moduleBase(entry.id), { recursive: true })
19
+ fs.writeFileSync(path.join(moduleBase('dependent'), 'index.ts'), `exports.metadata = { requires: [${JSON.stringify(requirement)}] }`)
20
+ fs.writeFileSync(path.join(moduleBase('consents'), 'index.ts'), 'exports.metadata = {}')
21
+ return {
22
+ isMonorepo: () => true,
23
+ getRootDir: () => root,
24
+ getAppDir: () => root,
25
+ getOutputDir: () => path.join(root, '.mercato', 'generated'),
26
+ getModulesConfigPath: () => path.join(root, 'src', 'modules.ts'),
27
+ discoverPackages: () => [],
28
+ loadEnabledModules: () => enabled,
29
+ getModulePaths: (entry) => ({ appBase: moduleBase(entry.id), pkgBase: path.join(root, 'empty', entry.id) }),
30
+ getModuleImportBase: (entry) => ({ appBase: `@/modules/${entry.id}`, pkgBase: `@unused/modules/${entry.id}` }),
31
+ getPackageOutputDir: () => path.join(root, '.mercato', 'generated'),
32
+ getPackageRoot: () => root,
33
+ }
34
+ }
35
+
36
+ it.each([generateModuleRegistry, generateModuleRegistryApp, generateModuleRegistryCli])('satisfies canonical source dependencies in %p', async (generate) => {
37
+ const resolver = fixture('https://github.com/PKARW/marketing-consents.git#main')
38
+ const result = await generate({ resolver, quiet: true })
39
+ expect(result.errors).toEqual([])
40
+ })
41
+
42
+ it.each([generateModuleRegistry, generateModuleRegistryApp, generateModuleRegistryCli])('satisfies retained npm identity after npm module ejection in %p', async (generate) => {
43
+ const resolver = fixture('npm:@pkarw/marketing_consents@next')
44
+ resolver.loadEnabledModules()[1].locator = 'npm:@pkarw/marketing_consents'
45
+ const result = await generate({ resolver, quiet: true })
46
+ expect(result.errors).toEqual([])
47
+ })
48
+
49
+ it.each([generateModuleRegistry, generateModuleRegistryApp, generateModuleRegistryCli])('rejects wrong publisher despite matching module name in %p', async (generate) => {
50
+ const resolver = fixture('github:other/marketing-consents')
51
+ const errors = jest.spyOn(console, 'error').mockImplementation(() => {})
52
+ jest.spyOn(process, 'exit').mockImplementation(() => { throw new Error('dependency failure') })
53
+ await expect(generate({ resolver, quiet: true })).rejects.toThrow('dependency failure')
54
+ expect(errors).toHaveBeenCalledWith("Fix: npx create-mercato-module install 'github:other/marketing-consents'")
55
+ })
56
+ })
@@ -1,3 +1,4 @@
1
+ import { getEnabledModuleIdentities } from '../module-identity'
1
2
  import fs from 'node:fs'
2
3
  import path from 'node:path'
3
4
  import ts from 'typescript-js'
@@ -331,6 +332,7 @@ export async function generateEntityIds(options: EntityIdsOptions): Promise<Gene
331
332
  const checksumFile = path.join(outputDir, 'entities.ids.generated.checksum')
332
333
 
333
334
  const entries = resolver.loadEnabledModules()
335
+ getEnabledModuleIdentities(resolver, entries)
334
336
 
335
337
  const consolidated: Record<string, Record<string, string>> = {}
336
338
  const grouped: Record<GroupKey, Record<string, Record<string, string>>> = {}
@@ -1,3 +1,4 @@
1
+ import { getEnabledModuleIdentities } from '../module-identity'
1
2
  import fs from 'node:fs'
2
3
  import path from 'node:path'
3
4
  import { VariableDeclarationKind } from 'ts-morph'
@@ -34,6 +35,7 @@ export async function generateModuleDi(options: ModuleDiOptions): Promise<Genera
34
35
  const checksumFile = path.join(outputDir, 'di.generated.checksum')
35
36
 
36
37
  const mods = resolver.loadEnabledModules()
38
+ getEnabledModuleIdentities(resolver, mods)
37
39
  const imports: Array<{ name: string; moduleSpecifier: string }> = []
38
40
  const registrars: string[] = []
39
41
  let i = 0
@@ -1,3 +1,4 @@
1
+ import { getEnabledModuleIdentities } from '../module-identity'
1
2
  import fs from 'node:fs'
2
3
  import path from 'node:path'
3
4
  import { VariableDeclarationKind } from 'ts-morph'
@@ -54,6 +55,7 @@ export async function generateModuleEntities(options: ModuleEntitiesOptions): Pr
54
55
  const checksumFile = path.join(outputDir, 'entities.generated.checksum')
55
56
 
56
57
  const mods = resolver.loadEnabledModules()
58
+ getEnabledModuleIdentities(resolver, mods)
57
59
  const imports: Array<{ name: string; moduleSpecifier: string }> = []
58
60
  const entitySources: Array<{ importName: string; moduleId: string }> = []
59
61
  const classNameEntries: EntityClassNameEntry[] = []