summaryrefslogtreecommitdiffstats
path: root/resources/resource_transformers/js/options.go
diff options
context:
space:
mode:
Diffstat (limited to 'resources/resource_transformers/js/options.go')
-rw-r--r--resources/resource_transformers/js/options.go353
1 files changed, 353 insertions, 0 deletions
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
+
+}