@open-mercato/cli 0.4.2-canary-c02407ff85

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 (51) hide show
  1. package/bin/mercato +21 -0
  2. package/build.mjs +78 -0
  3. package/dist/bin.js +51 -0
  4. package/dist/bin.js.map +7 -0
  5. package/dist/index.js +5 -0
  6. package/dist/index.js.map +7 -0
  7. package/dist/lib/db/commands.js +350 -0
  8. package/dist/lib/db/commands.js.map +7 -0
  9. package/dist/lib/db/index.js +7 -0
  10. package/dist/lib/db/index.js.map +7 -0
  11. package/dist/lib/generators/entity-ids.js +257 -0
  12. package/dist/lib/generators/entity-ids.js.map +7 -0
  13. package/dist/lib/generators/index.js +12 -0
  14. package/dist/lib/generators/index.js.map +7 -0
  15. package/dist/lib/generators/module-di.js +73 -0
  16. package/dist/lib/generators/module-di.js.map +7 -0
  17. package/dist/lib/generators/module-entities.js +104 -0
  18. package/dist/lib/generators/module-entities.js.map +7 -0
  19. package/dist/lib/generators/module-registry.js +1081 -0
  20. package/dist/lib/generators/module-registry.js.map +7 -0
  21. package/dist/lib/resolver.js +205 -0
  22. package/dist/lib/resolver.js.map +7 -0
  23. package/dist/lib/utils.js +161 -0
  24. package/dist/lib/utils.js.map +7 -0
  25. package/dist/mercato.js +1045 -0
  26. package/dist/mercato.js.map +7 -0
  27. package/dist/registry.js +7 -0
  28. package/dist/registry.js.map +7 -0
  29. package/jest.config.cjs +19 -0
  30. package/package.json +71 -0
  31. package/src/__tests__/mercato.test.ts +90 -0
  32. package/src/bin.ts +74 -0
  33. package/src/index.ts +2 -0
  34. package/src/lib/__tests__/resolver.test.ts +101 -0
  35. package/src/lib/__tests__/utils.test.ts +270 -0
  36. package/src/lib/db/__tests__/commands.test.ts +131 -0
  37. package/src/lib/db/commands.ts +431 -0
  38. package/src/lib/db/index.ts +1 -0
  39. package/src/lib/generators/__tests__/generators.test.ts +197 -0
  40. package/src/lib/generators/entity-ids.ts +336 -0
  41. package/src/lib/generators/index.ts +4 -0
  42. package/src/lib/generators/module-di.ts +89 -0
  43. package/src/lib/generators/module-entities.ts +124 -0
  44. package/src/lib/generators/module-registry.ts +1222 -0
  45. package/src/lib/resolver.ts +308 -0
  46. package/src/lib/utils.ts +200 -0
  47. package/src/mercato.ts +1106 -0
  48. package/src/registry.ts +2 -0
  49. package/tsconfig.build.json +4 -0
  50. package/tsconfig.json +12 -0
  51. package/watch.mjs +6 -0
