shithub: hugo

Download patch

ref: 85e4dd7370eae97ae367e596aa6a10ba42fd4b7c
parent: 3089fc0ba171be14670b19439bc2eab6b077b6c3
author: Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
date: Mon Oct 5 09:34:14 EDT 2020

Make js.Build fully support modules

Fixes #7816
Fixes #7777
Fixes #7916

--- a/.travis.yml
+++ b/.travis.yml
@@ -47,10 +47,10 @@
 
 before_install:
   - df -h
-  # https://travis-ci.community/t/go-cant-find-gcc-with-go1-11-1-on-windows/293/5
+    # https://travis-ci.community/t/go-cant-find-gcc-with-go1-11-1-on-windows/293/5
   - if [ "$TRAVIS_OS_NAME" = "windows" ]; then
-    choco install mingw -y;
-    export PATH=/c/tools/mingw64/bin:"$PATH";
+        choco install mingw -y;
+        export PATH=/c/tools/mingw64/bin:"$PATH";
     fi
   - gem install asciidoctor
   - type asciidoctor
@@ -65,12 +65,11 @@
 script:
   - go mod download
   - go mod verify
-  - travis_wait 20 mage -v test
-  - >
-    if [ "$TRAVIS_ARCH" = "amd64" ]; then
-      mage -v check;
+  - mage -v test
+  - if [ "$TRAVIS_ARCH" = "amd64" ]; then
+        mage -v check;
     else
-      HUGO_TIMEOUT=30000 mage -v check;
+        HUGO_TIMEOUT=30000 mage -v check;
     fi
   - mage -v hugo
   - HUGO_IGNOREERRORS=error-remote-getjson ./hugo -s docs/
--- a/commands/hugo.go
+++ b/commands/hugo.go
@@ -984,9 +984,11 @@
 	staticEvents := []fsnotify.Event{}
 	dynamicEvents := []fsnotify.Event{}
 
