@open-mercato/cli 0.6.7-develop.6669.1.40b669666b → 0.6.7-develop.6671.1.4b925c5096

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.
Files changed (66) hide show
  1. package/.turbo/turbo-build.log +2 -1
  2. package/AGENTS.md +1 -1
  3. package/dist/agentic/guides/module-facts.json +296 -53
  4. package/dist/agentic/guides/modules/ai_assistant.md +1 -1
  5. package/dist/agentic/guides/modules/api_docs.md +1 -1
  6. package/dist/agentic/guides/modules/api_keys.md +1 -1
  7. package/dist/agentic/guides/modules/attachments.md +1 -1
  8. package/dist/agentic/guides/modules/audit_logs.md +1 -1
  9. package/dist/agentic/guides/modules/auth.md +1 -1
  10. package/dist/agentic/guides/modules/business_rules.md +1 -1
  11. package/dist/agentic/guides/modules/catalog.md +1 -1
  12. package/dist/agentic/guides/modules/channel_gmail.md +1 -1
  13. package/dist/agentic/guides/modules/channel_imap.md +1 -1
  14. package/dist/agentic/guides/modules/checkout.md +1 -1
  15. package/dist/agentic/guides/modules/communication_channels.md +1 -1
  16. package/dist/agentic/guides/modules/configs.md +1 -1
  17. package/dist/agentic/guides/modules/content.md +1 -1
  18. package/dist/agentic/guides/modules/currencies.md +1 -1
  19. package/dist/agentic/guides/modules/customer_accounts.md +1 -1
  20. package/dist/agentic/guides/modules/customers.md +1 -1
  21. package/dist/agentic/guides/modules/dashboards.md +1 -1
  22. package/dist/agentic/guides/modules/data_sync.md +1 -1
  23. package/dist/agentic/guides/modules/dictionaries.md +1 -1
  24. package/dist/agentic/guides/modules/directory.md +1 -1
  25. package/dist/agentic/guides/modules/entities.md +1 -1
  26. package/dist/agentic/guides/modules/events.md +1 -1
  27. package/dist/agentic/guides/modules/feature_toggles.md +1 -1
  28. package/dist/agentic/guides/modules/gateway_stripe.md +1 -1
  29. package/dist/agentic/guides/modules/generators.md +1 -1
  30. package/dist/agentic/guides/modules/inbox_ops.md +1 -1
  31. package/dist/agentic/guides/modules/integrations.md +1 -1
  32. package/dist/agentic/guides/modules/messages.md +1 -1
  33. package/dist/agentic/guides/modules/notifications.md +1 -1
  34. package/dist/agentic/guides/modules/onboarding.md +1 -1
  35. package/dist/agentic/guides/modules/payment_gateways.md +1 -1
  36. package/dist/agentic/guides/modules/perspectives.md +1 -1
  37. package/dist/agentic/guides/modules/planner.md +1 -1
  38. package/dist/agentic/guides/modules/portal.md +1 -1
  39. package/dist/agentic/guides/modules/progress.md +1 -1
  40. package/dist/agentic/guides/modules/query_index.md +1 -1
  41. package/dist/agentic/guides/modules/record_locks.md +1 -1
  42. package/dist/agentic/guides/modules/resources.md +1 -1
  43. package/dist/agentic/guides/modules/sales.md +4 -2
  44. package/dist/agentic/guides/modules/scheduler.md +1 -1
  45. package/dist/agentic/guides/modules/search.md +1 -1
  46. package/dist/agentic/guides/modules/security.md +1 -1
  47. package/dist/agentic/guides/modules/shipping_carriers.md +1 -1
  48. package/dist/agentic/guides/modules/sso.md +1 -1
  49. package/dist/agentic/guides/modules/staff.md +1 -1
  50. package/dist/agentic/guides/modules/storage_s3.md +1 -1
  51. package/dist/agentic/guides/modules/sync_akeneo.md +1 -1
  52. package/dist/agentic/guides/modules/sync_excel.md +1 -1
  53. package/dist/agentic/guides/modules/system_status_overlays.md +1 -1
  54. package/dist/agentic/guides/modules/translations.md +1 -1
  55. package/dist/agentic/guides/modules/webhooks.md +1 -1
  56. package/dist/agentic/guides/modules/wms.md +77 -0
  57. package/dist/agentic/guides/modules/workflows.md +1 -1
  58. package/dist/lib/post-generate-invalidation.js +2 -2
  59. package/dist/lib/post-generate-invalidation.js.map +2 -2
  60. package/dist/mercato.js +15 -1
  61. package/dist/mercato.js.map +2 -2
  62. package/package.json +5 -5
  63. package/src/__tests__/mercato.test.ts +323 -1
  64. package/src/lib/__tests__/post-generate-invalidation.test.ts +24 -0
  65. package/src/lib/post-generate-invalidation.ts +5 -2
  66. package/src/mercato.ts +31 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/cli",
