@open-mercato/cli 0.6.6-develop.6352.1.8eee7e1399 → 0.6.6-develop.6353.1.efc82affea

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/cli",
3
- "version": "0.6.6-develop.6352.1.8eee7e1399",
3
+ "version": "0.6.6-develop.6353.1.efc82affea",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -60,8 +60,8 @@
60
60
  "@mikro-orm/decorators": "^7.1.4",
61
61
  "@mikro-orm/migrations": "^7.1.4",
62
62
  "@mikro-orm/postgresql": "^7.1.4",
63
- "@open-mercato/queue": "0.6.6-develop.6352.1.8eee7e1399",
64
- "@open-mercato/shared": "0.6.6-develop.6352.1.8eee7e1399",
63
+ "@open-mercato/queue": "0.6.6-develop.6353.1.efc82affea",
64
+ "@open-mercato/shared": "0.6.6-develop.6353.1.efc82affea",
65
65
  "cross-spawn": "^7.0.6",
66
66
  "pg": "8.22.0",
67
67
  "semver": "^7.8.5",
@@ -71,10 +71,10 @@
71
71
  "typescript": "^6.0.3"
72
72
  },
73
73
  "peerDependencies": {
74
- "@open-mercato/shared": "0.6.6-develop.6352.1.8eee7e1399"
74
+ "@open-mercato/shared": "0.6.6-develop.6353.1.efc82affea"
75
75
  },
76
76
  "devDependencies": {
77
- "@open-mercato/shared": "0.6.6-develop.6352.1.8eee7e1399",
77
+ "@open-mercato/shared": "0.6.6-develop.6353.1.efc82affea",
78
78
  "@types/jest": "^30.0.0",
79
79
  "jest": "^30.4.2",
80
80
  "ts-jest": "^29.4.11"
@@ -196,6 +196,8 @@ function scaffoldFixture(): ModuleEntry[] {
196
196
  touchFile(pkgModulePath('orders', 'data', 'guards.ts'), `export const guards = [\n { id: 'orders.prevent-duplicate', entity: 'orders:sales_order', event: 'create', description: 'Prevents duplicate orders', async validate(input: any) { return { ok: true } } },\n]\n`)
197
197
  touchFile(pkgModulePath('orders', 'api', 'interceptors.ts'), `export const interceptors = [\n { id: 'orders.validate-total', targetRoute: 'orders', methods: ['POST', 'PUT'], priority: 100, async before(request: any) { return { ok: true } } },\n]\n`)
198
198
  touchFile(pkgModulePath('orders', 'commands', 'interceptors.ts'), `export const interceptors = [\n { id: 'orders.audit-log', commandId: 'orders.create', phase: 'after', async handler(command: any) { return { ok: true } } },\n]\n`)
199
+ touchFile(pkgModulePath('orders', 'commands', 'create.ts'), `import { registerCommand } from '@open-mercato/shared/lib/commands'\nexport const ORDERS_CREATE_COMMAND_ID = 'orders.create'\nconst createOrderCommand = {\n id: ORDERS_CREATE_COMMAND_ID,\n async execute() { return { id: 'order-1' } },\n}\nregisterCommand(createOrderCommand)\n`)
200
+ touchFile(pkgModulePath('orders', 'commands', 'archive.ts'), `import { registerCommand } from '@open-mercato/shared/lib/commands'\nconst commandId = 'orders.archive'\nregisterCommand({\n id: commandId,\n async execute() { return { ok: true } },\n})\n`)
199
201
  touchFile(pkgModulePath('orders', 'acl.ts'), "export const features = ['orders.view', 'orders.create', 'orders.edit', 'orders.delete']\n")
200
202
  touchFile(pkgModulePath('orders', 'setup.ts'), "export const setup = { defaultRoleFeatures: ['orders.view'] }\n")
201
203
  touchFile(pkgModulePath('orders', 'encryption.ts'), "export const defaultEncryptionMaps = [{ entityId: 'orders:sales_order', fields: [{ field: 'customer_email', hashField: 'customer_email_hash' }] }]\nexport default defaultEncryptionMaps\n")
@@ -1177,6 +1179,27 @@ describe('search.generated.ts', () => {
1177
1179
  })
1178
1180
  })
1179
1181
 
