js.Build: Generate tsconfig files

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.
This commit is contained in:
Andreas Richter 2020-09-12 00:19:36 -04:00 committed by Bjørn Erik Pedersen
parent e10e36cf70
commit 3089fc0ba1
4 changed files with 674 additions and 24 deletions

View file

@ -65,8 +65,9 @@ install:
script:
- go mod download
- go mod verify
- mage -v test
- if [ "$TRAVIS_ARCH" = "amd64" ]; then
- travis_wait 20 mage -v test
- >
if [ "$TRAVIS_ARCH" = "amd64" ]; then
mage -v check;
else
HUGO_TIMEOUT=30000 mage -v check;

View 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("included");
`)
}
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
`)
}

View file

@ -14,11 +14,16 @@
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)
opts.sourcefile = sfile
opts.resolveDir = t.sfs.RealFilename(sdir)
opts.contents = string(src)
opts.mediaType = ctx.InMediaType
buildOptions, err := toBuildOptions(opts)
sdir, sfile := filepath.Split(t.sfs.RealFilename(ctx.SourcePath))
opts.workDir, err = filepath.Abs(t.rs.WorkingDir)
if err != nil {
return err
}
result := api.Build(buildOptions)
if len(result.Errors) > 0 {
return fmt.Errorf("%s", result.Errors[0].Text)
opts.sourcefile = sfile
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 {
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)
}
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,

View 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{},
})
}