@open-mercato/cli 0.7.1-develop.7123.1.25365e718d → 0.7.1-develop.7131.1.20abf9e885

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 (86) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/agentic/guides/module-facts.json +144 -144
  3. package/dist/agentic/guides/module-facts.v2.json +144 -144
  4. package/dist/agentic/guides/modules/ai_assistant/index.md +1 -1
  5. package/dist/agentic/guides/modules/api_docs/index.md +1 -1
  6. package/dist/agentic/guides/modules/api_keys/index.md +1 -1
  7. package/dist/agentic/guides/modules/attachments/index.md +1 -1
  8. package/dist/agentic/guides/modules/audit_logs/index.md +1 -1
  9. package/dist/agentic/guides/modules/auth/index.md +1 -1
  10. package/dist/agentic/guides/modules/business_rules/index.md +1 -1
  11. package/dist/agentic/guides/modules/catalog/index.md +1 -1
  12. package/dist/agentic/guides/modules/channel_apns/index.md +1 -1
  13. package/dist/agentic/guides/modules/channel_discord/index.md +1 -1
  14. package/dist/agentic/guides/modules/channel_expo/index.md +1 -1
  15. package/dist/agentic/guides/modules/channel_fcm/index.md +1 -1
  16. package/dist/agentic/guides/modules/channel_gmail/index.md +1 -1
  17. package/dist/agentic/guides/modules/channel_imap/index.md +1 -1
  18. package/dist/agentic/guides/modules/checkout/index.md +1 -1
  19. package/dist/agentic/guides/modules/communication_channels/index.md +1 -1
  20. package/dist/agentic/guides/modules/configs/index.md +1 -1
  21. package/dist/agentic/guides/modules/content/index.md +1 -1
  22. package/dist/agentic/guides/modules/currencies/index.md +1 -1
  23. package/dist/agentic/guides/modules/customer_accounts/index.md +1 -1
  24. package/dist/agentic/guides/modules/customers/active-extension-bindings.md +9 -9
  25. package/dist/agentic/guides/modules/customers/index.md +1 -1
  26. package/dist/agentic/guides/modules/dashboards/index.md +1 -1
  27. package/dist/agentic/guides/modules/data_sync/index.md +1 -1
  28. package/dist/agentic/guides/modules/design_system/index.md +1 -1
  29. package/dist/agentic/guides/modules/devices/index.md +1 -1
  30. package/dist/agentic/guides/modules/dictionaries/active-extension-bindings.md +2 -2
  31. package/dist/agentic/guides/modules/dictionaries/index.md +1 -1
  32. package/dist/agentic/guides/modules/directory/active-extension-bindings.md +1 -1
  33. package/dist/agentic/guides/modules/directory/index.md +1 -1
  34. package/dist/agentic/guides/modules/documents/index.md +1 -1
  35. package/dist/agentic/guides/modules/entities/index.md +1 -1
  36. package/dist/agentic/guides/modules/eudr/index.md +1 -1
  37. package/dist/agentic/guides/modules/events/index.md +1 -1
  38. package/dist/agentic/guides/modules/feature_toggles/active-extension-bindings.md +1 -1
  39. package/dist/agentic/guides/modules/feature_toggles/index.md +1 -1
  40. package/dist/agentic/guides/modules/gateway_stripe/index.md +1 -1
  41. package/dist/agentic/guides/modules/generators/index.md +1 -1
  42. package/dist/agentic/guides/modules/inbox_ops/index.md +1 -1
  43. package/dist/agentic/guides/modules/integrations/index.md +1 -1
  44. package/dist/agentic/guides/modules/messages/index.md +1 -1
  45. package/dist/agentic/guides/modules/notifications/index.md +1 -1
  46. package/dist/agentic/guides/modules/onboarding/index.md +1 -1
  47. package/dist/agentic/guides/modules/payment_gateways/index.md +1 -1
  48. package/dist/agentic/guides/modules/perspectives/index.md +1 -1
  49. package/dist/agentic/guides/modules/planner/index.md +1 -1
  50. package/dist/agentic/guides/modules/portal/index.md +1 -1
  51. package/dist/agentic/guides/modules/progress/index.md +1 -1
  52. package/dist/agentic/guides/modules/push_notifications/index.md +1 -1
  53. package/dist/agentic/guides/modules/query_index/index.md +1 -1
  54. package/dist/agentic/guides/modules/record_locks/index.md +1 -1
  55. package/dist/agentic/guides/modules/resources/active-extension-bindings.md +1 -1
  56. package/dist/agentic/guides/modules/resources/index.md +1 -1
  57. package/dist/agentic/guides/modules/sales/active-extension-bindings.md +1 -1
  58. package/dist/agentic/guides/modules/sales/index.md +1 -1
  59. package/dist/agentic/guides/modules/scheduler/index.md +1 -1
  60. package/dist/agentic/guides/modules/search/index.md +1 -1
  61. package/dist/agentic/guides/modules/security/index.md +1 -1
  62. package/dist/agentic/guides/modules/shipping_carriers/index.md +1 -1
  63. package/dist/agentic/guides/modules/sso/index.md +1 -1
  64. package/dist/agentic/guides/modules/staff/index.md +1 -1
  65. package/dist/agentic/guides/modules/storage_s3/index.md +1 -1
  66. package/dist/agentic/guides/modules/sync_akeneo/index.md +1 -1
  67. package/dist/agentic/guides/modules/sync_excel/index.md +1 -1
  68. package/dist/agentic/guides/modules/system_status_overlays/index.md +1 -1
  69. package/dist/agentic/guides/modules/translations/active-extension-bindings.md +1 -1
  70. package/dist/agentic/guides/modules/translations/index.md +1 -1
  71. package/dist/agentic/guides/modules/warranty_claims/index.md +1 -1
  72. package/dist/agentic/guides/modules/webhooks/index.md +1 -1
  73. package/dist/agentic/guides/modules/wms/index.md +1 -1
  74. package/dist/agentic/guides/modules/workflows/index.md +1 -1
  75. package/dist/agentic/guides/reference-module-facts.json +1 -1
  76. package/dist/agentic/guides/upstream/BACKWARD_COMPATIBILITY.md +4 -2
  77. package/dist/agentic/guides/upstream/manifest.json +2 -2
  78. package/dist/lib/generators/entity-class-declarations.js +186 -0
  79. package/dist/lib/generators/entity-class-declarations.js.map +7 -0
  80. package/dist/lib/generators/module-entities.js +28 -0
  81. package/dist/lib/generators/module-entities.js.map +2 -2
  82. package/package.json +6 -6
  83. package/src/lib/generators/__tests__/entity-class-declarations.test.ts +287 -0
  84. package/src/lib/generators/__tests__/module-entities.test.ts +219 -0
  85. package/src/lib/generators/entity-class-declarations.ts +255 -0
  86. package/src/lib/generators/module-entities.ts +54 -1
