Skip to content

Commit

Permalink
fix #127 and fix #191: support external paths
Browse files Browse the repository at this point in the history
  • Loading branch information
evanw committed Jul 4, 2020
1 parent 501682c commit 66fb3f5
Show file tree
Hide file tree
Showing 8 changed files with 242 additions and 76 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@

You can now mark scoped packages as external. For example, `--external:@babel/core` marks the package `@babel/core` as external. This was contributed by [@floydspace](https://github.com/floydspace).

* Add support for external paths ([#127](https://github.com/evanw/esbuild/issues/127) and [#191](https://github.com/evanw/esbuild/issues/191))

Previously the `--external:M` flag only worked if `M` was a package name. For example, you can mark the `fs` package as external with `--external:fs`.

With this release, you can now also mark file paths as external using the same syntax. For example, `--external:./index.js` marks the file `index.js` in the current working directory as external. The path to the external module used in the output file will be relative to the output directory.

## 0.5.19

* Fix bug with TypeScript `typeof` operator ([#213](https://github.com/evanw/esbuild/issues/213))
Expand Down
66 changes: 36 additions & 30 deletions internal/bundler/bundler.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,9 +184,6 @@ func parseFile(args parseArgs) {

// Determine the destination folder
targetFolder := args.options.AbsOutputDir
if targetFolder == "" {
targetFolder = args.fs.Dir(args.options.AbsOutputFile)
}

// Export the resulting relative path as a string
expr := ast.Expr{Data: &ast.EString{Value: lexer.StringToUTF16(baseName)}}
Expand Down Expand Up @@ -243,7 +240,7 @@ func lowerCaseAbsPathForWindows(absPath string) string {

func baseNameForAvoidingCollisions(fs fs.FS, absPath string) string {
var toHash []byte
if relPath, ok := fs.RelativeToCwd(absPath); ok {
if relPath, ok := fs.Rel(fs.Cwd(), absPath); ok {
// Attempt to generate the same base name regardless of what machine or
// operating system we're on. We want to avoid absolute paths because they
// will have different home directories. We also want to avoid path
Expand Down Expand Up @@ -382,10 +379,9 @@ func ScanBundle(log logging.Log, fs fs.FS, res resolver.Resolver, entryPaths []s
continue
}

sourcePath := source.AbsolutePath
pathText := record.Path.Text
pathRange := source.RangeOfString(record.Path.Loc)
resolveResult := res.Resolve(sourcePath, pathText)
resolveResult := res.Resolve(source.AbsolutePath, pathText)

switch resolveResult.Status {
case resolver.ResolveEnabled, resolver.ResolveDisabled:
Expand All @@ -407,6 +403,14 @@ func ScanBundle(log logging.Log, fs fs.FS, res resolver.Resolver, entryPaths []s

case resolver.ResolveMissing:
log.AddRangeError(&source, pathRange, fmt.Sprintf("Could not resolve %q", pathText))

case resolver.ResolveExternalRelative:
// If the path to the external module is relative to the source
// file, rewrite the path to be relative to the working directory
if relPath, ok := fs.Rel(options.AbsOutputDir, resolveResult.AbsolutePath); ok {
// Prevent issues with path separators being different on Windows
record.Path.Text = strings.ReplaceAll(relPath, "\\", "/")
}
}
}
}
Expand Down Expand Up @@ -531,34 +535,36 @@ func (b *Bundle) Compile(log logging.Log, options config.Options) []OutputFile {
})
}

// Make sure an output file never overwrites an input file
sourceAbsPaths := make(map[string]uint32)
for _, group := range resultGroups {
for _, sourceIndex := range group.reachableFiles {
lowerAbsPath := lowerCaseAbsPathForWindows(b.sources[sourceIndex].AbsolutePath)
sourceAbsPaths[lowerAbsPath] = sourceIndex
if !options.WriteToStdout {
// Make sure an output file never overwrites an input file
sourceAbsPaths := make(map[string]uint32)
for _, group := range resultGroups {
for _, sourceIndex := range group.reachableFiles {
lowerAbsPath := lowerCaseAbsPathForWindows(b.sources[sourceIndex].AbsolutePath)
sourceAbsPaths[lowerAbsPath] = sourceIndex
}
}
}
for _, outputFile := range outputFiles {
lowerAbsPath := lowerCaseAbsPathForWindows(outputFile.AbsPath)
if sourceIndex, ok := sourceAbsPaths[lowerAbsPath]; ok {
log.AddError(nil, ast.Loc{}, "Refusing to overwrite input file: "+b.sources[sourceIndex].PrettyPath)
for _, outputFile := range outputFiles {
lowerAbsPath := lowerCaseAbsPathForWindows(outputFile.AbsPath)
if sourceIndex, ok := sourceAbsPaths[lowerAbsPath]; ok {
log.AddError(nil, ast.Loc{}, "Refusing to overwrite input file: "+b.sources[sourceIndex].PrettyPath)
}
}
}

// Make sure an output file never overwrites another output file. This
// is almost certainly unintentional and would otherwise happen silently.
outputFileMap := make(map[string]bool)
for _, outputFile := range outputFiles {
lowerAbsPath := lowerCaseAbsPathForWindows(outputFile.AbsPath)
if outputFileMap[lowerAbsPath] {
outputPath := outputFile.AbsPath
if relPath, ok := b.fs.RelativeToCwd(outputPath); ok {
outputPath = relPath
// Make sure an output file never overwrites another output file. This
// is almost certainly unintentional and would otherwise happen silently.
outputFileMap := make(map[string]bool)
for _, outputFile := range outputFiles {
lowerAbsPath := lowerCaseAbsPathForWindows(outputFile.AbsPath)
if outputFileMap[lowerAbsPath] {
outputPath := outputFile.AbsPath
if relPath, ok := b.fs.Rel(b.fs.Cwd(), outputPath); ok {
outputPath = relPath
}
log.AddError(nil, ast.Loc{}, "Two output files share the same path: "+outputPath)
} else {
outputFileMap[lowerAbsPath] = true
}
log.AddError(nil, ast.Loc{}, "Two output files share the same path: "+outputPath)
} else {
outputFileMap[lowerAbsPath] = true
}
}

Expand Down
72 changes: 59 additions & 13 deletions internal/bundler/bundler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ func expectBundled(t *testing.T, args bundled) {
t.Run("", func(t *testing.T) {
fs := fs.MockFS(args.files)
args.options.ExtensionOrder = []string{".tsx", ".ts", ".jsx", ".js", ".json"}
if args.options.AbsOutputFile != "" {
args.options.AbsOutputDir = path.Dir(args.options.AbsOutputFile)
}
log := logging.NewDeferLog()
resolver := resolver.NewResolver(fs, log, args.options)
bundle := ScanBundle(log, fs, resolver, args.entryPaths, args.options)
Expand All @@ -68,9 +71,6 @@ func expectBundled(t *testing.T, args bundled) {

log = logging.NewDeferLog()
args.options.OmitRuntimeForTests = true
if args.options.AbsOutputFile != "" {
args.options.AbsOutputDir = path.Dir(args.options.AbsOutputFile)
}
results := bundle.Compile(log, args.options)
msgs = log.Done()
assertLog(t, msgs, args.expectedCompileLog)
Expand Down Expand Up @@ -4448,8 +4448,10 @@ func TestImportReExportES6Issue149(t *testing.T) {
Factory: []string{"h"},
},
AbsOutputFile: "/out.js",
ExternalModules: map[string]bool{
"preact": true,
ExternalModules: config.ExternalModules{
NodeModules: map[string]bool{
"preact": true,
},
},
},
expected: map[string]string{
Expand All @@ -4468,7 +4470,7 @@ render(h(App, null), document.getElementById("app"));
})
}

func TestExternalModuleExclusion(t *testing.T) {
func TestExternalModuleExclusionPackage(t *testing.T) {
expectBundled(t, bundled{
files: map[string]string{
"/index.js": `
Expand All @@ -4482,8 +4484,10 @@ func TestExternalModuleExclusion(t *testing.T) {
options: config.Options{
IsBundling: true,
AbsOutputFile: "/out.js",
ExternalModules: map[string]bool{
"aws-sdk": true,
ExternalModules: config.ExternalModules{
NodeModules: map[string]bool{
"aws-sdk": true,
},
},
},
expected: map[string]string{
Expand Down Expand Up @@ -4515,8 +4519,10 @@ func TestScopedExternalModuleExclusion(t *testing.T) {
options: config.Options{
IsBundling: true,
AbsOutputFile: "/out.js",
ExternalModules: map[string]bool{
"@scope/foo": true,
ExternalModules: config.ExternalModules{
NodeModules: map[string]bool{
"@scope/foo": true,
},
},
},
expected: map[string]string{
Expand All @@ -4534,6 +4540,44 @@ export {
})
}

func TestExternalModuleExclusionRelativePath(t *testing.T) {
expectBundled(t, bundled{
files: map[string]string{
"/Users/user/project/src/index.js": `
import './nested/folder/test'
`,
"/Users/user/project/src/nested/folder/test.js": `
import foo from './foo.js'
import sha256 from '../../sha256.min.js'
import config from '/api/config?a=1&b=2'
console.log(foo, sha256, config)
`,
},
entryPaths: []string{"/Users/user/project/src/index.js"},
options: config.Options{
IsBundling: true,
AbsOutputDir: "/Users/user/project/out",
ExternalModules: config.ExternalModules{
AbsPaths: map[string]bool{
"/Users/user/project/src/nested/folder/foo.js": true,
"/Users/user/project/src/sha256.min.js": true,
"/api/config?a=1&b=2": true,
},
},
},
expected: map[string]string{
"/Users/user/project/out/index.js": `// /Users/user/project/src/nested/folder/test.js
import foo2 from "../src/nested/folder/foo.js";
import sha256 from "../src/sha256.min.js";
import config from "/api/config?a=1&b=2";
console.log(foo2, sha256, config);
// /Users/user/project/src/index.js
`,
},
})
}

// This test case makes sure many entry points don't cause a crash
func TestManyEntryPoints(t *testing.T) {
expectBundled(t, bundled{
Expand Down Expand Up @@ -5015,9 +5059,11 @@ func TestReExportDefaultExternal(t *testing.T) {
options: config.Options{
IsBundling: true,
AbsOutputFile: "/out.js",
ExternalModules: map[string]bool{
"foo": true,
"bar": true,
ExternalModules: config.ExternalModules{
NodeModules: map[string]bool{
"foo": true,
"bar": true,
},
},
},
expected: map[string]string{
Expand Down
7 changes: 6 additions & 1 deletion internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,11 @@ type StdinInfo struct {
SourceFile string
}

type ExternalModules struct {
NodeModules map[string]bool
AbsPaths map[string]bool
}

type Options struct {
// true: imports are scanned and bundled along with the file
// false: imports are left alone and the file is passed through as-is
Expand All @@ -148,7 +153,7 @@ type Options struct {
Platform Platform

ExtensionOrder []string
ExternalModules map[string]bool
ExternalModules ExternalModules

AbsOutputFile string
AbsOutputDir string
Expand Down
73 changes: 61 additions & 12 deletions internal/fs/fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"os"
"path"
"path/filepath"
"strings"
"sync"
)

Expand Down Expand Up @@ -34,7 +35,8 @@ type FS interface {
Base(path string) string
Ext(path string) string
Join(parts ...string) string
RelativeToCwd(path string) (string, bool)
Cwd() string
Rel(base string, target string) (string, bool)
}

////////////////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -104,8 +106,52 @@ func (*mockFS) Join(parts ...string) string {
return path.Clean(path.Join(parts...))
}

func (*mockFS) RelativeToCwd(path string) (string, bool) {
return "", false
func (*mockFS) Cwd() string {
return ""
}

func splitOnSlash(path string) (string, string) {
if slash := strings.IndexByte(path, '/'); slash != -1 {
return path[:slash], path[slash+1:]
}
return path, ""
}

func (*mockFS) Rel(base string, target string) (string, bool) {
// Base cases
if base == "" {
return target, true
}
if base == target {
return ".", true
}

// Find the common parent directory
for {
bHead, bTail := splitOnSlash(base)
tHead, tTail := splitOnSlash(target)
if bHead != tHead {
break
}
base = bTail
target = tTail
}

// Stop now if base is a subpath of target
if base == "" {
return target, true
}

// Traverse up to the common parent
commonParent := strings.Repeat("../", strings.Count(base, "/")+1)

// Stop now if target is a subpath of base
if target == "" {
return commonParent[:len(commonParent)-1], true
}

// Otherwise, down to the parent
return commonParent + target, true
}

////////////////////////////////////////////////////////////////////////////////
Expand All @@ -116,16 +162,17 @@ type realFS struct {
entries map[string]map[string]Entry

// For the current working directory
cwd string
cwdOk bool
cwd string
}

func RealFS() FS {
cwd, cwdErr := os.Getwd()
cwd, err := os.Getwd()
if err != nil {
cwd = ""
}
return &realFS{
entries: make(map[string]map[string]Entry),
cwd: cwd,
cwdOk: cwdErr == nil,
}
}

Expand Down Expand Up @@ -222,11 +269,13 @@ func (*realFS) Join(parts ...string) string {
return filepath.Clean(filepath.Join(parts...))
}

func (fs *realFS) RelativeToCwd(path string) (string, bool) {
if fs.cwdOk {
if rel, err := filepath.Rel(fs.cwd, path); err == nil {
return rel, true
}
func (fs *realFS) Cwd() string {
return fs.cwd
}

func (*realFS) Rel(base string, target string) (string, bool) {
if rel, err := filepath.Rel(base, target); err == nil {
return rel, true
}
return "", false
}
Expand Down
Loading

0 comments on commit 66fb3f5

Please sign in to comment.