hugo

Fork of github.com/gohugoio/hugo with reverse pagination support

git clone git://git.shimmy1996.com/hugo.git

options.go (11145B)

    1 // Copyright 2020 The Hugo Authors. All rights reserved.
    2 //
    3 // Licensed under the Apache License, Version 2.0 (the "License");
    4 // you may not use this file except in compliance with the License.
    5 // You may obtain a copy of the License at
    6 // http://www.apache.org/licenses/LICENSE-2.0
    7 //
    8 // Unless required by applicable law or agreed to in writing, software
    9 // distributed under the License is distributed on an "AS IS" BASIS,
   10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   11 // See the License for the specific language governing permissions and
   12 // limitations under the License.
   13 
   14 package js
   15 
   16 import (
   17 	"encoding/json"
   18 	"fmt"
   19 	"io/ioutil"
   20 	"path/filepath"
   21 	"strings"
   22 
   23 	"github.com/gohugoio/hugo/common/maps"
   24 	"github.com/spf13/afero"
   25 
   26 	"github.com/evanw/esbuild/pkg/api"
   27 
   28 	"github.com/gohugoio/hugo/helpers"
   29 	"github.com/gohugoio/hugo/hugofs"
   30 	"github.com/gohugoio/hugo/media"
   31 	"github.com/mitchellh/mapstructure"
   32 )
   33 
   34 const (
   35 	nsImportHugo = "ns-hugo"
   36 	nsParams     = "ns-params"
   37 
   38 	stdinImporter = "<stdin>"
   39 )
   40 
   41 // Options esbuild configuration
   42 type Options struct {
   43 	// If not set, the source path will be used as the base target path.
   44 	// Note that the target path's extension may change if the target MIME type
   45 	// is different, e.g. when the source is TypeScript.
   46 	TargetPath string
   47 
   48 	// Whether to minify to output.
   49 	Minify bool
   50 
   51 	// Whether to write mapfiles
   52 	SourceMap string
   53 
   54 	// The language target.
   55 	// One of: es2015, es2016, es2017, es2018, es2019, es2020 or esnext.
   56 	// Default is esnext.
   57 	Target string
   58 
   59 	// The output format.
   60 	// One of: iife, cjs, esm
   61 	// Default is to esm.
   62 	Format string
   63 
   64 	// External dependencies, e.g. "react".
   65 	Externals []string
   66 
   67 	// This option allows you to automatically replace a global variable with an import from another file.
   68 	// The filenames must be relative to /assets.
   69 	// See https://esbuild.github.io/api/#inject
   70 	Inject []string
   71 
   72 	// User defined symbols.
   73 	Defines map[string]any
   74 
   75 	// Maps a component import to another.
   76 	Shims map[string]string
   77 
   78 	// User defined params. Will be marshaled to JSON and available as "@params", e.g.
   79 	//     import * as params from '@params';
   80 	Params any
   81 
   82 	// What to use instead of React.createElement.
   83 	JSXFactory string
   84 
   85 	// What to use instead of React.Fragment.
   86 	JSXFragment string
   87 
   88 	// There is/was a bug in WebKit with severe performance issue with the tracking
   89 	// of TDZ checks in JavaScriptCore.
   90 	//
   91 	// Enabling this flag removes the TDZ and `const` assignment checks and
   92 	// may improve performance of larger JS codebases until the WebKit fix
   93 	// is in widespread use.
   94 	//
   95 	// See https://bugs.webkit.org/show_bug.cgi?id=199866
   96 	// Deprecated: This no longer have any effect and will be removed.
   97 	// TODO(bep) remove. See https://github.com/evanw/esbuild/commit/869e8117b499ca1dbfc5b3021938a53ffe934dba
   98 	AvoidTDZ bool
   99 
  100 	mediaType  media.Type
  101 	outDir     string
  102 	contents   string
  103 	sourceDir  string
  104 	resolveDir string
  105 	tsConfig   string
  106 }
  107 
  108 func decodeOptions(m map[string]any) (Options, error) {
  109 	var opts Options
  110 
  111 	if err := mapstructure.WeakDecode(m, &opts); err != nil {
  112 		return opts, err
  113 	}
  114 
  115 	if opts.TargetPath != "" {
  116 		opts.TargetPath = helpers.ToSlashTrimLeading(opts.TargetPath)
  117 	}
  118 
  119 	opts.Target = strings.ToLower(opts.Target)
  120 	opts.Format = strings.ToLower(opts.Format)
  121 
  122 	return opts, nil
  123 }
  124 
  125 var extensionToLoaderMap = map[string]api.Loader{
  126 	".js":   api.LoaderJS,
  127 	".mjs":  api.LoaderJS,
  128 	".cjs":  api.LoaderJS,
  129 	".jsx":  api.LoaderJSX,
  130 	".ts":   api.LoaderTS,
  131 	".tsx":  api.LoaderTSX,
  132 	".css":  api.LoaderCSS,
  133 	".json": api.LoaderJSON,
  134 	".txt":  api.LoaderText,
  135 }
  136 
  137 func loaderFromFilename(filename string) api.Loader {
  138 	l, found := extensionToLoaderMap[filepath.Ext(filename)]
  139 	if found {
  140 		return l
  141 	}
  142 	return api.LoaderJS
  143 }
  144 
  145 func resolveComponentInAssets(fs afero.Fs, impPath string) *hugofs.FileMeta {
  146 	findFirst := func(base string) *hugofs.FileMeta {
  147 		// This is the most common sub-set of ESBuild's default extensions.
  148 		// We assume that imports of JSON, CSS etc. will be using their full
  149 		// name with extension.
  150 		for _, ext := range []string{".js", ".ts", ".tsx", ".jsx"} {
  151 			if strings.HasSuffix(impPath, ext) {
  152 				// Import of foo.js.js need the full name.
  153 				return nil
  154 			}
  155 			if fi, err := fs.Stat(base + ext); err == nil {
  156 				return fi.(hugofs.FileMetaInfo).Meta()
  157 			}
  158 		}
  159 
  160 		// Not found.
  161 		return nil
  162 	}
  163 
  164 	var m *hugofs.FileMeta
  165 
  166 	// See issue #8949.
  167 	// We need to check if this is a regular file imported without an extension.
  168 	// There may be ambigous situations where both foo.js and foo/index.js exists.
  169 	// This import order is in line with both how Node and ESBuild's native
  170 	// import resolver works.
  171 	// This was fixed in Hugo 0.88.
  172 
  173 	// It may be a regular file imported without an extension, e.g.
  174 	// foo or foo/index.
  175 	m = findFirst(impPath)
  176 	if m != nil {
  177 		return m
  178 	}
  179 	if filepath.Base(impPath) == "index" {
  180 		m = findFirst(impPath + ".esm")
  181 		if m != nil {
  182 			return m
  183 		}
  184 	}
  185 
  186 	// Finally check the path as is.
  187 	fi, err := fs.Stat(impPath)
  188 
  189 	if err == nil {
  190 		if fi.IsDir() {
  191 			m = findFirst(filepath.Join(impPath, "index"))
  192 			if m == nil {
  193 				m = findFirst(filepath.Join(impPath, "index.esm"))
  194 			}
  195 		} else {
  196 			m = fi.(hugofs.FileMetaInfo).Meta()
  197 		}
  198 	}
  199 
  200 	return m
  201 }
  202 
  203 func createBuildPlugins(c *Client, opts Options) ([]api.Plugin, error) {
  204 	fs := c.rs.Assets
  205 
  206 	resolveImport := func(args api.OnResolveArgs) (api.OnResolveResult, error) {
  207 		impPath := args.Path
  208 		if opts.Shims != nil {
  209 			override, found := opts.Shims[impPath]
  210 			if found {
  211 				impPath = override
  212 			}
  213 		}
  214 		isStdin := args.Importer == stdinImporter
  215 		var relDir string
  216 		if !isStdin {
  217 			rel, found := fs.MakePathRelative(args.Importer)
  218 			if !found {
  219 				// Not in any of the /assets folders.
  220 				// This is an import from a node_modules, let
  221 				// ESBuild resolve this.
  222 				return api.OnResolveResult{}, nil
  223 			}
  224 			relDir = filepath.Dir(rel)
  225 		} else {
  226 			relDir = opts.sourceDir
  227 		}
  228 
  229 		// Imports not starting with a "." is assumed to live relative to /assets.
  230 		// Hugo makes no assumptions about the directory structure below /assets.
  231 		if relDir != "" && strings.HasPrefix(impPath, ".") {
  232 			impPath = filepath.Join(relDir, impPath)
  233 		}
  234 
  235 		m := resolveComponentInAssets(fs.Fs, impPath)
  236 
  237 		if m != nil {
  238 			// Store the source root so we can create a jsconfig.json
  239 			// to help intellisense when the build is done.
  240 			// This should be a small number of elements, and when
  241 			// in server mode, we may get stale entries on renames etc.,
  242 			// but that shouldn't matter too much.
  243 			c.rs.JSConfigBuilder.AddSourceRoot(m.SourceRoot)
  244 			return api.OnResolveResult{Path: m.Filename, Namespace: nsImportHugo}, nil
  245 		}
  246 
  247 		// Fall back to ESBuild's resolve.
  248 		return api.OnResolveResult{}, nil
  249 	}
  250 
  251 	importResolver := api.Plugin{
  252 		Name: "hugo-import-resolver",
  253 		Setup: func(build api.PluginBuild) {
  254 			build.OnResolve(api.OnResolveOptions{Filter: `.*`},
  255 				func(args api.OnResolveArgs) (api.OnResolveResult, error) {
  256 					return resolveImport(args)
  257 				})
  258 			build.OnLoad(api.OnLoadOptions{Filter: `.*`, Namespace: nsImportHugo},
  259 				func(args api.OnLoadArgs) (api.OnLoadResult, error) {
  260 					b, err := ioutil.ReadFile(args.Path)
  261 					if err != nil {
  262 						return api.OnLoadResult{}, fmt.Errorf("failed to read %q: %w", args.Path, err)
  263 					}
  264 					c := string(b)
  265 					return api.OnLoadResult{
  266 						// See https://github.com/evanw/esbuild/issues/502
  267 						// This allows all modules to resolve dependencies
  268 						// in the main project's node_modules.
  269 						ResolveDir: opts.resolveDir,
  270 						Contents:   &c,
  271 						Loader:     loaderFromFilename(args.Path),
  272 					}, nil
  273 				})
  274 		},
  275 	}
  276 
  277 	params := opts.Params
  278 	if params == nil {
  279 		// This way @params will always resolve to something.
  280 		params = make(map[string]any)
  281 	}
  282 
  283 	b, err := json.Marshal(params)
  284 	if err != nil {
  285 		return nil, fmt.Errorf("failed to marshal params: %w", err)
  286 	}
  287 	bs := string(b)
  288 	paramsPlugin := api.Plugin{
  289 		Name: "hugo-params-plugin",
  290 		Setup: func(build api.PluginBuild) {
  291 			build.OnResolve(api.OnResolveOptions{Filter: `^@params$`},
  292 				func(args api.OnResolveArgs) (api.OnResolveResult, error) {
  293 					return api.OnResolveResult{
  294 						Path:      args.Path,
  295 						Namespace: nsParams,
  296 					}, nil
  297 				})
  298 			build.OnLoad(api.OnLoadOptions{Filter: `.*`, Namespace: nsParams},
  299 				func(args api.OnLoadArgs) (api.OnLoadResult, error) {
  300 					return api.OnLoadResult{
  301 						Contents: &bs,
  302 						Loader:   api.LoaderJSON,
  303 					}, nil
  304 				})
  305 		},
  306 	}
  307 
  308 	return []api.Plugin{importResolver, paramsPlugin}, nil
  309 }
  310 
  311 func toBuildOptions(opts Options) (buildOptions api.BuildOptions, err error) {
  312 	var target api.Target
  313 	switch opts.Target {
  314 	case "", "esnext":
  315 		target = api.ESNext
  316 	case "es5":
  317 		target = api.ES5
  318 	case "es6", "es2015":
  319 		target = api.ES2015
  320 	case "es2016":
  321 		target = api.ES2016
  322 	case "es2017":
  323 		target = api.ES2017
  324 	case "es2018":
  325 		target = api.ES2018
  326 	case "es2019":
  327 		target = api.ES2019
  328 	case "es2020":
  329 		target = api.ES2020
  330 	default:
  331 		err = fmt.Errorf("invalid target: %q", opts.Target)
  332 		return
  333 	}
  334 
  335 	mediaType := opts.mediaType
  336 	if mediaType.IsZero() {
  337 		mediaType = media.JavascriptType
  338 	}
  339 
  340 	var loader api.Loader
  341 	switch mediaType.SubType {
  342 	// TODO(bep) ESBuild support a set of other loaders, but I currently fail
  343 	// to see the relevance. That may change as we start using this.
  344 	case media.JavascriptType.SubType:
  345 		loader = api.LoaderJS
  346 	case media.TypeScriptType.SubType:
  347 		loader = api.LoaderTS
  348 	case media.TSXType.SubType:
  349 		loader = api.LoaderTSX
  350 	case media.JSXType.SubType:
  351 		loader = api.LoaderJSX
  352 	default:
  353 		err = fmt.Errorf("unsupported Media Type: %q", opts.mediaType)
  354 		return
  355 	}
  356 
  357 	var format api.Format
  358 	// One of: iife, cjs, esm
  359 	switch opts.Format {
  360 	case "", "iife":
  361 		format = api.FormatIIFE
  362 	case "esm":
  363 		format = api.FormatESModule
  364 	case "cjs":
  365 		format = api.FormatCommonJS
  366 	default:
  367 		err = fmt.Errorf("unsupported script output format: %q", opts.Format)
  368 		return
  369 	}
  370 
  371 	var defines map[string]string
  372 	if opts.Defines != nil {
  373 		defines = maps.ToStringMapString(opts.Defines)
  374 	}
  375 
  376 	// By default we only need to specify outDir and no outFile
  377 	outDir := opts.outDir
  378 	outFile := ""
  379 	var sourceMap api.SourceMap
  380 	switch opts.SourceMap {
  381 	case "inline":
  382 		sourceMap = api.SourceMapInline
  383 	case "external":
  384 		sourceMap = api.SourceMapExternal
  385 	case "":
  386 		sourceMap = api.SourceMapNone
  387 	default:
  388 		err = fmt.Errorf("unsupported sourcemap type: %q", opts.SourceMap)
  389 		return
  390 	}
  391 
  392 	buildOptions = api.BuildOptions{
  393 		Outfile: outFile,
  394 		Bundle:  true,
  395 
  396 		Target:    target,
  397 		Format:    format,
  398 		Sourcemap: sourceMap,
  399 
  400 		MinifyWhitespace:  opts.Minify,
  401 		MinifyIdentifiers: opts.Minify,
  402 		MinifySyntax:      opts.Minify,
  403 
  404 		Outdir: outDir,
  405 		Define: defines,
  406 
  407 		External: opts.Externals,
  408 
  409 		JSXFactory:  opts.JSXFactory,
  410 		JSXFragment: opts.JSXFragment,
  411 
  412 		Tsconfig: opts.tsConfig,
  413 
  414 		// Note: We're not passing Sourcefile to ESBuild.
  415 		// This makes ESBuild pass `stdin` as the Importer to the import
  416 		// resolver, which is what we need/expect.
  417 		Stdin: &api.StdinOptions{
  418 			Contents:   opts.contents,
  419 			ResolveDir: opts.resolveDir,
  420 			Loader:     loader,
  421 		},
  422 	}
  423 	return
  424 }