pipes: Add external source map support to js.Build and Babel
authorAndreas Richter <richtera@users.noreply.github.com>
Mon, 18 Jan 2021 09:38:09 +0000 (04:38 -0500)
committerGitHub <noreply@github.com>
Mon, 18 Jan 2021 09:38:09 +0000 (10:38 +0100)
Fixes #8132

hugolib/js_test.go
hugolib/resource_chain_babel_test.go
resources/resource_transformers/babel/babel.go
resources/resource_transformers/js/build.go
resources/resource_transformers/js/options.go
resources/resource_transformers/js/options_test.go

index a1b74a871a234cb18595655867843c4fae765b85..145a057c131b05d2dae45a14156665b062c29385 100644 (file)
@@ -109,14 +109,16 @@ document.body.textContent = greeter(user);`
 JS:  {{ template "print" $js }}
 {{ $jsx := resources.Get "js/myjsx.jsx" | js.Build $options }}
 JSX: {{ template "print" $jsx }}
-{{ $ts := resources.Get "js/myts.ts" | js.Build }}
+{{ $ts := resources.Get "js/myts.ts" | js.Build (dict "sourcemap" "inline")}}
 TS: {{ template "print" $ts }}
-
+{{ $ts2 := resources.Get "js/myts.ts" | js.Build (dict "sourcemap" "external" "TargetPath" "js/myts2.js")}}
+TS2: {{ template "print" $ts2 }}
 {{ define "print" }}RelPermalink: {{.RelPermalink}}|MIME: {{ .MediaType }}|Content: {{ .Content | safeJS }}{{ end }}
 
 `)
 
        jsDir := filepath.Join(workDir, "assets", "js")
+       fmt.Println(workDir)
        b.Assert(os.MkdirAll(jsDir, 0777), qt.IsNil)
        b.Assert(os.Chdir(workDir), qt.IsNil)
        b.WithSourceFile("package.json", packageJSON)
@@ -133,6 +135,8 @@ TS: {{ template "print" $ts }}
 
        b.Build(BuildCfg{})
 
+       b.AssertFileContent("public/js/myts.js", `//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJz`)
+       b.AssertFileContent("public/js/myts2.js.map", `"version": 3,`)
        b.AssertFileContent("public/index.html", `
 console.log(&#34;included&#34;);
 if (hasSpace.test(string))
index d5e99cd17eb8ec53b54f631c9f644f86b3663eaa..77c81f9cae2e2df423990c4778468874ee0f6497 100644 (file)
@@ -78,6 +78,15 @@ class Car {
     this.carname = brand;
   }
 }
+`
+
+       js2 := `
+/* A Car2 */
+class Car2 {
+  constructor(brand) {
+    this.carname = brand;
+  }
+}
 `
 
        workDir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugo-test-babel")
@@ -103,11 +112,18 @@ class Car {
 {{ $transpiled := resources.Get "js/main.js" | babel -}}
 Transpiled: {{ $transpiled.Content | safeJS }}
 
+{{ $transpiled := resources.Get "js/main2.js" | babel (dict "sourceMap" "inline") -}}
+Transpiled2: {{ $transpiled.Content | safeJS }}
+
+{{ $transpiled := resources.Get "js/main2.js" | babel (dict "sourceMap" "external") -}}
+Transpiled3: {{ $transpiled.Permalink }}
+
 `)
 
        jsDir := filepath.Join(workDir, "assets", "js")
        b.Assert(os.MkdirAll(jsDir, 0777), qt.IsNil)
        b.WithSourceFile("assets/js/main.js", js)
+       b.WithSourceFile("assets/js/main2.js", js2)
        b.WithSourceFile("package.json", packageJSON)
        b.WithSourceFile("babel.config.js", babelConfig)
 
@@ -129,4 +145,21 @@ var Car = function Car(brand) {
  this.carname = brand;
 };
 `)
+       b.AssertFileContent("public/index.html", `
+var Car2 = function Car2(brand) {
+ _classCallCheck(this, Car2);
+
+ this.carname = brand;
+};
+`)
+       b.AssertFileContent("public/js/main2.js", `
+var Car2 = function Car2(brand) {
+ _classCallCheck(this, Car2);
+
+ this.carname = brand;
+};
+`)
+       b.AssertFileContent("public/js/main2.js.map", `{"version":3,`)
+       b.AssertFileContent("public/index.html", `
+//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozL`)
 }