1182
+ // ---------------------------------------------------------------------------
1183
+ // command-loaders.generated.ts
1184
+ // ---------------------------------------------------------------------------
1185
+
1186
+ describe('command-loaders.generated.ts', () => {
1187
+ it('includes command ids declared through const variables', async () => {
1188
+ const enabled = scaffoldFixture()
1189
+ const resolver = createMockResolver(enabled)
1190
+ await generateModuleRegistry({ resolver, quiet: true })
1191
+ const content = readGenerated('command-loaders.generated.ts')
1192
+
1193
+ expectExports(content, ['commandLoaderEntries'], 'command-loaders.generated.ts')
1194
+ expect(content).toContain('id: "orders.archive"')
1195
+ expect(content).toContain('id: "orders.create"')
1196
+ expect(content).toContain('key: "orders:commands:archive"')
1197
+ expect(content).toContain('key: "orders:commands:create"')
1198
+ expect(content).toContain('@open-mercato/core/modules/orders/commands/archive')
1199
+ expect(content).toContain('@open-mercato/core/modules/orders/commands/create')
1200
+ })
1201
+ })
1202
+
1180
1203
  // ---------------------------------------------------------------------------
1181
1204
  // modules.app.generated.ts
1182
1205
  // ---------------------------------------------------------------------------
@@ -80,6 +80,13 @@ type DashboardWidgetEntry = {
80
80
  importPath: string
81
81
  }
82
82
 
