@open-mercato/cli 0.6.4-develop.3929.1.fcf7afece2 → 0.6.4-develop.3944.1.4100aa7fbe
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/.turbo/turbo-build.log +1 -1
- package/dist/lib/generate-watch-structure.js +153 -0
- package/dist/lib/generate-watch-structure.js.map +7 -0
- package/dist/mercato.js +100 -17
- package/dist/mercato.js.map +2 -2
- package/package.json +5 -5
- package/src/lib/__tests__/dev-env-reload.test.ts +1 -1
- package/src/lib/__tests__/generate-watch-structure.test.ts +80 -0
- package/src/lib/generate-watch-structure.ts +170 -0
- package/src/mercato.ts +112 -18
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/cli",
|
|
3
|
-
"version": "0.6.4-develop.
|
|
3
|
+
"version": "0.6.4-develop.3944.1.4100aa7fbe",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -59,8 +59,8 @@
|
|
|
59
59
|
"@mikro-orm/decorators": "^7.1.1",
|
|
60
60
|
"@mikro-orm/migrations": "^7.1.1",
|
|
61
61
|
"@mikro-orm/postgresql": "^7.1.1",
|
|
62
|
-
"@open-mercato/queue": "0.6.4-develop.
|
|
63
|
-
"@open-mercato/shared": "0.6.4-develop.
|
|
62
|
+
"@open-mercato/queue": "0.6.4-develop.3944.1.4100aa7fbe",
|
|
63
|
+
"@open-mercato/shared": "0.6.4-develop.3944.1.4100aa7fbe",
|
|
64
64
|
"cross-spawn": "^7.0.6",
|
|
65
65
|
"pg": "8.21.0",
|
|
66
66
|
"semver": "^7.8.1",
|
|
@@ -70,10 +70,10 @@
|
|
|
70
70
|
"typescript": "^6.0.3"
|
|
71
71
|
},
|
|
72
72
|
"peerDependencies": {
|
|
73
|
-
"@open-mercato/shared": "0.6.4-develop.
|
|
73
|
+
"@open-mercato/shared": "0.6.4-develop.3944.1.4100aa7fbe"
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
|
-
"@open-mercato/shared": "0.6.4-develop.
|
|
76
|
+
"@open-mercato/shared": "0.6.4-develop.3944.1.4100aa7fbe",
|
|
77
77
|
"@types/jest": "^30.0.0",
|
|
78
78
|
"jest": "^30.4.2",
|
|
79
79
|
"ts-jest": "^29.4.11"
|
|
@@ -61,7 +61,7 @@ describe('dev env reload helpers', () => {
|
|
|
61
61
|
expect(environment.REMOVED_LATER).toBeUndefined()
|
|
62
62
|
})
|
|
63
63
|
|
|
64
|
-
it('watches generated runtime files
|
|
64
|
+
it('watches generated runtime files when explicitly requested', async () => {
|
|
65
65
|
const generatedDir = path.join(appDir, '.mercato', 'generated')
|
|
66
66
|
fs.mkdirSync(generatedDir, { recursive: true })
|
|
67
67
|
const generatedFile = path.join(generatedDir, 'backend-routes.generated.ts')
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { calculateGenerateWatchStructureChecksum } from '../generate-watch-structure'
|
|
5
|
+
|
|
6
|
+
function write(filePath: string, content: string): void {
|
|
7
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true })
|
|
8
|
+
fs.writeFileSync(filePath, content)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
describe('calculateGenerateWatchStructureChecksum', () => {
|
|
12
|
+
let root: string
|
|
13
|
+
let appDir: string
|
|
14
|
+
let pkgModule: string
|
|
15
|
+
let appModule: string
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
root = fs.mkdtempSync(path.join(os.tmpdir(), 'om-generate-watch-'))
|
|
19
|
+
appDir = path.join(root, 'apps', 'mercato')
|
|
20
|
+
pkgModule = path.join(root, 'packages', 'core', 'src', 'modules', 'customers')
|
|
21
|
+
appModule = path.join(appDir, 'src', 'modules', 'customers')
|
|
22
|
+
write(path.join(appDir, 'src', 'modules.ts'), 'export const enabledModules = []\n')
|
|
23
|
+
write(path.join(pkgModule, 'index.ts'), 'export const metadata = { id: "customers" }\n')
|
|
24
|
+
write(path.join(pkgModule, 'backend', 'customers', 'people', 'page.tsx'), 'export default function Page() { return null }\n')
|
|
25
|
+
write(path.join(pkgModule, 'components', 'detail', 'PersonDetailTabs.tsx'), 'export function PersonDetailTabs() { return null }\n')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
fs.rmSync(root, { recursive: true, force: true })
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
function currentChecksum(): string {
|
|
33
|
+
return calculateGenerateWatchStructureChecksum({
|
|
34
|
+
modulesFile: path.join(appDir, 'src', 'modules.ts'),
|
|
35
|
+
moduleRoots: [{ appBase: appModule, pkgBase: pkgModule }],
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
it('ignores ordinary component edits outside generator discovery paths', () => {
|
|
40
|
+
const before = currentChecksum()
|
|
41
|
+
|
|
42
|
+
write(path.join(pkgModule, 'components', 'detail', 'PersonDetailTabs.tsx'), 'export function PersonDetailTabs() { return "changed" }\n')
|
|
43
|
+
|
|
44
|
+
expect(currentChecksum()).toBe(before)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('changes when a discovered backend page is added', () => {
|
|
48
|
+
const before = currentChecksum()
|
|
49
|
+
|
|
50
|
+
write(path.join(pkgModule, 'backend', 'customers', 'companies', 'page.tsx'), 'export default function Page() { return null }\n')
|
|
51
|
+
|
|
52
|
+
expect(currentChecksum()).not.toBe(before)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('changes when route metadata changes', () => {
|
|
56
|
+
write(path.join(pkgModule, 'backend', 'customers', 'people', 'page.meta.ts'), 'export const metadata = { nav: { label: "People" } }\n')
|
|
57
|
+
const before = currentChecksum()
|
|
58
|
+
|
|
59
|
+
write(path.join(pkgModule, 'backend', 'customers', 'people', 'page.meta.ts'), 'export const metadata = { nav: { label: "Contacts" } }\n')
|
|
60
|
+
|
|
61
|
+
expect(currentChecksum()).not.toBe(before)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('changes when inline page metadata changes', () => {
|
|
65
|
+
write(path.join(pkgModule, 'backend', 'customers', 'people', 'page.tsx'), 'export const metadata = { nav: { label: "People" } }\nexport default function Page() { return null }\n')
|
|
66
|
+
const before = currentChecksum()
|
|
67
|
+
|
|
68
|
+
write(path.join(pkgModule, 'backend', 'customers', 'people', 'page.tsx'), 'export const metadata = { nav: { label: "Contacts" } }\nexport default function Page() { return null }\n')
|
|
69
|
+
|
|
70
|
+
expect(currentChecksum()).not.toBe(before)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('changes when a convention file changes', () => {
|
|
74
|
+
const before = currentChecksum()
|
|
75
|
+
|
|
76
|
+
write(path.join(pkgModule, 'acl.ts'), 'export const features = [{ id: "customers.view" }]\n')
|
|
77
|
+
|
|
78
|
+
expect(currentChecksum()).not.toBe(before)
|
|
79
|
+
})
|
|
80
|
+
})
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import crypto from 'node:crypto'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import {
|
|
5
|
+
MODULE_CODE_EXTENSIONS,
|
|
6
|
+
SCAN_CONFIGS,
|
|
7
|
+
scanModuleDir,
|
|
8
|
+
stripModuleCodeExtension,
|
|
9
|
+
type ModuleRoots,
|
|
10
|
+
} from './generators/scanner'
|
|
11
|
+
|
|
12
|
+
const STRUCTURAL_CONVENTION_FILES = [
|
|
13
|
+
'index.ts',
|
|
14
|
+
'cli.ts',
|
|
15
|
+
'di.ts',
|
|
16
|
+
'acl.ts',
|
|
17
|
+
'setup.ts',
|
|
18
|
+
'encryption.ts',
|
|
19
|
+
'ce.ts',
|
|
20
|
+
'search.ts',
|
|
21
|
+
'events.ts',
|
|
22
|
+
'notifications.ts',
|
|
23
|
+
'notifications.client.ts',
|
|
24
|
+
'notifications.handlers.ts',
|
|
25
|
+
'translations.ts',
|
|
26
|
+
'generators.ts',
|
|
27
|
+
'ai-tools.ts',
|
|
28
|
+
'ai-agents.ts',
|
|
29
|
+
'analytics.ts',
|
|
30
|
+
'workflows.ts',
|
|
31
|
+
'inbox-actions.ts',
|
|
32
|
+
'message-types.ts',
|
|
33
|
+
'message-objects.ts',
|
|
34
|
+
'integration.ts',
|
|
35
|
+
'security.mfa-providers.ts',
|
|
36
|
+
'security.sudo.ts',
|
|
37
|
+
'data/entities.ts',
|
|
38
|
+
'data/extensions.ts',
|
|
39
|
+
'data/fields.ts',
|
|
40
|
+
'data/enrichers.ts',
|
|
41
|
+
'data/guards.ts',
|
|
42
|
+
'api/interceptors.ts',
|
|
43
|
+
'commands/interceptors.ts',
|
|
44
|
+
'widgets/components.ts',
|
|
45
|
+
'widgets/injection-table.ts',
|
|
46
|
+
'frontend/middleware.ts',
|
|
47
|
+
'backend/middleware.ts',
|
|
48
|
+
] as const
|
|
49
|
+
|
|
50
|
+
const CONTENT_SENSITIVE_SCAN_CONFIGS = [
|
|
51
|
+
SCAN_CONFIGS.apiRoutes,
|
|
52
|
+
SCAN_CONFIGS.apiPlainFiles,
|
|
53
|
+
SCAN_CONFIGS.subscribers,
|
|
54
|
+
SCAN_CONFIGS.workers,
|
|
55
|
+
SCAN_CONFIGS.dashboardWidgets,
|
|
56
|
+
SCAN_CONFIGS.injectionWidgets,
|
|
57
|
+
] as const
|
|
58
|
+
|
|
59
|
+
const ROUTE_SHAPE_SCAN_CONFIGS = [
|
|
60
|
+
SCAN_CONFIGS.frontendPages,
|
|
61
|
+
SCAN_CONFIGS.backendPages,
|
|
62
|
+
] as const
|
|
63
|
+
|
|
64
|
+
function checksum(value: string): string {
|
|
65
|
+
return crypto.createHash('md5').update(value).digest('hex')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function fileRecord(filePath: string, base: string, mode: 'content' | 'shape'): string | null {
|
|
69
|
+
if (!fs.existsSync(filePath)) return null
|
|
70
|
+
let stat: fs.Stats
|
|
71
|
+
try {
|
|
72
|
+
stat = fs.statSync(filePath)
|
|
73
|
+
} catch {
|
|
74
|
+
return null
|
|
75
|
+
}
|
|
76
|
+
if (!stat.isFile()) return null
|
|
77
|
+
const rel = path.relative(base, filePath).replace(/\\/g, '/')
|
|
78
|
+
if (mode === 'shape') {
|
|
79
|
+
return `file:${rel}`
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
return `file:${rel}:${stat.size}:${checksum(fs.readFileSync(filePath, 'utf8'))}`
|
|
83
|
+
} catch {
|
|
84
|
+
return `file:${rel}:unreadable`
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function resolveCodeFile(base: string, relativePath: string): string | null {
|
|
89
|
+
const stripped = stripModuleCodeExtension(relativePath)
|
|
90
|
+
const candidates = MODULE_CODE_EXTENSIONS.map((extension) => `${stripped}${extension}`)
|
|
91
|
+
for (const candidate of candidates) {
|
|
92
|
+
const filePath = path.join(base, ...candidate.split('/'))
|
|
93
|
+
if (fs.existsSync(filePath)) return filePath
|
|
94
|
+
}
|
|
95
|
+
return null
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function hasInlinePageMetadata(filePath: string): boolean {
|
|
99
|
+
try {
|
|
100
|
+
const source = fs.readFileSync(filePath, 'utf8')
|
|
101
|
+
return /\bexport\s+(?:const|let|var|function|class)\s+metadata\b/.test(source)
|
|
102
|
+
|| /\bexport\s+\{[^}]*\bmetadata\b[^}]*\}/.test(source)
|
|
103
|
+
} catch {
|
|
104
|
+
return false
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function addConventionRecords(records: string[], roots: ModuleRoots): void {
|
|
109
|
+
for (const base of [roots.pkgBase, roots.appBase]) {
|
|
110
|
+
records.push(`module-root:${base}:${fs.existsSync(base) ? 'present' : 'missing'}`)
|
|
111
|
+
for (const relativePath of STRUCTURAL_CONVENTION_FILES) {
|
|
112
|
+
const filePath = resolveCodeFile(base, relativePath)
|
|
113
|
+
if (!filePath) {
|
|
114
|
+
records.push(`missing:${base}:${relativePath}`)
|
|
115
|
+
continue
|
|
116
|
+
}
|
|
117
|
+
const record = fileRecord(filePath, base, 'content')
|
|
118
|
+
if (record) records.push(record)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function addScannedRecords(records: string[], roots: ModuleRoots): void {
|
|
124
|
+
for (const config of CONTENT_SENSITIVE_SCAN_CONFIGS) {
|
|
125
|
+
for (const scanned of scanModuleDir(roots, config)) {
|
|
126
|
+
const base = scanned.fromApp ? roots.appBase : roots.pkgBase
|
|
127
|
+
const filePath = path.join(base, ...config.folder.split('/'), ...scanned.relPath.split('/'))
|
|
128
|
+
const record = fileRecord(filePath, base, 'content')
|
|
129
|
+
if (record) records.push(`${config.folder}:${record}`)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
for (const config of ROUTE_SHAPE_SCAN_CONFIGS) {
|
|
134
|
+
for (const scanned of scanModuleDir(roots, config)) {
|
|
135
|
+
const base = scanned.fromApp ? roots.appBase : roots.pkgBase
|
|
136
|
+
const folderPath = path.join(base, ...config.folder.split('/'))
|
|
137
|
+
const filePath = path.join(folderPath, ...scanned.relPath.split('/'))
|
|
138
|
+
const pageRecord = fileRecord(filePath, base, hasInlinePageMetadata(filePath) ? 'content' : 'shape')
|
|
139
|
+
if (pageRecord) records.push(`${config.folder}:${pageRecord}`)
|
|
140
|
+
|
|
141
|
+
const dir = path.dirname(filePath)
|
|
142
|
+
const stem = stripModuleCodeExtension(path.basename(filePath))
|
|
143
|
+
const metaCandidates = stem === 'page'
|
|
144
|
+
? ['page.meta', 'meta']
|
|
145
|
+
: [`${stem}.meta`, 'meta']
|
|
146
|
+
for (const candidate of metaCandidates) {
|
|
147
|
+
const metaPath = resolveCodeFile(dir, candidate)
|
|
148
|
+
if (!metaPath) continue
|
|
149
|
+
const record = fileRecord(metaPath, base, 'content')
|
|
150
|
+
if (record) records.push(`${config.folder}:meta:${record}`)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function calculateGenerateWatchStructureChecksum(options: {
|
|
157
|
+
modulesFile: string
|
|
158
|
+
moduleRoots: ModuleRoots[]
|
|
159
|
+
}): string {
|
|
160
|
+
const records: string[] = []
|
|
161
|
+
const modulesRecord = fileRecord(options.modulesFile, path.dirname(options.modulesFile), 'content')
|
|
162
|
+
records.push(modulesRecord ?? `missing:${options.modulesFile}`)
|
|
163
|
+
|
|
164
|
+
for (const roots of options.moduleRoots) {
|
|
165
|
+
addConventionRecords(records, roots)
|
|
166
|
+
addScannedRecords(records, roots)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return checksum(records.sort((a, b) => a.localeCompare(b)).join('\n'))
|
|
170
|
+
}
|
package/src/mercato.ts
CHANGED
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
import { parseModuleInstallArgs } from './lib/module-install-args'
|
|
33
33
|
import { resolveNextBuildIdCandidate } from './lib/next-build-id'
|
|
34
34
|
import { acquireServerStartLock } from './lib/server-start-lock'
|
|
35
|
-
import { createDevEnvReloader, watchDevEnvFiles
|
|
35
|
+
import { createDevEnvReloader, watchDevEnvFiles } from './lib/dev-env-reload'
|
|
36
36
|
// Lazy-imported to avoid pulling in `testcontainers` (devDependency) at startup
|
|
37
37
|
const lazyIntegration = () => import('./lib/testing/integration')
|
|
38
38
|
import type { ChildProcess } from 'node:child_process'
|
|
@@ -341,6 +341,92 @@ type DevServerRestartResult = {
|
|
|
341
341
|
|
|
342
342
|
type DevServerExitResult = ManagedProcessExitResult | DevServerRestartResult
|
|
343
343
|
|
|
344
|
+
function resolveDevRuntimeBaseUrl(environment: NodeJS.ProcessEnv = process.env): string {
|
|
345
|
+
const configured =
|
|
346
|
+
environment.APP_URL
|
|
347
|
+
?? environment.NEXT_PUBLIC_APP_URL
|
|
348
|
+
?? environment.NEXTAUTH_URL
|
|
349
|
+
if (configured?.trim()) {
|
|
350
|
+
return configured.trim().replace(/\/+$/, '')
|
|
351
|
+
}
|
|
352
|
+
return `http://localhost:${environment.PORT?.trim() || '3000'}`
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function writeDevSplashChildState(state: Record<string, unknown>): void {
|
|
356
|
+
if (process.env.OM_DEV_SPLASH_RUNTIME_WRAPPER === '1') return
|
|
357
|
+
const stateFile = process.env.OM_DEV_SPLASH_CHILD_STATE_FILE
|
|
358
|
+
if (!stateFile?.trim()) return
|
|
359
|
+
|
|
360
|
+
try {
|
|
361
|
+
fs.mkdirSync(path.dirname(stateFile), { recursive: true })
|
|
362
|
+
fs.writeFileSync(stateFile, `${JSON.stringify({
|
|
363
|
+
mode: process.env.OM_DEV_SPLASH_MODE || 'dev',
|
|
364
|
+
failed: false,
|
|
365
|
+
failureLines: [],
|
|
366
|
+
failureCommand: null,
|
|
367
|
+
...state,
|
|
368
|
+
}, null, 2)}\n`)
|
|
369
|
+
} catch {
|
|
370
|
+
// Splash state is best-effort; terminal logs remain authoritative.
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function writeDevSplashRuntimeStarting(detail = 'Starting Next.js dev server'): void {
|
|
375
|
+
writeDevSplashChildState({
|
|
376
|
+
phase: 'Preparing app runtime',
|
|
377
|
+
detail,
|
|
378
|
+
ready: false,
|
|
379
|
+
readyUrl: null,
|
|
380
|
+
loginUrl: null,
|
|
381
|
+
progressLabel: 'Launching app runtime',
|
|
382
|
+
activity: detail,
|
|
383
|
+
})
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function resolveSplashProgressFallback(): { current: number; total: number } {
|
|
387
|
+
const current = Number.parseInt(process.env.OM_DEV_SPLASH_STAGE_CURRENT ?? '', 10)
|
|
388
|
+
const total = Number.parseInt(process.env.OM_DEV_SPLASH_STAGE_TOTAL ?? '', 10)
|
|
389
|
+
if (Number.isFinite(current) && Number.isFinite(total) && total > 0) {
|
|
390
|
+
return { current, total }
|
|
391
|
+
}
|
|
392
|
+
if (process.env.OM_DEV_SPLASH_MODE === 'greenfield' || process.env.OM_DEV_SPLASH_MODE === 'setup') {
|
|
393
|
+
return { current: 5, total: 5 }
|
|
394
|
+
}
|
|
395
|
+
return { current: 3, total: 3 }
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function writeDevSplashRuntimeRestarting(reason: string): void {
|
|
399
|
+
const progress = resolveSplashProgressFallback()
|
|
400
|
+
writeDevSplashChildState({
|
|
401
|
+
phase: 'App runtime is restarting',
|
|
402
|
+
detail: `Reason: ${reason}`,
|
|
403
|
+
ready: false,
|
|
404
|
+
readyUrl: null,
|
|
405
|
+
loginUrl: null,
|
|
406
|
+
progressCurrent: progress.current,
|
|
407
|
+
progressTotal: progress.total,
|
|
408
|
+
progressLabel: 'Restarting app runtime',
|
|
409
|
+
activity: `App runtime restart: ${reason}`,
|
|
410
|
+
})
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function writeDevSplashRuntimeReady(reason?: string): void {
|
|
414
|
+
const readyUrl = resolveDevRuntimeBaseUrl()
|
|
415
|
+
const progress = resolveSplashProgressFallback()
|
|
416
|
+
writeDevSplashChildState({
|
|
417
|
+
phase: 'App is ready',
|
|
418
|
+
detail: reason ? `Restart completed after ${reason}` : 'Next.js dev server is ready',
|
|
419
|
+
ready: true,
|
|
420
|
+
readyUrl,
|
|
421
|
+
loginUrl: `${readyUrl}/login`,
|
|
422
|
+
progressCurrent: progress.current,
|
|
423
|
+
progressTotal: progress.total,
|
|
424
|
+
progressPercent: 100,
|
|
425
|
+
progressLabel: 'App is ready',
|
|
426
|
+
activity: reason ? `Restart completed after ${reason}` : 'App runtime is ready',
|
|
427
|
+
})
|
|
428
|
+
}
|
|
429
|
+
|
|
344
430
|
type ModuleCommandLookupResult =
|
|
345
431
|
| {
|
|
346
432
|
status: 'ok'
|
|
@@ -601,18 +687,17 @@ async function runGeneratorSuite(quiet: boolean): Promise<void> {
|
|
|
601
687
|
function createGenerateWatchChecksumFn(): () => Promise<string> {
|
|
602
688
|
return async () => {
|
|
603
689
|
const { createResolver } = await import('./lib/resolver')
|
|
604
|
-
const {
|
|
690
|
+
const { calculateGenerateWatchStructureChecksum } = await import('./lib/generate-watch-structure')
|
|
605
691
|
const resolver = createResolver()
|
|
606
|
-
const
|
|
607
|
-
path.join(resolver.getAppDir(), 'src', 'modules.ts'),
|
|
608
|
-
path.join(resolver.getAppDir(), 'src', 'modules'),
|
|
609
|
-
])
|
|
692
|
+
const moduleRoots = []
|
|
610
693
|
for (const entry of resolver.loadEnabledModules()) {
|
|
611
694
|
const roots = resolver.getModulePaths(entry)
|
|
612
|
-
|
|
613
|
-
tracked.add(roots.pkgBase)
|
|
695
|
+
moduleRoots.push({ appBase: roots.appBase, pkgBase: roots.pkgBase })
|
|
614
696
|
}
|
|
615
|
-
return
|
|
697
|
+
return calculateGenerateWatchStructureChecksum({
|
|
698
|
+
modulesFile: path.join(resolver.getAppDir(), 'src', 'modules.ts'),
|
|
699
|
+
moduleRoots,
|
|
700
|
+
})
|
|
616
701
|
}
|
|
617
702
|
}
|
|
618
703
|
|
|
@@ -1694,6 +1779,7 @@ export async function run(argv = process.argv) {
|
|
|
1694
1779
|
let activeLazySupervisor: ReturnType<typeof startLazyWorkerSupervisor> | null = null
|
|
1695
1780
|
let activeLazySchedulerSupervisor: ReturnType<typeof startLazySchedulerSupervisor> | null = null
|
|
1696
1781
|
let activeGenerateWatcher: GenerateWatcherHandle | null = null
|
|
1782
|
+
let lastRestartReason: string | null = null
|
|
1697
1783
|
const generateWatcherMode: GenerateWatcherMode = resolveGenerateWatcherMode(process.env)
|
|
1698
1784
|
const envReloader = createDevEnvReloader(appDir, process.env, initialProcessEnvironmentEntries)
|
|
1699
1785
|
|
|
@@ -1787,14 +1873,6 @@ export async function run(argv = process.argv) {
|
|
|
1787
1873
|
filePath,
|
|
1788
1874
|
})
|
|
1789
1875
|
})
|
|
1790
|
-
const stopRuntimeWatcher = watchDevRuntimeFiles(appDir, (filePath) => {
|
|
1791
|
-
devRestartPromiseResolve?.({
|
|
1792
|
-
label: 'Runtime graph change',
|
|
1793
|
-
restart: true,
|
|
1794
|
-
filePath,
|
|
1795
|
-
})
|
|
1796
|
-
})
|
|
1797
|
-
|
|
1798
1876
|
const waitForDevRestart = (): Promise<DevServerRestartResult> =>
|
|
1799
1877
|
new Promise((resolve) => {
|
|
1800
1878
|
devRestartPromiseResolve = resolve
|
|
@@ -1802,6 +1880,11 @@ export async function run(argv = process.argv) {
|
|
|
1802
1880
|
|
|
1803
1881
|
const startNextDev = (runtimeEnv: NodeJS.ProcessEnv): Promise<ManagedProcessExitResult> =>
|
|
1804
1882
|
new Promise((resolve) => {
|
|
1883
|
+
writeDevSplashRuntimeStarting(
|
|
1884
|
+
lastRestartReason
|
|
1885
|
+
? `Restarting Next.js dev server. Reason: ${lastRestartReason}`
|
|
1886
|
+
: 'Starting Next.js dev server',
|
|
1887
|
+
)
|
|
1805
1888
|
const nextProcess = spawn('node', [nextBin, 'dev', '--turbopack'], {
|
|
1806
1889
|
stdio: ['inherit', 'pipe', 'pipe'],
|
|
1807
1890
|
env: runtimeEnv,
|
|
@@ -1810,11 +1893,17 @@ export async function run(argv = process.argv) {
|
|
|
1810
1893
|
processes.push(nextProcess)
|
|
1811
1894
|
|
|
1812
1895
|
let combinedOutput = ''
|
|
1896
|
+
let reportedReady = false
|
|
1813
1897
|
const appendOutput = (chunk: string) => {
|
|
1814
1898
|
combinedOutput += chunk
|
|
1815
1899
|
if (combinedOutput.length > 32_768) {
|
|
1816
1900
|
combinedOutput = combinedOutput.slice(-32_768)
|
|
1817
1901
|
}
|
|
1902
|
+
if (!reportedReady && /\bready in\b/i.test(chunk)) {
|
|
1903
|
+
reportedReady = true
|
|
1904
|
+
writeDevSplashRuntimeReady(lastRestartReason ?? undefined)
|
|
1905
|
+
lastRestartReason = null
|
|
1906
|
+
}
|
|
1818
1907
|
}
|
|
1819
1908
|
|
|
1820
1909
|
nextProcess.stdout?.on('data', (chunk: Buffer | string) => {
|
|
@@ -1831,6 +1920,8 @@ export async function run(argv = process.argv) {
|
|
|
1831
1920
|
nextProcess.on('exit', async (code, signal) => {
|
|
1832
1921
|
if (!didRetryCorruptedTurbopackCache && isTurbopackCacheCorruption(combinedOutput)) {
|
|
1833
1922
|
didRetryCorruptedTurbopackCache = true
|
|
1923
|
+
lastRestartReason = 'corrupted Turbopack dev cache'
|
|
1924
|
+
writeDevSplashRuntimeRestarting(lastRestartReason)
|
|
1834
1925
|
console.log('[server] Detected corrupted Turbopack dev cache. Clearing .mercato/next/dev and restarting Next.js once...')
|
|
1835
1926
|
removeTurbopackDevCache(appDir)
|
|
1836
1927
|
return resolve(await startNextDev(runtimeEnv))
|
|
@@ -1934,6 +2025,10 @@ export async function run(argv = process.argv) {
|
|
|
1934
2025
|
}
|
|
1935
2026
|
|
|
1936
2027
|
const firstExit = await Promise.race(managedExitPromises)
|
|
2028
|
+
if (isDevServerRestartResult(firstExit)) {
|
|
2029
|
+
lastRestartReason = `${firstExit.label.toLowerCase()} (${path.basename(firstExit.filePath)})`
|
|
2030
|
+
writeDevSplashRuntimeRestarting(lastRestartReason)
|
|
2031
|
+
}
|
|
1937
2032
|
await cleanupAndWait()
|
|
1938
2033
|
devRestartPromiseResolve = null
|
|
1939
2034
|
|
|
@@ -1950,7 +2045,6 @@ export async function run(argv = process.argv) {
|
|
|
1950
2045
|
}
|
|
1951
2046
|
} finally {
|
|
1952
2047
|
stopEnvWatcher()
|
|
1953
|
-
stopRuntimeWatcher()
|
|
1954
2048
|
}
|
|
1955
2049
|
},
|
|
1956
2050
|
},
|