Skip to content

Commit

Permalink
add file loader (#135)
Browse files Browse the repository at this point in the history
  • Loading branch information
viankakrisna authored May 29, 2020
1 parent 57b68b6 commit 12ff76f
Show file tree
Hide file tree
Showing 8 changed files with 123 additions and 32 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ Options:
--jsx-factory=... What to use instead of React.createElement
--jsx-fragment=... What to use instead of React.Fragment
--loader:X=L Use loader L to load file extension X, where L is
one of: js, jsx, ts, tsx, json, text, base64, dataurl
one of: js, jsx, ts, tsx, json, text, base64, file, dataurl

Advanced options:
--version Print the current version and exit
Expand Down
15 changes: 14 additions & 1 deletion cmd/esbuild/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ Options:
--jsx-factory=... What to use instead of React.createElement
--jsx-fragment=... What to use instead of React.Fragment
--loader:X=L Use loader L to load file extension X, where L is
one of: js, jsx, ts, tsx, json, text, base64, dataurl
one of: js, jsx, ts, tsx, json, text, base64, file, dataurl
Advanced options:
--version Print the current version and exit (` + esbuildVersion + `)
Expand Down Expand Up @@ -176,6 +176,8 @@ func (args *argsObject) parseLoader(text string) bundler.Loader {
return bundler.LoaderBase64
case "dataurl":
return bundler.LoaderDataURL
case "file":
return bundler.LoaderFile
default:
return bundler.LoaderNone
}
Expand Down Expand Up @@ -645,6 +647,17 @@ func run(fs fs.FS, args argsObject) {
}
args.logInfo(fmt.Sprintf("Wrote to %s (%s)", path, toSize(len(item.JsContents))))

// Write out the additional files
for _, file := range item.AdditionalFiles {
if file.Path != "" {
err := ioutil.WriteFile(file.Path, []byte(file.Contents), 0644)
path := resolver.PrettyPath(file.Path)
if err != nil {
exitWithError(fmt.Sprintf("Failed to write to %s (%s)", path, err.Error()))
}
args.logInfo(fmt.Sprintf("Wrote to %s (%s)", path, toSize(len(file.Contents))))
}
}
// Also write the source map
if item.SourceMapAbsPath != "" {
err := ioutil.WriteFile(item.SourceMapAbsPath, item.SourceMapContents, 0644)
Expand Down
5 changes: 4 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,7 @@ module github.com/evanw/esbuild

go 1.13

require golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3
require (
github.com/kylelemons/godebug v1.1.0
golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3
)
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3 h1:5B6i6EAiSYyejWfvc5Rc9BbI3rzIsrrXfAQBWnYfn+w=
golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
76 changes: 51 additions & 25 deletions internal/bundler/bundler.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"mime"
"net/http"
"os"
"strings"
"path"
"sync"

"github.com/evanw/esbuild/internal/ast"
Expand All @@ -33,6 +33,9 @@ type file struct {
// This is used by the printer to write out the source index for modules that
// are referenced in the AST.
resolvedImports map[string]uint32

// This is used for file-loader to emit files
additionalFile AdditionalFile
}

func (f *file) resolveImport(path ast.Path) (uint32, bool) {
Expand All @@ -51,15 +54,16 @@ type Bundle struct {
}

type parseResult struct {
source logging.Source
ast ast.AST
ok bool
source logging.Source
ast ast.AST
ok bool
outputPath string
}

func parseFile(
log logging.Log,
res resolver.Resolver,
path string,
sourcePath string,
sourceIndex uint32,
isStdin bool,
importSource logging.Source,
Expand All @@ -69,9 +73,9 @@ func parseFile(
bundleOptions BundleOptions,
results chan parseResult,
) {
prettyPath := path
prettyPath := sourcePath
if !isStdin {
prettyPath = res.PrettyPath(path)
prettyPath = res.PrettyPath(sourcePath)
}
contents := ""

Expand All @@ -87,9 +91,9 @@ func parseFile(
contents = string(bytes)
} else {
var ok bool
contents, ok = res.Read(path)
contents, ok = res.Read(sourcePath)
if !ok {
log.AddRangeError(importSource, pathRange, fmt.Sprintf("Could not read from file: %s", path))
log.AddRangeError(importSource, pathRange, fmt.Sprintf("Could not read from file: %s", sourcePath))
results <- parseResult{}
return
}
Expand All @@ -99,16 +103,13 @@ func parseFile(
source := logging.Source{
Index: sourceIndex,
IsStdin: isStdin,
AbsolutePath: path,
AbsolutePath: sourcePath,
PrettyPath: prettyPath,
Contents: contents,
}

// Get the file extension
extension := ""
if lastDot := strings.LastIndexByte(path, '.'); lastDot >= 0 {
extension = path[lastDot:]
}
extension := path.Ext(sourcePath)

// Pick the loader based on the file extension
loader := bundleOptions.ExtensionToLoader[extension]
Expand All @@ -121,39 +122,39 @@ func parseFile(
switch loader {
case LoaderJS:
ast, ok := parser.Parse(log, source, parseOptions)
results <- parseResult{source, ast, ok}
results <- parseResult{source, ast, ok, ""}

case LoaderJSX:
parseOptions.JSX.Parse = true
ast, ok := parser.Parse(log, source, parseOptions)
results <- parseResult{source, ast, ok}
results <- parseResult{source, ast, ok, ""}

case LoaderTS:
parseOptions.TS.Parse = true
ast, ok := parser.Parse(log, source, parseOptions)
results <- parseResult{source, ast, ok}
results <- parseResult{source, ast, ok, ""}

case LoaderTSX:
parseOptions.TS.Parse = true
parseOptions.JSX.Parse = true
ast, ok := parser.Parse(log, source, parseOptions)
results <- parseResult{source, ast, ok}
results <- parseResult{source, ast, ok, ""}

case LoaderJSON:
expr, ok := parser.ParseJSON(log, source, parser.ParseJSONOptions{})
ast := parser.ModuleExportsAST(log, source, parseOptions, expr)
results <- parseResult{source, ast, ok}
results <- parseResult{source, ast, ok, ""}

case LoaderText:
expr := ast.Expr{ast.Loc{0}, &ast.EString{lexer.StringToUTF16(source.Contents)}}
ast := parser.ModuleExportsAST(log, source, parseOptions, expr)
results <- parseResult{source, ast, true}
results <- parseResult{source, ast, true, ""}

case LoaderBase64:
encoded := base64.StdEncoding.EncodeToString([]byte(source.Contents))
expr := ast.Expr{ast.Loc{0}, &ast.EString{lexer.StringToUTF16(encoded)}}
ast := parser.ModuleExportsAST(log, source, parseOptions, expr)
results <- parseResult{source, ast, true}
results <- parseResult{source, ast, true, ""}

case LoaderDataURL:
mimeType := mime.TypeByExtension(extension)
Expand All @@ -164,10 +165,20 @@ func parseFile(
url := "data:" + mimeType + ";base64," + encoded
expr := ast.Expr{ast.Loc{0}, &ast.EString{lexer.StringToUTF16(url)}}
ast := parser.ModuleExportsAST(log, source, parseOptions, expr)
results <- parseResult{source, ast, true}
results <- parseResult{source, ast, true, ""}

case LoaderFile:
url := path.Base(sourcePath)
targetFolder := bundleOptions.AbsOutputDir
if targetFolder == "" {
targetFolder = path.Dir(bundleOptions.AbsOutputFile)
}
expr := ast.Expr{ast.Loc{0}, &ast.EString{lexer.StringToUTF16(url)}}
ast := parser.ModuleExportsAST(log, source, parseOptions, expr)
results <- parseResult{source, ast, true, path.Join(targetFolder, url)}

default:
log.AddRangeError(importSource, pathRange, fmt.Sprintf("File extension not supported: %s", path))
log.AddRangeError(importSource, pathRange, fmt.Sprintf("File extension not supported: %s", sourcePath))
results <- parseResult{}
}
}
Expand Down Expand Up @@ -205,7 +216,7 @@ func ScanBundle(
runtimeParseOptions.IsBundling = true

ast, ok := parser.Parse(log, source, runtimeParseOptions)
results <- parseResult{source, ast, ok}
results <- parseResult{source, ast, ok, ""}
}()
}

Expand Down Expand Up @@ -286,7 +297,8 @@ func ScanBundle(
}

sources[source.Index] = source
files[source.Index] = file{result.ast, resolvedImports}

files[source.Index] = file{result.ast, resolvedImports, AdditionalFile{result.outputPath, source.Contents}}
}

return Bundle{fs, sources, files, entryPoints}
Expand All @@ -304,6 +316,7 @@ const (
LoaderText
LoaderBase64
LoaderDataURL
LoaderFile
)

func DefaultExtensionToLoaderMap() map[string]Loader {
Expand Down Expand Up @@ -355,11 +368,17 @@ type BundleOptions struct {
omitRuntimeForTests bool
}

type AdditionalFile struct {
Path string
Contents string
}

type BundleResult struct {
JsAbsPath string
JsContents []byte
SourceMapAbsPath string
SourceMapContents []byte
AdditionalFiles []AdditionalFile
}

type lineColumnOffset struct {
Expand Down Expand Up @@ -415,6 +434,13 @@ func (b *Bundle) Compile(log logging.Log, options BundleOptions) []BundleResult
var results []BundleResult
for _, group := range resultGroups {
results = append(results, group...)
for _, bundle := range group {
for _, additionalFile := range bundle.AdditionalFiles {
if additionalFile.Path != "" {
results = append(results, BundleResult{additionalFile.Path, []byte(additionalFile.Contents), "", []byte(""), []AdditionalFile{}})
}
}
}
}
return results
}
45 changes: 44 additions & 1 deletion internal/bundler/bundler_test.go
Original file line number Diff line number Diff line change
@@ -1,19 +1,28 @@
package bundler

import (
"fmt"
"path"
"strings"
"testing"

"github.com/evanw/esbuild/internal/fs"
"github.com/evanw/esbuild/internal/logging"
"github.com/evanw/esbuild/internal/parser"
"github.com/evanw/esbuild/internal/printer"
"github.com/evanw/esbuild/internal/resolver"
"github.com/kylelemons/godebug/diff"
)

func assertEqual(t *testing.T, a interface{}, b interface{}) {
if a != b {
t.Fatalf("%s != %s", a, b)
stringA := fmt.Sprintf("%v", a)
stringB := fmt.Sprintf("%v", b)
if strings.Contains(stringA, "\n") {
t.Fatal(diff.Diff(stringA, stringB))
} else {
t.Fatalf("%s != %s", a, b)
}
}
}

Expand Down Expand Up @@ -2739,6 +2748,40 @@ func testAutoDetectMimeTypeFromExtension(t *testing.T) {
})
}

func TestLoaderFile(t *testing.T) {
expectBundled(t, bundled{
files: map[string]string{
"/entry.js": `
console.log(require('./test.svg'))
`,
"/test.svg": "<svg></svg>",
},
entryPaths: []string{"/entry.js"},
parseOptions: parser.ParseOptions{
IsBundling: true,
},
bundleOptions: BundleOptions{
IsBundling: true,
AbsOutputDir: "/out/",
ExtensionToLoader: map[string]Loader{
".js": LoaderJS,
".svg": LoaderFile,
},
},
expected: map[string]string{
"/out/test.svg": "<svg></svg>",
"/out/entry.js": `// /test.svg
var require_test = __commonJS((exports, module) => {
module.exports = "test.svg";
});
// /entry.js
console.log(require_test());
`,
},
})
}

func TestRequireBadExtension(t *testing.T) {
expectBundled(t, bundled{
files: map[string]string{
Expand Down
8 changes: 6 additions & 2 deletions internal/bundler/linker.go
Original file line number Diff line number Diff line change
Expand Up @@ -1714,6 +1714,7 @@ func (c *linkerContext) generateCodeForFileInChunk(
}

func (c *linkerContext) generateChunk(chunk chunkMeta) BundleResult {
additionalFiles := []AdditionalFile{}
filesInChunkInOrder := c.chunkFileOrder(chunk)
compileResults := make([]compileResult, 0, len(filesInChunkInOrder))
runtimeMembers := c.files[ast.RuntimeSourceIndex].ast.ModuleScope.Members
Expand All @@ -1738,6 +1739,9 @@ func (c *linkerContext) generateChunk(chunk chunkMeta) BundleResult {
// Generate JavaScript for each file in parallel
waitGroup := sync.WaitGroup{}
for _, sourceIndex := range filesInChunkInOrder {
file := c.files[sourceIndex]

additionalFiles = append(additionalFiles, file.additionalFile)
// Skip the runtime in test output
if sourceIndex == ast.RuntimeSourceIndex && c.options.omitRuntimeForTests {
continue
Expand Down Expand Up @@ -1797,7 +1801,6 @@ func (c *linkerContext) generateChunk(chunk chunkMeta) BundleResult {
var entryPointTail *printer.PrintResult
for _, compileResult := range compileResults {
isRuntime := compileResult.sourceIndex == ast.RuntimeSourceIndex

// If this is the entry point, it may have some extra code to stick at the
// end of the chunk after all modules have evaluated
if compileResult.entryPointTail != nil {
Expand Down Expand Up @@ -1856,7 +1859,8 @@ func (c *linkerContext) generateChunk(chunk chunkMeta) BundleResult {
}

result := BundleResult{
JsAbsPath: c.fs.Join(c.options.AbsOutputDir, chunk.name),
AdditionalFiles: additionalFiles,
JsAbsPath: c.fs.Join(c.options.AbsOutputDir, chunk.name),
}

if c.options.SourceMap != SourceMapNone {
Expand Down
2 changes: 1 addition & 1 deletion npm/esbuild/lib/main.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export declare type Target = 'esnext' | 'es6' | 'es2015' | 'es2016' | 'es2017' | 'es2018' | 'es2019' | 'es2020';
export declare type Platform = 'browser' | 'node';
export declare type Format = 'iife' | 'cjs' | 'esm';
export declare type Loader = 'js' | 'jsx' | 'ts' | 'tsx' | 'json' | 'text' | 'base64' | 'dataurl';
export declare type Loader = 'js' | 'jsx' | 'ts' | 'tsx' | 'json' | 'text' | 'base64' | 'file' | 'dataurl';
export declare type LogLevel = 'info' | 'warning' | 'error';

interface CommonOptions {
Expand Down

0 comments on commit 12ff76f

Please sign in to comment.