@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,255 @@
1
+ import fs from 'node:fs'
2
+ import ts from 'typescript-js'
3
+
4
+ /**
5
+ * Names of the `@Entity()`-decorated classes a module's entity file exports.
6
+ *
7
+ * Only decorated classes count: an entity file may also export plain helper classes,
8
+ * and reporting those as entities would raise false collisions. Only a decorator bound
9
+ * to a `@mikro-orm/*` import counts, so an unrelated `Entity` decorator from another
10
+ * library never registers as one.
11
+ *
12
+ * Parsing is used rather than importing because the caller runs during generation,
13
+ * before the module graph exists. A monorepo checkout and a standalone install that
14
+ * ships `src/modules` both hand this TypeScript source; a package that ships compiled
15
+ * output only hands it the built file instead, where a decorated class has become a
16
+ * class expression plus a decorator-helper call, so both shapes are recognised.
17
+ */
18
+
19
+ const MIKRO_ORM_MODULE_PREFIX = '@mikro-orm/'
20
+
21
+ type EntityDecoratorBindings = {
22
+ /** Local names bound to MikroORM's `Entity` export, including aliased imports. */
23
+ named: Set<string>
24
+ /** Local names bound to a whole MikroORM module, for `@orm.Entity()`. */
25
+ namespaces: Set<string>
26
+ }
27
+
28
+ function inferScriptKind(filePath: string): ts.ScriptKind {
29
+ if (filePath.endsWith('.tsx')) return ts.ScriptKind.TSX
30
+ if (filePath.endsWith('.jsx')) return ts.ScriptKind.JSX
31
+ if (filePath.endsWith('.js') || filePath.endsWith('.cjs') || filePath.endsWith('.mjs')) return ts.ScriptKind.JS
32
+ return ts.ScriptKind.TS
33
+ }
34
+
35
+ function isMikroOrmModule(specifier: string): boolean {
36
+ return specifier.startsWith(MIKRO_ORM_MODULE_PREFIX)
37
+ }
38
+
39
+ /** `require('@mikro-orm/core')` as it survives into compiled CommonJS output. */
40
+ function readRequiredModuleSpecifier(expression: ts.Expression | undefined): string | undefined {
41
+ if (!expression || !ts.isCallExpression(expression)) return undefined
42
+ if (!ts.isIdentifier(expression.expression) || expression.expression.text !== 'require') return undefined
43
+ const [argument] = expression.arguments
44
+ return argument && ts.isStringLiteralLike(argument) ? argument.text : undefined
45
+ }
46
+
47
+ function collectEntityDecoratorBindings(sourceFile: ts.SourceFile): EntityDecoratorBindings {
48
+ const named = new Set<string>()
49
+ const namespaces = new Set<string>()
50
+
51
+ const addFromNamedBindings = (bindings: ts.NamedImportBindings | undefined): void => {
52
+ if (!bindings) return
53
+ if (ts.isNamespaceImport(bindings)) {
54
+ namespaces.add(bindings.name.text)
55
+ return
56
+ }
57
+ for (const element of bindings.elements) {
58
+ if ((element.propertyName ?? element.name).text === 'Entity') named.add(element.name.text)
59
+ }
60
+ }
61
+
62
+ sourceFile.forEachChild((node) => {
63
+ if (ts.isImportDeclaration(node)) {
64
+ if (!ts.isStringLiteralLike(node.moduleSpecifier) || !isMikroOrmModule(node.moduleSpecifier.text)) return
65
+ addFromNamedBindings(node.importClause?.namedBindings)
66
+ return
67
+ }
68
+ if (!ts.isVariableStatement(node)) return
69
+ for (const declaration of node.declarationList.declarations) {
70
+ const specifier = readRequiredModuleSpecifier(declaration.initializer)
71
+ if (!specifier || !isMikroOrmModule(specifier)) continue
72
+ if (ts.isIdentifier(declaration.name)) {
73
+ namespaces.add(declaration.name.text)
74
+ continue
75
+ }
76
+ if (!ts.isObjectBindingPattern(declaration.name)) continue
77
+ for (const element of declaration.name.elements) {
78
+ const sourceName = element.propertyName ?? element.name
79
+ if (ts.isIdentifier(sourceName) && sourceName.text === 'Entity' && ts.isIdentifier(element.name)) {
80
+ named.add(element.name.text)
81
+ }
82
+ }
83
+ }
84
+ })
85
+
86
+ return { named, namespaces }
87
+ }
88
+
89
+ /** Unwraps the `(0, core_1.Entity)` form a CommonJS emit produces. */
90
+ function unwrapReference(expression: ts.Expression): ts.Expression {
91
+ if (ts.isParenthesizedExpression(expression)) return unwrapReference(expression.expression)
92
+ if (ts.isBinaryExpression(expression) && expression.operatorToken.kind === ts.SyntaxKind.CommaToken) {
93
+ return unwrapReference(expression.right)
94
+ }
95
+ return expression
96
+ }
97
+
98
+ /** Accepts `Entity`, an aliased local name, and `<mikroOrmNamespace>.Entity`. */
99
+ function isEntityReference(expression: ts.Expression, bindings: EntityDecoratorBindings): boolean {
100
+ const reference = unwrapReference(expression)
101
+ if (ts.isIdentifier(reference)) return bindings.named.has(reference.text)
102
+ if (!ts.isPropertyAccessExpression(reference)) return false
103
+ if (reference.name.text !== 'Entity') return false
104
+ return ts.isIdentifier(reference.expression) && bindings.namespaces.has(reference.expression.text)
105
+ }
106
+
107
+ /** Accepts `@Entity`, `@Entity(...)` and the aliased or namespace-qualified forms. */
108
+ function isEntityDecorator(decorator: ts.Decorator, bindings: EntityDecoratorBindings): boolean {
109
+ const expression = ts.isCallExpression(decorator.expression)
110
+ ? decorator.expression.expression
111
+ : decorator.expression
112
+ return isEntityReference(expression, bindings)
113
+ }
114
+
115
+ function hasEntityDecorator(node: ts.ClassDeclaration, bindings: EntityDecoratorBindings): boolean {
116
+ if (!ts.canHaveDecorators(node)) return false
117
+ return (ts.getDecorators(node) ?? []).some((decorator) => isEntityDecorator(decorator, bindings))
118
+ }
119
+
120
+ function isExportedDeclaration(node: ts.Node): boolean {
121
+ return ts.canHaveModifiers(node)
122
+ ? (ts.getModifiers(node) ?? []).some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword)
123
+ : false
124
+ }
125
+
126
+ /**
127
+ * Names the file exports without redeclaring them: `export { X }`, the CommonJS
128
+ * `exports.X = X`, and the `__export(exports, { X: () => X })` table a bundler emits.
129
+ */
130
+ function collectIndirectExportNames(sourceFile: ts.SourceFile): Set<string> {
131
+ const names = new Set<string>()
132
+
133
+ const addExportTable = (expression: ts.Expression): void => {
134
+ if (!ts.isObjectLiteralExpression(expression)) return
135
+ for (const property of expression.properties) {
136
+ if (!property.name) continue
137
+ const name = ts.isIdentifier(property.name) || ts.isStringLiteralLike(property.name)
138
+ ? property.name.text
139
+ : undefined
140
+ if (name) names.add(name)
141
+ }
142
+ }
143
+
144
+ sourceFile.forEachChild((node) => {
145
+ if (ts.isExportDeclaration(node) && !node.moduleSpecifier) {
146
+ const clause = node.exportClause
147
+ if (clause && ts.isNamedExports(clause)) {
148
+ for (const element of clause.elements) names.add((element.propertyName ?? element.name).text)
149
+ }
150
+ return
151
+ }
152
+ if (!ts.isExpressionStatement(node)) return
153
+ const expression = node.expression
154
+ if (ts.isCallExpression(expression)) {
155
+ const callee = expression.expression
156
+ // esbuild emits `__export(ns, {...})`, SWC `_export(exports, {...})`.
157
+ if (ts.isIdentifier(callee) && /^_*export$/.test(callee.text) && expression.arguments.length >= 2) {
158
+ addExportTable(expression.arguments[1])
159
+ }
160
+ return
161
+ }
162
+ if (!ts.isBinaryExpression(expression) || expression.operatorToken.kind !== ts.SyntaxKind.EqualsToken) return
163
+ const target = expression.left
164
+ if (!ts.isPropertyAccessExpression(target)) return
165
+ if (!ts.isIdentifier(target.expression) || target.expression.text !== 'exports') return
166
+ names.add(target.name.text)
167
+ })
168
+
169
+ return names
170
+ }
171
+
172
+ /**
173
+ * The compiled shape of a decorated class: the class becomes an expression assigned to a
174
+ * variable, and the decorators move into a helper call — `X = __decorateClass([...], X)`
175
+ * for esbuild, `X = __decorate([...], X)` for tsc, `_ts_decorate` for SWC. The helper's
176
+ * name is not matched, since it varies by toolchain; the shape is: a call whose first
177
+ * argument is an array literal and whose result is assigned back to the class binding.
178
+ */
179
+ function collectCompiledDecoratedClassNames(
180
+ sourceFile: ts.SourceFile,
181
+ bindings: EntityDecoratorBindings,
182
+ ): string[] {
183
+ const names: string[] = []
184
+ sourceFile.forEachChild((node) => {
185
+ if (!ts.isExpressionStatement(node)) return
186
+ const assignment = node.expression
187
+ if (!ts.isBinaryExpression(assignment) || assignment.operatorToken.kind !== ts.SyntaxKind.EqualsToken) return
188
+ if (!ts.isIdentifier(assignment.left)) return
189
+ const call = assignment.right
190
+ if (!ts.isCallExpression(call) || call.arguments.length === 0) return
191
+ const [decorators] = call.arguments
192
+ if (!ts.isArrayLiteralExpression(decorators)) return
193
+ const target = call.arguments[call.arguments.length - 1]
194
+ if (!target || !ts.isIdentifier(target) || target.text !== assignment.left.text) return
195
+ const decoratesEntity = decorators.elements.some((element) =>
196
+ isEntityReference(ts.isCallExpression(element) ? element.expression : element, bindings))
197
+ if (decoratesEntity) names.push(assignment.left.text)
198
+ })
199
+ return names
200
+ }
201
+
202
+ /** Class-expression bindings a compiled file declares, so `let X = class {}` is seen. */
203
+ function collectVariableClassNames(sourceFile: ts.SourceFile): Map<string, boolean> {
204
+ const declared = new Map<string, boolean>()
205
+ sourceFile.forEachChild((node) => {
206
+ if (!ts.isVariableStatement(node)) return
207
+ const exported = isExportedDeclaration(node)
208
+ for (const declaration of node.declarationList.declarations) {
209
+ if (!ts.isIdentifier(declaration.name) || !declaration.initializer) continue
210
+ if (!ts.isClassExpression(declaration.initializer)) continue
211
+ declared.set(declaration.name.text, exported)
212
+ }
213
+ })
214
+ return declared
215
+ }
216
+
217
+ export function parseEntityClassNames(filePath: string): string[] {
218
+ let source: string
219
+ try {
220
+ source = fs.readFileSync(filePath, 'utf8')
221
+ } catch {
222
+ // A warning path must never break generation.
223
+ return []
224
+ }
225
+ const sourceFile = ts.createSourceFile(
226
+ filePath,
227
+ source,
228
+ ts.ScriptTarget.ES2020,
229
+ true,
230
+ inferScriptKind(filePath),
231
+ )
232
+ const bindings = collectEntityDecoratorBindings(sourceFile)
233
+ const indirectExports = collectIndirectExportNames(sourceFile)
234
+ const classNames: string[] = []
235
+ const add = (className: string): void => {
236
+ if (!classNames.includes(className)) classNames.push(className)
237
+ }
238
+
239
+ sourceFile.forEachChild((node) => {
240
+ if (!ts.isClassDeclaration(node) || !node.name) return
241
+ if (!hasEntityDecorator(node, bindings)) return
242
+ const className = node.name.text
243
+ if (!isExportedDeclaration(node) && !indirectExports.has(className)) return
244
+ add(className)
245
+ })
246
+
247
+ const variableClasses = collectVariableClassNames(sourceFile)
248
+ for (const className of collectCompiledDecoratedClassNames(sourceFile, bindings)) {
249
+ if (!variableClasses.has(className)) continue
250
+ if (!variableClasses.get(className) && !indirectExports.has(className)) continue
251
+ add(className)
252
+ }
253
+
254
+ return classNames
255
+ }
@@ -1,7 +1,13 @@
1
1
  import fs from 'node:fs'
