@open-mercato/cli 0.6.3-develop.3881.1.0b590ac4eb → 0.6.3-develop.3901.1.ddad60693a

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/src/mercato.ts CHANGED
@@ -21,6 +21,14 @@ import {
21
21
  resolveLazySchedulerRestart,
22
22
  } from './lib/auto-spawn-scheduler'
23
23
  import { startLazySchedulerSupervisor } from './lib/scheduler-supervisor'
24
+ import {
25
+ startInProcessGenerateWatcher,
26
+ type GenerateWatcherHandle,
27
+ } from './lib/in-process-generate-watcher'
28
+ import {
29
+ resolveGenerateWatcherMode,
30
+ type GenerateWatcherMode,
31
+ } from './lib/in-process-generate-watcher-mode'
24
32
  import { parseModuleInstallArgs } from './lib/module-install-args'
25
33
  import { resolveNextBuildIdCandidate } from './lib/next-build-id'
26
34
  import { acquireServerStartLock } from './lib/server-start-lock'
@@ -556,6 +564,58 @@ async function runPostGenerateStructuralCachePurge(quiet: boolean): Promise<void
556
564
  }
557
565
  }
558
566
 
567
+ /**
568
+ * Generator suite invoked by both `mercato generate all` and the in-process
569
+ * generate watcher embedded in `mercato server dev`. Hoisted to module scope
570
+ * so the watcher embedded in the server lifecycle can reuse the same closure
571
+ * without re-importing the closure-scoped version inside `buildBaseModules`.
572
+ */
573
+ async function runGeneratorSuite(quiet: boolean): Promise<void> {
574
+ const { createResolver } = await import('./lib/resolver')
575
+ const {
576
+ generateEntityIds,
577
+ generateModuleRegistry,
578
+ generateModuleRegistryApp,
579
+ generateModuleRegistryCli,
580
+ generateModuleEntities,
581
+ generateModuleDi,
582
+ generateModulePackageSources,
583
+ generateOpenApi,
584
+ } = await import('./lib/generators')
585
+ const resolver = createResolver()
586
+ await generateEntityIds({ resolver, quiet })
587
+ await generateModuleRegistry({ resolver, quiet })
588
+ await generateModuleRegistryApp({ resolver, quiet })
589
+ await generateModuleRegistryCli({ resolver, quiet })
590
+ await generateModuleEntities({ resolver, quiet })
591
+ await generateModuleDi({ resolver, quiet })
592
+ await generateModulePackageSources({ resolver, quiet })
593
+ await generateOpenApi({ resolver, quiet })
594
+ }
595
+
596
+ /**
597
+ * Builds the structural-fingerprint function used by the in-process generate
598
+ * watcher. Walks the same module roots the legacy `mercato generate watch`
599
+ * CLI command tracked, so the polling semantics are byte-for-byte identical.
600
+ */
601
+ function createGenerateWatchChecksumFn(): () => Promise<string> {
602
+ return async () => {
603
+ const { createResolver } = await import('./lib/resolver')
604
+ const { calculateStructureChecksum } = await import('./lib/utils')
605
+ const resolver = createResolver()
606
+ const tracked = new Set<string>([
607
+ path.join(resolver.getAppDir(), 'src', 'modules.ts'),
608
+ path.join(resolver.getAppDir(), 'src', 'modules'),
609
+ ])
610
+ for (const entry of resolver.loadEnabledModules()) {
611
+ const roots = resolver.getModulePaths(entry)
612
+ tracked.add(roots.appBase)
613
+ tracked.add(roots.pkgBase)
614
+ }
615
+ return calculateStructureChecksum(Array.from(tracked))
616
+ }
617
+ }
618
+
559
619
  // Build all CLI modules (registered + built-in)
560
620
  async function buildAllModules(): Promise<Module[]> {
561
621
  const modules = getCliModules()
@@ -1475,30 +1535,6 @@ export async function run(argv = process.argv) {
1475
1535
  },
1476
1536
  ],
1477
1537
  } as any)
1478
-
1479
- const runGeneratorSuite = async (quiet: boolean) => {
1480
- const { createResolver } = await import('./lib/resolver')
1481
- const {
1482
- generateEntityIds,
1483
- generateModuleRegistry,
1484
- generateModuleRegistryApp,
1485
- generateModuleRegistryCli,
1486
- generateModuleEntities,
1487
- generateModuleDi,
1488
- generateModulePackageSources,
1489
- generateOpenApi,
1490
- } = await import('./lib/generators')
1491
- const resolver = createResolver()
1492
-
1493
- await generateEntityIds({ resolver, quiet })
1494
- await generateModuleRegistry({ resolver, quiet })
1495
- await generateModuleRegistryApp({ resolver, quiet })
1496
- await generateModuleRegistryCli({ resolver, quiet })
1497
- await generateModuleEntities({ resolver, quiet })
1498
- await generateModuleDi({ resolver, quiet })
1499
- await generateModulePackageSources({ resolver, quiet })
1500
- await generateOpenApi({ resolver, quiet })
1501
- }
1502
1538
 
