From 22956af87775d8dea2787ef87ae4d46821df4710 Mon Sep 17 00:00:00 2001 From: Evan Wallace Date: Sun, 28 Mar 2021 11:07:53 -0700 Subject: [PATCH] use the file namespace for file entry points --- CHANGELOG.md | 6 ++++ internal/bundler/bundler.go | 64 ++++++++++++++++++++++++------------- scripts/plugin-tests.js | 19 +++++++++++ 3 files changed, 67 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d6f19f4c706..5540078d58f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -102,6 +102,12 @@ Previously esbuild would determine the output path for an entry point by looking at the post-resolved path. For example, running `esbuild --bundle react --outdir=out` would generate the output path `out/index.js` because the input path `react` was resolved to `node_modules/react/index.js`. With this release, the output path is now determined by looking at the pre-resolved path. For example, For example, running `esbuild --bundle react --outdir=out` now generates the output path `out/react.js`. If you need to keep using the output path that esbuild previously generated with the old behavior, you can use the custom output path feature (described above). +* Use the `file` namespace for file entry points ([#791](https://github.com/evanw/esbuild/issues/791)) + + Plugins that contain an `onResolve` callback with the `file` filter don't apply to entry point paths because it's not clear that entry point paths are files. For example, you could potentially bundle an entry point of `https://www.example.com/file.js` with a HTTP plugin that automatically downloads data from the server at that URL. But this behavior can be unexpected for people writing plugins. + + With this release, esbuild will do a quick check first to see if the entry point path exists on the file system before running plugins. If it exists as a file, the namespace will now be `file` for that entry point path. This only checks the exact entry point name and doesn't attempt to search for the file, so for example it won't handle cases where you pass a package path as an entry point or where you pass an entry point without an extension. Hopefully this should help improve this situation in the common case where the entry point is an exact path. + ## Unreleased * Warn about mutation of private methods ([#1067](https://github.com/evanw/esbuild/pull/1067)) diff --git a/internal/bundler/bundler.go b/internal/bundler/bundler.go index c50b72376e9..44e5a4f8693 100644 --- a/internal/bundler/bundler.go +++ b/internal/bundler/bundler.go @@ -463,6 +463,7 @@ func parseFile(args parseArgs) { &args.caches.FSCache, &source, record.Range, + source.KeyPath.Namespace, record.Path.Text, record.Kind, absResolveDir, @@ -698,6 +699,7 @@ func runOnResolvePlugins( fsCache *cache.FSCache, importSource *logger.Source, importPathRange logger.Range, + importNamespace string, path string, kind ast.ImportKind, absResolveDir string, @@ -709,10 +711,14 @@ func runOnResolvePlugins( Kind: kind, PluginData: pluginData, } - applyPath := logger.Path{Text: path} + applyPath := logger.Path{ + Text: path, + Namespace: importNamespace, + } if importSource != nil { resolverArgs.Importer = importSource.KeyPath - applyPath.Namespace = importSource.KeyPath.Namespace + } else { + resolverArgs.Importer.Namespace = importNamespace } // Apply resolver plugins in order until one succeeds @@ -975,6 +981,7 @@ type scanner struct { type EntryPoint struct { InputPath string OutputPath string + IsFile bool } func ScanBundle( @@ -1263,27 +1270,34 @@ func (s *scanner) addEntryPoints(entryPoints []EntryPoint) []entryMeta { }) } - // Entry point paths without a leading "./" are interpreted as package - // paths. This happens because they go through general path resolution - // like all other import paths so that plugins can run on them. Requiring - // a leading "./" for a relative path simplifies writing plugins because - // entry points aren't a special case. - // - // However, requiring a leading "./" also breaks backward compatibility - // and makes working with the CLI more difficult. So attempt to insert - // "./" automatically when needed. We don't want to unconditionally insert - // a leading "./" because the path may not be a file system path. For - // example, it may be a URL. So only insert a leading "./" when the path - // is an exact match for an existing file. + // Check each entry point ahead of time to see if it's a real file entryPointAbsResolveDir := s.fs.Cwd() - for i, entryPoint := range entryPoints { - if !s.fs.IsAbs(entryPoint.InputPath) && resolver.IsPackagePath(entryPoint.InputPath) { - absPath := s.fs.Join(entryPointAbsResolveDir, entryPoint.InputPath) - dir := s.fs.Dir(absPath) - base := s.fs.Base(absPath) - if entries, err := s.fs.ReadDirectory(dir); err == nil { - if entry, _ := entries.Get(base); entry != nil && entry.Kind(s.fs) == fs.FileEntry { - entryPoints[i].InputPath = "./" + entryPoint.InputPath + for i := range entryPoints { + entryPoint := &entryPoints[i] + absPath := entryPoint.InputPath + if !s.fs.IsAbs(absPath) { + absPath = s.fs.Join(entryPointAbsResolveDir, absPath) + } + dir := s.fs.Dir(absPath) + base := s.fs.Base(absPath) + if entries, err := s.fs.ReadDirectory(dir); err == nil { + if entry, _ := entries.Get(base); entry != nil && entry.Kind(s.fs) == fs.FileEntry { + entryPoint.IsFile = true + + // Entry point paths without a leading "./" are interpreted as package + // paths. This happens because they go through general path resolution + // like all other import paths so that plugins can run on them. Requiring + // a leading "./" for a relative path simplifies writing plugins because + // entry points aren't a special case. + // + // However, requiring a leading "./" also breaks backward compatibility + // and makes working with the CLI more difficult. So attempt to insert + // "./" automatically when needed. We don't want to unconditionally insert + // a leading "./" because the path may not be a file system path. For + // example, it may be a URL. So only insert a leading "./" when the path + // is an exact match for an existing file. + if !s.fs.IsAbs(entryPoint.InputPath) && resolver.IsPackagePath(entryPoint.InputPath) { + entryPoint.InputPath = "./" + entryPoint.InputPath } } } @@ -1297,6 +1311,11 @@ func (s *scanner) addEntryPoints(entryPoints []EntryPoint) []entryMeta { entryPointWaitGroup.Add(len(entryPoints)) for i, entryPoint := range entryPoints { go func(i int, entryPoint EntryPoint) { + namespace := "" + if entryPoint.IsFile { + namespace = "file" + } + // Run the resolver and log an error if the path couldn't be resolved resolveResult, didLogError, debug := runOnResolvePlugins( s.options.Plugins, @@ -1306,6 +1325,7 @@ func (s *scanner) addEntryPoints(entryPoints []EntryPoint) []entryMeta { &s.caches.FSCache, nil, logger.Range{}, + namespace, entryPoint.InputPath, ast.ImportEntryPoint, entryPointAbsResolveDir, diff --git a/scripts/plugin-tests.js b/scripts/plugin-tests.js index bb8812d90fe..36708d4add7 100644 --- a/scripts/plugin-tests.js +++ b/scripts/plugin-tests.js @@ -863,6 +863,25 @@ let pluginTests = { assert.strictEqual(result.outputFiles[3].text, `// virtual-ns:input a/b/c.d.e\nconsole.log("input a/b/c.d.e");\n`) }, + async entryPointFileNamespace({ esbuild, testDir }) { + const input = path.join(testDir, 'in.js') + let worked = false + await writeFileAsync(input, 'stuff') + await esbuild.build({ + entryPoints: [input], + write: false, + plugins: [{ + name: 'name', + setup(build) { + build.onResolve({ filter: /.*/, namespace: 'file' }, () => { + worked = true + }) + }, + }], + }) + assert(worked) + }, + async stdinImporter({ esbuild, testDir }) { const output = path.join(testDir, 'out.js') await esbuild.build({