Make js.Build fully support modules
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Mon, 5 Oct 2020 11:34:14 +0000 (13:34 +0200)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Tue, 3 Nov 2020 12:04:37 +0000 (13:04 +0100)
Fixes #7816
Fixes #7777
Fixes #7916

22 files changed:
.travis.yml
commands/hugo.go
config/commonConfig.go
deps/deps.go
docs/content/en/getting-started/configuration.md
docs/content/en/hugo-pipes/js.md
go.mod
go.sum
hugofs/fileinfo.go
hugofs/rootmapping_fs.go
hugolib/hugo_sites.go
hugolib/hugo_sites_build.go
hugolib/js_test.go
hugolib/site.go
resources/jsconfig/jsconfig.go [new file with mode: 0644]
resources/jsconfig/jsconfig_test.go [new file with mode: 0644]
resources/resource_cache.go
resources/resource_spec.go
resources/resource_transformers/js/build.go
resources/resource_transformers/js/build_test.go
resources/resource_transformers/js/options.go [new file with mode: 0644]
resources/resource_transformers/js/options_test.go [new file with mode: 0644]

index bff54e4dd50236af67d545ad406941b150fa11c1..9ff6029b18c1b7455bc41b67703119e2f492ccfd 100644 (file)
@@ -47,10 +47,10 @@ cache:
 
 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 @@ install:
 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/
