js.Build: Generate tsconfig files
authorAndreas Richter <andy@selfstudy.com>
Sat, 12 Sep 2020 04:19:36 +0000 (00:19 -0400)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Tue, 3 Nov 2020 12:04:37 +0000 (13:04 +0100)
Updates #7777

Added support to allow SourceMap files to be external to the build.
In addition added more information when the js compilation has an error.
Correctly append sourceMappingURL to output file.
Fix merge conflict.

.travis.yml
hugolib/js_test.go
resources/resource_transformers/js/build.go
resources/resource_transformers/js/build_test.go

index 9ff6029b18c1b7455bc41b67703119e2f492ccfd..bff54e4dd50236af67d545ad406941b150fa11c1 100644 (file)
@@ -47,10 +47,10 @@ cache:
 
 before_install:
   - df -h
-    # https://travis-ci.community/t/go-cant-find-gcc-with-go1-11-1-on-windows/293/5
+  # https://travis-ci.community/t/go-cant-find-gcc-with-go1-11-1-on-windows/293/5
   - if [ "$TRAVIS_OS_NAME" = "windows" ]; then
-        choco install mingw -y;
-        export PATH=/c/tools/mingw64/bin:"$PATH";
+    choco install mingw -y;
+    export PATH=/c/tools/mingw64/bin:"$PATH";
     fi
   - gem install asciidoctor
   - type asciidoctor
@@ -65,11 +65,12 @@ install:
 script:
   - go mod download
   - go mod verify
-  - mage -v test
-  - if [ "$TRAVIS_ARCH" = "amd64" ]; then
-        mage -v check;
+  - travis_wait 20 mage -v test
+  - >
+    if [ "$TRAVIS_ARCH" = "amd64" ]; then
+      mage -v check;
     else
-        HUGO_TIMEOUT=30000 mage -v check;
+      HUGO_TIMEOUT=30000 mage -v check;
     fi
   - mage -v hugo
   - HUGO_IGNOREERRORS=error-remote-getjson ./hugo -s docs/
