├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── RELEASE.md ├── doc └── config.sample.js ├── package.json ├── src ├── @types │ └── posthtml │ │ └── index.d.ts ├── action.ts ├── cli.js ├── main.ts ├── manifest.ts ├── outputName.ts ├── plan.ts ├── plugins.ts ├── plugins │ ├── html.js │ ├── inline.ts │ ├── manifest.ts │ └── modify.ts ├── state.ts ├── types.ts └── utils.ts ├── test ├── data.js ├── data │ ├── 1.css │ ├── 2.css │ ├── hello.js │ ├── image1.svg │ ├── image2.svg │ ├── image3.svg │ ├── page1.html │ ├── page2.html │ ├── page3.html │ ├── site.webmanifest │ └── vendor │ │ └── viz.js ├── outputNameTest.js ├── planTest.js ├── plugins │ ├── htmlTest.js │ ├── inlineTest.js │ ├── manifestTest.js │ └── modifyTest.js └── util.js ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .tern-project 3 | dist 4 | *.log 5 | 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - 10 5 | - 11 6 | - 13 7 | - 14 8 | 9 | cache: yarn 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | 3 | MOCHA := ./node_modules/mocha/bin/mocha 4 | NODEMON := ./node_modules/nodemon/bin/nodemon.js 5 | TSC := ./node_modules/typescript/bin/tsc 6 | 7 | info: 8 | @echo "Webtamp." 9 | @echo 10 | @echo "Commands:" 11 | @echo 12 | @echo " * build -- Compiles Typescript to Javascript." 13 | @echo " * clean -- Remove generated output." 14 | @echo " * publish -- Builds and publishes." 15 | @echo " * test -- Run tests." 16 | @echo " * watch -- Watches for changes, compiles and runs tests." 17 | @echo 18 | 19 | clean: 20 | rm -rf dist 21 | 22 | build: 23 | $(TSC) --pretty 24 | 25 | test: build 26 | $(MOCHA) --trace-deprecation 'test/**/*Test.js' 27 | 28 | watch: 29 | find src test yarn.lock Makefile *.json -type f | entr -cr make build test 30 | 31 | publish: clean test 32 | npm publish 33 | git push 34 | git push --tags 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # webtamp [![Build Status](https://travis-ci.org/japgolly/webtamp.svg?branch=master)](https://travis-ci.org/japgolly/webtamp) [![npm](https://img.shields.io/npm/v/webtamp.svg)](https://www.npmjs.com/package/webtamp) 2 | 3 | webtamp is bundler for assets. 4 | It is inspired by webpack, and meant to be a companion to JS-driven bundlers like webpack. 5 | 6 | You can do a lot of cool and useful things with webpack and its plugin community. 7 | However, there are a number of things that either can't be done at all, 8 | or can be done poorly and with difficulty by plugin that hack webpack in a way it wasn't meant for. 9 | The webpack authors are very clear about the scope and boundaries of webpack and fair enough. 10 | 11 | webtamp exists to address the gap. 12 | It does all the things I need to do for my webapp's assets, after I've used webpack. 13 | 14 | ### Contents 15 | - [Features](#features) 16 | - [Usage](#usage) 17 | - [Plugins](#plugins) 18 | - [Example](#example) 19 | - [Support](#support) 20 | - [Legal](#legal) 21 | 22 | # Features 23 | 24 | * Anything can be a top-level asset. No JS loader required or generated. 25 | * Dynamic filenames with hashing options. 26 | * Easy to integrate non-module based libraries, including those that expect relative assets with precise names. 27 | * CDNs supported directly. 28 | * Integrity can be specified manually. 29 | * Integrity can be calculated from local files. 30 | * Assets can be optional and will only be included when referenced (with transitivity). 31 | * Generate URL manifests. 32 | * Formats can be JSON and Scala. 33 | * Configure what is/isn't included in the manifest, and the names of entries. 34 | * Includes inlined assets. 35 | * Inliner plugin to inline assets (usually with size < n) into `data:` URIs. 36 | * HTML integration 37 | * Replaces `` with tags that load the asset, and all of its dependencies in order. 38 | * Replaces attributes like `webtamp://manifest/welcomeSvg` with real URLs. 39 | * Loads from CDN and locally-served assets alike. 40 | * Any missing assets fail fast. 41 | * Plugin system. 42 | 43 | # Usage 44 | 45 | 1. Install. 46 | 47 | ``` 48 | npm install --save-dev webtamp 49 | ``` 50 | 51 | 2. Create a config file, default name is `webtamp.config.js`. 52 | 53 | For details on all available options, see [doc/config.sample.js](doc/config.sample.js). 54 | 55 | A good starting point with commonly-used would be: 56 | 57 | ```js 58 | const webtamp = require('webtamp'); 59 | 60 | module.exports = { 61 | 62 | output: { 63 | dir: 'dist', 64 | name: '[name]-[hash].[ext]', 65 | }, 66 | 67 | assets: { 68 | // mandatory assets go here 69 | }, 70 | 71 | optional: { 72 | // optional assets go here 73 | }, 74 | 75 | plugins: [ 76 | ], 77 | }; 78 | ``` 79 | 80 | 3. Run it. 81 | 82 | ``` 83 | ./node_modules/.bin/webtamp 84 | ``` 85 | 86 | Or if you named your config file differently: 87 | ``` 88 | ./node_modules/.bin/webtamp --config 89 | ``` 90 | 91 | There's also a dry-run mode so no one gets hurt: 92 | ``` 93 | ./node_modules/.bin/webtamp [--config ] --dryrun 94 | ``` 95 | 96 | # Plugins 97 | 98 | * `webtamp.plugins.Modify.content` - Modify certain files' content. 99 | * `webtamp.plugins.Modify.rename` - Modify rename certain files. 100 | * `webtamp.plugins.Modify.{stateful,stateless}` - Modify files' names and content with more control. 101 | * `webtamp.plugins.Inline.data` - For files that given criteria, exclude from output and replace with a data URI. 102 | * `webtamp.plugins.Html.replace` - Replace `` tags and `webtamp://` URIs with real asset tags/links. Missing assets will fail the build. 103 | * `webtamp.plugins.Html.minify` - Minify HTML. 104 | * `webtamp.plugins.Manifest.extractCss` - Extract URLs from CSS and add those to the manifest. 105 | * `webtamp.plugins.Manifest.generate.scala` - Generate the asset manifest in [Scala](http://scala-lang.org/)/[Scala.JS](https://www.scala-js.org/). 106 | 107 | 108 | # Example 109 | 110 | This will demonstrate a number of features. Not all but enough to be useful. 111 | 112 | Say you have a tree of files like: 113 | ``` 114 | example 115 | ├── node_modules 116 | │   ├── jquery 117 | │   │   └── dist 118 | │   │   └── jquery.min.js 119 | │   └── katex 120 | │   └── dist 121 | │   ├── fonts 122 | │   │   ├── KaTeX_Size1-Regular.eot 123 | │   │   ├── KaTeX_Size1-Regular.ttf 124 | │   │   ├── KaTeX_Size1-Regular.woff 125 | │   │   └── KaTeX_Size1-Regular.woff2 126 | │   ├── katex.min.css 127 | │   └── katex.min.js 128 | ├── src 129 | │   ├── assets 130 | │   │   ├── tiny.svg 131 | │   │   └── welcome.svg 132 | │   └── html 133 | │   └── index.html 134 | └── vendor 135 | └── blerb.js 136 | ``` 137 | 138 | And a webtamp config like: 139 | ```js 140 | const camelcase = require('camelcase'); 141 | const webtamp = require('webtamp'); 142 | 143 | module.exports = { 144 | 145 | output: { 146 | dir: 'dist', 147 | name: '[hash:8]-[name].[ext]', 148 | }, 149 | 150 | assets: { 151 | html: { type: 'local', src: 'src/html', files: '**/*.html', outputName: '[path]/[basename]' }, 152 | images: { type: 'local', src: 'src/assets', files: '**/*.{svg,ico}', manifest: camelcase }, 153 | main: [ 'blerb', 'katex' ], 154 | }, 155 | 156 | optional: { 157 | 158 | jquery: { 159 | type: 'cdn', 160 | url: `https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.4/jquery.min.js`, 161 | integrity: { files: 'node_modules/jquery/dist/jquery.min.js' }, 162 | }, 163 | 164 | blerb: [ 165 | { type: 'local', files: 'vendor/blerb.js', manifest: true }, 166 | 'jquery', // This here means blerb requires jquery 167 | ], 168 | 169 | katex: [ 170 | { type: 'local', src: 'node_modules/katex/dist', files: '*.min.js' }, 171 | { type: 'local', src: 'node_modules/katex/dist', files: 'fonts/**/*', transitive: true }, 172 | ], 173 | }, 174 | 175 | plugins: [ 176 | Webtamp.plugins.Inline.data(i => /\.svg$/.test(i.dest) && i.size() < 4096), 177 | Webtamp.plugins.Html.replace(), 178 | ], 179 | } 180 | ``` 181 | 182 | Now lets say the content of your `src/html/index.html` is as follows: 183 | ```html 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | Tiny! 192 | Welcome! 193 | 194 | 195 | ``` 196 | 197 | After running webtamp, you'll have a `dist` directory like this: 198 | ``` 199 | dist 200 | ├── 03bef6aa-katex.min.js 201 | ├── 1b40ddd6-katex.min.css 202 | ├── 5eb3a560-welcome.svg 203 | ├── 88fee037-blerb.js 204 | ├── fonts 205 | │   ├── KaTeX_Size1-Regular.eot 206 | │   ├── KaTeX_Size1-Regular.ttf 207 | │   ├── KaTeX_Size1-Regular.woff 208 | │   └── KaTeX_Size1-Regular.woff2 209 | └── index.html 210 | ``` 211 | 212 | And the `dist/index.html` after the `Html.replace` plugin now looks like this: 213 | ```html 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | Tiny! 225 | Welcome! 226 | 227 | 228 | ``` 229 | 230 | # Support 231 | If you like what I do 232 | —my OSS libraries, my contributions to other OSS libs, [my programming blog](https://japgolly.blogspot.com)— 233 | and you'd like to support me, more content, more lib maintenance, [please become a patron](https://www.patreon.com/japgolly)! 234 | I do all my OSS work unpaid so showing your support will make a big difference. 235 | 236 | 237 | # Legal 238 | 239 | ``` 240 | Copyright 2017 David Barri 241 | 242 | Licensed under the Apache License, Version 2.0 (the "License"); 243 | you may not use this file except in compliance with the License. 244 | You may obtain a copy of the License at 245 | 246 | http://www.apache.org/licenses/LICENSE-2.0 247 | 248 | Unless required by applicable law or agreed to in writing, software 249 | distributed under the License is distributed on an "AS IS" BASIS, 250 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 251 | See the License for the specific language governing permissions and 252 | limitations under the License. 253 | ``` 254 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Release process 2 | 3 | First: `npm login` 4 | 5 | 1. `npm version ` 6 | 2. `make publish` 7 | -------------------------------------------------------------------------------- /doc/config.sample.js: -------------------------------------------------------------------------------- 1 | export default { 2 | 3 | // [Optional] Directory from which all assets paths are relative. 4 | // Default = "." 5 | src: ".", 6 | 7 | output: { 8 | 9 | // [Mandatory] Directory in which all webtamp output is written. 10 | dir: "dist", 11 | 12 | // [Optional] The filename template for all output. 13 | // 14 | // Replaces tokens: 15 | // * [name] - The filename without path and without file extension. 16 | // * [ext] - The file extension. 17 | // * [basename] - The file basename, equivalent to [name].[ext] 18 | // * [path] - The path relative to the src directory. 19 | // * [hash] - A hash of the file content (using the default hash algorithm). 20 | // You can also specify a specific algorithm like [sha256], [md5], etc. 21 | // * [hash:n] - As above but truncated to n chars. 22 | // 23 | // Default = "[basename]" 24 | name: '[basename]', 25 | 26 | // [Optional] 27 | // True - Write the manifest to manifest.json. 28 | // False - Don't create a manifest file. 29 | // String - Write the manifest to this filename. 30 | // 31 | // Default = true 32 | manifest: true, 33 | }, 34 | 35 | // Mandatory assets 36 | // 37 | // An object where the: 38 | // * keys are the names (ids) of the asset(s). Used to require other assets. 39 | // * values are the asset value(s) which can be: 40 | // * String - Requires another asset as a dependency. 41 | // * Object[type=local] - Zero or more local files. These will be copied to config.output.dir when required. 42 | // * Object[type=cdn] - A CDN-hosted asset. 43 | // * Object[type=external] - A path that will be served by your server, that you want to trust to exist. 44 | // * Array - A collection of the above. 45 | assets: { 46 | 47 | // Example of type=local 48 | localExample: { 49 | type: 'local', 50 | 51 | // [Mandatory] Local files to glob. 52 | files: 'images/**/*.{png,jpg}', 53 | 54 | // [Optional] 55 | // Bool - Whether to include these files in the manifest. 56 | // Path => String - Function that takes an asset path+filename and if it is desirable to 57 | // - include it in the manifest, returns a manifest name. 58 | // 59 | // Default = false 60 | manifest: false, 61 | 62 | // [Optional] Directory from which the files glob is relative. 63 | // If unspecified, the root config.src value is used. 64 | // 65 | // Default = undefined = config.src 66 | src: undefined, 67 | 68 | // [Optional] Specify a sub-directory in the output directory in which to copy assets. 69 | // 70 | // Default = undefined = "/" 71 | outputPath: undefined, 72 | 73 | // [Optional] Override the filename template from config.output.name 74 | // 75 | // Default = undefined = config.output.name 76 | outputName: undefined, 77 | 78 | // [Optional] Whether these files are transitive dependencies of something else. 79 | // These are typically fonts and images used by a 3rd-party non-modular library. 80 | // 81 | // Transitive dependencies: 82 | // * have outputName of "[path]/[basename]" 83 | // * are not modified or renamed by plugins 84 | // * are not loaded directly (i.e. the HTML.replace plugin will not insert tags to load these assets). 85 | // 86 | // Default = false 87 | transitive: false, 88 | 89 | // [Optional] Validate the glob results. 90 | // 91 | // The main format is a function that takes an array of matched files and returns 92 | // an erorr msg (or array of error msgs) if anything is wrong. 93 | // (files :: Array, glob :: String, srcDir :: String) => String | Array String | something falsy 94 | // 95 | // Other acceptable values are: 96 | // Bool - No validation/errors. Any results pass. 97 | // String - Always fail with given error. 98 | // 99 | // Default = fail when 0 files found 100 | validate: false, 101 | } 102 | 103 | // Example of type=cdn 104 | cdnExample: { 105 | type: 'cdn', 106 | 107 | // [Mandatory] The asset URL 108 | url: 'https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.4/jquery.min.js', 109 | 110 | // [Optional] Specify the link integrity attribute. (SRI) 111 | // Default = undefined 112 | // 113 | // Example: A trusted value to use. 114 | integrity: 'sha256-BbhdlvQf/xTY9gja0Dq3HiwQF8LaCRTXxZKRutelT44=', 115 | // Example: Calculate the integrity using trusted, local files. 116 | integrity: { 117 | files: 'node_modules/jquery/dist/jquery*.js', // A glob pattern (multiple files are legal) 118 | algo: 'sha256', // String | Array String - hash algorithms to use (multiple files are legal) 119 | }, 120 | 121 | // [Optional] 122 | // undefined | "style" | "script" 123 | as: undefined, 124 | 125 | // [Optional] 126 | // Bool - Whether to include this in the manifest. 127 | // String => String - Function that takes the URL if it is desirable to include it in the manifest, 128 | // returns a manifest name. 129 | // 130 | // Default = false 131 | manifest: false, 132 | }, 133 | 134 | // Example of type=external 135 | externalExample: { 136 | type: 'external', 137 | 138 | // [Mandatory] The asset path relative to your server root 139 | path: '/stats.json?time=1d', 140 | 141 | // [Optional] 142 | // Bool - Whether to include this in the manifest. 143 | // String => String - Function that takes the path above if it is desirable to include it in the manifest, 144 | // returns a manifest name. 145 | // 146 | // Default = false 147 | manifest: false, 148 | }, 149 | 150 | // You can create new assets that simply depend on another. Effectively an alias. 151 | aliasExample: "cdnExample", 152 | 153 | // You can merge any of the above into an array to make a bundle. 154 | bundleExample: [ 155 | {type: 'local', files: 'robot.txt'}, 156 | 'aliasExample', 157 | 'localExample', 158 | ], 159 | }, 160 | 161 | // Optional assets 162 | optional: { 163 | // same as 'assets' above 164 | }, 165 | 166 | plugins: [ 167 | // State => Unit 168 | ] 169 | } 170 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webtamp", 3 | "version": "0.5.4", 4 | "author": "David Barri ", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/japgolly/webtamp.git" 8 | }, 9 | "homepage": "https://github.com/japgolly/webtamp", 10 | "license": "Apache-2.0", 11 | "main": "dist/main.js", 12 | "bin": "dist/cli.js", 13 | "dependencies": { 14 | "camelcase": "^6.0.0", 15 | "commander": "^2.9.0", 16 | "fs-extra": "^9.0.1", 17 | "glob": "^7.1.1", 18 | "html-minifier": "^4.0.0", 19 | "mime-types": "^2.1.15", 20 | "posthtml": "^0.9.2", 21 | "sprintf-js": "^1.0.3" 22 | }, 23 | "devDependencies": { 24 | "@types/fs-extra": "^9.0.1", 25 | "@types/glob": "^7.1.3", 26 | "@types/html-minifier": "^4.0.0", 27 | "@types/mime-types": "^2.1.0", 28 | "@types/node": "^14.6.0", 29 | "@types/sprintf-js": "^1.1.2", 30 | "chai": "^4.0.2", 31 | "mocha": "^8.1.1", 32 | "typescript": "^4.0.2" 33 | }, 34 | "engines": { 35 | "node": ">=10" 36 | }, 37 | "scripts": { 38 | "test": "make test" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/@types/posthtml/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'posthtml' { 2 | 3 | type ObjectTo = {[key: string]: V} 4 | 5 | interface RecObjectTo { 6 | [key: string]: A | RecObjectTo 7 | } 8 | 9 | export type Node = RecObjectTo 10 | 11 | export type MatchCallback = (n: Node) => Node 12 | 13 | export interface MatchOptions { 14 | attrs?: boolean 15 | tag?: string 16 | } 17 | 18 | export interface PostHTMLTree { 19 | match: (expr: MatchOptions | Array, cb: MatchCallback) => void 20 | } 21 | 22 | export type Plugin = (t: PostHTMLTree) => PostHTMLTree 23 | 24 | export class PostHTML { 25 | constructor(plugins?: Array) 26 | 27 | process(tree, options): SyncResult 28 | } 29 | 30 | export interface SyncResult { 31 | html: string 32 | tree: PostHTMLTree 33 | } 34 | 35 | export function create(plugins?: Array): PostHTML 36 | 37 | export = create 38 | } 39 | -------------------------------------------------------------------------------- /src/action.ts: -------------------------------------------------------------------------------- 1 | import { Op, OpCopy, OpWrite, OpReduction } from './types' 2 | import { opReduce } from './utils' 3 | import { sprintf } from 'sprintf-js' 4 | import * as fs from 'fs-extra' 5 | import * as Path from 'path' 6 | 7 | const fmtInt = (n: number): string => n.toLocaleString() 8 | 9 | const fmtPath = (p: string): string => { 10 | const r = Path.relative(process.cwd(), p) 11 | return r.startsWith("../../../") ? p : r 12 | } 13 | 14 | /** 15 | * Compares ops for sorting (for aesthetic purposes). 16 | */ 17 | const opCmp = (a: Op, b: Op): number => 18 | a.type !== b.type ? a.type.localeCompare(b.type) : 19 | a.type === 'copy' ? a.from.abs.localeCompare((b as OpCopy).from.abs) : 20 | a.type === 'write' ? a.to.path.localeCompare((b as OpWrite).to.path) : 21 | 0 22 | 23 | type Stats = { files: number, bytes: number } 24 | type Runner = (ord: Number) => (op: Op) => void 25 | 26 | export type Options = { dryRun?: boolean } 27 | 28 | export function run(plan: { ops: Array, errors: Array, warns: Array }, options: Options = {}): void { 29 | const { ops, errors, warns } = plan 30 | const { dryRun } = options 31 | 32 | warns.forEach(msg => console.warn(`[WARN] ${msg}`)) 33 | 34 | if (errors.length === 0) { 35 | const stats: Stats = { files: 0, bytes: 0 } 36 | const runner = runnerAppend([ 37 | recordStats(stats), 38 | runnerLog(ops), // 39 | !dryRun && runnerPerform(errors), // 40 | ]) 41 | 42 | if (dryRun) 43 | console.info("DRY-RUN MODE. No actions will be performed.\n") 44 | 45 | ops 46 | .sort(opCmp) 47 | .forEach((op, i) => runner(i + 1)(op)) 48 | 49 | if (ops.length > 0) 50 | console.info() 51 | console.info(`Wrote ${fmtInt(stats.files)} files comprising ${fmtInt(stats.bytes)} bytes.`) 52 | } 53 | 54 | errors.forEach(msg => console.error(`[ERROR] ${msg}`)) 55 | console.error(`${fmtInt(warns.length)} warnings, ${fmtInt(errors.length)} errors.`) 56 | } 57 | 58 | const recordStats = (stats: Stats): Runner => _ => { 59 | const add = (n: number) => (k: 'files' | 'bytes') => stats[k] = stats[k] + n 60 | const inc = add(1) 61 | return opReduce({ 62 | copy: op => { 63 | inc('files') 64 | add(op.from.size())('bytes') 65 | }, 66 | write: op => { 67 | inc('files') 68 | add(op.content.length)('bytes') 69 | }, 70 | }) 71 | } 72 | 73 | const runnerLog = (ops: Array): Runner => { 74 | const determineColumnLength = () => { 75 | const get: OpReduction = { 76 | copy: op => fmtPath(op.to.path).length, 77 | write: op => fmtPath(op.to.path).length, 78 | } 79 | const lens: Array = ops.map(opReduce(get)) 80 | return lens.concat([1]).reduce((a, b) => Math.max(a, b)) 81 | } 82 | const colLen = determineColumnLength() 83 | const idxLen = (ops.length + '').length 84 | const fmtIdx = `[%${idxLen}d/${ops.length}]` 85 | const fmtCopy = `${fmtIdx} Copy %-${colLen}s ← %s` 86 | const fmtWrite = `${fmtIdx} Write %-${colLen}s ← %s bytes` 87 | return n => opReduce({ 88 | copy: op => console.log(sprintf(fmtCopy, n, fmtPath(op.to.path), fmtPath(op.from.abs))), 89 | write: op => console.log(sprintf(fmtWrite, n, fmtPath(op.to.path), fmtInt(op.content.length))), 90 | }) 91 | } 92 | 93 | const runnerPerform = (errors: Array): Runner => _ => opReduce({ 94 | copy: op => { 95 | const from = op.from.abs 96 | const to = op.to.abs 97 | try { 98 | fs.copySync(from, to) 99 | } catch (err) { 100 | errors.push(`Error copying ${fmtPath(from)}: ${err}`) 101 | } 102 | }, 103 | write: op => { 104 | const to = op.to.abs 105 | try { 106 | fs.outputFileSync(to, op.content) 107 | } catch (err) { 108 | errors.push(`Error creating ${fmtPath(to)}: ${err}`) 109 | } 110 | }, 111 | }) 112 | 113 | const runnerAppend = (runners: Array): Runner => { 114 | const rs: Array = [] 115 | runners.forEach(r => {if (r) rs.push(r)}) 116 | return ord => { 117 | const run = (op: Op) => {rs.forEach(r => r(ord)(op))} 118 | return opReduce({ 119 | copy : run, 120 | write: run, 121 | }) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const 4 | commander = require('commander'), 5 | FS = require('fs'), 6 | Path = require('path'), 7 | Webtamp = require('./main'); 8 | 9 | commander 10 | .version(require(__dirname + '/../package.json').version) 11 | .option('-c, --config [path]', 'Path to the config file', 'webtamp.config.js') 12 | .option('-n, --dryrun', "Run without making any modifications. Log actions instead of performing them.") 13 | .parse(process.argv); 14 | 15 | const configFile = Path.resolve(process.cwd(), commander.config); 16 | if (!(configFile && FS.existsSync(configFile))) { 17 | console.error("File not found: " + configFile); 18 | process.exit(1); 19 | } 20 | const config = require(configFile); 21 | 22 | const options = { 23 | dryRun: commander.dryrun, 24 | } 25 | 26 | const s = Webtamp.run(config, options); 27 | process.exit(s.ok() ? 0 : 2); 28 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import * as Action from './action' 2 | import * as Plan from './plan' 3 | import * as plugins from './plugins' 4 | import State from './state' 5 | 6 | function run(cfg: Plan.RawConfig, options: Action.Options = {}): State { 7 | const state = Plan.run(cfg) 8 | Action.run(state.results(), options) 9 | return state 10 | } 11 | 12 | export { 13 | plugins, 14 | run, 15 | } -------------------------------------------------------------------------------- /src/manifest.ts: -------------------------------------------------------------------------------- 1 | import { CDN, ObjectTo, URL } from './types' 2 | import * as Utils from './utils' 3 | import State from './state' 4 | 5 | type Local = string 6 | 7 | export type Entry = { 8 | local?: Local 9 | cdn ?: CDN 10 | url ?: URL 11 | list ?: Array 12 | } 13 | 14 | export class Manifest { 15 | private state: State 16 | private entries: ObjectTo 17 | 18 | constructor(s: State) { 19 | this.entries = {} 20 | this.state = s 21 | } 22 | 23 | getEntries() { 24 | return this.entries 25 | } 26 | 27 | addEntry(k: string, v: Entry): void { 28 | if (this.entries[k] && this.entries[k] !== v) { 29 | const o = {k: this.entries[k]} 30 | this.state.addWarn("Overwriting manifest entry: " + JSON.stringify(o)) 31 | } 32 | this.entries[k] = v 33 | } 34 | 35 | addPathLocal(name: string, local: Local): void { 36 | this.addEntry(name, { local }) 37 | } 38 | 39 | addPathCdn(name: string, cdn: CDN): void { 40 | this.addEntry(name, { cdn }) 41 | } 42 | 43 | addUrl(name: string, url: string | URL): void { 44 | const e: Entry = 45 | (typeof url === 'string') 46 | ? { url: {url }} 47 | : { url } 48 | this.addEntry(name, e) 49 | } 50 | 51 | addList(name: string, urls: Array): void { 52 | this.addEntry(name, { list: urls }) 53 | } 54 | 55 | delete(name: string): void { 56 | delete this.entries[name] 57 | } 58 | 59 | mapValues(f: (_: Entry) => Entry): void { 60 | this.entries = Utils.mapObjectValues(this.entries, f) 61 | } 62 | 63 | writeOpJson() { 64 | return JSON.stringify(this.entries, null, ' ') 65 | } 66 | 67 | static url = (entry: Entry, allowCdn: boolean): string | null => { 68 | let r = entry.local || entry.url?.url || null 69 | if (!r && allowCdn && entry.cdn) r = entry.cdn.url 70 | return r 71 | } 72 | 73 | static manifestEntryToUrlEntry = (m: Entry): URL | null => { 74 | if (m.cdn) { 75 | const cdn = m.cdn 76 | const u: URL = { 77 | url: cdn.url, 78 | crossorigin: 'anonymous' // aaaaaaaaaah the temp hacks 79 | } 80 | if (cdn.integrity) u.integrity = cdn.integrity 81 | if (cdn.transitive) u.transitive = cdn.transitive 82 | if (cdn.as) u.as = cdn.as 83 | return u 84 | } else { 85 | const url = Manifest.url(m, false) 86 | return url ? { url } : null 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/outputName.ts: -------------------------------------------------------------------------------- 1 | import { Algo, InlineFile } from './types' 2 | import * as Path from 'path' 3 | import * as Utils from './utils' 4 | 5 | export type MakeOptions = { 6 | defaultHashAlgo ?: Algo 7 | defaultHashWidth?: number 8 | } 9 | 10 | export type MakeResult = (f: InlineFile) => string 11 | 12 | export function make(pat0: string, options: MakeOptions = {}): MakeResult { 13 | 14 | const { defaultHashAlgo = 'sha256', defaultHashWidth = 32 } = options 15 | 16 | const pat = pat0 17 | .replace(/\[hash(?::(\d+))?\]/g, (_, w) => `[${defaultHashAlgo}:${w || defaultHashWidth}]`) 18 | 19 | const wtf = { fn: (i: InlineFile, n: string): string => n } 20 | 21 | const add = (f: (i: InlineFile, n: string) => string): void => { 22 | const next = wtf.fn 23 | wtf.fn = (i, n) => next(i, f(i, n)) 24 | } 25 | 26 | const addToken = (token: string, fn: (i: InlineFile) => string): void => { 27 | const regex = new RegExp(`\\[${token}\\]`, "g") 28 | if (regex.test(pat)) 29 | add((i, n) => n.replace(regex, fn(i))) 30 | } 31 | 32 | const addHash = (algo: Algo, token?: string): void => { 33 | const hasher = Utils.hashData(algo, 'hex') 34 | const regex = new RegExp(`\\[${token ?? algo}(?::(\\d+))?\\]`, "g") 35 | if (regex.test(pat)) 36 | add((i, n) => { 37 | const hash = Utils.memoise(() => hasher(i.contents())) 38 | const replace = (_: any, width: number): string => width ? hash().substr(0, width) : hash() 39 | return n.replace(regex, replace) 40 | }) 41 | } 42 | 43 | const nameOnly = (f: (f: string) => string) => (i: InlineFile) => f(i.name) 44 | 45 | addToken('basename', nameOnly(Path.basename)) 46 | addToken('name', nameOnly(f => Path.basename(f, Path.extname(f)))) 47 | addToken('ext', nameOnly(f => Path.extname(f).replace(/^\./, ''))) 48 | addToken('path', nameOnly(Path.dirname)) 49 | addHash('md5') 50 | addHash('sha1') 51 | addHash('sha256') 52 | addHash('sha384') 53 | addHash('sha512') 54 | 55 | const fn = wtf.fn 56 | return i => fn(i, pat) 57 | } 58 | -------------------------------------------------------------------------------- /src/plan.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Algo, 3 | As, 4 | CDN, 5 | Falsy, 6 | InlineFile, 7 | LocalSrc, 8 | ObjectTo, 9 | Op, 10 | URL, 11 | } from './types' 12 | import { MakeOptions } from './outputName' 13 | import * as FS from 'fs' 14 | import * as Glob from "glob" 15 | import * as OutputName from './outputName' 16 | import * as Path from 'path' 17 | import * as Utils from './utils' 18 | import State from './state' 19 | 20 | export type RawConfig = ObjectTo 21 | 22 | type ConfigParser = (inArray: boolean) => (name: string, value: any) => A 23 | 24 | type ConfigReduction = { 25 | string : ConfigParser 26 | local : ConfigParser 27 | external: ConfigParser 28 | cdn : ConfigParser 29 | } 30 | 31 | type OutputNameFn = (_: InlineFile) => string 32 | 33 | type MkOutputNameFn = (template: string) => OutputNameFn 34 | 35 | type ValidationFn = (fs: Array, files: string, src: string) => (Array | string | Falsy) 36 | 37 | type PlanCtx = {state: State, mkOutputNameFn: MkOutputNameFn} 38 | 39 | // ===================================================================================================================== 40 | 41 | function readConfigValue(state: State, 42 | name: string, 43 | config: RawConfig, 44 | type: string, 45 | key: string, 46 | defaultValue?: A 47 | ): A | undefined { 48 | const v = config[key] 49 | if (typeof v === type) 50 | return v 51 | else if (typeof v === 'undefined') { 52 | if (defaultValue !== undefined) 53 | return defaultValue 54 | else 55 | state.addError(`Invalid ${name}: ${key} is missing.`) 56 | } else 57 | state.addError(`Invalid ${name}: ${key} must be of type ${type} but is ${typeof v}.`) 58 | } 59 | 60 | function readConfigString(state: State, 61 | name: string, 62 | config: RawConfig, 63 | key: string, 64 | defaultValue?: string 65 | ): string | undefined { 66 | return readConfigValue(state, name, config, 'string', key, defaultValue) 67 | } 68 | 69 | function readConfigOptionalString(state: State, 70 | name: string, 71 | config: RawConfig, 72 | key: string, 73 | ): string | undefined { 74 | return readConfigValue(state, name, config, 'string', key, null) || undefined 75 | } 76 | 77 | function readConfigObject(state: State, 78 | name: string, 79 | config: RawConfig, 80 | key: string, 81 | defaultValue?: ObjectTo 82 | ): ObjectTo | undefined { 83 | return readConfigValue(state, name, config, 'object', key, defaultValue) 84 | } 85 | 86 | export function foldAsset(state: State, cases: ConfigReduction): (name: string, value: any) => (A | undefined) { 87 | const go: ConfigParser = (inArray) => (name, value) => { 88 | if (Array.isArray(value)) { 89 | const g = go(true) 90 | value.forEach(v => g(name, v)) 91 | } else if (typeof value === 'string') 92 | cases.string(inArray)(name, value) 93 | else if (typeof value === 'object') 94 | switch (value.type) { 95 | case 'local': 96 | return cases.local(inArray)(name, value) 97 | case 'external': 98 | return cases.external(inArray)(name, value) 99 | case 'cdn': 100 | return cases.cdn(inArray)(name, value) 101 | default: 102 | state.addError(`${name} has invalid asset type: ${JSON.stringify(value.type)}`) 103 | } 104 | else 105 | state.addError(`${name} has an invalid value: ${JSON.stringify(value)}`) 106 | } 107 | return go(false) 108 | } 109 | 110 | /** 111 | * @param {Any} manifestSetting Whatever the user has specified as their manifest setting 112 | */ 113 | function addManifest(state : State, 114 | desc : string, 115 | manifestSetting: any, 116 | nameWhenTrue : () => (string | undefined), 117 | fnArg : () => any, 118 | addFn : (_: string) => void): void { 119 | if (manifestSetting) { 120 | const maybeAdd = (n: string | undefined) => { if (n) addFn(n) } 121 | if (manifestSetting === true) 122 | maybeAdd(nameWhenTrue()) 123 | else if (typeof manifestSetting === 'string') 124 | addFn(manifestSetting) 125 | else if (typeof manifestSetting === 'function') 126 | maybeAdd(manifestSetting(fnArg())) 127 | else 128 | state.addError(`${desc} has an invalid manifest: ${JSON.stringify(manifestSetting)}`) 129 | } 130 | } 131 | 132 | /** 133 | * @param {Any} manifest Whatever the user has specified as their manifest setting 134 | */ 135 | function arityAwareManifestName(state : State, 136 | subname : string, 137 | inArray : boolean, 138 | name : string, 139 | manifest: any, 140 | fnArg : () => any, 141 | use : (_: string) => void): void { 142 | const desc = inArray ? `${name}:${subname}` : name 143 | const whenTrue = () => { 144 | if (inArray) 145 | state.addError(`${desc} requires an explicit manifest name because it's in an array.`) 146 | else 147 | return name 148 | } 149 | addManifest(state, desc, manifest, whenTrue, fnArg, use) 150 | } 151 | 152 | const planLocal: (_: { state: State, mkOutputNameFn: MkOutputNameFn }, outputNameFn0: OutputNameFn) => (inArray: boolean) => (name: string, value: { 153 | src: string 154 | files: string, 155 | outputName?: string, 156 | outputPath?: string, 157 | manifest: boolean | ((path: string) => (string | null)), 158 | validate?: string | boolean | ValidationFn, 159 | transitive?: boolean 160 | }) => void = 161 | ({ state, mkOutputNameFn }, outputNameFn0) => inArray => (name, value) => { 162 | const { files, outputName, outputPath, manifest, validate, transitive } = value 163 | state.checkThenRunIfNoErrors(() => { 164 | if (!files) 165 | state.addError(`${name} missing key: files`) 166 | if (manifest === true && inArray) 167 | state.addError(`${name} has {manifest: true} but requires an explicit name or function.`) 168 | }, () => { 169 | const src = value.src ? Path.resolve(state.src, value.src) : state.src 170 | const fs = Glob.sync(files, { cwd: src, nodir: true }).sort() 171 | 172 | // Validate 173 | let validateFn: ValidationFn | null = 174 | typeof validate === 'function' ? validate : 175 | typeof validate === 'string' ? _ => validate : 176 | typeof validate === 'boolean' ? _ => [] : 177 | typeof validate === 'undefined' ? defaultLocalFileValidation : 178 | null // lol do more lazy! 179 | 180 | const validationErrors: Array = [] 181 | if (validateFn) 182 | Utils.asArray(validateFn(fs, files, src)).forEach(e => {if (e) validationErrors.push(e)}) 183 | 184 | if (validationErrors.length > 0) { 185 | const descSrc = value.src ? value.src + '/' : '' 186 | const desc = `${name}:${descSrc}${files}` 187 | validationErrors.forEach(e => state.addError(`${desc} - ${e}`)) 188 | 189 | } else { 190 | const outputNameFn: OutputNameFn = 191 | transitive ? mkOutputNameFn("[path]/[basename]") : 192 | outputName ? mkOutputNameFn(outputName) : 193 | outputNameFn0 194 | state.registerNow(name) 195 | 196 | // Add each local file 197 | for (const f of fs) { 198 | const srcFilename = Path.resolve(src, f) 199 | const contents = Utils.memoise(() => FS.readFileSync(srcFilename)) 200 | let newName = outputNameFn({ name: f, contents }) 201 | if (outputPath) 202 | newName = `${outputPath}/${newName}` 203 | newName = newName.replace(/^\.\//g, '') 204 | 205 | // Copy file 206 | state.addOpCopy(new LocalSrc(src, f), newName, transitive) 207 | 208 | const url = '/' + newName 209 | const urlEntry: URL = { url } 210 | if (transitive !== undefined) 211 | urlEntry.transitive = transitive 212 | state.addUrl(name, urlEntry) 213 | 214 | // Add to manifest 215 | const whenTrue = () => { 216 | if (fs.length > 1) 217 | state.addWarn(`${name} has {manifest: true} but '${files}' matches more than 1 file.`) 218 | else 219 | return name 220 | } 221 | addManifest(state, name, manifest, whenTrue, () => f, n => state.manifest.addPathLocal(n, url)) 222 | } 223 | } 224 | }) 225 | } 226 | 227 | const defaultLocalFileValidation: ValidationFn = (fs, glob, src) => 228 | fs.length === 0 && `0 files found.` 229 | // fs.length === 0 && `0 files found. (Add {validate: false} to disable this check.)` 230 | 231 | const planCdn: (ctx: PlanCtx) => ConfigParser = ctx => inArray => (name, config) => { 232 | const { state } = ctx 233 | const cfgName = `config in ${name}` 234 | const defaultAlgos: Algo = 'sha256' 235 | state.checkThenRunIfNoErrors(() => { 236 | const url = readConfigString(state, cfgName, config, "url") 237 | const _as = readConfigOptionalString(state, cfgName, config, "as") 238 | let as: As | undefined | null = null 239 | switch (_as) { 240 | case 'script': 241 | case 'style': 242 | case undefined: 243 | as = _as 244 | break; 245 | default: 246 | state.addError(`Invalid "at" value: ${_as}. Must be either 'script' or 'style'.`) 247 | } 248 | return (url && (as !== null)) && { url, as } || false 249 | }, ({ url, as }) => { 250 | const { integrity, manifest } = config 251 | const desc = name 252 | let i: string | false | undefined = false // Option[ValidIntegrityAttribute] 253 | 254 | if (!integrity) 255 | i = undefined 256 | 257 | else if (typeof integrity === 'string') 258 | i = integrity 259 | 260 | // integrity: { files: 'image2.svg', algo: 'sha384' } 261 | else if (typeof integrity === 'object') { 262 | const algos = Utils.asArray(integrity.algo || defaultAlgos) 263 | const { files } = integrity 264 | if (files) { 265 | const fs = Glob.sync(files, { cwd: state.src, nodir: true }).sort() 266 | if (fs.length == 0) 267 | state.addError(`${desc} integrity file(s) not found: ${files}`) 268 | else 269 | state.checkThenRunIfNoErrors(() => { 270 | const hashes = [] 271 | for (const algo of algos) { 272 | const hasher = Utils.hashData(algo, 'base64') 273 | for (const f of fs) { 274 | const h = hasher(FS.readFileSync(Path.resolve(state.src, f))) 275 | hashes.push(`${algo}-${h}`) 276 | } 277 | } 278 | return hashes 279 | }, hashes => { 280 | i = hashes.join(' ') 281 | }) 282 | } else 283 | state.addError(`${desc} integrity missing key: files`) 284 | 285 | } else 286 | state.addError(`${desc} has an invalid integrity value: ${JSON.stringify(integrity)}`) 287 | if (i || i === undefined) { 288 | const cdn: CDN = Utils.removeUndefinedValues({ url, as, integrity: i }) 289 | const fnArg = () => cdn 290 | state.registerNow(name) 291 | const urlObj: URL = Utils.removeUndefinedValues({ crossorigin: 'anonymous', url, as, integrity: i }) 292 | state.addUrl(name, urlObj) 293 | arityAwareManifestName(state, url, inArray, name, manifest, fnArg, n => { 294 | state.manifest.addPathCdn(n, cdn) 295 | }) 296 | } 297 | }) 298 | } 299 | 300 | const planExternal: (ctx: PlanCtx) => ConfigParser = ctx => inArray => (name, config) => { 301 | const { state } = ctx 302 | const cfgName = `config in ${name}` 303 | state.checkThenRunIfNoErrors(() => { 304 | const path = readConfigString(state, cfgName, config, "path") 305 | const manifest = config.manifest 306 | return path && { path, manifest } || false 307 | }, ({ path, manifest }) => { 308 | const url = path.replace(/^\/?/, '/') 309 | state.registerNow(name) 310 | state.addUrl(name, { url }) 311 | arityAwareManifestName(state, path, inArray, name, manifest, () => path, n => { 312 | state.manifest.addPathLocal(n, url) 313 | }) 314 | }) 315 | } 316 | 317 | const planRef: (ctx: PlanCtx) => ConfigParser = ctx => inArray => (name, config) => { 318 | const { state } = ctx 319 | if (typeof config === 'string') { 320 | state.registerNow(name) 321 | state.addDependency(name, config) 322 | } else 323 | state.addError(`Invalid value for ${name} config. Expected a string, got: ${JSON.stringify(config)}`) 324 | } 325 | 326 | export const parse = (config: RawConfig): State => { 327 | let state = new State("", "") 328 | const _src = readConfigString(state, "config", config, "src", ".") 329 | const output = readConfigObject(state, "config", config, "output", {}) 330 | if (!_src || !output) 331 | return state 332 | 333 | const _dir = readConfigString(state, "config.output", output, "dir") 334 | if (!_dir) 335 | return state 336 | 337 | const src = Path.resolve(_src) 338 | const target = Path.resolve(_dir) 339 | state = new State(src, target) 340 | 341 | if (!FS.existsSync(src)) 342 | state.addError(`Src dir doesn't exist: ${src}`) 343 | 344 | if (state.ok()) { 345 | const 346 | outputNameFnDefaults: MakeOptions = {}, 347 | mkOutputNameFn: MkOutputNameFn = f => OutputName.make(f, outputNameFnDefaults), 348 | outputNameFn: OutputNameFn = mkOutputNameFn(config.output.name || '[basename]'), 349 | ctx: PlanCtx = { state, mkOutputNameFn } 350 | 351 | const cases: ConfigReduction = { 352 | string : planRef(ctx), 353 | local : planLocal(ctx, outputNameFn), 354 | external: planExternal(ctx), 355 | cdn : planCdn(ctx), 356 | } 357 | 358 | // Parse config.optional 359 | { 360 | const o = config.optional 361 | if (o) { 362 | const defer: (f: ConfigParser) => ConfigParser = f => inArray => (n, v) => { 363 | state.registerForLater(n, () => f(inArray)(n, v)) 364 | } 365 | const casesDeferred = Utils.mapObjectValues(cases, defer) as ConfigReduction 366 | const add = foldAsset(state, casesDeferred) 367 | for (const [name, value] of Object.entries(o)) 368 | add(name, value) 369 | } 370 | } 371 | 372 | // Parse config.assets 373 | { 374 | const add = foldAsset(state, cases) 375 | for (const [name, value] of Object.entries(config.assets)) { 376 | // console.log(`Parsing asset "${name}" = ${JSON.stringify(value)}`) 377 | add(name, value) 378 | } 379 | } 380 | 381 | // Graph dependencies 382 | state.resolvePending() 383 | state.graphDependencies() 384 | } 385 | 386 | return state 387 | } 388 | 389 | export const runPlugins = (cfg: RawConfig) => Utils.tap((state: State) => { 390 | if (state.ok()) 391 | for (const p of Utils.asArray(cfg.plugins)) 392 | if (state.ok() && p) 393 | p(state) 394 | }) 395 | 396 | export const generateManifest = (cfg: RawConfig) => Utils.tap((state: State) => { 397 | if (state.ok()) { 398 | const gen = (filename: string) => { 399 | const content = state.manifest.writeOpJson() 400 | state.addOpWrite(filename, content) 401 | } 402 | const m = cfg.output.manifest 403 | if (m === undefined || m === true) 404 | gen('manifest.json') 405 | else if (typeof m === 'string') 406 | gen(m) 407 | else if (m !== false) 408 | state.addError(`Invalid value for config.output.manifest: ${JSON.stringify(m)}`) 409 | } 410 | }) 411 | 412 | const ensureNoDuplicateTargets = Utils.tap((state: State) => { 413 | const targetIndex: ObjectTo> = {} 414 | const getTarget = Utils.opReduce({ 415 | copy: op => op.to.abs, 416 | write: op => op.to.abs, 417 | }) 418 | state.ops.forEach(op => { 419 | const t = getTarget(op) 420 | if (!targetIndex[t]) targetIndex[t] = [] 421 | targetIndex[t].push(op) 422 | }) 423 | Object.entries(targetIndex).forEach(([t, ops]) => { 424 | if (ops.length !== 1) 425 | state.addError(`Multiple assets write to the same target: ${Path.relative(state.target, t)}`) 426 | }) 427 | }) 428 | 429 | export const run = (cfg: RawConfig) => 430 | Utils.compose([ 431 | ensureNoDuplicateTargets, 432 | generateManifest(cfg), 433 | runPlugins(cfg) 434 | ])(parse(cfg)) 435 | -------------------------------------------------------------------------------- /src/plugins.ts: -------------------------------------------------------------------------------- 1 | import * as Html from './plugins/html' 2 | import * as Inline from './plugins/inline' 3 | import * as Manifest from './plugins/manifest' 4 | import * as Modify from './plugins/modify' 5 | 6 | export { 7 | Html, 8 | Inline, 9 | Manifest, 10 | Modify, 11 | } 12 | -------------------------------------------------------------------------------- /src/plugins/html.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const 4 | HtmlMinifier = require('html-minifier'), 5 | Manifest = require('../manifest').Manifest, 6 | Modify = require('./modify'), 7 | PostHtml = require('posthtml'); 8 | 9 | const isHtmlFile = i => /\.html$/.test(i.filename); 10 | 11 | const replacementPlugin = ({ test = isHtmlFile, modTag = i => t => t } = {}) => { 12 | return Modify.stateful(state => i => { 13 | if (test(i)) { 14 | const transformations = [ 15 | transformRequireTag(state, modTag(i)), 16 | transformWebtampUrls(state), 17 | ]; 18 | const newContent = PostHtml(transformations) 19 | .process(i.content(), { sync: true }) 20 | .html 21 | return { newContent }; 22 | } 23 | }); 24 | } 25 | 26 | const transformRequireTag = (state, modTag) => tree => { 27 | tree.match({ tag: 'require' }, node => { 28 | const attrs = node.attrs || {}; 29 | const assetName = attrs.asset; 30 | const manifestName = attrs.manifest; 31 | 32 | const replace = fn => { 33 | const links = []; 34 | const add = tag => { 35 | const t = modTag(tag); 36 | if (t) links.push(t); 37 | }; 38 | fn(add); 39 | node.tag = false; 40 | node.content = [links.join("\n")].concat(node.content); 41 | } 42 | 43 | // Validate attribute count 44 | if (attrs.length === 0) 45 | state.addError(` missing attributes.`); 46 | else if (attrs.length > 1) 47 | state.addError(`Multiple attributes specified in : ${attrs}`); 48 | 49 | // asset="..." 50 | else if (assetName) 51 | replace(addTag => { 52 | const seen = new Set(); 53 | const addAsset = name => { 54 | if (state.urls[name] === undefined) 55 | state.addError(`Asset referenced in not found: ${name}`); 56 | else if (name && !seen.has(name)) { 57 | seen.add(name); 58 | 59 | // Add dependencies 60 | Object.keys(state.graph[name]).forEach(addAsset); 61 | 62 | // Add named 63 | for (const urlEntry of state.urls[name]) 64 | if (!urlEntry.transitive) { 65 | withTagForUrlEntry(state, urlEntry, addTag); 66 | } 67 | } 68 | }; 69 | 70 | addAsset(assetName); 71 | }) 72 | 73 | // manifest="..." 74 | else if (manifestName) 75 | replace(addTag => 76 | withManifestEntry(state, manifestName, manifestEntry => { 77 | const urlEntry = Manifest.manifestEntryToUrlEntry(manifestEntry); 78 | withTagForUrlEntry(state, urlEntry, addTag); 79 | }) 80 | ) 81 | 82 | // 83 | else 84 | state.addError(" tag needs an 'asset' attribute."); 85 | 86 | return node; 87 | }); 88 | return tree; 89 | }; 90 | 91 | const tagForUrlEntry = o => { 92 | const attrArray = []; 93 | const add = (k, vv) => { 94 | const v = vv || o[k]; 95 | if (typeof v === 'string') 96 | attrArray.push(`${k}="${v}"`); 97 | }; 98 | const attrs = () => attrArray.join(' '); 99 | 100 | const { url, as } = o; 101 | const urlEscaped = url; // Entities.escape(url); 102 | if (as === 'script' || /\.js$/.test(url)) { 103 | add('src', urlEscaped); 104 | add('integrity'); 105 | add('crossorigin'); 106 | return ``; 107 | } else if (as === 'style' ||/\.css$/.test(url)) { 108 | add('href', urlEscaped); 109 | add('integrity'); 110 | add('crossorigin'); 111 | return ``; 112 | } 113 | } 114 | 115 | const withTagForUrlEntry = (state, urlEntry, use) => { 116 | const tag = tagForUrlEntry(urlEntry); 117 | if (tag) 118 | return use(tag); 119 | else 120 | state.addError("Don't know what kind of HTML tag is needed to load: " + urlEntry.url); 121 | }; 122 | 123 | const transformWebtampUrls = state => tree => { 124 | 125 | const replaceManifestUrl = string => 126 | state.resolveWebtampUrl(string, true) || string 127 | 128 | tree.match({ attrs: true }, node => { 129 | 130 | for (const [attr, attrValue] of Object.entries(node.attrs)) 131 | node.attrs[attr] = replaceManifestUrl(node.attrs[attr]); 132 | 133 | if (node.tag == 'style' && node.content) 134 | // node.content is an Array 135 | node.content = node.content.map(c => 136 | c.replace( 137 | /(url *\( *)(.+?)( *\))/, 138 | (m, p1, p2, p3) => `${p1}${replaceManifestUrl(p2)}${p3}`)); 139 | 140 | return node; 141 | }); 142 | return tree; 143 | }; 144 | 145 | const withManifestEntry = (state, name, use) => { 146 | const entry = state.manifest.entries[name]; 147 | if (entry === undefined) 148 | state.addError(`Manifest entry not found: ${name}`); 149 | else 150 | return use(entry); 151 | } 152 | 153 | const minifyPlugin = ({ test = isHtmlFile, options = {} } = {}) => 154 | Modify.content(/\.html$/, html => 155 | HtmlMinifier.minify(html, options)) 156 | 157 | export const minify = minifyPlugin 158 | export const replace = replacementPlugin 159 | -------------------------------------------------------------------------------- /src/plugins/inline.ts: -------------------------------------------------------------------------------- 1 | import * as Mime from 'mime-types' 2 | import * as FS from 'fs' 3 | import * as Utils from '../utils' 4 | import { Plugin, Op, FileContent } from '../types' 5 | 6 | type CriteriaArg = { 7 | dest? : string 8 | manifestName: string 9 | size : () => number 10 | src? : string 11 | stats? : () => FS.Stats 12 | } 13 | 14 | // string = yes with custom mimeType 15 | type CriteriaResult = undefined | boolean | string | null 16 | 17 | type Criteria = (a: CriteriaArg) => CriteriaResult 18 | 19 | export const data: (_: Criteria) => Plugin = criteria => state => { 20 | 21 | for (const [name, value] of Object.entries(state.manifest.getEntries())) { 22 | const dest = value.local ? Utils.fixRelativePath(value.local) : undefined 23 | 24 | const inline = (op: Op, arg: CriteriaArg, contentBufferFn: FileContent) => { 25 | const result = criteria(arg) 26 | if (result || result === '') 27 | state.checkThenRunIfNoErrors(() => { 28 | if (result === true) { 29 | const m = (arg.dest && Mime.lookup(arg.dest)) || (arg.src && Mime.lookup(arg.src)) 30 | if (m) 31 | return m 32 | else 33 | state.addError(`Error inlining ${name}. Unable to discern mime-type for ${arg.dest}`) 34 | } else if (typeof result === 'string') 35 | return result 36 | else 37 | state.addError(`Error inlining ${name}. Invalid mime-type: ${JSON.stringify(result)}`) 38 | }, mimeType => { 39 | state.removeOp(op) 40 | const mediatype = mimeType === '' ? '' : `${mimeType};` 41 | const data = contentBufferFn().toString("base64") 42 | state.manifest.delete(name) 43 | state.manifest.addUrl(name, `data:${mediatype}base64,${data}`) 44 | }) 45 | } 46 | 47 | let i = state.ops.length 48 | while (i-- > 0) { 49 | // TODO Below we use 'from' & 'originallyFrom' but wouldn't 'to' make more sense? 50 | const op = state.ops[i] 51 | if (op.type === 'write' && op.to.path === dest) { 52 | const arg = { 53 | manifestName: name, 54 | src: op.originallyFrom?.abs, 55 | stats: op.originallyFrom?.stats, // TODO yuk 56 | size: () => op.content.length, 57 | dest, 58 | } 59 | inline(op, arg, () => Buffer.from(op.content)) 60 | i = 0 61 | } else if (op.type === 'copy' && op.to.path === dest) { 62 | const arg = { 63 | manifestName: name, 64 | src: op.from.abs, 65 | stats: op.from.stats, 66 | size: op.from.size, 67 | dest, 68 | } 69 | inline(op, arg, op.from.content) 70 | i = 0 71 | } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/plugins/manifest.ts: -------------------------------------------------------------------------------- 1 | import { Manifest } from '../manifest' 2 | import { Plugin } from '../types' 3 | import * as Path from 'path' 4 | import State from '../state' 5 | 6 | const term = (s: string): string => 7 | /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(s) ? s : "`" + s + "`" 8 | 9 | const stringLiteral = (s: string): string => 10 | /["\n\r\t\\]/.test(s) ? `"""${s}"""` : `"${s}"` 11 | 12 | type ExtractCssArgs = { 13 | fileFilter : (_: string) => string 14 | newManifestName: (_: string) => string 15 | } 16 | 17 | export const extractCss: (_: ExtractCssArgs) => Plugin = ({ 18 | fileFilter = f => /\.css$/.test(f), 19 | newManifestName = n => `${n}Urls`, 20 | }) => state => { 21 | 22 | for (const [manifestKey, manifestValue] of Object.entries(state.manifest.getEntries())) { 23 | const filename = manifestValue.local 24 | if (filename && fileFilter(filename)) { 25 | const op = state.getOpThatCreatesLocalFile(filename) 26 | if (op) { 27 | const name = newManifestName(manifestKey) 28 | const css = State.opContent(op) 29 | const urls: Array = []; 30 | (css.match(/url *\( *.+? *\)/g) || []).forEach((s) => { 31 | if (!/url\(data:/.test(s)) { 32 | const m = s.match(/url *\( *['"]?(.+?)['"]? *\)$/) 33 | if (m && m[1]) 34 | urls.push(m[1]) 35 | } 36 | }) 37 | state.manifest.addList(name, urls) 38 | } 39 | } 40 | } 41 | } 42 | 43 | type ScalaArgs = { 44 | object : string 45 | filename : string 46 | outputPath: string 47 | nameMod? : (_: string) => string 48 | abstract? : boolean 49 | } 50 | 51 | const scala: (_: ScalaArgs) => Plugin = args => state => { 52 | const { object, filename, outputPath, abstract, nameMod = n => n } = args 53 | 54 | const modify: (s: String) => String = 55 | abstract ? s => `modify(${s})` : s => s 56 | 57 | const def: String = 58 | abstract ? "final val" : "def" 59 | 60 | const type: String = 61 | abstract ? "A" : "String" 62 | 63 | const fqcn = object.match(/^(.+)\.([^.]+)$/) 64 | if (!fqcn) { 65 | state.addError(`Invalid object FQCN: ${object}`) 66 | } else { 67 | 68 | const manifest = state.manifest 69 | const [, pkg, obj] = fqcn 70 | let cdnUsed = false 71 | 72 | const defs = [] 73 | for (const k of Object.keys(manifest.getEntries()).sort()) { 74 | const v = manifest.getEntries()[k] 75 | // console.log(`${k} = ${require('../utils').inspect(v)}`) 76 | 77 | const name = nameMod(k) 78 | const url = Manifest.url(v, false) 79 | if (v.cdn) { 80 | cdnUsed = true 81 | const {url, integrity} = v.cdn 82 | const i = integrity ? `Some(${stringLiteral(integrity)})` : 'None' 83 | defs.push(`${def} ${term(name)} = CDN(\n href = ${stringLiteral(url)},\n integrity = ${i})`) 84 | } else if (url) { 85 | defs.push(`${def} ${term(name)} = ${modify(stringLiteral(url))}`) 86 | } else if (v.list) { 87 | const vs = v.list.map(i => `\n ${modify(stringLiteral(i))} ::`).join('') 88 | defs.push(`${def} ${term(name)}: List[${type}] =${vs}\n Nil`) 89 | } 90 | 91 | // final case class Resource(url: String, integrity: Option[String]) 92 | } 93 | 94 | const lines: Array = [] 95 | lines.push(`package ${pkg}`) 96 | lines.push('') 97 | lines.push("/** Generated by webtamp. */") 98 | 99 | if (abstract) { 100 | if (cdnUsed) { 101 | lines.push(`object ${obj} {`) 102 | lines.push(" final case class CDN(href: String, integrity: Option[String])") 103 | lines.push("}") 104 | lines.push("") 105 | } 106 | lines.push(`abstract class ${obj}[+A] {`) 107 | if (cdnUsed) { 108 | lines.push(` import ${obj}._`) 109 | lines.push('') 110 | } 111 | lines.push(' protected def modify(f: String): A') 112 | lines.push('') 113 | 114 | } else { 115 | // Non-abstract 116 | lines.push(`object ${obj} {`) 117 | lines.push("") 118 | if (cdnUsed) { 119 | lines.push(" final case class CDN(href: String, integrity: Option[String])") 120 | lines.push("") 121 | } 122 | } 123 | 124 | lines.push(defs.map(l => l.replace(/^/gm, " ")).join("\n\n")) 125 | lines.push("}") 126 | 127 | // console.log("-------------------------------------------------------------------------") 128 | // console.log(content) 129 | // console.log("-------------------------------------------------------------------------") 130 | 131 | const content = lines.join("\n") 132 | 133 | let outfile = filename || `${obj}.scala` 134 | if (outputPath) 135 | outfile = Path.join(outputPath, outfile) 136 | 137 | state.addOpWrite(outfile, content) 138 | } 139 | } 140 | 141 | export const generate = { scala } 142 | -------------------------------------------------------------------------------- /src/plugins/modify.ts: -------------------------------------------------------------------------------- 1 | import { LocalSrc, Plugin, Op, Falsy } from '../types' 2 | import * as Utils from '../utils' 3 | import State from '../state' 4 | 5 | const assertResult = Utils.assertObject([], ['newFilename', 'newContent']) 6 | 7 | export type LogicInput = { 8 | originallyFrom?: LocalSrc 9 | filename : string 10 | content : () => string 11 | } 12 | 13 | export type LogicResult = Falsy | { 14 | newFilename?: string 15 | newContent? : string 16 | } 17 | 18 | export type Logic = (s: State) => (_: LogicInput) => LogicResult 19 | 20 | const main: (_: Logic) => Plugin = logic => state => { 21 | const modifyFn = logic(state) 22 | 23 | const attempt = (op: Op, input: LogicInput) => { 24 | const desc = input.originallyFrom ? input.originallyFrom.path : input.filename 25 | const result = state.scopeErrors(desc + ":", () => modifyFn(input)) 26 | if (result) { 27 | assertResult(result) 28 | 29 | const contentChanged = result.newContent !== undefined && result.newContent !== input.content() 30 | const newContent = (contentChanged && result.newContent) || input.content() 31 | 32 | const oldFilename = op.to.path 33 | const newFilename = result.newFilename !== undefined ? Utils.fixRelativePath(result.newFilename) : oldFilename 34 | const newTo = op.to.withNewPath(newFilename) 35 | const filenameChanged = newFilename !== oldFilename 36 | 37 | state.removeOp(op) 38 | 39 | if (op.type === 'copy' && !contentChanged) 40 | state.ops.push(Object.assign({}, op, { to: newTo })) 41 | else 42 | state.addOpWrite(newTo, newContent, input.originallyFrom) 43 | 44 | if (filenameChanged) 45 | renameLocal(state, oldFilename, newFilename) 46 | } 47 | } 48 | 49 | const renameLocal = (state: State, before: string, after: string) => { 50 | const beforeUrl = '/' + before 51 | const afterUrl = '/' + after 52 | 53 | const replaceAtKey: (key: String) => (_: A) => A = key => o => { 54 | // @ts-ignore 55 | if (o[key] === beforeUrl) { 56 | const r = Object.assign({}, o) 57 | // @ts-ignore 58 | r[key] = afterUrl 59 | return r 60 | } else 61 | return o 62 | } 63 | 64 | // Update state.urls 65 | state.urls = Utils.mapObjectValues(state.urls, urls => urls.map(replaceAtKey('url'))) 66 | 67 | // Replace state.manifest 68 | state.manifest.mapValues(replaceAtKey('local')) 69 | } 70 | 71 | for (const op of state.ops) { 72 | if (op.type === 'copy') { 73 | if (!op.transitive) { 74 | const originallyFrom = op.from 75 | const filename = op.to.path 76 | const content = () => op.from.content().toString() 77 | attempt(op, { originallyFrom, filename, content }) 78 | } 79 | } else if (op.type === 'write') { 80 | const originallyFrom = op.originallyFrom 81 | const filename = op.to.path 82 | const content = () => op.content 83 | attempt(op, { originallyFrom, filename, content }) 84 | } 85 | } 86 | } 87 | 88 | export const stateless = (f: (_: LogicInput) => LogicResult) => main(_ => f) 89 | 90 | type FilenameTest = RegExp | ((_: string) => boolean) 91 | 92 | const widenFilenameTest: (t: FilenameTest) => (i: LogicInput) => boolean = t => { 93 | const test = (t instanceof RegExp) ? t.test.bind(t) : t 94 | return i => test(i.filename) || (i.originallyFrom && test(i.originallyFrom.path)) || false 95 | } 96 | 97 | const modifyContent = (testFilename: FilenameTest, modify: (_: string) => string, { failUnlessChange }: ({ failUnlessChange?: boolean }) = {}) => { 98 | const test = widenFilenameTest(testFilename) 99 | return main(s => { 100 | 101 | const done = (a: string) => ({ newContent: a }) 102 | 103 | const apply: (i: LogicInput, before: string, after: string) => LogicResult = 104 | failUnlessChange 105 | ? (i, before, after) => {if (before === after) s.addError(`Failed to change ${i.filename}`); else return done(after)} 106 | : (i, before, after) => done(after) 107 | 108 | const run = (i: LogicInput) => { 109 | const content = i.content() 110 | return apply(i, content, modify(content)) 111 | } 112 | 113 | return i => test(i) && run(i) 114 | }) 115 | } 116 | 117 | export const rename = (testFilename: FilenameTest, modifyFilename: (_: string) => string) => { 118 | const test = widenFilenameTest(testFilename) 119 | return stateless(i => test(i) && { newFilename: modifyFilename(i.filename) }) 120 | } 121 | 122 | export type ReplaceWebtampUrlOptions = { 123 | testFilename : FilenameTest 124 | urlPattern? : RegExp | Array 125 | urlQuotes? : Array 126 | preprocessUrl?: (url: string) => (string | Falsy) 127 | allowCDN? : boolean 128 | } 129 | 130 | export const replaceWebtampUrls: (_: ReplaceWebtampUrlOptions) => Plugin = opts => { 131 | const test = widenFilenameTest(opts.testFilename) 132 | 133 | const urlPatterns = Utils.asArray(opts.urlPattern ?? []) 134 | if (opts.urlQuotes) 135 | for (const q of opts.urlQuotes) { 136 | const [q1, q2] = typeof q === 'string' ? [q, q] : [q[0], q[1] || q[0]] 137 | const r1 = Utils.escapeRegExp(q1) 138 | const r2 = Utils.escapeRegExp(q2) 139 | const r = new RegExp(`(?<=${r1}).+?(?=${r2})`, "g") 140 | urlPatterns.push(r) 141 | } 142 | 143 | const preprocessUrl = opts.preprocessUrl ?? (u => u) 144 | 145 | const allowCDN = opts.allowCDN ?? true 146 | 147 | if (urlPatterns.length == 0) 148 | return s => s.addWarn(`No url patterns or quotes defined in: ${JSON.stringify(opts)}`) 149 | else 150 | return main(state => i => { 151 | 152 | const replaceUrl = (origUrl: string): string => { 153 | const url = preprocessUrl(origUrl) 154 | if (url) { 155 | const newUrl = state.resolveWebtampUrl(url, allowCDN) 156 | return newUrl ?? url 157 | } else 158 | return origUrl 159 | } 160 | 161 | const doIt = (i: LogicInput): LogicResult => { 162 | const orig = i.content() 163 | let result = orig 164 | for (const urlPattern of urlPatterns) { 165 | result = result.replace(urlPattern, replaceUrl) 166 | } 167 | return (result !== orig) && { newContent: result } 168 | } 169 | 170 | return test(i) && doIt(i) 171 | }) 172 | } 173 | 174 | export const content = modifyContent 175 | export const stateful = main 176 | -------------------------------------------------------------------------------- /src/state.ts: -------------------------------------------------------------------------------- 1 | import { assertObject, fixRelativePath, opReduce } from './utils' 2 | import { Manifest, Entry } from './manifest' 3 | import { 4 | LocalSrc, 5 | ObjectTo, 6 | Op, 7 | OpReduction, 8 | OutputFile, 9 | RecObjectTo, 10 | URL, 11 | } from './types' 12 | 13 | export default class State { 14 | public readonly src: string 15 | public readonly target: string 16 | public ops : Array 17 | private errors : Array 18 | private warns : Array 19 | public manifest: Manifest 20 | public urls : ObjectTo> 21 | private deps : ObjectTo> 22 | private pending : ObjectTo any> | undefined> 23 | private graph : undefined | RecObjectTo 24 | 25 | constructor(src: string, target: string) { 26 | this.src = src 27 | this.target = target 28 | this.ops = [] 29 | this.errors = [] 30 | this.warns = [] 31 | this.manifest = new Manifest(this) 32 | this.urls = {} 33 | this.deps = {} 34 | this.pending = {} 35 | } 36 | 37 | toObject(): ObjectTo { 38 | return { 39 | src : this.src, 40 | target : this.target, 41 | ops : this.ops, 42 | errors : this.errors, 43 | warns : this.warns, 44 | manifest: this.manifest, 45 | urls : this.urls, 46 | deps : this.deps, 47 | pending : this.pending, 48 | } 49 | } 50 | 51 | private addOp(o: Op): void { 52 | this.ops.push(o) 53 | } 54 | 55 | addOpCopy(from: LocalSrc, to: string | OutputFile, transitive?: boolean): void { 56 | this.addOp({ 57 | type: 'copy', 58 | from, 59 | to: (to instanceof OutputFile) ? to : new OutputFile(this.target, to), 60 | transitive: !!transitive, 61 | }) 62 | } 63 | 64 | addOpWrite(to: string | OutputFile, content: string, originallyFrom?: LocalSrc): void { 65 | this.addOp({ 66 | type: 'write', 67 | originallyFrom, 68 | to: (to instanceof OutputFile) ? to : new OutputFile(this.target, to), 69 | content, 70 | }) 71 | } 72 | 73 | addError(o: string): void { 74 | this.errors.push(o) 75 | } 76 | 77 | addWarn(o: string): void { 78 | this.warns.push(o) 79 | } 80 | 81 | addUrl(assetName: string, url: URL): void { 82 | assertObject(['url'], ['integrity', 'crossorigin', 'transitive', 'as'])(url) 83 | if (!this.urls[assetName]) 84 | this.addError(`Asset not registered: ${assetName}`) 85 | this.urls[assetName].push(url) 86 | } 87 | 88 | scopeErrors(mod: string | ((_: string) => string), block: () => A): A { 89 | if (mod === '' || !mod) return block() 90 | const e = this.errors.length 91 | const r = block() 92 | let i = this.errors.length 93 | const f: (_: string) => string = 94 | (typeof mod === 'string') ? 95 | e => `${mod} ${e}` : 96 | mod 97 | while (--i >= e) 98 | this.errors[i] = f(this.errors[i]) 99 | return r 100 | } 101 | 102 | removeOp(op: Op): void { 103 | this.ops = this.ops.filter(o => o !== op) 104 | } 105 | 106 | getOpThatCreatesLocalFile(path: string): Op | undefined { 107 | const p = fixRelativePath(path) 108 | const f: OpReduction = { 109 | copy : (op: Op) => op.to.path === p, 110 | write: (op: Op) => op.to.path === p, 111 | } 112 | const r = this.ops.filter(opReduce(f)) 113 | if (r.length === 0) 114 | this.addError(`Unable to find op that writes to ${path}`) 115 | else if (r.length > 1) 116 | this.addError(`Multiple ops write to ${path}: ${r}`) 117 | return r[0] 118 | } 119 | 120 | checkThenRunIfNoErrors(check: () => (A | false), run: (_: A) => any): void { 121 | const errCount = this.errors.length 122 | const a = check() 123 | if (this.errors.length === errCount && a !== false) 124 | run(a) 125 | } 126 | 127 | registerNow(name: string): void { 128 | if (this.pending[name]) 129 | this.addError(`Duplicate asset: ${name}`) 130 | else if (!this.deps[name]) { 131 | this.deps[name] = [] 132 | this.urls[name] = [] 133 | } 134 | } 135 | 136 | registerForLater(name: string, register: () => any) { 137 | if (this.deps[name]) 138 | this.addError(`Duplicate asset: ${name}`) 139 | else { 140 | if (!this.pending[name]) 141 | this.pending[name] = [] 142 | // @ts-ignore: undefined 143 | this.pending[name].push(register) 144 | } 145 | } 146 | 147 | addDependency(from: string, to: string) { 148 | const a = this.deps[from] 149 | if (!a) 150 | this.deps[from] = [to] 151 | else if (Object.isFrozen(a)) 152 | this.addError(`Can't add dependency ${to} to terminal asset ${from}.`) 153 | else if (!a.includes(to)) 154 | a.push(to) 155 | } 156 | 157 | /** Resolve required, pending deps */ 158 | resolvePending() { 159 | const loop = () => { 160 | // This seems stupid, lazy way of doing it but it's been too long a day so meh 161 | const changed = [false] 162 | for (const [name, deps] of Object.entries(this.deps)) 163 | for (const dep of deps) { 164 | if (this.deps[dep]) { 165 | // Already registered - do nothing 166 | } else if (this.pending[dep]) { 167 | const fns = this.pending[dep] 168 | this.pending[dep] = undefined 169 | if (fns) 170 | fns.forEach(fn => fn()) 171 | changed[0] = true 172 | } else { 173 | this.addError(`${name} referenced an unspecified asset: ${dep}`) 174 | } 175 | } 176 | return changed[0] 177 | } 178 | while (loop()); 179 | } 180 | 181 | graphDependencies() { 182 | this.graph = undefined 183 | if (this.ok()) { 184 | const graph: RecObjectTo = {} 185 | const add = (n: string) => { 186 | 187 | if (graph[n] === undefined) { 188 | 189 | graph[n] = null 190 | const deps = this.deps[n] || [] 191 | deps.forEach(add) 192 | const graphN: RecObjectTo = {} 193 | graph[n] = graphN 194 | deps.forEach(d => graphN[d] = graph[d]) 195 | Object.freeze(graph[n]) 196 | 197 | } else if (graph[n] === null) { 198 | this.addError(`Circular dependency on asset: ${n}`) 199 | } 200 | } 201 | Object.keys(this.deps).forEach(add) 202 | Object.freeze(graph) 203 | this.graph = graph 204 | } 205 | // console.log("State: ", this) 206 | // console.log("Manifest: ", this.manifest.getEntries()) 207 | } 208 | 209 | ok() { 210 | return this.errors.length == 0 211 | } 212 | 213 | results() { 214 | return { 215 | ops: this.ops, 216 | errors: this.errors.sort(), 217 | warns: this.warns.sort(), 218 | manifest: this.manifest, 219 | graph: this.graph, 220 | } 221 | } 222 | 223 | resolveWebtampUrlEntry(url: string): Entry | undefined { 224 | let m = url.match(/^webtamp:\/\/(.*)$/) 225 | if (!m) 226 | return 227 | const path = m[1] 228 | 229 | // Manifest URLs 230 | if (m = path.match(/manifest\/(.*)$/)) { 231 | const name = m[1] 232 | const entry = this.manifest.getEntries()[name]; 233 | if (entry === undefined) 234 | this.addError(`Manifest entry not found: ${name}`); 235 | else 236 | return entry 237 | } 238 | 239 | // Invalid URL type 240 | else 241 | this.addError(`Invalid webtamp url: ${url}`) 242 | } 243 | 244 | resolveWebtampUrl(url: string, allowCdn: boolean): string | undefined { 245 | const e = this.resolveWebtampUrlEntry(url) 246 | if (!e) 247 | return 248 | const result = Manifest.url(e, allowCdn) 249 | if (result) 250 | return result 251 | this.addError(`Unable to discern URL for manifest entry: {${url}: ${e}}`) 252 | } 253 | 254 | static opContent: (op: Op) => string = 255 | opReduce({ 256 | copy : (op) => op.from.content().toString(), 257 | write: (op) => op.content, 258 | }) 259 | } 260 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { memoise, fixRelativePath } from './utils' 2 | import * as FS from 'fs' 3 | import * as Path from 'path' 4 | import State from './state' 5 | 6 | export type Algo = 7 | 'blake2b512' | 8 | 'blake2s256' | 9 | 'md4' | 10 | 'md5' | 11 | 'md5-sha1' | 12 | 'mdc2' | 13 | 'ripemd160' | 14 | 'sha1' | 15 | 'sha224' | 16 | 'sha256' | 17 | 'sha3-224' | 18 | 'sha3-256' | 19 | 'sha3-384' | 20 | 'sha3-512' | 21 | 'sha384' | 22 | 'sha512' | 23 | 'sha512-224' | 24 | 'sha512-256' | 25 | 'shake128' | 26 | 'shake256' | 27 | 'sm3' | 28 | 'whirlpool' 29 | 30 | export type ObjectTo = {[key: string]: V} 31 | 32 | export interface RecObjectTo { 33 | [key: string]: A | RecObjectTo 34 | } 35 | 36 | export type RecArray = Array | Array> 37 | 38 | export type ValueOrRecArray = A | RecArray 39 | 40 | export type ValueOrArray = A | Array 41 | 42 | export class LocalSrc { 43 | public ctx : string 44 | public path : string 45 | public abs : string 46 | public stats : () => FS.Stats 47 | public size : () => number 48 | public content: () => Buffer 49 | 50 | constructor(ctx: string, path: string) { 51 | this.ctx = ctx 52 | this.path = fixRelativePath(path) 53 | this.abs = Path.resolve(this.ctx, this.path) 54 | this.stats = memoise(() => FS.statSync(this.abs)) 55 | this.size = () => this.stats().size 56 | this.content = memoise(() => FS.readFileSync(this.abs)) 57 | } 58 | } 59 | 60 | export class OutputFile { 61 | public ctx : string 62 | public path: string 63 | public abs : string 64 | 65 | constructor(ctx: string, path: string) { 66 | this.ctx = ctx 67 | this.path = fixRelativePath(path) 68 | this.abs = Path.resolve(this.ctx, this.path) 69 | } 70 | 71 | withNewPath(path: string): OutputFile { 72 | return new OutputFile(this.ctx, path) 73 | } 74 | } 75 | 76 | export type As = 'script' | 'style' 77 | 78 | export type CDN = { 79 | url : string 80 | integrity? : string 81 | transitive?: boolean 82 | as? : As 83 | } 84 | 85 | export type URL = { 86 | url : string 87 | integrity? : string 88 | crossorigin?: 'anonymous' 89 | transitive? : boolean 90 | as? : As 91 | } 92 | 93 | export type Op = OpCopy | OpWrite 94 | 95 | export type OpCopy = { 96 | type : 'copy' 97 | from : LocalSrc 98 | to : OutputFile 99 | transitive: boolean 100 | } 101 | 102 | export type OpWrite = { 103 | type : 'write' 104 | originallyFrom?: LocalSrc 105 | to : OutputFile 106 | content : string 107 | } 108 | 109 | export type OpReduction = { 110 | copy : (op: OpCopy ) => A 111 | write: (op: OpWrite) => A 112 | } 113 | 114 | export type OpReductionConst = { 115 | copy : A 116 | write: A 117 | } 118 | 119 | export type FileContent = () => (string | Buffer) 120 | 121 | export type InlineFile = {name: string, contents: FileContent} 122 | 123 | export type Falsy = null | undefined | false 124 | 125 | export type Plugin = (s: State) => void 126 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Algo, 3 | ObjectTo, 4 | Op, 5 | OpReduction, 6 | OpReductionConst, 7 | RecArray, 8 | ValueOrRecArray 9 | } from './types' 10 | import * as Crypto from 'crypto' 11 | import * as Util from 'util' 12 | import { type } from 'os' 13 | 14 | export function arrayMinus(a: Array, b: Array): Array { 15 | return a.filter(k => b.indexOf(k) === -1) 16 | } 17 | 18 | export function asArray(v: ValueOrRecArray | undefined): Array { 19 | // @ts-ignore 20 | return v === undefined ? [] : flatten([v]) 21 | } 22 | 23 | const assert = (cond: boolean, msg: string): void => { 24 | if (cond !== true) 25 | throw `Assertion failed: ${msg}` 26 | } 27 | 28 | // TODO delete assertObject 29 | export const assertObject = (mandatoryKeys: Array, optionalKeys: Array = []) => (o: any) => { 30 | assert(typeof o === 'object' && !Array.isArray(o), `Object expected: ${o}`) 31 | const keys = arrayMinus(Object.keys(o), optionalKeys) 32 | const missing = arrayMinus(mandatoryKeys, keys) 33 | const extra = arrayMinus(keys, mandatoryKeys) 34 | assert(missing.length + extra.length === 0, `Missing: [${missing}]. Extra: [${extra}].`) 35 | } 36 | 37 | /** left-to-right function composition */ 38 | export const chain: (_: Array<(_: A) => A>) => (_: A) => A = 39 | fs => input => { 40 | let a = input 41 | for (const f of fs) 42 | a = f(a) 43 | return a 44 | } 45 | 46 | /** right-to-left function composition */ 47 | export const compose: (_: Array<(_: A) => A>) => (_: A) => A = 48 | fs => chain(fs.reverse()) 49 | 50 | export const fixRelativePath: (_: string) => string = 51 | s => s.replace(/^(?:\.\/+)*\/*/g, '') 52 | 53 | export function flatten(array: RecArray): Array { 54 | // @ts-ignore 55 | return array.reduce((a, b) => a.concat(Array.isArray(b) ? flatten(b) : b), []) 56 | } 57 | 58 | export const hashData: (algo: Algo, outFmt: Crypto.HexBase64Latin1Encoding) => (data: Crypto.BinaryLike) => string = 59 | (algo, outFmt) => (data) => { 60 | const h = Crypto.createHash(algo) 61 | h.update(data) 62 | return h.digest(outFmt) 63 | } 64 | 65 | export const inspect = (o: any): string => Util.inspect(o, true, null, true) 66 | 67 | export function mapObjectValues(src: ObjectTo, f: (_: A) => B): ObjectTo { 68 | const o: ObjectTo = {} 69 | for (const [k, v] of Object.entries(src)) 70 | o[k] = f(v) 71 | return o 72 | } 73 | 74 | export function memoise(fn: () => A): () => A { 75 | const r: Array = [] 76 | return () => r[0] || (r[0] = fn()) || r[0] 77 | } 78 | 79 | export function opReduce(f: OpReduction): (op: Op) => A { 80 | return (op) => { 81 | switch (op.type) { 82 | case 'copy' : return f.copy(op) 83 | case 'write': return f.write(op) 84 | } 85 | } 86 | } 87 | 88 | export function opReduceConst(f: OpReductionConst): (op: Op) => A { 89 | return (op) => { 90 | switch (op.type) { 91 | case 'copy' : return f.copy 92 | case 'write': return f.write 93 | } 94 | } 95 | } 96 | 97 | export function tap(f: (_: A) => any): (_: A) => A { 98 | return a => { 99 | f(a) 100 | return a 101 | } 102 | } 103 | 104 | export function removeUndefinedValues(a: A): A { 105 | if (typeof a === 'object') { 106 | const o: object = {} 107 | for (const [k, v] of Object.entries(a)) 108 | if (v !== undefined) 109 | // @ts-ignore 110 | o[k] = v 111 | // @ts-ignore 112 | return o as A 113 | } else 114 | return a 115 | } 116 | 117 | export function escapeRegExp(string: string): string { 118 | return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string 119 | } -------------------------------------------------------------------------------- /test/data.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const Path = require('path'); 4 | 5 | // cat test/data/image1.svg | openssl dgst -sha256 -binary | openssl base64 -A 6 | 7 | const data = { 8 | 9 | css1: { type: 'local', files: '1.css', manifest: 'css1' }, 10 | css2: { type: 'local', files: '2.css', manifest: 'css2' }, 11 | 12 | vizJs: { type: 'local', files: 'vendor/v?z.js', manifest: true }, 13 | vizJsExplicit: { type: 'local', files: 'vendor/v?z.js', manifest: 'vizJs' }, 14 | 15 | image1SvgSha256: 'sha256-A/Q7jy5ivY2cPMuPnY+LJpxE7xyEJhPi5UchebJAaVA=', 16 | image2SvgSha256: 'sha256-iN39iYUkBuORbiinlAfVZAPIrV558O7KzRSzSP0aZng=', 17 | image2SvgSha384: 'sha384-MY1+aNx3EQM6G5atTiVuZcv6x2a+erMjYoaEH7WPHA6CpuihomIrPuqDHpL48fWI', 18 | 19 | jqueryCdn: Object.freeze({ 20 | type: 'cdn', 21 | url: 'https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.4/jquery.min.js', 22 | integrity: 'sha256-BbhdlvQf/xTY9gja0Dq3HiwQF8LaCRTXxZKRutelT44=', 23 | }), 24 | 25 | bootstrapCssCdn: Object.freeze({ 26 | type: 'cdn', 27 | url: 'https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css', 28 | integrity: 'sha384-rwoIResjU2yc3z8GV/NPeZWAv56rSmLldC3R/AZzGRnGxQQKnKkoFVhFQhNUwEyJ', 29 | }), 30 | 31 | src: Path.resolve(__dirname, 'data'), 32 | target: '/tmp/tool-thingy', 33 | cfg: o => Object.assign({ src: data.src, output: { dir: data.target, manifest: false } }, o || {}), 34 | }; 35 | 36 | data.jqueryUrl = data.jqueryCdn.url; 37 | 38 | data.jqueryCdnM = Object.assign({manifest: true}, data.jqueryCdn); 39 | 40 | data.jqueryManifestEntry = Object.freeze({ 41 | cdn: { 42 | url: data.jqueryCdn.url, 43 | integrity: data.jqueryCdn.integrity, 44 | } 45 | }); 46 | 47 | data.jqueryUrlEntry = Object.freeze({ 48 | url: data.jqueryCdn.url, 49 | integrity: data.jqueryCdn.integrity, 50 | crossorigin: 'anonymous', 51 | }); 52 | 53 | Object.freeze(data); 54 | 55 | module.exports = data; 56 | -------------------------------------------------------------------------------- /test/data/1.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Lato:400,700,400italic,700italic&subset=latin'); 2 | 3 | @font-face { 4 | font-family: Icons; 5 | src: url(icons.eot); 6 | src: url(icons.eot?#iefix) format("embedded-opentype"), 7 | url(icons.woff2) format("woff2"), 8 | url(icons.woff) format("woff"), 9 | url(icons.ttf) format("truetype"), 10 | url(icons.svg#icons) format("svg"); 11 | font-style: normal; 12 | font-weight: 400; 13 | font-variant: normal; 14 | text-decoration: inherit; 15 | text-transform: none 16 | } 17 | -------------------------------------------------------------------------------- /test/data/2.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: Icons; 3 | font-style: normal; 4 | font-weight: 400; 5 | font-variant: normal; 6 | text-decoration: inherit; 7 | text-transform: none 8 | } 9 | -------------------------------------------------------------------------------- /test/data/hello.js: -------------------------------------------------------------------------------- 1 | console.log("hello"); 2 | -------------------------------------------------------------------------------- /test/data/image1.svg: -------------------------------------------------------------------------------- 1 | image1 2 | -------------------------------------------------------------------------------- /test/data/image2.svg: -------------------------------------------------------------------------------- 1 | image2 2 | -------------------------------------------------------------------------------- /test/data/image3.svg: -------------------------------------------------------------------------------- 1 | I am image 3. I am large. 2 | I am image 3. I am large. 3 | I am image 3. I am large. 4 | I am image 3. I am large. 5 | I am image 3. I am large. 6 | I am image 3. I am large. 7 | I am image 3. I am large. 8 | I am image 3. I am large. 9 | I am image 3. I am large. 10 | I am image 3. I am large. 11 | I am image 3. I am large. 12 | I am image 3. I am large. 13 | I am image 3. I am large. 14 | I am image 3. I am large. 15 | I am image 3. I am large. 16 | I am image 3. I am large. 17 | I am image 3. I am large. 18 | I am image 3. I am large. 19 | I am image 3. I am large. 20 | I am image 3. I am large. 21 | I am image 3. I am large. 22 | I am image 3. I am large. 23 | I am image 3. I am large. 24 | I am image 3. I am large. 25 | I am image 3. I am large. 26 | I am image 3. I am large. 27 | I am image 3. I am large. 28 | I am image 3. I am large. 29 | I am image 3. I am large. 30 | I am image 3. I am large. 31 | I am image 3. I am large. 32 | I am image 3. I am large. 33 | I am image 3. I am large. 34 | I am image 3. I am large. 35 | I am image 3. I am large. 36 | I am image 3. I am large. 37 | I am image 3. I am large. 38 | I am image 3. I am large. 39 | I am image 3. I am large. 40 | I am image 3. I am large. 41 | I am image 3. I am large. 42 | I am image 3. I am large. 43 | I am image 3. I am large. 44 | I am image 3. I am large. 45 | I am image 3. I am large. 46 | I am image 3. I am large. 47 | I am image 3. I am large. 48 | I am image 3. I am large. 49 | I am image 3. I am large. 50 | I am image 3. I am large. 51 | I am image 3. I am large. 52 | I am image 3. I am large. 53 | I am image 3. I am large. 54 | I am image 3. I am large. 55 | I am image 3. I am large. 56 | I am image 3. I am large. 57 | I am image 3. I am large. 58 | I am image 3. I am large. 59 | I am image 3. I am large. 60 | I am image 3. I am large. 61 | I am image 3. I am large. 62 | I am image 3. I am large. 63 | I am image 3. I am large. 64 | I am image 3. I am large. 65 | -------------------------------------------------------------------------------- /test/data/page1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Page 1 7 | 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /test/data/page2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Page 2 6 | 7 | 12 | 13 | 14 | Image 2 15 | 16 | 17 | -------------------------------------------------------------------------------- /test/data/page3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Page 1 7 | 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /test/data/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "webtamp://manifest/image1", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "webtamp://manifest/image2", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /test/data/vendor/viz.js: -------------------------------------------------------------------------------- 1 | console.log("hello, i'm viz.js"); 2 | -------------------------------------------------------------------------------- /test/outputNameTest.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const 4 | Assert = require('chai').assert, 5 | OutputName = require('../dist/outputName'); 6 | 7 | describe('OutputName', () => { 8 | describe('make', () => { 9 | const i = { name: 'y/hi.txt', contents: () => "12345678" } 10 | it("should replace [basename]", () => { 11 | const fn = OutputName.make("x/[basename]"); 12 | Assert.equal(fn(i), "x/hi.txt") 13 | }) 14 | it("should replace [name] & [ext]", () => { 15 | const fn = OutputName.make("[name]-hehe.[ext]"); 16 | Assert.equal(fn(i), "hi-hehe.txt") 17 | }) 18 | it("should replace [path]", () => { 19 | const fn = OutputName.make("[path]/123"); 20 | Assert.equal(fn(i), "y/123") 21 | }) 22 | it("should replace [md5]", () => { 23 | const fn = OutputName.make("[md5].[ext]"); 24 | Assert.equal(fn(i), "25d55ad283aa400af464c76d713c07ad.txt") 25 | }) 26 | it("should replace [sha256]", () => { 27 | const fn = OutputName.make("[sha256].[ext]"); 28 | Assert.equal(fn(i), "ef797c8118f02dfb649607dd5d3f8c7623048c9c063d532cc95c5ed7a898a64f.txt") 29 | }) 30 | it("should replace [sha256:16]", () => { 31 | const fn = OutputName.make("[sha256:16].[ext]"); 32 | Assert.equal(fn(i), "ef797c8118f02dfb.txt") 33 | }) 34 | it("should replace [hash]", () => { 35 | const fn = OutputName.make("[hash].[ext]"); 36 | Assert.equal(fn(i), "ef797c8118f02dfb649607dd5d3f8c76.txt") 37 | }) 38 | it("should replace [hash:16]", () => { 39 | const fn = OutputName.make("[hash:16].[ext]"); 40 | Assert.equal(fn(i), "ef797c8118f02dfb.txt") 41 | }) 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /test/planTest.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const 4 | Assert = require('chai').assert, 5 | Path = require('path'), 6 | Plan = require('../dist/plan'), 7 | State = require('../dist/state').default, 8 | TestData = require('./data'), 9 | TestUtil = require('./util'), 10 | LocalSrc = require('../dist/types').LocalSrc; 11 | 12 | const { vizJs, vizJsExplicit, image1SvgSha256, image2SvgSha256, image2SvgSha384, jqueryUrl, src, target } = TestData; 13 | 14 | const svgs = { type: 'local', files: '*{1,2}.svg', manifest: f => f.replace(/\.svg$/, 'Svg') }; 15 | 16 | function addSvgExpectations(expect) { 17 | for (const i of [1, 2]) { 18 | const f = `image${i}.svg`; 19 | expect.addOpCopy(new LocalSrc(src, f), f); 20 | expect.manifest.addPathLocal(`image${i}Svg`, '/' + f) 21 | } 22 | } 23 | 24 | // TODO add manifest writing tests 25 | 26 | describe('Plan', () => { 27 | describe('run', () => { 28 | 29 | const testPlan = TestUtil.testPlan(); 30 | const makeCfg = TestData.cfg; 31 | 32 | describe('local', () => { 33 | 34 | it('simple', () => { 35 | const cfg = makeCfg({ assets: { vizJs } }); 36 | testPlan(cfg, expect => { 37 | expect.addOpCopy(new LocalSrc(src, 'vendor/viz.js'), 'viz.js'); 38 | expect.manifest.addPathLocal('vizJs', '/viz.js') 39 | }) 40 | }); 41 | 42 | it('with src', () => { 43 | const cfg = makeCfg({ 44 | assets: { vizJs: { type: 'local', src: 'vendor', files: 'v?z.js', manifest: true } }, 45 | }); 46 | testPlan(cfg, expect => { 47 | expect.addOpCopy(new LocalSrc(src + '/vendor', 'viz.js'), 'viz.js'); 48 | expect.manifest.addPathLocal('vizJs', '/viz.js') 49 | }) 50 | }); 51 | 52 | it('no manifest', () => { 53 | const cfg = makeCfg({ 54 | assets: { vizJs: { type: 'local', files: 'vendor/v?z.js' } }, 55 | }); 56 | testPlan(cfg, expect => { 57 | expect.addOpCopy(new LocalSrc(src, 'vendor/viz.js'), 'viz.js'); 58 | }); 59 | }); 60 | 61 | it('manifest string', () => { 62 | const cfg = makeCfg({ 63 | assets: { vizJs: { type: 'local', files: 'vendor/v?z.js', manifest: 'omgJs' } }, 64 | }); 65 | testPlan(cfg, expect => { 66 | expect.addOpCopy(new LocalSrc(src, 'vendor/viz.js'), 'viz.js'); 67 | expect.manifest.addPathLocal('omgJs', '/viz.js') 68 | }); 69 | }); 70 | 71 | it('error when {manifest: true} in array', () => { 72 | const cfg = makeCfg({ 73 | assets: { vizJs: [vizJs] }, 74 | }); 75 | testPlan(cfg, expect => { 76 | expect.addError('vizJs has {manifest: true} but requires an explicit name or function.') 77 | }); 78 | }); 79 | 80 | it('hashed filename', () => { 81 | const cfg = makeCfg({ 82 | output: { dir: target, name: '[hash].[ext]', manifest: false }, 83 | assets: { vizJs }, 84 | }); 85 | testPlan(cfg, expect => { 86 | expect.addOpCopy(new LocalSrc(src, 'vendor/viz.js'), 'e4e91995e194dd59cafba1c0dad576c6.js'); 87 | expect.manifest.addPathLocal('vizJs', '/e4e91995e194dd59cafba1c0dad576c6.js') 88 | }); 89 | }); 90 | 91 | it('manifest fn', () => { 92 | const cfg = makeCfg({ 93 | assets: { svgs }, 94 | }); 95 | testPlan(cfg, expect => { 96 | addSvgExpectations(expect); 97 | }); 98 | }); 99 | 100 | it('manifest fn and outputPath', () => { 101 | const cfg = makeCfg({ 102 | assets: { svgs: Object.assign({ outputPath: 'img' }, svgs) } 103 | }); 104 | testPlan(cfg, expect => { 105 | for (const i of [1, 2]) { 106 | const f = `image${i}.svg`; 107 | expect.addOpCopy(new LocalSrc(src, f), 'img/' + f); 108 | expect.manifest.addPathLocal(`image${i}Svg`, '/img/' + f) 109 | } 110 | }); 111 | }); 112 | 113 | it('manifest fn and outputName', () => { 114 | const cfg = makeCfg({ 115 | assets: { svgs: Object.assign({ outputName: '[hash].[ext]' }, svgs) } 116 | }); 117 | testPlan(cfg, expect => { 118 | const hashes = ['03f43b8f2e62bd8d9c3ccb8f9d8f8b26', '88ddfd89852406e3916e28a79407d564']; 119 | for (const i of [1, 2]) { 120 | const fi = `image${i}.svg`; 121 | const fo = `${hashes[i-1]}.svg`; 122 | expect.addOpCopy(new LocalSrc(src, fi), fo); 123 | expect.manifest.addPathLocal(`image${i}Svg`, '/' + fo) 124 | } 125 | }); 126 | }); 127 | 128 | it('error if no files found (by default)', () => { 129 | const cfg = makeCfg({ 130 | assets: { blah: { type: 'local', files: '**/*.blah' } }, 131 | }); 132 | testPlan(cfg, expect => { 133 | expect.addError('blah:**/*.blah - 0 files found.'); 134 | }); 135 | }); 136 | 137 | it('error using custom validate', () => { 138 | const cfg = makeCfg({ 139 | assets: { 140 | blah1: { type: 'local', files: '*.svg', validate: _ => 'nah mate' }, 141 | blah2: { type: 'local', files: '*.ico', validate: _ => ['oi', 'nope'] }, 142 | }, 143 | }); 144 | testPlan(cfg, expect => { 145 | expect.addError(`blah1:*.svg - nah mate`); 146 | expect.addError(`blah2:*.ico - oi`); 147 | expect.addError(`blah2:*.ico - nope`); 148 | }); 149 | }); 150 | 151 | it('disable validation', () => { 152 | const cfg = makeCfg({ 153 | assets: { blah: { type: 'local', files: '**/*.blah', validate: false } }, 154 | }); 155 | testPlan(cfg, expect => {}); 156 | }); 157 | 158 | it('error when multiple assets write to same dest', () => { 159 | const cfg = makeCfg({ 160 | assets: { 161 | a: { type: 'local', files: 'image1.svg', outputName: 'x' }, 162 | b: { type: 'local', files: 'image2.svg', outputName: 'x' }, 163 | }, 164 | }); 165 | testPlan(cfg, expect => { 166 | expect.addOpCopy(new LocalSrc(src, 'image1.svg'), 'x'); 167 | expect.addOpCopy(new LocalSrc(src, 'image2.svg'), 'x'); 168 | expect.addError("Multiple assets write to the same target: x"); 169 | }); 170 | }); 171 | }); 172 | 173 | function testManifestRequiredInArray(valueA, valueB, subname, okA, okB) { 174 | Object.freeze(valueA); 175 | Object.freeze(valueB); 176 | 177 | it('manifest data not required', () => { 178 | const cfg = makeCfg({ assets: { extA: valueA, extB: valueB } }); 179 | testPlan(cfg, expect => { 180 | okA(expect); 181 | okB(expect); 182 | }); 183 | }); 184 | 185 | it('{manifest: true} reads manifest name from asset name', () => { 186 | const cfg = makeCfg({ 187 | assets: { 188 | extA: Object.assign({ manifest: true }, valueA), 189 | extB: Object.assign({ manifest: true }, valueB), 190 | } 191 | }); 192 | testPlan(cfg, expect => { 193 | okA(expect, "extA"); 194 | okB(expect, "extB"); 195 | }); 196 | }); 197 | 198 | it('explicit manifest names', () => { 199 | const cfg = makeCfg({ 200 | assets: { 201 | exts: [ 202 | Object.assign({ manifest: 'extA' }, valueA), 203 | Object.assign({ manifest: 'extB' }, valueB), 204 | ], 205 | }, 206 | }); 207 | testPlan(cfg, expect => { 208 | okA(expect, "extA"); 209 | okB(expect, "extB"); 210 | }); 211 | }); 212 | }; 213 | 214 | describe('external', () => { 215 | const a = { type: 'external', path: 'a.js' }; 216 | const b = { type: 'external', path: '/b.js' }; 217 | const okA = (expect, mName) => { if (mName) expect.manifest.addPathLocal(mName, '/a.js') }; 218 | const okB = (expect, mName) => { if (mName) expect.manifest.addPathLocal(mName, '/b.js') }; 219 | testManifestRequiredInArray(a, b, i => i.path, okA, okB); 220 | }); 221 | 222 | describe('optional', () => { 223 | it('ignored when not referenced', () => { 224 | const cfg = makeCfg({ 225 | assets: {}, 226 | optional: { vizJs }, 227 | }); 228 | testPlan(cfg, expect => {}); 229 | }); 230 | }); 231 | 232 | describe('dependencies', () => { 233 | 234 | [ 235 | ['optional', 'vizJs'], 236 | ['same optional twice', ['vizJs', 'vizJs']], 237 | ].map(([testName, assetValue]) => { 238 | it('main → ' + testName, () => { 239 | const cfg = makeCfg({ 240 | assets: { omg: assetValue }, 241 | optional: { vizJs }, 242 | }); 243 | testPlan(cfg, expect => { 244 | expect.addOpCopy(new LocalSrc(src, 'vendor/viz.js'), 'viz.js'); 245 | expect.manifest.addPathLocal('vizJs', '/viz.js'); 246 | }); 247 | }); 248 | }); 249 | 250 | it('cycle: self-reference', () => { 251 | const cfg = makeCfg({ 252 | assets: { omg: 'omg' }, 253 | }); 254 | testPlan(cfg, expect => { 255 | expect.addError('Circular dependency on asset: omg'); 256 | }); 257 | }); 258 | 259 | it('cycle: a↔b', () => { 260 | const cfg = makeCfg({ 261 | assets: { a: 'b', b: 'a' }, 262 | }); 263 | testPlan(cfg, expect => { 264 | expect.addError('Circular dependency on asset: a'); 265 | }); 266 | }); 267 | }); 268 | 269 | describe('cdn', () => { 270 | const url = jqueryUrl; 271 | 272 | const test = (def, expectFn) => { 273 | const cfg = makeCfg({ assets: { x: Object.assign({ type: 'cdn', manifest: true }, def) } }); 274 | testPlan(cfg, expectFn); 275 | }; 276 | 277 | const testOk = (def, out) => test(def, expect => expect.manifest.addPathCdn('x', out)); 278 | const testErr = (def, err) => test(def, expect => expect.addError(err)); 279 | 280 | it('integrity specified', () => { 281 | const integrity = TestData.jqueryCdn.integrity; 282 | testOk({ url, integrity }, { url, integrity }); 283 | }); 284 | 285 | it('integrity from file', () => { 286 | testOk( // 287 | { url, integrity: { files: 'image2.svg' } }, // 288 | { url, integrity: image2SvgSha256 }); 289 | }); 290 | 291 | it('integrity from multiple files', () => { 292 | testOk( // 293 | { url, integrity: { files: 'image{1,2}.svg' } }, // 294 | { url, integrity: `${image1SvgSha256} ${image2SvgSha256}` }); 295 | }); 296 | 297 | it('integrity with different algorithm', () => { 298 | testOk( // 299 | { url, integrity: { files: 'image2.svg', algo: 'sha384' } }, // 300 | { url, integrity: image2SvgSha384 }); 301 | }); 302 | 303 | it('integrity with multiple algorithms', () => { 304 | testOk( // 305 | { url, integrity: { files: 'image2.svg', algo: ['sha384', 'sha256'] } }, // 306 | { url, integrity: `${image2SvgSha384} ${image2SvgSha256}` }); 307 | }); 308 | 309 | it('allows no integrity', () => { 310 | testOk( // 311 | { url }, // 312 | { url }); 313 | }); 314 | 315 | it('error when no url', () => { 316 | testErr({ integrity: image2SvgSha256 }, 'Invalid config in x: url is missing.'); 317 | }); 318 | 319 | it('error when no files match', () => { 320 | testErr( // 321 | { url, integrity: { files: 'whatever.js' } }, // 322 | 'x integrity file(s) not found: whatever.js'); 323 | }); 324 | 325 | const url2 = 'https://unpkg.com/react@15.3.1/dist/react.min.js'; 326 | const a = { type: 'cdn', url: url, integrity: image1SvgSha256 }; 327 | const b = { type: 'cdn', url: url2, integrity: image2SvgSha256 }; 328 | const okA = (expect, mName) => { if (mName) expect.manifest.addPathCdn(mName, { url: url, integrity: image1SvgSha256 }) }; 329 | const okB = (expect, mName) => { if (mName) expect.manifest.addPathCdn(mName, { url: url2, integrity: image2SvgSha256 }) }; 330 | testManifestRequiredInArray(a, b, i => i.url, okA, okB); 331 | }); 332 | 333 | describe('multi-feature', () => { 334 | it('example #1', () => { 335 | const cfg = makeCfg({ 336 | assets: { 337 | a: 'b', 338 | m: [svgs, 'n', 'j'], 339 | }, 340 | optional: { 341 | x: { type: 'external', path: 'x' }, // not referenced 342 | b: ['c'], 343 | c: ['d', 'e', 'm'], 344 | d: [vizJsExplicit, 'e', 'k'], 345 | e: 'f', 346 | f: [{ type: 'external', path: 'f' }, 'l'], 347 | n: [{ type: 'external', path: 'n', manifest: 'n' }], 348 | j: { type: 'cdn', url: jqueryUrl, integrity: image1SvgSha256, manifest: true }, 349 | k: { type: 'cdn', url: jqueryUrl + '/k', integrity: image2SvgSha256 }, 350 | l: [{ type: 'external', path: 'l', manifest: 'l' }], 351 | }, 352 | }); 353 | testPlan(cfg, expect => { 354 | addSvgExpectations(expect); 355 | expect.addOpCopy(new LocalSrc(src, 'vendor/viz.js'), 'viz.js'); 356 | expect.manifest.addPathLocal('vizJs', '/viz.js'); 357 | expect.manifest.addPathLocal('n', '/n'); 358 | expect.manifest.addPathLocal('l', '/l'); 359 | expect.manifest.addPathCdn('j', { url: jqueryUrl, integrity: image1SvgSha256 }); 360 | }); 361 | // console.log(Plan.run(cfg)); 362 | }); 363 | }); 364 | 365 | }); 366 | }); 367 | -------------------------------------------------------------------------------- /test/plugins/htmlTest.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const 4 | Assert = require('chai').assert, 5 | CamelCase = require('camelcase'), 6 | FS = require('fs'), 7 | Path = require('path'), 8 | Plan = require('../../dist/plan'), 9 | Plugins = require('../../dist/plugins'), 10 | TestData = require('../data'), 11 | TestUtil = require('../util'), 12 | Utils = require('../../dist/utils'); 13 | 14 | const { src, target, jqueryCdn, bootstrapCssCdn } = TestData; 15 | 16 | const testPage = (cfg, expectedContent, to) => { 17 | const state = Plan.run(cfg); 18 | Assert.deepEqual(state.errors, []); 19 | const norm = i => { 20 | const o = Object.assign({}, i); 21 | o.content = o.content.split("\n") 22 | return o; 23 | } 24 | TestUtil.assertOps(state.ops, op => op.to.path === to, [{ 25 | type: 'write', 26 | to: [target, to], 27 | content: expectedContent, 28 | }], norm); 29 | } 30 | 31 | const makePageTest = n => { 32 | const p = {}; 33 | p.src = `page${n}.html`; 34 | p.prep = cfg => { 35 | const c = TestData.cfg(cfg); 36 | c.output.name = 'out-[basename]'; 37 | if (!c.assets) c.assets = {}; 38 | c.assets.test = { type: 'local', files: p.src }; 39 | if (!c.plugins) c.plugins = [Plugins.Html.replace()]; 40 | return c; 41 | }; 42 | p.content = FS.readFileSync(`${src}/${p.src}`).toString(); 43 | p.test = (cfg, expectedContent) => testPage( 44 | p.prep(cfg), 45 | typeof expectedContent === 'function' ? expectedContent(p.content) : expectedContent, 46 | `out-${p.src}`); 47 | p.testError = (expectedErrors, cfg, cfgMod) => { 48 | const c = p.prep(cfg); 49 | if (cfgMod) cfgMod(c); 50 | const state = Plan.run(c); 51 | Assert.deepEqual(state.errors, Utils.asArray(expectedErrors).map(e => `${p.src}: ${e}`)); 52 | } 53 | return p; 54 | }; 55 | const pageTest = [undefined].concat([1, 2, 3].map(makePageTest)); 56 | 57 | const requireAssetTag = ''; 58 | const requireManifestTag = ''; 59 | 60 | function testPage1(cfg, expectedReplacement, { expectMod = e => e, replace = true } = {}) { 61 | pageTest[1].test(cfg, c => 62 | expectMod(replace ? c.replace(requireAssetTag, expectedReplacement) : c)); 63 | } 64 | 65 | function testPage3(cfg, expectedReplacement) { 66 | pageTest[3].test(cfg, c => c.replace(requireManifestTag, expectedReplacement)); 67 | } 68 | 69 | const choseLocal = o => Object.assign({}, { assets: { chosen: { type: 'local', files: 'hello.js' } } }, o || {}); 70 | 71 | describe('Plugins.Html', () => { 72 | describe('replace', () => { 73 | describe('', () => { 74 | 75 | it('link to JS: local', () => { 76 | const cfg = choseLocal(); 77 | const exp = '' 78 | testPage1(cfg, exp) 79 | }); 80 | 81 | it('link to JS: external', () => { 82 | const cfg = { assets: { chosen: { type: 'external', path: '/thing.js' } } }; 83 | const exp = '' 84 | testPage1(cfg, exp) 85 | }); 86 | 87 | it('link to JS: cdn', () => { 88 | const cfg = { assets: { chosen: jqueryCdn } }; 89 | const exp = ``; 90 | testPage1(cfg, exp) 91 | }); 92 | 93 | it('link to CSS', () => { 94 | const cfg = { assets: { chosen: bootstrapCssCdn } }; 95 | const exp = ``; 96 | testPage1(cfg, exp) 97 | }); 98 | 99 | it('link to Google Fonts CSS', () => { 100 | const url = 'https://fonts.googleapis.com/css?family=Lato:400,700,400italic,700italic&subset=latin' 101 | const cfg = { assets: { chosen: { type: 'cdn', url, as: 'style' } } }; 102 | const exp = ``; 103 | testPage1(cfg, exp) 104 | }); 105 | 106 | it('link without integrity', () => { 107 | const url = 'https://x.com/x.js' 108 | const cfg = { assets: { chosen: { type: 'cdn', url } } }; 109 | const exp = ``; 110 | testPage1(cfg, exp) 111 | }); 112 | 113 | it('loads dependencies in order', () => { 114 | const cfg = { 115 | assets: { 116 | chosen: ['d', { type: 'local', files: 'hello.js' }, 'c'], 117 | }, 118 | optional: { 119 | a: ['z', { type: 'external', path: '/a.css', manifest: 'a_css' }], 120 | b: { type: 'external', path: '/b.js' }, 121 | c: [{ type: 'external', path: '/c.js', manifest: 'c_js' }, 'b', 'z'], 122 | d: ['a'], 123 | z: { type: 'external', path: '/z.js' }, 124 | }, 125 | }; 126 | const exps = [ 127 | '', // chosen -> c -> a -> z 128 | '', // chosen -> a 129 | '', // chosen -> c -> b 130 | '', // chosen -> c 131 | '', // chosen 132 | ]; 133 | testPage1(cfg, exps.join("\n")) 134 | }); 135 | 136 | it('works on "write" ops', () => { 137 | const modStr = s => s.replace(/Page 1/g, 'PAGE ONE!!!'); 138 | const modPlugin = Plugins.Modify.content(/\.html$/, modStr); 139 | const cfg = choseLocal({ plugins: [modPlugin, Plugins.Html.replace()] }); 140 | const exp = '' 141 | testPage1(cfg, exp, { expectMod: modStr }) 142 | }); 143 | 144 | it('error when asset attribute missing', () => { 145 | const modStr = s => s.replace(' asset="chosen"', ''); 146 | const modPlugin = Plugins.Modify.content(/\.html$/, modStr); 147 | const cfg = choseLocal({ plugins: [modPlugin, Plugins.Html.replace()] }); 148 | pageTest[1].testError(" tag needs an 'asset' attribute.", cfg); 149 | }); 150 | 151 | it('error when invalid asset name', () => { 152 | const modStr = s => s.replace('chosen', 'nope'); 153 | const modPlugin = Plugins.Modify.content(/\.html$/, modStr); 154 | const cfg = choseLocal({ plugins: [modPlugin, Plugins.Html.replace()] }); 155 | pageTest[1].testError("Asset referenced in not found: nope", cfg); 156 | }); 157 | 158 | it('follows renames', () => { 159 | const modPlugin = Plugins.Modify.rename(/\.js$/, f => "renamed-" + f); 160 | const cfg = choseLocal({ plugins: [modPlugin, Plugins.Html.replace()] }); 161 | const exp = ''; 162 | testPage1(cfg, exp); 163 | }); 164 | 165 | it('error when file type unrecognised', () => { 166 | const modPlugin = Plugins.Modify.rename(/\.js$/, f => f + ".what"); 167 | const cfg = choseLocal({ plugins: [modPlugin, Plugins.Html.replace()] }); 168 | pageTest[1].testError("Don't know what kind of HTML tag is needed to load: /out-hello.js.what", cfg); 169 | }); 170 | 171 | it('ignores transitive dependencies', () => { 172 | const cfg = { 173 | assets: { 174 | v: { type: 'local', files: 'vendor/v?z.js', transitive: true }, 175 | h: { type: 'local', files: 'hello.js' }, 176 | chosen: ['v', 'h'], 177 | } 178 | }; 179 | const exp = '' 180 | testPage1(cfg, exp) 181 | }); 182 | 183 | describe('', () => { 184 | const choose = o => Object.assign({}, o, { manifest: 'chooseMe' }); 185 | 186 | it('link to JS: local', () => { 187 | const cfg = { assets: { x: choose({ type: 'local', files: 'hello.js' }) } }; 188 | const exp = ''; 189 | testPage3(cfg, exp); 190 | }); 191 | 192 | it('link to JS: external', () => { 193 | const cfg = { assets: { x: choose({ type: 'external', path: '/thing.js' }) } }; 194 | const exp = '' 195 | testPage3(cfg, exp); 196 | }); 197 | 198 | it('link to JS: cdn', () => { 199 | const cfg = { assets: { x: choose(jqueryCdn) } }; 200 | const exp = ``; 201 | testPage3(cfg, exp) 202 | }); 203 | }); 204 | 205 | describe("webtamp: //manifest:", () => { 206 | it('replace with local url', () => { 207 | const cfg = { assets: { x: { type: 'local', files: '*.svg', manifest: CamelCase } } }; 208 | const expect = c => c 209 | .replace('webtamp://manifest/image1Svg', '/out-image1.svg') 210 | .replace('webtamp://manifest/image3Svg', '/out-image3.svg') 211 | .replace('webtamp://manifest/image2Svg', '/out-image2.svg'); 212 | pageTest[2].test(cfg, expect); 213 | }); 214 | it('error when no manifest entry', () => { 215 | const cfg = { assets: { x: { type: 'local', files: '*.svg' } } }; 216 | pageTest[2].testError([1, 3, 2].map(i => `Manifest entry not found: image${i}Svg`), cfg); 217 | }); 218 | }); 219 | }); 220 | }); 221 | 222 | // TODO re-enable 223 | // describe("minify", () => { 224 | // console.log("Plugins.Html: ", Plugins.Html) 225 | // const minify = Plugins.Html.minify({ options: { removeComments: true, collapseWhitespace: true } }); 226 | // const expMin = e => e.replace(/\n\s*|/g, '').replace('sen" /', 'sen"'); 227 | // it("minifies HTML", () => { 228 | // const cfg = { assets: {}, plugins: [minify] }; 229 | // testPage1(cfg, null, { expectMod: expMin, replace: false }); 230 | // }); 231 | // it("minifies after repalcement", () => { 232 | // const cfg = choseLocal({ plugins: [Plugins.Html.replace(), minify] }); 233 | // const exp = '' 234 | // testPage1(cfg, exp, { expectMod: expMin }); 235 | // }); 236 | // }); 237 | }); 238 | -------------------------------------------------------------------------------- /test/plugins/inlineTest.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const 4 | Assert = require('chai').assert, 5 | CamelCase = require('camelcase'), 6 | Path = require('path'), 7 | Plan = require('../../dist/plan'), 8 | Plugins = require('../../dist/plugins'), 9 | TestData = require('../data'), 10 | TestUtil = require('../util'), 11 | LocalSrc = require('../../dist/types').LocalSrc; 12 | 13 | const { src, target } = TestData; 14 | const testPlan = TestUtil.testPlan(); 15 | const svgs123 = { type: 'local', files: '*{1,2,3}.svg', manifest: CamelCase }; 16 | const svg1 = { type: 'local', files: 'image1.svg', manifest: true }; 17 | 18 | describe('Plugins.Inline', () => { 19 | describe('data', () => { 20 | 21 | it('criteria function input', () => { 22 | const seen = []; 23 | const add = o => { 24 | const o2 = Object.assign({}, o); 25 | delete o2.size; 26 | delete o2.stats; 27 | seen.push(o2); 28 | return false; 29 | } 30 | const plugins = [Plugins.Inline.data(add)]; 31 | const cfg = TestData.cfg({ assets: { svgs123 }, plugins }); 32 | Plan.run(cfg); 33 | const expect = [1, 2, 3].map(i => ({ 34 | manifestName: `image${i}Svg`, 35 | src: Path.resolve(src, `image${i}.svg`), 36 | dest: `image${i}.svg`, 37 | })); 38 | Assert.deepEqual(seen.sort(), expect); 39 | }); 40 | 41 | it('size limit', () => { 42 | const plugins = [Plugins.Inline.data(i => i.size() < 1000)]; 43 | const cfg = TestData.cfg({ assets: { svgs123 }, plugins }); 44 | testPlan(cfg, expect => { 45 | expect.addOpCopy(new LocalSrc(src, 'image3.svg'), 'image3.svg'); 46 | expect.manifest.addUrl('image1Svg', 'data:image/svg+xml;base64,aW1hZ2UxCg==') 47 | expect.manifest.addUrl('image2Svg', 'data:image/svg+xml;base64,aW1hZ2UyCg==') 48 | expect.manifest.addPathLocal('image3Svg', '/image3.svg') 49 | }); 50 | }); 51 | 52 | it('custom mimeType', () => { 53 | const plugins = [Plugins.Inline.data(i => 'hello')]; 54 | const cfg = TestData.cfg({ assets: { svg1 }, plugins }); 55 | testPlan(cfg, expect => { 56 | expect.manifest.addUrl('svg1', 'data:hello;base64,aW1hZ2UxCg==') 57 | }); 58 | }); 59 | 60 | it('no mimeType', () => { 61 | const plugins = [Plugins.Inline.data(i => '')]; 62 | const cfg = TestData.cfg({ assets: { svg1 }, plugins }); 63 | testPlan(cfg, expect => { 64 | expect.manifest.addUrl('svg1', 'data:base64,aW1hZ2UxCg==' ) 65 | }); 66 | }); 67 | 68 | it('works on new content after a content modification', () => { 69 | const plugins = [ 70 | Plugins.Modify.content(/\.svg$/, c => "hello"), 71 | Plugins.Inline.data(i => i.size() == 5) 72 | // Plugins.Inline.data(i => {console.log(`size = ${i.size()}`,i); return i.size() == 5}) 73 | ]; 74 | const cfg = TestData.cfg({ assets: { svg1 }, plugins }); 75 | testPlan(cfg, expect => { 76 | expect.manifest.addUrl('svg1', 'data:image/svg+xml;base64,aGVsbG8=') 77 | }); 78 | }); 79 | 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /test/plugins/manifestTest.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const 4 | Assert = require('chai').assert, 5 | CamelCase = require('camelcase'), 6 | Plan = require('../../dist/plan'), 7 | Plugins = require('../../dist/plugins'), 8 | TestData = require('../data'), 9 | TestUtil = require('../util'); 10 | 11 | const { assertManifest } = TestUtil 12 | const { target, vizJs, jqueryCdnM, css1, css2 } = TestData; 13 | 14 | const wwJs = { type: 'external', path: '/ww.js', manifest: true }; 15 | 16 | describe('Plugins.Manifest', () => { 17 | 18 | describe('extractCss', () => { 19 | it("extracts URLs from CSS to make a new manifest entry", () => { 20 | const cfg = TestData.cfg({ 21 | assets: { css1 }, 22 | plugins: [ Plugins.Manifest.extractCss({}) ], 23 | }); 24 | const state = Plan.run(cfg); 25 | assertManifest(state.manifest, {entries: { 26 | css1: { local: '/1.css' }, 27 | css1Urls: { list: [ 28 | 'https://fonts.googleapis.com/css?family=Lato:400,700,400italic,700italic&subset=latin', 29 | 'icons.eot', 30 | 'icons.eot?#iefix', 31 | 'icons.woff2', 32 | 'icons.woff', 33 | 'icons.ttf', 34 | 'icons.svg#icons', 35 | ]}, 36 | }}); 37 | }); 38 | }); 39 | 40 | describe('generate.scala', () => { 41 | 42 | it("generates a Scala manifest", () => { 43 | const cfg = TestData.cfg({ 44 | assets: { 45 | vizJs, 46 | svgs: { type: 'local', files: '*{1,2}.svg', manifest: CamelCase }, 47 | wwJs, 48 | css1, css2, 49 | jquery: jqueryCdnM, 50 | }, 51 | plugins: [ 52 | Plugins.Inline.data(i => /image2/.test(i.dest)), 53 | Plugins.Manifest.extractCss({}), 54 | Plugins.Manifest.generate.scala({ object: "demo.test.Manifest" }), 55 | ], 56 | }); 57 | const filename = "Manifest.scala" 58 | cfg.output.name = "[name]-[hash:16].[ext]"; 59 | const state = Plan.run(cfg); 60 | Assert.deepEqual(state.errors, []); 61 | // console.log(state.ops[4].content) 62 | TestUtil.assertOps(state.ops, op => op.type === 'write' && op.to.path === filename, [{ 63 | type: 'write', 64 | to: [target, filename], 65 | content: ` 66 | package demo.test 67 | 68 | /** Generated by webtamp. */ 69 | object Manifest { 70 | 71 | final case class CDN(href: String, integrity: Option[String]) 72 | 73 | def css1 = "/1-24299309df91abd8.css" 74 | 75 | def css1Urls: List[String] = 76 | "https://fonts.googleapis.com/css?family=Lato:400,700,400italic,700italic&subset=latin" :: 77 | "icons.eot" :: 78 | "icons.eot?#iefix" :: 79 | "icons.woff2" :: 80 | "icons.woff" :: 81 | "icons.ttf" :: 82 | "icons.svg#icons" :: 83 | Nil 84 | 85 | def css2 = "/2-469d3cc8794b9f5a.css" 86 | 87 | def css2Urls: List[String] = 88 | Nil 89 | 90 | def image1Svg = "/image1-03f43b8f2e62bd8d.svg" 91 | 92 | def image2Svg = "data:image/svg+xml;base64,aW1hZ2UyCg==" 93 | 94 | def jquery = CDN( 95 | href = "https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.4/jquery.min.js", 96 | integrity = Some("sha256-BbhdlvQf/xTY9gja0Dq3HiwQF8LaCRTXxZKRutelT44=")) 97 | 98 | def vizJs = "/viz-e4e91995e194dd59.js" 99 | 100 | def wwJs = "/ww.js" 101 | } 102 | `.trim() 103 | }]); 104 | }); 105 | 106 | it("generates an abstract Scala manifest", () => { 107 | const cfg = TestData.cfg({ 108 | assets: { 109 | vizJs, 110 | svgs: { type: 'local', files: '*{1,2}.svg', manifest: CamelCase }, 111 | wwJs, 112 | css1, css2, 113 | jquery: jqueryCdnM, 114 | }, 115 | plugins: [ 116 | Plugins.Inline.data(i => /image2/.test(i.dest)), 117 | Plugins.Manifest.extractCss({}), 118 | Plugins.Manifest.generate.scala({ abstract: true, object: "demo.test.Manifest" }), 119 | ], 120 | }); 121 | const filename = "Manifest.scala" 122 | cfg.output.name = "[name]-[hash:16].[ext]"; 123 | const state = Plan.run(cfg); 124 | Assert.deepEqual(state.errors, []); 125 | // console.log(state.ops[4].content) 126 | TestUtil.assertOps(state.ops, op => op.type === 'write' && op.to.path === filename, [{ 127 | type: 'write', 128 | to: [target, filename], 129 | content: ` 130 | package demo.test 131 | 132 | /** Generated by webtamp. */ 133 | object Manifest { 134 | final case class CDN(href: String, integrity: Option[String]) 135 | } 136 | 137 | abstract class Manifest[+A] { 138 | import Manifest._ 139 | 140 | protected def modify(f: String): A 141 | 142 | final val css1 = modify("/1-24299309df91abd8.css") 143 | 144 | final val css1Urls: List[A] = 145 | modify("https://fonts.googleapis.com/css?family=Lato:400,700,400italic,700italic&subset=latin") :: 146 | modify("icons.eot") :: 147 | modify("icons.eot?#iefix") :: 148 | modify("icons.woff2") :: 149 | modify("icons.woff") :: 150 | modify("icons.ttf") :: 151 | modify("icons.svg#icons") :: 152 | Nil 153 | 154 | final val css2 = modify("/2-469d3cc8794b9f5a.css") 155 | 156 | final val css2Urls: List[A] = 157 | Nil 158 | 159 | final val image1Svg = modify("/image1-03f43b8f2e62bd8d.svg") 160 | 161 | final val image2Svg = modify("data:image/svg+xml;base64,aW1hZ2UyCg==") 162 | 163 | final val jquery = CDN( 164 | href = "https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.4/jquery.min.js", 165 | integrity = Some("sha256-BbhdlvQf/xTY9gja0Dq3HiwQF8LaCRTXxZKRutelT44=")) 166 | 167 | final val vizJs = modify("/viz-e4e91995e194dd59.js") 168 | 169 | final val wwJs = modify("/ww.js") 170 | } 171 | `.trim() 172 | }]); 173 | }); 174 | 175 | it("respects options: outputPath, filename", () => { 176 | const outputPath = "omg" 177 | const filename = "Hi.scala" 178 | const cfg = TestData.cfg({ 179 | assets: { wwJs }, 180 | plugins: [ Plugins.Manifest.generate.scala({ object: "hehe.Hi", outputPath, filename }) ], 181 | }); 182 | const state = Plan.run(cfg); 183 | Assert.deepEqual(state.errors, []); 184 | TestUtil.assertOps(state.ops, _ => true, [{ 185 | type: 'write', 186 | to: [target, `${outputPath}/${filename}`], 187 | content: ` 188 | package hehe 189 | 190 | /** Generated by webtamp. */ 191 | object Hi { 192 | 193 | def wwJs = "/ww.js" 194 | } 195 | `.trim() 196 | }]); 197 | }); 198 | 199 | }); 200 | }); 201 | -------------------------------------------------------------------------------- /test/plugins/modifyTest.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const 4 | Assert = require('chai').assert, 5 | Plan = require('../../dist/plan'), 6 | Plugins = require('../../dist/plugins'), 7 | TestData = require('../data'), 8 | TestUtil = require('../util'); 9 | 10 | const { assertManifest } = TestUtil 11 | const { src, target, jqueryCdnM, jqueryUrlEntry, jqueryManifestEntry } = TestData; 12 | 13 | describe('Plugins.Modify', () => { 14 | 15 | describe('replaceWebtampUrls', () => { 16 | 17 | it("replaces urls in site.webmanifest", () => { 18 | 19 | const cfg = { 20 | src, 21 | output: { dir: target }, 22 | assets: { 23 | image1: { type: 'local', files: 'image1.svg', outputName: 'omg-wow-mah-1-image.svg', manifest: true }, 24 | image2: { type: 'local', files: 'image2.svg', outputName: '[hash:32].[ext]', manifest: true }, 25 | siteman: { type: 'local', files: 'site.webmanifest' }, 26 | }, 27 | plugins: [ 28 | Plugins.Modify.replaceWebtampUrls({ 29 | testFilename: /^site/, 30 | urlQuotes: [`'`, `"`], 31 | }) 32 | ] 33 | } 34 | const state = Plan.run(cfg) 35 | // console.log(JSON.stringify(state.ops, null, 2)) 36 | Assert.deepEqual(state.errors, []); 37 | 38 | const expectedContent = `{ 39 | "name": "", 40 | "short_name": "", 41 | "icons": [ 42 | { 43 | "src": "/omg-wow-mah-1-image.svg", 44 | "sizes": "192x192", 45 | "type": "image/png" 46 | }, 47 | { 48 | "src": "/88ddfd89852406e3916e28a79407d564.svg", 49 | "sizes": "512x512", 50 | "type": "image/png" 51 | } 52 | ], 53 | "theme_color": "#ffffff", 54 | "background_color": "#ffffff", 55 | "display": "standalone" 56 | } 57 | ` 58 | TestUtil.assertWriteOp(state.ops,"site.webmanifest", expectedContent) 59 | }) 60 | }) 61 | 62 | describe('rename', () => { 63 | 64 | const makeCfg = o => 65 | Object.assign({}, { src, target, output: { dir: target, name: 'copy-[basename]', manifest: false } }, o); 66 | 67 | it("affects type: local", () => { 68 | const cfg = makeCfg({ assets: { hello: { type: 'local', files: 'hello.js' } } }) 69 | cfg.plugins = [Plugins.Modify.rename(/^copy-/, f => f.substring(5))]; 70 | const state = Plan.run(cfg); 71 | Assert.deepEqual(state.errors, []); 72 | Assert.deepEqual(state.urls, { hello: [{ url: '/hello.js' }] }); 73 | assertManifest(state.manifest, {entries: {}}); 74 | }); 75 | 76 | it("affects type: local with manifest", () => { 77 | const cfg = makeCfg({ assets: { hello: { type: 'local', files: 'hello.js', manifest: 'wow' } } }) 78 | cfg.plugins = [Plugins.Modify.rename(/^copy-/, f => f.substring(5))]; 79 | const state = Plan.run(cfg); 80 | Assert.deepEqual(state.errors, []); 81 | Assert.deepEqual(state.urls, { hello: [{ url: '/hello.js' }] }); 82 | assertManifest(state.manifest, {entries: { wow: { local: '/hello.js' } }}); 83 | }); 84 | 85 | it("uses specified filename test", () => { 86 | const cfg = makeCfg({ assets: { hello: { type: 'local', files: 'hello.js', manifest: 'wow' } } }) 87 | cfg.plugins = [Plugins.Modify.rename(/nope/, f => "nope")]; 88 | const state = Plan.run(cfg); 89 | Assert.deepEqual(state.errors, []); 90 | Assert.deepEqual(state.urls, { hello: [{ url: '/copy-hello.js' }] }); 91 | assertManifest(state.manifest, {entries: { wow: { local: '/copy-hello.js' } }}); 92 | }); 93 | 94 | it("ignores type: cdn", () => { 95 | const cfg = makeCfg({ assets: { hello: jqueryCdnM } }) 96 | cfg.plugins = [Plugins.Modify.rename(/.js$/, f => f + ".nope")]; 97 | const state = Plan.run(cfg); 98 | Assert.deepEqual(state.errors, []); 99 | Assert.deepEqual(state.urls, { hello: [jqueryUrlEntry] }); 100 | assertManifest(state.manifest, {entries: { hello: jqueryManifestEntry }}); 101 | }); 102 | 103 | it("ignores type: external", () => { 104 | const cfg = makeCfg({ assets: { hello: { type: 'external', path: '/thing.js', manifest: 'wow' } } }) 105 | cfg.plugins = [Plugins.Modify.rename(/.js$/, f => f + ".nope")]; 106 | const state = Plan.run(cfg); 107 | Assert.deepEqual(state.errors, []); 108 | Assert.deepEqual(state.urls, { hello: [{ url: '/thing.js' }] }); 109 | assertManifest(state.manifest, {entries: { wow: { local: '/thing.js' } }}); 110 | }); 111 | 112 | it("ignores transitive assets", () => { 113 | const cfg = makeCfg({ assets: { hello: { type: 'local', files: 'hello.js', transitive: true } } }) 114 | cfg.plugins = [Plugins.Modify.rename(/hello/, _ => "nope")]; 115 | const state = Plan.run(cfg); 116 | Assert.deepEqual(state.errors, []); 117 | Assert.deepEqual(state.urls, { hello: [{ url: '/hello.js', transitive: true }] }); 118 | assertManifest(state.manifest, {entries: {}}); 119 | }); 120 | 121 | }); 122 | }); 123 | -------------------------------------------------------------------------------- /test/util.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const 4 | Assert = require('chai').assert, 5 | Plan = require('../dist/plan'), 6 | State = require('../dist/state').default, 7 | tap = require('../dist/utils').tap, 8 | TestData = require('./data'); 9 | 10 | function assertManifest(actual, expect) { 11 | const a = Object.assign({}, actual) 12 | delete a.state 13 | Assert.deepEqual(a, expect) 14 | } 15 | 16 | const assertState = normaliseState => (actual, addExpectations) => { 17 | const expect = new State(actual.src, actual.target); 18 | addExpectations(expect); 19 | const e = normaliseState(expect); 20 | const a = normaliseState(actual); 21 | Assert.deepEqual(a, e); 22 | return a; 23 | }; 24 | 25 | const identity = a => a; 26 | 27 | const removeManifestState = m => { 28 | const o = Object.assign({}, m) 29 | delete o.manifest.state 30 | return o 31 | }; 32 | 33 | const removeGraph = tap(r => delete r.graph); 34 | 35 | const simplifyOp = ({ 36 | copy: tap(op => { 37 | const f = op.from; 38 | const t = op.to; 39 | op.from = [f.ctx, f.path]; 40 | op.to = [t.ctx, t.path]; 41 | }), 42 | write: tap(op => { 43 | const t = op.to; 44 | delete op.originallyFrom; 45 | op.to = [t.ctx, t.path]; 46 | }), 47 | }); 48 | 49 | const simplifyOpArray = ops => 50 | ops.map(op => (simplifyOp[op.type] || identity)(op)); 51 | 52 | const simplifyTypes = tap(r => { 53 | r.ops = simplifyOpArray(r.ops); 54 | }); 55 | 56 | const defaultStateNormalisation = s => 57 | removeGraph(simplifyTypes(removeManifestState(s.results()))); 58 | 59 | const testPlan = (normaliseState = defaultStateNormalisation) => { 60 | const f = assertState(normaliseState); 61 | return (cfg, addExpectations) => f(Plan.run(cfg), addExpectations); 62 | }; 63 | 64 | const assertOps = (ops, opCriteria, expect, normalise) => { 65 | const normArray = normalise ? ops => ops.map(normalise) : identity; 66 | const actual = simplifyOpArray(normArray(ops.filter(opCriteria))).sort(); 67 | Assert.deepEqual(actual, normArray(expect).sort()); 68 | } 69 | 70 | const assertWriteOp = (ops, toPath, expectedContent, normalise) => { 71 | assertOps(ops, op => op.to.path === toPath, [{ 72 | type: 'write', 73 | to: [TestData.target, toPath], 74 | content: expectedContent, 75 | }], normalise) 76 | } 77 | 78 | module.exports = { 79 | assertManifest, 80 | assertOps, 81 | assertWriteOp, 82 | assertState, 83 | defaultStateNormalisation, 84 | testPlan, 85 | } 86 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "lib": [], /* Specify library files to be included in the compilation. */ 7 | // "checkJs": true, /* Report errors in .js files. */ 8 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 9 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 10 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 11 | // "outFile": "./", /* Concatenate and emit output to single file. */ 12 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 13 | // "composite": true, /* Enable project compilation */ 14 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 15 | // "removeComments": true, /* Do not emit comments to output. */ 16 | // "noEmit": true, /* Do not emit outputs. */ 17 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 18 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 19 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 20 | 21 | /* Strict Type-Checking Options */ 22 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 23 | // "strictNullChecks": true, /* Enable strict null checks. */ 24 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 25 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 26 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 27 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 28 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 29 | 30 | /* Additional Checks */ 31 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 32 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 33 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 34 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 35 | 36 | /* Module Resolution Options */ 37 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 38 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 39 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 40 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 41 | // "types": [], /* Type declaration files to be included in compilation. */ 42 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 43 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 44 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 45 | 46 | /* Source Map Options */ 47 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 48 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 49 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 50 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 51 | 52 | /* Experimental Options */ 53 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 54 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 55 | 56 | 57 | "allowJs": true, /* Allow javascript files to be compiled. */ 58 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 59 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 60 | "forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */ 61 | "incremental": true, /* Enable incremental compilation */ 62 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 63 | "outDir": "./dist", /* Redirect output structure to the directory. */ 64 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 65 | "strict": true, /* Enable all strict type-checking options. */ 66 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 67 | "typeRoots": [ /* List of folders to include type definitions from. */ 68 | "./src/@types", 69 | "node_modules/@types" 70 | ], 71 | }, 72 | "include": ["./src/**/*"] 73 | } 74 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@types/clean-css@*": 6 | version "4.2.2" 7 | resolved "https://registry.yarnpkg.com/@types/clean-css/-/clean-css-4.2.2.tgz#99fd79f6939c2b325938a1c569712e07dd97d709" 8 | integrity sha512-xiTJn3bmDh1lA8c6iVJs4ZhHw+pcmxXlJQXOB6G1oULaak8rmarIeFKI4aTJ7849dEhaO612wgIualZfbxTJwA== 9 | dependencies: 10 | "@types/node" "*" 11 | 12 | "@types/color-name@^1.1.1": 13 | version "1.1.1" 14 | resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" 15 | integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== 16 | 17 | "@types/fs-extra@^9.0.1": 18 | version "9.0.1" 19 | resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-9.0.1.tgz#91c8fc4c51f6d5dbe44c2ca9ab09310bd00c7918" 20 | integrity sha512-B42Sxuaz09MhC3DDeW5kubRcQ5by4iuVQ0cRRWM2lggLzAa/KVom0Aft/208NgMvNQQZ86s5rVcqDdn/SH0/mg== 21 | dependencies: 22 | "@types/node" "*" 23 | 24 | "@types/glob@^7.1.3": 25 | version "7.1.3" 26 | resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.3.tgz#e6ba80f36b7daad2c685acd9266382e68985c183" 27 | integrity sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w== 28 | dependencies: 29 | "@types/minimatch" "*" 30 | "@types/node" "*" 31 | 32 | "@types/html-minifier@^4.0.0": 33 | version "4.0.0" 34 | resolved "https://registry.yarnpkg.com/@types/html-minifier/-/html-minifier-4.0.0.tgz#2065cb9944f2d1b241146707c6935aa7b947d279" 35 | integrity sha512-eFnGhrKmjWBlnSGNtunetE3UU2Tc/LUl92htFslSSTmpp9EKHQVcYQadCyYfnzUEFB5G/3wLWo/USQS/mEPKrA== 36 | dependencies: 37 | "@types/clean-css" "*" 38 | "@types/relateurl" "*" 39 | "@types/uglify-js" "*" 40 | 41 | "@types/mime-types@^2.1.0": 42 | version "2.1.0" 43 | resolved "https://registry.yarnpkg.com/@types/mime-types/-/mime-types-2.1.0.tgz#9ca52cda363f699c69466c2a6ccdaad913ea7a73" 44 | integrity sha1-nKUs2jY/aZxpRmwqbM2q2RPqenM= 45 | 46 | "@types/minimatch@*": 47 | version "3.0.3" 48 | resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" 49 | integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== 50 | 51 | "@types/node@*": 52 | version "14.6.0" 53 | resolved "https://registry.yarnpkg.com/@types/node/-/node-14.6.0.tgz#7d4411bf5157339337d7cff864d9ff45f177b499" 54 | integrity sha512-mikldZQitV94akrc4sCcSjtJfsTKt4p+e/s0AGscVA6XArQ9kFclP+ZiYUMnq987rc6QlYxXv/EivqlfSLxpKA== 55 | 56 | "@types/node@^14.6.0": 57 | version "14.6.2" 58 | resolved "https://registry.yarnpkg.com/@types/node/-/node-14.6.2.tgz#264b44c5a28dfa80198fc2f7b6d3c8a054b9491f" 59 | integrity sha512-onlIwbaeqvZyniGPfdw/TEhKIh79pz66L1q06WUQqJLnAb6wbjvOtepLYTGHTqzdXgBYIE3ZdmqHDGsRsbBz7A== 60 | 61 | "@types/relateurl@*": 62 | version "0.2.28" 63 | resolved "https://registry.yarnpkg.com/@types/relateurl/-/relateurl-0.2.28.tgz#6bda7db8653fa62643f5ee69e9f69c11a392e3a6" 64 | integrity sha1-a9p9uGU/piZD9e5p6facEaOS46Y= 65 | 66 | "@types/sprintf-js@^1.1.2": 67 | version "1.1.2" 68 | resolved "https://registry.yarnpkg.com/@types/sprintf-js/-/sprintf-js-1.1.2.tgz#a4fcb84c7344f39f70dc4eec0e1e7f10a48597a3" 69 | integrity sha512-hkgzYF+qnIl8uTO8rmUSVSfQ8BIfMXC4yJAF4n8BE758YsKBZvFC4NumnAegj7KmylP0liEZNpb9RRGFMbFejA== 70 | 71 | "@types/uglify-js@*": 72 | version "3.9.3" 73 | resolved "https://registry.yarnpkg.com/@types/uglify-js/-/uglify-js-3.9.3.tgz#d94ed608e295bc5424c9600e6b8565407b6b4b6b" 74 | integrity sha512-KswB5C7Kwduwjj04Ykz+AjvPcfgv/37Za24O2EDzYNbwyzOo8+ydtvzUfZ5UMguiVu29Gx44l1A6VsPPcmYu9w== 75 | dependencies: 76 | source-map "^0.6.1" 77 | 78 | ansi-colors@4.1.1: 79 | version "4.1.1" 80 | resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" 81 | integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== 82 | 83 | ansi-regex@^3.0.0: 84 | version "3.0.0" 85 | resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" 86 | integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= 87 | 88 | ansi-regex@^4.1.0: 89 | version "4.1.0" 90 | resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" 91 | integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== 92 | 93 | ansi-styles@^3.2.0: 94 | version "3.2.1" 95 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" 96 | integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== 97 | dependencies: 98 | color-convert "^1.9.0" 99 | 100 | ansi-styles@^4.1.0: 101 | version "4.2.1" 102 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.2.1.tgz#90ae75c424d008d2624c5bf29ead3177ebfcf359" 103 | integrity sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA== 104 | dependencies: 105 | "@types/color-name" "^1.1.1" 106 | color-convert "^2.0.1" 107 | 108 | anymatch@~3.1.1: 109 | version "3.1.1" 110 | resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142" 111 | integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg== 112 | dependencies: 113 | normalize-path "^3.0.0" 114 | picomatch "^2.0.4" 115 | 116 | argparse@^1.0.7: 117 | version "1.0.10" 118 | resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" 119 | integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== 120 | dependencies: 121 | sprintf-js "~1.0.2" 122 | 123 | array.prototype.map@^1.0.1: 124 | version "1.0.2" 125 | resolved "https://registry.yarnpkg.com/array.prototype.map/-/array.prototype.map-1.0.2.tgz#9a4159f416458a23e9483078de1106b2ef68f8ec" 126 | integrity sha512-Az3OYxgsa1g7xDYp86l0nnN4bcmuEITGe1rbdEBVkrqkzMgDcbdQ2R7r41pNzti+4NMces3H8gMmuioZUilLgw== 127 | dependencies: 128 | define-properties "^1.1.3" 129 | es-abstract "^1.17.0-next.1" 130 | es-array-method-boxes-properly "^1.0.0" 131 | is-string "^1.0.4" 132 | 133 | assertion-error@^1.1.0: 134 | version "1.1.0" 135 | resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" 136 | integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== 137 | 138 | at-least-node@^1.0.0: 139 | version "1.0.0" 140 | resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" 141 | integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== 142 | 143 | balanced-match@^1.0.0: 144 | version "1.0.0" 145 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" 146 | integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= 147 | 148 | binary-extensions@^2.0.0: 149 | version "2.1.0" 150 | resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.1.0.tgz#30fa40c9e7fe07dbc895678cd287024dea241dd9" 151 | integrity sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ== 152 | 153 | brace-expansion@^1.1.7: 154 | version "1.1.11" 155 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" 156 | integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== 157 | dependencies: 158 | balanced-match "^1.0.0" 159 | concat-map "0.0.1" 160 | 161 | braces@~3.0.2: 162 | version "3.0.2" 163 | resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" 164 | integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== 165 | dependencies: 166 | fill-range "^7.0.1" 167 | 168 | browser-stdout@1.3.1: 169 | version "1.3.1" 170 | resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" 171 | integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== 172 | 173 | camel-case@^3.0.0: 174 | version "3.0.0" 175 | resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-3.0.0.tgz#ca3c3688a4e9cf3a4cda777dc4dcbc713249cf73" 176 | integrity sha1-yjw2iKTpzzpM2nd9xNy8cTJJz3M= 177 | dependencies: 178 | no-case "^2.2.0" 179 | upper-case "^1.1.1" 180 | 181 | camelcase@^5.0.0, camelcase@^5.3.1: 182 | version "5.3.1" 183 | resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" 184 | integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== 185 | 186 | camelcase@^6.0.0: 187 | version "6.0.0" 188 | resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.0.0.tgz#5259f7c30e35e278f1bdc2a4d91230b37cad981e" 189 | integrity sha512-8KMDF1Vz2gzOq54ONPJS65IvTUaB1cHJ2DMM7MbPmLZljDH1qpzzLsWdiN9pHh6qvkRVDTi/07+eNGch/oLU4w== 190 | 191 | chai@^4.0.2: 192 | version "4.2.0" 193 | resolved "https://registry.yarnpkg.com/chai/-/chai-4.2.0.tgz#760aa72cf20e3795e84b12877ce0e83737aa29e5" 194 | integrity sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw== 195 | dependencies: 196 | assertion-error "^1.1.0" 197 | check-error "^1.0.2" 198 | deep-eql "^3.0.1" 199 | get-func-name "^2.0.0" 200 | pathval "^1.1.0" 201 | type-detect "^4.0.5" 202 | 203 | chalk@^4.0.0: 204 | version "4.1.0" 205 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.0.tgz#4e14870a618d9e2edd97dd8345fd9d9dc315646a" 206 | integrity sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A== 207 | dependencies: 208 | ansi-styles "^4.1.0" 209 | supports-color "^7.1.0" 210 | 211 | check-error@^1.0.2: 212 | version "1.0.2" 213 | resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" 214 | integrity sha1-V00xLt2Iu13YkS6Sht1sCu1KrII= 215 | 216 | chokidar@3.4.2: 217 | version "3.4.2" 218 | resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.2.tgz#38dc8e658dec3809741eb3ef7bb0a47fe424232d" 219 | integrity sha512-IZHaDeBeI+sZJRX7lGcXsdzgvZqKv6sECqsbErJA4mHWfpRrD8B97kSFN4cQz6nGBGiuFia1MKR4d6c1o8Cv7A== 220 | dependencies: 221 | anymatch "~3.1.1" 222 | braces "~3.0.2" 223 | glob-parent "~5.1.0" 224 | is-binary-path "~2.1.0" 225 | is-glob "~4.0.1" 226 | normalize-path "~3.0.0" 227 | readdirp "~3.4.0" 228 | optionalDependencies: 229 | fsevents "~2.1.2" 230 | 231 | clean-css@^4.2.1: 232 | version "4.2.3" 233 | resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.2.3.tgz#507b5de7d97b48ee53d84adb0160ff6216380f78" 234 | integrity sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA== 235 | dependencies: 236 | source-map "~0.6.0" 237 | 238 | cliui@^5.0.0: 239 | version "5.0.0" 240 | resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" 241 | integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA== 242 | dependencies: 243 | string-width "^3.1.0" 244 | strip-ansi "^5.2.0" 245 | wrap-ansi "^5.1.0" 246 | 247 | color-convert@^1.9.0: 248 | version "1.9.3" 249 | resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" 250 | integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== 251 | dependencies: 252 | color-name "1.1.3" 253 | 254 | color-convert@^2.0.1: 255 | version "2.0.1" 256 | resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" 257 | integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== 258 | dependencies: 259 | color-name "~1.1.4" 260 | 261 | color-name@1.1.3: 262 | version "1.1.3" 263 | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" 264 | integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= 265 | 266 | color-name@~1.1.4: 267 | version "1.1.4" 268 | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" 269 | integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== 270 | 271 | commander@^2.19.0: 272 | version "2.20.3" 273 | resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" 274 | integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== 275 | 276 | commander@^2.9.0: 277 | version "2.11.0" 278 | resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563" 279 | 280 | concat-map@0.0.1: 281 | version "0.0.1" 282 | resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" 283 | integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= 284 | 285 | debug@4.1.1: 286 | version "4.1.1" 287 | resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" 288 | integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== 289 | dependencies: 290 | ms "^2.1.1" 291 | 292 | decamelize@^1.2.0: 293 | version "1.2.0" 294 | resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" 295 | integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= 296 | 297 | deep-eql@^3.0.1: 298 | version "3.0.1" 299 | resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-3.0.1.tgz#dfc9404400ad1c8fe023e7da1df1c147c4b444df" 300 | integrity sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw== 301 | dependencies: 302 | type-detect "^4.0.0" 303 | 304 | define-properties@^1.1.2, define-properties@^1.1.3: 305 | version "1.1.3" 306 | resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" 307 | integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== 308 | dependencies: 309 | object-keys "^1.0.12" 310 | 311 | diff@4.0.2: 312 | version "4.0.2" 313 | resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" 314 | integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== 315 | 316 | dom-serializer@0: 317 | version "0.1.0" 318 | resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82" 319 | dependencies: 320 | domelementtype "~1.1.1" 321 | entities "~1.1.1" 322 | 323 | domelementtype@1: 324 | version "1.3.0" 325 | resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.0.tgz#b17aed82e8ab59e52dd9c19b1756e0fc187204c2" 326 | 327 | domelementtype@^1.3.1: 328 | version "1.3.1" 329 | resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" 330 | integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== 331 | 332 | domelementtype@~1.1.1: 333 | version "1.1.3" 334 | resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.1.3.tgz#bd28773e2642881aec51544924299c5cd822185b" 335 | 336 | domhandler@^2.3.0: 337 | version "2.4.1" 338 | resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.1.tgz#892e47000a99be55bbf3774ffea0561d8879c259" 339 | dependencies: 340 | domelementtype "1" 341 | 342 | domutils@^1.5.1: 343 | version "1.6.2" 344 | resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.6.2.tgz#1958cc0b4c9426e9ed367fb1c8e854891b0fa3ff" 345 | dependencies: 346 | dom-serializer "0" 347 | domelementtype "1" 348 | 349 | emoji-regex@^7.0.1: 350 | version "7.0.3" 351 | resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" 352 | integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== 353 | 354 | entities@^1.1.1, entities@~1.1.1: 355 | version "1.1.1" 356 | resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0" 357 | 358 | es-abstract@^1.17.0-next.1, es-abstract@^1.17.4, es-abstract@^1.17.5: 359 | version "1.17.6" 360 | resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.6.tgz#9142071707857b2cacc7b89ecb670316c3e2d52a" 361 | integrity sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw== 362 | dependencies: 363 | es-to-primitive "^1.2.1" 364 | function-bind "^1.1.1" 365 | has "^1.0.3" 366 | has-symbols "^1.0.1" 367 | is-callable "^1.2.0" 368 | is-regex "^1.1.0" 369 | object-inspect "^1.7.0" 370 | object-keys "^1.1.1" 371 | object.assign "^4.1.0" 372 | string.prototype.trimend "^1.0.1" 373 | string.prototype.trimstart "^1.0.1" 374 | 375 | es-array-method-boxes-properly@^1.0.0: 376 | version "1.0.0" 377 | resolved "https://registry.yarnpkg.com/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz#873f3e84418de4ee19c5be752990b2e44718d09e" 378 | integrity sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA== 379 | 380 | es-get-iterator@^1.0.2: 381 | version "1.1.0" 382 | resolved "https://registry.yarnpkg.com/es-get-iterator/-/es-get-iterator-1.1.0.tgz#bb98ad9d6d63b31aacdc8f89d5d0ee57bcb5b4c8" 383 | integrity sha512-UfrmHuWQlNMTs35e1ypnvikg6jCz3SK8v8ImvmDsh36fCVUR1MqoFDiyn0/k52C8NqO3YsO8Oe0azeesNuqSsQ== 384 | dependencies: 385 | es-abstract "^1.17.4" 386 | has-symbols "^1.0.1" 387 | is-arguments "^1.0.4" 388 | is-map "^2.0.1" 389 | is-set "^2.0.1" 390 | is-string "^1.0.5" 391 | isarray "^2.0.5" 392 | 393 | es-to-primitive@^1.2.1: 394 | version "1.2.1" 395 | resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" 396 | integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== 397 | dependencies: 398 | is-callable "^1.1.4" 399 | is-date-object "^1.0.1" 400 | is-symbol "^1.0.2" 401 | 402 | escape-string-regexp@4.0.0: 403 | version "4.0.0" 404 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" 405 | integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== 406 | 407 | esprima@^4.0.0: 408 | version "4.0.1" 409 | resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" 410 | integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== 411 | 412 | fill-range@^7.0.1: 413 | version "7.0.1" 414 | resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" 415 | integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== 416 | dependencies: 417 | to-regex-range "^5.0.1" 418 | 419 | find-up@5.0.0: 420 | version "5.0.0" 421 | resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" 422 | integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== 423 | dependencies: 424 | locate-path "^6.0.0" 425 | path-exists "^4.0.0" 426 | 427 | find-up@^3.0.0: 428 | version "3.0.0" 429 | resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" 430 | integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== 431 | dependencies: 432 | locate-path "^3.0.0" 433 | 434 | flat@^4.1.0: 435 | version "4.1.0" 436 | resolved "https://registry.yarnpkg.com/flat/-/flat-4.1.0.tgz#090bec8b05e39cba309747f1d588f04dbaf98db2" 437 | integrity sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw== 438 | dependencies: 439 | is-buffer "~2.0.3" 440 | 441 | fs-extra@^9.0.1: 442 | version "9.0.1" 443 | resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.0.1.tgz#910da0062437ba4c39fedd863f1675ccfefcb9fc" 444 | integrity sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ== 445 | dependencies: 446 | at-least-node "^1.0.0" 447 | graceful-fs "^4.2.0" 448 | jsonfile "^6.0.1" 449 | universalify "^1.0.0" 450 | 451 | fs.realpath@^1.0.0: 452 | version "1.0.0" 453 | resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" 454 | integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= 455 | 456 | fsevents@~2.1.2: 457 | version "2.1.3" 458 | resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" 459 | integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== 460 | 461 | function-bind@^1.1.1: 462 | version "1.1.1" 463 | resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" 464 | integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== 465 | 466 | get-caller-file@^2.0.1: 467 | version "2.0.5" 468 | resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" 469 | integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== 470 | 471 | get-func-name@^2.0.0: 472 | version "2.0.0" 473 | resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41" 474 | integrity sha1-6td0q+5y4gQJQzoGY2YCPdaIekE= 475 | 476 | glob-parent@~5.1.0: 477 | version "5.1.1" 478 | resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" 479 | integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ== 480 | dependencies: 481 | is-glob "^4.0.1" 482 | 483 | glob@7.1.6, glob@^7.1.1: 484 | version "7.1.6" 485 | resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" 486 | integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== 487 | dependencies: 488 | fs.realpath "^1.0.0" 489 | inflight "^1.0.4" 490 | inherits "2" 491 | minimatch "^3.0.4" 492 | once "^1.3.0" 493 | path-is-absolute "^1.0.0" 494 | 495 | graceful-fs@^4.1.6: 496 | version "4.1.11" 497 | resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" 498 | 499 | graceful-fs@^4.2.0: 500 | version "4.2.4" 501 | resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" 502 | integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== 503 | 504 | growl@1.10.5: 505 | version "1.10.5" 506 | resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" 507 | integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== 508 | 509 | has-flag@^4.0.0: 510 | version "4.0.0" 511 | resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" 512 | integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== 513 | 514 | has-symbols@^1.0.0, has-symbols@^1.0.1: 515 | version "1.0.1" 516 | resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" 517 | integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg== 518 | 519 | has@^1.0.3: 520 | version "1.0.3" 521 | resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" 522 | integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== 523 | dependencies: 524 | function-bind "^1.1.1" 525 | 526 | he@1.2.0, he@^1.2.0: 527 | version "1.2.0" 528 | resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" 529 | integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== 530 | 531 | html-minifier@^4.0.0: 532 | version "4.0.0" 533 | resolved "https://registry.yarnpkg.com/html-minifier/-/html-minifier-4.0.0.tgz#cca9aad8bce1175e02e17a8c33e46d8988889f56" 534 | integrity sha512-aoGxanpFPLg7MkIl/DDFYtb0iWz7jMFGqFhvEDZga6/4QTjneiD8I/NXL1x5aaoCp7FSIT6h/OhykDdPsbtMig== 535 | dependencies: 536 | camel-case "^3.0.0" 537 | clean-css "^4.2.1" 538 | commander "^2.19.0" 539 | he "^1.2.0" 540 | param-case "^2.1.1" 541 | relateurl "^0.2.7" 542 | uglify-js "^3.5.1" 543 | 544 | htmlparser2@^3.8.3: 545 | version "3.10.1" 546 | resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f" 547 | integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ== 548 | dependencies: 549 | domelementtype "^1.3.1" 550 | domhandler "^2.3.0" 551 | domutils "^1.5.1" 552 | entities "^1.1.1" 553 | inherits "^2.0.1" 554 | readable-stream "^3.1.1" 555 | 556 | inflight@^1.0.4: 557 | version "1.0.6" 558 | resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" 559 | integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= 560 | dependencies: 561 | once "^1.3.0" 562 | wrappy "1" 563 | 564 | inherits@2, inherits@^2.0.3: 565 | version "2.0.4" 566 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" 567 | integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== 568 | 569 | inherits@^2.0.1: 570 | version "2.0.3" 571 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" 572 | 573 | is-arguments@^1.0.4: 574 | version "1.0.4" 575 | resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.0.4.tgz#3faf966c7cba0ff437fb31f6250082fcf0448cf3" 576 | integrity sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA== 577 | 578 | is-binary-path@~2.1.0: 579 | version "2.1.0" 580 | resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" 581 | integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== 582 | dependencies: 583 | binary-extensions "^2.0.0" 584 | 585 | is-buffer@~2.0.3: 586 | version "2.0.4" 587 | resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.4.tgz#3e572f23c8411a5cfd9557c849e3665e0b290623" 588 | integrity sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A== 589 | 590 | is-callable@^1.1.4, is-callable@^1.2.0: 591 | version "1.2.0" 592 | resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.0.tgz#83336560b54a38e35e3a2df7afd0454d691468bb" 593 | integrity sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw== 594 | 595 | is-date-object@^1.0.1: 596 | version "1.0.2" 597 | resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e" 598 | integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g== 599 | 600 | is-extglob@^2.1.1: 601 | version "2.1.1" 602 | resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" 603 | integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= 604 | 605 | is-fullwidth-code-point@^2.0.0: 606 | version "2.0.0" 607 | resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" 608 | integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= 609 | 610 | is-glob@^4.0.1, is-glob@~4.0.1: 611 | version "4.0.1" 612 | resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" 613 | integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== 614 | dependencies: 615 | is-extglob "^2.1.1" 616 | 617 | is-map@^2.0.1: 618 | version "2.0.1" 619 | resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.1.tgz#520dafc4307bb8ebc33b813de5ce7c9400d644a1" 620 | integrity sha512-T/S49scO8plUiAOA2DBTBG3JHpn1yiw0kRp6dgiZ0v2/6twi5eiB0rHtHFH9ZIrvlWc6+4O+m4zg5+Z833aXgw== 621 | 622 | is-number@^7.0.0: 623 | version "7.0.0" 624 | resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" 625 | integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== 626 | 627 | is-plain-obj@^1.1.0: 628 | version "1.1.0" 629 | resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" 630 | integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4= 631 | 632 | is-regex@^1.1.0: 633 | version "1.1.1" 634 | resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.1.tgz#c6f98aacc546f6cec5468a07b7b153ab564a57b9" 635 | integrity sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg== 636 | dependencies: 637 | has-symbols "^1.0.1" 638 | 639 | is-set@^2.0.1: 640 | version "2.0.1" 641 | resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.1.tgz#d1604afdab1724986d30091575f54945da7e5f43" 642 | integrity sha512-eJEzOtVyenDs1TMzSQ3kU3K+E0GUS9sno+F0OBT97xsgcJsF9nXMBtkT9/kut5JEpM7oL7X/0qxR17K3mcwIAA== 643 | 644 | is-string@^1.0.4, is-string@^1.0.5: 645 | version "1.0.5" 646 | resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.5.tgz#40493ed198ef3ff477b8c7f92f644ec82a5cd3a6" 647 | integrity sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ== 648 | 649 | is-symbol@^1.0.2: 650 | version "1.0.3" 651 | resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937" 652 | integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ== 653 | dependencies: 654 | has-symbols "^1.0.1" 655 | 656 | isarray@1.0.0: 657 | version "1.0.0" 658 | resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" 659 | integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= 660 | 661 | isarray@^2.0.5: 662 | version "2.0.5" 663 | resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" 664 | integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== 665 | 666 | isexe@^2.0.0: 667 | version "2.0.0" 668 | resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" 669 | integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= 670 | 671 | isobject@^2.1.0: 672 | version "2.1.0" 673 | resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" 674 | integrity sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk= 675 | dependencies: 676 | isarray "1.0.0" 677 | 678 | iterate-iterator@^1.0.1: 679 | version "1.0.1" 680 | resolved "https://registry.yarnpkg.com/iterate-iterator/-/iterate-iterator-1.0.1.tgz#1693a768c1ddd79c969051459453f082fe82e9f6" 681 | integrity sha512-3Q6tudGN05kbkDQDI4CqjaBf4qf85w6W6GnuZDtUVYwKgtC1q8yxYX7CZed7N+tLzQqS6roujWvszf13T+n9aw== 682 | 683 | iterate-value@^1.0.0: 684 | version "1.0.2" 685 | resolved "https://registry.yarnpkg.com/iterate-value/-/iterate-value-1.0.2.tgz#935115bd37d006a52046535ebc8d07e9c9337f57" 686 | integrity sha512-A6fMAio4D2ot2r/TYzr4yUWrmwNdsN5xL7+HUiyACE4DXm+q8HtPcnFTp+NnW3k4N05tZ7FVYFFb2CR13NxyHQ== 687 | dependencies: 688 | es-get-iterator "^1.0.2" 689 | iterate-iterator "^1.0.1" 690 | 691 | js-yaml@3.14.0: 692 | version "3.14.0" 693 | resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482" 694 | integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A== 695 | dependencies: 696 | argparse "^1.0.7" 697 | esprima "^4.0.0" 698 | 699 | jsonfile@^6.0.1: 700 | version "6.0.1" 701 | resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.0.1.tgz#98966cba214378c8c84b82e085907b40bf614179" 702 | integrity sha512-jR2b5v7d2vIOust+w3wtFKZIfpC2pnRmFAhAC/BuweZFQR8qZzxH1OyrQ10HmdVYiXWkYUqPVsz91cG7EL2FBg== 703 | dependencies: 704 | universalify "^1.0.0" 705 | optionalDependencies: 706 | graceful-fs "^4.1.6" 707 | 708 | locate-path@^3.0.0: 709 | version "3.0.0" 710 | resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" 711 | integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== 712 | dependencies: 713 | p-locate "^3.0.0" 714 | path-exists "^3.0.0" 715 | 716 | locate-path@^6.0.0: 717 | version "6.0.0" 718 | resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" 719 | integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== 720 | dependencies: 721 | p-locate "^5.0.0" 722 | 723 | log-symbols@4.0.0: 724 | version "4.0.0" 725 | resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.0.0.tgz#69b3cc46d20f448eccdb75ea1fa733d9e821c920" 726 | integrity sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA== 727 | dependencies: 728 | chalk "^4.0.0" 729 | 730 | lower-case@^1.1.1: 731 | version "1.1.4" 732 | resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac" 733 | integrity sha1-miyr0bno4K6ZOkv31YdcOcQujqw= 734 | 735 | mime-db@1.44.0: 736 | version "1.44.0" 737 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" 738 | integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== 739 | 740 | mime-types@^2.1.15: 741 | version "2.1.27" 742 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" 743 | integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== 744 | dependencies: 745 | mime-db "1.44.0" 746 | 747 | minimatch@3.0.4, minimatch@^3.0.4: 748 | version "3.0.4" 749 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" 750 | integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== 751 | dependencies: 752 | brace-expansion "^1.1.7" 753 | 754 | mocha@^8.1.1: 755 | version "8.1.3" 756 | resolved "https://registry.yarnpkg.com/mocha/-/mocha-8.1.3.tgz#5e93f873e35dfdd69617ea75f9c68c2ca61c2ac5" 757 | integrity sha512-ZbaYib4hT4PpF4bdSO2DohooKXIn4lDeiYqB+vTmCdr6l2woW0b6H3pf5x4sM5nwQMru9RvjjHYWVGltR50ZBw== 758 | dependencies: 759 | ansi-colors "4.1.1" 760 | browser-stdout "1.3.1" 761 | chokidar "3.4.2" 762 | debug "4.1.1" 763 | diff "4.0.2" 764 | escape-string-regexp "4.0.0" 765 | find-up "5.0.0" 766 | glob "7.1.6" 767 | growl "1.10.5" 768 | he "1.2.0" 769 | js-yaml "3.14.0" 770 | log-symbols "4.0.0" 771 | minimatch "3.0.4" 772 | ms "2.1.2" 773 | object.assign "4.1.0" 774 | promise.allsettled "1.0.2" 775 | serialize-javascript "4.0.0" 776 | strip-json-comments "3.0.1" 777 | supports-color "7.1.0" 778 | which "2.0.2" 779 | wide-align "1.1.3" 780 | workerpool "6.0.0" 781 | yargs "13.3.2" 782 | yargs-parser "13.1.2" 783 | yargs-unparser "1.6.1" 784 | 785 | ms@2.1.2, ms@^2.1.1: 786 | version "2.1.2" 787 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" 788 | integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== 789 | 790 | no-case@^2.2.0: 791 | version "2.3.2" 792 | resolved "https://registry.yarnpkg.com/no-case/-/no-case-2.3.2.tgz#60b813396be39b3f1288a4c1ed5d1e7d28b464ac" 793 | integrity sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ== 794 | dependencies: 795 | lower-case "^1.1.1" 796 | 797 | normalize-path@^3.0.0, normalize-path@~3.0.0: 798 | version "3.0.0" 799 | resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" 800 | integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== 801 | 802 | object-inspect@^1.7.0: 803 | version "1.8.0" 804 | resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.8.0.tgz#df807e5ecf53a609cc6bfe93eac3cc7be5b3a9d0" 805 | integrity sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA== 806 | 807 | object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1: 808 | version "1.1.1" 809 | resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" 810 | integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== 811 | 812 | object.assign@4.1.0, object.assign@^4.1.0: 813 | version "4.1.0" 814 | resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da" 815 | integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w== 816 | dependencies: 817 | define-properties "^1.1.2" 818 | function-bind "^1.1.1" 819 | has-symbols "^1.0.0" 820 | object-keys "^1.0.11" 821 | 822 | once@^1.3.0: 823 | version "1.4.0" 824 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 825 | integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= 826 | dependencies: 827 | wrappy "1" 828 | 829 | p-limit@^2.0.0: 830 | version "2.3.0" 831 | resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" 832 | integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== 833 | dependencies: 834 | p-try "^2.0.0" 835 | 836 | p-limit@^3.0.2: 837 | version "3.0.2" 838 | resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.0.2.tgz#1664e010af3cadc681baafd3e2a437be7b0fb5fe" 839 | integrity sha512-iwqZSOoWIW+Ew4kAGUlN16J4M7OB3ysMLSZtnhmqx7njIHFPlxWBX8xo3lVTyFVq6mI/lL9qt2IsN1sHwaxJkg== 840 | dependencies: 841 | p-try "^2.0.0" 842 | 843 | p-locate@^3.0.0: 844 | version "3.0.0" 845 | resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" 846 | integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== 847 | dependencies: 848 | p-limit "^2.0.0" 849 | 850 | p-locate@^5.0.0: 851 | version "5.0.0" 852 | resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" 853 | integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== 854 | dependencies: 855 | p-limit "^3.0.2" 856 | 857 | p-try@^2.0.0: 858 | version "2.2.0" 859 | resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" 860 | integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== 861 | 862 | param-case@^2.1.1: 863 | version "2.1.1" 864 | resolved "https://registry.yarnpkg.com/param-case/-/param-case-2.1.1.tgz#df94fd8cf6531ecf75e6bef9a0858fbc72be2247" 865 | integrity sha1-35T9jPZTHs915r75oIWPvHK+Ikc= 866 | dependencies: 867 | no-case "^2.2.0" 868 | 869 | path-exists@^3.0.0: 870 | version "3.0.0" 871 | resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" 872 | integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= 873 | 874 | path-exists@^4.0.0: 875 | version "4.0.0" 876 | resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" 877 | integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== 878 | 879 | path-is-absolute@^1.0.0: 880 | version "1.0.1" 881 | resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" 882 | integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= 883 | 884 | pathval@^1.1.0: 885 | version "1.1.0" 886 | resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.0.tgz#b942e6d4bde653005ef6b71361def8727d0645e0" 887 | integrity sha1-uULm1L3mUwBe9rcTYd74cn0GReA= 888 | 889 | picomatch@^2.0.4, picomatch@^2.2.1: 890 | version "2.2.2" 891 | resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" 892 | integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== 893 | 894 | posthtml-parser@^0.2.0: 895 | version "0.2.1" 896 | resolved "https://registry.yarnpkg.com/posthtml-parser/-/posthtml-parser-0.2.1.tgz#35d530de386740c2ba24ff2eb2faf39ccdf271dd" 897 | integrity sha1-NdUw3jhnQMK6JP8usvrznM3ycd0= 898 | dependencies: 899 | htmlparser2 "^3.8.3" 900 | isobject "^2.1.0" 901 | 902 | posthtml-render@^1.0.5: 903 | version "1.2.3" 904 | resolved "https://registry.yarnpkg.com/posthtml-render/-/posthtml-render-1.2.3.tgz#da1cf7ba4efb42cfe9c077f4f41669745de99b6d" 905 | integrity sha512-rGGayND//VwTlsYKNqdILsA7U/XP0WJa6SMcdAEoqc2WRM5QExplGg/h9qbTuHz7mc2PvaXU+6iNxItvr5aHMg== 906 | 907 | posthtml@^0.9.2: 908 | version "0.9.2" 909 | resolved "https://registry.yarnpkg.com/posthtml/-/posthtml-0.9.2.tgz#f4c06db9f67b61fd17c4e256e7e3d9515bf726fd" 910 | integrity sha1-9MBtufZ7Yf0XxOJW5+PZUVv3Jv0= 911 | dependencies: 912 | posthtml-parser "^0.2.0" 913 | posthtml-render "^1.0.5" 914 | 915 | promise.allsettled@1.0.2: 916 | version "1.0.2" 917 | resolved "https://registry.yarnpkg.com/promise.allsettled/-/promise.allsettled-1.0.2.tgz#d66f78fbb600e83e863d893e98b3d4376a9c47c9" 918 | integrity sha512-UpcYW5S1RaNKT6pd+s9jp9K9rlQge1UXKskec0j6Mmuq7UJCvlS2J2/s/yuPN8ehftf9HXMxWlKiPbGGUzpoRg== 919 | dependencies: 920 | array.prototype.map "^1.0.1" 921 | define-properties "^1.1.3" 922 | es-abstract "^1.17.0-next.1" 923 | function-bind "^1.1.1" 924 | iterate-value "^1.0.0" 925 | 926 | randombytes@^2.1.0: 927 | version "2.1.0" 928 | resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" 929 | integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== 930 | dependencies: 931 | safe-buffer "^5.1.0" 932 | 933 | readable-stream@^3.1.1: 934 | version "3.6.0" 935 | resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" 936 | integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== 937 | dependencies: 938 | inherits "^2.0.3" 939 | string_decoder "^1.1.1" 940 | util-deprecate "^1.0.1" 941 | 942 | readdirp@~3.4.0: 943 | version "3.4.0" 944 | resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.4.0.tgz#9fdccdf9e9155805449221ac645e8303ab5b9ada" 945 | integrity sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ== 946 | dependencies: 947 | picomatch "^2.2.1" 948 | 949 | relateurl@^0.2.7: 950 | version "0.2.7" 951 | resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9" 952 | integrity sha1-VNvzd+UUQKypCkzSdGANP/LYiKk= 953 | 954 | require-directory@^2.1.1: 955 | version "2.1.1" 956 | resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" 957 | integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= 958 | 959 | require-main-filename@^2.0.0: 960 | version "2.0.0" 961 | resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" 962 | integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== 963 | 964 | safe-buffer@^5.1.0, safe-buffer@~5.2.0: 965 | version "5.2.1" 966 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" 967 | integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== 968 | 969 | serialize-javascript@4.0.0: 970 | version "4.0.0" 971 | resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa" 972 | integrity sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw== 973 | dependencies: 974 | randombytes "^2.1.0" 975 | 976 | set-blocking@^2.0.0: 977 | version "2.0.0" 978 | resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" 979 | integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= 980 | 981 | source-map@^0.6.1, source-map@~0.6.0: 982 | version "0.6.1" 983 | resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" 984 | integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== 985 | 986 | sprintf-js@^1.0.3: 987 | version "1.1.2" 988 | resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.2.tgz#da1765262bf8c0f571749f2ad6c26300207ae673" 989 | integrity sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug== 990 | 991 | sprintf-js@~1.0.2: 992 | version "1.0.3" 993 | resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" 994 | integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= 995 | 996 | "string-width@^1.0.2 || 2": 997 | version "2.1.1" 998 | resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" 999 | integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== 1000 | dependencies: 1001 | is-fullwidth-code-point "^2.0.0" 1002 | strip-ansi "^4.0.0" 1003 | 1004 | string-width@^3.0.0, string-width@^3.1.0: 1005 | version "3.1.0" 1006 | resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" 1007 | integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== 1008 | dependencies: 1009 | emoji-regex "^7.0.1" 1010 | is-fullwidth-code-point "^2.0.0" 1011 | strip-ansi "^5.1.0" 1012 | 1013 | string.prototype.trimend@^1.0.1: 1014 | version "1.0.1" 1015 | resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz#85812a6b847ac002270f5808146064c995fb6913" 1016 | integrity sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g== 1017 | dependencies: 1018 | define-properties "^1.1.3" 1019 | es-abstract "^1.17.5" 1020 | 1021 | string.prototype.trimstart@^1.0.1: 1022 | version "1.0.1" 1023 | resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz#14af6d9f34b053f7cfc89b72f8f2ee14b9039a54" 1024 | integrity sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw== 1025 | dependencies: 1026 | define-properties "^1.1.3" 1027 | es-abstract "^1.17.5" 1028 | 1029 | string_decoder@^1.1.1: 1030 | version "1.3.0" 1031 | resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" 1032 | integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== 1033 | dependencies: 1034 | safe-buffer "~5.2.0" 1035 | 1036 | strip-ansi@^4.0.0: 1037 | version "4.0.0" 1038 | resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" 1039 | integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= 1040 | dependencies: 1041 | ansi-regex "^3.0.0" 1042 | 1043 | strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: 1044 | version "5.2.0" 1045 | resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" 1046 | integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== 1047 | dependencies: 1048 | ansi-regex "^4.1.0" 1049 | 1050 | strip-json-comments@3.0.1: 1051 | version "3.0.1" 1052 | resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.0.1.tgz#85713975a91fb87bf1b305cca77395e40d2a64a7" 1053 | integrity sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw== 1054 | 1055 | supports-color@7.1.0: 1056 | version "7.1.0" 1057 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.1.0.tgz#68e32591df73e25ad1c4b49108a2ec507962bfd1" 1058 | integrity sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g== 1059 | dependencies: 1060 | has-flag "^4.0.0" 1061 | 1062 | supports-color@^7.1.0: 1063 | version "7.2.0" 1064 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" 1065 | integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== 1066 | dependencies: 1067 | has-flag "^4.0.0" 1068 | 1069 | to-regex-range@^5.0.1: 1070 | version "5.0.1" 1071 | resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" 1072 | integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== 1073 | dependencies: 1074 | is-number "^7.0.0" 1075 | 1076 | type-detect@^4.0.0, type-detect@^4.0.5: 1077 | version "4.0.8" 1078 | resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" 1079 | integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== 1080 | 1081 | typescript@^4.0.2: 1082 | version "4.0.2" 1083 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.2.tgz#7ea7c88777c723c681e33bf7988be5d008d05ac2" 1084 | integrity sha512-e4ERvRV2wb+rRZ/IQeb3jm2VxBsirQLpQhdxplZ2MEzGvDkkMmPglecnNDfSUBivMjP93vRbngYYDQqQ/78bcQ== 1085 | 1086 | uglify-js@^3.5.1: 1087 | version "3.10.1" 1088 | resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.10.1.tgz#dd14767eb7150de97f2573a5ff210db14fffe4ad" 1089 | integrity sha512-RjxApKkrPJB6kjJxQS3iZlf///REXWYxYJxO/MpmlQzVkDWVI3PSnCBWezMecmTU/TRkNxrl8bmsfFQCp+LO+Q== 1090 | 1091 | universalify@^1.0.0: 1092 | version "1.0.0" 1093 | resolved "https://registry.yarnpkg.com/universalify/-/universalify-1.0.0.tgz#b61a1da173e8435b2fe3c67d29b9adf8594bd16d" 1094 | integrity sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug== 1095 | 1096 | upper-case@^1.1.1: 1097 | version "1.1.3" 1098 | resolved "https://registry.yarnpkg.com/upper-case/-/upper-case-1.1.3.tgz#f6b4501c2ec4cdd26ba78be7222961de77621598" 1099 | 1100 | util-deprecate@^1.0.1: 1101 | version "1.0.2" 1102 | resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" 1103 | integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= 1104 | 1105 | which-module@^2.0.0: 1106 | version "2.0.0" 1107 | resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" 1108 | integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= 1109 | 1110 | which@2.0.2: 1111 | version "2.0.2" 1112 | resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" 1113 | integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== 1114 | dependencies: 1115 | isexe "^2.0.0" 1116 | 1117 | wide-align@1.1.3: 1118 | version "1.1.3" 1119 | resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" 1120 | integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== 1121 | dependencies: 1122 | string-width "^1.0.2 || 2" 1123 | 1124 | workerpool@6.0.0: 1125 | version "6.0.0" 1126 | resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.0.0.tgz#85aad67fa1a2c8ef9386a1b43539900f61d03d58" 1127 | integrity sha512-fU2OcNA/GVAJLLyKUoHkAgIhKb0JoCpSjLC/G2vYKxUjVmQwGbRVeoPJ1a8U4pnVofz4AQV5Y/NEw8oKqxEBtA== 1128 | 1129 | wrap-ansi@^5.1.0: 1130 | version "5.1.0" 1131 | resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" 1132 | integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q== 1133 | dependencies: 1134 | ansi-styles "^3.2.0" 1135 | string-width "^3.0.0" 1136 | strip-ansi "^5.0.0" 1137 | 1138 | wrappy@1: 1139 | version "1.0.2" 1140 | resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 1141 | integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= 1142 | 1143 | y18n@^4.0.0: 1144 | version "4.0.0" 1145 | resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" 1146 | integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== 1147 | 1148 | yargs-parser@13.1.2, yargs-parser@^13.1.2: 1149 | version "13.1.2" 1150 | resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38" 1151 | integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg== 1152 | dependencies: 1153 | camelcase "^5.0.0" 1154 | decamelize "^1.2.0" 1155 | 1156 | yargs-parser@^15.0.1: 1157 | version "15.0.1" 1158 | resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-15.0.1.tgz#54786af40b820dcb2fb8025b11b4d659d76323b3" 1159 | integrity sha512-0OAMV2mAZQrs3FkNpDQcBk1x5HXb8X4twADss4S0Iuk+2dGnLOE/fRHrsYm542GduMveyA77OF4wrNJuanRCWw== 1160 | dependencies: 1161 | camelcase "^5.0.0" 1162 | decamelize "^1.2.0" 1163 | 1164 | yargs-unparser@1.6.1: 1165 | version "1.6.1" 1166 | resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-1.6.1.tgz#bd4b0ee05b4c94d058929c32cb09e3fce71d3c5f" 1167 | integrity sha512-qZV14lK9MWsGCmcr7u5oXGH0dbGqZAIxTDrWXZDo5zUr6b6iUmelNKO6x6R1dQT24AH3LgRxJpr8meWy2unolA== 1168 | dependencies: 1169 | camelcase "^5.3.1" 1170 | decamelize "^1.2.0" 1171 | flat "^4.1.0" 1172 | is-plain-obj "^1.1.0" 1173 | yargs "^14.2.3" 1174 | 1175 | yargs@13.3.2: 1176 | version "13.3.2" 1177 | resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd" 1178 | integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw== 1179 | dependencies: 1180 | cliui "^5.0.0" 1181 | find-up "^3.0.0" 1182 | get-caller-file "^2.0.1" 1183 | require-directory "^2.1.1" 1184 | require-main-filename "^2.0.0" 1185 | set-blocking "^2.0.0" 1186 | string-width "^3.0.0" 1187 | which-module "^2.0.0" 1188 | y18n "^4.0.0" 1189 | yargs-parser "^13.1.2" 1190 | 1191 | yargs@^14.2.3: 1192 | version "14.2.3" 1193 | resolved "https://registry.yarnpkg.com/yargs/-/yargs-14.2.3.tgz#1a1c3edced1afb2a2fea33604bc6d1d8d688a414" 1194 | integrity sha512-ZbotRWhF+lkjijC/VhmOT9wSgyBQ7+zr13+YLkhfsSiTriYsMzkTUFP18pFhWwBeMa5gUc1MzbhrO6/VB7c9Xg== 1195 | dependencies: 1196 | cliui "^5.0.0" 1197 | decamelize "^1.2.0" 1198 | find-up "^3.0.0" 1199 | get-caller-file "^2.0.1" 1200 | require-directory "^2.1.1" 1201 | require-main-filename "^2.0.0" 1202 | set-blocking "^2.0.0" 1203 | string-width "^3.0.0" 1204 | which-module "^2.0.0" 1205 | y18n "^4.0.0" 1206 | yargs-parser "^15.0.1" 1207 | --------------------------------------------------------------------------------