1503
1539
  // Built-in CLI module: generate
1504
1540
  all.push({
@@ -1518,75 +1554,46 @@ export async function run(argv = process.argv) {
1518
1554
  {
1519
1555
  command: 'watch',
1520
1556
  run: async (args: string[]) => {
1521
- const { createResolver } = await import('./lib/resolver')
1522
- const { calculateStructureChecksum } = await import('./lib/utils')
1523
1557
  const quiet = args.includes('--quiet') || args.includes('-q')
1524
1558
  const skipInitial = args.includes('--skip-initial')
1525
1559
  const intervalArg = args.find((arg) => arg.startsWith('--interval='))
1526
1560
  const parsedInterval = intervalArg ? Number.parseInt(intervalArg.split('=')[1] ?? '', 10) : NaN
1527
1561
  const intervalMs = Number.isFinite(parsedInterval) && parsedInterval >= 250 ? parsedInterval : 1000
1528
- let previousChecksum = ''
1529
- let running = false
1530
- let pending = false
1531
-
1532
- const getTrackedPaths = () => {
1533
- const resolver = createResolver()
1534
- const tracked = new Set<string>([
1535
- path.join(resolver.getAppDir(), 'src', 'modules.ts'),
1536
- path.join(resolver.getAppDir(), 'src', 'modules'),
1537
- ])
1538
- for (const entry of resolver.loadEnabledModules()) {
1539
- const roots = resolver.getModulePaths(entry)
1540
- tracked.add(roots.appBase)
1541
- tracked.add(roots.pkgBase)
1542
- }
1543
- return Array.from(tracked)
1544
- }
1545
1562
 
1546
- const runWatchGeneration = async (reason: string) => {
1547
- if (running) {
1548
- pending = true
1549
- return
1550
- }
1551
- running = true
1552
- try {
1553
- if (!quiet) {
1554
- console.log(`[generate:watch] Regenerating (${reason})...`)
1555
- }
1563
+ const watcher = startInProcessGenerateWatcher({
1564
+ pollMs: intervalMs,
1565
+ skipInitial,
1566
+ quiet,
1567
+ computeStructureChecksum: createGenerateWatchChecksumFn(),
1568
+ runGenerators: async () => {
1556
1569
  await runGeneratorSuite(true)
1557
1570
  await runPostGenerateStructuralCachePurge(true)
1558
- if (!quiet) {
1559
- console.log('[generate:watch] Generators completed.')
1560
- }
1561
- } catch (error) {
1562
- console.error('[generate:watch] Generation failed:', error instanceof Error ? error.message : error)
1563
- } finally {
1564
- running = false
1565
- if (pending) {
1566
- pending = false
1567
- await runWatchGeneration('queued change')
1568
- }
1569
- }
1570
- }
1571
+ },
1572
+ })
1571
1573
 
1572
- if (!skipInitial) {
1573
- await runWatchGeneration('initial')
1574
+ const shutdownSignals: NodeJS.Signals[] = ['SIGINT', 'SIGTERM']
1575
+ let shuttingDown = false
1576
+ const handleSignal = () => {
1577
+ if (shuttingDown) return
1578
+ shuttingDown = true
1579
+ void watcher.close()
1574
1580
  }
1575
- previousChecksum = calculateStructureChecksum(getTrackedPaths())
1576
- if (!quiet) {
1577
- if (skipInitial) {
1578
- console.log('[generate:watch] Skipping initial regeneration and watching the current generated state.')
1579
- }
1580
- console.log(`[generate:watch] Watching structural module files every ${intervalMs}ms`)
1581
+ for (const signal of shutdownSignals) {
1582
+ process.once(signal, handleSignal)
1581
1583
  }
1582
1584
 
1583
- // eslint-disable-next-line no-constant-condition
1584
- while (true) {
1585
- await new Promise((resolve) => setTimeout(resolve, intervalMs))
1586
- const nextChecksum = calculateStructureChecksum(getTrackedPaths())
1587
- if (nextChecksum === previousChecksum) continue
1588
- previousChecksum = nextChecksum
1589
- await runWatchGeneration('structure change')
1585
+ // The watcher's polling timer is `unref()`-ed so the event loop
1586
+ // would otherwise exit immediately for a standalone CLI invocation.
1587
+ // `keepAlive` holds the loop open until a shutdown signal calls
1588
+ // `watcher.close()`, which resolves `watcher.done`.
1589
+ const keepAlive = setInterval(() => {}, 1 << 30)
1590
+ try {
1591
+ await watcher.done
1592
+ } finally {
1593
+ clearInterval(keepAlive)
1594
+ for (const signal of shutdownSignals) {
1595
+ process.removeListener(signal, handleSignal)
1596
+ }
1590
1597
  }
1591
1598
  },
1592
1599
  },
@@ -1686,6 +1693,8 @@ export async function run(argv = process.argv) {
1686
1693
  let envChangePromiseResolve: ((result: DevServerRestartResult) => void) | null = null
1687
1694
  let activeLazySupervisor: ReturnType<typeof startLazyWorkerSupervisor> | null = null
1688
1695
  let activeLazySchedulerSupervisor: ReturnType<typeof startLazySchedulerSupervisor> | null = null
1696
+ let activeGenerateWatcher: GenerateWatcherHandle | null = null
1697
+ const generateWatcherMode: GenerateWatcherMode = resolveGenerateWatcherMode(process.env)
1689
1698
  const envReloader = createDevEnvReloader(appDir, process.env, initialProcessEnvironmentEntries)
1690
1699
 
1691
1700
  function cleanup() {
@@ -1701,6 +1710,9 @@ export async function run(argv = process.argv) {
1701
1710
  if (activeLazySchedulerSupervisor) {
1702
1711
  void activeLazySchedulerSupervisor.close().catch(() => undefined)
1703
1712
  }
1713
+ if (activeGenerateWatcher) {
1714
+ void activeGenerateWatcher.close().catch(() => undefined)
1715
+ }
1704
1716
  }
1705
1717
 
1706
1718
  async function cleanupAndWait() {
@@ -1731,6 +1743,14 @@ export async function run(argv = process.argv) {
1731
1743
  }
1732
1744
  activeLazySchedulerSupervisor = null
1733
1745
  }
1746
+ if (activeGenerateWatcher) {
1747
+ try {
1748
+ await activeGenerateWatcher.close()
1749
+ } catch {
1750
+ // In-process generate watcher close errors must never block dev shutdown.
1751
+ }
1752
+ activeGenerateWatcher = null
1753
+ }
1734
1754
  // Safety net: remove Next.js dev lock file in case the child didn't clean up
1735
1755
  const lockFile = path.join(appDir, '.mercato', 'next', 'dev', 'lock')
1736
1756
  try {
@@ -1881,6 +1901,31 @@ export async function run(argv = process.argv) {
1881
1901
  }
1882
1902
  }
1883
1903
 
1904
+ if (generateWatcherMode === 'in-process') {
1905
+ // Run the structural regeneration watcher inside this process
1906
+ // instead of spawning a dedicated `mercato generate watch --skip-initial`
1907
+ // sidecar. Saves ~190 MB of resident RSS on a typical dev box
1908
+ // (measured against the legacy sidecar). Opt back into the
1909
+ // sidecar with `OM_DEV_GENERATE_WATCH_MODE=legacy` if needed.
1910
+ console.log('[server] In-process generate watcher enabled — structural changes will regenerate without a sidecar process.')
1911
+ activeGenerateWatcher = startInProcessGenerateWatcher({
1912
+ // `--skip-initial` equivalent: `yarn dev` always runs an
1913
+ // initial `mercato generate` before reaching the server
1914
+ // command, so the watcher must not re-run generators at
1915
+ // boot time. Otherwise dev startup pays a generator pass
1916
+ // twice in a row.
1917
+ skipInitial: true,
1918
+ quiet: false,
1919
+ computeStructureChecksum: createGenerateWatchChecksumFn(),
1920
+ runGenerators: async () => {
1921
+ await runGeneratorSuite(true)
1922
+ await runPostGenerateStructuralCachePurge(true)
1923
+ },
1924
+ })
1925
+ } else {
1926
+ console.log('[server] Legacy out-of-process generate watcher selected via OM_DEV_GENERATE_WATCH_MODE=legacy — expect the dev orchestrator to spawn `mercato generate watch --skip-initial`.')
1927
+ }
1928
+
1884
1929
  const firstExit = await Promise.race(managedExitPromises)
1885
1930
  await cleanupAndWait()
1886
1931
  envChangePromiseResolve = null