create-mercato-app 0.6.6-develop.6317.1.b3be10ab84 → 0.6.6-develop.6331.1.a33b8e99b7
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
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"dev": "node ./scripts/dev.mjs",
|
|
13
13
|
"dev:classic": "node ./scripts/dev.mjs --classic",
|
|
14
14
|
"dev:verbose": "node ./scripts/dev.mjs --verbose",
|
|
15
|
+
"dev:fix-wsl-watchers": "node ./scripts/fix-wsl-inotify.mjs",
|
|
15
16
|
"dev:reset": "node ./scripts/dev-reset.mjs",
|
|
16
17
|
"build": "yarn generate && cross-env NODE_OPTIONS=--max-old-space-size=8192 next build",
|
|
17
18
|
"start": "mercato server start",
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import { spawnSync as defaultSpawnSync } from 'node:child_process'
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_INOTIFY_LIMITS = {
|
|
5
|
+
max_user_watches: 4194304,
|
|
6
|
+
max_user_instances: 4096,
|
|
7
|
+
max_queued_events: 65536,
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const INOTIFY_PROC_DIR = '/proc/sys/fs/inotify'
|
|
11
|
+
const WSL_MARKER_PATHS = [
|
|
12
|
+
'/proc/sys/kernel/osrelease',
|
|
13
|
+
'/proc/version',
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
export const VSCODE_WATCHER_EXCLUDES = {
|
|
17
|
+
'**/.git/**': true,
|
|
18
|
+
'**/.turbo/**': true,
|
|
19
|
+
'**/dist/**': true,
|
|
20
|
+
'**/node_modules/**': true,
|
|
21
|
+
'.ai/tmp/**': true,
|
|
22
|
+
'.claude/**': true,
|
|
23
|
+
'.mercato/**': true,
|
|
24
|
+
'apps/mercato/.mercato/**': true,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parseInteger(value) {
|
|
28
|
+
const parsed = Number.parseInt(String(value ?? '').trim(), 10)
|
|
29
|
+
return Number.isInteger(parsed) ? parsed : null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function detectWsl({
|
|
33
|
+
platform = process.platform,
|
|
34
|
+
readFileSync = fs.readFileSync,
|
|
35
|
+
} = {}) {
|
|
36
|
+
if (platform !== 'linux') return false
|
|
37
|
+
|
|
38
|
+
for (const markerPath of WSL_MARKER_PATHS) {
|
|
39
|
+
try {
|
|
40
|
+
const marker = String(readFileSync(markerPath, 'utf8')).toLowerCase()
|
|
41
|
+
if (marker.includes('microsoft') || marker.includes('wsl')) {
|
|
42
|
+
return true
|
|
43
|
+
}
|
|
44
|
+
} catch {
|
|
45
|
+
// Non-WSL Linux images may not expose both marker files.
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return false
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function readCurrentInotifyLimits({
|
|
53
|
+
readFileSync = fs.readFileSync,
|
|
54
|
+
} = {}) {
|
|
55
|
+
const values = {}
|
|
56
|
+
const errors = {}
|
|
57
|
+
|
|
58
|
+
for (const name of Object.keys(DEFAULT_INOTIFY_LIMITS)) {
|
|
59
|
+
const filePath = `${INOTIFY_PROC_DIR}/${name}`
|
|
60
|
+
try {
|
|
61
|
+
const value = parseInteger(readFileSync(filePath, 'utf8'))
|
|
62
|
+
if (value === null) {
|
|
63
|
+
errors[name] = `invalid value in ${filePath}`
|
|
64
|
+
} else {
|
|
65
|
+
values[name] = value
|
|
66
|
+
}
|
|
67
|
+
} catch (error) {
|
|
68
|
+
errors[name] = error?.message ?? String(error)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return { values, errors }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function findInotifyLimitIssues(current, required = DEFAULT_INOTIFY_LIMITS) {
|
|
76
|
+
const issues = []
|
|
77
|
+
|
|
78
|
+
for (const [name, minimum] of Object.entries(required)) {
|
|
79
|
+
const currentValue = current?.[name]
|
|
80
|
+
if (typeof currentValue !== 'number') continue
|
|
81
|
+
if (currentValue < minimum) {
|
|
82
|
+
issues.push({ name, current: currentValue, required: minimum })
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return issues
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function buildSysctlAssignments(
|
|
90
|
+
issues,
|
|
91
|
+
required = DEFAULT_INOTIFY_LIMITS,
|
|
92
|
+
) {
|
|
93
|
+
return issues.map((issue) => `fs.inotify.${issue.name}=${required[issue.name]}`)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function buildPersistentSysctlConfig(required = DEFAULT_INOTIFY_LIMITS) {
|
|
97
|
+
return [
|
|
98
|
+
'# Open Mercato dev server file-watch limits',
|
|
99
|
+
...Object.entries(required).map(([name, value]) => `fs.inotify.${name}=${value}`),
|
|
100
|
+
'',
|
|
101
|
+
].join('\n')
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function buildManualInotifyFix({ required = DEFAULT_INOTIFY_LIMITS } = {}) {
|
|
105
|
+
const assignments = Object.entries(required)
|
|
106
|
+
.map(([name, value]) => `fs.inotify.${name}=${value}`)
|
|
107
|
+
.join(' ')
|
|
108
|
+
const config = buildPersistentSysctlConfig(required).replace(/\n/g, '\\n')
|
|
109
|
+
|
|
110
|
+
return [
|
|
111
|
+
`sudo sysctl -w ${assignments}`,
|
|
112
|
+
`printf '${config}' | sudo tee /etc/sysctl.d/99-open-mercato-inotify.conf >/dev/null`,
|
|
113
|
+
'sudo sysctl --system',
|
|
114
|
+
]
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function mergeVsCodeWatcherExcludes(settings, excludes = VSCODE_WATCHER_EXCLUDES) {
|
|
118
|
+
const next = {
|
|
119
|
+
...(settings && typeof settings === 'object' && !Array.isArray(settings) ? settings : {}),
|
|
120
|
+
}
|
|
121
|
+
let changed = false
|
|
122
|
+
|
|
123
|
+
for (const key of ['files.watcherExclude', 'search.exclude']) {
|
|
124
|
+
const current = next[key] && typeof next[key] === 'object' && !Array.isArray(next[key])
|
|
125
|
+
? next[key]
|
|
126
|
+
: {}
|
|
127
|
+
const merged = { ...current }
|
|
128
|
+
|
|
129
|
+
for (const [pattern, value] of Object.entries(excludes)) {
|
|
130
|
+
if (merged[pattern] !== value) {
|
|
131
|
+
merged[pattern] = value
|
|
132
|
+
changed = true
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (next[key] !== merged) {
|
|
137
|
+
next[key] = merged
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return { settings: next, changed }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function resolveSysctlCommand({ getuid = process.getuid?.bind(process), nonInteractive = true } = {}) {
|
|
145
|
+
if (typeof getuid === 'function' && getuid() === 0) {
|
|
146
|
+
return { command: 'sysctl', args: ['-w'] }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
command: 'sudo',
|
|
151
|
+
args: [nonInteractive ? '-n' : null, 'sysctl', '-w'].filter(Boolean),
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function applyInotifyLimits({
|
|
156
|
+
issues,
|
|
157
|
+
required = DEFAULT_INOTIFY_LIMITS,
|
|
158
|
+
spawnSync = defaultSpawnSync,
|
|
159
|
+
getuid = process.getuid?.bind(process),
|
|
160
|
+
nonInteractive = true,
|
|
161
|
+
stdio = 'pipe',
|
|
162
|
+
} = {}) {
|
|
163
|
+
if (!Array.isArray(issues) || issues.length === 0) {
|
|
164
|
+
return { ok: true, skipped: true }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const { command, args } = resolveSysctlCommand({ getuid, nonInteractive })
|
|
168
|
+
const assignments = buildSysctlAssignments(issues, required)
|
|
169
|
+
const result = spawnSync(command, [...args, ...assignments], {
|
|
170
|
+
encoding: 'utf8',
|
|
171
|
+
stdio,
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
ok: result.status === 0,
|
|
176
|
+
command,
|
|
177
|
+
args: [...args, ...assignments],
|
|
178
|
+
status: result.status,
|
|
179
|
+
error: result.error,
|
|
180
|
+
stderr: result.stderr,
|
|
181
|
+
stdout: result.stdout,
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function formatInotifyLimitFailure({
|
|
186
|
+
current,
|
|
187
|
+
issues,
|
|
188
|
+
manualCommands = buildManualInotifyFix(),
|
|
189
|
+
isWsl = false,
|
|
190
|
+
} = {}) {
|
|
191
|
+
const location = isWsl ? 'WSL2/Linux' : 'Linux'
|
|
192
|
+
const issueLines = issues.map((issue) =>
|
|
193
|
+
` fs.inotify.${issue.name}: ${issue.current} < ${issue.required}`,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
return [
|
|
197
|
+
`❌ ${location} file-watch limits are too low for Turbopack.`,
|
|
198
|
+
'Turbopack will panic with "OS file watch limit reached" unless these sysctl values are raised.',
|
|
199
|
+
'',
|
|
200
|
+
...issueLines,
|
|
201
|
+
'',
|
|
202
|
+
'Run this once in your WSL/Linux terminal:',
|
|
203
|
+
...manualCommands.map((line) => ` ${line}`),
|
|
204
|
+
'',
|
|
205
|
+
`Current values: ${JSON.stringify(current)}`,
|
|
206
|
+
].join('\n')
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function ensureDevInotifyLimits({
|
|
210
|
+
env = process.env,
|
|
211
|
+
platform = process.platform,
|
|
212
|
+
readFileSync = fs.readFileSync,
|
|
213
|
+
spawnSync = defaultSpawnSync,
|
|
214
|
+
getuid = process.getuid?.bind(process),
|
|
215
|
+
required = DEFAULT_INOTIFY_LIMITS,
|
|
216
|
+
} = {}) {
|
|
217
|
+
const rawMode = String(env.OM_DEV_INOTIFY_CHECK ?? '').trim().toLowerCase()
|
|
218
|
+
if (['0', 'false', 'off', 'skip'].includes(rawMode)) {
|
|
219
|
+
return { ok: true, skipped: true, reason: 'disabled' }
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (platform !== 'linux') {
|
|
223
|
+
return { ok: true, skipped: true, reason: 'non-linux' }
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const isWsl = detectWsl({ platform, readFileSync })
|
|
227
|
+
const { values, errors } = readCurrentInotifyLimits({ readFileSync })
|
|
228
|
+
const issues = findInotifyLimitIssues(values, required)
|
|
229
|
+
|
|
230
|
+
if (issues.length === 0) {
|
|
231
|
+
return { ok: true, current: values, isWsl }
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const applyResult = applyInotifyLimits({
|
|
235
|
+
issues,
|
|
236
|
+
required,
|
|
237
|
+
spawnSync,
|
|
238
|
+
getuid,
|
|
239
|
+
nonInteractive: true,
|
|
240
|
+
stdio: 'pipe',
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
if (applyResult.ok) {
|
|
244
|
+
return {
|
|
245
|
+
ok: true,
|
|
246
|
+
fixed: true,
|
|
247
|
+
current: values,
|
|
248
|
+
issues,
|
|
249
|
+
isWsl,
|
|
250
|
+
applyResult,
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const manualCommands = buildManualInotifyFix({ required })
|
|
255
|
+
return {
|
|
256
|
+
ok: false,
|
|
257
|
+
current: values,
|
|
258
|
+
errors,
|
|
259
|
+
issues,
|
|
260
|
+
isWsl,
|
|
261
|
+
applyResult,
|
|
262
|
+
manualCommands,
|
|
263
|
+
message: formatInotifyLimitFailure({
|
|
264
|
+
current: values,
|
|
265
|
+
issues,
|
|
266
|
+
manualCommands,
|
|
267
|
+
isWsl,
|
|
268
|
+
}),
|
|
269
|
+
}
|
|
270
|
+
}
|
package/template/scripts/dev.mjs
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
stripAnsi,
|
|
24
24
|
} from './dev-splash-helpers.mjs'
|
|
25
25
|
import { purgeAppBuildCaches } from './dev-cache-purge.mjs'
|
|
26
|
+
import { ensureDevInotifyLimits } from './dev-inotify-limits.mjs'
|
|
26
27
|
import { killProcessTree } from './dev-shutdown-utils.mjs'
|
|
27
28
|
import { resolveSpawnCommand } from './dev-spawn-utils.mjs'
|
|
28
29
|
import { createDevSplashCodingFlow } from './dev-splash-coding-flow.mjs'
|
|
@@ -317,6 +318,32 @@ function printDevLogLocation() {
|
|
|
317
318
|
process.env.OM_DEV_LOG_ANNOUNCED = '1'
|
|
318
319
|
}
|
|
319
320
|
|
|
321
|
+
function ensureDevFileWatchLimits() {
|
|
322
|
+
const result = ensureDevInotifyLimits()
|
|
323
|
+
if (result.fixed) {
|
|
324
|
+
console.log('🔧 Raised Linux inotify file-watch limits for Turbopack')
|
|
325
|
+
return true
|
|
326
|
+
}
|
|
327
|
+
if (result.ok) {
|
|
328
|
+
return true
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
updateSplashState({
|
|
332
|
+
phase: 'File-watch limits too low',
|
|
333
|
+
detail: 'Raise Linux inotify limits before starting Turbopack',
|
|
334
|
+
failed: true,
|
|
335
|
+
failureLines: result.message.split('\n').filter(Boolean).slice(0, 10),
|
|
336
|
+
failureCommand: 'yarn dev:fix-wsl-watchers',
|
|
337
|
+
ready: false,
|
|
338
|
+
readyUrl: null,
|
|
339
|
+
loginUrl: null,
|
|
340
|
+
activity: 'File-watch limit preflight failed',
|
|
341
|
+
})
|
|
342
|
+
console.error(result.message)
|
|
343
|
+
shutdown(1)
|
|
344
|
+
return false
|
|
345
|
+
}
|
|
346
|
+
|
|
320
347
|
function spawnCommand(command, commandArgs, options = {}) {
|
|
321
348
|
const resolvedSpawn = resolveSpawnCommand(command, commandArgs)
|
|
322
349
|
const teeRequested = options.mirrorOutput === true
|
|
@@ -1756,6 +1783,7 @@ async function runClassicStandaloneDev() {
|
|
|
1756
1783
|
async function main() {
|
|
1757
1784
|
printDevLogLocation()
|
|
1758
1785
|
await startSplashServer()
|
|
1786
|
+
if (!ensureDevFileWatchLimits()) return
|
|
1759
1787
|
|
|
1760
1788
|
if (!isMonorepo) {
|
|
1761
1789
|
if (setupMode) {
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from 'node:child_process'
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import readline from 'node:readline/promises'
|
|
6
|
+
import {
|
|
7
|
+
DEFAULT_INOTIFY_LIMITS,
|
|
8
|
+
buildPersistentSysctlConfig,
|
|
9
|
+
buildSysctlAssignments,
|
|
10
|
+
buildManualInotifyFix,
|
|
11
|
+
detectWsl,
|
|
12
|
+
findInotifyLimitIssues,
|
|
13
|
+
mergeVsCodeWatcherExcludes,
|
|
14
|
+
readCurrentInotifyLimits,
|
|
15
|
+
} from './dev-inotify-limits.mjs'
|
|
16
|
+
|
|
17
|
+
const PERSIST_PATH = '/etc/sysctl.d/99-open-mercato-inotify.conf'
|
|
18
|
+
const noPersist = process.argv.includes('--no-persist')
|
|
19
|
+
const assumeYes = process.argv.includes('--yes') || process.argv.includes('-y')
|
|
20
|
+
const applyVsCode = process.argv.includes('--vscode')
|
|
21
|
+
const skipVsCode = process.argv.includes('--no-vscode')
|
|
22
|
+
|
|
23
|
+
async function confirm(question, { defaultYes = false } = {}) {
|
|
24
|
+
if (assumeYes) return true
|
|
25
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) return false
|
|
26
|
+
|
|
27
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
|
|
28
|
+
try {
|
|
29
|
+
const suffix = defaultYes ? ' [Y/n] ' : ' [y/N] '
|
|
30
|
+
const answer = (await rl.question(`${question}${suffix}`)).trim().toLowerCase()
|
|
31
|
+
if (!answer) return defaultYes
|
|
32
|
+
return answer === 'y' || answer === 'yes'
|
|
33
|
+
} finally {
|
|
34
|
+
rl.close()
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function run(command, args, options = {}) {
|
|
39
|
+
const result = spawnSync(command, args, {
|
|
40
|
+
stdio: options.input ? ['pipe', 'inherit', 'inherit'] : 'inherit',
|
|
41
|
+
input: options.input,
|
|
42
|
+
encoding: 'utf8',
|
|
43
|
+
})
|
|
44
|
+
if (result.status !== 0) {
|
|
45
|
+
process.exit(result.status ?? 1)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function maybeUpdateVsCodeSettings() {
|
|
50
|
+
if (skipVsCode) return
|
|
51
|
+
|
|
52
|
+
const shouldApply = applyVsCode || (!assumeYes && await confirm(
|
|
53
|
+
'Optionally update .vscode/settings.json to exclude generated/cache/worktree folders from VS Code file watching? This modifies a tracked workspace file.',
|
|
54
|
+
))
|
|
55
|
+
if (!shouldApply) return
|
|
56
|
+
|
|
57
|
+
const settingsPath = path.join(process.cwd(), '.vscode', 'settings.json')
|
|
58
|
+
let settings = {}
|
|
59
|
+
if (fs.existsSync(settingsPath)) {
|
|
60
|
+
try {
|
|
61
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'))
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.warn(`Could not parse ${settingsPath}; leaving VS Code settings unchanged: ${error?.message ?? error}`)
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const { settings: nextSettings, changed } = mergeVsCodeWatcherExcludes(settings)
|
|
69
|
+
if (!changed) {
|
|
70
|
+
console.log('VS Code watcher excludes already include the recommended Open Mercato patterns.')
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
fs.mkdirSync(path.dirname(settingsPath), { recursive: true })
|
|
75
|
+
fs.writeFileSync(settingsPath, `${JSON.stringify(nextSettings, null, 4)}\n`)
|
|
76
|
+
console.log(`Updated ${settingsPath}. Reload the WSL VS Code window so existing watcher handles are released.`)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function printCurrent(prefix) {
|
|
80
|
+
const { values } = readCurrentInotifyLimits()
|
|
81
|
+
console.log(`${prefix} ${JSON.stringify(values)}`)
|
|
82
|
+
return values
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (process.platform !== 'linux') {
|
|
86
|
+
console.log('Linux inotify limits do not apply on this platform; no sysctl changes are needed.')
|
|
87
|
+
process.exit(0)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const current = printCurrent('Current inotify limits:')
|
|
91
|
+
const issues = findInotifyLimitIssues(current)
|
|
92
|
+
|
|
93
|
+
if (issues.length === 0) {
|
|
94
|
+
console.log('Inotify limits already satisfy Open Mercato dev requirements.')
|
|
95
|
+
await maybeUpdateVsCodeSettings()
|
|
96
|
+
process.exit(0)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (detectWsl()) {
|
|
100
|
+
console.log('WSL detected; raising Linux inotify limits for Turbopack.')
|
|
101
|
+
} else {
|
|
102
|
+
console.log('Raising Linux inotify limits for Turbopack.')
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
for (const command of buildManualInotifyFix()) {
|
|
106
|
+
console.log(` ${command}`)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!await confirm('Apply these sysctl changes now?', { defaultYes: true })) {
|
|
110
|
+
console.log('No changes applied.')
|
|
111
|
+
process.exit(1)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
run('sudo', ['sysctl', '-w', ...buildSysctlAssignments(issues)])
|
|
115
|
+
|
|
116
|
+
if (!noPersist) {
|
|
117
|
+
run('sudo', ['tee', PERSIST_PATH], {
|
|
118
|
+
input: buildPersistentSysctlConfig(DEFAULT_INOTIFY_LIMITS),
|
|
119
|
+
})
|
|
120
|
+
run('sudo', ['sysctl', '--system'])
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
printCurrent('Updated inotify limits:')
|
|
124
|
+
await maybeUpdateVsCodeSettings()
|