@@ -0,0 +1,336 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import ts from 'typescript'
4
+ import type { PackageResolver, ModuleEntry } from '../resolver'
5
+ import {
6
+ calculateChecksum,
7
+ readChecksumRecord,
8
+ writeChecksumRecord,
9
+ ensureDir,
10
+ toVar,
11
+ toSnake,
12
+ rimrafDir,
13
+ logGenerationResult,
14
+ type GeneratorResult,
15
+ createGeneratorResult,
16
+ } from '../utils'
17
+
18
+ type GroupKey = '@app' | '@open-mercato/core' | string
19
+ type EntityFieldMap = Record<string, string[]>
20
+
21
+ export interface EntityIdsOptions {
22
+ resolver: PackageResolver
23
+ quiet?: boolean
24
+ }
25
+
26
+ /**
27
+ * Extract exported class names from a TypeScript source file without dynamic import.
28
+ * This is used for @app modules since Node.js can't import TypeScript files directly.
29
+ */
30
+ function parseExportedClassNamesFromFile(filePath: string): string[] {
31
+ const src = fs.readFileSync(filePath, 'utf8')
32
+ const sf = ts.createSourceFile(filePath, src, ts.ScriptTarget.ES2020, true, ts.ScriptKind.TS)
33
+ const classNames: string[] = []
34
+
35
+ sf.forEachChild((node) => {
36
+ // Check for exported class declarations
37
+ if (ts.isClassDeclaration(node) && node.name) {
38
+ const hasExport = node.modifiers?.some(
39
+ (m) => m.kind === ts.SyntaxKind.ExportKeyword
40
+ )
41
+ if (hasExport) {
42
+ classNames.push(node.name.text)
43
+ }
44
+ }
45
+ })
46
+
47
+ return classNames
48
+ }
49
+
50
+ function parseEntityFieldsFromFile(filePath: string, exportedClassNames: string[]): EntityFieldMap {
51
+ const src = fs.readFileSync(filePath, 'utf8')
52
+ const sf = ts.createSourceFile(filePath, src, ts.ScriptTarget.ES2020, true, ts.ScriptKind.TS)
53
+
54
+ const exported = new Set(exportedClassNames)
55
+ const result: EntityFieldMap = {}
56
+
57
+ function getDecoratorArgNameLiteral(dec: ts.Decorator | undefined): string | undefined {
58
+ if (!dec) return undefined
59
+ const expr = dec.expression
60
+ if (!ts.isCallExpression(expr)) return undefined
61
+ if (!expr.arguments.length) return undefined
62
+ const first = expr.arguments[0]
63
+ if (!ts.isObjectLiteralExpression(first)) return undefined
64
+ for (const prop of first.properties) {
65
+ if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name) && prop.name.text === 'name') {
66
+ if (ts.isStringLiteral(prop.initializer)) return prop.initializer.text
67
+ }
68
+ }
69
+ return undefined
70
+ }
71
+
72
+ function normalizeDbName(propertyName: string, _decoratorName?: string, nameOverride?: string): string {
73
+ if (nameOverride) return nameOverride
74
+ return toSnake(propertyName)
75
+ }
76
+
77
+ sf.forEachChild((node) => {
78
+ if (!ts.isClassDeclaration(node) || !node.name) return
79
+ const clsName = node.name.text
80
+ if (!exported.has(clsName)) return
81
+ const entityKey = toSnake(clsName)
82
+ const fields: string[] = []
83
+
84
+ for (const member of node.members) {
85
+ if (!ts.isPropertyDeclaration(member) || !member.name) continue
86
+ const name = ts.isIdentifier(member.name)
87
+ ? member.name.text
88
+ : ts.isStringLiteral(member.name)
89
+ ? member.name.text
90
+ : undefined
91
+ if (!name) continue
92
+ if (member.modifiers?.some((m) => m.kind === ts.SyntaxKind.StaticKeyword)) continue
93
+ const decorators = ts.canHaveDecorators(member)
94
+ ? ts.getDecorators(member) ?? []
95
+ : []
96
+ let dbName: string | undefined
97
+ if (decorators && decorators.length) {
98
+ for (const d of decorators) {
99
+ const nameOverride = getDecoratorArgNameLiteral(d)
100
+ dbName = normalizeDbName(name, undefined, nameOverride)
101
+ if (dbName) break
102
+ }
103
+ }
104
+ if (!dbName) dbName = normalizeDbName(name)
105
+ fields.push(dbName)
106
+ }
107
+ result[entityKey] = Array.from(new Set(fields))
108
+ })
109
+
110
+ return result
111
+ }
112
+
113
+ function writePerEntityFieldFiles(outRoot: string, fieldsByEntity: EntityFieldMap): void {
114
+ fs.mkdirSync(outRoot, { recursive: true })
115
+ rimrafDir(outRoot)
116
+ fs.mkdirSync(outRoot, { recursive: true })
117
+ for (const [entity, fields] of Object.entries(fieldsByEntity)) {
118
+ const entDir = path.join(outRoot, entity)
119
+ fs.mkdirSync(entDir, { recursive: true })
120
+ const idx = fields.map((f) => `export const ${toVar(f)} = '${f}'`).join('\n') + '\n'
121
+ fs.writeFileSync(path.join(entDir, 'index.ts'), idx)
122
+ }
123
+ }
124
+
125
+ function writeEntityFieldsRegistry(generatedRoot: string, fieldsByEntity: EntityFieldMap): void {
126
+ const entities = Object.keys(fieldsByEntity).sort()
127
+
128
+ // Always write the file, even if empty, to prevent TypeScript import errors
129
+ const imports = entities.length > 0
130
+ ? entities.map((e) => `import * as ${toVar(e)} from './entities/${e}/index'`).join('\n')
131
+ : ''
132
+ const registryEntries = entities.length > 0
133
+ ? entities.map((e) => ` ${toVar(e)}`).join(',\n')
134
+ : ''
135
+
136
+ const src = `// AUTO-GENERATED by mercato generate entity-ids
137
+ // Static registry for entity fields - eliminates dynamic imports for Turbopack compatibility
138
+ ${imports}
139
+
140
+ export const entityFieldsRegistry: Record<string, Record<string, string>> = {
141
+ ${registryEntries}
142
+ }
143
+
144
+ export function getEntityFields(slug: string): Record<string, string> | undefined {
145
+ return entityFieldsRegistry[slug]
146
+ }
147
+ `
148
+ const outPath = path.join(generatedRoot, 'entity-fields-registry.ts')
149
+ ensureDir(outPath)
150
+ fs.writeFileSync(outPath, src)
151
+ }
152
+
153
+ export async function generateEntityIds(options: EntityIdsOptions): Promise<GeneratorResult> {
154
+ const { resolver, quiet = false } = options
155
+ const result = createGeneratorResult()
156
+
157
+ const outputDir = resolver.getOutputDir()
158
+ const outFile = path.join(outputDir, 'entities.ids.generated.ts')
159
+ const checksumFile = path.join(outputDir, 'entities.ids.generated.checksum')
160
+
161
+ const entries = resolver.loadEnabledModules()
162
+
163
+ const consolidated: Record<string, Record<string, string>> = {}
164
+ const grouped: Record<GroupKey, Record<string, Record<string, string>>> = {}
165
+ const modulesDict: Record<string, string> = {}
166
+ const groupedModulesDict: Record<GroupKey, Record<string, string>> = {}
167
+
168
+ const fieldsByGroup: Record<GroupKey, Record<string, EntityFieldMap>> = {}
169
+
170
+ for (const entry of entries) {
171
+ const modId = entry.id
172
+ const roots = resolver.getModulePaths(entry)
173
+ const imps = resolver.getModuleImportBase(entry)
174
+ const group: GroupKey = (entry.from as GroupKey) || '@open-mercato/core'
175
+
176
+ // Locate entities definition file (prefer app override)
177
+ const appData = path.join(roots.appBase, 'data')
178
+ const pkgData = path.join(roots.pkgBase, 'data')
179
+ const appDb = path.join(roots.appBase, 'db')
180
+ const pkgDb = path.join(roots.pkgBase, 'db')
181
+ const bases = [appData, pkgData, appDb, pkgDb]
182
+ const candidates = ['entities.override.ts', 'entities.ts', 'schema.ts']
183
+ let importPath: string | null = null
184
+ let filePath: string | null = null
185
+
186
+ for (const base of bases) {
187
+ for (const f of candidates) {
188
+ const p = path.join(base, f)
189
+ if (fs.existsSync(p)) {
190
+ const fromApp = base.startsWith(roots.appBase)
191
+ const sub = path.basename(base) // 'data' | 'db'
192
+ importPath = `${fromApp ? imps.appBase : imps.pkgBase}/${sub}/${f.replace(/\.ts$/, '')}`
193
+ filePath = p
194
+ break
195
+ }
196
+ }
197
+ if (importPath) break
198
+ }
199
+
200
+ // No entities file found -> still register module id
201
+ if (!importPath) {
202
+ modulesDict[modId] = modId
203
+ groupedModulesDict[group] = groupedModulesDict[group] || {}
204
+ groupedModulesDict[group][modId] = modId
205
+ continue
206
+ }
207
+
208
+ // Get exported class names - either via dynamic import or TypeScript parsing
209
+ let exportNames: string[]
210
+ const isAppModule = entry.from === '@app'
211
+
212
+ if (isAppModule && filePath) {
213
+ // For @app modules, parse TypeScript source directly
214
+ // since Node.js can't import TypeScript files (path alias @/ doesn't resolve at runtime)
215
+ exportNames = parseExportedClassNamesFromFile(filePath)
216
+ } else {
217
+ // For package modules, use dynamic import
218
+ let mod: Record<string, unknown>
219
+ try {
220
+ mod = await import(importPath)
221
+ } catch (err) {
222
+ // Module import failed, record error and skip
223
+ const errorMessage = err instanceof Error ? err.message : String(err)
224
+ result.errors.push(`Failed to import ${importPath}: ${errorMessage}`)
225
+ modulesDict[modId] = modId
226
+ groupedModulesDict[group] = groupedModulesDict[group] || {}
227
+ groupedModulesDict[group][modId] = modId
228
+ continue
229
+ }
230
+ exportNames = Object.keys(mod).filter((k) => typeof mod[k] === 'function')
231
+ }
232
+
233
+ const entityNames = exportNames
234
+ .map((k) => toSnake(k))
235
+ .filter((k, idx, arr) => arr.indexOf(k) === idx)
236
+
237
+ // Build dictionaries
238
+ modulesDict[modId] = modId
239
+ groupedModulesDict[group] = groupedModulesDict[group] || {}
240
+ groupedModulesDict[group][modId] = modId
241
+
242
+ consolidated[modId] = consolidated[modId] || {}
243
+ grouped[group] = grouped[group] || {}
244
+ grouped[group][modId] = grouped[group][modId] || {}
245
+
246
+ for (const en of entityNames) {
247
+ consolidated[modId][en] = `${modId}:${en}`
248
+ grouped[group][modId][en] = `${modId}:${en}`
249
+ }
250
+
251
+ if (filePath) {
252
+ // exportNames already contains only class/function names from either source
253
+ const entityFieldMap = parseEntityFieldsFromFile(filePath, exportNames)
254
+ fieldsByGroup[group] = fieldsByGroup[group] || {}
255
+ fieldsByGroup[group][modId] = entityFieldMap
256
+ }
257
+ }
258
+
259
+ // Write consolidated output
260
+ const consolidatedSrc = `// AUTO-GENERATED by mercato generate entity-ids
261
+ export const M = ${JSON.stringify(modulesDict, null, 2)} as const
262
+ export const E = ${JSON.stringify(consolidated, null, 2)} as const
263
+ export type KnownModuleId = keyof typeof M
264
+ export type KnownEntities = typeof E
265
+ `
266
+
267
+ // Check if content has changed
268
+ const newChecksum = calculateChecksum(consolidatedSrc)
269
+ let shouldWrite = true
270
+
271
+ const existingRecord = readChecksumRecord(checksumFile)
272
+ if (existingRecord && existingRecord.content === newChecksum) {
273
+ shouldWrite = false
274
+ }
275
+
276
+ if (shouldWrite) {
277
+ ensureDir(outFile)
278
+ fs.writeFileSync(outFile, consolidatedSrc)
279
+ writeChecksumRecord(checksumFile, { content: newChecksum, structure: '' })
280
+ result.filesWritten.push(outFile)
281
+ if (!quiet) {
282
+ logGenerationResult(path.relative(process.cwd(), outFile), true)
283
+ }
284
+ } else {
285
+ result.filesUnchanged.push(outFile)
286
+ }
287
+
288
+ // Write per-group outputs
289
+ const groups = Object.keys(grouped) as GroupKey[]
290
+ for (const g of groups) {
291
+ const pkgOutputDir = resolver.getPackageOutputDir(g)
292
+ // Skip @app group since it writes to the same location as the consolidated output
293
+ if (g === '@app' && pkgOutputDir === outputDir) {
294
+ continue
295
+ }
296
+ const out = path.join(pkgOutputDir, 'entities.ids.generated.ts')
297
+
298
+ const src = `// AUTO-GENERATED by mercato generate entity-ids
299
+ export const M = ${JSON.stringify(groupedModulesDict[g] || {}, null, 2)} as const
300
+ export const E = ${JSON.stringify(grouped[g] || {}, null, 2)} as const
301
+ export type KnownModuleId = keyof typeof M
302
+ export type KnownEntities = typeof E
303
+ `
304
+ ensureDir(out)
305
+ fs.writeFileSync(out, src)
306
+ result.filesWritten.push(out)
307
+
308
+ const fieldsRoot = path.join(pkgOutputDir, 'entities')
309
+ const fieldsByModule = fieldsByGroup[g] || {}
310
+ const combined: EntityFieldMap = {}
311
+ for (const mId of Object.keys(fieldsByModule)) {
312
+ const mMap = fieldsByModule[mId]
313
+ for (const [entity, fields] of Object.entries(mMap)) {
314
+ combined[entity] = Array.from(new Set([...(combined[entity] || []), ...fields]))
315
+ }
316
+ }
317
+ writePerEntityFieldFiles(fieldsRoot, combined)
318
+
319
+ // Generate static entity fields registry for Turbopack compatibility
320
+ writeEntityFieldsRegistry(pkgOutputDir, combined)
321
+ }
322
+
323
+ // Write combined entity fields to root generated/ folder
324
+ const combinedAll: EntityFieldMap = {}
325
+ for (const groupFields of Object.values(fieldsByGroup)) {
326
+ for (const mMap of Object.values(groupFields)) {
327
+ for (const [entity, fields] of Object.entries(mMap)) {
328
+ combinedAll[entity] = Array.from(new Set([...(combinedAll[entity] || []), ...fields]))
329
+ }
330
+ }
331
+ }
332
+ writePerEntityFieldFiles(path.join(outputDir, 'entities'), combinedAll)
333
+ writeEntityFieldsRegistry(outputDir, combinedAll)
334
+
335
+ return result
336
+ }
@@ -0,0 +1,4 @@
1
+ export { generateEntityIds, type EntityIdsOptions } from './entity-ids'
2
+ export { generateModuleRegistry, generateModuleRegistryCli, type ModuleRegistryOptions } from './module-registry'
3
+ export { generateModuleEntities, type ModuleEntitiesOptions } from './module-entities'
4
+ export { generateModuleDi, type ModuleDiOptions } from './module-di'
@@ -0,0 +1,89 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import type { PackageResolver } from '../resolver'
4
+ import {
5
+ calculateChecksum,
6
+ readChecksumRecord,
7
+ writeChecksumRecord,
8
+ ensureDir,
9
+ toVar,
10
+ logGenerationResult,
11
+ type GeneratorResult,
12
+ createGeneratorResult,
13
+ } from '../utils'
14
+
15
+ export interface ModuleDiOptions {
16
+ resolver: PackageResolver
17
+ quiet?: boolean
18
+ }
19
+
20
+ export async function generateModuleDi(options: ModuleDiOptions): Promise<GeneratorResult> {
21
+ const { resolver, quiet = false } = options
22
+ const result = createGeneratorResult()
23
+
24
+ const outputDir = resolver.getOutputDir()
25
+ const outFile = path.join(outputDir, 'di.generated.ts')
26
+ const checksumFile = path.join(outputDir, 'di.generated.checksum')
27
+
28
+ const mods = resolver.loadEnabledModules()
29
+ const imports: string[] = []
30
+ const registrars: string[] = []
31
+ let i = 0
32
+
33
+ for (const entry of mods) {
34
+ const modId = entry.id
35
+ const roots = resolver.getModulePaths(entry)
36
+ const imp = resolver.getModuleImportBase(entry)
37
+ const appDi = path.join(roots.appBase, 'di.ts')
38
+ const pkgDi = path.join(roots.pkgBase, 'di.ts')
39
+ const useApp = fs.existsSync(appDi)
40
+ const usePkg = fs.existsSync(pkgDi)
41
+ const importName = `D_${toVar(modId)}_${i++}`
42
+
43
+ if (useApp) {
44
+ // For @app modules, use relative path to work in both Next.js and Node.js CLI context
45
+ // From .mercato/generated/, go up two levels (../..) to reach the app root, then into src/modules/
46
+ const isAppModule = entry.from === '@app'
47
+ const importPath = isAppModule ? `../../src/modules/${modId}/di` : `${imp.appBase}/di`
48
+ imports.push(`import * as ${importName} from '${importPath}'`)
49
+ registrars.push(`${importName}.register`)
50
+ } else if (usePkg) {
51
+ imports.push(`import * as ${importName} from '${imp.pkgBase}/di'`)
52
+ registrars.push(`${importName}.register`)
53
+ }
54
+ }
55
+
56
+ const output = `// AUTO-GENERATED by mercato generate di
57
+ ${imports.join('\n')}
58
+
59
+ const diRegistrars = [
60
+ ${registrars.join(',\n ')}
61
+ ].filter(Boolean) as (((c: any) => void)|undefined)[]
62
+
63
+ export { diRegistrars }
64
+ export default diRegistrars
65
+ `
66
+
67
+ // Check if content has changed
68
+ const newChecksum = calculateChecksum(output)
69
+ let shouldWrite = true
70
+
71
+ const existingRecord = readChecksumRecord(checksumFile)
72
+ if (existingRecord && existingRecord.content === newChecksum) {
73
+ shouldWrite = false
74
+ }
75
+
76
+ if (shouldWrite) {
77
+ ensureDir(outFile)
78
+ fs.writeFileSync(outFile, output)
79
+ writeChecksumRecord(checksumFile, { content: newChecksum, structure: '' })
80
+ result.filesWritten.push(outFile)
81
+ if (!quiet) {
82
+ logGenerationResult(path.relative(process.cwd(), outFile), true)
83
+ }
84
+ } else {
85
+ result.filesUnchanged.push(outFile)
86
+ }
87
+
88
+ return result
89
+ }
@@ -0,0 +1,124 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import type { PackageResolver } from '../resolver'
4
+ import {
5
+ calculateChecksum,
6
+ readChecksumRecord,
7
+ writeChecksumRecord,
8
+ ensureDir,
9
+ toVar,
10
+ logGenerationResult,
11
+ type GeneratorResult,
12
+ createGeneratorResult,
13
+ } from '../utils'
14
+
15
+ export interface ModuleEntitiesOptions {
16
+ resolver: PackageResolver
17
+ quiet?: boolean
18
+ }
19
+
20
+ export async function generateModuleEntities(options: ModuleEntitiesOptions): Promise<GeneratorResult> {
21
+ const { resolver, quiet = false } = options
22
+ const result = createGeneratorResult()
23
+
24
+ const outputDir = resolver.getOutputDir()
25
+ const outFile = path.join(outputDir, 'entities.generated.ts')
26
+ const checksumFile = path.join(outputDir, 'entities.generated.checksum')
27
+
28
+ const mods = resolver.loadEnabledModules()
29
+ const imports: string[] = []
30
+ const entitySources: Array<{ importName: string; moduleId: string }> = []
31
+ let n = 0
32
+
33
+ for (const entry of mods) {
34
+ const modId = entry.id
35
+ const roots = resolver.getModulePaths(entry)
36
+ const imp = resolver.getModuleImportBase(entry)
37
+
38
+ // prefer app override data/, fallback to core data/, then legacy db/
39
+ const appData = path.join(roots.appBase, 'data')
40
+ const pkgData = path.join(roots.pkgBase, 'data')
41
+ const appDb = path.join(roots.appBase, 'db')
42
+ const pkgDb = path.join(roots.pkgBase, 'db')
43
+ const bases = [appData, pkgData, appDb, pkgDb]
44
+ const candidates = ['entities.override.ts', 'entities.ts', 'schema.ts']
45
+
46
+ let found: { base: string; file: string } | null = null
47
+ for (const base of bases) {
48
+ for (const f of candidates) {
49
+ const p = path.join(base, f)
50
+ if (fs.existsSync(p)) {
51
+ found = { base, file: f }
52
+ break
53
+ }
54
+ }
55
+ if (found) break
56
+ }
57
+ if (!found) continue
58
+
59
+ const importName = `E_${toVar(modId)}_${n++}`
60
+ const sub = path.basename(found.base) // 'data' or 'db'
61
+ const fromApp = found.base.startsWith(roots.appBase)
62
+ const isAppModule = entry.from === '@app'
63
+ // For @app modules, use relative path to ensure it works both in Next.js and Node.js CLI context
64
+ // From .mercato/generated/, the relative path to src/modules/ is ../src/modules/
65
+ let relImport: string
66
+ if (isAppModule && fromApp) {
67
+ // From .mercato/generated/, go up two levels (../..) to reach the app root, then into src/modules/
68
+ relImport = `../../src/modules/${modId}/${sub}/${found.file.replace(/\.ts$/, '')}`
69
+ } else {
70
+ const baseImport = fromApp ? imp.appBase : imp.pkgBase
71
+ relImport = `${baseImport}/${sub}/${found.file.replace(/\.ts$/, '')}`
72
+ }
73
+ imports.push(`import * as ${importName} from '${relImport}'`)
74
+ entitySources.push({ importName, moduleId: modId })
75
+ }
76
+
77
+ const output = `// AUTO-GENERATED by mercato generate entities
78
+ ${imports.join('\n')}
79
+
80
+ function enhanceEntities(namespace: Record<string, unknown>, moduleId: string): any[] {
81
+ return Object.entries(namespace)
82
+ .filter(([, value]) => typeof value === 'function')
83
+ .map(([exportName, value]) => {
84
+ const entity = value as { entityName?: string }
85
+ if (entity && typeof entity === 'function' && !Object.prototype.hasOwnProperty.call(entity, 'entityName')) {
86
+ Object.defineProperty(entity, 'entityName', {
87
+ value: \`\${moduleId}.\${exportName}\`,
88
+ configurable: true,
89
+ enumerable: false,
90
+ writable: false,
91
+ })
92
+ }
93
+ return entity
94
+ })
95
+ }
96
+
97
+ export const entities = [
98
+ ${entitySources.map(({ importName, moduleId }) => `...enhanceEntities(${importName}, '${moduleId}')`).join(',\n ')}
99
+ ]
100
+ `
101
+
102
+ // Check if content has changed
103
+ const newChecksum = calculateChecksum(output)
104
+ let shouldWrite = true
105
+
106
+ const existingRecord = readChecksumRecord(checksumFile)
107
+ if (existingRecord && existingRecord.content === newChecksum) {
108
+ shouldWrite = false
109
+ }
110
+
111
+ if (shouldWrite) {
112
+ ensureDir(outFile)
113
+ fs.writeFileSync(outFile, output)
114
+ writeChecksumRecord(checksumFile, { content: newChecksum, structure: '' })
115
+ result.filesWritten.push(outFile)
116
+ if (!quiet) {
117
+ logGenerationResult(path.relative(process.cwd(), outFile), true)
118
+ }
119
+ } else {
120
+ result.filesUnchanged.push(outFile)
121
+ }
122
+
123
+ return result
124
+ }