3
- "version": "0.6.7-develop.6669.1.40b669666b",
3
+ "version": "0.6.7-develop.6671.1.4b925c5096",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -60,8 +60,8 @@
60
60
  "@mikro-orm/decorators": "^7.1.5",
61
61
  "@mikro-orm/migrations": "^7.1.5",
62
62
  "@mikro-orm/postgresql": "^7.1.5",
63
- "@open-mercato/queue": "0.6.7-develop.6669.1.40b669666b",
64
- "@open-mercato/shared": "0.6.7-develop.6669.1.40b669666b",
63
+ "@open-mercato/queue": "0.6.7-develop.6671.1.4b925c5096",
64
+ "@open-mercato/shared": "0.6.7-develop.6671.1.4b925c5096",
65
65
  "cross-spawn": "^7.0.6",
66
66
  "pg": "8.22.0",
67
67
  "semver": "^7.8.5",
@@ -72,10 +72,10 @@
72
72
  "typescript-js": "npm:typescript@6.0.3"
73
73
  },
74
74
  "peerDependencies": {
75
- "@open-mercato/shared": "0.6.7-develop.6669.1.40b669666b"
75
+ "@open-mercato/shared": "0.6.7-develop.6671.1.4b925c5096"
76
76
  },
77
77
  "devDependencies": {
78
- "@open-mercato/shared": "0.6.7-develop.6669.1.40b669666b",
78
+ "@open-mercato/shared": "0.6.7-develop.6671.1.4b925c5096",
79
79
  "@types/jest": "^30.0.0",
80
80
  "jest": "^30.4.2",
81
81
  "ts-jest": "^29.4.11"
@@ -37,7 +37,10 @@ function buildMockChildProcessModule(routeAutoExit: MockChildSpawnRouter) {
37
37
  if (autoExit) {
38
38
  queueMicrotask(() => {
39
39
  if (child.exitCode !== null || child.signalCode !== null) return
40
- if (pathIncludes(spawnargs[1] ?? '', 'next/dist/bin/next') && spawnargs.includes('dev')) {
40
+ // A routed non-zero exit models a crash during startup, so the child must
41
+ // never announce readiness first — that is what the cold-start retry keys on.
42
+ const crashesBeforeReady = typeof autoExit.code === 'number' && autoExit.code !== 0
43
+ if (!crashesBeforeReady && pathIncludes(spawnargs[1] ?? '', 'next/dist/bin/next') && spawnargs.includes('dev')) {
41
44
  child.stdout.emit('data', '✓ Ready in 1ms\n')
42
45
  }
43
46
  queueMicrotask(() => {
@@ -868,6 +871,325 @@ describe('server dev managed process exits', () => {
868
871
  consoleLogSpy.mockRestore()
869
872
  })
870
873
 
874
+ it('retries the Next.js dev server once when it exits before reporting ready', async () => {
875
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation()
876
+ const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation()
877
+ process.env.AUTO_SPAWN_WORKERS = 'false'
878
+
879
+ jest.doMock('node:fs', () => {
880
+ const actual = jest.requireActual('node:fs')
881
+ return {
882
+ ...actual,
883
+ existsSync: jest.fn((candidate: string) =>
884
+ pathIncludes(candidate, 'next/dist/bin/next') || pathIncludes(candidate, '@open-mercato/cli/bin/mercato'),
885
+ ),
886
+ unlinkSync: jest.fn(),
887
+ }
888
+ })
889
+ jest.doMock('../lib/generators', () => ({
890
+ generateModulePackageSources: jest.fn().mockResolvedValue(undefined),
891
+ }))
892
+ jest.doMock('../lib/resolver', () => ({
893
+ resolveEnvironment: () => ({
894
+ appDir: '/tmp/test-app',
895
+ rootDir: '/tmp/test-root',
896
+ }),
897
+ createResolver: () => ({}),
898
+ }))
899
+ jest.doMock('child_process', () => {
900
+ const { EventEmitter } = jest.requireActual('node:events')
901
+ let nextSpawnCount = 0
902
+
903
+ const createChild = (autoExit?: { code: number | null; signal?: NodeJS.Signals | null; ready?: boolean }) => {
904
+ const child = new EventEmitter() as any
905
+ child.stdout = new EventEmitter()
906
+ child.stderr = new EventEmitter()
907
+ child.killed = false
908
+ child.exitCode = null
909
+ child.signalCode = null
910
+ child.kill = jest.fn(() => true)
911
+
912
+ if (autoExit) {
913
+ queueMicrotask(() => {
914
+ if (autoExit.ready) child.stdout.emit('data', '✓ Ready in 1ms\n')
915
+ queueMicrotask(() => {
916
+ child.exitCode = autoExit.code
917
+ child.signalCode = autoExit.signal ?? null
918
+ child.emit('exit', child.exitCode, child.signalCode)
919
+ })
920
+ })
921
+ }
922
+
923
+ return child
924
+ }
925
+
926
+ return {
927
+ spawn: jest.fn((_command: string, args: string[]) => {
928
+ if (pathIncludes(args[0] ?? '', 'next/dist/bin/next')) {
929
+ nextSpawnCount += 1
930
+ return nextSpawnCount === 1
931
+ ? createChild({ code: 1 })
932
+ : createChild({ code: null, signal: 'SIGTERM', ready: true })
933
+ }
934
+ return createChild()
935
+ }),
936
+ }
937
+ })
938
+
939
+ const mercato = await import('../mercato')
940
+ const exitCode = await mercato.run(['node', 'mercato', 'server', 'dev'])
941
+ const { spawn } = await import('child_process')
942
+ const nextSpawns = (spawn as jest.Mock).mock.calls.filter((call) =>
943
+ pathIncludes(call[1]?.[0] ?? '', 'next/dist/bin/next'),
944
+ )
945
+
946
+ expect(exitCode).toBe(0)
947
+ expect(nextSpawns).toHaveLength(2)
948
+ expect(consoleLogSpy).toHaveBeenCalledWith(
949
+ '[server] Next.js dev server exited before becoming ready (exit code 1). Retrying once...',
950
+ )
951
+ expect(consoleErrorSpy).not.toHaveBeenCalled()
952
+
953
+ consoleErrorSpy.mockRestore()
954
+ consoleLogSpy.mockRestore()
955
+ })
956
+
957
+ it('reports the failure when the retried Next.js dev server also exits before reporting ready', async () => {
958
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation()
959
+ const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation()
960
+ process.env.AUTO_SPAWN_WORKERS = 'false'
961
+
962
+ jest.doMock('node:fs', () => {
963
+ const actual = jest.requireActual('node:fs')
964
+ return {
965
+ ...actual,
966
+ existsSync: jest.fn((candidate: string) =>
967
+ pathIncludes(candidate, 'next/dist/bin/next') || pathIncludes(candidate, '@open-mercato/cli/bin/mercato'),
968
+ ),
969
+ unlinkSync: jest.fn(),
970
+ }
971
+ })
972
+ jest.doMock('../lib/generators', () => ({
973
+ generateModulePackageSources: jest.fn().mockResolvedValue(undefined),
974
+ }))
975
+ jest.doMock('../lib/resolver', () => ({
976
+ resolveEnvironment: () => ({
977
+ appDir: '/tmp/test-app',
978
+ rootDir: '/tmp/test-root',
979
+ }),
980
+ createResolver: () => ({}),
981
+ }))
982
+ jest.doMock('child_process', () =>
983
+ buildMockChildProcessModule((args) =>
984
+ pathIncludes(args[0] ?? '', 'next/dist/bin/next') ? { code: 1 } : undefined,
985
+ ),
986
+ )
987
+
988
+ const mercato = await import('../mercato')
989
+ const exitCode = await mercato.run(['node', 'mercato', 'server', 'dev'])
990
+ const { spawn } = await import('child_process')
991
+ const nextSpawns = (spawn as jest.Mock).mock.calls.filter((call) =>
992
+ pathIncludes(call[1]?.[0] ?? '', 'next/dist/bin/next'),
993
+ )
994
+
995
+ expect(exitCode).toBe(1)
996
+ expect(nextSpawns).toHaveLength(2)
997
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
998
+ '💥 Failed: [server] Next.js dev server exited unexpectedly with exit code 1.',
999
+ )
1000
+
1001
+ consoleErrorSpy.mockRestore()
1002
+ consoleLogSpy.mockRestore()
1003
+ })
1004
+
1005
+ it.each([
1006
+ ['Failed to restore task data (corrupted database or bug)'],
1007
+ ['Unable to open static sorted file'],
1008
+ ['TurbopackInternalError'],
1009
+ ])('clears the Turbopack dev cache and restarts once on the lone signature %s', async (signature) => {
1010
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation()
1011
+ const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation()
1012
+ process.env.AUTO_SPAWN_WORKERS = 'false'
1013
+ const rmSync = jest.fn()
1014
+
1015
+ jest.doMock('node:fs', () => {
1016
+ const actual = jest.requireActual('node:fs')
1017
+ return {
1018
+ ...actual,
1019
+ existsSync: jest.fn((candidate: string) =>
1020
+ pathIncludes(candidate, 'next/dist/bin/next') || pathIncludes(candidate, '@open-mercato/cli/bin/mercato'),
1021
+ ),
1022
+ unlinkSync: jest.fn(),
1023
+ rmSync,
1024
+ }
1025
+ })
1026
+ jest.doMock('../lib/generators', () => ({
1027
+ generateModulePackageSources: jest.fn().mockResolvedValue(undefined),
1028
+ }))
1029
+ jest.doMock('../lib/resolver', () => ({
1030
+ resolveEnvironment: () => ({
1031
+ appDir: '/tmp/test-app',
1032
+ rootDir: '/tmp/test-root',
1033
+ }),
1034
+ createResolver: () => ({}),
1035
+ }))
1036
+ jest.doMock('child_process', () => {
1037
+ const { EventEmitter } = jest.requireActual('node:events')
1038
+ let nextSpawnCount = 0
1039
+
1040
+ const createChild = (options?: { output?: string; code?: number | null; signal?: NodeJS.Signals | null; ready?: boolean }) => {
1041
+ const child = new EventEmitter() as any
1042
+ child.stdout = new EventEmitter()
1043
+ child.stderr = new EventEmitter()
1044
+ child.killed = false
1045
+ child.exitCode = null
1046
+ child.signalCode = null
1047
+ child.kill = jest.fn(() => true)
1048
+
1049
+ if (options) {
1050
+ queueMicrotask(() => {
1051
+ if (options.output) child.stderr.emit('data', options.output)
1052
+ if (options.ready) child.stdout.emit('data', '✓ Ready in 1ms\n')
1053
+ queueMicrotask(() => {
1054
+ child.exitCode = options.code ?? null
1055
+ child.signalCode = options.signal ?? null
1056
+ child.emit('exit', child.exitCode, child.signalCode)
1057
+ })
1058
+ })
1059
+ }
1060
+
1061
+ return child
1062
+ }
1063
+
1064
+ return {
1065
+ spawn: jest.fn((_command: string, args: string[]) => {
1066
+ if (pathIncludes(args[0] ?? '', 'next/dist/bin/next')) {
1067
+ nextSpawnCount += 1
1068
+ return nextSpawnCount === 1
1069
+ ? createChild({ output: `⨯ ${signature}\n`, code: 1 })
1070
+ : createChild({ code: null, signal: 'SIGTERM', ready: true })
1071
+ }
1072
+ return createChild()
1073
+ }),
1074
+ }
1075
+ })
1076
+
1077
+ const mercato = await import('../mercato')
1078
+ const exitCode = await mercato.run(['node', 'mercato', 'server', 'dev'])
1079
+ const { spawn } = await import('child_process')
1080
+ const nextSpawns = (spawn as jest.Mock).mock.calls.filter((call) =>
1081
+ pathIncludes(call[1]?.[0] ?? '', 'next/dist/bin/next'),
1082
+ )
1083
+ const cacheRemovals = rmSync.mock.calls.filter(([target]) =>
1084
+ pathIncludes(String(target), '.mercato/next/dev'),
1085
+ )
1086
+
1087
+ expect(exitCode).toBe(0)
1088
+ expect(nextSpawns).toHaveLength(2)
1089
+ expect(cacheRemovals).toHaveLength(1)
1090
+ expect(cacheRemovals[0][1]).toEqual({ recursive: true, force: true })
1091
+ expect(consoleLogSpy).toHaveBeenCalledWith(
1092
+ '[server] Detected corrupted Turbopack dev cache. Clearing .mercato/next/dev and restarting Next.js once...',
1093
+ )
1094
+ // Corruption recovery owns this exit; the generic cold-start retry must not also fire.
1095
+ expect(consoleLogSpy).not.toHaveBeenCalledWith(
1096
+ expect.stringContaining('exited before becoming ready'),
1097
+ )
1098
+
1099
+ consoleErrorSpy.mockRestore()
1100
+ consoleLogSpy.mockRestore()
1101
+ })
1102
+
1103
+ it('takes the plain cold-start retry without purging the cache when no corruption signature is present', async () => {
1104
+ const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation()
1105
+ const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation()
1106
+ process.env.AUTO_SPAWN_WORKERS = 'false'
1107
+ const rmSync = jest.fn()
1108
+
1109
+ jest.doMock('node:fs', () => {
1110
+ const actual = jest.requireActual('node:fs')
1111
+ return {
1112
+ ...actual,
1113
+ existsSync: jest.fn((candidate: string) =>
1114
+ pathIncludes(candidate, 'next/dist/bin/next') || pathIncludes(candidate, '@open-mercato/cli/bin/mercato'),
1115
+ ),
1116
+ unlinkSync: jest.fn(),
1117
+ rmSync,
1118
+ }
1119
+ })
1120
+ jest.doMock('../lib/generators', () => ({
1121
+ generateModulePackageSources: jest.fn().mockResolvedValue(undefined),
1122
+ }))
1123
+ jest.doMock('../lib/resolver', () => ({
1124
+ resolveEnvironment: () => ({
1125
+ appDir: '/tmp/test-app',
1126
+ rootDir: '/tmp/test-root',
1127
+ }),
1128
+ createResolver: () => ({}),
1129
+ }))
1130
+ jest.doMock('child_process', () => {
1131
+ const { EventEmitter } = jest.requireActual('node:events')
1132
+ let nextSpawnCount = 0
1133
+
1134
+ const createChild = (options?: { output?: string; code?: number | null; signal?: NodeJS.Signals | null; ready?: boolean }) => {
1135
+ const child = new EventEmitter() as any
1136
+ child.stdout = new EventEmitter()
1137
+ child.stderr = new EventEmitter()
1138
+ child.killed = false
1139
+ child.exitCode = null
1140
+ child.signalCode = null
1141
+ child.kill = jest.fn(() => true)
1142
+
1143
+ if (options) {
1144
+ queueMicrotask(() => {
1145
+ if (options.output) child.stderr.emit('data', options.output)
1146
+ if (options.ready) child.stdout.emit('data', '✓ Ready in 1ms\n')
1147
+ queueMicrotask(() => {
1148
+ child.exitCode = options.code ?? null
1149
+ child.signalCode = options.signal ?? null
1150
+ child.emit('exit', child.exitCode, child.signalCode)
1151
+ })
1152
+ })
1153
+ }
1154
+
1155
+ return child
1156
+ }
1157
+
1158
+ return {
1159
+ spawn: jest.fn((_command: string, args: string[]) => {
1160
+ if (pathIncludes(args[0] ?? '', 'next/dist/bin/next')) {
1161
+ nextSpawnCount += 1
1162
+ return nextSpawnCount === 1
1163
+ ? createChild({ output: 'Another next dev server is already running.\n', code: 1 })
1164
+ : createChild({ code: null, signal: 'SIGTERM', ready: true })
1165
+ }
1166
+ return createChild()
1167
+ }),
1168
+ }
1169
+ })
1170
+
1171
+ const mercato = await import('../mercato')
1172
+ const exitCode = await mercato.run(['node', 'mercato', 'server', 'dev'])
1173
+ const { spawn } = await import('child_process')
1174
+ const nextSpawns = (spawn as jest.Mock).mock.calls.filter((call) =>
1175
+ pathIncludes(call[1]?.[0] ?? '', 'next/dist/bin/next'),
1176
+ )
1177
+ const cacheRemovals = rmSync.mock.calls.filter(([target]) =>
1178
+ pathIncludes(String(target), '.mercato/next/dev'),
1179
+ )
1180
+
1181
+ expect(exitCode).toBe(0)
1182
+ expect(nextSpawns).toHaveLength(2)
1183
+ // A lock conflict is not cache corruption — the dev cache must survive.
1184
+ expect(cacheRemovals).toHaveLength(0)
1185
+ expect(consoleLogSpy).toHaveBeenCalledWith(
1186
+ '[server] Next.js dev server exited before becoming ready (exit code 1). Retrying once...',
1187
+ )
1188
+
1189
+ consoleErrorSpy.mockRestore()
1190
+ consoleLogSpy.mockRestore()
1191
+ })
1192
+
871
1193
  it('starts the lazy worker supervisor instead of `queue worker --all` when OM_AUTO_SPAWN_WORKERS_LAZY=true', async () => {
872
1194
  const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation()
873
1195
  const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation()
@@ -47,6 +47,30 @@ describe('runPostGenerateStructuralInvalidation', () => {
47
47
  }
48
48
  })
49
49
 
50
+ it('advances mtime without reopening generated artifacts for writing', async () => {
51
+ const appDir = fs.mkdtempSync(path.join(os.tmpdir(), 'post-generate-invalidation-mtime-'))
52
+ try {
53
+ const generatedDir = path.join(appDir, '.mercato', 'generated')
54
+ fs.mkdirSync(generatedDir, { recursive: true })
55
+ const generatedTs = path.join(generatedDir, 'modules.generated.ts')
56
+ fs.writeFileSync(generatedTs, 'export const modules = []\n')
57
+ const staleTime = new Date(Date.now() - 60_000)
58
+ fs.utimesSync(generatedTs, staleTime, staleTime)
59
+ const before = fs.statSync(generatedTs)
60
+
61
+ await runPostGenerateStructuralInvalidation(appDir)
62
+
63
+ const after = fs.statSync(generatedTs)
64
+ expect(after.mtimeMs).toBeGreaterThan(before.mtimeMs)
65
+ expect(after.size).toBe(before.size)
66
+ // ctime advances on a metadata-only touch; a rewrite would also bump the
67
+ // inode's write generation, so assert the content survived byte for byte.
68
+ expect(fs.readFileSync(generatedTs, 'utf8')).toBe('export const modules = []\n')
69
+ } finally {
70
+ fs.rmSync(appDir, { recursive: true, force: true })
71
+ }
72
+ })
73
+
50
74
  it('still refreshes generated artifacts when cache maintenance fails', async () => {
51
75
  const appDir = fs.mkdtempSync(path.join(os.tmpdir(), 'post-generate-invalidation-failure-'))
52
76
  const error = new Error('cache unavailable')
@@ -21,13 +21,16 @@ function touchGeneratedFiles(appDir: string): string[] {
21
21
  if (!fs.existsSync(generatedDir) || !fs.statSync(generatedDir).isDirectory()) return []
22
22
 
23
23
  const touched: string[] = []
24
+ // `utimes` advances mtime without opening the file for writing. Rewriting the
25
+ // identical bytes would truncate multi-megabyte registries first, and Turbopack
26
+ // reads them concurrently during a dev compile — it can observe the empty window.
27
+ const touchedAt = new Date()
24
28
  const entries = fs.readdirSync(generatedDir, { withFileTypes: true })
25
29
  .sort((left, right) => left.name.localeCompare(right.name))
26
30
  for (const entry of entries) {
27
31
  if (!entry.isFile() || !TOUCHABLE_GENERATED_FILE_PATTERN.test(entry.name)) continue
28
32
  const filePath = path.join(generatedDir, entry.name)
29
- const contents = fs.readFileSync(filePath)
30
- fs.writeFileSync(filePath, contents)
33
+ fs.utimesSync(filePath, touchedAt, touchedAt)
31
34
  touched.push(filePath)
32
35
  }
33
36
  return touched
package/src/mercato.ts CHANGED
@@ -100,6 +100,11 @@ const TURBOPACK_CORRUPTION_PATTERNS = [
100
100
  'TurbopackInternalError',
101
101
  ]
102
102
 
103
+ // Next.js gives up on `.mercato/next/dev/lock` after 1000ms, so a dev server
104
+ // that lost a startup race against a still-shutting-down sibling needs a
105
+ // slightly longer pause before the retry can win the lock.
106
+ const DEV_COLD_START_RETRY_DELAY_MS = 1500
107
+
103
108
  const BUILTIN_CLI_MODULE_IDS = new Set(['queue', 'generate', 'deploy', 'db', 'server', 'test'])
104
109
 
105
110
  function collectNestedErrors(error: unknown, seen = new Set<unknown>()): ErrorWithCause[] {
@@ -264,7 +269,7 @@ async function ensureDatabaseExists(dbUrl: string): Promise<boolean> {
264
269
  }
265
270
 
266
271
  function isTurbopackCacheCorruption(output: string): boolean {
267
- return TURBOPACK_CORRUPTION_PATTERNS.every((pattern) => output.includes(pattern))
272
+ return TURBOPACK_CORRUPTION_PATTERNS.some((pattern) => output.includes(pattern))
268
273
  }
269
274
 
270
275
  function removeTurbopackDevCache(appDir: string): void {
@@ -1960,6 +1965,7 @@ export async function run(argv = process.argv) {
1960
1965
 
1961
1966
  let processes: ChildProcess[] = []
1962
1967
  let didRetryCorruptedTurbopackCache = false
1968
+ let didRetryFailedColdStart = false
1963
1969
  let stopping = false
1964
1970
  let devRestartPromiseResolve: ((result: DevServerRestartResult) => void) | null = null
1965
1971
  let activeLazySupervisor: ReturnType<typeof startLazyWorkerSupervisor> | null = null
@@ -2122,6 +2128,30 @@ export async function run(argv = process.argv) {
2122
2128
  restarted.readyPromise.then(readyResolve)
2123
2129
  return resolve(await restarted.exitPromise)
2124
2130
  }
2131
+ // A dev server that dies before it ever reported "ready in" lost a
2132
+ // startup race rather than crashing on user code: a sibling instance
2133
+ // still holding the dev lock, a port that is about to free up, or a
2134
+ // bundler init hiccup on a cold cache. Those all clear on their own,
2135
+ // which is why a second `yarn dev` usually succeeds — retry once here
2136
+ // so the first run succeeds too.
2137
+ if (
2138
+ !didRetryFailedColdStart
2139
+ && !reportedReady
2140
+ && !stopping
2141
+ && typeof code === 'number'
2142
+ && code !== 0
2143
+ ) {
2144
+ didRetryFailedColdStart = true
2145
+ lastRestartReason = 'a failed cold start'
2146
+ writeDevSplashRuntimeRestarting(lastRestartReason)
2147
+ console.log(`[server] Next.js dev server exited before becoming ready (exit code ${code}). Retrying once...`)
2148
+ await new Promise((wake) => setTimeout(wake, DEV_COLD_START_RETRY_DELAY_MS))
2149
+ if (!stopping) {
2150
+ const restarted = startNextDev(runtimeEnv)
2151
+ restarted.readyPromise.then(readyResolve)
2152
+ return resolve(await restarted.exitPromise)
2153
+ }
2154
+ }
2125
2155
  resolve({
2126
2156
  label: 'Next.js dev server',
2127
2157
  code,