@open-mercato/shared 0.7.1-develop.7123.1.25365e718d → 0.7.1-develop.7130.1.fef2396fd8

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.
@@ -0,0 +1,172 @@
1
+ import {
2
+ DUPLICATE_ENTITY_CLASS_NAMES_REASON,
3
+ DUPLICATE_ENTITY_CLASS_NAMES_REMEDIATION,
4
+ findDuplicateEntityClassNames,
5
+ formatDuplicateEntityClassNamesWarning,
6
+ toDuplicateEntityClassNameFields,
7
+ } from '../duplicateEntityClassNames'
8
+
9
+ describe('findDuplicateEntityClassNames', () => {
10
+ it('reports nothing for an empty or single-entry list', () => {
11
+ expect(findDuplicateEntityClassNames([])).toEqual([])
12
+ expect(findDuplicateEntityClassNames([{ className: 'Invoice', moduleId: 'billing' }])).toEqual([])
13
+ })
14
+
15
+ it('reports nothing when class names differ', () => {
16
+ const groups = findDuplicateEntityClassNames([
17
+ { className: 'Invoice', moduleId: 'billing' },
18
+ { className: 'Ledger', moduleId: 'subscriptions' },
19
+ ])
20
+
21
+ expect(groups).toEqual([])
22
+ })
23
+
24
+ it('reports a name declared by two modules', () => {
25
+ const groups = findDuplicateEntityClassNames([
26
+ { className: 'Invoice', moduleId: 'billing', sourcePath: 'modules/billing/data/entities.ts' },
27
+ { className: 'Invoice', moduleId: 'subscriptions', sourcePath: 'modules/subscriptions/data/entities.ts' },
28
+ ])
29
+
30
+ expect(groups).toEqual([
31
+ {
32
+ className: 'Invoice',
33
+ sources: [
34
+ { moduleId: 'billing', sourcePath: 'modules/billing/data/entities.ts' },
35
+ { moduleId: 'subscriptions', sourcePath: 'modules/subscriptions/data/entities.ts' },
36
+ ],
37
+ },
38
+ ])
39
+ })
40
+
41
+ it('treats identical module and path as the same class, not a collision', () => {
42
+ const groups = findDuplicateEntityClassNames([
43
+ { className: 'Invoice', moduleId: 'billing', sourcePath: 'modules/billing/data/entities.ts' },
44
+ { className: 'Invoice', moduleId: 'billing', sourcePath: 'modules/billing/data/entities.ts' },
45
+ ])
46
+
47
+ expect(groups).toEqual([])
48
+ })
49
+
50
+ it('treats a shared target as the same class reached twice', () => {
51
+ const target = class Invoice {}
52
+ const groups = findDuplicateEntityClassNames([
53
+ { className: 'Invoice', moduleId: 'billing', target },
54
+ { className: 'Invoice', moduleId: 'subscriptions', target },
55
+ ])
56
+
57
+ expect(groups).toEqual([])
58
+ })
59
+
60
+ it('collects three-way collisions and multiple names in one pass', () => {
61
+ const groups = findDuplicateEntityClassNames([
62
+ { className: 'Invoice', moduleId: 'billing' },
63
+ { className: 'Ledger', moduleId: 'billing' },
64
+ { className: 'Invoice', moduleId: 'subscriptions' },
65
+ { className: 'Ledger', moduleId: 'subscriptions' },
66
+ { className: 'Ledger', moduleId: 'reporting' },
67
+ ])
68
+
69
+ expect(groups.map((group) => group.className)).toEqual(['Invoice', 'Ledger'])
70
+ expect(groups[1].sources.map((source) => source.moduleId)).toEqual(['billing', 'subscriptions', 'reporting'])
71
+ })
72
+
73
+ it('keeps unidentifiable entries distinct so a collision fails open', () => {
74
+ // Neither entry names a module, a file, or a class, so nothing distinguishes them.
75
+ // Collapsing them into one bucket would hide a genuine collision.
76
+ const groups = findDuplicateEntityClassNames([{ className: 'Invoice' }, { className: 'Invoice' }])
77
+
78
+ expect(groups).toHaveLength(1)
79
+ expect(groups[0].sources).toHaveLength(2)
80
+ })
81
+
82
+ it('skips entries without a class name', () => {
83
+ expect(findDuplicateEntityClassNames([{ className: '' }, { className: '' }])).toEqual([])
84
+ })
85
+ })
86
+
87
+ describe('formatDuplicateEntityClassNamesWarning', () => {
88
+ it('names the class, the modules and the source files', () => {
89
+ const message = formatDuplicateEntityClassNamesWarning([
90
+ {
91
+ className: 'Invoice',
92
+ sources: [
93
+ { moduleId: 'billing', sourcePath: '/repo/modules/billing/data/entities.ts' },
94
+ { moduleId: 'subscriptions', sourcePath: '/repo/modules/subscriptions/data/entities.ts' },
95
+ ],
96
+ },
97
+ ])
98
+
99
+ expect(message).toContain('Duplicate entity class name(s) defined by more than one enabled module: "Invoice".')
100
+ expect(message).toContain(' Invoice')
101
+ expect(message).toContain(' - billing (/repo/modules/billing/data/entities.ts)')
102
+ expect(message).toContain(' - subscriptions (/repo/modules/subscriptions/data/entities.ts)')
103
+ expect(message).toContain('Rename all but one of the colliding classes')
104
+ })
105
+
106
+ it('lists every colliding name in one message', () => {
107
+ const message = formatDuplicateEntityClassNamesWarning([
108
+ { className: 'Invoice', sources: [{ moduleId: 'a' }, { moduleId: 'b' }] },
109
+ { className: 'Ledger', sources: [{ moduleId: 'a' }, { moduleId: 'b' }] },
110
+ ])
111
+
112
+ expect(message).toContain('"Invoice", "Ledger"')
113
+ expect(message).toContain(' Invoice')
114
+ expect(message).toContain(' Ledger')
115
+ })
116
+
117
+ it('degrades gracefully when the module id or path is unavailable', () => {
118
+ const message = formatDuplicateEntityClassNamesWarning([
119
+ {
120
+ className: 'Invoice',
121
+ sources: [
122
+ { moduleId: undefined, sourcePath: '/repo/a/entities.ts' },
123
+ { moduleId: undefined, sourcePath: undefined },
124
+ ],
125
+ },
126
+ ])
127
+
128
+ expect(message).toContain(' - /repo/a/entities.ts')
129
+ expect(message).toContain(' - unknown module')
130
+ })
131
+
132
+ it('omits a source path that degraded to a bare class name', () => {
133
+ // MikroORM derives the decorator path by parsing a stack trace and falls back to
134
+ // the class name when that parse fails.
135
+ const message = formatDuplicateEntityClassNamesWarning([
136
+ { className: 'Invoice', sources: [{ moduleId: 'billing', sourcePath: 'Invoice' }] },
137
+ ])
138
+
139
+ const sourceLine = message.split('\n').find((line) => line.startsWith(' - '))
140
+ expect(sourceLine).toBe(' - billing')
141
+ })
142
+ })
143
+
144
+ describe('toDuplicateEntityClassNameFields', () => {
145
+ // The runtime surface logs through the structured facade, where the message must stay
146
+ // constant and every dynamic value has to be a queryable field.
147
+ it('carries the collisions as fields beside the constant explanation', () => {
148
+ const groups = findDuplicateEntityClassNames([
149
+ { className: 'Invoice', moduleId: 'billing', sourcePath: '/repo/modules/billing/data/entities.ts' },
150
+ { className: 'Invoice', moduleId: 'subscriptions', sourcePath: '/repo/modules/subscriptions/data/entities.ts' },
151
+ ])
152
+
153
+ expect(toDuplicateEntityClassNameFields(groups)).toEqual({
154
+ classNames: ['Invoice'],
155
+ duplicates: [
156
+ {
157
+ className: 'Invoice',
158
+ sources: [
159
+ { moduleId: 'billing', sourcePath: '/repo/modules/billing/data/entities.ts' },
160
+ { moduleId: 'subscriptions', sourcePath: '/repo/modules/subscriptions/data/entities.ts' },
161
+ ],
162
+ },
163
+ ],
164
+ reason: DUPLICATE_ENTITY_CLASS_NAMES_REASON,
165
+ remediation: DUPLICATE_ENTITY_CLASS_NAMES_REMEDIATION,
166
+ })
167
+ })
168
+
169
+ it('stays empty for no collisions', () => {
170
+ expect(toDuplicateEntityClassNameFields([]).classNames).toEqual([])
171
+ })
172
+ })
@@ -0,0 +1,63 @@
1
+ import { existsSync, readFileSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+
4
+ const PACKAGE_ROOT = join(__dirname, '..', '..', '..', '..')
5
+ const SUBPATH = './lib/db/duplicateEntityClassNames'
6
+
7
+ type ExportTarget = { types?: string | string[]; default?: string }
8
+
9
+ function readExports(): Record<string, ExportTarget | string> {
10
+ const packageJson = JSON.parse(readFileSync(join(PACKAGE_ROOT, 'package.json'), 'utf8')) as {
11
+ exports?: Record<string, ExportTarget | string>
12
+ }
13
+ return packageJson.exports ?? {}
14
+ }
15
+
16
+ /**
17
+ * Node's subpath resolution: an exact key wins outright, and only when none matches do
18
+ * the `*` patterns compete on the longest static prefix. `@open-mercato/cli` imports this
19
+ * subpath across a workspace boundary, and resolvers do not agree on wildcard handling,
20
+ * so it must not depend on one.
21
+ */
22
+ function resolveSubpath(exports: Record<string, ExportTarget | string>, subpath: string): string | null {
23
+ if (Object.prototype.hasOwnProperty.call(exports, subpath)) return subpath
24
+ let best: string | null = null
25
+ for (const key of Object.keys(exports)) {
26
+ const star = key.indexOf('*')
27
+ if (star < 0) continue
28
+ const prefix = key.slice(0, star)
29
+ const suffix = key.slice(star + 1)
30
+ if (!subpath.startsWith(prefix) || !subpath.endsWith(suffix)) continue
31
+ if (subpath.length < prefix.length + suffix.length) continue
32
+ if (!best || prefix.length > best.slice(0, best.indexOf('*')).length) best = key
33
+ }
34
+ return best
35
+ }
36
+
37
+ describe('@open-mercato/shared duplicateEntityClassNames export map', () => {
38
+ const exports = readExports()
39
+
40
+ it('resolves through an explicit entry rather than a wildcard pattern', () => {
41
+ expect(resolveSubpath(exports, SUBPATH)).toBe(SUBPATH)
42
+ })
43
+
44
+ it('maps types to the source module and default to the built module', () => {
45
+ expect(exports[SUBPATH]).toEqual({
46
+ types: './src/lib/db/duplicateEntityClassNames.ts',
47
+ default: './dist/lib/db/duplicateEntityClassNames.js',
48
+ })
49
+ })
50
+
51
+ // Both halves of the mapping must exist in a packed package. `default` is produced by
52
+ // `yarn build:packages`, which the validation gate runs before `yarn test`.
53
+ it('ships both mapped files', () => {
54
+ const target = exports[SUBPATH] as ExportTarget
55
+ expect(existsSync(join(PACKAGE_ROOT, target.types as string))).toBe(true)
56
+ expect(existsSync(join(PACKAGE_ROOT, target.default as string))).toBe(true)
57
+ })
58
+
59
+ it('keeps the module importable under the mapped default condition', async () => {
60
+ const target = exports[SUBPATH] as ExportTarget
61
+ await expect(import(join(PACKAGE_ROOT, target.default as string))).resolves.toBeDefined()
62
+ })
63
+ })
@@ -0,0 +1,7 @@
1
+ import { Entity, PrimaryKey } from '@mikro-orm/decorators/legacy'
2
+
3
+ @Entity({ tableName: 'duplicate_entity_fixture_billing' })
4
+ export class Invoice {
5
+ @PrimaryKey({ type: 'string' })
6
+ id!: string
7
+ }
@@ -0,0 +1,7 @@
1
+ import { Entity, PrimaryKey } from '@mikro-orm/decorators/legacy'
2
+
3
+ @Entity({ tableName: 'duplicate_entity_fixture_reporting' })
4
+ export class Invoice {
5
+ @PrimaryKey({ type: 'string' })
6
+ id!: string
7
+ }
@@ -0,0 +1,7 @@
1
+ import { Entity, PrimaryKey } from '@mikro-orm/decorators/legacy'
2
+
3
+ @Entity({ tableName: 'duplicate_entity_fixture_subscriptions' })
4
+ export class Invoice {
5
+ @PrimaryKey({ type: 'string' })
6
+ id!: string
7
+ }
@@ -0,0 +1,7 @@
1
+ import { Entity, PrimaryKey } from '@mikro-orm/decorators/legacy'
2
+
3
+ @Entity({ tableName: 'duplicate_entity_fixture_ledger_billing' })
4
+ export class Ledger {
5
+ @PrimaryKey({ type: 'string' })
6
+ id!: string
7
+ }
@@ -0,0 +1,7 @@
1
+ import { Entity, PrimaryKey } from '@mikro-orm/decorators/legacy'
2
+
3
+ @Entity({ tableName: 'duplicate_entity_fixture_ledger_reporting' })
4
+ export class Ledger {
5
+ @PrimaryKey({ type: 'string' })
6
+ id!: string
7
+ }
@@ -0,0 +1,7 @@
1
+ import { Entity, PrimaryKey } from '@mikro-orm/decorators/legacy'
2
+
3
+ @Entity({ tableName: 'duplicate_entity_fixture_ledger_subscriptions' })
4
+ export class Ledger {
5
+ @PrimaryKey({ type: 'string' })
6
+ id!: string
7
+ }
@@ -0,0 +1,164 @@
1
+ import 'reflect-metadata'
2
+ import { MetadataStorage } from '@mikro-orm/core'
3
+ import { Invoice as InvoiceBilling } from './fixtures/invoiceBilling'
4
+ import { Invoice as InvoiceSubscriptions } from './fixtures/invoiceSubscriptions'
5
+ import { Invoice as InvoiceReporting } from './fixtures/invoiceReporting'
6
+ import { Ledger as LedgerBilling } from './fixtures/ledgerBilling'
7
+ import { Ledger as LedgerSubscriptions } from './fixtures/ledgerSubscriptions'
8
+
9
+ const warn = jest.fn()
10
+
11
+ jest.mock('../../logger', () => ({
12
+ createLogger: () => ({
13
+ child: () => ({ debug: jest.fn(), info: jest.fn(), warn, error: jest.fn() }),
14
+ }),
15
+ }))
16
+
17
+ const GLOBAL_ENTITIES_KEY = '__openMercatoOrmEntities__'
18
+ const GLOBAL_REPORTED_KEY = '__openMercatoReportedDuplicateEntityClassNames__'
19
+
20
+ function readSourcePath(entity: unknown): string {
21
+ return (entity as Record<symbol, string>)[MetadataStorage.PATH_SYMBOL]
22
+ }
23
+
24
+ function stampModuleId(entity: unknown, stamp: string): void {
25
+ Object.defineProperty(entity, 'entityName', {
26
+ value: stamp,
27
+ configurable: true,
28
+ enumerable: false,
29
+ writable: false,
30
+ })
31
+ }
32
+
33
+ describe('registerOrmEntities duplicate class name reporting', () => {
34
+ const originalEntities = (globalThis as Record<string, unknown>)[GLOBAL_ENTITIES_KEY]
35
+
36
+ beforeEach(() => {
37
+ warn.mockClear()
38
+ // Collisions are reported once per process; start each case from a clean slate.
39
+ delete (globalThis as Record<string, unknown>)[GLOBAL_REPORTED_KEY]
40
+ })
41
+
42
+ afterEach(() => {
43
+ for (const entity of [InvoiceBilling, InvoiceSubscriptions]) {
44
+ delete (entity as Record<string, unknown>).entityName
45
+ }
46
+ if (typeof originalEntities === 'undefined') {
47
+ delete (globalThis as Record<string, unknown>)[GLOBAL_ENTITIES_KEY]
48
+ return
49
+ }
50
+ ;(globalThis as Record<string, unknown>)[GLOBAL_ENTITIES_KEY] = originalEntities
51
+ })
52
+
53
+ it('warns and still registers when two modules contribute the same class name', async () => {
54
+ stampModuleId(InvoiceBilling, 'billing.Invoice')
55
+ stampModuleId(InvoiceSubscriptions, 'subscriptions.Invoice')
56
+ const entities = [InvoiceBilling, InvoiceSubscriptions]
57
+ const { registerOrmEntities, getOrmEntities } = await import('../mikro')
58
+
59
+ registerOrmEntities(entities)
60
+
61
+ expect(warn).toHaveBeenCalledTimes(1)
62
+ // The structured-logging contract keeps the message constant and the dynamic values
63
+ // in queryable fields.
64
+ const [message, fields] = warn.mock.calls[0] as [string, Record<string, unknown>]
65
+ expect(message).toBe('Duplicate entity class names across enabled modules')
66
+ expect(fields.classNames).toEqual(['Invoice'])
67
+ expect(fields.duplicates).toEqual([
68
+ {
69
+ className: 'Invoice',
70
+ sources: [
71
+ { moduleId: 'billing', sourcePath: readSourcePath(InvoiceBilling) },
72
+ { moduleId: 'subscriptions', sourcePath: readSourcePath(InvoiceSubscriptions) },
73
+ ],
74
+ },
75
+ ])
76
+ expect(typeof fields.remediation).toBe('string')
77
+ // Warning-only by design: registration must still complete.
78
+ expect(getOrmEntities()).toBe(entities)
79
+ })
80
+
81
+ it('stays silent for a registration with unique class names', async () => {
82
+ const { registerOrmEntities } = await import('../mikro')
83
+
84
+ registerOrmEntities([InvoiceBilling])
85
+
86
+ expect(warn).not.toHaveBeenCalled()
87
+ })
88
+
89
+ it('stays silent when the same class is registered twice', async () => {
90
+ const { registerOrmEntities } = await import('../mikro')
91
+
92
+ registerOrmEntities([InvoiceBilling, InvoiceBilling])
93
+
94
+ expect(warn).not.toHaveBeenCalled()
95
+ })
96
+
97
+ it('stays silent for non-entity exports that share a name', async () => {
98
+ const { registerOrmEntities } = await import('../mikro')
99
+
100
+ registerOrmEntities([
101
+ function helper(): void {},
102
+ function helper(): void {},
103
+ { name: 'TestEntity' },
104
+ { name: 'TestEntity' },
105
+ ])
106
+
107
+ expect(warn).not.toHaveBeenCalled()
108
+ })
109
+
110
+ it('reports a standing collision once, not on every HMR re-registration', async () => {
111
+ stampModuleId(InvoiceBilling, 'billing.Invoice')
112
+ stampModuleId(InvoiceSubscriptions, 'subscriptions.Invoice')
113
+ const entities = [InvoiceBilling, InvoiceSubscriptions]
114
+ const { registerOrmEntities } = await import('../mikro')
115
+
116
+ registerOrmEntities(entities)
117
+ registerOrmEntities(entities)
118
+ registerOrmEntities(entities)
119
+
120
+ expect(warn).toHaveBeenCalledTimes(1)
121
+ })
122
+
123
+ it('reports a newly introduced collision even after an earlier one was reported', async () => {
124
+ const { registerOrmEntities } = await import('../mikro')
125
+
126
+ registerOrmEntities([InvoiceBilling, InvoiceSubscriptions])
127
+ expect(warn).toHaveBeenCalledTimes(1)
128
+
129
+ registerOrmEntities([InvoiceBilling, InvoiceSubscriptions, LedgerBilling, LedgerSubscriptions])
130
+
131
+ expect(warn).toHaveBeenCalledTimes(2)
132
+ // The already-reported name is not repeated.
133
+ expect(warn.mock.calls[1][1].classNames).toEqual(['Ledger'])
134
+ })
135
+
136
+ it('reports a collision again after it was fixed and reintroduced', async () => {
137
+ const { registerOrmEntities } = await import('../mikro')
138
+
139
+ registerOrmEntities([InvoiceBilling, InvoiceSubscriptions])
140
+ expect(warn).toHaveBeenCalledTimes(1)
141
+
142
+ // The developer renames one of them; the next reload is clean.
143
+ registerOrmEntities([InvoiceBilling])
144
+ expect(warn).toHaveBeenCalledTimes(1)
145
+
146
+ // Reverting the rename must warn again rather than stay silent forever.
147
+ registerOrmEntities([InvoiceBilling, InvoiceSubscriptions])
148
+
149
+ expect(warn).toHaveBeenCalledTimes(2)
150
+ expect(warn.mock.calls[1][1].classNames).toEqual(['Invoice'])
151
+ })
152
+
153
+ it('reports again when the same class name starts colliding with a different module', async () => {
154
+ const { registerOrmEntities } = await import('../mikro')
155
+
156
+ registerOrmEntities([InvoiceBilling, InvoiceSubscriptions])
157
+ expect(warn).toHaveBeenCalledTimes(1)
158
+
159
+ registerOrmEntities([InvoiceBilling, InvoiceReporting])
160
+
161
+ expect(warn).toHaveBeenCalledTimes(2)
162
+ expect(warn.mock.calls[1][1].classNames).toEqual(['Invoice'])
163
+ })
164
+ })
@@ -0,0 +1,89 @@
1
+ import { EntitySchema, MetadataStorage } from '@mikro-orm/core'
2
+ import {
3
+ findDuplicateEntityClassNames,
4
+ type DuplicateEntityClassNameGroup,
5
+ type EntityClassNameEntry,
6
+ } from './duplicateEntityClassNames'
7
+
8
+ /**
9
+ * Adapts a registered ORM entity array to the dependency-free collision detector in
10
+ * `./duplicateEntityClassNames`, which explains the underlying MikroORM behaviour.
11
+ */
12
+
13
+ type DecoratedEntityClass = {
14
+ readonly name?: unknown
15
+ readonly entityName?: unknown
16
+ readonly [MetadataStorage.PATH_SYMBOL]?: unknown
17
+ }
18
+
19
+ function readString(value: unknown): string | undefined {
20
+ return typeof value === 'string' && value.length > 0 ? value : undefined
21
+ }
22
+
23
+ /**
24
+ * `enhanceEntities()` in the generated entity registry stamps `<moduleId>.<ExportName>`
25
+ * onto every entity export. MikroORM ignores that stamp, but it is the only place the
26
+ * contributing module id survives to runtime. Module ids may contain dots; the export
27
+ * name never does, so split on the last one. A class that declares its own `entityName`
28
+ * keeps it — `enhanceEntities()` never overwrites one — so a declared value containing a
29
+ * dot yields a bogus module id here; cosmetic in the warning text, and a dot-free value
30
+ * degrades to `undefined`.
31
+ */
32
+ function readModuleIdFromStamp(stamp: unknown): string | undefined {
33
+ const value = readString(stamp)
34
+ if (!value) return undefined
35
+ const separator = value.lastIndexOf('.')
36
+ return separator > 0 ? value.slice(0, separator) : undefined
37
+ }
38
+
39
+ /**
40
+ * The registered array holds every function export of each module's entity file, so
41
+ * plain helper functions travel alongside real entities. Only classes touched by a
42
+ * MikroORM decorator carry `MetadataStorage.PATH_SYMBOL` as an own property; everything
43
+ * else is skipped so helpers that happen to share a name are never reported.
44
+ */
45
+ function toEntityClassNameEntry(value: unknown): EntityClassNameEntry | null {
46
+ // `enhanceEntities()` in the generated registry keeps only `typeof value === 'function'`
47
+ // exports, so an EntitySchema a module exports never arrives through it. This branch is
48
+ // live only for direct callers such as the testing bootstrap — and since the module id
49
+ // stamp is applied to those same function exports, an EntitySchema carries no module id
50
+ // and reports by path alone.
51
+ if (EntitySchema.is(value)) {
52
+ const className = readString(value.meta?.className)
53
+ if (!className) return null
54
+ return { className, sourcePath: readString(value.meta?.path), target: value }
55
+ }
56
+ if (typeof value !== 'function') return null
57
+ if (!Object.prototype.hasOwnProperty.call(value, MetadataStorage.PATH_SYMBOL)) return null
58
+ const entity = value as DecoratedEntityClass
59
+ const className = readString(entity.name)
60
+ if (!className) return null
61
+ return {
62
+ className,
63
+ moduleId: readModuleIdFromStamp(entity.entityName),
64
+ sourcePath: readString(entity[MetadataStorage.PATH_SYMBOL]),
65
+ target: value,
66
+ }
67
+ }
68
+
69
+ export function collectEntityClassNameEntries(entities: readonly unknown[]): EntityClassNameEntry[] {
70
+ const entries: EntityClassNameEntry[] = []
71
+ for (const value of entities) {
72
+ let entry: EntityClassNameEntry | null = null
73
+ try {
74
+ entry = toEntityClassNameEntry(value)
75
+ } catch {
76
+ // Reading a name off an exotic export (a throwing getter, a proxy) must not turn
77
+ // a diagnostic into a boot failure. Skip the value and keep checking the rest.
78
+ continue
79
+ }
80
+ if (entry) entries.push(entry)
81
+ }
82
+ return entries
83
+ }
84
+
85
+ export function findDuplicateRegisteredEntityClassNames(
86
+ entities: readonly unknown[],
87
+ ): DuplicateEntityClassNameGroup[] {
88
+ return findDuplicateEntityClassNames(collectEntityClassNameEntries(entities))
89
+ }