2
2
  import path from 'node:path'
3
3
  import { StructureKind, VariableDeclarationKind } from 'ts-morph'
4
- import type { PackageResolver } from '../resolver'
4
+ import {
5
+ findDuplicateEntityClassNames,
6
+ formatDuplicateEntityClassNamesWarning,
7
+ type EntityClassNameEntry,
8
+ } from '@open-mercato/shared/lib/db/duplicateEntityClassNames'
9
+ import type { ModuleEntry, PackageResolver } from '../resolver'
10
+ import { parseEntityClassNames } from './entity-class-declarations'
5
11
  import { MODULE_CODE_EXTENSIONS, stripModuleCodeExtension } from './scanner'
6
12
  import {
7
13
  toVar,
@@ -50,6 +56,7 @@ export async function generateModuleEntities(options: ModuleEntitiesOptions): Pr
50
56
  const mods = resolver.loadEnabledModules()
51
57
  const imports: Array<{ name: string; moduleSpecifier: string }> = []
52
58
  const entitySources: Array<{ importName: string; moduleId: string }> = []
59
+ const classNameEntries: EntityClassNameEntry[] = []
53
60
  let n = 0
54
61
 
55
62
  for (const entry of mods) {
@@ -94,6 +101,11 @@ export async function generateModuleEntities(options: ModuleEntitiesOptions): Pr
94
101
  }
95
102
  imports.push({ name: importName, moduleSpecifier: relImport })
96
103
  entitySources.push({ importName, moduleId: modId })
104
+
105
+ const entityFilePath = resolveEntitySourceFile(resolver, entry, found, fromApp)
106
+ for (const className of parseEntityClassNames(entityFilePath)) {
107
+ classNameEntries.push({ className, moduleId: modId, sourcePath: entityFilePath })
108
+ }
97
109
  }
