@open-mercato/cli 0.5.1-develop.2699.f8b50c8046 → 0.5.1-develop.2709.b6bdd776ac

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.5.1-develop.2699.f8b50c8046",
3
+ "version": "0.5.1-develop.2709.b6bdd776ac",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "exports": {
@@ -59,8 +59,8 @@
59
59
  "@mikro-orm/decorators": "^7.0.10",
60
60
  "@mikro-orm/migrations": "^7.0.10",
61
61
  "@mikro-orm/postgresql": "^7.0.10",
62
- "@open-mercato/queue": "0.5.1-develop.2699.f8b50c8046",
63
- "@open-mercato/shared": "0.5.1-develop.2699.f8b50c8046",
62
+ "@open-mercato/queue": "0.5.1-develop.2709.b6bdd776ac",
63
+ "@open-mercato/shared": "0.5.1-develop.2709.b6bdd776ac",
64
64
  "cross-spawn": "^7.0.6",
65
65
  "pg": "8.20.0",
66
66
  "semver": "^7.7.4",
@@ -70,10 +70,10 @@
70
70
  "typescript": "^5.9.3"
71
71
  },
72
72
  "peerDependencies": {
73
- "@open-mercato/shared": "0.5.1-develop.2699.f8b50c8046"
73
+ "@open-mercato/shared": "0.5.1-develop.2709.b6bdd776ac"
74
74
  },
