Skip to content

Commit

Permalink
fix #752: basic support for watch mode with plugins
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Feb 5, 2021
1 parent 51b4d9c commit aeda824
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 0 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@

This change improves esbuild's compliance with the JavaScript specification. It is now an error to use legacy octal numeric literals and the identifiers `implements`, `interface`, `let`, `package`, `private`, `protected`, `public`, `static`, and `yield` in strict mode code.
* Basic support for watch mode with plugins ([#752](https://github.com/evanw/esbuild/issues/752))
With this release, watch mode should now work with simple [on-load plugins](https://esbuild.github.io/plugins/#load-callbacks). Watch mode is implemented by tracking all file system accesses made by esbuild as it does a build. However, this doesn't catch external file system accesses such as those made by plugins. Now if an on-load plugin is used on a path in the `file` namespace, esbuild will also read the file during watch mode so that watch mode is aware of the file system access. Note that there is not yet API support for a plugin to return additional paths for watch mode to monitor.

## 0.8.41

* Fix memory leak with watch mode when using the CLI ([#750](https://github.com/evanw/esbuild/issues/750))
Expand Down
8 changes: 8 additions & 0 deletions internal/bundler/bundler.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@ func parseFile(args parseArgs) {
args.importSource,
args.importPathRange,
args.pluginData,
args.options.WatchMode,
)
if !ok {
if args.inject != nil {
Expand Down Expand Up @@ -710,6 +711,7 @@ func runOnLoadPlugins(
importSource *logger.Source,
importPathRange logger.Range,
pluginData interface{},
isWatchMode bool,
) (loaderPluginResult, bool) {
loaderArgs := config.OnLoadArgs{
Path: source.KeyPath,
Expand All @@ -732,6 +734,9 @@ func runOnLoadPlugins(

// Stop now if there was an error
if didLogError {
if isWatchMode && source.KeyPath.Namespace == "file" {
fsCache.ReadFile(fs, source.KeyPath.Text) // Read the file for watch mode tracking
}
return loaderPluginResult{}, false
}

Expand All @@ -748,6 +753,9 @@ func runOnLoadPlugins(
if result.AbsResolveDir == "" && source.KeyPath.Namespace == "file" {
result.AbsResolveDir = fs.Dir(source.KeyPath.Text)
}
if isWatchMode && source.KeyPath.Namespace == "file" {
fsCache.ReadFile(fs, source.KeyPath.Text) // Read the file for watch mode tracking
}
return loaderPluginResult{
loader: loader,
absResolveDir: result.AbsResolveDir,
Expand Down
1 change: 1 addition & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ type Options struct {
MinifyIdentifiers bool
MangleSyntax bool
CodeSplitting bool
WatchMode bool

// Setting this to true disables warnings about code that is very likely to
// be a bug. This is used to ignore issues inside "node_modules" directories.
Expand Down
1 change: 1 addition & 0 deletions pkg/api/api_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,7 @@ func rebuildImpl(
InjectAbsPaths: make([]string, len(buildOpts.Inject)),
Banner: buildOpts.Banner,
Footer: buildOpts.Footer,
WatchMode: buildOpts.Watch != nil,
Plugins: plugins,
}
for i, path := range buildOpts.Inject {
Expand Down
71 changes: 71 additions & 0 deletions scripts/plugin-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -1259,6 +1259,77 @@ let pluginTests = {
})
assert.strictEqual(result.outputFiles[0].text, 'import("import");\n')
},

async pluginWithWatchMode({ esbuild, service, testDir }) {
for (const toTest of [esbuild, service]) {
const srcDir = path.join(testDir, 'src')
const outfile = path.join(testDir, 'out.js')
const input = path.join(srcDir, 'in.js')
const example = path.join(srcDir, 'example.js')
await mkdirAsync(srcDir, { recursive: true })
await writeFileAsync(input, `import {x} from "./example.js"; exports.x = x`)
await writeFileAsync(example, `export let x = 1`)

let onRebuild = () => { }
const result = await toTest.build({
entryPoints: [input],
outfile,
format: 'cjs',
watch: {
onRebuild: (...args) => onRebuild(args),
},
bundle: true,
plugins: [
{
name: 'some-plugin',
setup(build) {
build.onLoad({ filter: /example\.js$/ }, async (args) => {
const contents = await fs.promises.readFile(args.path, 'utf8')
return { contents }
})
},
},
],
})
const rebuildUntil = (mutator, condition) => {
let timeout
return new Promise((resolve, reject) => {
timeout = setTimeout(() => reject(new Error('Timeout after 30 seconds')), 30 * 1000)
onRebuild = args => {
try { if (condition(...args)) clearTimeout(timeout), resolve(args) }
catch (e) { clearTimeout(timeout), reject(e) }
}
mutator()
})
}

try {
let code = await readFileAsync(outfile, 'utf8')
let exports = {}
new Function('exports', code)(exports)
assert.strictEqual(result.outputFiles, void 0)
assert.strictEqual(typeof result.stop, 'function')
assert.strictEqual(exports.x, 1)

// First rebuild: edit
{
const [error2, result2] = await rebuildUntil(
() => writeFileAsync(example, `export let x = 2`),
() => fs.readFileSync(outfile, 'utf8') !== code,
)
code = await readFileAsync(outfile, 'utf8')
exports = {}
new Function('exports', code)(exports)
assert.strictEqual(error2, null)
assert.strictEqual(result2.outputFiles, void 0)
assert.strictEqual(result2.stop, result.stop)
assert.strictEqual(exports.x, 2)
}
} finally {
result.stop()
}
}
},
}

async function main() {
Expand Down

0 comments on commit aeda824

Please sign in to comment.