83
+ type CommandLoaderGenerationEntry = {
84
+ moduleId: string
85
+ key: string
86
+ importPath: string
87
+ ids: string[]
88
+ }
89
+
83
90
  type RuntimeApiMethodMetadata = {
84
91
  requireAuth?: boolean
85
92
  requireRoles?: string[]
@@ -1031,6 +1038,164 @@ function buildDynamicImportExpression(importPath: string): string {
1031
1038
  return `import(${toLiteral(sanitizeGeneratedModuleSpecifier(importPath))})`
1032
1039
  }
1033
1040
 
1041
+ const COMMAND_SCAN_CONFIG = {
1042
+ folder: 'commands',
1043
+ include: (name: string) =>
1044
+ ['.ts', '.js', '.tsx', '.jsx'].some((extension) => name.endsWith(extension)) &&
1045
+ !name.endsWith('.d.ts') &&
1046
+ !/\.(test|spec)\.[jt]sx?$/.test(name) &&
1047
+ stripModuleCodeExtension(name) !== 'index',
1048
+ sort: (a: string, b: string) => a.localeCompare(b),
1049
+ }
1050
+
1051
+ function getStaticStringExpression(
1052
+ expr: ts.Expression,
1053
+ stringConstants: Map<string, string> = new Map(),
1054
+ ): string | null {
1055
+ const unwrapped = unwrapExpression(expr)
1056
+ if (ts.isStringLiteral(unwrapped) || ts.isNoSubstitutionTemplateLiteral(unwrapped)) return unwrapped.text
1057
+ if (ts.isIdentifier(unwrapped)) {
1058
+ const direct = stringConstants.get(unwrapped.text)
1059
+ if (direct) return direct
1060
+ }
1061
+ return null
1062
+ }
1063
+
1064
+ function getPropertyNameText(name: ts.PropertyName): string | null {
1065
+ if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) return name.text
1066
+ return null
1067
+ }
1068
+
1069
+ function getObjectStringProperty(
1070
+ object: ts.ObjectLiteralExpression,
1071
+ propertyName: string,
1072
+ stringConstants: Map<string, string> = new Map(),
1073
+ ): string | null {
1074
+ for (const property of object.properties) {
1075
+ if (!ts.isPropertyAssignment(property)) continue
1076
+ const name = getPropertyNameText(property.name)
1077
+ if (name !== propertyName) continue
1078
+ return getStaticStringExpression(property.initializer, stringConstants)
1079
+ }
1080
+ return null
1081
+ }
1082
+
1083
+ function collectStringArrayElements(expr: ts.Expression, stringConstants: Map<string, string> = new Map()): string[] {
1084
+ const unwrapped = unwrapExpression(expr)
1085
+ if (!ts.isArrayLiteralExpression(unwrapped)) return []
1086
+ const values: string[] = []
1087
+ for (const element of unwrapped.elements) {
1088
+ const value = getStaticStringExpression(element, stringConstants)
1089
+ if (value) values.push(value)
1090
+ }
1091
+ return values
1092
+ }
1093
+
1094
+ function extractCommandIdsFromSource(sourcePath: string): string[] {
1095
+ const sourceText = fs.readFileSync(sourcePath, 'utf8')
1096
+ const sourceFile = ts.createSourceFile(sourcePath, sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX)
1097
+ const ids = new Set<string>()
1098
+ const stringConstants = new Map<string, string>()
1099
+ const variableCommandIds = new Map<string, string>()
1100
+
1101
+ const collectVariables = (node: ts.Node): void => {
1102
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer) {
1103
+ const stringValue = getStaticStringExpression(node.initializer, stringConstants)
1104
+ if (stringValue) {
1105
+ stringConstants.set(node.name.text, stringValue)
1106
+ }
1107
+ if (ts.isObjectLiteralExpression(node.initializer)) {
1108
+ const id = getObjectStringProperty(node.initializer, 'id', stringConstants)
1109
+ if (id) variableCommandIds.set(node.name.text, id)
1110
+ } else if (node.name.text === 'commandIds') {
1111
+ for (const id of collectStringArrayElements(node.initializer, stringConstants)) ids.add(id)
1112
+ }
1113
+ }
1114
+ ts.forEachChild(node, collectVariables)
1115
+ }
1116
+
1117
+ const collectRegistrations = (node: ts.Node): void => {
1118
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
1119
+ const callName = node.expression.text
1120
+ const firstArg = node.arguments[0]
1121
+ if (callName === 'registerCommand' && firstArg) {
1122
+ if (ts.isIdentifier(firstArg)) {
1123
+ const id = variableCommandIds.get(firstArg.text)
1124
+ if (id) ids.add(id)
1125
+ } else if (ts.isObjectLiteralExpression(firstArg)) {
1126
+ const id = getObjectStringProperty(firstArg, 'id', stringConstants)
1127
+ if (id) ids.add(id)
1128
+ }
1129
+ }
1130
+ if (callName === 'registerDictionaryEntryCommands' && firstArg && ts.isObjectLiteralExpression(firstArg)) {
1131
+ const prefix = getObjectStringProperty(firstArg, 'commandPrefix', stringConstants)
1132
+ if (prefix) {
1133
+ ids.add(`${prefix}.create`)
1134
+ ids.add(`${prefix}.update`)
1135
+ ids.add(`${prefix}.delete`)
1136
+ }
1137
+ }
1138
+ }
1139
+ ts.forEachChild(node, collectRegistrations)
1140
+ }
1141
+
1142
+ collectVariables(sourceFile)
1143
+ collectRegistrations(sourceFile)
1144
+ return Array.from(ids).sort((a, b) => a.localeCompare(b))
1145
+ }
1146
+
1147
+ function collectCommandLoaderEntries(
1148
+ roots: ModuleRoots,
1149
+ imps: ModuleImports,
1150
+ modId: string,
1151
+ ): CommandLoaderGenerationEntry[] {
1152
+ const files = scanModuleDir(roots, COMMAND_SCAN_CONFIG)
1153
+ const entries: CommandLoaderGenerationEntry[] = []
1154
+ for (const file of files) {
1155
+ const relPath = `commands/${file.relPath}`
1156
+ const resolved = resolveModuleFile(roots, imps, relPath)
1157
+ if (!resolved) continue
1158
+ const logicalKey = stripModuleCodeExtension(file.relPath)
1159
+ const basename = path.basename(logicalKey)
1160
+ if (basename === 'shared' || basename === 'factory') continue
1161
+ entries.push({
1162
+ moduleId: modId,
1163
+ key: `${modId}:commands:${logicalKey}`,
1164
+ importPath: resolved.importPath,
1165
+ ids: extractCommandIdsFromSource(resolved.absolutePath),
1166
+ })
1167
+ }
1168
+ return entries
1169
+ }
1170
+
1171
+ function renderCommandLoadersFile(entries: CommandLoaderGenerationEntry[]): string {
1172
+ const seenCommandIds = new Map<string, string>()
1173
+ const rendered: string[] = []
1174
+
1175
+ for (const entry of entries) {
1176
+ const loadExpr = `() => ${buildDynamicImportExpression(entry.importPath)}`
1177
+ for (const id of entry.ids) {
1178
+ const previous = seenCommandIds.get(id)
1179
+ if (previous && previous !== entry.key) {
1180
+ throw new Error(`[generate] Duplicate command id "${id}" discovered in "${previous}" and "${entry.key}"`)
1181
+ }
1182
+ seenCommandIds.set(id, entry.key)
1183
+ rendered.push(` { moduleId: ${toLiteral(entry.moduleId)}, id: ${toLiteral(id)}, key: ${toLiteral(entry.key)}, load: ${loadExpr} },`)
1184
+ }
1185
+ rendered.push(` { moduleId: ${toLiteral(entry.moduleId)}, key: ${toLiteral(entry.key)}, load: ${loadExpr} },`)
1186
+ }
1187
+
1188
+ return `// AUTO-GENERATED by mercato generate command-loaders
1189
+ import type { CommandLoader } from '@open-mercato/shared/lib/commands'
1190
+
1191
+ export const commandLoaderEntries: CommandLoader[] = [
1192
+ ${rendered.join('\n')}
1193
+ ]
1194
+
1195
+ export default commandLoaderEntries
1196
+ `
1197
+ }
1198
+
1034
1199
  function serializeGeneratedImport(statement: GeneratedImportStatement): string {
1035
1200
  if (typeof statement === 'string') {
1036
1201
  return statement
@@ -2577,6 +2742,8 @@ export async function generateModuleRegistry(options: ModuleRegistryOptions): Pr
2577
2742
  const bootstrapRegsChecksumFile = path.join(outputDir, 'bootstrap-registrations.generated.checksum')
2578
2743
  const legacySubscribersOutFile = path.join(outputDir, 'subscribers.generated.ts')
2579
2744
  const legacySubscribersChecksumFile = path.join(outputDir, 'subscribers.generated.checksum')
2745
+ const commandLoadersOutFile = path.join(outputDir, 'command-loaders.generated.ts')
2746
+ const commandLoadersChecksumFile = path.join(outputDir, 'command-loaders.generated.checksum')
2580
2747
 
2581
2748
  const enabled = resolver.loadEnabledModules()
2582
2749
  for (const entry of enabled) {
@@ -2621,6 +2788,7 @@ export async function generateModuleRegistry(options: ModuleRegistryOptions): Pr
2621
2788
  const importIdRef = { value: 0 }
2622
2789
  const trackedRoots = new Set<string>()
2623
2790
  const requiresByModule = new Map<string, string[]>()
2791
+ const commandLoaderEntries: CommandLoaderGenerationEntry[] = []
2624
2792
  let hasRouteComponents = false
2625
2793
 
2626
2794
  // UMES conflict detection: collect file paths during module processing
@@ -2641,6 +2809,7 @@ export async function generateModuleRegistry(options: ModuleRegistryOptions): Pr
2641
2809
  const isAppModule = entry.from === '@app'
2642
2810
  const appImportBase = isAppModule ? `../../src/modules/${modId}` : rawImps.appBase
2643
2811
  const imps: ModuleImports = { appBase: appImportBase, pkgBase: rawImps.pkgBase }
2812
+ commandLoaderEntries.push(...collectCommandLoaderEntries(roots, imps, modId))
2644
2813
 
2645
2814
  const frontendRoutes: string[] = []
2646
2815
  const backendRoutes: string[] = []
@@ -3124,6 +3293,8 @@ export async function generateModuleRegistry(options: ModuleRegistryOptions): Pr
3124
3293
  writeGeneratedFile({ outFile: backendRoutesOutFile, checksumFile: backendRoutesChecksumFile, content: backendRoutesOutput, structureChecksum, result, quiet })
3125
3294
  writeGeneratedFile({ outFile: apiRoutesOutFile, checksumFile: apiRoutesChecksumFile, content: apiRoutesOutput, structureChecksum, result, quiet })
3126
3295
  writeGeneratedFile({ outFile: legacySubscribersOutFile, checksumFile: legacySubscribersChecksumFile, content: legacySubscribersOutput, structureChecksum, result, quiet })
3296
+ const commandLoadersOutput = renderCommandLoadersFile(commandLoaderEntries)
3297
+ writeGeneratedFile({ outFile: commandLoadersOutFile, checksumFile: commandLoadersChecksumFile, content: commandLoadersOutput, structureChecksum, result, quiet })
3127
3298
 
3128
3299
  for (const extension of extensions) {
3129
3300
  const outputs = extension.generateOutput()
@@ -3200,6 +3371,8 @@ export async function generateModuleRegistryApp(options: ModuleRegistryOptions):
3200
3371
  const legacyChecksumFile = path.join(outputDir, 'bootstrap-modules.generated.checksum')
3201
3372
  const enabledIdsOutFile = path.join(outputDir, 'enabled-module-ids.generated.ts')
3202
3373
  const enabledIdsChecksumFile = path.join(outputDir, 'enabled-module-ids.generated.checksum')
3374
+ const commandLoadersOutFile = path.join(outputDir, 'command-loaders.generated.ts')
3375
+ const commandLoadersChecksumFile = path.join(outputDir, 'command-loaders.generated.checksum')
3203
3376
 
3204
3377
  const enabled = resolver.loadEnabledModules()
3205
3378
  for (const entry of enabled) {
@@ -3210,6 +3383,7 @@ export async function generateModuleRegistryApp(options: ModuleRegistryOptions):
3210
3383
  const importIdRef = { value: 0 }
3211
3384
  const trackedRoots = new Set<string>()
3212
3385
  const requiresByModule = new Map<string, string[]>()
3386
+ const commandLoaderEntries: CommandLoaderGenerationEntry[] = []
3213
3387
  let hasRouteComponents = false
3214
3388
 
3215
3389
  for (const entry of enabled) {
@@ -3222,6 +3396,7 @@ export async function generateModuleRegistryApp(options: ModuleRegistryOptions):
3222
3396
  const isAppModule = entry.from === '@app'
3223
3397
  const appImportBase = isAppModule ? `../../src/modules/${modId}` : rawImps.appBase
3224
3398
  const imps: ModuleImports = { appBase: appImportBase, pkgBase: rawImps.pkgBase }
3399
+ commandLoaderEntries.push(...collectCommandLoaderEntries(roots, imps, modId))
3225
3400
 
3226
3401
  const frontendRoutes: WriterFunction[] = []
3227
3402
  const backendRoutes: WriterFunction[] = []
@@ -3514,6 +3689,9 @@ export async function generateModuleRegistryApp(options: ModuleRegistryOptions):
3514
3689
  const enabledIdsOutput = renderEnabledModuleIdsFile(enabled.map((entry) => entry.id))
3515
3690
  writeGeneratedFile({ outFile: enabledIdsOutFile, checksumFile: enabledIdsChecksumFile, content: enabledIdsOutput, structureChecksum, result, quiet })
3516
3691
 
3692
+ const commandLoadersOutput = renderCommandLoadersFile(commandLoaderEntries)
3693
+ writeGeneratedFile({ outFile: commandLoadersOutFile, checksumFile: commandLoadersChecksumFile, content: commandLoadersOutput, structureChecksum, result, quiet })
3694
+
3517
3695
  return result
3518
3696
  }
3519
3697
 
@@ -3534,6 +3712,8 @@ export async function generateModuleRegistryCli(options: ModuleRegistryOptions):
3534
3712
  const checksumFile = path.join(outputDir, 'modules.cli.generated.checksum')
3535
3713
  const legacyOutFile = path.join(outputDir, 'cli-modules.generated.ts')
3536
3714
  const legacyChecksumFile = path.join(outputDir, 'cli-modules.generated.checksum')
3715
+ const commandLoadersOutFile = path.join(outputDir, 'command-loaders.generated.ts')
3716
+ const commandLoadersChecksumFile = path.join(outputDir, 'command-loaders.generated.checksum')
3537
3717
 
3538
3718
  const enabled = resolver.loadEnabledModules()
3539
3719
  for (const entry of enabled) {
@@ -3545,6 +3725,7 @@ export async function generateModuleRegistryCli(options: ModuleRegistryOptions):
3545
3725
  const importIdRef = { value: 0 }
3546
3726
  const trackedRoots = new Set<string>()
3547
3727
  const requiresByModule = new Map<string, string[]>()
3728
+ const commandLoaderEntries: CommandLoaderGenerationEntry[] = []
3548
3729
 
3549
3730
  for (const entry of enabled) {
3550
3731
  const modId = entry.id
@@ -3556,6 +3737,7 @@ export async function generateModuleRegistryCli(options: ModuleRegistryOptions):
3556
3737
  const isAppModule = entry.from === '@app'
3557
3738
  const appImportBase = isAppModule ? `../../src/modules/${modId}` : rawImps.appBase
3558
3739
  const imps: ModuleImports = { appBase: appImportBase, pkgBase: rawImps.pkgBase }
3740
+ commandLoaderEntries.push(...collectCommandLoaderEntries(roots, imps, modId))
3559
3741
 
3560
3742
  let cliImportName: string | null = null
3561
3743
  const translations: GeneratedObjectEntry[] = []
@@ -3848,6 +4030,8 @@ export async function generateModuleRegistryCli(options: ModuleRegistryOptions):
3848
4030
  const structureChecksum = calculateStructureChecksum(Array.from(trackedRoots))
3849
4031
  writeGeneratedFile({ outFile, checksumFile, content: output, structureChecksum, result, quiet })
3850
4032
  writeGeneratedFile({ outFile: legacyOutFile, checksumFile: legacyChecksumFile, content: legacyOutput, structureChecksum, result, quiet })
4033
+ const commandLoadersOutput = renderCommandLoadersFile(commandLoaderEntries)
4034
+ writeGeneratedFile({ outFile: commandLoadersOutFile, checksumFile: commandLoadersChecksumFile, content: commandLoadersOutput, structureChecksum, result, quiet })
3851
4035
 
3852
4036
  return result
3853
4037
  }