index 2041537056f609bef05da061c68debfb27c59793..e291b210b030559b54d5df970759b0514d75244e 100644 (file)
@@ -16,7 +16,11 @@ package babel
 import (
        "bytes"
        "io"
+       "io/ioutil"
+       "os"
+       "path"
        "path/filepath"
+       "regexp"
        "strconv"
 
        "github.com/cli/safeexec"
@@ -43,8 +47,10 @@ type Options struct {
        Compact    *bool
        Verbose    bool
        NoBabelrc  bool
+       SourceMap  string
 }
 
+// DecodeOptions decodes options to and generates command flags
 func DecodeOptions(m map[string]interface{}) (opts Options, err error) {
        if m == nil {
                return
@@ -56,6 +62,14 @@ func DecodeOptions(m map[string]interface{}) (opts Options, err error) {
 func (opts Options) toArgs() []string {
        var args []string
 
+       // external is not a known constant on the babel command line
+       // .sourceMaps must be a boolean, "inline", "both", or undefined
+       switch opts.SourceMap {
+       case "external":
+               args = append(args, "--source-maps")
+       case "inline":
+               args = append(args, "--source-maps=inline")
+       }
        if opts.Minified {
                args = append(args, "--minified")
        }
@@ -141,6 +155,8 @@ func (t *babelTransformation) Transform(ctx *resources.ResourceTransformationCtx
                }
        }
 
+       ctx.ReplaceOutPathExtension(".js")
+
        var cmdArgs []string
 
        if configFile != "" {
@@ -153,13 +169,24 @@ func (t *babelTransformation) Transform(ctx *resources.ResourceTransformationCtx
        }
        cmdArgs = append(cmdArgs, "--filename="+ctx.SourcePath)
 
+       // Create compile into a real temp file:
+       // 1. separate stdout/stderr messages from babel (https://github.com/gohugoio/hugo/issues/8136)
+       // 2. allow generation and retrieval of external source map.
+       compileOutput, err := ioutil.TempFile("", "compileOut-*.js")
+       if err != nil {
+               return err
+       }
+
+       cmdArgs = append(cmdArgs, "--out-file="+compileOutput.Name())
+       defer os.Remove(compileOutput.Name())
+
        cmd, err := hexec.SafeCommand(binary, cmdArgs...)
        if err != nil {
                return err
        }
 
-       cmd.Stdout = ctx.To
        cmd.Stderr = io.MultiWriter(infoW, &errBuf)
+       cmd.Stdout = cmd.Stderr
        cmd.Env = hugo.GetExecEnviron(t.rs.WorkingDir, t.rs.Cfg, t.rs.BaseFs.Assets.Fs)
 
        stdin, err := cmd.StdinPipe()
@@ -177,6 +204,28 @@ func (t *babelTransformation) Transform(ctx *resources.ResourceTransformationCtx
                return errors.Wrap(err, errBuf.String())
        }
 
+       content, err := ioutil.ReadAll(compileOutput)
+       if err != nil {
+               return err
+       }
+
+       mapFile := compileOutput.Name() + ".map"
+       if _, err := os.Stat(mapFile); err == nil {
+               defer os.Remove(mapFile)
+               sourceMap, err := ioutil.ReadFile(mapFile)
+               if err != nil {
+                       return err
+               }
+               if err = ctx.PublishSourceMap(string(sourceMap)); err != nil {
+                       return err
+               }
+               targetPath := path.Base(ctx.OutPath) + ".map"
+               re := regexp.MustCompile(`//# sourceMappingURL=.*\n?`)
+               content = []byte(re.ReplaceAllString(string(content), "//# sourceMappingURL="+targetPath+"\n"))
+       }
+
+       ctx.To.Write(content)
+
        return nil
 }
 
index 5ff21cf02df6bbdbe79cd6672ca5bb2aaa2195c4..0d70bdc337f2dad20c8dc6350a5c094e05c8de50 100644 (file)
@@ -18,6 +18,8 @@ import (
        "fmt"
        "io/ioutil"
        "os"
+       "path"
+       "regexp"
        "strings"
 
        "github.com/spf13/afero"
@@ -92,6 +94,14 @@ func (t *buildTransformation) Transform(ctx *resources.ResourceTransformationCtx
                return err
        }
 
+       if buildOptions.Sourcemap == api.SourceMapExternal && buildOptions.Outdir == "" {
+               buildOptions.Outdir, err = ioutil.TempDir(os.TempDir(), "compileOutput")
+               if err != nil {
+                       return err
+               }
+               defer os.Remove(buildOptions.Outdir)
+       }
+
        result := api.Build(buildOptions)
 
        if len(result.Errors) > 0 {
@@ -145,7 +155,25 @@ func (t *buildTransformation) Transform(ctx *resources.ResourceTransformationCtx
                return errors[0]
        }
 
-       ctx.To.Write(result.OutputFiles[0].Contents)
+       if buildOptions.Sourcemap == api.SourceMapExternal {
+               content := string(result.OutputFiles[1].Contents)
+               symPath := path.Base(ctx.OutPath) + ".map"
+               re := regexp.MustCompile(`//# sourceMappingURL=.*\n?`)
+               content = re.ReplaceAllString(content, "//# sourceMappingURL="+symPath+"\n")
+
+               if err = ctx.PublishSourceMap(string(result.OutputFiles[0].Contents)); err != nil {
+                       return err
+               }
+               _, err := ctx.To.Write([]byte(content))
+               if err != nil {
+                       return err
+               }
+       } else {
+               _, err := ctx.To.Write(result.OutputFiles[0].Contents)
+               if err != nil {
+                       return err
+               }
+       }
        return nil
 }
 
index 75daa0cadbcacf8eeb8d0a371d05b88d57a16cbb..5236fe126da3f019f6a30cba28580c2c9f21762f 100644 (file)
@@ -338,6 +338,8 @@ func toBuildOptions(opts Options) (buildOptions api.BuildOptions, err error) {
        switch opts.SourceMap {
        case "inline":
                sourceMap = api.SourceMapInline
+       case "external":
+               sourceMap = api.SourceMapExternal
        case "":
                sourceMap = api.SourceMapNone
        default:
index f07ccc26b3e7ab743f3bbd0a384653306f3a5c96..f425c3e75ba229b1ed672a6e663b3cadc77be58f 100644 (file)
@@ -109,4 +109,22 @@ func TestToBuildOptions(t *testing.T) {
                        Loader: api.LoaderJS,
                },
        })
+
+       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{
+                       Loader: api.LoaderJS,
+               },
+       })
 }