98
110
 
99
111
  const sourceFile = createGeneratedSourceFile('entities.generated.ts')
@@ -211,9 +223,50 @@ export async function generateModuleEntities(options: ModuleEntitiesOptions): Pr
211
223
  quiet,
212
224
  })
213
225
 
226
+ warnOnDuplicateEntityClassNames(classNameEntries)
227
+
214
228
  return result
215
229
  }
216
230
 
231
+ /**
232
+ * A duplicate entity class name across modules silently corrupts entity resolution at
233
+ * runtime and no build step otherwise catches it, so surface it here — where the
234
+ * colliding registry is produced. Reported as a warning; generation still succeeds.
235
+ */
236
+ function warnOnDuplicateEntityClassNames(entries: readonly EntityClassNameEntry[]): void {
237
+ try {
238
+ const duplicates = findDuplicateEntityClassNames(entries)
239
+ if (duplicates.length === 0) return
240
+ console.warn(`\x1b[33m[Entities Warning]\x1b[0m ${formatDuplicateEntityClassNamesWarning(duplicates)}`)
241
+ } catch {
242
+ // This check is a diagnostic. It must never be the reason generation fails.
243
+ }
244
+ }
245
+
246
+ /**
247
+ * The file to read class names out of, which is not always the file the runtime imports.
248
+ * A standalone install resolves a package module to its compiled `dist/modules` tree, and
249
+ * published packages ship `src/modules` alongside it — so prefer the shipped source, the
250
+ * way `eject` already does, and leave the runtime import pointing at `dist`. A package
251
+ * that ships compiled output only falls back to the resolved base, which the parser also
252
+ * understands. An app override wins over the package tree entirely: the registry imports
253
+ * the app file, so that is the file whose class names are registered.
254
+ */
255
+ function resolveEntitySourceFile(
256
+ resolver: PackageResolver,
257
+ entry: ModuleEntry,
258
+ found: { base: string; file: string },
259
+ fromApp: boolean,
260
+ ): string {
261
+ const resolved = path.join(found.base, found.file)
262
+ const from = entry.from
263
+ if (fromApp || !from || from === '@app') return resolved
264
+ const sourceBase = path.join(resolver.getPackageRoot(from), 'src', 'modules', entry.id, path.basename(found.base))
265
+ if (path.resolve(sourceBase) === path.resolve(found.base)) return resolved
266
+ const sourceFile = resolveConventionFile(sourceBase, stripModuleCodeExtension(found.file))
267
+ return sourceFile ?? resolved
268
+ }
269
+
217
270
  function resolveConventionFile(baseDir: string, basename: string): string | null {
218
271
  for (const extension of MODULE_CODE_EXTENSIONS) {
219
272
  const candidate = path.join(baseDir, `${basename}${extension}`)