create-mercato-app 0.6.4-develop.3944.1.4100aa7fbe → 0.6.4-develop.3962.1.70f30e284c

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": "create-mercato-app",
3
- "version": "0.6.4-develop.3944.1.4100aa7fbe",
3
+ "version": "0.6.4-develop.3962.1.70f30e284c",
4
4
  "type": "module",
5
5
  "description": "Create a new Open Mercato application",
6
6
  "main": "./dist/index.js",
@@ -1,11 +1,20 @@
1
1
  import fs from 'node:fs'
2
2
  import path from 'node:path'
3
3
 
4
- // Greenfield must not inherit the prior run's compiler state. Wiping the
5
- // configured Next.js distDir (`.mercato/next`) plus the legacy `.next` location guarantees Turbopack rebuilds the route table
6
- // and middleware manifest from scratch on the next launch.
4
+ // Greenfield must not inherit stale route manifests, but wiping the whole
5
+ // configured Next.js distDir also discards Turbopack's reusable compiler cache
6
+ // and makes first /login warmup much slower. Remove manifests/locks that encode
7
+ // route shape while preserving `.mercato/next/dev/cache/turbopack`.
7
8
  export const GREENFIELD_PURGE_TARGETS = Object.freeze([
8
- Object.freeze(['.mercato', 'next']),
9
+ Object.freeze(['.mercato', 'next', 'dev', 'lock']),
10
+ Object.freeze(['.mercato', 'next', 'dev', 'build-manifest.json']),
11
+ Object.freeze(['.mercato', 'next', 'dev', 'fallback-build-manifest.json']),
12
+ Object.freeze(['.mercato', 'next', 'dev', 'prerender-manifest.json']),
13
+ Object.freeze(['.mercato', 'next', 'dev', 'routes-manifest.json']),
14
+ Object.freeze(['.mercato', 'next', 'dev', 'server', 'app-paths-manifest.json']),
15
+ Object.freeze(['.mercato', 'next', 'dev', 'server', 'middleware-build-manifest.js']),
16
+ Object.freeze(['.mercato', 'next', 'dev', 'server', 'middleware-manifest.json']),
17
+ Object.freeze(['.mercato', 'next', 'dev', 'server', 'pages-manifest.json']),
9
18
  Object.freeze(['.next']),
10
19
  ])
11
20
 