index a421ed338f3b9fe600c314c0df826ff2c59d8417..e34ce386790a267cd34ed4add2673b83480b6d1b 100644 (file)
@@ -22,6 +22,7 @@ import (
 
        "github.com/gohugoio/hugo/htesting"
 
+       "github.com/spf13/afero"
        "github.com/spf13/viper"
 
        qt "github.com/frankban/quicktest"
@@ -81,7 +82,9 @@ document.body.textContent = greeter(user);`
   "scripts": {},
 
   "dependencies": {
-    "to-camel-case": "1.0.0"
+               "to-camel-case": "1.0.0",
+               "react": "^16",
+               "react-dom": "^16"
   }
 }
 `
@@ -198,3 +201,285 @@ console.log(&#34;included&#34;);
 `)
 
 }
+
+func TestJSBuildGlobals(t *testing.T) {
+       if !isCI() {
+               t.Skip("skip (relative) long running modules test when running locally")
+       }
+
+       wd, _ := os.Getwd()
+       defer func() {
+               os.Chdir(wd)
+       }()
+
+       c := qt.New(t)
+
+       workDir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugo-test-js")
+       c.Assert(err, qt.IsNil)
+       defer clean()
+
+       v := viper.New()
+       v.Set("workingDir", workDir)
+       v.Set("disableKinds", []string{"taxonomy", "term", "page"})
+       b := newTestSitesBuilder(t).WithLogger(loggers.NewWarningLogger())
+
+       b.Fs = hugofs.NewDefault(v)
+       b.WithWorkingDir(workDir)
+       b.WithViper(v)
+       b.WithContent("p1.md", "")
+
+       jsDir := filepath.Join(workDir, "assets", "js")
+       b.Assert(os.MkdirAll(jsDir, 0777), qt.IsNil)
+       b.Assert(os.Chdir(workDir), qt.IsNil)
+
+       b.WithTemplates("index.html", `
+{{- $js := resources.Get "js/main-project.js" | js.Build -}}
+{{ template "print" (dict "js" $js "name" "root") }}
+
+{{- define "print" -}}
+{{ printf "rellink-%s-%s" .name .js.RelPermalink | safeHTML }}
+{{ printf "mime-%s-%s" .name .js.MediaType | safeHTML }}
+{{ printf "content-%s-%s" .name .js.Content | safeHTML }}
+{{- end -}}
+`)
+
+       b.WithSourceFile("assets/js/normal.js", `
+const name = "root-normal";
+export default name;
+`)
+       b.WithSourceFile("assets/js/main-project.js", `
+import normal from "@js/normal";
+window.normal = normal; // make sure not to tree-shake
+`)
+
+       b.Build(BuildCfg{})
+
+       b.AssertFileContent("public/index.html", `
+const name = "root-normal";
+`)
+}
+
+func TestJSBuildOverride(t *testing.T) {
+       if !isCI() {
+               t.Skip("skip (relative) long running modules test when running locally")
+       }
+
+       wd, _ := os.Getwd()
+       defer func() {
+               os.Chdir(wd)
+       }()
+
+       c := qt.New(t)
+
+       workDir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugo-test-js2")
+       c.Assert(err, qt.IsNil)
+       defer clean()
+       // workDir := "/tmp/hugo-test-js2"
+       c.Assert(os.Chdir(workDir), qt.IsNil)
+
+       cfg := viper.New()
+       cfg.Set("workingDir", workDir)
+       fs := hugofs.NewFrom(afero.NewOsFs(), cfg)
+
+       b := newTestSitesBuilder(t)
+       b.Fs = fs
+       b.WithLogger(loggers.NewWarningLogger())
+
+       realWrite := func(name string, content string) {
+               realLocation := filepath.Join(workDir, name)
+               realDir := filepath.Dir(realLocation)
+               if _, err := os.Stat(realDir); err != nil {
+                       os.MkdirAll(realDir, 0777)
+               }
+               bytesContent := []byte(content)
+               // c.Assert(ioutil.WriteFile(realLocation, bytesContent, 0777), qt.IsNil)
+               c.Assert(afero.WriteFile(b.Fs.Source, realLocation, bytesContent, 0777), qt.IsNil)
+       }
+
+       realWrite("config.toml", `
+baseURL="https://example.org"
+
+[module]
+[[module.imports]]
+path="mod2"
+[[module.imports.mounts]]
+source="assets"
+target="assets"
+[[module.imports.mounts]]
+source="layouts"
+target="layouts"
+[[module.imports]]
+path="mod1"
+[[module.imports.mounts]]
+source="assets"
+target="assets"
+[[module.imports.mounts]]
+source="layouts"
+target="layouts"
+`)
+
+       realWrite("content/p1.md", `---
+layout: sample
+---
+`)
+       realWrite("themes/mod1/layouts/_default/sample.html", `
+{{- $js := resources.Get "js/main-project.js" | js.Build -}}
+{{ template "print" (dict "js" $js "name" "root") }}
+
+{{- $js = resources.Get "js/main-mod1.js" | js.Build -}}
+{{ template "print" (dict "js" $js "name" "mod1") }}
+
+{{- $js = resources.Get "js/main-mod2.js" | js.Build (dict "data" .Site.Params) -}}
+{{ template "print" (dict "js" $js "name" "mod2") }}
+
+{{- $js = resources.Get "js/main-mod2.js" | js.Build (dict "data" .Site.Params "sourceMap" "inline" "targetPath" "js/main-mod2-inline.js") -}}
+{{ template "print" (dict "js" $js "name" "mod2") }}
+
+{{- $js = resources.Get "js/main-mod2.js" | js.Build (dict "data" .Site.Params "sourceMap" "external" "targetPath" "js/main-mod2-external.js") -}}
+{{ template "print" (dict "js" $js "name" "mod2") }}
+
+{{- define "print" -}}
+{{ printf "rellink-%s-%s" .name .js.RelPermalink | safeHTML }}
+{{ printf "mime-%s-%s" .name .js.MediaType | safeHTML }}
+{{ printf "content-%s-%s" .name .js.Content | safeHTML }}
+{{- end -}}
+`)
+
+       // Override project included file
+       // This file will override the one in mod1 and mod2
+       realWrite("assets/js/override.js", `
+const name = "root-override";
+export default name;
+`)
+
+       // Add empty theme mod config files
+       realWrite("themes/mod1/config.yml", ``)
+       realWrite("themes/mod2/config.yml", ``)
+
+       // This is the main project js file.
+       // try to include @js/override which is overridden inside of project
+       // try to include @js/override-mod which is overridden in mod2
+       realWrite("assets/js/main-project.js", `
+import override from "@js/override";
+import overrideMod from "@js/override-mod";
+window.override = override; // make sure to prevent tree-shake
+window.overrideMod  = overrideMod; // make sure to prevent tree-shake
+`)
+       // This is the mod1 js file
+       // try to include @js/override which is overridden inside of the project
+       // try to include @js/override-mod which is overridden in mod2
+       realWrite("themes/mod1/assets/js/main-mod1.js", `
+import override from "@js/override";
+import overrideMod from "@js/override-mod";
+window.mod = "mod1";
+window.override = override; // make sure to prevent tree-shake
+window.overrideMod  = overrideMod; // make sure to prevent tree-shake
+`)
+       // This is the mod1 js file (overridden in mod2)
+       // try to include @js/override which is overridden inside of the project
+       // try to include @js/override-mod which is overridden in mod2
+       realWrite("themes/mod2/assets/js/main-mod1.js", `
+import override from "@js/override";
+import overrideMod from "@js/override-mod";
+window.mod = "mod2";
+window.override = override; // make sure to prevent tree-shake
+window.overrideMod  = overrideMod; // make sure to prevent tree-shake
+`)
+       // This is mod2 js file
+       // try to include @js/override which is overridden inside of the project
+       // try to include @js/override-mod which is overridden in mod2
+       // try to include @config which is declared in a local jsconfig.json file
+       // try to include @data which was passed as "data" into js.Build
+       realWrite("themes/mod2/assets/js/main-mod2.js", `
+import override from "@js/override";
+import overrideMod from "@js/override-mod";
+import config from "@config";
+import data from "@data";
+window.data = data;
+window.override = override; // make sure to prevent tree-shake
+window.overrideMod  = overrideMod; // make sure to prevent tree-shake
+window.config = config;
+`)
+       realWrite("themes/mod2/assets/js/jsconfig.json", `
+{
+       "compilerOptions": {
+               "baseUrl": ".",
+               "paths": {
+                       "@config": ["./config.json"]
+               }
+       }
+}
+`)
+       realWrite("themes/mod2/assets/js/config.json", `
+{
+       "data": {
+               "sample": "sample"
+       }
+}
+`)
+       realWrite("themes/mod1/assets/js/override.js", `
+const name = "mod1-override";
+export default name;
+`)
+       realWrite("themes/mod2/assets/js/override.js", `
+const name = "mod2-override";
+export default name;
+`)
+       realWrite("themes/mod1/assets/js/override-mod.js", `
+const nameMod = "mod1-override";
+export default nameMod;
+`)
+       realWrite("themes/mod2/assets/js/override-mod.js", `
+const nameMod = "mod2-override";
+export default nameMod;
+`)
+       b.WithConfigFile("toml", `
+baseURL="https://example.org"
+themesDir="./themes"
+[module]
+[[module.imports]]
+path="mod2"
+[[module.imports.mounts]]
+source="assets"
+target="assets"
+[[module.imports.mounts]]
+source="layouts"
+target="layouts"
+[[module.imports]]
+path="mod1"
+[[module.imports.mounts]]
+source="assets"
+target="assets"
+[[module.imports.mounts]]
+source="layouts"
+target="layouts"
+`)
+
+       b.WithWorkingDir(workDir)
+       b.LoadConfig()
+
+       b.Build(BuildCfg{})
+
+       b.AssertFileContent("public/js/main-mod1.js", `
+name = "root-override";
+nameMod = "mod2-override";
+window.mod = "mod2";
+`)
+       b.AssertFileContent("public/js/main-mod2.js", `
+name = "root-override";
+nameMod = "mod2-override";
+sample: "sample"
+"sect"
+`)
+       b.AssertFileContent("public/js/main-project.js", `
+name = "root-override";
+nameMod = "mod2-override";
+`)
+       b.AssertFileContent("public/js/main-mod2-external.js.map", `
+const nameMod = \"mod2-override\";\nexport default nameMod;\n
+"\nimport override from \"@js/override\";\nimport overrideMod from \"@js/override-mod\";\nimport config from \"@config\";\nimport data from \"@data\";\nwindow.data = data;\nwindow.override = override; // make sure to prevent tree-shake\nwindow.overrideMod  = overrideMod; // make sure to prevent tree-shake\nwindow.config = config;\n"
+`)
+       b.AssertFileContent("public/js/main-mod2-inline.js", `
+       sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsiYXNzZXRzL2pzL292ZXJyaWRlLmpzIiwgInRoZW
+`)
+}
index e4b4b1c2085ba0bcdb54abe825b03b209ec8bb6d..d316bc85b6496a75a5e5e11b04b34905f83cbd81 100644 (file)
 package js
 
 import (
+       "encoding/json"
        "fmt"
        "io/ioutil"
+       "os"
        "path"
+       "path/filepath"
+       "reflect"
        "strings"
 
+       "github.com/achiku/varfmt"
        "github.com/spf13/cast"
 
        "github.com/gohugoio/hugo/helpers"
@@ -33,6 +38,7 @@ import (
        "github.com/gohugoio/hugo/resources/resource"
 )
 
+// Options esbuild configuration
 type Options struct {
        // If not set, the source path will be used as the base target path.
        // Note that the target path's extension may change if the target MIME type
@@ -42,7 +48,7 @@ type Options struct {
        // Whether to minify to output.
        Minify bool
 
-       // Whether to write mapfiles (currently inline only)
+       // Whether to write mapfiles
        SourceMap string
 
        // The language target.
@@ -61,6 +67,9 @@ type Options struct {
        // User defined symbols.
        Defines map[string]interface{}
 
+       // User defined data (must be JSON marshall'able)
+       Data interface{}
+
        // What to use instead of React.createElement.
        JSXFactory string
 
@@ -72,6 +81,8 @@ type Options struct {
        contents   string
        sourcefile string
        resolveDir string
+       workDir    string
+       tsConfig   string
 }
 
 func decodeOptions(m map[string]interface{}) (Options, error) {
@@ -91,11 +102,13 @@ func decodeOptions(m map[string]interface{}) (Options, error) {
        return opts, nil
 }
 
+// Client context for esbuild
 type Client struct {
        rs  *resources.Spec
        sfs *filesystems.SourceFilesystem
 }
 
+// New create new client context
 func New(fs *filesystems.SourceFilesystem, rs *resources.Spec) *Client {
        return &Client{rs: rs, sfs: fs}
 }
@@ -110,6 +123,13 @@ func (t *buildTransformation) Key() internal.ResourceTransformationKey {
        return internal.NewResourceTransformationKey("jsbuild", t.optsm)
 }
 
+func appendExts(list []string, rel string) []string {
+       for _, ext := range []string{".tsx", ".ts", ".jsx", ".mjs", ".cjs", ".js", ".json"} {
+               list = append(list, fmt.Sprintf("%s/index%s", rel, ext))
+       }
+       return list
+}
+
 func (t *buildTransformation) Transform(ctx *resources.ResourceTransformationCtx) error {
        ctx.OutMediaType = media.JavascriptType
 
@@ -129,25 +149,345 @@ func (t *buildTransformation) Transform(ctx *resources.ResourceTransformationCtx
                return err
        }
 
-       sdir, sfile := path.Split(ctx.SourcePath)
+       sdir, sfile := filepath.Split(t.sfs.RealFilename(ctx.SourcePath))
+       opts.workDir, err = filepath.Abs(t.rs.WorkingDir)
+       if err != nil {
+               return err
+       }
+
        opts.sourcefile = sfile
-       opts.resolveDir = t.sfs.RealFilename(sdir)
+       opts.resolveDir = sdir
        opts.contents = string(src)
        opts.mediaType = ctx.InMediaType
 
+       // Create new temporary tsconfig file
+       newTSConfig, err := ioutil.TempFile("", "tsconfig.*.json")
+       if err != nil {
+               return err
+       }
+
+       filesToDelete := make([]*os.File, 0)
+
+       defer func() {
+               for _, file := range filesToDelete {
+                       os.Remove(file.Name())
+               }
+       }()
+
+       filesToDelete = append(filesToDelete, newTSConfig)
+       configDir, _ := filepath.Split(newTSConfig.Name())
+
+       // Search for the innerMost tsconfig or jsconfig
+       innerTsConfig := ""
+       tsDir := opts.resolveDir
+       baseURLAbs := configDir
+       baseURL := "."
+       for tsDir != "." {
+               tryTsConfig := path.Join(tsDir, "tsconfig.json")
+               _, err := os.Stat(tryTsConfig)
+               if err != nil {
+                       tryTsConfig := path.Join(tsDir, "jsconfig.json")
+                       _, err = os.Stat(tryTsConfig)
+                       if err == nil {
+                               innerTsConfig = tryTsConfig
+                               baseURLAbs = tsDir
+                               break
+                       }
+               } else {
+                       innerTsConfig = tryTsConfig
+                       baseURLAbs = tsDir
+                       break
+               }
+               if tsDir == opts.workDir {
+                       break
+               }
+               tsDir = path.Dir(tsDir)
+       }
+
+       // Resolve paths for @assets and @js (@js is just an alias for assets/js)
+       dirs := make([]string, 0)
+       rootPaths := make([]string, 0)
+       for _, dir := range t.sfs.RealDirs(".") {
+               rootDir := dir
+               if !strings.HasSuffix(dir, "package.json") {
+                       dirs = append(dirs, dir)
+               } else {
+                       rootDir, _ = path.Split(dir)
+               }
+               nodeModules := path.Join(rootDir, "node_modules")
+               if _, err := os.Stat(nodeModules); err == nil {
+                       rootPaths = append(rootPaths, nodeModules)
+               }
+       }
+
+       // Construct new temporary tsconfig file content
+       config := make(map[string]interface{})
+       if innerTsConfig != "" {
+               oldConfig, err := ioutil.ReadFile(innerTsConfig)
+               if err == nil {
+                       // If there is an error, it just means there is no config file here.
+                       // Since we're also using the tsConfig file path to detect where
+                       // to put the temp file, this is ok.
+                       err = json.Unmarshal(oldConfig, &config)
+                       if err != nil {
+                               return err
+                       }
+               }
+       }
+
+       if config["compilerOptions"] == nil {
+               config["compilerOptions"] = map[string]interface{}{}
+       }
+
+       // Assign new global paths to the config file while reading existing ones.
+       compilerOptions := config["compilerOptions"].(map[string]interface{})
+
+       // Handle original baseUrl if it's there
+       if compilerOptions["baseUrl"] != nil {
+               baseURL = compilerOptions["baseUrl"].(string)
+               oldBaseURLAbs := path.Join(tsDir, baseURL)
+               rel, _ := filepath.Rel(configDir, oldBaseURLAbs)
+               configDir = oldBaseURLAbs
+               baseURLAbs = configDir
+               if "/" != helpers.FilePathSeparator {
+                       // On windows we need to use slashes instead of backslash
+                       rel = strings.ReplaceAll(rel, helpers.FilePathSeparator, "/")
+               }
+               if rel != "" {
+                       if strings.HasPrefix(rel, ".") {
+                               baseURL = rel
+                       } else {
+                               baseURL = fmt.Sprintf("./%s", rel)
+                       }
+               }
+               compilerOptions["baseUrl"] = baseURL
+       } else {
+               compilerOptions["baseUrl"] = baseURL
+       }
+
+       jsRel := func(refPath string) string {
+               rel, _ := filepath.Rel(configDir, refPath)
+               if "/" != helpers.FilePathSeparator {
+                       // On windows we need to use slashes instead of backslash
+                       rel = strings.ReplaceAll(rel, helpers.FilePathSeparator, "/")
+               }
+               if rel != "" {
+                       if !strings.HasPrefix(rel, ".") {
+                               rel = fmt.Sprintf("./%s", rel)
+                       }
+               } else {
+                       rel = "."
+               }
+               return rel
+       }
+
+       // Handle possible extends
+       if config["extends"] != nil {
+               extends := config["extends"].(string)
+               extendsAbs := path.Join(tsDir, extends)
+               rel := jsRel(extendsAbs)
+               config["extends"] = rel
+       }
+
+       var optionsPaths map[string]interface{}
+       // Get original paths if they exist
+       if compilerOptions["paths"] != nil {
+               optionsPaths = compilerOptions["paths"].(map[string]interface{})
+       } else {
+               optionsPaths = make(map[string]interface{})
+       }
+       compilerOptions["paths"] = optionsPaths
+
+       assets := make([]string, 0)
+       assetsExact := make([]string, 0)
+       js := make([]string, 0)
+       jsExact := make([]string, 0)
+       for _, dir := range dirs {
+               rel := jsRel(dir)
+               assets = append(assets, fmt.Sprintf("%s/*", rel))
+               assetsExact = appendExts(assetsExact, rel)
+
+               rel = jsRel(filepath.Join(dir, "js"))
+               js = append(js, fmt.Sprintf("%s/*", rel))
+               jsExact = appendExts(jsExact, rel)
+       }
+
+       optionsPaths["@assets/*"] = assets
+       optionsPaths["@js/*"] = js
+
+       // Make @js and @assets absolue matches search for index files
+       // to get around the problem in ESBuild resolving folders as index files.
+       optionsPaths["@assets"] = assetsExact
+       optionsPaths["@js"] = jsExact
+
+       var newDataFile *os.File
+       if opts.Data != nil {
+               // Create a data file
+               lines := make([]string, 0)
+               lines = append(lines, "// auto generated data import")
+               exports := make([]string, 0)
+               keys := make(map[string]bool)
+
+               var bytes []byte
+
+               conv := reflect.ValueOf(opts.Data)
+               convType := conv.Kind()
+               if convType == reflect.Interface {
+                       if conv.IsNil() {
+                               conv = reflect.Value{}
+                       }
+               }
+
+               if conv.Kind() != reflect.Map {
+                       // Write out as single JSON file
+                       newDataFile, err = ioutil.TempFile("", "data.*.json")
+                       // Output the data
+                       bytes, err = json.MarshalIndent(conv.InterfaceData(), "", "  ")
+                       if err != nil {
+                               return err
+                       }
+               } else {
+                       // Try to allow tree shaking at the root
+                       newDataFile, err = ioutil.TempFile(configDir, "data.*.js")
+                       for _, key := range conv.MapKeys() {
+                               strKey := key.Interface().(string)
+                               if keys[strKey] {
+                                       continue
+                               }
+                               keys[strKey] = true
+
+                               value := conv.MapIndex(key)
+
+                               keyVar := varfmt.PublicVarName(strKey)
+
+                               // Output the data
+                               bytes, err := json.MarshalIndent(value.Interface(), "", "  ")
+                               if err != nil {
+                                       return err
+                               }
+                               jsonValue := string(bytes)
+
+                               lines = append(lines, fmt.Sprintf("export const %s = %s;", keyVar, jsonValue))
+                               exports = append(exports, fmt.Sprintf("  %s,", keyVar))
+                               if strKey != keyVar {
+                                       exports = append(exports, fmt.Sprintf("  [\"%s\"]: %s,", strKey, keyVar))
+                               }
+                       }
+
+                       lines = append(lines, "const all = {")
+                       for _, line := range exports {
+                               lines = append(lines, line)
+                       }
+                       lines = append(lines, "};")
+                       lines = append(lines, "export default all;")
+
+                       bytes = []byte(strings.Join(lines, "\n"))
+               }
+
+               // Write tsconfig file
+               _, err = newDataFile.Write(bytes)
+               if err != nil {
+                       return err
+               }
+               err = newDataFile.Close()
+               if err != nil {
+                       return err
+               }
+
+               // Link this file into `import data from "@data"`
+               dataFiles := make([]string, 1)
+               rel, _ := filepath.Rel(baseURLAbs, newDataFile.Name())
+               dataFiles[0] = rel
+               optionsPaths["@data"] = dataFiles
+
+               filesToDelete = append(filesToDelete, newDataFile)
+       }
+
+       if len(rootPaths) > 0 {
+               // This will allow import "react" to resolve a react module that's
+               // either in the root node_modules or in one of the hugo mods.
+               optionsPaths["*"] = rootPaths
+       }
+
+       // Output the new config file
+       bytes, err := json.MarshalIndent(config, "", "  ")
+       if err != nil {
+               return err
+       }
+
+       // Write tsconfig file
+       _, err = newTSConfig.Write(bytes)
+       if err != nil {
+               return err
+       }
+       err = newTSConfig.Close()
+       if err != nil {
+               return err
+       }
+
+       // Tell ESBuild about this new config file to use
+       opts.tsConfig = newTSConfig.Name()
+
        buildOptions, err := toBuildOptions(opts)
        if err != nil {
+               os.Remove(opts.tsConfig)
                return err
        }
 
        result := api.Build(buildOptions)
+
+       if len(result.Warnings) > 0 {
+               for _, value := range result.Warnings {
+                       if value.Location != nil {
+                               t.rs.Logger.WARN.Println(fmt.Sprintf("%s:%d: WARN: %s",
+                                       filepath.Join(sdir, value.Location.File),
+                                       value.Location.Line, value.Text))
+                               t.rs.Logger.WARN.Println("  ", value.Location.LineText)
+                       } else {
+                               t.rs.Logger.WARN.Println(fmt.Sprintf("%s: WARN: %s",
+                                       sdir,
+                                       value.Text))
+                       }
+               }
+       }
        if len(result.Errors) > 0 {
-               return fmt.Errorf("%s", result.Errors[0].Text)
+               output := result.Errors[0].Text
+               for _, value := range result.Errors {
+                       var line string
+                       if value.Location != nil {
+                               line = fmt.Sprintf("%s:%d ERROR: %s",
+                                       filepath.Join(sdir, value.Location.File),
+                                       value.Location.Line, value.Text)
+                       } else {
+                               line = fmt.Sprintf("%s ERROR: %s",
+                                       sdir,
+                                       value.Text)
+                       }
+                       t.rs.Logger.ERROR.Println(line)
+                       output = fmt.Sprintf("%s\n%s", output, line)
+                       if value.Location != nil {
+                               t.rs.Logger.ERROR.Println("  ", value.Location.LineText)
+                       }
+               }
+               return fmt.Errorf("%s", output)
+       }
+
+       if buildOptions.Outfile != "" {
+               _, tfile := path.Split(opts.TargetPath)
+               output := fmt.Sprintf("%s//# sourceMappingURL=%s\n",
+                       string(result.OutputFiles[1].Contents), tfile+".map")
+               _, err := ctx.To.Write([]byte(output))
+               if err != nil {
+                       return err
+               }
+               ctx.PublishSourceMap(string(result.OutputFiles[0].Contents))
+       } else {
+               ctx.To.Write(result.OutputFiles[0].Contents)
        }
-       ctx.To.Write(result.OutputFiles[0].Contents)
        return nil
 }
 
+// Process process esbuild transform
 func (c *Client) Process(res resources.ResourceTransformer, opts map[string]interface{}) (resource.Resource, error) {
        return res.Transform(
                &buildTransformation{rs: c.rs, sfs: c.sfs, optsm: opts},
@@ -212,7 +552,6 @@ func toBuildOptions(opts Options) (buildOptions api.BuildOptions, err error) {
        default:
                err = fmt.Errorf("unsupported script output format: %q", opts.Format)
                return
-
        }
 
        var defines map[string]string
@@ -220,10 +559,19 @@ func toBuildOptions(opts Options) (buildOptions api.BuildOptions, err error) {
                defines = cast.ToStringMapString(opts.Defines)
        }
 
+       // By default we only need to specify outDir and no outFile
+       var outDir = opts.outDir
+       var outFile = ""
        var sourceMap api.SourceMap
        switch opts.SourceMap {
        case "inline":
                sourceMap = api.SourceMapInline
+       case "external":
+               // When doing external sourcemaps we should specify
+               // out file and no out dir
+               sourceMap = api.SourceMapExternal
+               outFile = filepath.Join(opts.workDir, opts.TargetPath)
+               outDir = ""
        case "":
                sourceMap = api.SourceMapNone
        default:
@@ -232,7 +580,7 @@ func toBuildOptions(opts Options) (buildOptions api.BuildOptions, err error) {
        }
 
        buildOptions = api.BuildOptions{
-               Outfile: "",
+               Outfile: outFile,
                Bundle:  true,
 
                Target:    target,
@@ -243,7 +591,7 @@ func toBuildOptions(opts Options) (buildOptions api.BuildOptions, err error) {
                MinifyIdentifiers: opts.Minify,
                MinifySyntax:      opts.Minify,
 
-               Outdir:  opts.outDir,
+               Outdir:  outDir,
                Defines: defines,
 
                Externals: opts.Externals,
@@ -251,7 +599,7 @@ func toBuildOptions(opts Options) (buildOptions api.BuildOptions, err error) {
                JSXFactory:  opts.JSXFactory,
                JSXFragment: opts.JSXFragment,
 
-               //Tsconfig: opts.TSConfig,
+               Tsconfig: opts.tsConfig,
 
                Stdin: &api.StdinOptions{
                        Contents:   opts.contents,
index c04c0ed12b79c167f17994f7d1b16cf1f18506d5..8839c646e74f19c043ebc03a1cd1c9be613d41d6 100644 (file)
@@ -77,4 +77,20 @@ func TestToBuildOptions(t *testing.T) {
                Sourcemap:         api.SourceMapInline,
                Stdin:             &api.StdinOptions{},
        })
+
+       opts, err = toBuildOptions(Options{
+               Target: "es2018", Format: "cjs", Minify: true, mediaType: media.JavascriptType,
+               SourceMap: "external"})
+       c.Assert(err, qt.IsNil)
+       c.Assert(opts, qt.DeepEquals, api.BuildOptions{
+               Bundle:            true,
+               Target:            api.ES2018,
+               Format:            api.FormatCommonJS,
+               MinifyIdentifiers: true,
+               MinifySyntax:      true,
+               MinifyWhitespace:  true,
+               Sourcemap:         api.SourceMapExternal,
+               Stdin:             &api.StdinOptions{},
+       })
+
 }