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,11 +1,20 @@
|
|
|
1
1
|
import fs from 'node:fs'
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
|
|
4
|
-
// Greenfield must not inherit
|
|
5
|
-
// configured Next.js distDir
|
|
6
|
-
// and
|
|
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
|
|
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:
|
|
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:
|
|
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 :
|
|
1747
|
+
const progressCurrent = splashState.ready ? runtimeReadyProgressCurrent : runtimeWarmupProgressCurrent
|
|
1658
1748
|
return {
|
|
1659
1749
|
type: 'status',
|
|
1660
1750
|
message: timing,
|
package/template/scripts/dev.mjs
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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
|
-
|
|
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,51 +1,77 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
|
-
|
|
5
|
-
//
|
|
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
|
|
8
|
-
import
|
|
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
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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}</>
|