@@ -0,0 +1,287 @@
1
+ import fs from 'node:fs'
2
+ import os from 'node:os'
3
+ import path from 'node:path'
4
+ import { parseEntityClassNames } from '../entity-class-declarations'
5
+
6
+ let tmpDir: string
7
+
8
+ function writeSource(content: string, fileName = 'entities.ts'): string {
9
+ const filePath = path.join(tmpDir, fileName)
10
+ fs.writeFileSync(filePath, content)
11
+ return filePath
12
+ }
13
+
14
+ beforeEach(() => {
15
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'entity-class-declarations-test-'))
16
+ })
17
+
18
+ afterEach(() => {
19
+ fs.rmSync(tmpDir, { recursive: true, force: true })
20
+ })
21
+
22
+ describe('parseEntityClassNames', () => {
23
+ it('collects exported classes decorated with @Entity()', () => {
24
+ const filePath = writeSource(`
25
+ import { Entity, PrimaryKey } from '@mikro-orm/decorators/legacy'
26
+
27
+ @Entity({ tableName: 'invoices' })
28
+ export class Invoice {
29
+ @PrimaryKey({ type: 'uuid' })
30
+ id!: string
31
+ }
32
+ `)
33
+
34
+ expect(parseEntityClassNames(filePath)).toEqual(['Invoice'])
35
+ })
36
+
37
+ it('looks past comments and stacked decorators', () => {
38
+ const filePath = writeSource(`
39
+ import { Entity, Index, Unique } from '@mikro-orm/decorators/legacy'
40
+
41
+ @Entity({ tableName: 'invoices' })
42
+ // The unique constraint bounds the lookup.
43
+ @Unique({ properties: ['number'] })
44
+ @Index({
45
+ name: 'invoices_number_idx',
46
+ expression: 'create index "invoices_number_idx" on "invoices" ("number")',
47
+ })
48
+ export class Invoice {}
49
+ `)
50
+
51
+ expect(parseEntityClassNames(filePath)).toEqual(['Invoice'])
52
+ })
53
+
54
+ it('ignores exported classes that are not entities', () => {
55
+ const filePath = writeSource(`
56
+ import { Entity } from '@mikro-orm/decorators/legacy'
57
+
58
+ export class InvoiceBuilder {}
59
+
60
+ @Entity({ tableName: 'invoices' })
61
+ export class Invoice {}
62
+ `)
63
+
64
+ expect(parseEntityClassNames(filePath)).toEqual(['Invoice'])
65
+ })
66
+
67
+ it('ignores entity classes that are not exported', () => {
68
+ const filePath = writeSource(`
69
+ import { Entity } from '@mikro-orm/decorators/legacy'
70
+
71
+ @Entity({ tableName: 'invoices' })
72
+ class Invoice {}
73
+ `)
74
+
75
+ expect(parseEntityClassNames(filePath)).toEqual([])
76
+ })
77
+
78
+ it('handles a local export list', () => {
79
+ const filePath = writeSource(`
80
+ import { Entity } from '@mikro-orm/decorators/legacy'
81
+
82
+ @Entity({ tableName: 'invoices' })
83
+ class Invoice {}
84
+
85
+ export { Invoice }
86
+ `)
87
+
88
+ expect(parseEntityClassNames(filePath)).toEqual(['Invoice'])
89
+ })
90
+
91
+ it('accepts a bare @Entity decorator', () => {
92
+ const filePath = writeSource(`
93
+ import { Entity } from '@mikro-orm/decorators/legacy'
94
+
95
+ @Entity
96
+ export class Invoice {}
97
+ `)
98
+
99
+ expect(parseEntityClassNames(filePath)).toEqual(['Invoice'])
100
+ })
101
+
102
+ it('returns nothing for an unreadable file instead of throwing', () => {
103
+ expect(parseEntityClassNames(path.join(tmpDir, 'missing.ts'))).toEqual([])
104
+ })
105
+
106
+ it('recognises an aliased Entity import', () => {
107
+ const filePath = writeSource(`
108
+ import { Entity as OrmEntity } from '@mikro-orm/decorators/legacy'
109
+
110
+ @OrmEntity({ tableName: 'invoices' })
111
+ export class Invoice {}
112
+ `)
113
+
114
+ expect(parseEntityClassNames(filePath)).toEqual(['Invoice'])
115
+ })
116
+
117
+ it('recognises a namespace-qualified Entity decorator', () => {
118
+ const filePath = writeSource(`
119
+ import * as orm from '@mikro-orm/decorators/legacy'
120
+
121
+ @orm.Entity({ tableName: 'invoices' })
122
+ export class Invoice {}
123
+ `)
124
+
125
+ expect(parseEntityClassNames(filePath)).toEqual(['Invoice'])
126
+ })
127
+
128
+ it('does not treat an unrelated decorator as an entity', () => {
129
+ const filePath = writeSource(`
130
+ @Injectable()
131
+ export class InvoiceService {}
132
+ `)
133
+
134
+ expect(parseEntityClassNames(filePath)).toEqual([])
135
+ })
136
+
137
+ it('ignores an Entity decorator imported from an unrelated library', () => {
138
+ // Any library may export an `Entity` decorator; only MikroORM's collides in the ORM
139
+ // metadata registry, so only a decorator bound to a @mikro-orm/* import counts.
140
+ const filePath = writeSource(`
141
+ import { Entity } from 'some-other-orm'
142
+
143
+ @Entity({ tableName: 'invoices' })
144
+ export class Invoice {}
145
+ `)
146
+
147
+ expect(parseEntityClassNames(filePath)).toEqual([])
148
+ })
149
+
150
+ it('ignores a namespace-qualified Entity decorator from an unrelated library', () => {
151
+ const filePath = writeSource(`
152
+ import * as other from 'some-other-orm'
153
+
154
+ @other.Entity({ tableName: 'invoices' })
155
+ export class Invoice {}
156
+ `)
157
+
158
+ expect(parseEntityClassNames(filePath)).toEqual([])
159
+ })
160
+
161
+ it('ignores an Entity decorator with no import to bind it', () => {
162
+ const filePath = writeSource(`
163
+ @Entity({ tableName: 'invoices' })
164
+ export class Invoice {}
165
+ `)
166
+
167
+ expect(parseEntityClassNames(filePath)).toEqual([])
168
+ })
169
+
170
+ it('parses a .tsx entity file as JSX rather than dropping declarations', () => {
171
+ // MODULE_CODE_EXTENSIONS accepts .tsx, where `<T>` is JSX, not a type assertion.
172
+ const filePath = writeSource(`
173
+ import { Entity } from '@mikro-orm/decorators/legacy'
174
+
175
+ export const icon = () => <span>invoice</span>
176
+
177
+ @Entity({ tableName: 'invoices' })
178
+ export class Invoice {}
179
+ `, 'entities.tsx')
180
+
181
+ expect(parseEntityClassNames(filePath)).toEqual(['Invoice'])
182
+ })
183
+ })
184
+
185
+ describe('parseEntityClassNames on compiled package output', () => {
186
+ // A package that ships dist only hands the generator its built entity file, where a
187
+ // decorated class has become a class expression plus a decorator-helper call. Without
188
+ // this the standalone generator sees no entities at all and misses every collision.
189
+ it('reads an esbuild ESM emit', () => {
190
+ const filePath = writeSource(`
191
+ var __decorateClass = (decorators, target) => target;
192
+ import { Entity, PrimaryKey } from "@mikro-orm/decorators/legacy";
193
+ let ApiKey = class {
194
+ };
195
+ __decorateClass([
196
+ PrimaryKey({ type: "uuid" })
197
+ ], ApiKey.prototype, "id", 2);
198
+ ApiKey = __decorateClass([
199
+ Entity({ tableName: "api_keys" })
200
+ ], ApiKey);
201
+ export {
202
+ ApiKey
203
+ };
204
+ `, 'entities.js')
205
+
206
+ expect(parseEntityClassNames(filePath)).toEqual(['ApiKey'])
207
+ })
208
+
209
+ it('reads a tsc CommonJS emit', () => {
210
+ const filePath = writeSource(`
211
+ "use strict";
212
+ Object.defineProperty(exports, "__esModule", { value: true });
213
+ exports.ApiKey = void 0;
214
+ const legacy_1 = require("@mikro-orm/decorators/legacy");
215
+ let ApiKey = class ApiKey {
216
+ };
217
+ ApiKey = __decorate([
218
+ (0, legacy_1.Entity)({ tableName: "api_keys" })
219
+ ], ApiKey);
220
+ exports.ApiKey = ApiKey;
221
+ `, 'entities.js')
222
+
223
+ expect(parseEntityClassNames(filePath)).toEqual(['ApiKey'])
224
+ })
225
+
226
+ it('reads a bundler export table', () => {
227
+ const filePath = writeSource(`
228
+ import { Entity } from "@mikro-orm/core";
229
+ var entities_exports = {};
230
+ __export(entities_exports, {
231
+ ApiKey: () => ApiKey
232
+ });
233
+ let ApiKey = class {
234
+ };
235
+ ApiKey = __decorateClass([
236
+ Entity({ tableName: "api_keys" })
237
+ ], ApiKey);
238
+ `, 'entities.js')
239
+
240
+ expect(parseEntityClassNames(filePath)).toEqual(['ApiKey'])
241
+ })
242
+
243
+ it('ignores a compiled class that is never exported', () => {
244
+ const filePath = writeSource(`
245
+ import { Entity } from "@mikro-orm/decorators/legacy";
246
+ let Internal = class {
247
+ };
248
+ Internal = __decorateClass([
249
+ Entity({ tableName: "internal" })
250
+ ], Internal);
251
+ `, 'entities.js')
252
+
253
+ expect(parseEntityClassNames(filePath)).toEqual([])
254
+ })
255
+
256
+ it('ignores a compiled class decorated by an unrelated library', () => {
257
+ const filePath = writeSource(`
258
+ import { Entity } from "some-other-orm";
259
+ let Invoice = class {
260
+ };
261
+ Invoice = __decorateClass([
262
+ Entity({ tableName: "invoices" })
263
+ ], Invoice);
264
+ export {
265
+ Invoice
266
+ };
267
+ `, 'entities.js')
268
+
269
+ expect(parseEntityClassNames(filePath)).toEqual([])
270
+ })
271
+
272
+ it('ignores property decorators, which share the helper call', () => {
273
+ const filePath = writeSource(`
274
+ import { Entity, Property } from "@mikro-orm/decorators/legacy";
275
+ let Helper = class {
276
+ };
277
+ __decorateClass([
278
+ Property({ type: "text" })
279
+ ], Helper.prototype, "name", 2);
280
+ export {
281
+ Helper
282
+ };
283
+ `, 'entities.js')
284
+
285
+ expect(parseEntityClassNames(filePath)).toEqual([])
286
+ })
287
+ })
@@ -135,3 +135,222 @@ describe('generateModuleEntities', () => {
135
135
  expect(secondStat.mtimeMs).toBe(firstStat.mtimeMs)
136
136
  })
