From 2c8b5d9165011c4b24b494e661ae60dfc7bb7d1b Mon Sep 17 00:00:00 2001 From: Andreas Richter Date: Mon, 18 Jan 2021 04:38:09 -0500 Subject: [PATCH] pipes: Add external source map support to js.Build and Babel Fixes #8132 --- hugolib/js_test.go | 8 ++- hugolib/resource_chain_babel_test.go | 33 ++++++++++++ .../resource_transformers/babel/babel.go | 51 ++++++++++++++++++- resources/resource_transformers/js/build.go | 30 ++++++++++- resources/resource_transformers/js/options.go | 2 + .../resource_transformers/js/options_test.go | 18 +++++++ 6 files changed, 138 insertions(+), 4 deletions(-) diff --git a/hugolib/js_test.go b/hugolib/js_test.go index a1b74a87..145a057c 100644 --- a/hugolib/js_test.go +++ b/hugolib/js_test.go @@ -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("included"); if (hasSpace.test(string)) diff --git a/hugolib/resource_chain_babel_test.go b/hugolib/resource_chain_babel_test.go index d5e99cd1..77c81f9c 100644 --- a/hugolib/resource_chain_babel_test.go +++ b/hugolib/resource_chain_babel_test.go @@ -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`) } diff --git a/resources/resource_transformers/babel/babel.go b/resources/resource_transformers/babel/babel.go index 20415370..e291b210 100644 --- a/resources/resource_transformers/babel/babel.go +++ b/resources/resource_transformers/babel/babel.go @@ -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 } diff --git a/resources/resource_transformers/js/build.go b/resources/resource_transformers/js/build.go index 5ff21cf0..0d70bdc3 100644 --- a/resources/resource_transformers/js/build.go +++ b/resources/resource_transformers/js/build.go @@ -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 } diff --git a/resources/resource_transformers/js/options.go b/resources/resource_transformers/js/options.go index 75daa0ca..5236fe12 100644 --- a/resources/resource_transformers/js/options.go +++ b/resources/resource_transformers/js/options.go @@ -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: diff --git a/resources/resource_transformers/js/options_test.go b/resources/resource_transformers/js/options_test.go index f07ccc26..f425c3e7 100644 --- a/resources/resource_transformers/js/options_test.go +++ b/resources/resource_transformers/js/options_test.go @@ -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, + }, + }) } -- 2.30.2