index 058f1ec7ce779c0828e47b0536750e4b50818463..58f33b7752e940113f3dd9c7be7922e2f3b01944 100644 (file)
@@ -984,9 +984,11 @@ func (c *commandeer) handleEvents(watcher *watcher.Batcher,
        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 {
index 522ced85412561a54cc04bbccd1c9a03753f0252..9b4edfd90f92cf037dd30523c3685116a4a72e95 100644 (file)
@@ -41,6 +41,10 @@ type Build struct {
        // 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 {
index b70dcb5a0222913e03b88261ad49298404e9889c..f6b64c279e393a5e1eb7311f5b5d83635b0a8bf6 100644 (file)
@@ -316,14 +316,16 @@ func (d Deps) ForLanguage(cfg DepsCfg, onCreated func(d *Deps) error) (*Deps, er
 
        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
index 392f71a66f901b092dc332050058de9b5a7ccc09..fda7e2327a55e56a2d5c1db6b47a8345091ddac5 100644 (file)
@@ -304,6 +304,7 @@ The `build` configuration section contains global build-related configuration op
 [build]
 useResourceCacheWhen="fallback"
 writeStats = false
+noJSConfigInAssets = false
 {{< /code-toggle >}}
 
 
@@ -313,6 +314,9 @@ useResourceCacheWhen
 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
 
 {{< new-in "0.67.0" >}}
index e7a0e90079ed5649a11b0d0365f6a9966097effb..5e9c027d577d8898b1b31d7f8b0888078eb1b12c 100644 (file)
@@ -23,6 +23,20 @@ targetPath [string]
 : 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,7 +64,51 @@ defines [map]
 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
 
@@ -69,7 +127,8 @@ Or with options:
 <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`:
diff --git a/go.mod b/go.mod
index d349c8a6bedcb1a6c07bd23384b9b74eb102abad..f48a2619ce4af51ef20579a74350be95a587144e 100644 (file)
--- a/go.mod
+++ b/go.mod
@@ -15,7 +15,7 @@ require (
        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
diff --git a/go.sum b/go.sum
index 178b2d277a4aece90348fb05a0590ae93070ee39..b240a8a77ba34d03f9c0d3b30b9ce8f435d81ab2 100644 (file)
--- a/go.sum
+++ b/go.sum
@@ -9,12 +9,9 @@ cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxK
 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 h1:d+Bc7a5rLufV
 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/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
 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/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgI
 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/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
 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/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25Kn
 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,12 +179,20 @@ github.com/frankban/quicktest v1.4.1 h1:Wv2VwvNn73pAdFIVUQRXYDFp31lXKbqblIXo/Q5G
 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=
 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/handlers v1.4.1/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/
 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,20 +295,22 @@ github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/J
 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=
 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/kyokomi/emoji v2.2.4+incompatible/go.mod h1:mZ6aGCD7yk8j6QY6KICwnZ2px
 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/muesli/smartcrop v0.3.0/go.mod h1:i2fCI/UorTfgEpPPLWiFBv4pye+YAG78Rwc
 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/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJ
 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/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40T
 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/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4k
 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 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
 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.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
 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,12 +508,14 @@ github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1
 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=
 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-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL
 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-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTk
 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-20190420063019-afa5a82059c6/go.mod h1:t9HGtf8HONx5eT2rtn
 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 h1:Wo7BWFiOk0QRFMLYMqJGFM
 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-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgw
 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.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9Ywl
 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-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRn
 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 h1:cfg4PD8YEdSFnm7qLV4++93WcmhH2nIUhMjhdCvl3j8=
 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.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 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=
index 79d89a88b56cb78d5b5ffc7a400dfd6c172608dd..5bfb1ff3233f71cd2f48c747e9ce5b6348d9cab4 100644 (file)
@@ -37,6 +37,7 @@ import (
 const (
        metaKeyFilename = "filename"
 
+       metaKeySourceRoot                 = "sourceRoot"
        metaKeyBaseDir                    = "baseDir" // Abs base directory of source file.
        metaKeyMountRoot                  = "mountRoot"
        metaKeyModule                     = "module"
@@ -128,6 +129,10 @@ func (f FileMeta) PathFile() string {
        return strings.TrimPrefix(strings.TrimPrefix(f.Filename(), base), filepathSeparator)
 }
 
+func (f FileMeta) SourceRoot() string {
+       return f.stringV(metaKeySourceRoot)
+}
+
 func (f FileMeta) MountRoot() string {
        return f.stringV(metaKeyMountRoot)
 }
index 2c4f0df52e448c07374b1fe47ccb35faf279df53..a38560d0a1b2a6be0f01c1870a0d5842a13e65d0 100644 (file)
@@ -60,6 +60,7 @@ func NewRootMappingFs(fs afero.Fs, rms ...RootMapping) (*RootMappingFs, error) {
                        rm.Meta = make(FileMeta)
                }
 
+               rm.Meta[metaKeySourceRoot] = rm.To
                rm.Meta[metaKeyBaseDir] = rm.ToBasedir
                rm.Meta[metaKeyMountRoot] = rm.path
                rm.Meta[metaKeyModule] = rm.Module
index 1a6a07b034fbf861fe44c1e2e8145c724650e891..25ae3dd196f4747c1b37edd7171c83d22fb8418a 100644 (file)
@@ -22,6 +22,8 @@ import (
        "sync"
        "sync/atomic"
 
+       "github.com/fsnotify/fsnotify"
+
        "github.com/gohugoio/hugo/identity"
 
        radix "github.com/armon/go-radix"
@@ -85,6 +87,10 @@ type HugoSites struct {
        // 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 @@ type HugoSites struct {
        *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 @@ func newHugoSites(cfg deps.DepsCfg, sites ...*Site) (*HugoSites, error) {
        }
 
        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(),
index 3c0440a976833fa435b371c629955244d7596c67..603772afd546d7ea6dc466766f699d96eed89acd 100644 (file)
@@ -33,8 +33,6 @@ import (
 
        "github.com/spf13/afero"
 
-       "github.com/gohugoio/hugo/resources/resource"
-
        "github.com/gohugoio/hugo/output"
 
        "github.com/pkg/errors"
@@ -351,14 +349,45 @@ func (h *HugoSites) postProcess() error {
                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
        }
 
index e34ce386790a267cd34ed4add2673b83480b6d1b..6c27219f332eb11545faa0842a7d44f3ed421b06 100644 (file)
@@ -14,6 +14,7 @@
 package hugolib
 
 import (
+       "fmt"
        "os"
        "os/exec"
        "path/filepath"
@@ -22,7 +23,6 @@ import (
 
        "github.com/gohugoio/hugo/htesting"
 
-       "github.com/spf13/afero"
        "github.com/spf13/viper"
 
        qt "github.com/frankban/quicktest"
@@ -82,9 +82,7 @@ document.body.textContent = greeter(user);`
   "scripts": {},
 
   "dependencies": {
-               "to-camel-case": "1.0.0",
-               "react": "^16",
-               "react-dom": "^16"
+    "to-camel-case": "1.0.0"
   }
 }
 `
@@ -153,333 +151,46 @@ func TestJSBuild(t *testing.T) {
 
        c := qt.New(t)
 
-       mainJS := `
-       import "./included";
-       
-       console.log("main");
-
-`
-       includedJS := `
-       console.log("included");
-       
-       `
-
-       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", "")
-
-       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")
+       workDir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugo-test-js-mod")
        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)
-       }
+       config := fmt.Sprintf(`
+baseURL = "https://example.org"
+workingDir = %q
 
-       realWrite("config.toml", `
-baseURL="https://example.org"
+disableKinds = ["page", "section", "term", "taxonomy"]
 
 [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") }}
-
-{{- $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") }}
+       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
 
-{{- define "print" -}}
-{{ printf "rellink-%s-%s" .name .js.RelPermalink | safeHTML }}
-{{ printf "mime-%s-%s" .name .js.MediaType | safeHTML }}
-{{ printf "content-%s-%s" .name .js.Content | safeHTML }}
-{{- end -}}
 `)
 
-       // Override project included file
-       // This file will override the one in mod1 and mod2
-       realWrite("assets/js/override.js", `
-const name = "root-override";
-export default name;
-`)
-
-       // Add empty theme mod config files
-       realWrite("themes/mod1/config.yml", ``)
-       realWrite("themes/mod2/config.yml", ``)
-
-       // This is the main project js file.
-       // try to include @js/override which is overridden inside of project
-       // try to include @js/override-mod which is overridden in mod2
-       realWrite("assets/js/main-project.js", `
-import override from "@js/override";
-import overrideMod from "@js/override-mod";
-window.override = override; // make sure to prevent tree-shake
-window.overrideMod  = overrideMod; // make sure to prevent tree-shake
-`)
-       // This is the mod1 js file
-       // try to include @js/override which is overridden inside of the project
-       // try to include @js/override-mod which is overridden in mod2
-       realWrite("themes/mod1/assets/js/main-mod1.js", `
-import override from "@js/override";
-import overrideMod from "@js/override-mod";
-window.mod = "mod1";
-window.override = override; // make sure to prevent tree-shake
-window.overrideMod  = overrideMod; // make sure to prevent tree-shake
-`)
-       // This is the mod1 js file (overridden in mod2)
-       // try to include @js/override which is overridden inside of the project
-       // try to include @js/override-mod which is overridden in mod2
-       realWrite("themes/mod2/assets/js/main-mod1.js", `
-import override from "@js/override";
-import overrideMod from "@js/override-mod";
-window.mod = "mod2";
-window.override = override; // make sure to prevent tree-shake
-window.overrideMod  = overrideMod; // make sure to prevent tree-shake
-`)
-       // This is mod2 js file
-       // try to include @js/override which is overridden inside of the project
-       // try to include @js/override-mod which is overridden in mod2
-       // try to include @config which is declared in a local jsconfig.json file
-       // try to include @data which was passed as "data" into js.Build
-       realWrite("themes/mod2/assets/js/main-mod2.js", `
-import override from "@js/override";
-import overrideMod from "@js/override-mod";
-import config from "@config";
-import data from "@data";
-window.data = data;
-window.override = override; // make sure to prevent tree-shake
-window.overrideMod  = overrideMod; // make sure to prevent tree-shake
-window.config = config;
-`)
-       realWrite("themes/mod2/assets/js/jsconfig.json", `
-{
-       "compilerOptions": {
-               "baseUrl": ".",
-               "paths": {
-                       "@config": ["./config.json"]
-               }
-       }
-}
-`)
-       realWrite("themes/mod2/assets/js/config.json", `
-{
-       "data": {
-               "sample": "sample"
-       }
-}
-`)
-       realWrite("themes/mod1/assets/js/override.js", `
-const name = "mod1-override";
-export default name;
-`)
-       realWrite("themes/mod2/assets/js/override.js", `
-const name = "mod2-override";
-export default name;
-`)
-       realWrite("themes/mod1/assets/js/override-mod.js", `
-const nameMod = "mod1-override";
-export default nameMod;
-`)
-       realWrite("themes/mod2/assets/js/override-mod.js", `
-const nameMod = "mod2-override";
-export default nameMod;
-`)
-       b.WithConfigFile("toml", `
-baseURL="https://example.org"
-themesDir="./themes"
-[module]
-[[module.imports]]
-path="mod2"
-[[module.imports.mounts]]
-source="assets"
-target="assets"
-[[module.imports.mounts]]
-source="layouts"
-target="layouts"
-[[module.imports]]
-path="mod1"
-[[module.imports.mounts]]
-source="assets"
-target="assets"
-[[module.imports.mounts]]
-source="layouts"
-target="layouts"
-`)
-
-       b.WithWorkingDir(workDir)
-       b.LoadConfig()
+       b.WithContent("p1.md", "").WithNothingAdded()
 
        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!";`)
+
 }
index ec29395300222b22a17ecd4735e4d494d17405a5..3679e354cca2410befeb1e1ffb77d333dc9702c6 100644 (file)
@@ -997,6 +997,16 @@ func (s *Site) translateFileEvents(events []fsnotify.Event) []fsnotify.Event {
        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,19 +1038,24 @@ func (s *Site) processPartial(config *BuildCfg, init func(config *BuildCfg) erro
                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
                        }
                }
 
@@ -1088,8 +1103,11 @@ func (s *Site) processPartial(config *BuildCfg, init func(config *BuildCfg) erro
        // 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)
                }
        }
 
diff --git a/resources/jsconfig/jsconfig.go b/resources/jsconfig/jsconfig.go
new file mode 100644 (file)
index 0000000..9b399bf
--- /dev/null
@@ -0,0 +1,93 @@
+// Copyright 2020 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package jsconfig
+
+import (
+       "path/filepath"
+       "sort"
+       "sync"
+)
+
+// Builder builds a jsconfig.json file that, currently, is used only to assist
+// intellinsense in editors.
+type Builder struct {
+       sourceRootsMu sync.RWMutex
+       sourceRoots   map[string]bool
+}
+
+// NewBuilder creates a new Builder.
+func NewBuilder() *Builder {
+       return &Builder{sourceRoots: make(map[string]bool)}
+}
+
+// Build builds a new Config with paths relative to dir.
+// This method is thread safe.
+func (b *Builder) Build(dir string) *Config {
+       b.sourceRootsMu.RLock()
+       defer b.sourceRootsMu.RUnlock()
+
+       if len(b.sourceRoots) == 0 {
+               return nil
+       }
+       conf := newJSConfig()
+
+       var roots []string
+       for root := range b.sourceRoots {
+               rel, err := filepath.Rel(dir, filepath.Join(root, "*"))
+               if err == nil {
+                       roots = append(roots, rel)
+               }
+       }
+       sort.Strings(roots)
+       conf.CompilerOptions.Paths["*"] = roots
+
+       return conf
+}
+
+// AddSourceRoot adds a new source root.
+// This method is thread safe.
+func (b *Builder) AddSourceRoot(root string) {
+       b.sourceRootsMu.RLock()
+       found := b.sourceRoots[root]
+       b.sourceRootsMu.RUnlock()
+
+       if found {
+               return
+       }
+
+       b.sourceRootsMu.Lock()
+       b.sourceRoots[root] = true
+       b.sourceRootsMu.Unlock()
+
+}
+
+// CompilerOptions holds compilerOptions for jsonconfig.json.
+type CompilerOptions struct {
+       BaseURL string              `json:"baseUrl"`
+       Paths   map[string][]string `json:"paths"`
+}
+
+// Config holds the data for jsconfig.json.
+type Config struct {
+       CompilerOptions CompilerOptions `json:"compilerOptions"`
+}
+
+func newJSConfig() *Config {
+       return &Config{
+               CompilerOptions: CompilerOptions{
+                       BaseURL: ".",
+                       Paths:   make(map[string][]string),
+               },
+       }
+}
diff --git a/resources/jsconfig/jsconfig_test.go b/resources/jsconfig/jsconfig_test.go
new file mode 100644 (file)
index 0000000..9a96578
--- /dev/null
@@ -0,0 +1,35 @@
+// Copyright 2020 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package jsconfig
+
+import (
+       "path/filepath"
+       "testing"
+
+       qt "github.com/frankban/quicktest"
+)
+
+func TestJsConfigBuilder(t *testing.T) {
+       c := qt.New(t)
+
+       b := NewBuilder()
+       b.AddSourceRoot("/c/assets")
+       b.AddSourceRoot("/d/assets")
+
+       conf := b.Build("/a/b")
+       c.Assert(conf.CompilerOptions.BaseURL, qt.Equals, ".")
+       c.Assert(conf.CompilerOptions.Paths["*"], qt.DeepEquals, []string{filepath.FromSlash("../../c/assets/*"), filepath.FromSlash("../../d/assets/*")})
+
+       c.Assert(NewBuilder().Build("/a/b"), qt.IsNil)
+}
index feaa94f5cf0723d36bfae3f79238ae2afd7332f8..6c4ba951bdf08dfa964015f54327f237b7d5bc9c 100644 (file)
@@ -18,6 +18,7 @@ import (
        "io"
        "path"
        "path/filepath"
+       "regexp"
        "strings"
        "sync"
 
@@ -296,21 +297,15 @@ func (c *ResourceCache) DeletePartitions(partitions ...string) {
 
 }
 
-func (c *ResourceCache) DeleteContains(parts ...string) {
+func (c *ResourceCache) DeleteMatches(re *regexp.Regexp) {
        c.Lock()
        defer c.Unlock()
 
        for k := range c.cache {
-               clear := false
-               for _, part := range parts {
-                       if strings.Contains(k, part) {
-                               clear = true
-                               break
-                       }
-               }
-               if clear {
+               if re.MatchString(k) {
                        delete(c.cache, k)
                }
+
        }
 
 }
index 17225e3f5f904d3cfcd2363382577ad6053ef69e..0ca60fe31363e1b90d746d66cefffa765b275779 100644 (file)
@@ -23,6 +23,8 @@ import (
        "strings"
        "sync"
 
+       "github.com/gohugoio/hugo/resources/jsconfig"
+
        "github.com/gohugoio/hugo/common/herrors"
 
        "github.com/gohugoio/hugo/config"
@@ -76,17 +78,20 @@ func NewSpec(
        }
 
        rs := &Spec{
-               PathSpec:             s,
-               Logger:               logger,
-               ErrorSender:          errorHandler,
-               imaging:              imaging,
-               incr:                 incr,
-               MediaTypes:           mimeTypes,
-               OutputFormats:        outputFormats,
-               Permalinks:           permalinks,
-               BuildConfig:          config.DecodeBuild(s.Cfg),
-               FileCaches:           fileCaches,
-               PostProcessResources: make(map[string]postpub.PostPublishedResource),
+               PathSpec:      s,
+               Logger:        logger,
+               ErrorSender:   errorHandler,
+               imaging:       imaging,
+               incr:          incr,
+               MediaTypes:    mimeTypes,
+               OutputFormats: outputFormats,
+               Permalinks:    permalinks,
+               BuildConfig:   config.DecodeBuild(s.Cfg),
+               FileCaches:    fileCaches,
+               PostBuildAssets: &PostBuildAssets{
+                       PostProcessResources: make(map[string]postpub.PostPublishedResource),
+                       JSConfigBuilder:      jsconfig.NewBuilder(),
+               },
                imageCache: newImageCache(
                        fileCaches.ImageCache(),
 
@@ -121,8 +126,15 @@ type Spec struct {
        ResourceCache *ResourceCache
        FileCaches    filecache.Caches
 
+       // Assets used after the build is done.
+       // This is shared between all sites.
+       *PostBuildAssets
+}
+
+type PostBuildAssets struct {
        postProcessMu        sync.RWMutex
        PostProcessResources map[string]postpub.PostPublishedResource
+       JSConfigBuilder      *jsconfig.Builder
 }
 
 func (r *Spec) New(fd ResourceSourceDescriptor) (resource.Resource, error) {
index d316bc85b6496a75a5e5e11b04b34905f83cbd81..8a7c21592ef44a32e04ec7be67f0a03763836b01 100644 (file)
 package js
 
 import (
-       "encoding/json"
+       "errors"
        "fmt"
        "io/ioutil"
        "os"
        "path"
        "path/filepath"
-       "reflect"
        "strings"
 
-       "github.com/achiku/varfmt"
-       "github.com/spf13/cast"
+       "github.com/gohugoio/hugo/hugofs"
+
+       "github.com/spf13/afero"
+
+       "github.com/gohugoio/hugo/common/herrors"
 
-       "github.com/gohugoio/hugo/helpers"
        "github.com/gohugoio/hugo/hugolib/filesystems"
        "github.com/gohugoio/hugo/media"
        "github.com/gohugoio/hugo/resources/internal"
 
-       "github.com/mitchellh/mapstructure"
-
        "github.com/evanw/esbuild/pkg/api"
        "github.com/gohugoio/hugo/resources"
        "github.com/gohugoio/hugo/resources/resource"
 )
 
-// Options esbuild configuration
-type Options struct {
-       // If not set, the source path will be used as the base target path.
-       // Note that the target path's extension may change if the target MIME type
-       // is different, e.g. when the source is TypeScript.
-       TargetPath string
-
-       // Whether to minify to output.
-       Minify bool
-
-       // Whether to write mapfiles
-       SourceMap string
-
-       // The language target.
-       // One of: es2015, es2016, es2017, es2018, es2019, es2020 or esnext.
-       // Default is esnext.
-       Target string
-
-       // The output format.
-       // One of: iife, cjs, esm
-       // Default is to esm.
-       Format string
-
-       // External dependencies, e.g. "react".
-       Externals []string `hash:"set"`
-
-       // User defined symbols.
-       Defines map[string]interface{}
-
-       // User defined data (must be JSON marshall'able)
-       Data interface{}
-
-       // What to use instead of React.createElement.
-       JSXFactory string
-
-       // What to use instead of React.Fragment.
-       JSXFragment string
-
-       mediaType  media.Type
-       outDir     string
-       contents   string
-       sourcefile string
-       resolveDir string
-       workDir    string
-       tsConfig   string
-}
-
-func decodeOptions(m map[string]interface{}) (Options, error) {
-       var opts Options
-
-       if err := mapstructure.WeakDecode(m, &opts); err != nil {
-               return opts, err
-       }
-
-       if opts.TargetPath != "" {
-               opts.TargetPath = helpers.ToSlashTrimLeading(opts.TargetPath)
-       }
-
-       opts.Target = strings.ToLower(opts.Target)
-       opts.Format = strings.ToLower(opts.Format)
-
-       return opts, nil
-}
-
-// Client context for esbuild
+// Client context for ESBuild.
 type Client struct {
        rs  *resources.Spec
        sfs *filesystems.SourceFilesystem
 }
 
-// New create new client context
+// New creates a new client context.
 func New(fs *filesystems.SourceFilesystem, rs *resources.Spec) *Client {
-       return &Client{rs: rs, sfs: fs}
+       return &Client{
+               rs:  rs,
+               sfs: fs,
+       }
 }
 
 type buildTransformation struct {
        optsm map[string]interface{}
-       rs    *resources.Spec
-       sfs   *filesystems.SourceFilesystem
+       c     *Client
 }
 
 func (t *buildTransformation) Key() internal.ResourceTransformationKey {
        return internal.NewResourceTransformationKey("jsbuild", t.optsm)
 }
 
-func appendExts(list []string, rel string) []string {
-       for _, ext := range []string{".tsx", ".ts", ".jsx", ".mjs", ".cjs", ".js", ".json"} {
-               list = append(list, fmt.Sprintf("%s/index%s", rel, ext))
-       }
-       return list
-}
-
 func (t *buildTransformation) Transform(ctx *resources.ResourceTransformationCtx) error {
        ctx.OutMediaType = media.JavascriptType
 
@@ -149,465 +79,68 @@ func (t *buildTransformation) Transform(ctx *resources.ResourceTransformationCtx
                return err
        }
 
-       sdir, sfile := filepath.Split(t.sfs.RealFilename(ctx.SourcePath))
-       opts.workDir, err = filepath.Abs(t.rs.WorkingDir)
-       if err != nil {
-               return err
-       }
-
-       opts.sourcefile = sfile
-       opts.resolveDir = sdir
+       sdir, _ := path.Split(ctx.SourcePath)
+       opts.sourcefile = ctx.SourcePath
+       opts.resolveDir = t.c.sfs.RealFilename(sdir)
+       opts.workDir = t.c.rs.WorkingDir
        opts.contents = string(src)
        opts.mediaType = ctx.InMediaType
 
-       // Create new temporary tsconfig file
-       newTSConfig, err := ioutil.TempFile("", "tsconfig.*.json")
+       buildOptions, err := toBuildOptions(opts)
        if err != nil {
                return err
        }
 
-       filesToDelete := make([]*os.File, 0)
-
-       defer func() {
-               for _, file := range filesToDelete {
-                       os.Remove(file.Name())
-               }
-       }()
+       buildOptions.Plugins, err = createBuildPlugins(t.c, opts)
+       if err != nil {
+               return err
+       }
 
-       filesToDelete = append(filesToDelete, newTSConfig)
-       configDir, _ := filepath.Split(newTSConfig.Name())
+       result := api.Build(buildOptions)
 
-       // Search for the innerMost tsconfig or jsconfig
-       innerTsConfig := ""
-       tsDir := opts.resolveDir
-       baseURLAbs := configDir
-       baseURL := "."
-       for tsDir != "." {
-               tryTsConfig := path.Join(tsDir, "tsconfig.json")
-               _, err := os.Stat(tryTsConfig)
-               if err != nil {
-                       tryTsConfig := path.Join(tsDir, "jsconfig.json")
-                       _, err = os.Stat(tryTsConfig)
+       if len(result.Errors) > 0 {
+               first := result.Errors[0]
+               loc := first.Location
+               path := loc.File
+
+               var err error
+               var f afero.File
+               var filename string
+
+               if !strings.HasPrefix(path, "..") {
+                       // Try first in the assets fs
+                       var fi os.FileInfo
+                       fi, err = t.c.rs.BaseFs.Assets.Fs.Stat(path)
                        if err == nil {
-                               innerTsConfig = tryTsConfig
-                               baseURLAbs = tsDir
-                               break
+                               m := fi.(hugofs.FileMetaInfo).Meta()
+                               filename = m.Filename()
+                               f, err = m.Open()
                        }
-               } else {
-                       innerTsConfig = tryTsConfig
-                       baseURLAbs = tsDir
-                       break
                }
-               if tsDir == opts.workDir {
-                       break
-               }
-               tsDir = path.Dir(tsDir)
-       }
 
-       // Resolve paths for @assets and @js (@js is just an alias for assets/js)
-       dirs := make([]string, 0)
-       rootPaths := make([]string, 0)
-       for _, dir := range t.sfs.RealDirs(".") {
-               rootDir := dir
-               if !strings.HasSuffix(dir, "package.json") {
-                       dirs = append(dirs, dir)
-               } else {
-                       rootDir, _ = path.Split(dir)
+               if f == nil {
+                       path = filepath.Join(t.c.rs.WorkingDir, path)
+                       filename = path
+                       f, err = t.c.rs.Fs.Os.Open(path)
                }
-               nodeModules := path.Join(rootDir, "node_modules")
-               if _, err := os.Stat(nodeModules); err == nil {
-                       rootPaths = append(rootPaths, nodeModules)
-               }
-       }
 
-       // Construct new temporary tsconfig file content
-       config := make(map[string]interface{})
-       if innerTsConfig != "" {
-               oldConfig, err := ioutil.ReadFile(innerTsConfig)
                if err == nil {
-                       // If there is an error, it just means there is no config file here.
-                       // Since we're also using the tsConfig file path to detect where
-                       // to put the temp file, this is ok.
-                       err = json.Unmarshal(oldConfig, &config)
-                       if err != nil {
-                               return err
-                       }
-               }
-       }
-
-       if config["compilerOptions"] == nil {
-               config["compilerOptions"] = map[string]interface{}{}
-       }
-
-       // Assign new global paths to the config file while reading existing ones.
-       compilerOptions := config["compilerOptions"].(map[string]interface{})
-
-       // Handle original baseUrl if it's there
-       if compilerOptions["baseUrl"] != nil {
-               baseURL = compilerOptions["baseUrl"].(string)
-               oldBaseURLAbs := path.Join(tsDir, baseURL)
-               rel, _ := filepath.Rel(configDir, oldBaseURLAbs)
-               configDir = oldBaseURLAbs
-               baseURLAbs = configDir
-               if "/" != helpers.FilePathSeparator {
-                       // On windows we need to use slashes instead of backslash
-                       rel = strings.ReplaceAll(rel, helpers.FilePathSeparator, "/")
-               }
-               if rel != "" {
-                       if strings.HasPrefix(rel, ".") {
-                               baseURL = rel
-                       } else {
-                               baseURL = fmt.Sprintf("./%s", rel)
-                       }
-               }
-               compilerOptions["baseUrl"] = baseURL
-       } else {
-               compilerOptions["baseUrl"] = baseURL
-       }
-
-       jsRel := func(refPath string) string {
-               rel, _ := filepath.Rel(configDir, refPath)
-               if "/" != helpers.FilePathSeparator {
-                       // On windows we need to use slashes instead of backslash
-                       rel = strings.ReplaceAll(rel, helpers.FilePathSeparator, "/")
-               }
-               if rel != "" {
-                       if !strings.HasPrefix(rel, ".") {
-                               rel = fmt.Sprintf("./%s", rel)
-                       }
-               } else {
-                       rel = "."
-               }
-               return rel
-       }
-
-       // Handle possible extends
-       if config["extends"] != nil {
-               extends := config["extends"].(string)
-               extendsAbs := path.Join(tsDir, extends)
-               rel := jsRel(extendsAbs)
-               config["extends"] = rel
-       }
-
-       var optionsPaths map[string]interface{}
-       // Get original paths if they exist
-       if compilerOptions["paths"] != nil {
-               optionsPaths = compilerOptions["paths"].(map[string]interface{})
-       } else {
-               optionsPaths = make(map[string]interface{})
-       }
-       compilerOptions["paths"] = optionsPaths
-
-       assets := make([]string, 0)
-       assetsExact := make([]string, 0)
-       js := make([]string, 0)
-       jsExact := make([]string, 0)
-       for _, dir := range dirs {
-               rel := jsRel(dir)
-               assets = append(assets, fmt.Sprintf("%s/*", rel))
-               assetsExact = appendExts(assetsExact, rel)
-
-               rel = jsRel(filepath.Join(dir, "js"))
-               js = append(js, fmt.Sprintf("%s/*", rel))
-               jsExact = appendExts(jsExact, rel)
-       }
-
-       optionsPaths["@assets/*"] = assets
-       optionsPaths["@js/*"] = js
-
-       // Make @js and @assets absolue matches search for index files
-       // to get around the problem in ESBuild resolving folders as index files.
-       optionsPaths["@assets"] = assetsExact
-       optionsPaths["@js"] = jsExact
-
-       var newDataFile *os.File
-       if opts.Data != nil {
-               // Create a data file
-               lines := make([]string, 0)
-               lines = append(lines, "// auto generated data import")
-               exports := make([]string, 0)
-               keys := make(map[string]bool)
-
-               var bytes []byte
-
-               conv := reflect.ValueOf(opts.Data)
-               convType := conv.Kind()
-               if convType == reflect.Interface {
-                       if conv.IsNil() {
-                               conv = reflect.Value{}
-                       }
-               }
-
-               if conv.Kind() != reflect.Map {
-                       // Write out as single JSON file
-                       newDataFile, err = ioutil.TempFile("", "data.*.json")
-                       // Output the data
-                       bytes, err = json.MarshalIndent(conv.InterfaceData(), "", "  ")
-                       if err != nil {
-                               return err
-                       }
-               } else {
-                       // Try to allow tree shaking at the root
-                       newDataFile, err = ioutil.TempFile(configDir, "data.*.js")
-                       for _, key := range conv.MapKeys() {
-                               strKey := key.Interface().(string)
-                               if keys[strKey] {
-                                       continue
-                               }
-                               keys[strKey] = true
-
-                               value := conv.MapIndex(key)
-
-                               keyVar := varfmt.PublicVarName(strKey)
-
-                               // Output the data
-                               bytes, err := json.MarshalIndent(value.Interface(), "", "  ")
-                               if err != nil {
-                                       return err
-                               }
-                               jsonValue := string(bytes)
-
-                               lines = append(lines, fmt.Sprintf("export const %s = %s;", keyVar, jsonValue))
-                               exports = append(exports, fmt.Sprintf("  %s,", keyVar))
-                               if strKey != keyVar {
-                                       exports = append(exports, fmt.Sprintf("  [\"%s\"]: %s,", strKey, keyVar))
-                               }
-                       }
-
-                       lines = append(lines, "const all = {")
-                       for _, line := range exports {
-                               lines = append(lines, line)
-                       }
-                       lines = append(lines, "};")
-                       lines = append(lines, "export default all;")
-
-                       bytes = []byte(strings.Join(lines, "\n"))
-               }
-
-               // Write tsconfig file
-               _, err = newDataFile.Write(bytes)
-               if err != nil {
+                       fe := herrors.NewFileError("js", 0, loc.Line, loc.Column, errors.New(first.Text))
+                       err, _ := herrors.WithFileContext(fe, filename, f, herrors.SimpleLineMatcher)
+                       f.Close()
                        return err
                }
-               err = newDataFile.Close()
-               if err != nil {
-                       return err
-               }
-
-               // Link this file into `import data from "@data"`
-               dataFiles := make([]string, 1)
-               rel, _ := filepath.Rel(baseURLAbs, newDataFile.Name())
-               dataFiles[0] = rel
-               optionsPaths["@data"] = dataFiles
-
-               filesToDelete = append(filesToDelete, newDataFile)
-       }
-
-       if len(rootPaths) > 0 {
-               // This will allow import "react" to resolve a react module that's
-               // either in the root node_modules or in one of the hugo mods.
-               optionsPaths["*"] = rootPaths
-       }
-
-       // Output the new config file
-       bytes, err := json.MarshalIndent(config, "", "  ")
-       if err != nil {
-               return err
-       }
-
-       // Write tsconfig file
-       _, err = newTSConfig.Write(bytes)
-       if err != nil {
-               return err
-       }
-       err = newTSConfig.Close()
-       if err != nil {
-               return err
-       }
-
-       // Tell ESBuild about this new config file to use
-       opts.tsConfig = newTSConfig.Name()
-
-       buildOptions, err := toBuildOptions(opts)
-       if err != nil {
-               os.Remove(opts.tsConfig)
-               return err
-       }
-
-       result := api.Build(buildOptions)
 
-       if len(result.Warnings) > 0 {
-               for _, value := range result.Warnings {
-                       if value.Location != nil {
-                               t.rs.Logger.WARN.Println(fmt.Sprintf("%s:%d: WARN: %s",
-                                       filepath.Join(sdir, value.Location.File),
-                                       value.Location.Line, value.Text))
-                               t.rs.Logger.WARN.Println("  ", value.Location.LineText)
-                       } else {
-                               t.rs.Logger.WARN.Println(fmt.Sprintf("%s: WARN: %s",
-                                       sdir,
-                                       value.Text))
-                       }
-               }
-       }
-       if len(result.Errors) > 0 {
-               output := result.Errors[0].Text
-               for _, value := range result.Errors {
-                       var line string
-                       if value.Location != nil {
-                               line = fmt.Sprintf("%s:%d ERROR: %s",
-                                       filepath.Join(sdir, value.Location.File),
-                                       value.Location.Line, value.Text)
-                       } else {
-                               line = fmt.Sprintf("%s ERROR: %s",
-                                       sdir,
-                                       value.Text)
-                       }
-                       t.rs.Logger.ERROR.Println(line)
-                       output = fmt.Sprintf("%s\n%s", output, line)
-                       if value.Location != nil {
-                               t.rs.Logger.ERROR.Println("  ", value.Location.LineText)
-                       }
-               }
-               return fmt.Errorf("%s", output)
+               return fmt.Errorf("%s", result.Errors[0].Text)
        }
 
-       if buildOptions.Outfile != "" {
-               _, tfile := path.Split(opts.TargetPath)
-               output := fmt.Sprintf("%s//# sourceMappingURL=%s\n",
-                       string(result.OutputFiles[1].Contents), tfile+".map")
-               _, err := ctx.To.Write([]byte(output))
-               if err != nil {
-                       return err
-               }
-               ctx.PublishSourceMap(string(result.OutputFiles[0].Contents))
-       } else {
-               ctx.To.Write(result.OutputFiles[0].Contents)
-       }
+       ctx.To.Write(result.OutputFiles[0].Contents)
        return nil
 }
 
 // Process process esbuild transform
 func (c *Client) Process(res resources.ResourceTransformer, opts map[string]interface{}) (resource.Resource, error) {
        return res.Transform(
-               &buildTransformation{rs: c.rs, sfs: c.sfs, optsm: opts},
+               &buildTransformation{c: c, optsm: opts},
        )
 }
-
-func toBuildOptions(opts Options) (buildOptions api.BuildOptions, err error) {
-       var target api.Target
-       switch opts.Target {
-       case "", "esnext":
-               target = api.ESNext
-       case "es5":
-               target = api.ES5
-       case "es6", "es2015":
-               target = api.ES2015
-       case "es2016":
-               target = api.ES2016
-       case "es2017":
-               target = api.ES2017
-       case "es2018":
-               target = api.ES2018
-       case "es2019":
-               target = api.ES2019
-       case "es2020":
-               target = api.ES2020
-       default:
-               err = fmt.Errorf("invalid target: %q", opts.Target)
-               return
-       }
-
-       mediaType := opts.mediaType
-       if mediaType.IsZero() {
-               mediaType = media.JavascriptType
-       }
-
-       var loader api.Loader
-       switch mediaType.SubType {
-       // TODO(bep) ESBuild support a set of other loaders, but I currently fail
-       // to see the relevance. That may change as we start using this.
-       case media.JavascriptType.SubType:
-               loader = api.LoaderJS
-       case media.TypeScriptType.SubType:
-               loader = api.LoaderTS
-       case media.TSXType.SubType:
-               loader = api.LoaderTSX
-       case media.JSXType.SubType:
-               loader = api.LoaderJSX
-       default:
-               err = fmt.Errorf("unsupported Media Type: %q", opts.mediaType)
-               return
-       }
-
-       var format api.Format
-       // One of: iife, cjs, esm
-       switch opts.Format {
-       case "", "iife":
-               format = api.FormatIIFE
-       case "esm":
-               format = api.FormatESModule
-       case "cjs":
-               format = api.FormatCommonJS
-       default:
-               err = fmt.Errorf("unsupported script output format: %q", opts.Format)
-               return
-       }
-
-       var defines map[string]string
-       if opts.Defines != nil {
-               defines = cast.ToStringMapString(opts.Defines)
-       }
-
-       // By default we only need to specify outDir and no outFile
-       var outDir = opts.outDir
-       var outFile = ""
-       var sourceMap api.SourceMap
-       switch opts.SourceMap {
-       case "inline":
-               sourceMap = api.SourceMapInline
-       case "external":
-               // When doing external sourcemaps we should specify
-               // out file and no out dir
-               sourceMap = api.SourceMapExternal
-               outFile = filepath.Join(opts.workDir, opts.TargetPath)
-               outDir = ""
-       case "":
-               sourceMap = api.SourceMapNone
-       default:
-               err = fmt.Errorf("unsupported sourcemap type: %q", opts.SourceMap)
-               return
-       }
-
-       buildOptions = api.BuildOptions{
-               Outfile: outFile,
-               Bundle:  true,
-
-               Target:    target,
-               Format:    format,
-               Sourcemap: sourceMap,
-
-               MinifyWhitespace:  opts.Minify,
-               MinifyIdentifiers: opts.Minify,
-               MinifySyntax:      opts.Minify,
-
-               Outdir:  outDir,
-               Defines: defines,
-
-               Externals: opts.Externals,
-
-               JSXFactory:  opts.JSXFactory,
-               JSXFragment: opts.JSXFragment,
-
-               Tsconfig: opts.tsConfig,
-
-               Stdin: &api.StdinOptions{
-                       Contents:   opts.contents,
-                       Sourcefile: opts.sourcefile,
-                       ResolveDir: opts.resolveDir,
-                       Loader:     loader,
-               },
-       }
-       return
-
-}
index 8839c646e74f19c043ebc03a1cd1c9be613d41d6..30a4490edc22c9fe2ccd897a2a94298f719e28ee 100644 (file)
 // limitations under the License.
 
 package js
-
-import (
-       "testing"
-
-       "github.com/gohugoio/hugo/media"
-
-       "github.com/evanw/esbuild/pkg/api"
-
-       qt "github.com/frankban/quicktest"
-)
-
-// This test is added to test/warn against breaking the "stability" of the
-// cache key. It's sometimes needed to break this, but should be avoided if possible.
-func TestOptionKey(t *testing.T) {
-       c := qt.New(t)
-
-       opts := map[string]interface{}{
-               "TargetPath": "foo",
-               "Target":     "es2018",
-       }
-
-       key := (&buildTransformation{optsm: opts}).Key()
-
-       c.Assert(key.Value(), qt.Equals, "jsbuild_7891849149754191852")
-}
-
-func TestToBuildOptions(t *testing.T) {
-       c := qt.New(t)
-
-       opts, err := toBuildOptions(Options{mediaType: media.JavascriptType})
-       c.Assert(err, qt.IsNil)
-       c.Assert(opts, qt.DeepEquals, api.BuildOptions{
-               Bundle: true,
-               Target: api.ESNext,
-               Format: api.FormatIIFE,
-               Stdin:  &api.StdinOptions{},
-       })
-
-       opts, err = toBuildOptions(Options{
-               Target: "es2018", Format: "cjs", Minify: true, mediaType: media.JavascriptType})
-       c.Assert(err, qt.IsNil)
-       c.Assert(opts, qt.DeepEquals, api.BuildOptions{
-               Bundle:            true,
-               Target:            api.ES2018,
-               Format:            api.FormatCommonJS,
-               MinifyIdentifiers: true,
-               MinifySyntax:      true,
-               MinifyWhitespace:  true,
-               Stdin:             &api.StdinOptions{},
-       })
-
-       opts, err = toBuildOptions(Options{
-               Target: "es2018", Format: "cjs", Minify: true, mediaType: media.JavascriptType,
-               SourceMap: "inline"})
-       c.Assert(err, qt.IsNil)
-       c.Assert(opts, qt.DeepEquals, api.BuildOptions{
-               Bundle:            true,
-               Target:            api.ES2018,
-               Format:            api.FormatCommonJS,
-               MinifyIdentifiers: true,
-               MinifySyntax:      true,
-               MinifyWhitespace:  true,
-               Sourcemap:         api.SourceMapInline,
-               Stdin:             &api.StdinOptions{},
-       })
-
-       opts, err = toBuildOptions(Options{
-               Target: "es2018", Format: "cjs", Minify: true, mediaType: media.JavascriptType,
-               SourceMap: "external"})
-       c.Assert(err, qt.IsNil)
-       c.Assert(opts, qt.DeepEquals, api.BuildOptions{
-               Bundle:            true,
-               Target:            api.ES2018,
-               Format:            api.FormatCommonJS,
-               MinifyIdentifiers: true,
-               MinifySyntax:      true,
-               MinifyWhitespace:  true,
-               Sourcemap:         api.SourceMapExternal,
-               Stdin:             &api.StdinOptions{},
-       })
-
-}
diff --git a/resources/resource_transformers/js/options.go b/resources/resource_transformers/js/options.go
new file mode 100644 (file)
index 0000000..5e74982
--- /dev/null
@@ -0,0 +1,353 @@
+// Copyright 2020 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package js
+
+import (
+       "encoding/json"
+       "fmt"
+       "path/filepath"
+       "strings"
+       "sync"
+
+       "github.com/pkg/errors"
+
+       "github.com/evanw/esbuild/pkg/api"
+
+       "github.com/gohugoio/hugo/helpers"
+       "github.com/gohugoio/hugo/hugofs"
+       "github.com/gohugoio/hugo/media"
+       "github.com/mitchellh/mapstructure"
+       "github.com/spf13/cast"
+)
+
+// Options esbuild configuration
+type Options struct {
+       // If not set, the source path will be used as the base target path.
+       // Note that the target path's extension may change if the target MIME type
+       // is different, e.g. when the source is TypeScript.
+       TargetPath string
+
+       // Whether to minify to output.
+       Minify bool
+
+       // Whether to write mapfiles
+       SourceMap string
+
+       // The language target.
+       // One of: es2015, es2016, es2017, es2018, es2019, es2020 or esnext.
+       // Default is esnext.
+       Target string
+
+       // The output format.
+       // One of: iife, cjs, esm
+       // Default is to esm.
+       Format string
+
+       // External dependencies, e.g. "react".
+       Externals []string `hash:"set"`
+
+       // User defined symbols.
+       Defines map[string]interface{}
+
+       // User defined params. Will be marshaled to JSON and available as "@params", e.g.
+       //     import * as params from '@params';
+       Params interface{}
+
+       // What to use instead of React.createElement.
+       JSXFactory string
+
+       // What to use instead of React.Fragment.
+       JSXFragment string
+
+       mediaType  media.Type
+       outDir     string
+       contents   string
+       sourcefile string
+       resolveDir string
+       workDir    string
+       tsConfig   string
+}
+
+func decodeOptions(m map[string]interface{}) (Options, error) {
+       var opts Options
+
+       if err := mapstructure.WeakDecode(m, &opts); err != nil {
+               return opts, err
+       }
+
+       if opts.TargetPath != "" {
+               opts.TargetPath = helpers.ToSlashTrimLeading(opts.TargetPath)
+       }
+
+       opts.Target = strings.ToLower(opts.Target)
+       opts.Format = strings.ToLower(opts.Format)
+
+       return opts, nil
+}
+
+type importCache struct {
+       sync.RWMutex
+       m map[string]api.OnResolveResult
+}
+
+func createBuildPlugins(c *Client, opts Options) ([]api.Plugin, error) {
+       fs := c.rs.Assets
+
+       cache := importCache{
+               m: make(map[string]api.OnResolveResult),
+       }
+
+       resolveImport := func(args api.OnResolveArgs) (api.OnResolveResult, error) {
+               relDir := fs.MakePathRelative(args.ResolveDir)
+
+               if relDir == "" {
+                       // Not in a Hugo Module, probably in node_modules.
+                       return api.OnResolveResult{}, nil
+               }
+
+               impPath := args.Path
+
+               // stdin is the main entry file which already is at the relative root.
+               // Imports not starting with a "." is assumed to live relative to /assets.
+               // Hugo makes no assumptions about the directory structure below /assets.
+               if args.Importer != "<stdin>" && strings.HasPrefix(impPath, ".") {
+                       impPath = filepath.Join(relDir, args.Path)
+               }
+
+               findFirst := func(base string) hugofs.FileMeta {
+                       // This is the most common sub-set of ESBuild's default extensions.
+                       // We assume that imports of JSON, CSS etc. will be using their full
+                       // name with extension.
+                       for _, ext := range []string{".js", ".ts", ".tsx", ".jsx"} {
+                               if fi, err := fs.Fs.Stat(base + ext); err == nil {
+                                       return fi.(hugofs.FileMetaInfo).Meta()
+                               }
+                       }
+
+                       // Not found.
+                       return nil
+               }
+
+               var m hugofs.FileMeta
+
+               // First the path as is.
+               fi, err := fs.Fs.Stat(impPath)
+
+               if err == nil {
+                       if fi.IsDir() {
+                               m = findFirst(filepath.Join(impPath, "index"))
+                       } else {
+                               m = fi.(hugofs.FileMetaInfo).Meta()
+                       }
+               } else {
+                       // It may be a regular file imported without an extension.
+                       m = findFirst(impPath)
+               }
+
+               if m != nil {
+                       // Store the source root so we can create a jsconfig.json
+                       // to help intellisense when the build is done.
+                       // This should be a small number of elements, and when
+                       // in server mode, we may get stale entries on renames etc.,
+                       // but that shouldn't matter too much.
+                       c.rs.JSConfigBuilder.AddSourceRoot(m.SourceRoot())
+                       return api.OnResolveResult{Path: m.Filename(), Namespace: ""}, nil
+               }
+
+               return api.OnResolveResult{}, nil
+       }
+
+       importResolver := api.Plugin{
+               Name: "hugo-import-resolver",
+               Setup: func(build api.PluginBuild) {
+                       build.OnResolve(api.OnResolveOptions{Filter: `.*`},
+                               func(args api.OnResolveArgs) (api.OnResolveResult, error) {
+                                       // Try cache first.
+                                       cache.RLock()
+                                       v, found := cache.m[args.Path]
+                                       cache.RUnlock()
+
+                                       if found {
+                                               return v, nil
+                                       }
+
+                                       imp, err := resolveImport(args)
+                                       if err != nil {
+                                               return imp, err
+                                       }
+
+                                       cache.Lock()
+                                       defer cache.Unlock()
+
+                                       cache.m[args.Path] = imp
+
+                                       return imp, nil
+
+                               })
+               },
+       }
+
+       params := opts.Params
+       if params == nil {
+               // This way @params will always resolve to something.
+               params = make(map[string]interface{})
+       }
+
+       b, err := json.Marshal(params)
+       if err != nil {
+               return nil, errors.Wrap(err, "failed to marshal params")
+       }
+       bs := string(b)
+       paramsPlugin := api.Plugin{
+               Name: "hugo-params-plugin",
+               Setup: func(build api.PluginBuild) {
+                       build.OnResolve(api.OnResolveOptions{Filter: `^@params$`},
+                               func(args api.OnResolveArgs) (api.OnResolveResult, error) {
+                                       return api.OnResolveResult{
+                                               Path:      args.Path,
+                                               Namespace: "params",
+                                       }, nil
+                               })
+                       build.OnLoad(api.OnLoadOptions{Filter: `.*`, Namespace: "params"},
+                               func(args api.OnLoadArgs) (api.OnLoadResult, error) {
+                                       return api.OnLoadResult{
+                                               Contents: &bs,
+                                               Loader:   api.LoaderJSON,
+                                       }, nil
+                               })
+               },
+       }
+
+       return []api.Plugin{importResolver, paramsPlugin}, nil
+
+}
+
+func toBuildOptions(opts Options) (buildOptions api.BuildOptions, err error) {
+
+       var target api.Target
+       switch opts.Target {
+       case "", "esnext":
+               target = api.ESNext
+       case "es5":
+               target = api.ES5
+       case "es6", "es2015":
+               target = api.ES2015
+       case "es2016":
+               target = api.ES2016
+       case "es2017":
+               target = api.ES2017
+       case "es2018":
+               target = api.ES2018
+       case "es2019":
+               target = api.ES2019
+       case "es2020":
+               target = api.ES2020
+       default:
+               err = fmt.Errorf("invalid target: %q", opts.Target)
+               return
+       }
+
+       mediaType := opts.mediaType
+       if mediaType.IsZero() {
+               mediaType = media.JavascriptType
+       }
+
+       var loader api.Loader
+       switch mediaType.SubType {
+       // TODO(bep) ESBuild support a set of other loaders, but I currently fail
+       // to see the relevance. That may change as we start using this.
+       case media.JavascriptType.SubType:
+               loader = api.LoaderJS
+       case media.TypeScriptType.SubType:
+               loader = api.LoaderTS
+       case media.TSXType.SubType:
+               loader = api.LoaderTSX
+       case media.JSXType.SubType:
+               loader = api.LoaderJSX
+       default:
+               err = fmt.Errorf("unsupported Media Type: %q", opts.mediaType)
+               return
+       }
+
+       var format api.Format
+       // One of: iife, cjs, esm
+       switch opts.Format {
+       case "", "iife":
+               format = api.FormatIIFE
+       case "esm":
+               format = api.FormatESModule
+       case "cjs":
+               format = api.FormatCommonJS
+       default:
+               err = fmt.Errorf("unsupported script output format: %q", opts.Format)
+               return
+       }
+
+       var defines map[string]string
+       if opts.Defines != nil {
+               defines = cast.ToStringMapString(opts.Defines)
+       }
+
+       // By default we only need to specify outDir and no outFile
+       var outDir = opts.outDir
+       var outFile = ""
+       var sourceMap api.SourceMap
+       switch opts.SourceMap {
+       case "inline":
+               sourceMap = api.SourceMapInline
+       case "external":
+               // When doing external sourcemaps we should specify
+               // out file and no out dir
+               sourceMap = api.SourceMapExternal
+               outFile = filepath.Join(opts.workDir, opts.TargetPath)
+               outDir = ""
+       case "":
+               sourceMap = api.SourceMapNone
+       default:
+               err = fmt.Errorf("unsupported sourcemap type: %q", opts.SourceMap)
+               return
+       }
+
+       buildOptions = api.BuildOptions{
+               Outfile: outFile,
+               Bundle:  true,
+
+               Target:    target,
+               Format:    format,
+               Sourcemap: sourceMap,
+
+               MinifyWhitespace:  opts.Minify,
+               MinifyIdentifiers: opts.Minify,
+               MinifySyntax:      opts.Minify,
+
+               Outdir: outDir,
+               Define: defines,
+
+               External: opts.Externals,
+
+               JSXFactory:  opts.JSXFactory,
+               JSXFragment: opts.JSXFragment,
+
+               Tsconfig: opts.tsConfig,
+
+               Stdin: &api.StdinOptions{
+                       Contents:   opts.contents,
+                       Sourcefile: opts.sourcefile,
+                       ResolveDir: opts.resolveDir,
+                       Loader:     loader,
+               },
+       }
+       return
+
+}
diff --git a/resources/resource_transformers/js/options_test.go b/resources/resource_transformers/js/options_test.go
new file mode 100644 (file)
index 0000000..89d362a
--- /dev/null
@@ -0,0 +1,105 @@
+// Copyright 2020 The Hugo Authors. All rights reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package js
+
+import (
+       "testing"
+
+       "github.com/gohugoio/hugo/media"
+
+       "github.com/evanw/esbuild/pkg/api"
+
+       qt "github.com/frankban/quicktest"
+)
+
+// This test is added to test/warn against breaking the "stability" of the
+// cache key. It's sometimes needed to break this, but should be avoided if possible.
+func TestOptionKey(t *testing.T) {
+       c := qt.New(t)
+
+       opts := map[string]interface{}{
+               "TargetPath": "foo",
+               "Target":     "es2018",
+       }
+
+       key := (&buildTransformation{optsm: opts}).Key()
+
+       c.Assert(key.Value(), qt.Equals, "jsbuild_7891849149754191852")
+}
+
+func TestToBuildOptions(t *testing.T) {
+       c := qt.New(t)
+
+       opts, err := toBuildOptions(Options{mediaType: media.JavascriptType})
+
+       c.Assert(err, qt.IsNil)
+       c.Assert(opts, qt.DeepEquals, api.BuildOptions{
+               Bundle: true,
+               Target: api.ESNext,
+               Format: api.FormatIIFE,
+               Stdin: &api.StdinOptions{
+                       Loader: api.LoaderJS,
+               },
+       })
+
+       opts, err = toBuildOptions(Options{
+               Target: "es2018", Format: "cjs", Minify: true, mediaType: media.JavascriptType})
+       c.Assert(err, qt.IsNil)
+       c.Assert(opts, qt.DeepEquals, api.BuildOptions{
+               Bundle:            true,
+               Target:            api.ES2018,
+               Format:            api.FormatCommonJS,
+               MinifyIdentifiers: true,
+               MinifySyntax:      true,
+               MinifyWhitespace:  true,
+               Stdin: &api.StdinOptions{
+                       Loader: api.LoaderJS,
+               },
+       })
+
+       opts, err = toBuildOptions(Options{
+               Target: "es2018", Format: "cjs", Minify: true, mediaType: media.JavascriptType,
+               SourceMap: "inline"})
+       c.Assert(err, qt.IsNil)
+       c.Assert(opts, qt.DeepEquals, api.BuildOptions{
+               Bundle:            true,
+               Target:            api.ES2018,
+               Format:            api.FormatCommonJS,
+               MinifyIdentifiers: true,
+               MinifySyntax:      true,
+               MinifyWhitespace:  true,
+               Sourcemap:         api.SourceMapInline,
+               Stdin: &api.StdinOptions{
+                       Loader: api.LoaderJS,
+               },
+       })
+
+       opts, err = toBuildOptions(Options{
+               Target: "es2018", Format: "cjs", Minify: true, mediaType: media.JavascriptType,
+               SourceMap: "external"})
+       c.Assert(err, qt.IsNil)
+       c.Assert(opts, qt.DeepEquals, api.BuildOptions{
+               Bundle:            true,
+               Target:            api.ES2018,
+               Format:            api.FormatCommonJS,
+               MinifyIdentifiers: true,
+               MinifySyntax:      true,
+               MinifyWhitespace:  true,
+               Sourcemap:         api.SourceMapExternal,
+               Stdin: &api.StdinOptions{
+                       Loader: api.LoaderJS,
+               },
+       })
+
+}