@@ -23,7 +32,7 @@ export function purgeAppBuildCaches({
23
32
  removed.push(segments.join('/'))
24
33
  }
25
34
  if (removed.length === 0) {
26
- logger.log('🧹 [dev:greenfield] no stale Next/Turbopack build directories to purge')
35
+ logger.log('🧹 [dev:greenfield] no stale Next/Turbopack manifest files to purge')
27
36
  } else {
28
37
  for (const relPath of removed) {
29
38
  logger.log(`🧹 [dev:greenfield] removed ${relPath}`)
@@ -90,6 +90,8 @@ const verbose = !classic && (process.argv.includes('--verbose') || process.env.M
90
90
  const rawPassthrough = classic || verbose
91
91
  const interactiveLogToggle = !rawPassthrough && process.stdin.isTTY && process.stdout.isTTY && process.env.CI !== 'true'
92
92
  const splashChildStateFile = process.env.OM_DEV_SPLASH_CHILD_STATE_FILE?.trim() || null
93
+ const warmupReadyFile = process.env.OM_DEV_WARMUP_READY_FILE?.trim()
94
+ || (splashChildStateFile ? `${splashChildStateFile}.warmup-ready` : null)
93
95
  const splashMode = process.env.OM_DEV_SPLASH_MODE?.trim() || 'dev'
94
96
  const setupSplashMode = splashMode === 'setup'
95
97
  const startupSplashPhase = setupSplashMode ? 'Project setup is in progress...' : 'Installation and first compilation is in progress...'
@@ -98,6 +100,10 @@ const configuredRuntimeProgressCurrent = parsePositiveIntegerEnv(process.env.OM_
98
100
  const runtimeProgressTotal = configuredRuntimeProgressTotal ?? (setupSplashMode ? 5 : 4)
99
101
  const runtimeProgressCurrent = configuredRuntimeProgressCurrent ?? (setupSplashMode ? 4 : 0)
100
102
  const runtimeReadyProgressCurrent = Math.max(runtimeProgressCurrent, runtimeProgressTotal)
103
+ const runtimeWarmupProgressCurrent = Math.max(
104
+ runtimeProgressCurrent,
105
+ Math.min(runtimeReadyProgressCurrent, Math.max(0, runtimeProgressTotal - 1)),
106
+ )
101
107
  const children = new Set()
102
108
  let shuttingDown = false
103
109
  let logsVisible = false
@@ -116,6 +122,7 @@ const backgroundServiceModes = {
116
122
  workers: resolveAutoSpawnMode(process.env, 'AUTO_SPAWN_WORKERS', 'OM_AUTO_SPAWN_WORKERS', 'OM_AUTO_SPAWN_WORKERS_LAZY'),
117
123
  scheduler: resolveAutoSpawnMode(process.env, 'AUTO_SPAWN_SCHEDULER', 'OM_AUTO_SPAWN_SCHEDULER', 'OM_AUTO_SPAWN_SCHEDULER_LAZY'),
118
124
  }
125
+ const shutdownNoticeOwnedByParent = process.env.OM_DEV_SHUTDOWN_NOTICE_OWNER === 'parent'
119
126
  const splashState = {
120
127
  mode: splashMode,
121
128
  phase: startupSplashPhase,
@@ -169,11 +176,38 @@ const runtimeWarmupState = {
169
176
  failed: false,
170
177
  promise: null,
171
178
  retryTimer: null,
179
+ abortController: null,
180
+ generation: 0,
172
181
  retryAttempts: 0,
173
182
  tenantId: readNonEmptyEnvValue('OM_DEV_WARMUP_TENANT_ID') ?? null,
174
183
  tenantLookupAttempted: false,
175
184
  }
176
185
 
186
+ function clearWarmupReadyFile() {
187
+ if (!warmupReadyFile) return
188
+ try {
189
+ fs.rmSync(warmupReadyFile, { force: true })
190
+ } catch {
191
+ // Warmup readiness is best-effort; terminal status remains authoritative.
192
+ }
193
+ }
194
+
195
+ function writeWarmupReadyFile(reason) {
196
+ if (!warmupReadyFile) return
197
+ try {
198
+ fs.mkdirSync(path.dirname(warmupReadyFile), { recursive: true })
199
+ fs.writeFileSync(warmupReadyFile, `${JSON.stringify({
200
+ ready: true,
201
+ reason,
202
+ at: new Date().toISOString(),
203
+ }, null, 2)}\n`)
204
+ } catch {
205
+ // Warmup readiness is best-effort; background services can still run without it.
206
+ }
207
+ }
208
+
209
+ clearWarmupReadyFile()
210
+
177
211
  function printCompactSummary(icon, title, lines) {
178
212
  if (!Array.isArray(lines) || lines.length === 0) return
179
213
  console.log(`${icon} ${title}`)
@@ -498,6 +532,7 @@ function spawnMercato(args) {
498
532
  OM_CLI_QUIET: rawPassthrough ? process.env.OM_CLI_QUIET : '1',
499
533
  DOTENV_CONFIG_QUIET: rawPassthrough ? process.env.DOTENV_CONFIG_QUIET : 'true',
500
534
  ...(!rawPassthrough ? { OM_DEV_SPLASH_RUNTIME_WRAPPER: '1' } : {}),
535
+ ...(!rawPassthrough && warmupReadyFile ? { OM_DEV_WARMUP_READY_FILE: warmupReadyFile } : {}),
501
536
  },
502
537
  ...resolvedSpawn.spawnOptions,
503
538
  })
@@ -658,10 +693,18 @@ async function resolveWarmupTenantIdFromDatabase(email) {
658
693
  }
659
694
  }
660
695
 
661
- async function fetchWithTimeout(url, init = {}, timeoutMs = 45000) {
696
+ async function fetchWithTimeout(url, init = {}, timeoutMs = 45000, externalSignal = null) {
697
+ if (externalSignal?.aborted) {
698
+ throw externalSignal.reason ?? new Error('warmup request aborted')
699
+ }
700
+
662
701
  const controller = new AbortController()
663
702
  const timer = setTimeout(() => controller.abort(), timeoutMs)
664
703
  timer.unref?.()
704
+ const abortFromExternalSignal = () => {
705
+ controller.abort(externalSignal.reason ?? new Error('warmup request aborted'))
706
+ }
707
+ externalSignal?.addEventListener?.('abort', abortFromExternalSignal, { once: true })
665
708
 
666
709
  try {
667
710
  return await fetch(url, {
@@ -670,6 +713,7 @@ async function fetchWithTimeout(url, init = {}, timeoutMs = 45000) {
670
713
  })
671
714
  } finally {
672
715
  clearTimeout(timer)
716
+ externalSignal?.removeEventListener?.('abort', abortFromExternalSignal)
673
717
  }
674
718
  }
675
719
 
@@ -683,14 +727,14 @@ function isAbortLikeError(error) {
683
727
  return /aborted/i.test(String(error))
684
728
  }
685
729
 
686
- async function fetchWarmupWithRetry(url, init, detailLabel, progressLabel) {
730
+ async function fetchWarmupWithRetry(url, init, detailLabel, progressLabel, signal = null) {
687
731
  let lastError = null
688
732
 
689
733
  for (let index = 0; index < warmupRequestTimeoutsMs.length; index += 1) {
690
734
  const timeoutMs = warmupRequestTimeoutsMs[index]
691
735
 
692
736
  try {
693
- return await fetchWithTimeout(url, init, timeoutMs)
737
+ return await fetchWithTimeout(url, init, timeoutMs, signal)
694
738
  } catch (error) {
695
739
  lastError = error
696
740
 
@@ -775,6 +819,20 @@ function clearWarmupRetryTimer() {
775
819
  runtimeWarmupState.retryTimer = null
776
820
  }
777
821
 
822
+ function resetWarmupForRuntimeRestart(reason) {
823
+ clearWarmupRetryTimer()
824
+ runtimeWarmupState.generation += 1
825
+ runtimeWarmupState.readySignalSeen = false
826
+ runtimeWarmupState.started = false
827
+ runtimeWarmupState.completed = false
828
+ runtimeWarmupState.failed = false
829
+ runtimeWarmupState.promise = null
830
+ runtimeWarmupState.retryAttempts = 0
831
+ runtimeWarmupState.abortController?.abort(new Error(`warmup aborted because ${reason}`))
832
+ runtimeWarmupState.abortController = null
833
+ clearWarmupReadyFile()
834
+ }
835
+
778
836
  function scheduleWarmupRetry(delayMs = 2000) {
779
837
  clearWarmupRetryTimer()
780
838
  runtimeWarmupState.retryTimer = setTimeout(() => {
@@ -791,6 +849,9 @@ async function runTargetedRouteWarmup() {
791
849
 
792
850
  clearWarmupRetryTimer()
793
851
  runtimeWarmupState.started = true
852
+ const generation = runtimeWarmupState.generation
853
+ const abortController = new AbortController()
854
+ runtimeWarmupState.abortController = abortController
794
855
  const startedAt = Date.now()
795
856
  const progressLabel = 'Precompiling login and backend'
796
857
  const introMessage = '🔥 Precompiling /login, login POST, and /backend'
@@ -805,7 +866,9 @@ async function runTargetedRouteWarmup() {
805
866
  { method: 'GET', redirect: 'manual' },
806
867
  '/login',
807
868
  progressLabel,
869
+ abortController.signal,
808
870
  )
871
+ if (generation !== runtimeWarmupState.generation) return
809
872
  if (shouldRetryWarmupStatus(loginPageResponse.status)) {
810
873
  throw createWarmupTransientError(`/login returned HTTP ${loginPageResponse.status}`)
811
874
  }
@@ -834,7 +897,9 @@ async function runTargetedRouteWarmup() {
834
897
  },
835
898
  'POST /api/auth/login',
836
899
  progressLabel,
900
+ abortController.signal,
837
901
  )
902
+ if (generation !== runtimeWarmupState.generation) return
838
903
  const loginPayload = await readResponsePayload(loginResponse)
839
904
  if (!loginResponse.ok || !loginPayload || typeof loginPayload !== 'object' || loginPayload.ok !== true) {
840
905
  const failure = extractWarmupErrorMessage(loginPayload, `HTTP ${loginResponse.status}`)
@@ -873,7 +938,9 @@ async function runTargetedRouteWarmup() {
873
938
  },
874
939
  '/backend',
875
940
  progressLabel,
941
+ abortController.signal,
876
942
  )
943
+ if (generation !== runtimeWarmupState.generation) return
877
944
  if (backendResponse.status >= 300 && backendResponse.status < 400) {
878
945
  const location = backendResponse.headers.get('location') || 'redirect'
879
946
  if (isWarmupRetryableRedirect(location)) {
@@ -897,6 +964,7 @@ async function runTargetedRouteWarmup() {
897
964
  runtimeWarmupState.completed = true
898
965
  runtimeWarmupState.failed = false
899
966
  runtimeWarmupState.promise = null
967
+ runtimeWarmupState.abortController = null
900
968
  const completedMessage = `🚪 Login flow and backend warmed in ${formatDuration(Date.now() - startedAt)}`
901
969
  updateSplashState({
902
970
  phase: 'App is ready',
@@ -911,9 +979,15 @@ async function runTargetedRouteWarmup() {
911
979
  progressLabel: 'App is ready',
912
980
  activity: completedMessage,
913
981
  })
982
+ writeWarmupReadyFile('warmup-complete')
914
983
  console.log(formatStatusOutput(completedMessage, runtimeReadyProgressCurrent, 'App is ready'))
915
984
  } catch (error) {
985
+ if (generation !== runtimeWarmupState.generation) {
986
+ return
987
+ }
988
+
916
989
  runtimeWarmupState.promise = null
990
+ runtimeWarmupState.abortController = null
917
991
 
918
992
  if (isAbortLikeError(error) || isWarmupTransientError(error)) {
919
993
  runtimeWarmupState.started = false
@@ -987,12 +1061,14 @@ async function runTargetedRouteWarmup() {
987
1061
  activity: warmupWarning,
988
1062
  })
989
1063
  if (isCredentialsFailure) {
1064
+ writeWarmupReadyFile('warmup-credentials-failed')
990
1065
  console.log(formatStatusOutput(
991
1066
  '⚠️ Warmup login returned 401 — credentials invalid. Set OM_INIT_SUPERADMIN_EMAIL/PASSWORD in .env or run: yarn initialize',
992
1067
  runtimeReadyProgressCurrent,
993
1068
  'App is ready',
994
1069
  ))
995
1070
  } else {
1071
+ writeWarmupReadyFile('warmup-incomplete')
996
1072
  console.log(formatStatusOutput(warmupWarning, runtimeReadyProgressCurrent, 'App is ready'))
997
1073
  }
998
1074
  }
@@ -1147,6 +1223,18 @@ function shutdown(exitCode = 0) {
1147
1223
  rawModeEnabled = false
1148
1224
  }
1149
1225
 
1226
+ if (!shutdownNoticeOwnedByParent) {
1227
+ const message = 'Shutting down services...'
1228
+ updateSplashState({
1229
+ phase: message,
1230
+ detail: 'Stopping app runtime, workers, and scheduler',
1231
+ ready: false,
1232
+ progressLabel: message,
1233
+ activity: message,
1234
+ })
1235
+ console.log(message)
1236
+ }
1237
+
1150
1238
  const alive = Array.from(children).filter((child) => !child.killed)
1151
1239
  if (alive.length === 0) {
1152
1240
  process.exit(exitCode)
@@ -1594,6 +1682,7 @@ function classifyServerLine(line) {
1594
1682
  const runtimeRestartMatch = line.match(/^\[server\] Detected (.+?)\. Restarting app runtime\.\.\.$/)
1595
1683
  if (runtimeRestartMatch) {
1596
1684
  const reason = runtimeRestartMatch[1]
1685
+ resetWarmupForRuntimeRestart(reason)
1597
1686
  return {
1598
1687
  type: 'status',
1599
1688
  message: `🔄 Restarting app runtime: ${reason}`,
@@ -1608,6 +1697,7 @@ function classifyServerLine(line) {
1608
1697
 
1609
1698
  if (line === '[server] Detected corrupted Turbopack dev cache. Clearing .mercato/next/dev and restarting Next.js once...') {
1610
1699
  const reason = 'corrupted Turbopack dev cache'
1700
+ resetWarmupForRuntimeRestart(reason)
1611
1701
  return {
1612
1702
  type: 'status',
1613
1703
  message: `🔄 Restarting Next.js dev server: ${reason}`,
@@ -1630,7 +1720,7 @@ function classifyServerLine(line) {
1630
1720
  readyUrl: localMatch[1],
1631
1721
  loginUrl: `${localMatch[1].replace(/\/$/, '')}/login`,
1632
1722
  activity: `App runtime at ${localMatch[1]}`,
1633
- progressCurrent: 4,
1723
+ progressCurrent: runtimeWarmupProgressCurrent,
1634
1724
  progressLabel: 'Precompiling login page',
1635
1725
  }
1636
1726
  }
@@ -1645,7 +1735,7 @@ function classifyServerLine(line) {
1645
1735
  ready: false,
1646
1736
  runtimeReady: true,
1647
1737
  activity: timing,
1648
- progressCurrent: 4,
1738
+ progressCurrent: runtimeWarmupProgressCurrent,
1649
1739
  progressLabel: 'Precompiling login page',
1650
1740
  }
1651
1741
  }
@@ -1654,7 +1744,7 @@ function classifyServerLine(line) {
1654
1744
  const target = compiledMatch[1]?.trim()
1655
1745
  const detail = target ? ` ${target}` : ''
1656
1746
  const timing = `⚡ Compiled${detail} in ${parseDurationToken(compiledMatch[2])}`
1657
- const progressCurrent = splashState.ready ? runtimeReadyProgressCurrent : 3
1747
+ const progressCurrent = splashState.ready ? runtimeReadyProgressCurrent : runtimeWarmupProgressCurrent
1658
1748
  return {
1659
1749
  type: 'status',
1660
1750
  message: timing,
@@ -202,6 +202,10 @@ const splashEnabled = !classic && !appOnly && splashPortConfig.enabled
202
202
  const autoOpenSplash = splashEnabled && process.stdout.isTTY && process.env.CI !== 'true' && process.env.OM_DEV_AUTO_OPEN !== '0'
203
203
  const splashBindHost = isContainerRuntime() ? '0.0.0.0' : '127.0.0.1'
204
204
  const standaloneRuntimeScript = path.join(process.cwd(), 'scripts', 'dev-runtime.mjs')
205
+ const warmupReadyFilePath = path.join(
206
+ process.cwd(),
207
+ isMonorepo ? 'apps/mercato/.mercato/dev-warmup-ready.json' : '.mercato/dev-warmup-ready.json',
208
+ )
205
209
  const devLogTeeDisabled = process.env.OM_DEV_LOG_TEE === '0' || process.env.OM_DEV_LOG_TEE === 'false'
206
210
 
207
211
  let devLogSessionInstance = null
@@ -501,20 +505,31 @@ function buildSplashChildEnv(options = {}) {
501
505
  }
502
506
 
503
507
  if (!splashChildStateFile) {
504
- return Object.keys(childEnv).length > 0 ? childEnv : undefined
508
+ const env = {
509
+ ...childEnv,
510
+ OM_DEV_SHUTDOWN_NOTICE_OWNER: 'parent',
511
+ }
512
+ return Object.keys(env).length > 0 ? env : undefined
505
513
  }
506
514
 
507
515
  return {
508
516
  ...childEnv,
509
517
  OM_DEV_SPLASH_CHILD_STATE_FILE: splashChildStateFile,
518
+ OM_DEV_WARMUP_READY_FILE: warmupReadyFilePath,
510
519
  OM_DEV_SPLASH_MODE: splashMode,
520
+ OM_DEV_SHUTDOWN_NOTICE_OWNER: 'parent',
511
521
  ...(Number.isFinite(options.stageCurrent) ? { OM_DEV_SPLASH_STAGE_CURRENT: String(options.stageCurrent) } : {}),
512
522
  ...(Number.isFinite(options.stageTotal) ? { OM_DEV_SPLASH_STAGE_TOTAL: String(options.stageTotal) } : {}),
513
523
  }
514
524
  }
515
525
 
516
526
  function applyLocalDevBackgroundServiceDefaults(childEnv) {
517
- const env = childEnv ?? {}
527
+ const env = {
528
+ ...(childEnv ?? {}),
529
+ OM_DEV_WARMUP_READY_FILE: (childEnv && 'OM_DEV_WARMUP_READY_FILE' in childEnv)
530
+ ? childEnv.OM_DEV_WARMUP_READY_FILE
531
+ : warmupReadyFilePath,
532
+ }
518
533
  if (
519
534
  typeof process.env.OM_AUTO_SPAWN_WORKERS_LAZY !== 'string'
520
535
  || process.env.OM_AUTO_SPAWN_WORKERS_LAZY.trim() === ''
@@ -998,6 +1013,18 @@ function closeSplashServer() {
998
1013
  writeSplashChildStateFileClear()
999
1014
  }
1000
1015
 
1016
+ function announceShutdown() {
1017
+ const message = 'Shutting down services...'
1018
+ updateSplashState({
1019
+ phase: message,
1020
+ detail: 'Stopping app runtime, watchers, workers, and scheduler',
1021
+ ready: false,
1022
+ progressLabel: message,
1023
+ activity: message,
1024
+ })
1025
+ console.log(message)
1026
+ }
1027
+
1001
1028
  function openBrowser(url) {
1002
1029
  try {
1003
1030
  let child
@@ -1016,10 +1043,11 @@ function openBrowser(url) {
1016
1043
  function shutdown(exitCode = 0) {
1017
1044
  if (shuttingDown) return
1018
1045
  shuttingDown = true
1019
- closeSplashServer()
1046
+ announceShutdown()
1020
1047
 
1021
1048
  const alive = Array.from(children).filter((child) => !child.killed)
1022
1049
  if (alive.length === 0) {
1050
+ closeSplashServer()
1023
1051
  closeDevLogSession()
1024
1052
  process.exit(exitCode)
1025
1053
  return
@@ -1035,6 +1063,7 @@ function shutdown(exitCode = 0) {
1035
1063
  killProcessTree(child, 'SIGKILL')
1036
1064
  }
1037
1065
  }
1066
+ closeSplashServer()
1038
1067
  closeDevLogSession()
1039
1068
  process.exit(exitCode)
1040
1069
  }, 3000)
@@ -1,7 +1,6 @@
1
1
  @import "tailwindcss";
2
2
  @import "tw-animate-css";
3
3
  @import "react-big-calendar/lib/css/react-big-calendar.css";
4
- @import "@xyflow/react/dist/style.css";
5
4
  @import "../../.mercato/generated/module-package-sources.css";
6
5
 
7
6
  /*
@@ -1,51 +1,77 @@
1
1
  "use client"
2
2
 
3
3
  import * as React from 'react'
4
- import { injectionWidgetEntries } from '@/.mercato/generated/injection-widgets.generated'
5
- // Side-effect: registers translatable fields for client-side TranslationManager
4
+ // Side-effect imports: these register types/components on import, so they
5
+ // MUST stay top-level to be available during the first paint.
6
6
  import '@/.mercato/generated/translations-fields.generated'
7
- import { injectionTables } from '@/.mercato/generated/injection-tables.generated'
8
- import { enabledModuleIds } from '@/.mercato/generated/enabled-module-ids.generated'
7
+ import '@/.mercato/generated/messages.client.generated'
8
+ import '@/.mercato/generated/payments.client.generated'
9
+
9
10
  import { registerCoreInjectionWidgets, registerCoreInjectionTables, registerEnabledModuleIds } from '@open-mercato/core/modules/widgets/lib/injection'
10
11
  import { registerInjectionWidgets } from '@open-mercato/ui/backend/injection/widgetRegistry'
11
- import { dashboardWidgetEntries } from '@/.mercato/generated/dashboard-widgets.generated'
12
12
  import { registerDashboardWidgets } from '@open-mercato/ui/backend/dashboard/widgetRegistry'
13
- import { notificationHandlerEntries } from '@/.mercato/generated/notification-handlers.generated'
14
13
  import { registerNotificationHandlers } from '@open-mercato/shared/lib/notifications/handler-registry'
15
- // Side-effect: registers translatable fields for client-side TranslationManager
16
- import '@/.mercato/generated/translations-fields.generated'
17
- // Side-effect: configures message UI component and object type registries on the client.
18
- import '@/.mercato/generated/messages.client.generated'
19
- // Side-effect: registers provider-owned payment renderer widgets on the client.
20
- import '@/.mercato/generated/payments.client.generated'
21
14
 
22
15
  let _clientBootstrapped = false
16
+ let _bootstrapPromise: Promise<void> | null = null
23
17
 
24
- function clientBootstrap() {
18
+ async function clientBootstrap(): Promise<void> {
25
19
  if (_clientBootstrapped) return
26
- _clientBootstrapped = true
20
+ if (_bootstrapPromise) return _bootstrapPromise
21
+
22
+ _bootstrapPromise = (async () => {
23
+ try {
24
+ // Defer generated registry barrels to a dynamic import so each barrel
25
+ // becomes its own lazy chunk in Turbopack. Routes that mount this
26
+ // provider but never use injection/dashboard/notification registries
27
+ // still get the chunks compiled, but no longer during initial page
28
+ // parse — the first paint is no longer blocked on registering every
29
+ // module's client widgets.
30
+ const [
31
+ injectionWidgets,
32
+ injectionTables,
33
+ enabledModuleIds,
34
+ dashboardWidgets,
35
+ notificationHandlers,
36
+ ] = await Promise.all([
37
+ import('@/.mercato/generated/injection-widgets.generated'),
38
+ import('@/.mercato/generated/injection-tables.generated'),
39
+ import('@/.mercato/generated/enabled-module-ids.generated'),
40
+ import('@/.mercato/generated/dashboard-widgets.generated'),
41
+ import('@/.mercato/generated/notification-handlers.generated'),
42
+ ])
27
43
 
28
- // Register injection widgets
29
- registerInjectionWidgets(injectionWidgetEntries)
30
- registerCoreInjectionWidgets(injectionWidgetEntries)
31
- registerCoreInjectionTables(injectionTables)
32
- registerEnabledModuleIds(enabledModuleIds)
44
+ registerInjectionWidgets(injectionWidgets.injectionWidgetEntries)
45
+ registerCoreInjectionWidgets(injectionWidgets.injectionWidgetEntries)
46
+ registerCoreInjectionTables(injectionTables.injectionTables)
47
+ registerEnabledModuleIds(enabledModuleIds.enabledModuleIds)
48
+ registerDashboardWidgets(dashboardWidgets.dashboardWidgetEntries)
49
+ registerNotificationHandlers(notificationHandlers.notificationHandlerEntries)
33
50
 
34
- // Register dashboard widgets
35
- registerDashboardWidgets(dashboardWidgetEntries)
51
+ _clientBootstrapped = true
52
+ } catch (err) {
53
+ // A lazy registry chunk failed to load (e.g. a stale chunk after a
54
+ // deploy). Clear the cached promise so the next render retries instead
55
+ // of leaving every client registry empty forever — otherwise dashboard
56
+ // widget cards would wait on registration indefinitely with no error.
57
+ _bootstrapPromise = null
58
+ console.error('[ClientBootstrap] Failed to register client registries; will retry on next render', err)
59
+ }
60
+ })()
36
61
 
37
- // Register notification handlers for client-side reactive effects
38
- registerNotificationHandlers(notificationHandlerEntries)
62
+ return _bootstrapPromise
39
63
  }
40
64
 
41
65
  export function ClientBootstrapProvider({ children }: { children: React.ReactNode }) {
42
66
  React.useEffect(() => {
43
- clientBootstrap()
67
+ void clientBootstrap()
44
68
  }, [])
45
69
 
46
- // Also bootstrap synchronously on first render for SSR hydration
70
+ // Fire-and-forget on the very first client render so any consumer that
71
+ // reads registries during the same paint as this provider mounts still
72
+ // sees them populated by microtask flush. The promise is cached.
47
73
  if (typeof window !== 'undefined' && !_clientBootstrapped) {
48
- clientBootstrap()
74
+ void clientBootstrap()
49
75
  }
50
76
 
51
77
  return <>{children}</>