137
137
  })
138
+
139
+ describe('generateModuleEntities duplicate entity class names', () => {
140
+ const entitySource = (className: string, tableName: string) =>
141
+ `import { Entity } from '@mikro-orm/decorators/legacy'\n\n@Entity({ tableName: '${tableName}' })\nexport class ${className} {}\n`
142
+
143
+ let warnSpy: jest.SpyInstance
144
+
145
+ beforeEach(() => {
146
+ warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {})
147
+ })
148
+
149
+ afterEach(() => {
150
+ warnSpy.mockRestore()
151
+ })
152
+
153
+ function moduleWithEntities(id: string, content: string): ModuleEntry {
154
+ touchFile(path.join(tmpDir, 'packages', 'core', 'src', 'modules', id, 'data', 'entities.ts'), content)
155
+ return { id, from: '@open-mercato/core' }
156
+ }
157
+
158
+ it('warns when two modules declare the same entity class name, and still generates', async () => {
159
+ const modules = [
160
+ moduleWithEntities('billing', entitySource('Invoice', 'billing_invoices')),
161
+ moduleWithEntities('subscriptions', entitySource('Invoice', 'subscription_invoices')),
162
+ ]
163
+
164
+ const result = await generateModuleEntities({ resolver: createMockResolver(tmpDir, modules), quiet: true })
165
+
166
+ expect(warnSpy).toHaveBeenCalledTimes(1)
167
+ const message = warnSpy.mock.calls[0][0] as string
168
+ expect(message).toContain('[Entities Warning]')
169
+ expect(message).toContain('Invoice')
170
+ expect(message).toContain('billing')
171
+ expect(message).toContain('subscriptions')
172
+ // Warning-only by design: generation still succeeds.
173
+ expect(result.errors).toEqual([])
174
+ expect(readGenerated(tmpDir)).toContain('enhanceEntities')
175
+ })
176
+
177
+ it('stays silent when entity class names are unique', async () => {
178
+ const modules = [
179
+ moduleWithEntities('billing', entitySource('Invoice', 'billing_invoices')),
180
+ moduleWithEntities('subscriptions', entitySource('Subscription', 'subscriptions')),
181
+ ]
182
+
183
+ await generateModuleEntities({ resolver: createMockResolver(tmpDir, modules), quiet: true })
184
+
185
+ expect(warnSpy).not.toHaveBeenCalled()
186
+ })
187
+
188
+ it('reads the app override, not the package file, when an app supplies a package module\'s entities', async () => {
189
+ // The registry imports the app override for such a module, so the check must read the
190
+ // same file — parsing the package tree would report names that are never registered.
191
+ touchFile(
192
+ path.join(tmpDir, 'packages', 'core', 'src', 'modules', 'billing', 'data', 'entities.ts'),
193
+ entitySource('PackageInvoice', 'billing_invoices'),
194
+ )
195
+ touchFile(
196
+ path.join(tmpDir, 'app', 'src', 'modules', 'billing', 'data', 'entities.ts'),
197
+ entitySource('Invoice', 'billing_invoices'),
198
+ )
199
+ const modules: ModuleEntry[] = [
200
+ { id: 'billing', from: '@open-mercato/core' },
201
+ moduleWithEntities('subscriptions', entitySource('Invoice', 'subscription_invoices')),
202
+ ]
203
+
204
+ const result = await generateModuleEntities({ resolver: createMockResolver(tmpDir, modules), quiet: true })
205
+
206
+ expect(result.errors).toEqual([])
207
+ expect(readGenerated(tmpDir)).toContain('from "@/modules/billing/data/entities"')
208
+ expect(warnSpy).toHaveBeenCalledTimes(1)
209
+ const message = warnSpy.mock.calls[0][0] as string
210
+ expect(message).toContain('Invoice')
211
+ expect(message).toContain(path.join('app', 'src', 'modules', 'billing', 'data', 'entities.ts'))
212
+ expect(message).not.toContain('PackageInvoice')
213
+ })
214
+
215
+ it('stays silent when only one of the colliding classes is an entity', async () => {
216
+ const modules = [
217
+ moduleWithEntities('billing', entitySource('Invoice', 'billing_invoices')),
218
+ moduleWithEntities('subscriptions', 'export class Invoice {}\n'),
219
+ ]
220
+
221
+ await generateModuleEntities({ resolver: createMockResolver(tmpDir, modules), quiet: true })
222
+
223
+ expect(warnSpy).not.toHaveBeenCalled()
224
+ })
225
+ })
226
+
227
+ describe('generateModuleEntities duplicate detection in standalone installs', () => {
228
+ // A standalone install resolves a package module to its compiled dist tree, where a
229
+ // decorated class is no longer a class declaration. Published packages ship
230
+ // src/modules too, so parsing prefers that mirror and falls back to the compiled file.
231
+ const PACKAGE = '@open-mercato/core'
232
+
233
+ let warnSpy: jest.SpyInstance
234
+
235
+ beforeEach(() => {
236
+ warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {})
237
+ })
238
+
239
+ afterEach(() => {
240
+ warnSpy.mockRestore()
241
+ })
242
+
243
+ function packageRoot(): string {
244
+ return path.join(tmpDir, 'app', 'node_modules', PACKAGE)
245
+ }
246
+
247
+ function createStandaloneResolver(enabled: ModuleEntry[]): PackageResolver {
248
+ const outputDir = path.join(tmpDir, 'app', '.mercato', 'generated')
249
+ fs.mkdirSync(outputDir, { recursive: true })
250
+
251
+ return {
252
+ isMonorepo: () => false,
253
+ getRootDir: () => path.join(tmpDir, 'app'),
254
+ getAppDir: () => path.join(tmpDir, 'app'),
255
+ getOutputDir: () => outputDir,
256
+ getModulesConfigPath: () => path.join(tmpDir, 'app', 'src', 'modules.ts'),
257
+ discoverPackages: () => [],
258
+ loadEnabledModules: () => enabled,
259
+ getModulePaths: (entry: ModuleEntry) => ({
260
+ appBase: path.join(tmpDir, 'app', 'src', 'modules', entry.id),
261
+ pkgBase: path.join(packageRoot(), 'dist', 'modules', entry.id),
262
+ }),
263
+ getModuleImportBase: (entry: ModuleEntry) => ({
264
+ appBase: `@/modules/${entry.id}`,
265
+ pkgBase: `${PACKAGE}/modules/${entry.id}`,
266
+ }),
267
+ getPackageOutputDir: () => outputDir,
268
+ getPackageRoot: () => packageRoot(),
269
+ }
270
+ }
271
+
272
+ const compiledEntity = (className: string, tableName: string) => `
273
+ import { Entity } from "@mikro-orm/decorators/legacy";
274
+ let ${className} = class {
275
+ };
276
+ ${className} = __decorateClass([
277
+ Entity({ tableName: "${tableName}" })
278
+ ], ${className});
279
+ export {
280
+ ${className}
281
+ };
282
+ `
283
+
284
+ const sourceEntity = (className: string, tableName: string) =>
285
+ `import { Entity } from '@mikro-orm/decorators/legacy'\n\n@Entity({ tableName: '${tableName}' })\nexport class ${className} {}\n`
286
+
287
+ function distModule(id: string, className: string, tableName: string): ModuleEntry {
288
+ touchFile(path.join(packageRoot(), 'dist', 'modules', id, 'data', 'entities.js'), compiledEntity(className, tableName))
289
+ return { id, from: PACKAGE }
290
+ }
291
+
292
+ function withSourceMirror(id: string, className: string, tableName: string): void {
293
+ touchFile(path.join(packageRoot(), 'src', 'modules', id, 'data', 'entities.ts'), sourceEntity(className, tableName))
294
+ }
295
+
296
+ it('detects a package-to-package collision through the shipped source mirror', async () => {
297
+ const modules = [
298
+ distModule('billing', 'Invoice', 'billing_invoices'),
299
+ distModule('subscriptions', 'Invoice', 'subscription_invoices'),
300
+ ]
301
+ withSourceMirror('billing', 'Invoice', 'billing_invoices')
302
+ withSourceMirror('subscriptions', 'Invoice', 'subscription_invoices')
303
+
304
+ const result = await generateModuleEntities({ resolver: createStandaloneResolver(modules), quiet: true })
305
+
306
+ expect(warnSpy).toHaveBeenCalledTimes(1)
307
+ const message = warnSpy.mock.calls[0][0] as string
308
+ expect(message).toContain('Invoice')
309
+ expect(message).toContain(path.join('src', 'modules', 'billing', 'data', 'entities.ts'))
310
+ expect(result.errors).toEqual([])
311
+ // The runtime import still points at the compiled package entry.
312
+ expect(readGenerated(tmpDir)).toContain(`from "${PACKAGE}/modules/billing/data/entities"`)
313
+ })
314
+
315
+ it('detects the collision from compiled output when a package ships dist only', async () => {
316
+ const modules = [
317
+ distModule('billing', 'Invoice', 'billing_invoices'),
318
+ distModule('subscriptions', 'Invoice', 'subscription_invoices'),
319
+ ]
320
+
321
+ await generateModuleEntities({ resolver: createStandaloneResolver(modules), quiet: true })
322
+
323
+ expect(warnSpy).toHaveBeenCalledTimes(1)
324
+ const message = warnSpy.mock.calls[0][0] as string
325
+ expect(message).toContain('Invoice')
326
+ expect(message).toContain(path.join('dist', 'modules', 'subscriptions', 'data', 'entities.js'))
327
+ })
328
+
329
+ it('detects a package-to-app collision', async () => {
330
+ const modules = [
331
+ distModule('billing', 'Invoice', 'billing_invoices'),
332
+ { id: 'reporting', from: '@app' } as ModuleEntry,
333
+ ]
334
+ withSourceMirror('billing', 'Invoice', 'billing_invoices')
335
+ touchFile(
336
+ path.join(tmpDir, 'app', 'src', 'modules', 'reporting', 'data', 'entities.ts'),
337
+ sourceEntity('Invoice', 'reporting_invoices'),
338
+ )
339
+
340
+ await generateModuleEntities({ resolver: createStandaloneResolver(modules), quiet: true })
341
+
342
+ expect(warnSpy).toHaveBeenCalledTimes(1)
343
+ expect(warnSpy.mock.calls[0][0] as string).toContain('reporting')
344
+ })
345
+
346
+ it('stays silent when compiled package entity class names are unique', async () => {
347
+ const modules = [
348
+ distModule('billing', 'Invoice', 'billing_invoices'),
349
+ distModule('subscriptions', 'Subscription', 'subscriptions'),
350
+ ]
351
+
352
+ await generateModuleEntities({ resolver: createStandaloneResolver(modules), quiet: true })
353
+
354
+ expect(warnSpy).not.toHaveBeenCalled()
355
+ })
356
+ })