-	// Special handling for symbolic links inside /content.
 	filtered := []fsnotify.Event{}
 	for _, ev := range evs {
+		if c.hugo().ShouldSkipFileChangeEvent(ev) {
+			continue
+		}
 		// Check the most specific first, i.e. files.
 		contentMapped := c.hugo().ContentChanges.GetSymbolicLinkMappings(ev.Name)
 		if len(contentMapped) > 0 {
--- a/config/commonConfig.go
+++ b/config/commonConfig.go
@@ -41,6 +41,10 @@
 	// When enabled, will collect and write a hugo_stats.json with some build
 	// related aggregated data (e.g. CSS class names).
 	WriteStats bool
+
+	// Can be used to toggle off writing of the intellinsense /assets/jsconfig.js
+	// file.
+	NoJSConfigInAssets bool
 }
 
 func (b Build) UseResourceCache(err error) bool {
--- a/deps/deps.go
+++ b/deps/deps.go
@@ -316,14 +316,16 @@
 
 	d.Site = cfg.Site
 
-	// The resource cache is global so reuse.
+	// These are common for all sites, so reuse.
 	// TODO(bep) clean up these inits.
 	resourceCache := d.ResourceSpec.ResourceCache
+	postBuildAssets := d.ResourceSpec.PostBuildAssets
 	d.ResourceSpec, err = resources.NewSpec(d.PathSpec, d.ResourceSpec.FileCaches, d.BuildState, d.Log, d.globalErrHandler, cfg.OutputFormats, cfg.MediaTypes)
 	if err != nil {
 		return nil, err
 	}
 	d.ResourceSpec.ResourceCache = resourceCache
+	d.ResourceSpec.PostBuildAssets = postBuildAssets
 
 	d.Cfg = l
 	d.Language = l
--- a/docs/content/en/getting-started/configuration.md
+++ b/docs/content/en/getting-started/configuration.md
@@ -304,6 +304,7 @@
 [build]
 useResourceCacheWhen="fallback"
 writeStats = false
+noJSConfigInAssets = false
 {{< /code-toggle >}}
 
 
@@ -312,6 +313,9 @@
 
 writeStats {{< new-in "0.69.0" >}}
 : When enabled, a file named `hugo_stats.json` will be written to your project root with some aggregated data about the build, e.g. list of HTML entities published to be used to do [CSS pruning](/hugo-pipes/postprocess/#css-purging-with-postcss). If you're only using this for the production build, you should consider placing it below [config/production](/getting-started/configuration/#configuration-directory). It's also worth mentioning that, due to the nature of the partial server builds, new HTML entities will be added when you add or change them while the server is running, but the old values will not be removed until you restart the server or run a regular `hugo` build.
+
+noJSConfigInAssets {{< new-in "0.78.0" >}}
+: Turn off writing a `jsconfig.js` into your `/assets` folder with mapping of imports from running [js.Build](https://gohugo.io/hugo-pipes/js). This file is intended to help with intellisense/navigation inside code editors such as [VS Code](https://code.visualstudio.com/). Note that if you do not use `js.Build`, no file will be written.
 
 ## Configure Server
 
--- a/docs/content/en/hugo-pipes/js.md
+++ b/docs/content/en/hugo-pipes/js.md
@@ -23,6 +23,20 @@
 : 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.
 
+params [map or slice] {{< new-in "0.78.0" >}}
+: Params that can be imported as JSON in your JS files, e.g.:
+
+```go-html-template
+{{ $js := resources.Get "js/main.js" | js.Build (dict "params" (dict "api" "https://example.org/api" ) }}
+```
+And then in your JS file: 
+
+```js
+import * as params from '@params';
+``` 
+
+Note that this is meant for small data sets, e.g. config settings. For larger data, please put/mount the files into `/assets` and import them directly.
+
 minify [bool]
 : Let `js.Build` handle the minification.
 
@@ -50,8 +64,52 @@
 format [string] {{< new-in "0.74.3" >}}
 : The output format.
   One of: `iife`, `cjs`, `esm`.
-  Default is `iife`, a self-executing function, suitable for inclusion as a <script> tag. 
+  Default is `iife`, a self-executing function, suitable for inclusion as a <script> tag.
 
+
+### Import JS code from /assets
+
+{{< new-in "0.78.0" >}}
+
+Since Hugo `v0.78.0` `js.Build` has full support for the virtual union file system in [Hugo Modules](/hugo-modules/). You can see some simple examples in this [test project](https://github.com/gohugoio/hugoTestProjectJSModImports), but in short this means that you can do this:
+
+```js
+import { hello } from 'my/module';
+```
+
+And it will respolve to the top-most `index.{js,ts,tsx,jsx}` inside `assets/my/module` in the layered file system.
+
+```js
+import { hello3 } from 'my/module/hello3';
+```
+
+Wil resolve to `hello3.{js,ts,tsx,jsx}` inside `assets/my/module`.
+
+Any imports starting with `.` is resolved relative to the current file:
+
+```js
+import { hello4 } from './lib';
+```
+
+For other files (e.g. `JSON`, `CSS`) you need to use the relative path including any extension, e.g:
+
+```js
+import * as data from 'my/module/data.json';
+```
+
+Also note the new `params` option that can be passed from template to your JS files, e.g.:
+
+```go-html-template
+{{ $js := resources.Get "js/main.js" | js.Build (dict "params" (dict "api" "https://example.org/api" ) }}
+```
+And then in your JS file: 
+
+```js
+import * as params from '@params';
+```
+
+Hugo will, by default, generate a `assets/jsconfig.js` file that maps the imports. This is useful for navigation/intellisense help inside code editors, but if you don't need/want it, you can [turn it off](/getting-started/configuration/#configure-build).
+
 ### Examples
 
 ```go-html-template
@@ -69,7 +127,8 @@
 <script type="text/javascript" src="{{ $built.RelPermalink }}" defer></script>
 ```
 
-#### Shimming a JS library
+#### Shimming a JS library 
+
 It's a very common practice to load external libraries using CDN rather than importing all packages in a single JS file, making it bulky. To do the same with Hugo, you'll need to shim the libraries as follows. In this example, `algoliasearch` and `instantsearch.js` will be shimmed.
 
 Firstly, add the following to your project's `package.json`:
--- a/go.mod
+++ b/go.mod
@@ -15,7 +15,7 @@
 	github.com/bep/tmc v0.5.1
 	github.com/disintegration/gift v1.2.1
 	github.com/dustin/go-humanize v1.0.0
-	github.com/evanw/esbuild v0.7.18
+	github.com/evanw/esbuild v0.8.2
 	github.com/fortytw2/leaktest v1.3.0
 	github.com/frankban/quicktest v1.11.1
 	github.com/fsnotify/fsnotify v1.4.9
--- a/go.sum
+++ b/go.sum
@@ -9,12 +9,9 @@
 cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
 cloud.google.com/go v0.46.3 h1:AVXDdKsrtX33oR9fbCMu/+c1o8Ofjq6Ku/MInaLVg5Y=
 cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
-cloud.google.com/go/bigquery v1.0.1 h1:hL+ycaJpVE9M7nLoiXb/Pn10ENE2u+oddxbD8uu0ZVU=
 cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
-cloud.google.com/go/datastore v1.0.0 h1:Kt+gOPPp2LEPWp8CSfxhsM8ik9CcyE/gYu+0r+RnZvM=
 cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
 cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
-cloud.google.com/go/pubsub v1.0.1 h1:W9tAK3E57P75u0XLLR82LZyw8VpAnhmyTOxW9qzmyj8=
 cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
 cloud.google.com/go/storage v1.0.0 h1:VV2nUM3wwLLGh9lSABFgZMjInyUbJeaRSE64WuAIQ+4=
 cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
@@ -54,10 +51,14 @@
 github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
 github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
 github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI=
+github.com/achiku/varfmt v0.0.0-20160708124000-f820e1efecee h1:IfTwtLm+DUeY8kZ8NKSxGRr2kaCe8qqIpJz4Uwh1efU=
+github.com/achiku/varfmt v0.0.0-20160708124000-f820e1efecee/go.mod h1:RKS7P4TSY/jV2QjH/ZxoAE2l4EEXZRPwQ/tIzXiFrk0=
 github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
 github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U=
 github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI=
 github.com/alecthomas/chroma v0.7.2-0.20200305040604-4f3623dce67a/go.mod h1:fv5SzZPFJbwp2NXJWpFIX7DZS4HgV1K4ew4Pc2OZD9s=
+github.com/alecthomas/chroma v0.8.0 h1:HS+HE97sgcqjQGu5uVr8jIE55Mmh5UeQ7kckAhHg2pY=
+github.com/alecthomas/chroma v0.8.0/go.mod h1:sko8vR34/90zvl5QdcUdvzL3J8NKjAUx9va9jPuFNoM=
 github.com/alecthomas/chroma v0.8.1 h1:ym20sbvyC6RXz45u4qDglcgr8E313oPROshcuCHqiEE=
 github.com/alecthomas/chroma v0.8.1/go.mod h1:sko8vR34/90zvl5QdcUdvzL3J8NKjAUx9va9jPuFNoM=
 github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 h1:JHZL0hZKJ1VENNfmXvHbgYlbUOvpzYzvy2aZU5gXVeo=
@@ -75,6 +76,7 @@
 github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
 github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
 github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
+github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
 github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
 github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
 github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
@@ -82,6 +84,20 @@
 github.com/aws/aws-sdk-go v1.15.27/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0=
 github.com/aws/aws-sdk-go v1.18.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
 github.com/aws/aws-sdk-go v1.19.16/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
+github.com/aws/aws-sdk-go v1.34.20 h1:D9otznteZZyN5pRyFETqveYia/85Xzk7+RaPGB1I9fE=
+github.com/aws/aws-sdk-go v1.34.20/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
+github.com/aws/aws-sdk-go v1.34.21 h1:M97FXuiJgDHwD4mXhrIZ7RJ4xXV6uZVPvIC2qb+HfYE=
+github.com/aws/aws-sdk-go v1.34.21/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
+github.com/aws/aws-sdk-go v1.34.22 h1:7V2sKilVVgHqdjbW+O/xaVWYfnmuLwZdF/+6JuUh6Cw=
+github.com/aws/aws-sdk-go v1.34.22/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
+github.com/aws/aws-sdk-go v1.34.26 h1:tw4nsSfGvCDnXt2xPe8NkxIrDui+asAWinMknPLEf80=
+github.com/aws/aws-sdk-go v1.34.26/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
+github.com/aws/aws-sdk-go v1.34.27 h1:qBqccUrlz43Zermh0U1O502bHYZsgMlBm+LUVabzBPA=
+github.com/aws/aws-sdk-go v1.34.27/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
+github.com/aws/aws-sdk-go v1.34.33 h1:ymkFm0rNPEOlgjyX3ojEd4zqzW6kGICBkqWs7LqgHtU=
+github.com/aws/aws-sdk-go v1.34.33/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48=
+github.com/aws/aws-sdk-go v1.34.34 h1:5dC0ZU0xy25+UavGNEkQ/5MOQwxXDA2YXtjCL1HfYKI=
+github.com/aws/aws-sdk-go v1.34.34/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48=
 github.com/aws/aws-sdk-go v1.35.0 h1:Pxqn1MWNfBCNcX7jrXCCTfsKpg5ms2IMUMmmcGtYJuo=
 github.com/aws/aws-sdk-go v1.35.0/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48=
 github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
@@ -104,7 +120,9 @@
 github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI=
 github.com/coreos/bbolt v1.3.2 h1:wZwiHHUieZCquLkDL0B8UhzreNWsPHooDAG3q34zk0s=
 github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
+github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
 github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
+github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
 github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
 github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
 github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
@@ -132,8 +150,26 @@
 github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
 github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
 github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
-github.com/evanw/esbuild v0.7.18 h1:HNMBF6AbyXOhocM4X0WuEQdbfh+/c1URzN0TbihicAA=
-github.com/evanw/esbuild v0.7.18/go.mod h1:mptxmSXIzBIKKCe4jo9A5SToEd1G+AKZ9JmY85dYRJ0=
+github.com/evanw/esbuild v0.6.32 h1:hVuqC+IgEENPWnr0gic01EFgGCmyW8dUPnr78zC7K5k=
+github.com/evanw/esbuild v0.6.32/go.mod h1:mptxmSXIzBIKKCe4jo9A5SToEd1G+AKZ9JmY85dYRJ0=
+github.com/evanw/esbuild v0.7.1 h1:bkC9MpDxHPCLESOf3AQzK1QiyaxbnxFa3XLPnyARLSI=
+github.com/evanw/esbuild v0.7.1/go.mod h1:mptxmSXIzBIKKCe4jo9A5SToEd1G+AKZ9JmY85dYRJ0=
+github.com/evanw/esbuild v0.7.2 h1:LBY35Gw3fKs7jVpsbQwOmw7pJLDHdpliI1Mc/DqP0Hs=
+github.com/evanw/esbuild v0.7.2/go.mod h1:mptxmSXIzBIKKCe4jo9A5SToEd1G+AKZ9JmY85dYRJ0=
+github.com/evanw/esbuild v0.7.4 h1:mLb2tQ9315u23ulh/5Gg8xejOfgqHs2zm7bDNtNnNcM=
+github.com/evanw/esbuild v0.7.4/go.mod h1:mptxmSXIzBIKKCe4jo9A5SToEd1G+AKZ9JmY85dYRJ0=
+github.com/evanw/esbuild v0.7.7 h1:l/M5wHuU738LEX8RyGDP7Zkdrw84j3bpCPrJbKX33Ks=
+github.com/evanw/esbuild v0.7.7/go.mod h1:mptxmSXIzBIKKCe4jo9A5SToEd1G+AKZ9JmY85dYRJ0=
+github.com/evanw/esbuild v0.7.8 h1:DyCpTDLRAtjqRixfXFslGSsYaoKRQfYi+gwGkzW1FHI=
+github.com/evanw/esbuild v0.7.8/go.mod h1:mptxmSXIzBIKKCe4jo9A5SToEd1G+AKZ9JmY85dYRJ0=
+github.com/evanw/esbuild v0.7.9 h1:jXSoYpNpGkOK1VNx3tvd/KnbVbn5ULRYzvkumXaSkxo=
+github.com/evanw/esbuild v0.7.9/go.mod h1:mptxmSXIzBIKKCe4jo9A5SToEd1G+AKZ9JmY85dYRJ0=
+github.com/evanw/esbuild v0.7.15-0.20201011185726-43c0bbcbf178 h1:vFq5Tq6bGzkP8FHlP5LHninOaqOJuwhFi5BMQeXsCf0=
+github.com/evanw/esbuild v0.7.15-0.20201011185726-43c0bbcbf178/go.mod h1:mptxmSXIzBIKKCe4jo9A5SToEd1G+AKZ9JmY85dYRJ0=
+github.com/evanw/esbuild v0.8.1 h1:AqGawd1vAh0l88ZzAyuG9/w4B3Hswt0wM5s05AYHYXo=
+github.com/evanw/esbuild v0.8.1/go.mod h1:mptxmSXIzBIKKCe4jo9A5SToEd1G+AKZ9JmY85dYRJ0=
+github.com/evanw/esbuild v0.8.2 h1:pwvPPsU8dqwBLdPwBmETdp1ccpefC1l+8RKZD1PafcA=
+github.com/evanw/esbuild v0.8.2/go.mod h1:mptxmSXIzBIKKCe4jo9A5SToEd1G+AKZ9JmY85dYRJ0=
 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
 github.com/fortytw2/leaktest v1.2.0 h1:cj6GCiwJDH7l3tMHLjZDo0QqPtrXJiWSI9JgpeQKw+Q=
 github.com/fortytw2/leaktest v1.2.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
@@ -143,6 +179,10 @@
 github.com/frankban/quicktest v1.4.1/go.mod h1:36zfPVQyHxymz4cH7wlDmVwDrJuljRB60qkgn7rorfQ=
 github.com/frankban/quicktest v1.7.2 h1:2QxQoC1TS09S7fhCPsrvqYdvP1H5M1P1ih5ABm3BTYk=
 github.com/frankban/quicktest v1.7.2/go.mod h1:jaStnuzAqU1AJdCO0l53JDCJrVDKcS03DbaAcR7Ks/o=
+github.com/frankban/quicktest v1.10.2 h1:19ARM85nVi4xH7xPXuc5eM/udya5ieh7b/Sv+d844Tk=
+github.com/frankban/quicktest v1.10.2/go.mod h1:K+q6oSqb0W0Ininfk863uOk1lMy69l/P6txr3mVT54s=
+github.com/frankban/quicktest v1.11.0 h1:Yyrghcw93e1jKo4DTZkRFTTFvBsVhzbblBUPNU1vW6Q=
+github.com/frankban/quicktest v1.11.0/go.mod h1:K+q6oSqb0W0Ininfk863uOk1lMy69l/P6txr3mVT54s=
 github.com/frankban/quicktest v1.11.1 h1:stwUsXhUGliQs9t0ZS39BWCltFdOHgABiIlihop8AD4=
 github.com/frankban/quicktest v1.11.1/go.mod h1:K+q6oSqb0W0Ininfk863uOk1lMy69l/P6txr3mVT54s=
 github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
@@ -149,6 +189,10 @@
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
 github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/getkin/kin-openapi v0.14.0 h1:hqwQL7kze/adt0wB+0UJR2nJm+gfUHqM0Gu4D8nByVc=
+github.com/getkin/kin-openapi v0.14.0/go.mod h1:WGRs2ZMM1Q8LR1QBEwUxC6RJEfaBcD0s+pcEVXFuAjw=
+github.com/getkin/kin-openapi v0.22.0 h1:J5IFyKd/5yuB6AZAgwK0CMBKnabWcmkowtsl6bRkz4s=
+github.com/getkin/kin-openapi v0.22.0/go.mod h1:WGRs2ZMM1Q8LR1QBEwUxC6RJEfaBcD0s+pcEVXFuAjw=
 github.com/getkin/kin-openapi v0.22.1 h1:ODA1olTp175o//NfHko/uCAAhwUSfm5P4+K52XvTg4w=
 github.com/getkin/kin-openapi v0.22.1/go.mod h1:WGRs2ZMM1Q8LR1QBEwUxC6RJEfaBcD0s+pcEVXFuAjw=
 github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
@@ -217,6 +261,8 @@
 github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
 github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
 github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
+github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
+github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
 github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
 github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
 github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
@@ -249,6 +295,8 @@
 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
 github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
 github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/jdkato/prose v1.1.1 h1:r6CwY09U97IZNgNQEHoeCh2nvg2e8WCOGjPH/b7lowI=
+github.com/jdkato/prose v1.1.1/go.mod h1:jkF0lkxaX5PFSlk9l4Gh9Y+T57TqUZziWT7uZbW5ADg=
 github.com/jdkato/prose v1.2.0 h1:t/R3H6xOrVuIgNevWiOSJf1kEoeF2VWlrN6w76Tkzow=
 github.com/jdkato/prose v1.2.0/go.mod h1:WC4YKHtBdAMgBdmfdqBmEuVbBD0U5c9HQ6l1U8Cq0ts=
 github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
@@ -255,14 +303,14 @@
 github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
 github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
 github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
+github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc=
+github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
 github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
 github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
-github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
 github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
 github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
 github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
 github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
-github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024 h1:rBMNdlhTLzJjJSDIjNEXX1Pz3Hmwmz91v+zycvx9PJc=
 github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
 github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
@@ -286,6 +334,8 @@
 github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
 github.com/magefile/mage v1.10.0 h1:3HiXzCUY12kh9bIuyXShaVe529fJfyqoVM42o/uom2g=
 github.com/magefile/mage v1.10.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
+github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
+github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
 github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
 github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
 github.com/markbates/inflect v0.0.0-20171215194931-a12c3aec81a6 h1:LZhVjIISSbj8qLf2qDPP0D8z0uvOWAW5C85ly5mJW6c=
@@ -327,6 +377,8 @@
 github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
 github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
 github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
+github.com/nicksnyder/go-i18n v1.10.1 h1:isfg77E/aCD7+0lD/D00ebR2MV5vgeQ276WYyDaCRQc=
+github.com/nicksnyder/go-i18n v1.10.1/go.mod h1:e4Di5xjP9oTVrC6y3C7C0HoSYXjSbhh/dU0eUV32nB4=
 github.com/nicksnyder/go-i18n/v2 v2.1.1 h1:ATCOanRDlrfKVB4WHAdJnLEqZtDmKYsweqsOUYflnBU=
 github.com/nicksnyder/go-i18n/v2 v2.1.1/go.mod h1:d++QJC9ZVf7pa48qrsRWhMJ5pSHIPmS3OLqK1niyLxs=
 github.com/niklasfasching/go-org v1.3.2 h1:ZKTSd+GdJYkoZl1pBXLR/k7DRiRXnmB96TRiHmHdzwI=
@@ -343,6 +395,8 @@
 github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
 github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
 github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
+github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4=
+github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
 github.com/pelletier/go-toml v1.8.1 h1:1Nf83orprkJyknT6h7zbuEGUEjcyVlCxSUGTENmNCRM=
 github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
 github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
@@ -373,6 +427,8 @@
 github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
 github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/rogpeppe/go-internal v1.5.1 h1:asQ0uD7BN9RU5Im41SEEZTwCi/zAXdMOLS3npYaos2g=
+github.com/rogpeppe/go-internal v1.5.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
 github.com/rogpeppe/go-internal v1.6.2 h1:aIihoIOHCiLZHxyoNQ+ABL4NKhFTgKLBdMLyEAh98m0=
 github.com/rogpeppe/go-internal v1.6.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
 github.com/russross/blackfriday v1.5.3-0.20200218234912-41c5fccfd6f6 h1:tlXG832s5pa9x9Gs3Rp2rTvEqjiDEuETUOSfBEiTcns=
@@ -399,12 +455,16 @@
 github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
 github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
 github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
-github.com/spf13/afero v1.4.1 h1:asw9sl74539yqavKaglDM5hFpdJVK0Y5Dr/JOgQ89nQ=
-github.com/spf13/afero v1.4.1/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
+github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc=
+github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
+github.com/spf13/afero v1.4.0 h1:jsLTaI1zwYO3vjrzHalkVcIHXTNmdQFepW4OI8H3+x8=
+github.com/spf13/afero v1.4.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
 github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
 github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
 github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
 github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cobra v0.0.7 h1:FfTH+vuMXOas8jmfb5/M7dzEYx7LpcLb7a0LPe34uOU=
+github.com/spf13/cobra v0.0.7/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
 github.com/spf13/cobra v1.1.1 h1:KfztREH0tPxJJ+geloSLaAkaPkr4ki2Er5quFV1TDo4=
 github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI=
 github.com/spf13/fsync v0.9.0 h1:f9CEt3DOB2mnHxZaftmEOFWjABEvKM/xpf3cUwJrGOY=
@@ -417,6 +477,9 @@
 github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
 github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
+github.com/spf13/viper v1.6.1 h1:VPZzIkznI1YhVMRi6vNFLHSwhnhReBfgTxIPccpfdZk=
+github.com/spf13/viper v1.6.1/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k=
 github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
 github.com/spf13/viper v1.7.1 h1:pM5oEahlgWv/WnHXpgbKz7iLIxRf65tye2Ci+XFK5sk=
 github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
@@ -431,7 +494,6 @@
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
-github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
 github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
@@ -446,6 +508,7 @@
 github.com/uber-go/atomic v1.3.2/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g=
 github.com/uber/jaeger-client-go v2.15.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=
 github.com/uber/jaeger-lib v1.5.0/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
+github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
 github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
 github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
 github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
@@ -452,6 +515,7 @@
 github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I=
 github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y=
 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
+github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
 github.com/yuin/goldmark v1.1.22 h1:0e0f6Zee9SAQ5yOZGNMWaOxqVvcc/9/kUWu/Kl91Jk8=
 github.com/yuin/goldmark v1.1.22/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.2.1 h1:ruQGxdhGHe7FWOJPT0mKs5+pD2Xs1Bm/kdGlHO04FmM=
@@ -486,7 +550,6 @@
 golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
 golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
-golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136 h1:A1gGSx58LAGVHUUsOf7IiR0u8Xb6W51gRwfDBhkdcaw=
 golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
 golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
@@ -498,7 +561,6 @@
 golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
 golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
 golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
-golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs=
 golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
 golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
 golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
@@ -523,6 +585,8 @@
 golang.org/x/net v0.0.0-20190424112056-4829fb13d2c6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
 golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco=
+golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
 golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI=
@@ -532,6 +596,8 @@
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190319182350-c85d3e98c914/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20190523182746-aaccbc9213b0 h1:xFEXbcD0oa/xhqQmMXztdZ0bWvexAWds+8c1gRN8nu0=
+golang.org/x/oauth2 v0.0.0-20190523182746-aaccbc9213b0/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
@@ -600,7 +666,6 @@
 golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc h1:NCy3Ohtk6Iny5V/reW2Ktypo4zIpWBdRJ1uFMjBxdg8=
 golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -620,7 +685,8 @@
 google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
 google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
-google.golang.org/appengine v1.6.1 h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3I=
+google.golang.org/appengine v1.6.0 h1:Tfd7cKwKbFRsI8RMAD3oqqw7JPFRrvFlOsfbgVkjOOw=
+google.golang.org/appengine v1.6.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
 google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19 h1:Lj2SnHtxkRGJDqnGaSjo+CCdIieEnwVazbOXILwQemk=
@@ -630,6 +696,8 @@
 google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
 google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
 google.golang.org/genproto v0.0.0-20190508193815-b515fa19cec8/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190522204451-c2c4e71fbf69 h1:4rNOqY4ULrKzS6twXa619uQgI7h9PaVd4ZhjFQ7C5zs=
+google.golang.org/genproto v0.0.0-20190522204451-c2c4e71fbf69/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
 google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
 google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
 google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
@@ -640,6 +708,8 @@
 google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.19.1/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
 google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
+google.golang.org/grpc v1.21.0 h1:G+97AoqBnmZIT91cLG/EkCoK9NSelj64P8bOHHNmGn0=
+google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
 google.golang.org/grpc v1.21.1 h1:j6XxA85m/6txkUCHvzlV5f+HBNl/1r5cZ2A/3IEFOO8=
 google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
 gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
@@ -667,13 +737,11 @@
 gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
 gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM=
 honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
 pack.ag/amqp v0.8.0/go.mod h1:4/cbmt4EJXSKlG6LCfWHoqmN0uFdy5i/+YFz+fTfhV4=
 pack.ag/amqp v0.11.0/go.mod h1:4/cbmt4EJXSKlG6LCfWHoqmN0uFdy5i/+YFz+fTfhV4=
--- a/hugofs/fileinfo.go
+++ b/hugofs/fileinfo.go
@@ -37,6 +37,7 @@
 const (
 	metaKeyFilename = "filename"
 
+	metaKeySourceRoot                 = "sourceRoot"
 	metaKeyBaseDir                    = "baseDir" // Abs base directory of source file.
 	metaKeyMountRoot                  = "mountRoot"
 	metaKeyModule                     = "module"
@@ -126,6 +127,10 @@
 		return ""
 	}
 	return strings.TrimPrefix(strings.TrimPrefix(f.Filename(), base), filepathSeparator)
+}
+
+func (f FileMeta) SourceRoot() string {
+	return f.stringV(metaKeySourceRoot)
 }
 
 func (f FileMeta) MountRoot() string {
--- a/hugofs/rootmapping_fs.go
+++ b/hugofs/rootmapping_fs.go
@@ -60,6 +60,7 @@
 			rm.Meta = make(FileMeta)
 		}
 
+		rm.Meta[metaKeySourceRoot] = rm.To
 		rm.Meta[metaKeyBaseDir] = rm.ToBasedir
 		rm.Meta[metaKeyMountRoot] = rm.path
 		rm.Meta[metaKeyModule] = rm.Module
--- a/hugolib/hugo_sites.go
+++ b/hugolib/hugo_sites.go
@@ -22,6 +22,8 @@
 	"sync"
 	"sync/atomic"
 
+	"github.com/fsnotify/fsnotify"
+
 	"github.com/gohugoio/hugo/identity"
 
 	radix "github.com/armon/go-radix"
@@ -85,6 +87,10 @@
 	// Keeps track of bundle directories and symlinks to enable partial rebuilding.
 	ContentChanges *contentChangeMap
 
+	// File change events with filename stored in this map will be skipped.
+	skipRebuildForFilenamesMu sync.Mutex
+	skipRebuildForFilenames   map[string]bool
+
 	init *hugoSitesInit
 
 	workers    *para.Workers
@@ -94,6 +100,14 @@
 	*testCounters
 }
 
+// ShouldSkipFileChangeEvent allows skipping filesystem event early before
+// the build is started.
+func (h *HugoSites) ShouldSkipFileChangeEvent(ev fsnotify.Event) bool {
+	h.skipRebuildForFilenamesMu.Lock()
+	defer h.skipRebuildForFilenamesMu.Unlock()
+	return h.skipRebuildForFilenames[ev.Name]
+}
+
 func (h *HugoSites) getContentMaps() *pageMaps {
 	h.contentInit.Do(func() {
 		h.content = newPageMaps(h)
@@ -304,12 +318,13 @@
 	}
 
 	h := &HugoSites{
-		running:      cfg.Running,
-		multilingual: langConfig,
-		multihost:    cfg.Cfg.GetBool("multihost"),
-		Sites:        sites,
-		workers:      workers,
-		numWorkers:   numWorkers,
+		running:                 cfg.Running,
+		multilingual:            langConfig,
+		multihost:               cfg.Cfg.GetBool("multihost"),
+		Sites:                   sites,
+		workers:                 workers,
+		numWorkers:              numWorkers,
+		skipRebuildForFilenames: make(map[string]bool),
 		init: &hugoSitesInit{
 			data:         lazy.New(),
 			layouts:      lazy.New(),
--- a/hugolib/hugo_sites_build.go
+++ b/hugolib/hugo_sites_build.go
@@ -33,8 +33,6 @@
 
 	"github.com/spf13/afero"
 
-	"github.com/gohugoio/hugo/resources/resource"
-
 	"github.com/gohugoio/hugo/output"
 
 	"github.com/pkg/errors"
@@ -351,14 +349,45 @@
 		return err
 	}
 
-	var toPostProcess []resource.OriginProvider
-	for _, s := range h.Sites {
-		for _, v := range s.ResourceSpec.PostProcessResources {
-			toPostProcess = append(toPostProcess, v)
+	// This will only be set when js.Build have been triggered with
+	// imports that resolves to the project or a module.
+	// Write a jsconfig.json file to the project's /asset directory
+	// to help JS intellisense in VS Code etc.
+	if !h.ResourceSpec.BuildConfig.NoJSConfigInAssets && h.BaseFs.Assets.Dirs != nil {
+		m := h.BaseFs.Assets.Dirs[0].Meta()
+		assetsDir := m.Filename()
+		if strings.HasPrefix(assetsDir, h.ResourceSpec.WorkingDir) {
+			if jsConfig := h.ResourceSpec.JSConfigBuilder.Build(assetsDir); jsConfig != nil {
+
+				b, err := json.MarshalIndent(jsConfig, "", " ")
+				if err != nil {
+					h.Log.Warnf("Failed to create jsconfig.json: %s", err)
+
+				} else {
+					filename := filepath.Join(assetsDir, "jsconfig.json")
+					if h.running {
+						h.skipRebuildForFilenamesMu.Lock()
+						h.skipRebuildForFilenames[filename] = true
+						h.skipRebuildForFilenamesMu.Unlock()
+					}
+					// Make sure it's  written to the OS fs as this is used by
+					// editors.
+					if err := afero.WriteFile(hugofs.Os, filename, b, 0666); err != nil {
+						h.Log.Warnf("Failed to write jsconfig.json: %s", err)
+					}
+				}
+			}
+
 		}
 	}
 
+	var toPostProcess []postpub.PostPublishedResource
+	for _, r := range h.ResourceSpec.PostProcessResources {
+		toPostProcess = append(toPostProcess, r)
+	}
+
 	if len(toPostProcess) == 0 {
+		// Nothing more to do.
 		return nil
 	}
 
--- a/hugolib/js_test.go
+++ b/hugolib/js_test.go
@@ -14,6 +14,7 @@
 package hugolib
 
 import (
+	"fmt"
 	"os"
 	"os/exec"
 	"path/filepath"
@@ -22,7 +23,6 @@
 
 	"github.com/gohugoio/hugo/htesting"
 
-	"github.com/spf13/afero"
 	"github.com/spf13/viper"
 
 	qt "github.com/frankban/quicktest"
@@ -82,9 +82,7 @@
   "scripts": {},
 
   "dependencies": {
-		"to-camel-case": "1.0.0",
-		"react": "^16",
-		"react-dom": "^16"
+    "to-camel-case": "1.0.0"
   }
 }
 `
@@ -153,333 +151,46 @@
 
 	c := qt.New(t)
 
-	mainJS := `
-	import "./included";
-	
-	console.log("main");
-
-`
-	includedJS := `
-	console.log("included");
-	
-	`
-
-	workDir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugo-test-js")
+	workDir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugo-test-js-mod")
 	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())
+	config := fmt.Sprintf(`
+baseURL = "https://example.org"
+workingDir = %q
 
-	b.Fs = hugofs.NewDefault(v)
-	b.WithWorkingDir(workDir)
-	b.WithViper(v)
-	b.WithContent("p1.md", "")
+disableKinds = ["page", "section", "term", "taxonomy"]
 
-	b.WithTemplates("index.html", `
-{{ $js := resources.Get "js/main.js" | js.Build }}
-JS:  {{ template "print" $js }}
-
-
-{{ define "print" }}RelPermalink: {{.RelPermalink}}|MIME: {{ .MediaType }}|Content: {{ .Content | safeJS }}{{ end }}
-
-`)
-
-	jsDir := filepath.Join(workDir, "assets", "js")
-	b.Assert(os.MkdirAll(jsDir, 0777), qt.IsNil)
-	b.Assert(os.Chdir(workDir), qt.IsNil)
-	b.WithSourceFile("assets/js/main.js", mainJS)
-	b.WithSourceFile("assets/js/included.js", includedJS)
-
-	b.Build(BuildCfg{})
-
-	b.AssertFileContent("public/index.html", `
-console.log(&#34;included&#34;);
-
-`)
-
-}
-
-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"
-`)
+path="github.com/gohugoio/hugoTestProjectJSModImports"
 
-	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") }}
+`, workDir)
 
-{{- $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") }}
+	b := newTestSitesBuilder(t)
+	b.Fs = hugofs.NewDefault(viper.New())
+	b.WithWorkingDir(workDir).WithConfigFile("toml", config).WithLogger(loggers.NewInfoLogger())
+	b.WithSourceFile("go.mod", `module github.com/gohugoio/tests/testHugoModules
+        
+go 1.15
+        
+require github.com/gohugoio/hugoTestProjectJSModImports v0.3.0 // indirect
 
-{{- $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;
-`)
+	b.WithContent("p1.md", "").WithNothingAdded()
 
-	// 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
-`)
+	b.AssertFileContent("public/js/main.js", `
+greeting: "greeting configured in mod2"
+Hello1 from mod1: $
+return "Hello2 from mod1";
+var Hugo = "Rocks!";
+return "Hello3 from mod2";
+return "Hello from lib in the main project";
+var myparam = "Hugo Rocks!";`)
+
 }
--- a/hugolib/site.go
+++ b/hugolib/site.go
@@ -997,6 +997,16 @@
 	return filtered
 }
 
+var (
+	// These are only used for cache busting, so false positives are fine.
+	// We also deliberately do not match for file suffixes to also catch
+	// directory names.
+	// TODO(bep) consider this when completing the relevant PR rewrite on this.
+	cssFileRe   = regexp.MustCompile("(css|sass|scss)")
+	cssConfigRe = regexp.MustCompile(`(postcss|tailwind)\.config\.js`)
+	jsFileRe    = regexp.MustCompile("(js|ts|jsx|tsx)")
+)
+
 // reBuild partially rebuilds a site given the filesystem events.
 // It returns whetever the content source was changed.
 // TODO(bep) clean up/rewrite this method.
@@ -1028,20 +1038,25 @@
 		logger = helpers.NewDistinctFeedbackLogger()
 	)
 
-	var isCSSConfigRe = regexp.MustCompile(`(postcss|tailwind)\.config\.js`)
-	var isCSSFileRe = regexp.MustCompile(`\.(css|scss|sass)`)
-
 	var cachePartitions []string
 	// Special case
 	// TODO(bep) I have a ongoing branch where I have redone the cache. Consider this there.
-	var isCSSChange bool
+	var (
+		evictCSSRe *regexp.Regexp
+		evictJSRe  *regexp.Regexp
+	)
 
 	for _, ev := range events {
 		if assetsFilename := s.BaseFs.Assets.MakePathRelative(ev.Name); assetsFilename != "" {
 			cachePartitions = append(cachePartitions, resources.ResourceKeyPartitions(assetsFilename)...)
-			if !isCSSChange {
-				isCSSChange = isCSSFileRe.MatchString(assetsFilename) || isCSSConfigRe.MatchString(assetsFilename)
+			if evictCSSRe == nil {
+				if cssFileRe.MatchString(assetsFilename) || cssConfigRe.MatchString(assetsFilename) {
+					evictCSSRe = cssFileRe
+				}
 			}
+			if evictJSRe == nil && jsFileRe.MatchString(assetsFilename) {
+				evictJSRe = jsFileRe
+			}
 		}
 
 		id, found := s.eventToIdentity(ev)
@@ -1088,8 +1103,11 @@
 	// These in memory resource caches will be rebuilt on demand.
 	for _, s := range s.h.Sites {
 		s.ResourceSpec.ResourceCache.DeletePartitions(cachePartitions...)
-		if isCSSChange {
-			s.ResourceSpec.ResourceCache.DeleteContains("css", "scss", "sass")
+		if evictCSSRe != nil {
+			s.ResourceSpec.ResourceCache.DeleteMatches(evictCSSRe)
+		}
+		if evictJSRe != nil {
+			s.ResourceSpec.ResourceCache.DeleteMatches(evictJSRe)
 		}
 	}
 
--- /dev/null
+++ b/resources/jsconfig/jsconfig.go
@@ -1,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),
+		},
+	}
+}
--- /dev/null
+++ b/resources/jsconfig/jsconfig_test.go
@@ -1,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)
+}
--- a/resources/resource_cache.go
+++ b/resources/resource_cache.go
@@ -18,6 +18,7 @@
 	"io"
 	"path"
 	"path/filepath"
+	"regexp"
 	"strings"
 	"sync"
 
@@ -296,21 +297,15 @@
 
 }
 
-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)
 		}
+
 	}
 
 }
--- a/resources/resource_spec.go
+++ b/resources/resource_spec.go
@@ -23,6 +23,8 @@
 	"strings"
 	"sync"
 
+	"github.com/gohugoio/hugo/resources/jsconfig"
+
 	"github.com/gohugoio/hugo/common/herrors"
 
 	"github.com/gohugoio/hugo/config"
@@ -76,17 +78,20 @@
 	}
 
 	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 @@
 	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) {
--- a/resources/resource_transformers/js/build.go
+++ b/resources/resource_transformers/js/build.go
@@ -14,109 +14,46 @@
 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/gohugoio/hugo/helpers"
+	"github.com/spf13/afero"
+
+	"github.com/gohugoio/hugo/common/herrors"
+
 	"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 {
@@ -123,13 +60,6 @@
 	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,341 +79,62 @@
 		return err
 	}
 
-	sdir, sfile := filepath.Split(t.sfs.RealFilename(ctx.SourcePath))
-	opts.workDir, err = filepath.Abs(t.rs.WorkingDir)
+	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
+
+	buildOptions, err := toBuildOptions(opts)
 	if err != nil {
 		return err
 	}
 
-	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")
+	buildOptions.Plugins, err = createBuildPlugins(t.c, opts)
 	if err != nil {
 		return err
 	}
 
-	filesToDelete := make([]*os.File, 0)
+	result := api.Build(buildOptions)
 
-	defer func() {
-		for _, file := range filesToDelete {
-			os.Remove(file.Name())
-		}
-	}()
+	if len(result.Errors) > 0 {
+		first := result.Errors[0]
+		loc := first.Location
+		path := loc.File
 
-	filesToDelete = append(filesToDelete, newTSConfig)
-	configDir, _ := filepath.Split(newTSConfig.Name())
+		var err error
+		var f afero.File
+		var filename string
 
-	// 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 !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)
+		return fmt.Errorf("%s", result.Errors[0].Text)
 	}
 
-	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)
-	}
+	ctx.To.Write(result.OutputFiles[0].Contents)
 	return nil
 }
 
@@ -490,124 +141,6 @@
 // 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
-
 }
--- 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{},
-	})
-
-}
--- /dev/null
+++ b/resources/resource_transformers/js/options.go
@@ -1,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
+
+}
--- /dev/null
+++ b/resources/resource_transformers/js/options_test.go
@@ -1,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,
+		},
+	})
+
+}