Skip to content

Commit

Permalink
fix #1357: watch + metafile/mangle-cache
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Dec 30, 2022
1 parent 366f1e6 commit cd066b3
Show file tree
Hide file tree
Showing 3 changed files with 178 additions and 18 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
@@ -1,5 +1,11 @@
# Changelog

## Unreleased

* Fix `--metafile` and `--mangle-cache` with `--watch` ([#1357](https://github.com/evanw/esbuild/issues/1357))

The CLI calls the Go API and then also writes out the metafile and/or mangle cache JSON files if those features are enabled. This extra step is necessary because these files are returned by the Go API as in-memory strings. However, this extra step accidentally didn't happen for all builds after the initial build when watch mode was enabled. This behavior used to work but it was broken in version 0.14.18 by the introduction of the mangle cache feature. This release fixes the combination of these features, so the metafile and mangle cache features should now work with watch mode. This behavior was only broken for the CLI, not for the JS or Go APIs.

## 0.16.12

* Loader defaults to `js` for extensionless files ([#2776](https://github.com/evanw/esbuild/issues/2776))
Expand Down
39 changes: 21 additions & 18 deletions pkg/cli/cli_impl.go
Expand Up @@ -1211,22 +1211,35 @@ func runImpl(osArgs []string) int {
}
}
}

// Write out the metafile whenever we rebuild
if buildOptions.Watch != nil {
buildOptions.Watch.OnRebuild = func(result api.BuildResult) {
writeMetafile(result.Metafile)
}
}
}

// Always generate a metafile if we're analyzing, even if it won't be written out
if analyze {
buildOptions.Metafile = true
}

writeExtraFiles := func(result *api.BuildResult) {
// Write the metafile to the file system
if writeMetafile != nil {
writeMetafile(result.Metafile)
}

// Write the mangle cache to the file system
if writeMangleCache != nil {
writeMangleCache(result.MangleCache)
}
}

// Write out extra files whenever we rebuild
if buildOptions.Watch != nil {
buildOptions.Watch.OnRebuild = func(result api.BuildResult) {
writeExtraFiles(&result)
}
}

// Run the build
result := api.Build(*buildOptions)
writeExtraFiles(&result)

// Print the analysis after the build
if analyze {
Expand All @@ -1239,19 +1252,9 @@ func runImpl(osArgs []string) int {
os.Stderr.WriteString("\n")
}

// Write the metafile to the file system
if writeMetafile != nil {
writeMetafile(result.Metafile)
}

// Write the mangle cache to the file system
if writeMangleCache != nil {
writeMangleCache(result.MangleCache)
}

// Do not exit if we're in watch mode
if buildOptions.Watch != nil {
<-make(chan bool)
<-make(chan struct{})
}

// Stop if there were errors
Expand Down
151 changes: 151 additions & 0 deletions scripts/end-to-end-tests.js
Expand Up @@ -6618,6 +6618,80 @@ if (process.platform === 'darwin' || process.platform === 'win32') {
)
}

// End-to-end watch mode tests
tests.push(
// Validate that the CLI watch mode correctly updates the metafile
testWatch({ metafile: true }, async ({ infile, outfile, metafile }) => {
await waitForCondition(
'initial build',
20,
() => fs.writeFile(infile, 'foo()'),
async () => {
assert.strictEqual(await fs.readFile(outfile, 'utf8'), 'foo();\n')
assert.strictEqual(JSON.parse(await fs.readFile(metafile, 'utf8')).inputs[path.basename(infile)].bytes, 5)
},
)

await waitForCondition(
'subsequent build',
20,
() => fs.writeFile(infile, 'foo(123)'),
async () => {
assert.strictEqual(await fs.readFile(outfile, 'utf8'), 'foo(123);\n')
assert.strictEqual(JSON.parse(await fs.readFile(metafile, 'utf8')).inputs[path.basename(infile)].bytes, 8)
},
)
}),

// Validate that the CLI watch mode correctly updates the mangle cache
testWatch({ args: ['--mangle-props=.'], mangleCache: true }, async ({ infile, outfile, mangleCache }) => {
await waitForCondition(
'initial build',
20,
() => fs.writeFile(infile, 'foo()'),
async () => {
assert.strictEqual(await fs.readFile(outfile, 'utf8'), 'foo();\n')
assert.strictEqual(await fs.readFile(mangleCache, 'utf8'), '{}\n')
},
)

await waitForCondition(
'subsequent build',
20,
() => fs.writeFile(infile, 'foo(bar.baz)'),
async () => {
assert.strictEqual(await fs.readFile(outfile, 'utf8'), 'foo(bar.a);\n')
assert.strictEqual(await fs.readFile(mangleCache, 'utf8'), '{\n "baz": "a"\n}\n')
},
)
}),
)

function waitForCondition(what, seconds, mutator, condition) {
return new Promise(async (resolve, reject) => {
const start = Date.now()
let e
try {
await mutator()
while (true) {
if (Date.now() - start > seconds * 1000) {
throw new Error(`Timeout of ${seconds} seconds waiting for ${what}` + (e ? `: ${e && e.message || e}` : ''))
}
await new Promise(r => setTimeout(r, 50))
try {
await condition()
break
} catch (err) {
e = err
}
}
resolve()
} catch (e) {
reject(e)
}
})
}

function test(args, files, options) {
return async () => {
const hasBundle = args.includes('--bundle')
Expand Down Expand Up @@ -6761,6 +6835,83 @@ function testStdout(input, args, callback) {
}
}

function testWatch(options, callback) {
return async () => {
const thisTestDir = path.join(testDir, '' + testCount++)
const infile = path.join(thisTestDir, 'in.js')
const outdir = path.join(thisTestDir, 'out')
const outfile = path.join(outdir, path.basename(infile))
const args = ['--watch=forever', infile, '--outdir=' + outdir, '--color'].concat(options.args || [])
let metafile
let mangleCache

if (options.metafile) {
metafile = path.join(thisTestDir, 'meta.json')
args.push('--metafile=' + metafile)
}

if (options.mangleCache) {
mangleCache = path.join(thisTestDir, 'mangle.json')
args.push('--mangle-cache=' + mangleCache)
}

let stderrPromise
try {
await fs.mkdir(thisTestDir, { recursive: true })
const maxSeconds = 60

// Start the child
const child = childProcess.spawn(esbuildPath, args, {
cwd: thisTestDir,
stdio: ['inherit', 'inherit', 'pipe'],
timeout: maxSeconds * 1000,
})

// Make sure the child is always killed
try {
// Buffer stderr in case we need it
const stderr = []
child.stderr.on('data', data => stderr.push(data))
const exitPromise = new Promise((_, reject) => {
child.on('close', code => reject(new Error(`Child "esbuild" process exited with code ${code}`)))
})
stderrPromise = new Promise(resolve => {
child.stderr.on('end', () => resolve(Buffer.concat(stderr).toString()))
})

// Run whatever check the caller is doing
let timeout
await Promise.race([
new Promise((_, reject) => {
timeout = setTimeout(() => reject(new Error(`Timeout of ${maxSeconds} seconds exceeded`)), maxSeconds * 1000)
}),
exitPromise,
callback({
infile,
outfile,
metafile,
mangleCache,
}),
])
clearTimeout(timeout)

// Clean up test output
removeRecursiveSync(thisTestDir)
} finally {
child.kill()
}
} catch (e) {
let stderr = stderrPromise ? '\n stderr:' + ('\n' + await stderrPromise).split('\n').join('\n ') : ''
console.error(`❌ test failed: ${e && e.message || e}
dir: ${path.relative(dirname, thisTestDir)}
args: ${args.join(' ')}` + stderr)
return false
}

return true
}
}

async function main() {
// Create a fresh test directory
removeRecursiveSync(testDir)
Expand Down

0 comments on commit cd066b3

Please sign in to comment.