75
75
  "devDependencies": {
76
- "@open-mercato/shared": "0.5.1-develop.2699.f8b50c8046",
76
+ "@open-mercato/shared": "0.5.1-develop.2709.b6bdd776ac",
77
77
  "@types/jest": "^30.0.0",
78
78
  "jest": "^30.3.0",
79
79
  "ts-jest": "^29.4.9"
@@ -416,6 +416,76 @@ describe('generateModuleRegistry with module subsets', () => {
416
416
  expect(output).toContain('subscriber_meta:on-event')
417
417
  })
418
418
 
419
+ it('extracts worker metadata from imported queue constants when runtime import fails', async () => {
420
+ touchFile(
421
+ path.join(tmpDir, 'packages', 'core', 'src', 'modules', 'worker_meta', 'lib', 'queue.ts'),
422
+ "export const IMPORTED_QUEUE = 'worker.meta.queue'\n",
423
+ )
424
+ touchFile(
425
+ path.join(tmpDir, 'packages', 'core', 'src', 'modules', 'worker_meta', 'workers', 'process-job.ts'),
426
+ [
427
+ "import { IMPORTED_QUEUE } from '../lib/queue'",
428
+ "import { helper } from '../lib/helper'",
429
+ "export const metadata = { queue: IMPORTED_QUEUE, id: 'worker_meta:process-job', concurrency: 4 }",
430
+ 'export default async function handle() { return helper }',
431
+ '',
432
+ ].join('\n'),
433
+ )
434
+ touchFile(
435
+ path.join(tmpDir, 'packages', 'core', 'src', 'modules', 'worker_meta', 'lib', 'helper.ts'),
436
+ 'export const helper = true\n',
437
+ )
438
+
439
+ const resolver = createMockResolver(tmpDir, [
440
+ { id: 'worker_meta', from: '@open-mercato/core' },
441
+ ])
442
+
443
+ const result = await generateModuleRegistry({ resolver, quiet: true })
444
+
445
+ expect(result.errors).toEqual([])
446
+ const output = readGenerated(tmpDir, 'modules.generated.ts')!
447
+ expect(output).toContain('queue: "worker.meta.queue"')
448
+ expect(output).toContain('id: "worker_meta:process-job"')
449
+ expect(output).toContain('concurrency: 4')
450
+ })
451
+
452
+ it('extracts subscriber metadata from imported event objects when runtime import fails', async () => {
453
+ touchFile(
454
+ path.join(tmpDir, 'packages', 'core', 'src', 'modules', 'subscriber_event_meta', 'lib', 'events.ts'),
455
+ [
456
+ 'export const SUBSCRIBER_EVENTS = {',
457
+ " CREATED: 'subscriber.event.created',",
458
+ "} as const",
459
+ '',
460
+ ].join('\n'),
461
+ )
462
+ touchFile(
463
+ path.join(tmpDir, 'packages', 'core', 'src', 'modules', 'subscriber_event_meta', 'lib', 'helper.ts'),
464
+ 'export const helper = true\n',
465
+ )
466
+ touchFile(
467
+ path.join(tmpDir, 'packages', 'core', 'src', 'modules', 'subscriber_event_meta', 'subscribers', 'on-created.ts'),
468
+ [
469
+ "import { helper } from '../lib/helper'",
470
+ "import { SUBSCRIBER_EVENTS } from '../lib/events'",
471
+ "export const metadata = { event: SUBSCRIBER_EVENTS.CREATED, persistent: true, id: 'subscriber_event_meta:on-created' }",
472
+ 'export default async function handle() { return helper }',
473
+ '',
474
+ ].join('\n'),
475
+ )
476
+
477
+ const resolver = createMockResolver(tmpDir, [
478
+ { id: 'subscriber_event_meta', from: '@open-mercato/core' },
479
+ ])
480
+
481
+ const result = await generateModuleRegistry({ resolver, quiet: true })
482
+
483
+ expect(result.errors).toEqual([])
484
+ const output = readGenerated(tmpDir, 'modules.generated.ts')!
485
+ expect(output).toContain('subscriber.event.created')
486
+ expect(output).toContain('subscriber_event_meta:on-created')
487
+ })
488
+
419
489
  it('keeps backend route metadata runtime-backed when icons are non-serializable', async () => {
420
490
  touchFile(
421
491
  path.join(tmpDir, 'packages', 'core', 'src', 'modules', 'iconic', 'backend', 'dashboard', 'page.tsx'),
@@ -402,6 +402,83 @@ function collectLocalDeclarations(parsed: ts.SourceFile): Map<string, ts.Express
402
402
  return locals
403
403
  }
404
404
 
405
+ type ImportedBinding =
406
+ | {
407
+ kind: 'named'
408
+ sourceFile: string
409
+ exportName: string
410
+ }
411
+ | {
412
+ kind: 'default'
413
+ sourceFile: string
414
+ }
415
+ | {
416
+ kind: 'namespace'
417
+ sourceFile: string
418
+ }
419
+
420
+ type ParsedModuleResolutionContext = {
421
+ sourceFile: string
422
+ parsed: ts.SourceFile
423
+ locals: Map<string, ts.Expression>
424
+ exportedNames: Set<string>
425
+ imports: Map<string, ImportedBinding>
426
+ }
427
+
428
+ function resolveImportedModuleFile(sourceFile: string, moduleSpecifier: string): string | null {
429
+ if (!moduleSpecifier.startsWith('.')) return null
430
+ return findExistingModuleFileByBaseNames(path.dirname(sourceFile), [
431
+ moduleSpecifier,
432
+ path.join(moduleSpecifier, 'index'),
433
+ ])
434
+ }
435
+
436
+ function collectImportedBindings(
437
+ parsed: ts.SourceFile,
438
+ sourceFile: string,
439
+ ): Map<string, ImportedBinding> {
440
+ const bindings = new Map<string, ImportedBinding>()
441
+
442
+ for (const statement of parsed.statements) {
443
+ if (!ts.isImportDeclaration(statement)) continue
444
+ if (!ts.isStringLiteral(statement.moduleSpecifier)) continue
445
+
446
+ const resolvedSourceFile = resolveImportedModuleFile(sourceFile, statement.moduleSpecifier.text)
447
+ if (!resolvedSourceFile) continue
448
+
449
+ const importClause = statement.importClause
450
+ if (!importClause) continue
451
+
452
+ if (importClause.name) {
453
+ bindings.set(importClause.name.text, {
454
+ kind: 'default',
455
+ sourceFile: resolvedSourceFile,
456
+ })
457
+ }
458
+
459
+ const namedBindings = importClause.namedBindings
460
+ if (!namedBindings) continue
461
+
462
+ if (ts.isNamespaceImport(namedBindings)) {
463
+ bindings.set(namedBindings.name.text, {
464
+ kind: 'namespace',
465
+ sourceFile: resolvedSourceFile,
466
+ })
467
+ continue
468
+ }
469
+
470
+ for (const element of namedBindings.elements) {
471
+ bindings.set(element.name.text, {
472
+ kind: 'named',
473
+ sourceFile: resolvedSourceFile,
474
+ exportName: element.propertyName?.text ?? element.name.text,
475
+ })
476
+ }
477
+ }
478
+
479
+ return bindings
480
+ }
481
+
405
482
  function collectReexportedNames(parsed: ts.SourceFile): Set<string> {
406
483
  const exportedNames = new Set<string>()
407
484
  for (const statement of parsed.statements) {
@@ -464,10 +541,128 @@ function findExportedInitializer(
464
541
  return null
465
542
  }
466
543
 
544
+ function findDefaultExportInitializer(parsed: ts.SourceFile): ts.Expression | null {
545
+ for (const statement of parsed.statements) {
546
+ if (ts.isExportAssignment(statement) && !statement.isExportEquals) {
547
+ return statement.expression
548
+ }
549
+
550
+ if (!ts.isVariableStatement(statement)) continue
551
+ const modifiers = ts.canHaveModifiers(statement) ? ts.getModifiers(statement) : undefined
552
+ const isDefaultExport = modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword) ?? false
553
+ if (!isDefaultExport) continue
554
+
555
+ for (const declaration of statement.declarationList.declarations) {
556
+ if (declaration.initializer) return declaration.initializer
557
+ }
558
+ }
559
+
560
+ return null
561
+ }
562
+
563
+ function loadParsedModuleResolutionContext(
564
+ sourceFile: string,
565
+ cache: Map<string, ParsedModuleResolutionContext | null>,
566
+ ): ParsedModuleResolutionContext | null {
567
+ if (cache.has(sourceFile)) {
568
+ return cache.get(sourceFile) ?? null
569
+ }
570
+
571
+ let source = ''
572
+ try {
573
+ source = fs.readFileSync(sourceFile, 'utf8')
574
+ } catch {
575
+ cache.set(sourceFile, null)
576
+ return null
577
+ }
578
+
579
+ const parsed = ts.createSourceFile(
580
+ sourceFile,
581
+ source,
582
+ ts.ScriptTarget.Latest,
583
+ true,
584
+ inferScriptKind(sourceFile),
585
+ )
586
+
587
+ const context: ParsedModuleResolutionContext = {
588
+ sourceFile,
589
+ parsed,
590
+ locals: collectLocalDeclarations(parsed),
591
+ exportedNames: collectReexportedNames(parsed),
592
+ imports: collectImportedBindings(parsed, sourceFile),
593
+ }
594
+ cache.set(sourceFile, context)
595
+ return context
596
+ }
597
+
598
+ function resolveImportedBindingValue(
599
+ binding: ImportedBinding,
600
+ pathSegments: string[],
601
+ visited: Set<string>,
602
+ cache: Map<string, ParsedModuleResolutionContext | null>,
603
+ ): unknown {
604
+ const importedContext = loadParsedModuleResolutionContext(binding.sourceFile, cache)
605
+ if (!importedContext) return undefined
606
+
607
+ if (binding.kind === 'namespace') {
608
+ if (pathSegments.length === 0) return undefined
609
+ const [exportName, ...restPath] = pathSegments
610
+ const importKey = `${binding.sourceFile}::${exportName}`
611
+ if (visited.has(importKey)) return undefined
612
+
613
+ let initializer = findExportedInitializer(importedContext.parsed, exportName, importedContext.exportedNames)
614
+ if (!initializer && importedContext.exportedNames.has(exportName)) {
615
+ initializer = importedContext.locals.get(exportName) ?? null
616
+ }
617
+ if (!initializer) return undefined
618
+
619
+ const nextVisited = new Set(visited)
620
+ nextVisited.add(importKey)
621
+ let value = resolveExpressionValue(initializer, importedContext, nextVisited, cache)
622
+ for (const segment of restPath) {
623
+ if (value && typeof value === 'object' && !Array.isArray(value) && segment in (value as Record<string, unknown>)) {
624
+ value = (value as Record<string, unknown>)[segment]
625
+ } else {
626
+ return undefined
627
+ }
628
+ }
629
+ return value
630
+ }
631
+
632
+ const importKey = binding.kind === 'default'
633
+ ? `${binding.sourceFile}::default`
634
+ : `${binding.sourceFile}::${binding.exportName}`
635
+ if (visited.has(importKey)) return undefined
636
+
637
+ let initializer = binding.kind === 'default'
638
+ ? findDefaultExportInitializer(importedContext.parsed)
639
+ : findExportedInitializer(importedContext.parsed, binding.exportName, importedContext.exportedNames)
640
+
641
+ if (!initializer && binding.kind === 'named' && importedContext.exportedNames.has(binding.exportName)) {
642
+ initializer = importedContext.locals.get(binding.exportName) ?? null
643
+ }
644
+ if (!initializer) return undefined
645
+
646
+ const nextVisited = new Set(visited)
647
+ nextVisited.add(importKey)
648
+ let value = resolveExpressionValue(initializer, importedContext, nextVisited, cache)
649
+
650
+ for (const segment of pathSegments) {
651
+ if (value && typeof value === 'object' && !Array.isArray(value) && segment in (value as Record<string, unknown>)) {
652
+ value = (value as Record<string, unknown>)[segment]
653
+ } else {
654
+ return undefined
655
+ }
656
+ }
657
+
658
+ return value
659
+ }
660
+
467
661
  function resolveExpressionValue(
468
662
  expression: ts.Expression,
469
- locals: Map<string, ts.Expression>,
663
+ context: ParsedModuleResolutionContext,
470
664
  visited: Set<string>,
665
+ cache: Map<string, ParsedModuleResolutionContext | null>,
471
666
  ): unknown {
472
667
  const expr = unwrapExpression(expression)
473
668
 
@@ -488,19 +683,19 @@ function resolveExpressionValue(
488
683
  : ts.isStringLiteral(property.name) ? property.name.text
489
684
  : null
490
685
  if (!key) continue
491
- const value = resolveExpressionValue(property.initializer, locals, visited)
686
+ const value = resolveExpressionValue(property.initializer, context, visited, cache)
492
687
  if (value !== undefined) result[key] = value
493
688
  } else if (ts.isShorthandPropertyAssignment(property)) {
494
689
  const key = property.name.text
495
- const initializer = locals.get(key)
690
+ const initializer = context.locals.get(key)
496
691
  if (initializer && !visited.has(key)) {
497
692
  const nextVisited = new Set(visited)
498
693
  nextVisited.add(key)
499
- const value = resolveExpressionValue(initializer, locals, nextVisited)
694
+ const value = resolveExpressionValue(initializer, context, nextVisited, cache)
500
695
  if (value !== undefined) result[key] = value
501
696
  }
502
697
  } else if (ts.isSpreadAssignment(property)) {
503
- const spread = resolveExpressionValue(property.expression, locals, visited)
698
+ const spread = resolveExpressionValue(property.expression, context, visited, cache)
504
699
  if (spread && typeof spread === 'object' && !Array.isArray(spread)) {
505
700
  Object.assign(result, spread)
506
701
  }
@@ -513,11 +708,11 @@ function resolveExpressionValue(
513
708
  const result: unknown[] = []
514
709
  for (const element of expr.elements) {
515
710
  if (ts.isSpreadElement(element)) {
516
- const spread = resolveExpressionValue(element.expression, locals, visited)
711
+ const spread = resolveExpressionValue(element.expression, context, visited, cache)
517
712
  if (Array.isArray(spread)) result.push(...spread)
518
713
  continue
519
714
  }
520
- const value = resolveExpressionValue(element as ts.Expression, locals, visited)
715
+ const value = resolveExpressionValue(element as ts.Expression, context, visited, cache)
521
716
  if (value !== undefined) result.push(value)
522
717
  }
523
718
  return result
@@ -525,11 +720,16 @@ function resolveExpressionValue(
525
720
 
526
721
  if (ts.isIdentifier(expr)) {
527
722
  if (visited.has(expr.text)) return undefined
528
- const initializer = locals.get(expr.text)
529
- if (!initializer) return undefined
530
- const nextVisited = new Set(visited)
531
- nextVisited.add(expr.text)
532
- return resolveExpressionValue(initializer, locals, nextVisited)
723
+ const initializer = context.locals.get(expr.text)
724
+ if (initializer) {
725
+ const nextVisited = new Set(visited)
726
+ nextVisited.add(expr.text)
727
+ return resolveExpressionValue(initializer, context, nextVisited, cache)
728
+ }
729
+
730
+ const importBinding = context.imports.get(expr.text)
731
+ if (!importBinding) return undefined
732
+ return resolveImportedBindingValue(importBinding, [], visited, cache)
533
733
  }
534
734
 
535
735
  if (ts.isPropertyAccessExpression(expr)) {
@@ -542,40 +742,46 @@ function resolveExpressionValue(
542
742
  }
543
743
  if (!ts.isIdentifier(cursor)) return undefined
544
744
  const rootName = cursor.text
545
- if (visited.has(rootName)) return undefined
546
- const rootInit = locals.get(rootName)
547
- if (!rootInit) return undefined
548
- const unwrappedRoot = unwrapExpression(rootInit)
549
- const nextVisited = new Set(visited)
550
- nextVisited.add(rootName)
551
- // Handle `const crud = makeCrudRoute({ metadata: routeMetadata, ... })`
552
- if (ts.isCallExpression(unwrappedRoot) && unwrappedRoot.arguments.length > 0) {
553
- const firstArg = unwrapExpression(unwrappedRoot.arguments[0])
554
- if (ts.isObjectLiteralExpression(firstArg)) {
555
- const argObject = resolveExpressionValue(firstArg, locals, nextVisited)
556
- if (argObject && typeof argObject === 'object' && !Array.isArray(argObject)) {
557
- let current: unknown = argObject
558
- for (const segment of pathSegments) {
559
- if (current && typeof current === 'object' && !Array.isArray(current) && segment in (current as Record<string, unknown>)) {
560
- current = (current as Record<string, unknown>)[segment]
561
- } else {
562
- current = undefined
563
- break
745
+ if (!visited.has(rootName)) {
746
+ const rootInit = context.locals.get(rootName)
747
+ if (rootInit) {
748
+ const unwrappedRoot = unwrapExpression(rootInit)
749
+ const nextVisited = new Set(visited)
750
+ nextVisited.add(rootName)
751
+ // Handle `const crud = makeCrudRoute({ metadata: routeMetadata, ... })`
752
+ if (ts.isCallExpression(unwrappedRoot) && unwrappedRoot.arguments.length > 0) {
753
+ const firstArg = unwrapExpression(unwrappedRoot.arguments[0])
754
+ if (ts.isObjectLiteralExpression(firstArg)) {
755
+ const argObject = resolveExpressionValue(firstArg, context, nextVisited, cache)
756
+ if (argObject && typeof argObject === 'object' && !Array.isArray(argObject)) {
757
+ let current: unknown = argObject
758
+ for (const segment of pathSegments) {
759
+ if (current && typeof current === 'object' && !Array.isArray(current) && segment in (current as Record<string, unknown>)) {
760
+ current = (current as Record<string, unknown>)[segment]
761
+ } else {
762
+ current = undefined
763
+ break
764
+ }
765
+ }
766
+ if (current !== undefined) return current
564
767
  }
565
768
  }
566
- if (current !== undefined) return current
567
769
  }
770
+ let value = resolveExpressionValue(rootInit, context, nextVisited, cache)
771
+ for (const segment of pathSegments) {
772
+ if (value && typeof value === 'object' && !Array.isArray(value) && segment in (value as Record<string, unknown>)) {
773
+ value = (value as Record<string, unknown>)[segment]
774
+ } else {
775
+ return undefined
776
+ }
777
+ }
778
+ return value
568
779
  }
569
780
  }
570
- let value = resolveExpressionValue(rootInit, locals, nextVisited)
571
- for (const segment of pathSegments) {
572
- if (value && typeof value === 'object' && !Array.isArray(value) && segment in (value as Record<string, unknown>)) {
573
- value = (value as Record<string, unknown>)[segment]
574
- } else {
575
- return undefined
576
- }
577
- }
578
- return value
781
+
782
+ const importBinding = context.imports.get(rootName)
783
+ if (!importBinding) return undefined
784
+ return resolveImportedBindingValue(importBinding, pathSegments, visited, cache)
579
785
  }
580
786
 
581
787
  return undefined
@@ -629,27 +835,16 @@ export function hasNamedExport(sourceFile: string, exportName: string): boolean
629
835
  }
630
836
 
631
837
  export function resolveNamedObjectExport(sourceFile: string, exportName: string): Record<string, unknown> | null {
632
- let source = ''
633
- try {
634
- source = fs.readFileSync(sourceFile, 'utf8')
635
- } catch {
636
- return null
637
- }
638
- const parsed = ts.createSourceFile(
639
- sourceFile,
640
- source,
641
- ts.ScriptTarget.Latest,
642
- true,
643
- inferScriptKind(sourceFile),
644
- )
645
- const locals = collectLocalDeclarations(parsed)
646
- const exportedNames = collectReexportedNames(parsed)
647
- let initializer = findExportedInitializer(parsed, exportName, exportedNames)
648
- if (!initializer && exportedNames.has(exportName)) {
649
- initializer = locals.get(exportName) ?? null
838
+ const cache = new Map<string, ParsedModuleResolutionContext | null>()
839
+ const context = loadParsedModuleResolutionContext(sourceFile, cache)
840
+ if (!context) return null
841
+
842
+ let initializer = findExportedInitializer(context.parsed, exportName, context.exportedNames)
843
+ if (!initializer && context.exportedNames.has(exportName)) {
844
+ initializer = context.locals.get(exportName) ?? null
650
845
  }
651
846
  if (!initializer) return null
652
- const value = resolveExpressionValue(initializer, locals, new Set<string>())
847
+ const value = resolveExpressionValue(initializer, context, new Set<string>(), cache)
653
848
  if (!value || typeof value !== 'object' || Array.isArray(value)) return null
654
849
  const record = value as Record<string, unknown>
655
850
  return Object.keys(record).length > 0 ? record : null