@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.
- package/bin/mercato +21 -0
- package/build.mjs +78 -0
- package/dist/bin.js +51 -0
- package/dist/bin.js.map +7 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +7 -0
- package/dist/lib/db/commands.js +350 -0
- package/dist/lib/db/commands.js.map +7 -0
- package/dist/lib/db/index.js +7 -0
- package/dist/lib/db/index.js.map +7 -0
- package/dist/lib/generators/entity-ids.js +257 -0
- package/dist/lib/generators/entity-ids.js.map +7 -0
- package/dist/lib/generators/index.js +12 -0
- package/dist/lib/generators/index.js.map +7 -0
- package/dist/lib/generators/module-di.js +73 -0
- package/dist/lib/generators/module-di.js.map +7 -0
- package/dist/lib/generators/module-entities.js +104 -0
- package/dist/lib/generators/module-entities.js.map +7 -0
- package/dist/lib/generators/module-registry.js +1081 -0
- package/dist/lib/generators/module-registry.js.map +7 -0
- package/dist/lib/resolver.js +205 -0
- package/dist/lib/resolver.js.map +7 -0
- package/dist/lib/utils.js +161 -0
- package/dist/lib/utils.js.map +7 -0
- package/dist/mercato.js +1045 -0
- package/dist/mercato.js.map +7 -0
- package/dist/registry.js +7 -0
- package/dist/registry.js.map +7 -0
- package/jest.config.cjs +19 -0
- package/package.json +71 -0
- package/src/__tests__/mercato.test.ts +90 -0
- package/src/bin.ts +74 -0
- package/src/index.ts +2 -0
- package/src/lib/__tests__/resolver.test.ts +101 -0
- package/src/lib/__tests__/utils.test.ts +270 -0
- package/src/lib/db/__tests__/commands.test.ts +131 -0
- package/src/lib/db/commands.ts +431 -0
- package/src/lib/db/index.ts +1 -0
- package/src/lib/generators/__tests__/generators.test.ts +197 -0
- package/src/lib/generators/entity-ids.ts +336 -0
- package/src/lib/generators/index.ts +4 -0
- package/src/lib/generators/module-di.ts +89 -0
- package/src/lib/generators/module-entities.ts +124 -0
- package/src/lib/generators/module-registry.ts +1222 -0
- package/src/lib/resolver.ts +308 -0
- package/src/lib/utils.ts +200 -0
- package/src/mercato.ts +1106 -0
- package/src/registry.ts +2 -0
- package/tsconfig.build.json +4 -0
- package/tsconfig.json +12 -0
- 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
|
+
}
|