summaryrefslogtreecommitdiffhomepage
path: root/resources
diff options
context:
space:
mode:
Diffstat (limited to 'resources')
-rw-r--r--resources/jsconfig/jsconfig.go93
-rw-r--r--resources/jsconfig/jsconfig_test.go35
-rw-r--r--resources/resource_cache.go13
-rw-r--r--resources/resource_spec.go34
-rw-r--r--resources/resource_transformers/js/build.go565
-rw-r--r--resources/resource_transformers/js/build_test.go82
-rw-r--r--resources/resource_transformers/js/options.go353
-rw-r--r--resources/resource_transformers/js/options_test.go105
8 files changed, 662 insertions, 618 deletions
diff --git a/resources/jsconfig/jsconfig.go b/resources/jsconfig/jsconfig.go
new file mode 100644
index 000000000..9b399bfe7
--- /dev/null
+++ b/resources/jsconfig/jsconfig.go
@@ -0,0 +1,93 @@
+// Copyright 2020 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package jsconfig
+
+import (
+ "path/filepath"
+ "sort"
+ "sync"
+)
+
+// Builder builds a jsconfig.json file that, currently, is used only to assist
+// intellinsense in editors.
+type Builder struct {
+ sourceRootsMu sync.RWMutex
+ sourceRoots map[string]bool
+}
+
+// NewBuilder creates a new Builder.
+func NewBuilder() *Builder {
+ return &Builder{sourceRoots: make(map[string]bool)}
+}
+
+// Build builds a new Config with paths relative to dir.
+// This method is thread safe.
+func (b *Builder) Build(dir string) *Config {
+ b.sourceRootsMu.RLock()
+ defer b.sourceRootsMu.RUnlock()
+
+ if len(b.sourceRoots) == 0 {
+ return nil
+ }
+ conf := newJSConfig()
+
+ var roots []string
+ for root := range b.sourceRoots {
+ rel, err := filepath.Rel(dir, filepath.Join(root, "*"))
+ if err == nil {
+ roots = append(roots, rel)
+ }
+ }
+ sort.Strings(roots)
+ conf.CompilerOptions.Paths["*"] = roots
+
+ return conf
+}
+
+// AddSourceRoot adds a new source root.
+// This method is thread safe.
+func (b *Builder) AddSourceRoot(root string) {
+ b.sourceRootsMu.RLock()
+ found := b.sourceRoots[root]
+ b.sourceRootsMu.RUnlock()
+
+ if found {
+ return
+ }
+
+ b.sourceRootsMu.Lock()
+ b.sourceRoots[root] = true
+ b.sourceRootsMu.Unlock()
+
+}
+
+// CompilerOptions holds compilerOptions for jsonconfig.json.
+type CompilerOptions struct {
+ BaseURL string `json:"baseUrl"`
+ Paths map[string][]string `json:"paths"`
+}
+
+// Config holds the data for jsconfig.json.
+type Config struct {
+ CompilerOptions CompilerOptions `json:"compilerOptions"`
+}
+
+func newJSConfig() *Config {
+ return &Config{
+ CompilerOptions: CompilerOptions{
+ BaseURL: ".",
+ Paths: make(map[string][]string),
+ },
+ }
+}
diff --git a/resources/jsconfig/jsconfig_test.go b/resources/jsconfig/jsconfig_test.go
new file mode 100644
index 000000000..9a9657843
--- /dev/null
+++ b/resources/jsconfig/jsconfig_test.go
@@ -0,0 +1,35 @@
+// Copyright 2020 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package jsconfig
+
+import (
+ "path/filepath"
+ "testing"
+
+ qt "github.com/frankban/quicktest"
+)
+
+func TestJsConfigBuilder(t *testing.T) {
+ c := qt.New(t)
+
+ b := NewBuilder()
+ b.AddSourceRoot("/c/assets")
+ b.AddSourceRoot("/d/assets")
+
+ conf := b.Build("/a/b")
+ c.Assert(conf.CompilerOptions.BaseURL, qt.Equals, ".")
+ c.Assert(conf.CompilerOptions.Paths["*"], qt.DeepEquals, []string{filepath.FromSlash("../../c/assets/*"), filepath.FromSlash("../../d/assets/*")})
+
+ c.Assert(NewBuilder().Build("/a/b"), qt.IsNil)
+}
diff --git a/resources/resource_cache.go b/resources/resource_cache.go
index feaa94f5c..6c4ba951b 100644
--- a/resources/resource_cache.go
+++ b/resources/resource_cache.go
@@ -18,6 +18,7 @@ import (
"io"
"path"
"path/filepath"
+ "regexp"
"strings"
"sync"
@@ -296,21 +297,15 @@ func (c *ResourceCache) DeletePartitions(partitions ...string) {
}
-func (c *ResourceCache) DeleteContains(parts ...string) {
+func (c *ResourceCache) DeleteMatches(re *regexp.Regexp) {
c.Lock()
defer c.Unlock()
for k := range c.cache {
- clear := false
- for _, part := range parts {
- if strings.Contains(k, part) {
- clear = true
- break
- }
- }
- if clear {
+ if re.MatchString(k) {
delete(c.cache, k)
}
+
}
}
diff --git a/resources/resource_spec.go b/resources/resource_spec.go
index 17225e3f5..0ca60fe31 100644
--- a/resources/resource_spec.go
+++ b/resources/resource_spec.go
@@ -23,6 +23,8 @@ import (
"strings"
"sync"
+ "github.com/gohugoio/hugo/resources/jsconfig"
+
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/config"
@@ -76,17 +78,20 @@ func NewSpec(
}
rs := &Spec{
- PathSpec: s,
- Logger: logger,
- ErrorSender: errorHandler,
- imaging: imaging,
- incr: incr,
- MediaTypes: mimeTypes,
- OutputFormats: outputFormats,
- Permalinks: permalinks,
- BuildConfig: config.DecodeBuild(s.Cfg),
- FileCaches: fileCaches,
- PostProcessResources: make(map[string]postpub.PostPublishedResource),
+ PathSpec: s,
+ Logger: logger,
+ ErrorSender: errorHandler,
+ imaging: imaging,
+ incr: incr,
+ MediaTypes: mimeTypes,
+ OutputFormats: outputFormats,
+ Permalinks: permalinks,
+ BuildConfig: config.DecodeBuild(s.Cfg),
+ FileCaches: fileCaches,
+ PostBuildAssets: &PostBuildAssets{
+ PostProcessResources: make(map[string]postpub.PostPublishedResource),
+ JSConfigBuilder: jsconfig.NewBuilder(),
+ },
imageCache: newImageCache(
fileCaches.ImageCache(),
@@ -121,8 +126,15 @@ type Spec struct {
ResourceCache *ResourceCache
FileCaches filecache.Caches
+ // Assets used after the build is done.
+ // This is shared between all sites.
+ *PostBuildAssets
+}
+
+type PostBuildAssets struct {
postProcessMu sync.RWMutex
PostProcessResources map[string]postpub.PostPublishedResource
+ JSConfigBuilder *jsconfig.Builder
}
func (r *Spec) New(fd ResourceSourceDescriptor) (resource.Resource, error) {
diff --git a/resources/resource_transformers/js/build.go b/resources/resource_transformers/js/build.go
index d316bc85b..8a7c21592 100644
--- a/resources/resource_transformers/js/build.go
+++ b/resources/resource_transformers/js/build.go
@@ -14,122 +14,52 @@
package js
import (
- "encoding/json"
+ "errors"
"fmt"
"io/ioutil"
"os"
"path"
"path/filepath"
- "reflect"
"strings"
- "github.com/achiku/varfmt"
- "github.com/spf13/cast"
+ "github.com/gohugoio/hugo/hugofs"
+
+ "github.com/spf13/afero"
+
+ "github.com/gohugoio/hugo/common/herrors"
- "github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugolib/filesystems"
"github.com/gohugoio/hugo/media"
"github.com/gohugoio/hugo/resources/internal"
- "github.com/mitchellh/mapstructure"
-
"github.com/evanw/esbuild/pkg/api"
"github.com/gohugoio/hugo/resources"
"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
- // is different, e.g. when the source is TypeScript.
- TargetPath string
-
- // Whether to minify to output.
- Minify bool
-
- // Whether to write mapfiles
- SourceMap string
-
- // The language target.
- // One of: es2015, es2016, es2017, es2018, es2019, es2020 or esnext.
- // Default is esnext.
- Target string
-
- // The output format.
- // One of: iife, cjs, esm
- // Default is to esm.
- Format string
-
- // External dependencies, e.g. "react".
- Externals []string `hash:"set"`
-
- // 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
-
- // What to use instead of React.Fragment.
- JSXFragment string
-
- mediaType media.Type
- outDir string
- contents string
- sourcefile string
- resolveDir string
- workDir string
- tsConfig string
-}
-
-func decodeOptions(m map[string]interface{}) (Options, error) {
- var opts Options
-
- if err := mapstructure.WeakDecode(m, &opts); err != nil {
- return opts, err
- }
-
- if opts.TargetPath != "" {
- opts.TargetPath = helpers.ToSlashTrimLeading(opts.TargetPath)
- }
-
- opts.Target = strings.ToLower(opts.Target)
- opts.Format = strings.ToLower(opts.Format)
-
- return opts, nil
-}
-
-// Client context for esbuild
+// Client context for ESBuild.
type Client struct {
rs *resources.Spec
sfs *filesystems.SourceFilesystem
}
-// New create new client context
+// New creates a new client context.
func New(fs *filesystems.SourceFilesystem, rs *resources.Spec) *Client {
- return &Client{rs: rs, sfs: fs}
+ return &Client{
+ rs: rs,
+ sfs: fs,
+ }
}
type buildTransformation struct {
optsm map[string]interface{}
- rs *resources.Spec
- sfs *filesystems.SourceFilesystem
+ c *Client
}
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
@@ -149,465 +79,68 @@ func (t *buildTransformation) Transform(ctx *resources.ResourceTransformationCtx
return err
}
- 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 = sdir
+ sdir, _ := path.Split(ctx.SourcePath)
+ opts.sourcefile = ctx.SourcePath
+ opts.resolveDir = t.c.sfs.RealFilename(sdir)
+ opts.workDir = t.c.rs.WorkingDir
opts.contents = string(src)
opts.mediaType = ctx.InMediaType
- // Create new temporary tsconfig file
- newTSConfig, err := ioutil.TempFile("", "tsconfig.*.json")
+ buildOptions, err := toBuildOptions(opts)
if err != nil {
return err
}
- filesToDelete := make([]*os.File, 0)
-
- defer func() {
- for _, file := range filesToDelete {
- os.Remove(file.Name())
- }
- }()
+ buildOptions.Plugins, err = createBuildPlugins(t.c, opts)
+ if err != nil {
+ return err
+ }
- filesToDelete = append(filesToDelete, newTSConfig)
- configDir, _ := filepath.Split(newTSConfig.Name())
+ result := api.Build(buildOptions)
- // 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 len(result.Errors) > 0 {
+ first := result.Errors[0]
+ loc := first.Location
+ path := loc.File
+
+ var err error
+ var f afero.File
+ var filename string
+
+ if !strings.HasPrefix(path, "..") {
+ // Try first in the assets fs
+ var fi os.FileInfo
+ fi, err = t.c.rs.BaseFs.Assets.Fs.Stat(path)
if err == nil {
- innerTsConfig = tryTsConfig
- baseURLAbs = tsDir
- break
+ m := fi.(hugofs.FileMetaInfo).Meta()
+ filename = m.Filename()
+ f, err = m.Open()
}
- } 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)
+ if f == nil {
+ path = filepath.Join(t.c.rs.WorkingDir, path)
+ filename = path
+ f, err = t.c.rs.Fs.Os.Open(path)
}
- 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 {
+ fe := herrors.NewFileError("js", 0, loc.Line, loc.Column, errors.New(first.Text))
+ err, _ := herrors.WithFileContext(fe, filename, f, herrors.SimpleLineMatcher)
+ f.Close()
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)
+ return fmt.Errorf("%s", result.Errors[0].Text)
}
- 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},
+ &buildTransformation{c: c, optsm: opts},
)
}
-
-func toBuildOptions(opts Options) (buildOptions api.BuildOptions, err error) {
- var target api.Target
- switch opts.Target {
- case "", "esnext":
- target = api.ESNext
- case "es5":
- target = api.ES5
- case "es6", "es2015":
- target = api.ES2015
- case "es2016":
- target = api.ES2016
- case "es2017":
- target = api.ES2017
- case "es2018":
- target = api.ES2018
- case "es2019":
- target = api.ES2019
- case "es2020":
- target = api.ES2020
- default:
- err = fmt.Errorf("invalid target: %q", opts.Target)
- return
- }
-
- mediaType := opts.mediaType
- if mediaType.IsZero() {
- mediaType = media.JavascriptType
- }
-
- var loader api.Loader
- switch mediaType.SubType {
- // TODO(bep) ESBuild support a set of other loaders, but I currently fail
- // to see the relevance. That may change as we start using this.
- case media.JavascriptType.SubType:
- loader = api.LoaderJS
- case media.TypeScriptType.SubType:
- loader = api.LoaderTS
- case media.TSXType.SubType:
- loader = api.LoaderTSX
- case media.JSXType.SubType:
- loader = api.LoaderJSX
- default:
- err = fmt.Errorf("unsupported Media Type: %q", opts.mediaType)
- return
- }
-
- var format api.Format
- // One of: iife, cjs, esm
- switch opts.Format {
- case "", "iife":
- format = api.FormatIIFE
- case "esm":
- format = api.FormatESModule
- case "cjs":
- format = api.FormatCommonJS
- default:
- err = fmt.Errorf("unsupported script output format: %q", opts.Format)
- return
- }
-
- var defines map[string]string
- if opts.Defines != nil {
- 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:
- err = fmt.Errorf("unsupported sourcemap type: %q", opts.SourceMap)
- return
- }
-
- buildOptions = api.BuildOptions{
- Outfile: outFile,
- Bundle: true,
-
- Target: target,
- Format: format,
- Sourcemap: sourceMap,
-
- MinifyWhitespace: opts.Minify,
- MinifyIdentifiers: opts.Minify,
- MinifySyntax: opts.Minify,
-
- Outdir: outDir,
- Defines: defines,
-
- Externals: opts.Externals,
-
- JSXFactory: opts.JSXFactory,
- JSXFragment: opts.JSXFragment,
-
- Tsconfig: opts.tsConfig,
-
- Stdin: &api.StdinOptions{
- Contents: opts.contents,
- Sourcefile: opts.sourcefile,
- ResolveDir: opts.resolveDir,
- Loader: loader,
- },
- }
- return
-
-}
diff --git a/resources/resource_transformers/js/build_test.go b/resources/resource_transformers/js/build_test.go
index 8839c646e..30a4490ed 100644
--- a/resources/resource_transformers/js/build_test.go
+++ b/resources/resource_transformers/js/build_test.go
@@ -12,85 +12,3 @@
// limitations under the License.
package js
-
-import (
- "testing"
-
- "github.com/gohugoio/hugo/media"
-
- "github.com/evanw/esbuild/pkg/api"
-
- qt "github.com/frankban/quicktest"
-)
-
-// This test is added to test/warn against breaking the "stability" of the
-// cache key. It's sometimes needed to break this, but should be avoided if possible.
-func TestOptionKey(t *testing.T) {
- c := qt.New(t)
-
- opts := map[string]interface{}{
- "TargetPath": "foo",
- "Target": "es2018",
- }
-
- key := (&buildTransformation{optsm: opts}).Key()
-
- c.Assert(key.Value(), qt.Equals, "jsbuild_7891849149754191852")
-}
-
-func TestToBuildOptions(t *testing.T) {
- c := qt.New(t)
-
- opts, err := toBuildOptions(Options{mediaType: media.JavascriptType})
- c.Assert(err, qt.IsNil)
- c.Assert(opts, qt.DeepEquals, api.BuildOptions{
- Bundle: true,
- Target: api.ESNext,
- Format: api.FormatIIFE,
- Stdin: &api.StdinOptions{},
- })
-
- opts, err = toBuildOptions(Options{
- Target: "es2018", Format: "cjs", Minify: true, mediaType: media.JavascriptType})
- 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,
- Stdin: &api.StdinOptions{},
- })
-
- opts, err = toBuildOptions(Options{
- Target: "es2018", Format: "cjs", Minify: true, mediaType: media.JavascriptType,
- SourceMap: "inline"})
- 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.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{},
- })
-
-}
diff --git a/resources/resource_transformers/js/options.go b/resources/resource_transformers/js/options.go
new file mode 100644
index 000000000..5e74982d3
--- /dev/null
+++ b/resources/resource_transformers/js/options.go
@@ -0,0 +1,353 @@
+// Copyright 2020 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package js
+
+import (
+ "encoding/json"
+ "fmt"
+ "path/filepath"
+ "strings"
+ "sync"
+
+ "github.com/pkg/errors"
+
+ "github.com/evanw/esbuild/pkg/api"
+
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/gohugoio/hugo/hugofs"
+ "github.com/gohugoio/hugo/media"
+ "github.com/mitchellh/mapstructure"
+ "github.com/spf13/cast"
+)
+
+// 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
+ // is different, e.g. when the source is TypeScript.
+ TargetPath string
+
+ // Whether to minify to output.
+ Minify bool
+
+ // Whether to write mapfiles
+ SourceMap string
+
+ // The language target.
+ // One of: es2015, es2016, es2017, es2018, es2019, es2020 or esnext.
+ // Default is esnext.
+ Target string
+
+ // The output format.
+ // One of: iife, cjs, esm
+ // Default is to esm.
+ Format string
+
+ // External dependencies, e.g. "react".
+ Externals []string `hash:"set"`
+
+ // User defined symbols.
+ Defines map[string]interface{}
+
+ // User defined params. Will be marshaled to JSON and available as "@params", e.g.
+ // import * as params from '@params';
+ Params interface{}
+
+ // What to use instead of React.createElement.
+ JSXFactory string
+
+ // What to use instead of React.Fragment.
+ JSXFragment string
+
+ mediaType media.Type
+ outDir string
+ contents string
+ sourcefile string
+ resolveDir string
+ workDir string
+ tsConfig string
+}
+
+func decodeOptions(m map[string]interface{}) (Options, error) {
+ var opts Options
+
+ if err := mapstructure.WeakDecode(m, &opts); err != nil {
+ return opts, err
+ }
+
+ if opts.TargetPath != "" {
+ opts.TargetPath = helpers.ToSlashTrimLeading(opts.TargetPath)
+ }
+
+ opts.Target = strings.ToLower(opts.Target)
+ opts.Format = strings.ToLower(opts.Format)
+
+ return opts, nil
+}
+
+type importCache struct {
+ sync.RWMutex
+ m map[string]api.OnResolveResult
+}
+
+func createBuildPlugins(c *Client, opts Options) ([]api.Plugin, error) {
+ fs := c.rs.Assets
+
+ cache := importCache{
+ m: make(map[string]api.OnResolveResult),
+ }
+
+ resolveImport := func(args api.OnResolveArgs) (api.OnResolveResult, error) {
+ relDir := fs.MakePathRelative(args.ResolveDir)
+
+ if relDir == "" {
+ // Not in a Hugo Module, probably in node_modules.
+ return api.OnResolveResult{}, nil
+ }
+
+ impPath := args.Path
+
+ // stdin is the main entry file which already is at the relative root.
+ // Imports not starting with a "." is assumed to live relative to /assets.
+ // Hugo makes no assumptions about the directory structure below /assets.
+ if args.Importer != "<stdin>" && strings.HasPrefix(impPath, ".") {
+ impPath = filepath.Join(relDir, args.Path)
+ }
+
+ findFirst := func(base string) hugofs.FileMeta {
+ // This is the most common sub-set of ESBuild's default extensions.
+ // We assume that imports of JSON, CSS etc. will be using their full
+ // name with extension.
+ for _, ext := range []string{".js", ".ts", ".tsx", ".jsx"} {
+ if fi, err := fs.Fs.Stat(base + ext); err == nil {
+ return fi.(hugofs.FileMetaInfo).Meta()
+ }
+ }
+
+ // Not found.
+ return nil
+ }
+
+ var m hugofs.FileMeta
+
+ // First the path as is.
+ fi, err := fs.Fs.Stat(impPath)
+
+ if err == nil {
+ if fi.IsDir() {
+ m = findFirst(filepath.Join(impPath, "index"))
+ } else {
+ m = fi.(hugofs.FileMetaInfo).Meta()
+ }
+ } else {
+ // It may be a regular file imported without an extension.
+ m = findFirst(impPath)
+ }
+
+ if m != nil {
+ // Store the source root so we can create a jsconfig.json
+ // to help intellisense when the build is done.
+ // This should be a small number of elements, and when
+ // in server mode, we may get stale entries on renames etc.,
+ // but that shouldn't matter too much.
+ c.rs.JSConfigBuilder.AddSourceRoot(m.SourceRoot())
+ return api.OnResolveResult{Path: m.Filename(), Namespace: ""}, nil
+ }
+
+ return api.OnResolveResult{}, nil
+ }
+
+ importResolver := api.Plugin{
+ Name: "hugo-import-resolver",
+ Setup: func(build api.PluginBuild) {
+ build.OnResolve(api.OnResolveOptions{Filter: `.*`},
+ func(args api.OnResolveArgs) (api.OnResolveResult, error) {
+ // Try cache first.
+ cache.RLock()
+ v, found := cache.m[args.Path]
+ cache.RUnlock()
+
+ if found {
+ return v, nil
+ }
+
+ imp, err := resolveImport(args)
+ if err != nil {
+ return imp, err
+ }
+
+ cache.Lock()
+ defer cache.Unlock()
+
+ cache.m[args.Path] = imp
+
+ return imp, nil
+
+ })
+ },
+ }
+
+ params := opts.Params
+ if params == nil {
+ // This way @params will always resolve to something.
+ params = make(map[string]interface{})
+ }
+
+ b, err := json.Marshal(params)
+ if err != nil {
+ return nil, errors.Wrap(err, "failed to marshal params")
+ }
+ bs := string(b)
+ paramsPlugin := api.Plugin{
+ Name: "hugo-params-plugin",
+ Setup: func(build api.PluginBuild) {
+ build.OnResolve(api.OnResolveOptions{Filter: `^@params$`},
+ func(args api.OnResolveArgs) (api.OnResolveResult, error) {
+ return api.OnResolveResult{
+ Path: args.Path,
+ Namespace: "params",
+ }, nil
+ })
+ build.OnLoad(api.OnLoadOptions{Filter: `.*`, Namespace: "params"},
+ func(args api.OnLoadArgs) (api.OnLoadResult, error) {
+ return api.OnLoadResult{
+ Contents: &bs,
+ Loader: api.LoaderJSON,
+ }, nil
+ })
+ },
+ }
+
+ return []api.Plugin{importResolver, paramsPlugin}, nil
+
+}
+
+func toBuildOptions(opts Options) (buildOptions api.BuildOptions, err error) {
+
+ var target api.Target
+ switch opts.Target {
+ case "", "esnext":
+ target = api.ESNext
+ case "es5":
+ target = api.ES5
+ case "es6", "es2015":
+ target = api.ES2015
+ case "es2016":
+ target = api.ES2016
+ case "es2017":
+ target = api.ES2017
+ case "es2018":
+ target = api.ES2018
+ case "es2019":
+ target = api.ES2019
+ case "es2020":
+ target = api.ES2020
+ default:
+ err = fmt.Errorf("invalid target: %q", opts.Target)
+ return
+ }
+
+ mediaType := opts.mediaType
+ if mediaType.IsZero() {
+ mediaType = media.JavascriptType
+ }
+
+ var loader api.Loader
+ switch mediaType.SubType {
+ // TODO(bep) ESBuild support a set of other loaders, but I currently fail
+ // to see the relevance. That may change as we start using this.
+ case media.JavascriptType.SubType:
+ loader = api.LoaderJS
+ case media.TypeScriptType.SubType:
+ loader = api.LoaderTS
+ case media.TSXType.SubType:
+ loader = api.LoaderTSX
+ case media.JSXType.SubType:
+ loader = api.LoaderJSX
+ default:
+ err = fmt.Errorf("unsupported Media Type: %q", opts.mediaType)
+ return
+ }
+
+ var format api.Format
+ // One of: iife, cjs, esm
+ switch opts.Format {
+ case "", "iife":
+ format = api.FormatIIFE
+ case "esm":
+ format = api.FormatESModule
+ case "cjs":
+ format = api.FormatCommonJS
+ default:
+ err = fmt.Errorf("unsupported script output format: %q", opts.Format)
+ return
+ }
+
+ var defines map[string]string
+ if opts.Defines != nil {
+ 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:
+ err = fmt.Errorf("unsupported sourcemap type: %q", opts.SourceMap)
+ return
+ }
+
+ buildOptions = api.BuildOptions{
+ Outfile: outFile,
+ Bundle: true,
+
+ Target: target,
+ Format: format,
+ Sourcemap: sourceMap,
+
+ MinifyWhitespace: opts.Minify,
+ MinifyIdentifiers: opts.Minify,
+ MinifySyntax: opts.Minify,
+
+ Outdir: outDir,
+ Define: defines,
+
+ External: opts.Externals,
+
+ JSXFactory: opts.JSXFactory,
+ JSXFragment: opts.JSXFragment,
+
+ Tsconfig: opts.tsConfig,
+
+ Stdin: &api.StdinOptions{
+ Contents: opts.contents,
+ Sourcefile: opts.sourcefile,
+ ResolveDir: opts.resolveDir,
+ Loader: loader,
+ },
+ }
+ return
+
+}
diff --git a/resources/resource_transformers/js/options_test.go b/resources/resource_transformers/js/options_test.go
new file mode 100644
index 000000000..89d362ab9
--- /dev/null
+++ b/resources/resource_transformers/js/options_test.go
@@ -0,0 +1,105 @@
+// Copyright 2020 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package js
+
+import (
+ "testing"
+
+ "github.com/gohugoio/hugo/media"
+
+ "github.com/evanw/esbuild/pkg/api"
+
+ qt "github.com/frankban/quicktest"
+)
+
+// This test is added to test/warn against breaking the "stability" of the
+// cache key. It's sometimes needed to break this, but should be avoided if possible.
+func TestOptionKey(t *testing.T) {
+ c := qt.New(t)
+
+ opts := map[string]interface{}{
+ "TargetPath": "foo",
+ "Target": "es2018",
+ }
+
+ key := (&buildTransformation{optsm: opts}).Key()
+
+ c.Assert(key.Value(), qt.Equals, "jsbuild_7891849149754191852")
+}
+
+func TestToBuildOptions(t *testing.T) {
+ c := qt.New(t)
+
+ opts, err := toBuildOptions(Options{mediaType: media.JavascriptType})
+
+ c.Assert(err, qt.IsNil)
+ c.Assert(opts, qt.DeepEquals, api.BuildOptions{
+ Bundle: true,
+ Target: api.ESNext,
+ Format: api.FormatIIFE,
+ Stdin: &api.StdinOptions{
+ Loader: api.LoaderJS,
+ },
+ })
+
+ opts, err = toBuildOptions(Options{
+ Target: "es2018", Format: "cjs", Minify: true, mediaType: media.JavascriptType})
+ 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,
+ Stdin: &api.StdinOptions{
+ Loader: api.LoaderJS,
+ },
+ })
+
+ opts, err = toBuildOptions(Options{
+ Target: "es2018", Format: "cjs", Minify: true, mediaType: media.JavascriptType,
+ SourceMap: "inline"})
+ 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.SourceMapInline,
+ Stdin: &api.StdinOptions{
+ 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,
+ },
+ })
+
+}