├── .gitignore ├── .vscode └── launch.json ├── README.V1.md ├── README.md ├── package.json ├── spec ├── end_to_end.spec.js ├── fixtures │ ├── a-file │ ├── app.css │ ├── assets │ │ ├── foo.css │ │ ├── foo.js │ │ ├── foo.min.css │ │ └── foo.min.js │ ├── entry.js │ └── node_modules │ │ ├── bad-package │ │ └── package.json │ │ ├── bootstrap │ │ ├── dist │ │ │ └── css │ │ │ │ └── bootstrap.min.css │ │ └── package.json │ │ ├── fake-a-package │ │ ├── fake-a-bundle.js │ │ ├── fake-a-entry.js │ │ └── package.json │ │ ├── fake-b-package │ │ ├── fake-b-bundle.js │ │ ├── fake-b-entry.js │ │ └── package.json │ │ ├── fake-c-package │ │ ├── fake-c-bundle.js │ │ ├── fake-c-entry.js │ │ └── package.json │ │ ├── fake-other-package │ │ ├── fake-other-bundle.js │ │ └── package.json │ │ ├── no-version │ │ └── package.json │ │ └── the-package │ │ └── package.json ├── option_validation.spec.js └── support │ └── jasmine.json ├── src └── index.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | **.DS_store 2 | node_modules 3 | .idea 4 | dist 5 | !spec/fixtures/node_modules 6 | !spec/fixtures/node_modules/**/dist -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Yarn Test", 11 | "program": "${workspaceFolder}/node_modules/.bin/jasmine" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /README.V1.md: -------------------------------------------------------------------------------- 1 | Deploy Assets extension for the HTML Webpack Plugin 2 | ======================================== 3 | 4 | Enhances [html-webpack-plugin](https://github.com/ampedandwired/html-webpack-plugin) 5 | functionality by allowing you to specify js or css assets from node_modules to be copied and included. 6 | 7 | ----- 8 | 9 | This is the **old version** of this plugin. Please update to the `2.x` [version](https://github.com/jharris4/html-webpack-deploy-plugin/blob/master/README.md) if possible. 10 | 11 | ----- 12 | 13 | Installation 14 | ------------ 15 | You must be running webpack on node 0.12.x or higher 16 | 17 | Install the plugin with npm: 18 | ```shell 19 | $ npm install --save-dev html-webpack-deploy-assets-plugin 20 | ``` 21 | 22 | (Note that this plugin was **renamed** in version `2.x`) 23 | 24 | Options 25 | ------- 26 | The available options are: 27 | 28 | - `packagePath`: `string` 29 | 30 | The path to installed packages, relative to the current directory. Default is `node_modules`. 31 | 32 | - `append`: `boolean` 33 | 34 | Specifies whether the assets will be appended (`true`) or prepended (`false`) to the list of assets in the html file. Default is `false`. 35 | 36 | - `publicPath`: `boolean` or `string` 37 | 38 | Specifying whether the assets should be prepended with webpack's public path or a custom publicPath (`string`). 39 | 40 | A value of `false` may be used to disable prefixing with webpack's publicPath, or a value like `myPublicPath/` may be used to prefix all assets with the given string. Default is `true`. 41 | 42 | - `outputPath`: `string` 43 | 44 | A directory name that will be created for each of the deployed assets. 45 | 46 | Instances of `[name]` will be replaced with the package name. 47 | Instances of `[version]` will be replaced with the package version. 48 | 49 | Default is `[name]-[version]`. 50 | 51 | - `packages`: `object` 52 | 53 | Specifies the definition of the assets from installed packages to be deployed. Defaults is `{}`. 54 | 55 | The keys/properties of the packages option must be the name of an installed package, and the definition must be 56 | an object with the following properties: 57 | 58 | - 'outputPath': `string` 59 | 60 | Allows the global `outputPath` to be overriden on a per-package basis. Default is the global value. 61 | 62 | - `assets`: `object` 63 | 64 | Specifies files or directories to be copied from the package's directory. 65 | 66 | The keys/properies are the asset to be copied, and the values are the target asset location within webpack's output directory. 67 | 68 | These are used as the from & to properties for the internal usage of the [copy-webpack-plugin](https://github.com/kevlened/copy-webpack-plugin) 69 | 70 | - `entries`: `array` 71 | 72 | Specifies files to be included in the html file. 73 | 74 | The file paths should be relative to webpack's output directory. 75 | 76 | - `assets`: `object` 77 | 78 | Specifies the definition of the local assets to be deployed. Defaults is `{}`. 79 | 80 | The keys/properies are the asset to be copied, and the values are the target asset location within webpack's output directory. 81 | 82 | - `links`: `array` 83 | 84 | Specifies the definition of the links to be deployed. Defaults is `[]`. 85 | 86 | The objects in the links are of the shape: 87 | 88 | ```javascript 89 | { 90 | href: "path/to/asset", // required - must be a string 91 | rel: "icon", // required - must be a string 92 | sizes: '16x16', // example of optional extra attribute 93 | anyOtherAttribute: 'value' 94 | } 95 | ``` 96 | 97 | For which the following would be injected into the html header: 98 | 99 | ```html 100 | 101 | 102 | 103 | ``` 104 | 105 | 106 | Example 107 | ------- 108 | Deploying bootstrap css and fonts and an assets directory from local files: 109 | 110 | ```javascript 111 | plugins: [ 112 | new HtmlWebpackPlugin(), 113 | new HtmlWebpackDeployAssetsPlugin({ 114 | "packages": { 115 | "bootstrap": { 116 | "assets": { 117 | "dist/css": "css/", 118 | "dist/fonts": "fonts/" 119 | }, 120 | "entries": [ 121 | "css/bootstrap.min.css", 122 | "css/bootstrap-theme.min.css" 123 | ] 124 | } 125 | }, 126 | "assets": { 127 | "src/assets": "assets/" 128 | } 129 | "link": [ 130 | { 131 | "href": "/assets/icon.png", 132 | "rel": "icon" 133 | } 134 | ] 135 | }) 136 | ] 137 | ``` 138 | 139 | This will generate a `dist/index.html` with your webpack bundled output **and** the following: 140 | 141 | ```html 142 | 143 | 144 | 145 | 146 | Webpack App 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | ``` 157 | 158 | Note that additionally, the contents of the following directories will be copied: 159 | 160 | `node_modules/bootstrap/dist/css` -> `dist/bootstrap-3.3.7/css` 161 | `node_modules/bootstrap/dist/fonts` -> `dist/bootstrap-3.3.7/fonts` 162 | `src/assets` -> `dist/assets` 163 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Deployment extension for the HTML Webpack Plugin 2 | ======================================== 3 | 4 | Enhances [html-webpack-plugin](https://github.com/jantimon/html-webpack-plugin) allowing you to **copy** `assets` or `node_modules package assets` into your webpack build and **inject** them as tags into your `html`. 5 | 6 | Built on top of the [html-webpack-tags](https://github.com/jharris4/html-webpack-tags-plugin) and [copy-webpack](https://github.com/webpack-contrib/copy-webpack-plugin) plugins. 7 | 8 | Installation 9 | ------------ 10 | 11 | ```shell 12 | $ npm install --save-dev html-webpack-deploy-plugin 13 | ``` 14 | 15 | - You must be running **Node 10.0** along with **Webpack 5.0** or higher for version `3.x` of this plugin. 16 | 17 | - You must be running **Node 8.6** or higher for version `2.x` of this plugin. 18 | 19 | - This plugin was **renamed** from `html-webpack-deploy-assets-plugin` to `html-webpack-deploy-plugin` in version `2.x`. 20 | 21 | - For use with the `Node < 8.6` please use version `1.x` (old README [here](https://github.com/jharris4/html-webpack-deploy-plugin/blob/master/README.V1.md)) 22 | 23 | --- 24 | 25 | Integration 26 | ----------- 27 | 28 | The **`chunksSortMode`** option of [html-webpack-plugin](https://github.com/jantimon/html-webpack-plugin) `3.x` has a **default** value of `auto`. 29 | 30 | This option is known to cause issues with `code splitting` or `lazy loaded bundles` ([#981](https://github.com/jantimon/html-webpack-plugin/issues/981)). 31 | 32 | When using this plugin with version `3.x` you should set this **`chunksSortMode`** to **'none'**, like this: 33 | 34 | ```js 35 | new HtmlWebpackPlugin({ chunksSortMode: 'none' }) 36 | ``` 37 | 38 | Configuration 39 | ------------- 40 | 41 | The plugin configuration is specified by an options **object** passed to its constructor. 42 | 43 | This object has an **`assets`** option for copying & injecting local files, and a **`packages`** option for copying & injecting packages from your local `node_modules` directory. 44 | 45 | ```js 46 | new HtmlWebpackDeployPlugin({ 47 | assets: { 48 | copy: [{ from: 'a', to: 'b' }], 49 | links: [ 50 | 'b/my.css', 51 | { 52 | hash: false, 53 | publicPath: false, 54 | path: 'b/my.png', 55 | attributes: { 56 | rel: 'icon' 57 | } 58 | } 59 | ], 60 | scripts: 'b/my.js' 61 | }, 62 | packages: { 63 | 'bootstrap': { 64 | copy: [{ from: 'dist/bootstrap.min.css', to: 'bootstrap.min.css'}], 65 | links: { 66 | useCdn: true, 67 | path: 'bootstrap.min.css', 68 | cdnPath: 'dist/bootstrap.min.css' 69 | }, 70 | }, 71 | 'react': { 72 | copy: [{ from: 'umd', to: '' }], 73 | scripts: { 74 | variableName: 'React', 75 | path: 'react.production.min.js', 76 | cdnPath: 'umd/react.production.min.js', 77 | } 78 | }, 79 | 'react-dom': { 80 | copy: [{ from: 'umd', to: '' }], 81 | scripts: { 82 | variableName: 'ReactDOM', 83 | path: 'react-dom.production.min.js', 84 | cdnPath: 'umd/react-dom.production.min.js', 85 | }, 86 | useCdn: false 87 | } 88 | }, 89 | useCdn: true, 90 | getCdnPath: (packageName, packageVersion, packagePath) => `https://unpkg.com/${packageName}@${packageVersion}/${packagePath}` 91 | }); 92 | ``` 93 | 94 | All options for this plugin are **validated** as soon as the plugin is instantiated. 95 | 96 | Options are **inherited** by inner levels of the config and will be overridden if specified in a level. 97 | 98 | The available options are described below, grouped by at what level in the plugin config they may be used. 99 | 100 | #### Root Options 101 | 102 | These options are only available at the root level of the plugin config. 103 | 104 | |Name|Type|Default|Description| 105 | |:--:|:--:|:-----:|:----------| 106 | |**`assets`**|`{Object}`|`undefined`|The local assets to copy into the webpack output directory and inject into the template html file| 107 | |**`packages`**|`{Object}`|`undefined`|The `node_modules` packages to copy into the webpack output directory and inject into the template html file| 108 | |**`useAssetsPath`**|`{Boolean}`|`true`|Whether or not to prefix all assets with the `assetsPath`| 109 | |**`addAssetsPath`**|`{Function}`|`see below`|The function to call to get the output path for assets when copying and injecting them| 110 | |**`assetsPath`**|`{Boolean\|String\|Function}`|`undefined`|Shortcut for specifying both **`useAssetsPath`** and **`addAssetsPath`** at the same time| 111 | |**`usePackagesPath`**|`{Boolean}`|`true`|Whether or not to prefix all packages with the `packagesPath`| 112 | |**`addPackagesPath`**|`{Function}`|`see below`|The function to call to get the output path for `packages` when copying and injecting them| 113 | |**`packagesPath`**|`{Boolean\|String\|Function}`|`undefined`|Shortcut for specifying both **`usePackagesPath`** and **`addPackagesPath`** at the same time| 114 | |**`getPackagePath`**|`{Function}`|`see below`|The function to call to get the output path for a `package` & `version` when copying and injecting it| 115 | |**`findNodeModulesPath`**|`{Function}`|`see below`|The function to call to find the `node_modules` directory where packages to be deployed are installed so their `version` can be read from their `package.json` file. The default is to search upwards in the current working directory| 116 | |**`files`**|`{Array}`|`[]`|If specified this plugin will only inject tags into the html-webpack-plugin instances that are injecting into these files (uses [minimatch](https://github.com/isaacs/minimatch))| 117 | |**`prependExternals`**|`{Boolean}`|`true`|Whether to default **`append`** to **false** for any ` 234 | 235 | ``` 236 | 237 | --- 238 | 239 | The **`packages`** option can be used to specify package assets that should be copied to the webpack output directory and injected into the `index.html` as tags. 240 | 241 | This option requires an object with any of the `copy`, `links`, or `scripts` properties. 242 | 243 | The settings for these are based on the [copy-webpack-plugin](https://github.com/webpack-contrib/copy-webpack-plugin) and the [html-webpack-tags-plugin](https://github.com/jharris4/html-webpack-tags-plugin) 244 | 245 | For example, to copy some assets from `bootstrap` to webpack, and insert a `` and ` 279 | 280 | ``` 281 | 282 | 283 | --- 284 | 285 | Examples 286 | ------- 287 | Deploying `Bootstrap` css and fonts and an assets directory from local files: 288 | 289 | ```javascript 290 | plugins: [ 291 | new HtmlWebpackPlugin(), 292 | new HtmlWebpackDeployAssetsPlugin({ 293 | packages: { 294 | 'bootstrap': { 295 | copy: [ 296 | { from: 'dist/css', to: 'css/' }, 297 | { from: 'dist/fonts', to: 'fonts/' } 298 | ], 299 | links: [ 300 | 'css/bootstrap.min.css', 301 | 'css/bootstrap-theme.min.css' 302 | ] 303 | } 304 | }, 305 | assets: { 306 | copy: [{ from: 'src/assets', to: 'assets/' }], 307 | links: { 308 | path: '/assets/icon.png', 309 | attributes: { 310 | rel:'icon' 311 | } 312 | } 313 | } 314 | }) 315 | ] 316 | ``` 317 | 318 | This will generate a `index.html` something like: 319 | 320 | ```html 321 | 322 | 323 | 324 | 325 | Webpack App 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | ``` 336 | 337 | Note that additionally, the contents of the following directories will be copied: 338 | 339 | `node_modules/bootstrap/dist/css` -> `dist/bootstrap-3.3.7/css` 340 | `node_modules/bootstrap/dist/fonts` -> `dist/bootstrap-3.3.7/fonts` 341 | `src/assets` -> `dist/assets` 342 | 343 | --- 344 | 345 | Deploying `React` from a `CDN`: 346 | 347 | ```javascript 348 | plugins: [ 349 | new HtmlWebpackPlugin(), 350 | new HtmlWebpackDeployAssetsPlugin({ 351 | packages: { 352 | 'react': { 353 | copy: [{ from: 'umd', to: '' }], 354 | scripts: { 355 | variableName: 'React', 356 | path: 'react.production.min.js', 357 | cdnPath: 'umd/react.production.min.js', 358 | } 359 | }, 360 | 'react-dom': { 361 | copy: [{ from: 'umd', to: '' }], 362 | scripts: { 363 | variableName: 'ReactDOM', 364 | path: 'react-dom.production.min.js', 365 | cdnPath: 'umd/react-dom.production.min.js', 366 | } 367 | } 368 | } 369 | useCdn: true, 370 | getCdnPath: (packageName, packageVersion, packagePath) => `https://unpkg.com/${packageName}@${packageVersion}/${packagePath}` 371 | }) 372 | ] 373 | ``` 374 | 375 | This will generate a `index.html` with your webpack bundled output **and** the following: 376 | 377 | ```html 378 | 379 | 380 | 381 | 382 | Webpack App 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | ``` 393 | 394 | --- 395 | 396 | Deploying `React` from `Local UMD Bundles`: 397 | 398 | ```javascript 399 | plugins: [ 400 | new HtmlWebpackPlugin(), 401 | new HtmlWebpackDeployAssetsPlugin({ 402 | packages: { 403 | 'react': { 404 | copy: [{ from: 'umd', to: '' }], 405 | scripts: { 406 | variableName: 'React', 407 | path: 'react.production.min.js', 408 | devPath: 'react.development.js' 409 | } 410 | }, 411 | 'react-dom': { 412 | copy: [{ from: 'umd', to: '' }], 413 | scripts: { 414 | variableName: 'ReactDOM', 415 | path: 'react-dom.production.min.js', 416 | devPath: 'react-dom.development.js' 417 | } 418 | } 419 | } 420 | }) 421 | ] 422 | ``` 423 | 424 | This copies `react` and `react-dom` into webpack's output directory, versioning the directory automatically based on their installed version. They can now be referenced from the tag paths in the html. 425 | 426 | Webpack is instructed that `react` and `react-dom` are **external** so they are **no longer bundled** by webpack. 427 | 428 | The generated `index.html` looks like: 429 | 430 | ```html 431 | 432 | 433 | 434 | 435 | Webpack App 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | ``` 446 | 447 | Or in **development** mode because the **`devPath`** option was specified, the generated `index.html` looks like: 448 | 449 | ```html 450 | 451 | 452 | 453 | 454 | Webpack App 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | ``` 465 | 466 | --- 467 | 468 | ### Non CDN Options 469 | 470 | When doing custom deployment without a CDN it can be useful to configure the directories that all assets and packages are copied to and served from. 471 | 472 | The following examples show some useful settings for such situations. 473 | 474 | --- 475 | 476 | Disabling the grouping of assets into the `assets` directory: 477 | 478 | ```js 479 | new HtmlWebpackDeployAssetsPlugin({ useAssetsPath: false }); 480 | ``` 481 | 482 | ```html 483 | 484 | 485 | ``` 486 | 487 | becomes: 488 | 489 | ```html 490 | 491 | 492 | ``` 493 | 494 | --- 495 | 496 | Custom grouping of assets into a `configurable` directory: 497 | 498 | ```js 499 | new HtmlWebpackDeployAssetsPlugin({ assetsPath: 'my-assets-path' }); 500 | ``` 501 | 502 | ```html 503 | 504 | 505 | ``` 506 | 507 | becomes: 508 | 509 | ```html 510 | 511 | 512 | ``` 513 | 514 | --- 515 | 516 | Disabling the grouping of packages into the `packages` directory: 517 | 518 | ```js 519 | new HtmlWebpackDeployAssetsPlugin({ usePackagesPath: false }); 520 | ``` 521 | 522 | ```html 523 | 524 | 525 | ``` 526 | 527 | becomes: 528 | 529 | ```html 530 | 531 | 532 | ``` 533 | 534 | --- 535 | 536 | Custom grouping of packages into a `configurable` directory: 537 | 538 | ```js 539 | new HtmlWebpackDeployAssetsPlugin({ packagesPath: 'my-packages-path' }); 540 | ``` 541 | 542 | ```html 543 | 544 | 545 | ``` 546 | 547 | becomes: 548 | 549 | ```html 550 | 551 | 552 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "html-webpack-deploy-plugin", 3 | "version": "3.0.0", 4 | "description": "Add the ability to deploy assets from local files or node_modules packages", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/jharris4/html-webpack-deploy-plugin.git" 8 | }, 9 | "keywords": [ 10 | "webpack", 11 | "html", 12 | "deploy", 13 | "assets", 14 | "packages" 15 | ], 16 | "author": "jharris4", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/jharris4/html-webpack-deploy-plugin/issues" 20 | }, 21 | "homepage": "https://github.com/jharris4/html-webpack-deploy-plugin#readme", 22 | "engines": { 23 | "node": ">=10" 24 | }, 25 | "main": "src/index.js", 26 | "files": [ 27 | "src/index.js" 28 | ], 29 | "peerDependencies": { 30 | "html-webpack-plugin": "^5.0.0", 31 | "webpack": "^5.20.1" 32 | }, 33 | "dependencies": { 34 | "copy-webpack-plugin": "^7.0.0", 35 | "find-up": "^5.0.0", 36 | "html-webpack-tags-plugin": "^3.0.0" 37 | }, 38 | "devDependencies": { 39 | "bootstrap": "4.6.0", 40 | "bulma": "0.9.2", 41 | "cheerio": "1.0.0-rc.5", 42 | "css-loader": "^5.0.1", 43 | "dir-compare": "^3.0.0", 44 | "find-up": "^5.0.0", 45 | "html-webpack-plugin": "^5.0.0", 46 | "jasmine": "^3.6.4", 47 | "jasmine-expect": "^5.0.0", 48 | "mini-css-extract-plugin": "^1.3.5", 49 | "rimraf": "^3.0.2", 50 | "semistandard": "^16.0.0", 51 | "slash": "^3.0.0", 52 | "style-loader": "^2.0.0", 53 | "webpack": "^5.20.1" 54 | }, 55 | "scripts": { 56 | "prepublish": "npm run test", 57 | "pretest": "semistandard", 58 | "test": "jasmine", 59 | "testOne": "jasmine --filter=\"prependExternals*\"", 60 | "debug": "node-debug jasmine" 61 | }, 62 | "unmockedModulePathPatterns": [ 63 | "jasmine-expect" 64 | ], 65 | "semistandard": { 66 | "ignore": [ 67 | "spec/fixtures/**" 68 | ] 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /spec/end_to_end.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jasmine */ 2 | const path = require('path'); 3 | const fs = require('fs'); 4 | const rimraf = require('rimraf'); 5 | const dirCompare = require('dir-compare'); 6 | const cheerio = require('cheerio'); 7 | 8 | require('jasmine-expect'); 9 | const { addMatchers } = require('add-matchers'); 10 | 11 | const matcherToBeTag = (tagProperties, actual) => { 12 | if (!actual) { 13 | return false; 14 | } 15 | const node = actual; 16 | const nodeTagName = node.tagName || node.name; 17 | if (!nodeTagName || nodeTagName !== tagProperties.tagName) { 18 | return false; 19 | } 20 | if (tagProperties.attributes) { 21 | const tagAttrs = tagProperties.attributes; 22 | const nodeAttrs = node.attribs || {}; 23 | return !Object.keys(tagAttrs).some(tagAttr => tagAttrs[tagAttr] !== nodeAttrs[tagAttr]); 24 | } else { 25 | return true; 26 | } 27 | }; 28 | 29 | const matcherToContainTag = (tagProperties, actual) => { 30 | return actual.some(tag => matcherToBeTag(tagProperties, tag)); 31 | }; 32 | 33 | const matchersByName = { 34 | toBeTag: matcherToBeTag, 35 | toContainTag: matcherToContainTag 36 | }; 37 | 38 | addMatchers(matchersByName); 39 | 40 | const webpack = require('webpack'); 41 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 42 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 43 | 44 | const HtmlWebpackDeployPlugin = require('../src'); 45 | 46 | const packageJson = require('../package.json'); 47 | const { devDependencies } = packageJson; 48 | const { bootstrap, bulma } = devDependencies; 49 | 50 | const BOOTSTRAP_VERSION = bootstrap; 51 | const BULMA_VERSION = bulma; 52 | 53 | const OUTPUT_FILENAME = '[name].js'; 54 | 55 | const FIXTURES_PATH = path.join(__dirname, './fixtures'); 56 | const FIXTURES_ENTRY = path.join(FIXTURES_PATH, 'entry.js'); 57 | const FIXTURES_STYLE = path.join(FIXTURES_PATH, 'app.css'); 58 | 59 | const OUTPUT_DIR = path.join(FIXTURES_PATH, 'dist'); 60 | const OUPUT_HTML_FILE = path.join(OUTPUT_DIR, 'index.html'); 61 | 62 | const WEBPACK_CSS_RULE = { test: /\.css$/, use: [MiniCssExtractPlugin.loader, 'css-loader'] }; 63 | 64 | const WEBPACK_ENTRY = { 65 | app: FIXTURES_ENTRY, 66 | style: FIXTURES_STYLE 67 | }; 68 | 69 | const WEBPACK_OUTPUT = { 70 | path: OUTPUT_DIR, 71 | filename: OUTPUT_FILENAME 72 | }; 73 | 74 | const WEBPACK_MODULE = { 75 | rules: [WEBPACK_CSS_RULE] 76 | }; 77 | 78 | const areEqualDirectories = (dirA, dirB, { loose = false, files = null } = {}) => { 79 | const pathA = path.resolve(__dirname, dirA); 80 | const pathB = path.resolve(__dirname, dirB); 81 | const includeFilter = files ? files.join(',') : '**.js,**.js.map,**.css'; 82 | const compareSyncOptions = { 83 | compareSize: false, 84 | compareContent: false 85 | }; 86 | if (includeFilter) { 87 | compareSyncOptions.includeFilter = includeFilter; 88 | } 89 | const compareResult = dirCompare.compareSync(pathA, pathB, compareSyncOptions); 90 | if (files) { 91 | return compareResult.equalFiles === files.length; 92 | } 93 | return (loose ? (compareResult.right === 0) : compareResult.same); 94 | }; 95 | 96 | const createWebpackConfig = ({ 97 | webpackMode = 'production', 98 | webpackPublicPath = undefined, 99 | htmlOptions = {}, 100 | options = {} 101 | } = {}) => { 102 | return { 103 | entry: { ...WEBPACK_ENTRY }, 104 | output: { 105 | ...WEBPACK_OUTPUT, 106 | ...(webpackPublicPath ? { publicPath: webpackPublicPath } : {}) 107 | }, 108 | module: { ...WEBPACK_MODULE }, 109 | plugins: [ 110 | new MiniCssExtractPlugin({ filename: '[name].css' }), 111 | new HtmlWebpackPlugin(htmlOptions), 112 | new HtmlWebpackDeployPlugin(options) 113 | ], 114 | mode: webpackMode 115 | }; 116 | }; 117 | 118 | const cheerioLoadTags = (file, tagsCallback) => { 119 | fs.readFile(file, 'utf8', (er, data) => { 120 | expect(er).toBeFalsy(); 121 | const $ = cheerio.load(data); 122 | const cheerioLinks = $('link'); 123 | const cheerioScripts = $('script'); 124 | const links = cheerioLinks.get().map((link, i) => ({ ...link, jasmineToString: $(link).toString })); 125 | const scripts = cheerioScripts.get().map((script, i) => ({ ...script, jasmineToString: $(script).toString })); 126 | links.jasmineToString = () => cheerioLinks.toString(); 127 | scripts.jasmineToString = () => cheerioScripts.toString(); 128 | tagsCallback({ 129 | scripts, 130 | links, 131 | data 132 | }); 133 | }); 134 | }; 135 | 136 | describe('end to end', () => { 137 | beforeEach(done => { 138 | rimraf(OUTPUT_DIR, done); 139 | }); 140 | 141 | describe('options', () => { 142 | it('it does nothing for empty options', done => { 143 | webpack(createWebpackConfig(), (err, result) => { 144 | expect(err).toBeFalsy(); 145 | expect(JSON.stringify(result.compilation.errors)).toBe('[]'); 146 | cheerioLoadTags(OUPUT_HTML_FILE, ({ links, scripts }) => { 147 | expect(links.length).toBe(1); 148 | expect(scripts.length).toBe(2); 149 | expect(links).toContainTag({ tagName: 'link', attributes: { href: 'style.css', rel: 'stylesheet' } }); 150 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'app.js' } }); 151 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'style.js' } }); 152 | 153 | done(); 154 | }); 155 | }); 156 | }); 157 | 158 | it('uses a boolean true assetsPath', done => { 159 | webpack(createWebpackConfig({ 160 | options: { 161 | assetsPath: true, 162 | assets: { 163 | copy: [{ from: 'spec/fixtures/assets/foo.js', to: 'foo.js' }], 164 | scripts: 'foo.js' 165 | } 166 | } 167 | }), (err, result) => { 168 | expect(err).toBeFalsy(); 169 | expect(JSON.stringify(result.compilation.errors)).toBe('[]'); 170 | expect(areEqualDirectories('fixtures/assets', `${OUTPUT_DIR}/assets`, { files: ['foo.js'] })).toBe(true); 171 | cheerioLoadTags(OUPUT_HTML_FILE, ({ links, scripts }) => { 172 | expect(links.length).toBe(1); 173 | expect(scripts.length).toBe(3); 174 | expect(links).toContainTag({ tagName: 'link', attributes: { href: 'style.css', rel: 'stylesheet' } }); 175 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'app.js' } }); 176 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'style.js' } }); 177 | expect(scripts[2]).toBeTag({ tagName: 'script', attributes: { src: 'assets/foo.js' } }); 178 | 179 | done(); 180 | }); 181 | }); 182 | }); 183 | 184 | it('uses a boolean false assetsPath', done => { 185 | webpack(createWebpackConfig({ 186 | options: { 187 | assetsPath: false, 188 | assets: { 189 | copy: [{ from: 'spec/fixtures/assets/foo.js', to: 'foo.js' }], 190 | scripts: 'foo.js' 191 | } 192 | } 193 | }), (err, result) => { 194 | expect(err).toBeFalsy(); 195 | expect(JSON.stringify(result.compilation.errors)).toBe('[]'); 196 | expect(areEqualDirectories('fixtures/assets', `${OUTPUT_DIR}`, { files: ['foo.js'] })).toBe(true); 197 | cheerioLoadTags(OUPUT_HTML_FILE, ({ links, scripts, data }) => { 198 | expect(links.length).toBe(1); 199 | expect(scripts.length).toBe(3); 200 | expect(links).toContainTag({ tagName: 'link', attributes: { href: 'style.css', rel: 'stylesheet' } }); 201 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'app.js' } }); 202 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'style.js' } }); 203 | expect(scripts[2]).toBeTag({ tagName: 'script', attributes: { src: 'foo.js' } }); 204 | 205 | done(); 206 | }); 207 | }); 208 | }); 209 | 210 | it('uses a string assetsPath', done => { 211 | webpack(createWebpackConfig({ 212 | options: { 213 | assetsPath: 'my-assets-path', 214 | assets: { 215 | copy: [{ from: 'spec/fixtures/assets/foo.js', to: 'foo.js' }], 216 | scripts: 'foo.js' 217 | } 218 | } 219 | }), (err, result) => { 220 | expect(err).toBeFalsy(); 221 | expect(JSON.stringify(result.compilation.errors)).toBe('[]'); 222 | expect(areEqualDirectories('fixtures/assets', `${OUTPUT_DIR}/my-assets-path`, { files: ['foo.js'] })).toBe(true); 223 | cheerioLoadTags(OUPUT_HTML_FILE, ({ links, scripts }) => { 224 | expect(links.length).toBe(1); 225 | expect(scripts.length).toBe(3); 226 | expect(links).toContainTag({ tagName: 'link', attributes: { href: 'style.css', rel: 'stylesheet' } }); 227 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'app.js' } }); 228 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'style.js' } }); 229 | expect(scripts[2]).toBeTag({ tagName: 'script', attributes: { src: 'my-assets-path/foo.js' } }); 230 | 231 | done(); 232 | }); 233 | }); 234 | }); 235 | 236 | it('uses a function assetsPath', done => { 237 | webpack(createWebpackConfig({ 238 | options: { 239 | assetsPath: assetPath => path.join('func-assets', assetPath), 240 | assets: { 241 | copy: [{ from: 'spec/fixtures/assets/foo.js', to: 'foo.js' }], 242 | scripts: 'foo.js' 243 | } 244 | } 245 | }), (err, result) => { 246 | expect(err).toBeFalsy(); 247 | expect(JSON.stringify(result.compilation.errors)).toBe('[]'); 248 | expect(areEqualDirectories('fixtures/assets', `${OUTPUT_DIR}/func-assets`, { files: ['foo.js'] })).toBe(true); 249 | cheerioLoadTags(OUPUT_HTML_FILE, ({ links, scripts }) => { 250 | expect(links.length).toBe(1); 251 | expect(scripts.length).toBe(3); 252 | expect(links).toContainTag({ tagName: 'link', attributes: { href: 'style.css', rel: 'stylesheet' } }); 253 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'app.js' } }); 254 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'style.js' } }); 255 | expect(scripts[2]).toBeTag({ tagName: 'script', attributes: { src: 'func-assets/foo.js' } }); 256 | 257 | done(); 258 | }); 259 | }); 260 | }); 261 | 262 | it('uses a boolean true packagesPath', done => { 263 | webpack(createWebpackConfig({ 264 | options: { 265 | packagesPath: true, 266 | packages: { 267 | bootstrap: { 268 | copy: [{ 269 | from: 'dist/css', to: 'css/' 270 | }], 271 | links: [ 272 | 'css/bootstrap.min.css' 273 | ] 274 | } 275 | } 276 | } 277 | }), (err, result) => { 278 | expect(err).toBeFalsy(); 279 | expect(JSON.stringify(result.compilation.errors)).toBe('[]'); 280 | expect(areEqualDirectories('../node_modules/bootstrap/dist/css', `${OUTPUT_DIR}/packages/bootstrap-${BOOTSTRAP_VERSION}/css`)).toBe(true); 281 | cheerioLoadTags(OUPUT_HTML_FILE, ({ links, scripts }) => { 282 | expect(links.length).toBe(2); 283 | expect(scripts.length).toBe(2); 284 | expect(links).toContainTag({ tagName: 'link', attributes: { href: 'style.css', rel: 'stylesheet' } }); 285 | expect(links[1]).toBeTag({ tagName: 'link', attributes: { href: `packages/bootstrap-${BOOTSTRAP_VERSION}/css/bootstrap.min.css`, rel: 'stylesheet' } }); 286 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'app.js' } }); 287 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'style.js' } }); 288 | 289 | done(); 290 | }); 291 | }); 292 | }); 293 | 294 | it('uses a boolean false packagesPath', done => { 295 | webpack(createWebpackConfig({ 296 | options: { 297 | packagesPath: false, 298 | packages: { 299 | bootstrap: { 300 | copy: [{ 301 | from: 'dist/css', to: 'css/' 302 | }], 303 | links: [ 304 | 'css/bootstrap.min.css' 305 | ] 306 | } 307 | } 308 | } 309 | }), (err, result) => { 310 | expect(err).toBeFalsy(); 311 | expect(JSON.stringify(result.compilation.errors)).toBe('[]'); 312 | expect(areEqualDirectories('../node_modules/bootstrap/dist/css', `${OUTPUT_DIR}/bootstrap-${BOOTSTRAP_VERSION}/css`)).toBe(true); 313 | cheerioLoadTags(OUPUT_HTML_FILE, ({ links, scripts, data }) => { 314 | expect(links.length).toBe(2); 315 | expect(scripts.length).toBe(2); 316 | expect(links).toContainTag({ tagName: 'link', attributes: { href: 'style.css', rel: 'stylesheet' } }); 317 | expect(links[1]).toBeTag({ tagName: 'link', attributes: { href: `bootstrap-${BOOTSTRAP_VERSION}/css/bootstrap.min.css`, rel: 'stylesheet' } }); 318 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'app.js' } }); 319 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'style.js' } }); 320 | 321 | done(); 322 | }); 323 | }); 324 | }); 325 | 326 | it('uses a string packagesPath', done => { 327 | webpack(createWebpackConfig({ 328 | options: { 329 | packagesPath: 'my-packages-path', 330 | packages: { 331 | bootstrap: { 332 | copy: [{ 333 | from: 'dist/css', to: 'css/' 334 | }], 335 | links: [ 336 | 'css/bootstrap.min.css' 337 | ] 338 | } 339 | } 340 | } 341 | }), (err, result) => { 342 | expect(err).toBeFalsy(); 343 | expect(JSON.stringify(result.compilation.errors)).toBe('[]'); 344 | expect(areEqualDirectories('../node_modules/bootstrap/dist/css', `${OUTPUT_DIR}/my-packages-path/bootstrap-${BOOTSTRAP_VERSION}/css`)).toBe(true); 345 | cheerioLoadTags(OUPUT_HTML_FILE, ({ links, scripts }) => { 346 | expect(links.length).toBe(2); 347 | expect(scripts.length).toBe(2); 348 | expect(links).toContainTag({ tagName: 'link', attributes: { href: 'style.css', rel: 'stylesheet' } }); 349 | expect(links[1]).toBeTag({ tagName: 'link', attributes: { href: `my-packages-path/bootstrap-${BOOTSTRAP_VERSION}/css/bootstrap.min.css`, rel: 'stylesheet' } }); 350 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'app.js' } }); 351 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'style.js' } }); 352 | 353 | done(); 354 | }); 355 | }); 356 | }); 357 | 358 | it('uses a function packagesPath', done => { 359 | webpack(createWebpackConfig({ 360 | options: { 361 | packagesPath: packagePath => path.join('func-packages', packagePath), 362 | packages: { 363 | bootstrap: { 364 | copy: [{ 365 | from: 'dist/css', to: 'css/' 366 | }], 367 | links: [ 368 | 'css/bootstrap.min.css' 369 | ] 370 | } 371 | } 372 | } 373 | }), (err, result) => { 374 | expect(err).toBeFalsy(); 375 | expect(JSON.stringify(result.compilation.errors)).toBe('[]'); 376 | expect(areEqualDirectories('../node_modules/bootstrap/dist/css', `${OUTPUT_DIR}/func-packages/bootstrap-${BOOTSTRAP_VERSION}/css`)).toBe(true); 377 | cheerioLoadTags(OUPUT_HTML_FILE, ({ links, scripts }) => { 378 | expect(links.length).toBe(2); 379 | expect(scripts.length).toBe(2); 380 | expect(links).toContainTag({ tagName: 'link', attributes: { href: 'style.css', rel: 'stylesheet' } }); 381 | expect(links[1]).toBeTag({ tagName: 'link', attributes: { href: `func-packages/bootstrap-${BOOTSTRAP_VERSION}/css/bootstrap.min.css`, rel: 'stylesheet' } }); 382 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'app.js' } }); 383 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'style.js' } }); 384 | 385 | done(); 386 | }); 387 | }); 388 | }); 389 | 390 | it('uses a boolean true package copy fromAbsolute', done => { 391 | const fromPath = path.join(FIXTURES_PATH, 'a-file'); 392 | webpack(createWebpackConfig({ 393 | options: { 394 | packages: { 395 | bootstrap: { 396 | copy: [{ 397 | fromAbsolute: true, 398 | from: fromPath, 399 | to: 'css/' 400 | }], 401 | links: [ 402 | 'css/bootstrap.min.css' 403 | ] 404 | } 405 | }, 406 | assets: { 407 | copy: [{ 408 | from: fromPath, to: 'files' 409 | }] 410 | } 411 | } 412 | }), (err, result) => { 413 | expect(err).toBeFalsy(); 414 | expect(JSON.stringify(result.compilation.errors)).toBe('[]'); 415 | expect(areEqualDirectories(FIXTURES_PATH, `${OUTPUT_DIR}/packages/bootstrap-${BOOTSTRAP_VERSION}/css`, { files: ['a-file'] })).toBe(true); 416 | done(); 417 | }); 418 | }); 419 | 420 | it('uses a boolean false package copy fromAbsolute', done => { 421 | const fromPath = 'dist/css'; 422 | webpack(createWebpackConfig({ 423 | options: { 424 | packages: { 425 | bootstrap: { 426 | copy: [{ 427 | fromAbsolute: false, 428 | from: fromPath, 429 | to: 'css/' 430 | }], 431 | links: [ 432 | 'css/bootstrap.min.css' 433 | ] 434 | } 435 | } 436 | } 437 | }), (err, result) => { 438 | expect(err).toBeFalsy(); 439 | expect(JSON.stringify(result.compilation.errors)).toBe('[]'); 440 | expect(areEqualDirectories('../node_modules/bootstrap/dist/css', `${OUTPUT_DIR}/packages/bootstrap-${BOOTSTRAP_VERSION}/css`, { files: ['bootstrap.min.css'] })).toBe(true); 441 | done(); 442 | }); 443 | }); 444 | }); 445 | 446 | describe('tags plugin passthrough options', () => { 447 | describe('files', () => { 448 | it('does not output when files is set to a different file', done => { 449 | webpack(createWebpackConfig({ 450 | options: { 451 | packagesPath: false, 452 | packages: { 453 | bootstrap: { 454 | copy: [{ 455 | from: 'dist/css', to: 'css/' 456 | }], 457 | links: [ 458 | 'css/bootstrap.min.css' 459 | ] 460 | } 461 | }, 462 | files: 'index_123' 463 | } 464 | }), (err, result) => { 465 | expect(err).toBeFalsy(); 466 | expect(JSON.stringify(result.compilation.errors)).toBe('[]'); 467 | expect(areEqualDirectories('../node_modules/bootstrap/dist/css', `${OUTPUT_DIR}/bootstrap-${BOOTSTRAP_VERSION}/css`)).toBe(true); 468 | cheerioLoadTags(OUPUT_HTML_FILE, ({ links, scripts, data }) => { 469 | expect(links.length).toBe(1); 470 | expect(scripts.length).toBe(2); 471 | expect(links).toContainTag({ tagName: 'link', attributes: { href: 'style.css', rel: 'stylesheet' } }); 472 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'app.js' } }); 473 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'style.js' } }); 474 | 475 | done(); 476 | }); 477 | }); 478 | }); 479 | 480 | it('does output when files is set to the same file', done => { 481 | webpack(createWebpackConfig({ 482 | options: { 483 | packagesPath: false, 484 | packages: { 485 | bootstrap: { 486 | copy: [{ 487 | from: 'dist/css', to: 'css/' 488 | }], 489 | links: [ 490 | 'css/bootstrap.min.css' 491 | ] 492 | } 493 | }, 494 | files: 'index.html' 495 | } 496 | }), (err, result) => { 497 | expect(err).toBeFalsy(); 498 | expect(JSON.stringify(result.compilation.errors)).toBe('[]'); 499 | expect(areEqualDirectories('../node_modules/bootstrap/dist/css', `${OUTPUT_DIR}/bootstrap-${BOOTSTRAP_VERSION}/css`)).toBe(true); 500 | cheerioLoadTags(OUPUT_HTML_FILE, ({ links, scripts, data }) => { 501 | expect(links.length).toBe(2); 502 | expect(scripts.length).toBe(2); 503 | expect(links).toContainTag({ tagName: 'link', attributes: { href: 'style.css', rel: 'stylesheet' } }); 504 | expect(links[1]).toBeTag({ tagName: 'link', attributes: { href: `bootstrap-${BOOTSTRAP_VERSION}/css/bootstrap.min.css`, rel: 'stylesheet' } }); 505 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'app.js' } }); 506 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'style.js' } }); 507 | 508 | done(); 509 | }); 510 | }); 511 | }); 512 | }); 513 | 514 | describe('prependExternals', () => { 515 | it('auto prepends scripts with externals when prependExternals is true', done => { 516 | webpack(createWebpackConfig({ 517 | options: { 518 | packages: { 519 | bootstrap: { 520 | copy: [ 521 | { from: 'dist/css', to: 'css/' }, 522 | { from: 'dist/js', to: 'js/' } 523 | ], 524 | links: [ 525 | 'css/bootstrap.min.css' 526 | ], 527 | scripts: { 528 | variableName: 'Bootstrap', 529 | path: 'js/bootstrap.bundle.min.js' 530 | } 531 | } 532 | }, 533 | prependExternals: true 534 | } 535 | }), (err, result) => { 536 | expect(err).toBeFalsy(); 537 | expect(JSON.stringify(result.compilation.errors)).toBe('[]'); 538 | expect(JSON.stringify(result.compilation.options.externals)).toBe('{"bootstrap":"Bootstrap"}'); 539 | 540 | expect(areEqualDirectories('../node_modules/bootstrap/dist/css', `${OUTPUT_DIR}/packages/bootstrap-${BOOTSTRAP_VERSION}/css`)).toBe(true); 541 | expect(areEqualDirectories('../node_modules/bootstrap/dist/js', `${OUTPUT_DIR}/packages/bootstrap-${BOOTSTRAP_VERSION}/js`)).toBe(true); 542 | 543 | cheerioLoadTags(OUPUT_HTML_FILE, ({ links, scripts }) => { 544 | expect(links.length).toBe(2); 545 | expect(scripts.length).toBe(3); 546 | expect(links).toContainTag({ tagName: 'link', attributes: { href: 'style.css', rel: 'stylesheet' } }); 547 | expect(links[1]).toBeTag({ tagName: 'link', attributes: { href: `packages/bootstrap-${BOOTSTRAP_VERSION}/css/bootstrap.min.css`, rel: 'stylesheet' } }); 548 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'app.js' } }); 549 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'style.js' } }); 550 | expect(scripts[0]).toBeTag({ tagName: 'script', attributes: { src: `packages/bootstrap-${BOOTSTRAP_VERSION}/js/bootstrap.bundle.min.js` } }); 551 | 552 | done(); 553 | }); 554 | }); 555 | }); 556 | 557 | it('does not auto prepend scripts with externals when prependExternals is false', done => { 558 | webpack(createWebpackConfig({ 559 | options: { 560 | packages: { 561 | bootstrap: { 562 | copy: [ 563 | { from: 'dist/css', to: 'css/' }, 564 | { from: 'dist/js', to: 'js/' } 565 | ], 566 | links: [ 567 | 'css/bootstrap.min.css' 568 | ], 569 | scripts: { 570 | variableName: 'Bootstrap', 571 | path: 'js/bootstrap.bundle.min.js' 572 | } 573 | } 574 | }, 575 | prependExternals: false 576 | } 577 | }), (err, result) => { 578 | expect(err).toBeFalsy(); 579 | expect(JSON.stringify(result.compilation.errors)).toBe('[]'); 580 | expect(JSON.stringify(result.compilation.options.externals)).toBe('{"bootstrap":"Bootstrap"}'); 581 | 582 | expect(areEqualDirectories('../node_modules/bootstrap/dist/css', `${OUTPUT_DIR}/packages/bootstrap-${BOOTSTRAP_VERSION}/css`)).toBe(true); 583 | expect(areEqualDirectories('../node_modules/bootstrap/dist/js', `${OUTPUT_DIR}/packages/bootstrap-${BOOTSTRAP_VERSION}/js`)).toBe(true); 584 | 585 | cheerioLoadTags(OUPUT_HTML_FILE, ({ links, scripts }) => { 586 | expect(links.length).toBe(2); 587 | expect(scripts.length).toBe(3); 588 | expect(links).toContainTag({ tagName: 'link', attributes: { href: 'style.css', rel: 'stylesheet' } }); 589 | expect(links[1]).toBeTag({ tagName: 'link', attributes: { href: `packages/bootstrap-${BOOTSTRAP_VERSION}/css/bootstrap.min.css`, rel: 'stylesheet' } }); 590 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'app.js' } }); 591 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'style.js' } }); 592 | expect(scripts[2]).toBeTag({ tagName: 'script', attributes: { src: `packages/bootstrap-${BOOTSTRAP_VERSION}/js/bootstrap.bundle.min.js` } }); 593 | 594 | done(); 595 | }); 596 | }); 597 | }); 598 | }); 599 | 600 | describe('append', () => { 601 | describe('root level', () => { 602 | const baseOptions = { 603 | assets: { 604 | links: 'abc.css', 605 | scripts: [{ path: 'abc.js' }] 606 | }, 607 | packages: { 608 | bootstrap: { 609 | links: ['def.css'], 610 | scripts: { path: 'def.js' } 611 | } 612 | } 613 | }; 614 | const testOptions = [ 615 | { ...baseOptions, append: false }, 616 | { ...baseOptions, append: true } 617 | ]; 618 | const addedLinkCount = 2; 619 | const addedScriptCount = 2; 620 | const expectedLinkCount = 1 + addedLinkCount; 621 | const expetedScriptCount = 2 + addedScriptCount; 622 | 623 | testOptions.forEach(options => { 624 | const expectedLinkIndex = options.append ? expectedLinkCount - addedLinkCount : 0; 625 | const expectedScriptIndex = options.append ? expetedScriptCount - addedScriptCount : 0; 626 | it(`applies append ${options.append}`, done => { 627 | webpack(createWebpackConfig({ options }), (err, result) => { 628 | expect(err).toBeFalsy(); 629 | expect(JSON.stringify(result.compilation.errors)).toBe('[]'); 630 | 631 | cheerioLoadTags(OUPUT_HTML_FILE, ({ links, scripts }) => { 632 | expect(links.length).toBe(expectedLinkCount); 633 | expect(scripts.length).toBe(expetedScriptCount); 634 | expect(links).toContainTag({ tagName: 'link', attributes: { href: 'style.css', rel: 'stylesheet' } }); 635 | expect(links[expectedLinkIndex]).toBeTag({ tagName: 'link', attributes: { href: 'assets/abc.css', rel: 'stylesheet' } }); 636 | expect(links[expectedLinkIndex + 1]).toBeTag({ tagName: 'link', attributes: { href: `packages/bootstrap-${BOOTSTRAP_VERSION}/def.css`, rel: 'stylesheet' } }); 637 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'app.js' } }); 638 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'style.js' } }); 639 | expect(scripts[expectedScriptIndex]).toBeTag({ tagName: 'script', attributes: { src: 'assets/abc.js' } }); 640 | expect(scripts[expectedScriptIndex + 1]).toBeTag({ tagName: 'script', attributes: { src: `packages/bootstrap-${BOOTSTRAP_VERSION}/def.js` } }); 641 | 642 | done(); 643 | }); 644 | }); 645 | }); 646 | }); 647 | }); 648 | 649 | describe('assets level', () => { 650 | const baseAssets = { 651 | links: 'abc.css', 652 | scripts: [{ path: 'abc.js' }] 653 | }; 654 | const testOptions = [ 655 | { assets: { ...baseAssets, append: true }, append: false }, 656 | { assets: { ...baseAssets, append: false }, append: true } 657 | ]; 658 | const addedLinkCount = 1; 659 | const addedScriptCount = 1; 660 | const expectedLinkCount = 1 + addedLinkCount; 661 | const expetedScriptCount = 2 + addedScriptCount; 662 | 663 | testOptions.forEach(options => { 664 | const expectedLinkIndex = options.assets.append ? expectedLinkCount - addedLinkCount : 0; 665 | const expectedScriptIndex = options.assets.append ? expetedScriptCount - addedScriptCount : 0; 666 | it(`applies assets.append ${options.assets.append}`, done => { 667 | webpack(createWebpackConfig({ options }), (err, result) => { 668 | expect(err).toBeFalsy(); 669 | expect(JSON.stringify(result.compilation.errors)).toBe('[]'); 670 | 671 | cheerioLoadTags(OUPUT_HTML_FILE, ({ links, scripts }) => { 672 | expect(links.length).toBe(expectedLinkCount); 673 | expect(scripts.length).toBe(expetedScriptCount); 674 | expect(links).toContainTag({ tagName: 'link', attributes: { href: 'style.css', rel: 'stylesheet' } }); 675 | expect(links[expectedLinkIndex]).toBeTag({ tagName: 'link', attributes: { href: 'assets/abc.css', rel: 'stylesheet' } }); 676 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'app.js' } }); 677 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'style.js' } }); 678 | expect(scripts[expectedScriptIndex]).toBeTag({ tagName: 'script', attributes: { src: 'assets/abc.js' } }); 679 | 680 | done(); 681 | }); 682 | }); 683 | }); 684 | }); 685 | }); 686 | 687 | describe('packages level', () => { 688 | const basePackage = { 689 | links: 'abc.css', 690 | scripts: [{ path: 'abc.js' }] 691 | }; 692 | const testOptions = [ 693 | { packages: { bootstrap: { ...basePackage, append: true } }, append: false }, 694 | { packages: { bootstrap: { ...basePackage, append: false } }, append: true } 695 | ]; 696 | const addedLinkCount = 1; 697 | const addedScriptCount = 1; 698 | const expectedLinkCount = 1 + addedLinkCount; 699 | const expetedScriptCount = 2 + addedScriptCount; 700 | 701 | testOptions.forEach(options => { 702 | const expectedLinkIndex = options.packages.bootstrap.append ? expectedLinkCount - addedLinkCount : 0; 703 | const expectedScriptIndex = options.packages.bootstrap.append ? expetedScriptCount - addedScriptCount : 0; 704 | it(`applies assets.append ${options.packages.bootstrap.append}`, done => { 705 | webpack(createWebpackConfig({ options }), (err, result) => { 706 | expect(err).toBeFalsy(); 707 | expect(JSON.stringify(result.compilation.errors)).toBe('[]'); 708 | 709 | cheerioLoadTags(OUPUT_HTML_FILE, ({ links, scripts }) => { 710 | expect(links.length).toBe(expectedLinkCount); 711 | expect(scripts.length).toBe(expetedScriptCount); 712 | expect(links).toContainTag({ tagName: 'link', attributes: { href: 'style.css', rel: 'stylesheet' } }); 713 | expect(links[expectedLinkIndex]).toBeTag({ tagName: 'link', attributes: { href: `packages/bootstrap-${BOOTSTRAP_VERSION}/abc.css`, rel: 'stylesheet' } }); 714 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'app.js' } }); 715 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'style.js' } }); 716 | expect(scripts[expectedScriptIndex]).toBeTag({ tagName: 'script', attributes: { src: `packages/bootstrap-${BOOTSTRAP_VERSION}/abc.js` } }); 717 | 718 | done(); 719 | }); 720 | }); 721 | }); 722 | }); 723 | }); 724 | }); 725 | 726 | describe('publicPath', () => { 727 | describe('root level', () => { 728 | const baseOptions = { 729 | assets: { 730 | links: 'abc.css', 731 | scripts: [{ path: 'abc.js' }] 732 | }, 733 | packages: { 734 | bootstrap: { 735 | links: ['def.css'], 736 | scripts: { path: 'def.js' } 737 | } 738 | } 739 | }; 740 | const testOptions = [ 741 | { ...baseOptions, publicPath: false }, 742 | { ...baseOptions, publicPath: '/myPublicPath/' } 743 | ]; 744 | const addedLinkCount = 2; 745 | const addedScriptCount = 2; 746 | const expectedLinkCount = 1 + addedLinkCount; 747 | const expetedScriptCount = 2 + addedScriptCount; 748 | 749 | testOptions.forEach(options => { 750 | const prefix = options.publicPath ? options.publicPath : ''; 751 | it(`applies publicPath ${options.publicPath}`, done => { 752 | webpack(createWebpackConfig({ options }), (err, result) => { 753 | expect(err).toBeFalsy(); 754 | expect(JSON.stringify(result.compilation.errors)).toBe('[]'); 755 | 756 | cheerioLoadTags(OUPUT_HTML_FILE, ({ links, scripts }) => { 757 | expect(links.length).toBe(expectedLinkCount); 758 | expect(scripts.length).toBe(expetedScriptCount); 759 | expect(links).toContainTag({ tagName: 'link', attributes: { href: 'style.css', rel: 'stylesheet' } }); 760 | expect(links[1]).toBeTag({ tagName: 'link', attributes: { href: `${prefix}assets/abc.css`, rel: 'stylesheet' } }); 761 | expect(links[2]).toBeTag({ tagName: 'link', attributes: { href: `${prefix}packages/bootstrap-${BOOTSTRAP_VERSION}/def.css`, rel: 'stylesheet' } }); 762 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'app.js' } }); 763 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'style.js' } }); 764 | expect(scripts[2]).toBeTag({ tagName: 'script', attributes: { src: `${prefix}assets/abc.js` } }); 765 | expect(scripts[3]).toBeTag({ tagName: 'script', attributes: { src: `${prefix}packages/bootstrap-${BOOTSTRAP_VERSION}/def.js` } }); 766 | 767 | done(); 768 | }); 769 | }); 770 | }); 771 | }); 772 | }); 773 | 774 | describe('assets level', () => { 775 | const baseAssets = { 776 | links: 'abc.css', 777 | scripts: [{ path: 'abc.js' }] 778 | }; 779 | const testOptions = [ 780 | { assets: { ...baseAssets, publicPath: '/myPublicPath/' }, publicPath: false }, 781 | { assets: { ...baseAssets, publicPath: false }, publicPath: '/myPublicPath/' } 782 | ]; 783 | const addedLinkCount = 1; 784 | const addedScriptCount = 1; 785 | const expectedLinkCount = 1 + addedLinkCount; 786 | const expetedScriptCount = 2 + addedScriptCount; 787 | 788 | testOptions.forEach(options => { 789 | const prefix = options.assets.publicPath ? options.assets.publicPath : ''; 790 | it(`applies assets.append ${options.assets.append}`, done => { 791 | webpack(createWebpackConfig({ options }), (err, result) => { 792 | expect(err).toBeFalsy(); 793 | expect(JSON.stringify(result.compilation.errors)).toBe('[]'); 794 | 795 | cheerioLoadTags(OUPUT_HTML_FILE, ({ links, scripts }) => { 796 | expect(links.length).toBe(expectedLinkCount); 797 | expect(scripts.length).toBe(expetedScriptCount); 798 | expect(links).toContainTag({ tagName: 'link', attributes: { href: 'style.css', rel: 'stylesheet' } }); 799 | expect(links[1]).toBeTag({ tagName: 'link', attributes: { href: `${prefix}assets/abc.css`, rel: 'stylesheet' } }); 800 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'app.js' } }); 801 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'style.js' } }); 802 | expect(scripts[2]).toBeTag({ tagName: 'script', attributes: { src: `${prefix}assets/abc.js` } }); 803 | 804 | done(); 805 | }); 806 | }); 807 | }); 808 | }); 809 | }); 810 | 811 | describe('packages level', () => { 812 | const basePackage = { 813 | links: 'abc.css', 814 | scripts: [{ path: 'abc.js' }] 815 | }; 816 | const testOptions = [ 817 | { packages: { bootstrap: { ...basePackage, publicPath: '/myPublicPath/' } }, publicPath: false }, 818 | { packages: { bootstrap: { ...basePackage, publicPath: false } }, publicPath: '/myPublicPath/' } 819 | ]; 820 | const addedLinkCount = 1; 821 | const addedScriptCount = 1; 822 | const expectedLinkCount = 1 + addedLinkCount; 823 | const expetedScriptCount = 2 + addedScriptCount; 824 | 825 | testOptions.forEach(options => { 826 | const prefix = options.packages.bootstrap.publicPath ? options.packages.bootstrap.publicPath : ''; 827 | it(`applies assets.append ${options.packages.bootstrap.append}`, done => { 828 | webpack(createWebpackConfig({ options }), (err, result) => { 829 | expect(err).toBeFalsy(); 830 | expect(JSON.stringify(result.compilation.errors)).toBe('[]'); 831 | 832 | cheerioLoadTags(OUPUT_HTML_FILE, ({ links, scripts }) => { 833 | expect(links.length).toBe(expectedLinkCount); 834 | expect(scripts.length).toBe(expetedScriptCount); 835 | expect(links).toContainTag({ tagName: 'link', attributes: { href: 'style.css', rel: 'stylesheet' } }); 836 | expect(links[1]).toBeTag({ tagName: 'link', attributes: { href: `${prefix}packages/bootstrap-${BOOTSTRAP_VERSION}/abc.css`, rel: 'stylesheet' } }); 837 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'app.js' } }); 838 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'style.js' } }); 839 | expect(scripts[2]).toBeTag({ tagName: 'script', attributes: { src: `${prefix}packages/bootstrap-${BOOTSTRAP_VERSION}/abc.js` } }); 840 | 841 | done(); 842 | }); 843 | }); 844 | }); 845 | }); 846 | }); 847 | }); 848 | 849 | describe('hash', () => { 850 | describe('root level', () => { 851 | const baseOptions = { 852 | assets: { 853 | links: 'abc.css', 854 | scripts: [{ path: 'abc.js' }] 855 | }, 856 | packages: { 857 | bootstrap: { 858 | links: ['def.css'], 859 | scripts: { path: 'def.js' } 860 | } 861 | } 862 | }; 863 | const testOptions = [ 864 | { ...baseOptions, hash: false }, 865 | { ...baseOptions, hash: 'the-hash' } 866 | ]; 867 | const addedLinkCount = 2; 868 | const addedScriptCount = 2; 869 | const expectedLinkCount = 1 + addedLinkCount; 870 | const expetedScriptCount = 2 + addedScriptCount; 871 | 872 | testOptions.forEach(options => { 873 | const suffix = options.hash ? ('?' + options.hash) : ''; 874 | it(`applies publicPath ${options.publicPath}`, done => { 875 | webpack(createWebpackConfig({ options }), (err, result) => { 876 | expect(err).toBeFalsy(); 877 | expect(JSON.stringify(result.compilation.errors)).toBe('[]'); 878 | 879 | cheerioLoadTags(OUPUT_HTML_FILE, ({ links, scripts }) => { 880 | expect(links.length).toBe(expectedLinkCount); 881 | expect(scripts.length).toBe(expetedScriptCount); 882 | expect(links).toContainTag({ tagName: 'link', attributes: { href: 'style.css', rel: 'stylesheet' } }); 883 | expect(links[1]).toBeTag({ tagName: 'link', attributes: { href: `assets/abc.css${suffix}`, rel: 'stylesheet' } }); 884 | expect(links[2]).toBeTag({ tagName: 'link', attributes: { href: `packages/bootstrap-${BOOTSTRAP_VERSION}/def.css${suffix}`, rel: 'stylesheet' } }); 885 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'app.js' } }); 886 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'style.js' } }); 887 | expect(scripts[2]).toBeTag({ tagName: 'script', attributes: { src: `assets/abc.js${suffix}` } }); 888 | expect(scripts[3]).toBeTag({ tagName: 'script', attributes: { src: `packages/bootstrap-${BOOTSTRAP_VERSION}/def.js${suffix}` } }); 889 | 890 | done(); 891 | }); 892 | }); 893 | }); 894 | }); 895 | }); 896 | 897 | describe('assets level', () => { 898 | const baseAssets = { 899 | links: 'abc.css', 900 | scripts: [{ path: 'abc.js' }] 901 | }; 902 | const testOptions = [ 903 | { assets: { ...baseAssets, hash: 'the-hash' }, hash: false }, 904 | { assets: { ...baseAssets, hash: false }, hash: 'the-hash' } 905 | ]; 906 | const addedLinkCount = 1; 907 | const addedScriptCount = 1; 908 | const expectedLinkCount = 1 + addedLinkCount; 909 | const expetedScriptCount = 2 + addedScriptCount; 910 | 911 | testOptions.forEach(options => { 912 | const suffix = options.assets.hash ? ('?' + options.assets.hash) : ''; 913 | it(`applies assets.append ${options.assets.append}`, done => { 914 | webpack(createWebpackConfig({ options }), (err, result) => { 915 | expect(err).toBeFalsy(); 916 | expect(JSON.stringify(result.compilation.errors)).toBe('[]'); 917 | 918 | cheerioLoadTags(OUPUT_HTML_FILE, ({ links, scripts }) => { 919 | expect(links.length).toBe(expectedLinkCount); 920 | expect(scripts.length).toBe(expetedScriptCount); 921 | expect(links).toContainTag({ tagName: 'link', attributes: { href: 'style.css', rel: 'stylesheet' } }); 922 | expect(links[1]).toBeTag({ tagName: 'link', attributes: { href: `assets/abc.css${suffix}`, rel: 'stylesheet' } }); 923 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'app.js' } }); 924 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'style.js' } }); 925 | expect(scripts[2]).toBeTag({ tagName: 'script', attributes: { src: `assets/abc.js${suffix}` } }); 926 | 927 | done(); 928 | }); 929 | }); 930 | }); 931 | }); 932 | }); 933 | 934 | describe('packages level', () => { 935 | const basePackage = { 936 | links: 'abc.css', 937 | scripts: [{ path: 'abc.js' }] 938 | }; 939 | const testOptions = [ 940 | { packages: { bootstrap: { ...basePackage, hash: 'the-hash' } }, hash: false }, 941 | { packages: { bootstrap: { ...basePackage, hash: false } }, hash: 'the-hash' } 942 | ]; 943 | const addedLinkCount = 1; 944 | const addedScriptCount = 1; 945 | const expectedLinkCount = 1 + addedLinkCount; 946 | const expetedScriptCount = 2 + addedScriptCount; 947 | 948 | testOptions.forEach(options => { 949 | const suffix = options.packages.bootstrap.hash ? ('?' + options.packages.bootstrap.hash) : ''; 950 | it(`applies assets.append ${options.packages.bootstrap.append}`, done => { 951 | webpack(createWebpackConfig({ options }), (err, result) => { 952 | expect(err).toBeFalsy(); 953 | expect(JSON.stringify(result.compilation.errors)).toBe('[]'); 954 | 955 | cheerioLoadTags(OUPUT_HTML_FILE, ({ links, scripts }) => { 956 | expect(links.length).toBe(expectedLinkCount); 957 | expect(scripts.length).toBe(expetedScriptCount); 958 | expect(links).toContainTag({ tagName: 'link', attributes: { href: 'style.css', rel: 'stylesheet' } }); 959 | expect(links[1]).toBeTag({ tagName: 'link', attributes: { href: `packages/bootstrap-${BOOTSTRAP_VERSION}/abc.css${suffix}`, rel: 'stylesheet' } }); 960 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'app.js' } }); 961 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'style.js' } }); 962 | expect(scripts[2]).toBeTag({ tagName: 'script', attributes: { src: `packages/bootstrap-${BOOTSTRAP_VERSION}/abc.js${suffix}` } }); 963 | 964 | done(); 965 | }); 966 | }); 967 | }); 968 | }); 969 | }); 970 | }); 971 | }); 972 | 973 | describe('packages', () => { 974 | it('it copies and includes css from bootstrap', done => { 975 | webpack(createWebpackConfig({ 976 | options: { 977 | packages: { 978 | bootstrap: { 979 | copy: [{ 980 | from: 'dist/css', to: 'css/' 981 | }], 982 | links: [ 983 | 'css/bootstrap.min.css' 984 | ] 985 | } 986 | } 987 | } 988 | }), (err, result) => { 989 | expect(err).toBeFalsy(); 990 | expect(JSON.stringify(result.compilation.errors)).toBe('[]'); 991 | expect(areEqualDirectories('../node_modules/bootstrap/dist/css', `${OUTPUT_DIR}/packages/bootstrap-${BOOTSTRAP_VERSION}/css`)).toBe(true); 992 | cheerioLoadTags(OUPUT_HTML_FILE, ({ links, scripts }) => { 993 | expect(links.length).toBe(2); 994 | expect(scripts.length).toBe(2); 995 | expect(links).toContainTag({ tagName: 'link', attributes: { href: 'style.css', rel: 'stylesheet' } }); 996 | expect(links[1]).toBeTag({ tagName: 'link', attributes: { href: `packages/bootstrap-${BOOTSTRAP_VERSION}/css/bootstrap.min.css`, rel: 'stylesheet' } }); 997 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'app.js' } }); 998 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'style.js' } }); 999 | 1000 | done(); 1001 | }); 1002 | }); 1003 | }); 1004 | 1005 | it('it uses a custom addPackagesPath option ', done => { 1006 | webpack(createWebpackConfig({ 1007 | options: { 1008 | packages: { 1009 | bootstrap: { 1010 | copy: [{ 1011 | from: 'dist/css', to: 'css/' 1012 | }], 1013 | links: [ 1014 | 'css/bootstrap.min.css' 1015 | ] 1016 | } 1017 | }, 1018 | addPackagesPath: packagePath => path.join('my-packages', packagePath) 1019 | } 1020 | }), (err, result) => { 1021 | expect(err).toBeFalsy(); 1022 | expect(JSON.stringify(result.compilation.errors)).toBe('[]'); 1023 | expect(areEqualDirectories('../node_modules/bootstrap/dist/css', `${OUTPUT_DIR}/my-packages/bootstrap-${BOOTSTRAP_VERSION}/css`)).toBe(true); 1024 | cheerioLoadTags(OUPUT_HTML_FILE, ({ links, scripts }) => { 1025 | expect(links.length).toBe(2); 1026 | expect(scripts.length).toBe(2); 1027 | expect(links).toContainTag({ tagName: 'link', attributes: { href: 'style.css', rel: 'stylesheet' } }); 1028 | expect(links[1]).toBeTag({ tagName: 'link', attributes: { href: `my-packages/bootstrap-${BOOTSTRAP_VERSION}/css/bootstrap.min.css`, rel: 'stylesheet' } }); 1029 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'app.js' } }); 1030 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'style.js' } }); 1031 | 1032 | done(); 1033 | }); 1034 | }); 1035 | }); 1036 | 1037 | it('applies a package script variableName properly', done => { 1038 | webpack(createWebpackConfig({ 1039 | options: { 1040 | packages: { 1041 | bootstrap: { 1042 | copy: [ 1043 | { from: 'dist/css', to: 'css/' }, 1044 | { from: 'dist/js', to: 'js/' } 1045 | ], 1046 | links: [ 1047 | 'css/bootstrap.min.css' 1048 | ], 1049 | scripts: { 1050 | variableName: 'Bootstrap', 1051 | path: 'js/bootstrap.bundle.min.js' 1052 | } 1053 | } 1054 | } 1055 | } 1056 | }), (err, result) => { 1057 | expect(err).toBeFalsy(); 1058 | expect(JSON.stringify(result.compilation.errors)).toBe('[]'); 1059 | expect(JSON.stringify(result.compilation.options.externals)).toBe('{"bootstrap":"Bootstrap"}'); 1060 | 1061 | expect(areEqualDirectories('../node_modules/bootstrap/dist/css', `${OUTPUT_DIR}/packages/bootstrap-${BOOTSTRAP_VERSION}/css`)).toBe(true); 1062 | expect(areEqualDirectories('../node_modules/bootstrap/dist/js', `${OUTPUT_DIR}/packages/bootstrap-${BOOTSTRAP_VERSION}/js`)).toBe(true); 1063 | 1064 | cheerioLoadTags(OUPUT_HTML_FILE, ({ links, scripts }) => { 1065 | expect(links.length).toBe(2); 1066 | expect(scripts.length).toBe(3); 1067 | expect(links).toContainTag({ tagName: 'link', attributes: { href: 'style.css', rel: 'stylesheet' } }); 1068 | expect(links[1]).toBeTag({ tagName: 'link', attributes: { href: `packages/bootstrap-${BOOTSTRAP_VERSION}/css/bootstrap.min.css`, rel: 'stylesheet' } }); 1069 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'app.js' } }); 1070 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'style.js' } }); 1071 | expect(scripts[0]).toBeTag({ tagName: 'script', attributes: { src: `packages/bootstrap-${BOOTSTRAP_VERSION}/js/bootstrap.bundle.min.js` } }); 1072 | 1073 | done(); 1074 | }); 1075 | }); 1076 | }); 1077 | 1078 | it('injects cdn urls when useCdn is true', done => { 1079 | webpack(createWebpackConfig({ 1080 | options: { 1081 | packages: { 1082 | bootstrap: { 1083 | copy: [ 1084 | { from: 'dist/css', to: 'css/' }, 1085 | { from: 'dist/js', to: 'js/' } 1086 | ], 1087 | links: [ 1088 | 'css/bootstrap.min.css' 1089 | ], 1090 | scripts: { 1091 | // variableName: 'Bootstrap', 1092 | path: 'js/bootstrap.bundle.min.js' 1093 | } 1094 | } 1095 | }, 1096 | useCdn: true 1097 | } 1098 | }), (err, result) => { 1099 | expect(err).toBeFalsy(); 1100 | expect(JSON.stringify(result.compilation.errors)).toBe('[]'); 1101 | expect(areEqualDirectories('../node_modules/bootstrap/dist/css', `${OUTPUT_DIR}/packages/bootstrap-${BOOTSTRAP_VERSION}/css`)).toBe(true); 1102 | expect(areEqualDirectories('../node_modules/bootstrap/dist/js', `${OUTPUT_DIR}/packages/bootstrap-${BOOTSTRAP_VERSION}/js`)).toBe(true); 1103 | // expect(JSON.stringify(result.compilation.options.externals)).toBe('{"bootstrap":"Bootstrap"}'); 1104 | 1105 | cheerioLoadTags(OUPUT_HTML_FILE, ({ links, scripts }) => { 1106 | expect(links.length).toBe(2); 1107 | expect(scripts.length).toBe(3); 1108 | expect(links).toContainTag({ tagName: 'link', attributes: { href: 'style.css', rel: 'stylesheet' } }); 1109 | expect(links[1]).toBeTag({ tagName: 'link', attributes: { href: `https://unpkg.com/bootstrap@${BOOTSTRAP_VERSION}/css/bootstrap.min.css`, rel: 'stylesheet' } }); 1110 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'app.js' } }); 1111 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'style.js' } }); 1112 | expect(scripts[2]).toBeTag({ tagName: 'script', attributes: { src: `https://unpkg.com/bootstrap@${BOOTSTRAP_VERSION}/js/bootstrap.bundle.min.js` } }); 1113 | 1114 | done(); 1115 | }); 1116 | }); 1117 | }); 1118 | 1119 | it('injects cdn urls when useCdn is true but not for tags that have useCdn false', done => { 1120 | webpack(createWebpackConfig({ 1121 | options: { 1122 | packages: { 1123 | bootstrap: { 1124 | links: [ 1125 | 'link-a', 1126 | { 1127 | path: 'link-b', 1128 | useCdn: false 1129 | } 1130 | ], 1131 | scripts: [ 1132 | { 1133 | path: 'script-a', 1134 | useCdn: false 1135 | }, 1136 | 'script-b' 1137 | ] 1138 | } 1139 | }, 1140 | getCdnPath: (packageName, packageVersion, packagePath) => `http://mydomain.com/${packageName}@${packageVersion}/${packagePath}`, 1141 | useCdn: true 1142 | } 1143 | }), (err, result) => { 1144 | expect(err).toBeFalsy(); 1145 | expect(JSON.stringify(result.compilation.errors)).toBe('[]'); 1146 | 1147 | cheerioLoadTags(OUPUT_HTML_FILE, ({ links, scripts }) => { 1148 | expect(links.length).toBe(3); 1149 | expect(scripts.length).toBe(4); 1150 | expect(links).toContainTag({ tagName: 'link', attributes: { href: 'style.css', rel: 'stylesheet' } }); 1151 | expect(links[1]).toBeTag({ tagName: 'link', attributes: { href: `http://mydomain.com/bootstrap@${BOOTSTRAP_VERSION}/link-a`, rel: 'stylesheet' } }); 1152 | expect(links[2]).toBeTag({ tagName: 'link', attributes: { href: `packages/bootstrap-${BOOTSTRAP_VERSION}/link-b`, rel: 'stylesheet' } }); 1153 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'app.js' } }); 1154 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'style.js' } }); 1155 | expect(scripts[2]).toBeTag({ tagName: 'script', attributes: { src: `packages/bootstrap-${BOOTSTRAP_VERSION}/script-a` } }); 1156 | expect(scripts[3]).toBeTag({ tagName: 'script', attributes: { src: `http://mydomain.com/bootstrap@${BOOTSTRAP_VERSION}/script-b` } }); 1157 | 1158 | done(); 1159 | }); 1160 | }); 1161 | }); 1162 | 1163 | it('injects cdn urls when useCdn is true or false at the package or tag level', done => { 1164 | webpack(createWebpackConfig({ 1165 | options: { 1166 | packages: { 1167 | bootstrap: { 1168 | links: [ 1169 | 'link-a', 1170 | { 1171 | path: 'link-b', 1172 | useCdn: false 1173 | } 1174 | ], 1175 | scripts: [ 1176 | { 1177 | path: 'script-a', 1178 | useCdn: false 1179 | }, 1180 | 'script-b' 1181 | ] 1182 | }, 1183 | bulma: { 1184 | links: [ 1185 | 'link-c', 1186 | { 1187 | path: 'link-d', 1188 | useCdn: true 1189 | } 1190 | ], 1191 | scripts: [ 1192 | { 1193 | path: 'script-c', 1194 | useCdn: true 1195 | }, 1196 | 'script-d' 1197 | ], 1198 | useCdn: false 1199 | } 1200 | }, 1201 | getCdnPath: (packageName, packageVersion, packagePath) => `http://mydomain.com/${packageName}@${packageVersion}/${packagePath}`, 1202 | useCdn: true 1203 | } 1204 | }), (err, result) => { 1205 | expect(err).toBeFalsy(); 1206 | expect(JSON.stringify(result.compilation.errors)).toBe('[]'); 1207 | 1208 | cheerioLoadTags(OUPUT_HTML_FILE, ({ links, scripts }) => { 1209 | expect(links.length).toBe(5); 1210 | expect(scripts.length).toBe(6); 1211 | expect(links).toContainTag({ tagName: 'link', attributes: { href: 'style.css', rel: 'stylesheet' } }); 1212 | expect(links[1]).toBeTag({ tagName: 'link', attributes: { href: `http://mydomain.com/bootstrap@${BOOTSTRAP_VERSION}/link-a`, rel: 'stylesheet' } }); 1213 | expect(links[2]).toBeTag({ tagName: 'link', attributes: { href: `packages/bootstrap-${BOOTSTRAP_VERSION}/link-b`, rel: 'stylesheet' } }); 1214 | expect(links[3]).toBeTag({ tagName: 'link', attributes: { href: `packages/bulma-${BULMA_VERSION}/link-c`, rel: 'stylesheet' } }); 1215 | expect(links[4]).toBeTag({ tagName: 'link', attributes: { href: `http://mydomain.com/bulma@${BULMA_VERSION}/link-d`, rel: 'stylesheet' } }); 1216 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'app.js' } }); 1217 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'style.js' } }); 1218 | expect(scripts[2]).toBeTag({ tagName: 'script', attributes: { src: `packages/bootstrap-${BOOTSTRAP_VERSION}/script-a` } }); 1219 | expect(scripts[3]).toBeTag({ tagName: 'script', attributes: { src: `http://mydomain.com/bootstrap@${BOOTSTRAP_VERSION}/script-b` } }); 1220 | expect(scripts[4]).toBeTag({ tagName: 'script', attributes: { src: `http://mydomain.com/bulma@${BULMA_VERSION}/script-c` } }); 1221 | expect(scripts[5]).toBeTag({ tagName: 'script', attributes: { src: `packages/bulma-${BULMA_VERSION}/script-d` } }); 1222 | 1223 | done(); 1224 | }); 1225 | }); 1226 | }); 1227 | 1228 | it('injects cdn urls with a custom getCdnPath', done => { 1229 | webpack(createWebpackConfig({ 1230 | options: { 1231 | packages: { 1232 | bootstrap: { 1233 | copy: [ 1234 | { from: 'dist/css', to: 'css/' }, 1235 | { from: 'dist/js', to: 'js/' } 1236 | ], 1237 | links: [ 1238 | 'css/bootstrap.min.css' 1239 | ], 1240 | scripts: { 1241 | // variableName: 'Bootstrap', 1242 | path: 'js/bootstrap.bundle.min.js' 1243 | } 1244 | } 1245 | }, 1246 | useCdn: true, 1247 | getCdnPath: (packageName, packageVersion, packagePath) => `https://mydomain.com/${packageName}@${packageVersion}/${packagePath}` 1248 | } 1249 | }), (err, result) => { 1250 | expect(err).toBeFalsy(); 1251 | expect(JSON.stringify(result.compilation.errors)).toBe('[]'); 1252 | // expect(JSON.stringify(result.compilation.options.externals)).toBe('{"bootstrap":"Bootstrap"}'); 1253 | 1254 | cheerioLoadTags(OUPUT_HTML_FILE, ({ links, scripts }) => { 1255 | expect(links.length).toBe(2); 1256 | expect(scripts.length).toBe(3); 1257 | expect(links).toContainTag({ tagName: 'link', attributes: { href: 'style.css', rel: 'stylesheet' } }); 1258 | expect(links[1]).toBeTag({ tagName: 'link', attributes: { href: `https://mydomain.com/bootstrap@${BOOTSTRAP_VERSION}/css/bootstrap.min.css`, rel: 'stylesheet' } }); 1259 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'app.js' } }); 1260 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'style.js' } }); 1261 | expect(scripts[2]).toBeTag({ tagName: 'script', attributes: { src: `https://mydomain.com/bootstrap@${BOOTSTRAP_VERSION}/js/bootstrap.bundle.min.js` } }); 1262 | 1263 | done(); 1264 | }); 1265 | }); 1266 | }); 1267 | 1268 | it('injects any specified cdnPath', done => { 1269 | webpack(createWebpackConfig({ 1270 | options: { 1271 | useCdn: true, 1272 | getCdnPath: (packageName, packageVersion, packagePath) => `http://abc.com/${packageName}@${packageVersion}/${packagePath}`, 1273 | packages: { 1274 | bootstrap: { 1275 | links: [ 1276 | 'style-a.css', 1277 | { 1278 | path: 'style-b.css', 1279 | cdnPath: 'cdn-style-b.css' 1280 | } 1281 | ], 1282 | scripts: { 1283 | path: 'script-a.js', 1284 | cdnPath: 'cdn-script-a.js' 1285 | } 1286 | } 1287 | } 1288 | } 1289 | }), (err, result) => { 1290 | expect(err).toBeFalsy(); 1291 | expect(JSON.stringify(result.compilation.errors)).toBe('[]'); 1292 | // expect(JSON.stringify(result.compilation.options.externals)).toBe('{"bootstrap":"Bootstrap"}'); 1293 | 1294 | cheerioLoadTags(OUPUT_HTML_FILE, ({ links, scripts }) => { 1295 | expect(links.length).toBe(3); 1296 | expect(scripts.length).toBe(3); 1297 | expect(links).toContainTag({ tagName: 'link', attributes: { href: 'style.css', rel: 'stylesheet' } }); 1298 | expect(links[1]).toBeTag({ tagName: 'link', attributes: { href: `http://abc.com/bootstrap@${BOOTSTRAP_VERSION}/style-a.css`, rel: 'stylesheet' } }); 1299 | expect(links[2]).toBeTag({ tagName: 'link', attributes: { href: `http://abc.com/bootstrap@${BOOTSTRAP_VERSION}/cdn-style-b.css`, rel: 'stylesheet' } }); 1300 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'app.js' } }); 1301 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'style.js' } }); 1302 | expect(scripts[2]).toBeTag({ tagName: 'script', attributes: { src: `http://abc.com/bootstrap@${BOOTSTRAP_VERSION}/cdn-script-a.js` } }); 1303 | 1304 | done(); 1305 | }); 1306 | }); 1307 | }); 1308 | 1309 | it('uses a custom findNodeModulesPath option', done => { 1310 | webpack(createWebpackConfig({ 1311 | options: { 1312 | packages: { 1313 | bootstrap: { 1314 | copy: [{ 1315 | from: 'dist/css', to: 'css/' 1316 | }], 1317 | links: [ 1318 | 'css/bootstrap.min.css' 1319 | ] 1320 | } 1321 | }, 1322 | findNodeModulesPath: (cwd, packageName) => path.join(FIXTURES_PATH, 'node_modules', packageName) 1323 | } 1324 | }), (err, result) => { 1325 | expect(err).toBeFalsy(); 1326 | expect(JSON.stringify(result.compilation.errors)).toBe('[]'); 1327 | 1328 | cheerioLoadTags(OUPUT_HTML_FILE, ({ links, scripts }) => { 1329 | expect(links.length).toBe(2); 1330 | expect(scripts.length).toBe(2); 1331 | expect(links).toContainTag({ tagName: 'link', attributes: { href: 'style.css', rel: 'stylesheet' } }); 1332 | expect(links[1]).toBeTag({ tagName: 'link', attributes: { href: 'packages/bootstrap-fake-version/css/bootstrap.min.css', rel: 'stylesheet' } }); 1333 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'app.js' } }); 1334 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'style.js' } }); 1335 | 1336 | done(); 1337 | }); 1338 | }); 1339 | }); 1340 | 1341 | it('uses a custom getPackagePath option', done => { 1342 | webpack(createWebpackConfig({ 1343 | options: { 1344 | packages: { 1345 | bootstrap: { 1346 | copy: [{ 1347 | from: 'dist/css', to: 'css/' 1348 | }], 1349 | links: [ 1350 | 'css/bootstrap.min.css' 1351 | ] 1352 | } 1353 | }, 1354 | getPackagePath: (packageName, packageVersion, packagePath) => path.join(packageName + '---' + packageVersion + '---', packagePath) 1355 | } 1356 | }), (err, result) => { 1357 | expect(err).toBeFalsy(); 1358 | expect(JSON.stringify(result.compilation.errors)).toBe('[]'); 1359 | 1360 | cheerioLoadTags(OUPUT_HTML_FILE, ({ links, scripts }) => { 1361 | expect(links.length).toBe(2); 1362 | expect(scripts.length).toBe(2); 1363 | expect(links).toContainTag({ tagName: 'link', attributes: { href: 'style.css', rel: 'stylesheet' } }); 1364 | expect(links[1]).toBeTag({ tagName: 'link', attributes: { href: `packages/bootstrap---${BOOTSTRAP_VERSION}---/css/bootstrap.min.css`, rel: 'stylesheet' } }); 1365 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'app.js' } }); 1366 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'style.js' } }); 1367 | 1368 | done(); 1369 | }); 1370 | }); 1371 | }); 1372 | 1373 | it('applies devPath in development mode', done => { 1374 | webpack(createWebpackConfig({ 1375 | webpackMode: 'development', 1376 | options: { 1377 | packages: { 1378 | bootstrap: { 1379 | copy: [{ 1380 | from: 'dist/css', to: 'css/' 1381 | }], 1382 | links: { 1383 | path: 'css/bootstrap.min.css', 1384 | devPath: 'css/bootstrap.css' 1385 | } 1386 | } 1387 | } 1388 | } 1389 | }), (err, result) => { 1390 | expect(err).toBeFalsy(); 1391 | expect(JSON.stringify(result.compilation.errors)).toBe('[]'); 1392 | 1393 | cheerioLoadTags(OUPUT_HTML_FILE, ({ links, scripts }) => { 1394 | expect(links.length).toBe(2); 1395 | expect(scripts.length).toBe(2); 1396 | expect(links).toContainTag({ tagName: 'link', attributes: { href: 'style.css', rel: 'stylesheet' } }); 1397 | expect(links[1]).toBeTag({ tagName: 'link', attributes: { href: `packages/bootstrap-${BOOTSTRAP_VERSION}/css/bootstrap.css`, rel: 'stylesheet' } }); 1398 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'app.js' } }); 1399 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'style.js' } }); 1400 | 1401 | done(); 1402 | }); 1403 | }); 1404 | }); 1405 | 1406 | it('does not apply devPath for a tag when useCdn for the tag is true', done => { 1407 | webpack(createWebpackConfig({ 1408 | webpackMode: 'development', 1409 | options: { 1410 | packages: { 1411 | bootstrap: { 1412 | copy: [{ 1413 | from: 'dist/css', to: 'css/' 1414 | }], 1415 | links: { 1416 | path: 'css/bootstrap.min.css', 1417 | devPath: 'css/bootstrap.css' 1418 | }, 1419 | scripts: { 1420 | path: 'js/bootstrap.min.js', 1421 | devPath: 'js/bootstrap.js' 1422 | }, 1423 | useCdn: true, 1424 | getCdnPath: (packageName, packageVersion, packagePath) => `http://mydomain.com/${packageName}@${packageVersion}/${packagePath}` 1425 | } 1426 | } 1427 | } 1428 | }), (err, result) => { 1429 | expect(err).toBeFalsy(); 1430 | expect(JSON.stringify(result.compilation.errors)).toBe('[]'); 1431 | 1432 | cheerioLoadTags(OUPUT_HTML_FILE, ({ links, scripts }) => { 1433 | expect(links.length).toBe(2); 1434 | expect(scripts.length).toBe(3); 1435 | expect(links).toContainTag({ tagName: 'link', attributes: { href: 'style.css', rel: 'stylesheet' } }); 1436 | expect(links[1]).toBeTag({ tagName: 'link', attributes: { href: `http://mydomain.com/bootstrap@${BOOTSTRAP_VERSION}/css/bootstrap.min.css`, rel: 'stylesheet' } }); 1437 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'app.js' } }); 1438 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'style.js' } }); 1439 | expect(scripts[2]).toBeTag({ tagName: 'script', attributes: { src: `http://mydomain.com/bootstrap@${BOOTSTRAP_VERSION}/js/bootstrap.min.js` } }); 1440 | 1441 | done(); 1442 | }); 1443 | }); 1444 | }); 1445 | }); 1446 | 1447 | describe('assets', () => { 1448 | it('it copies files in the assets copy options', done => { 1449 | webpack(createWebpackConfig({ 1450 | options: { 1451 | assets: { 1452 | copy: [{ from: 'spec/fixtures/assets/foo.js', to: 'foo.js' }] 1453 | } 1454 | } 1455 | }), (err, result) => { 1456 | expect(err).toBeFalsy(); 1457 | expect(JSON.stringify(result.compilation.errors)).toBe('[]'); 1458 | expect(areEqualDirectories('fixtures/assets', `${OUTPUT_DIR}/assets`, { files: ['foo.js'] })).toBe(true); 1459 | done(); 1460 | }); 1461 | }); 1462 | 1463 | it('it includes assets links', done => { 1464 | webpack(createWebpackConfig({ 1465 | webpackPublicPath: '/public-path/', 1466 | options: { 1467 | assets: { 1468 | links: [ 1469 | { 1470 | path: 'the-href', 1471 | attributes: { 1472 | rel: 'the-rel' 1473 | } 1474 | } 1475 | ] 1476 | } 1477 | } 1478 | }), (err, result) => { 1479 | expect(err).toBeFalsy(); 1480 | expect(JSON.stringify(result.compilation.errors)).toBe('[]'); 1481 | 1482 | cheerioLoadTags(OUPUT_HTML_FILE, ({ links, scripts }) => { 1483 | expect(links.length).toBe(2); 1484 | expect(scripts.length).toBe(2); 1485 | expect(links).toContainTag({ tagName: 'link', attributes: { href: '/public-path/style.css', rel: 'stylesheet' } }); 1486 | expect(links[1]).toBeTag({ tagName: 'link', attributes: { href: '/public-path/assets/the-href', rel: 'the-rel' } }); 1487 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: '/public-path/app.js' } }); 1488 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: '/public-path/style.js' } }); 1489 | 1490 | done(); 1491 | }); 1492 | }); 1493 | }); 1494 | 1495 | it('it includes assets scripts', done => { 1496 | webpack(createWebpackConfig({ 1497 | webpackPublicPath: '/public-path/', 1498 | options: { 1499 | assets: { 1500 | scripts: [ 1501 | { 1502 | path: 'the-src', 1503 | publicPath: true 1504 | }, 1505 | { 1506 | path: 'the-src2', 1507 | publicPath: false 1508 | } 1509 | ] 1510 | } 1511 | } 1512 | }), (err, result) => { 1513 | expect(err).toBeFalsy(); 1514 | expect(JSON.stringify(result.compilation.errors)).toBe('[]'); 1515 | 1516 | cheerioLoadTags(OUPUT_HTML_FILE, ({ links, scripts }) => { 1517 | expect(links.length).toBe(1); 1518 | expect(scripts.length).toBe(4); 1519 | expect(links).toContainTag({ tagName: 'link', attributes: { href: '/public-path/style.css', rel: 'stylesheet' } }); 1520 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: '/public-path/app.js' } }); 1521 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: '/public-path/style.js' } }); 1522 | expect(scripts[2]).toBeTag({ tagName: 'script', attributes: { src: '/public-path/assets/the-src' } }); 1523 | expect(scripts[3]).toBeTag({ tagName: 'script', attributes: { src: 'assets/the-src2' } }); 1524 | 1525 | done(); 1526 | }); 1527 | }); 1528 | }); 1529 | 1530 | it('it uses a custom addAssetsPath option ', done => { 1531 | webpack(createWebpackConfig({ 1532 | webpackPublicPath: '/public-path/', 1533 | options: { 1534 | assets: { 1535 | links: [ 1536 | { 1537 | path: 'the-href', 1538 | attributes: { 1539 | rel: 'the-rel' 1540 | } 1541 | } 1542 | ] 1543 | }, 1544 | addAssetsPath: assetPath => path.join('my-assets', assetPath) 1545 | } 1546 | }), (err, result) => { 1547 | expect(err).toBeFalsy(); 1548 | expect(JSON.stringify(result.compilation.errors)).toBe('[]'); 1549 | 1550 | cheerioLoadTags(OUPUT_HTML_FILE, ({ links, scripts }) => { 1551 | expect(links.length).toBe(2); 1552 | expect(scripts.length).toBe(2); 1553 | expect(links).toContainTag({ tagName: 'link', attributes: { href: '/public-path/style.css', rel: 'stylesheet' } }); 1554 | expect(links[1]).toBeTag({ tagName: 'link', attributes: { href: '/public-path/my-assets/the-href', rel: 'the-rel' } }); 1555 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: '/public-path/app.js' } }); 1556 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: '/public-path/style.js' } }); 1557 | 1558 | done(); 1559 | }); 1560 | }); 1561 | }); 1562 | 1563 | it('applies devPath in development mode', done => { 1564 | webpack(createWebpackConfig({ 1565 | webpackMode: 'development', 1566 | options: { 1567 | assets: { 1568 | copy: [{ 1569 | from: path.join(FIXTURES_PATH, 'assets'), to: 'my-assets/' 1570 | }], 1571 | links: { 1572 | path: 'my-assets/foo.min.css', 1573 | devPath: 'my-assets/foo.css' 1574 | }, 1575 | scripts: { 1576 | path: 'my-assets/foo.min.js', 1577 | devPath: 'my-assets/foo.js' 1578 | } 1579 | } 1580 | } 1581 | }), (err, result) => { 1582 | expect(err).toBeFalsy(); 1583 | expect(JSON.stringify(result.compilation.errors)).toBe('[]'); 1584 | 1585 | cheerioLoadTags(OUPUT_HTML_FILE, ({ links, scripts }) => { 1586 | expect(links.length).toBe(2); 1587 | expect(scripts.length).toBe(3); 1588 | expect(links).toContainTag({ tagName: 'link', attributes: { href: 'style.css', rel: 'stylesheet' } }); 1589 | expect(links[1]).toBeTag({ tagName: 'link', attributes: { href: 'assets/my-assets/foo.css', rel: 'stylesheet' } }); 1590 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'app.js' } }); 1591 | expect(scripts).toContainTag({ tagName: 'script', attributes: { src: 'style.js' } }); 1592 | expect(scripts[2]).toBeTag({ tagName: 'script', attributes: { src: 'assets/my-assets/foo.js' } }); 1593 | 1594 | done(); 1595 | }); 1596 | }); 1597 | }); 1598 | }); 1599 | }); 1600 | -------------------------------------------------------------------------------- /spec/fixtures/a-file: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jharris4/html-webpack-deploy-plugin/0d995b1a4ed20482737ab04408312d44c334fa9f/spec/fixtures/a-file -------------------------------------------------------------------------------- /spec/fixtures/app.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jharris4/html-webpack-deploy-plugin/0d995b1a4ed20482737ab04408312d44c334fa9f/spec/fixtures/app.css -------------------------------------------------------------------------------- /spec/fixtures/assets/foo.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jharris4/html-webpack-deploy-plugin/0d995b1a4ed20482737ab04408312d44c334fa9f/spec/fixtures/assets/foo.css -------------------------------------------------------------------------------- /spec/fixtures/assets/foo.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jharris4/html-webpack-deploy-plugin/0d995b1a4ed20482737ab04408312d44c334fa9f/spec/fixtures/assets/foo.js -------------------------------------------------------------------------------- /spec/fixtures/assets/foo.min.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jharris4/html-webpack-deploy-plugin/0d995b1a4ed20482737ab04408312d44c334fa9f/spec/fixtures/assets/foo.min.css -------------------------------------------------------------------------------- /spec/fixtures/assets/foo.min.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jharris4/html-webpack-deploy-plugin/0d995b1a4ed20482737ab04408312d44c334fa9f/spec/fixtures/assets/foo.min.js -------------------------------------------------------------------------------- /spec/fixtures/entry.js: -------------------------------------------------------------------------------- 1 | var aVar = 'aValue'; -------------------------------------------------------------------------------- /spec/fixtures/node_modules/bad-package/package.json: -------------------------------------------------------------------------------- 1 | "bad non object content" -------------------------------------------------------------------------------- /spec/fixtures/node_modules/bootstrap/dist/css/bootstrap.min.css: -------------------------------------------------------------------------------- 1 | /* fake style */ -------------------------------------------------------------------------------- /spec/fixtures/node_modules/bootstrap/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bootstrap", 3 | "version": "fake-version" 4 | } -------------------------------------------------------------------------------- /spec/fixtures/node_modules/fake-a-package/fake-a-bundle.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : 3 | typeof define === 'function' && define.amd ? define(['exports'], factory) : 4 | (global = global || self, factory(global.FakeA = {})); 5 | }(this, function (exports) { 'use strict'; 6 | 7 | function fakeA() { 8 | return '% external fakeA %'; 9 | } 10 | 11 | exports.fakeA = fakeA; 12 | 13 | Object.defineProperty(exports, '__esModule', { value: true }); 14 | 15 | })); 16 | //# sourceMappingURL=fake-a-bundle.js.map 17 | -------------------------------------------------------------------------------- /spec/fixtures/node_modules/fake-a-package/fake-a-entry.js: -------------------------------------------------------------------------------- 1 | function fakeA () { 2 | return '% webpack fakeA %'; 3 | } 4 | 5 | module.exports = { 6 | fakeA: fakeA 7 | }; 8 | -------------------------------------------------------------------------------- /spec/fixtures/node_modules/fake-a-package/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fake-a-package", 3 | "version": "1.0.0", 4 | "main": "fake-a-entry.js" 5 | } 6 | -------------------------------------------------------------------------------- /spec/fixtures/node_modules/fake-b-package/fake-b-bundle.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : 3 | typeof define === 'function' && define.amd ? define(['exports'], factory) : 4 | (global = global || self, factory(global.FakeB = {})); 5 | }(this, function (exports) { 'use strict'; 6 | 7 | function fakeB() { 8 | return '% external fakeB %'; 9 | } 10 | 11 | exports.fakeB = fakeB; 12 | 13 | Object.defineProperty(exports, '__esModule', { value: true }); 14 | 15 | })); 16 | //# sourceMappingURL=fake-b-bundle.js.map 17 | -------------------------------------------------------------------------------- /spec/fixtures/node_modules/fake-b-package/fake-b-entry.js: -------------------------------------------------------------------------------- 1 | function fakeB () { 2 | return '% webpack fakeB %'; 3 | } 4 | 5 | module.exports = { 6 | fakeB: fakeB 7 | }; 8 | -------------------------------------------------------------------------------- /spec/fixtures/node_modules/fake-b-package/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fake-b-package", 3 | "version": "1.0.0", 4 | "main": "fake-b-entry.js" 5 | } 6 | -------------------------------------------------------------------------------- /spec/fixtures/node_modules/fake-c-package/fake-c-bundle.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('fake-b-package')) : 3 | typeof define === 'function' && define.amd ? define(['exports'], factory) : 4 | (global = global || self, factory(global.FakeC = {}, global.FakeB)); 5 | }(this, function (exports, FakeB) { 'use strict'; 6 | 7 | // var FakeB__default = 'default' in FakeB ? FakeB['default'] : FakeB; 8 | 9 | function fakeC() { 10 | return '% external fakeC % - depends on - ' + FakeB.fakeB(); 11 | } 12 | 13 | exports.fakeC = fakeC; 14 | 15 | Object.defineProperty(exports, '__esModule', { value: true }); 16 | 17 | })); 18 | // # sourceMappingURL=fake-c-bundle.js.map 19 | -------------------------------------------------------------------------------- /spec/fixtures/node_modules/fake-c-package/fake-c-entry.js: -------------------------------------------------------------------------------- 1 | const { fakeB } = require('fake-b-package'); 2 | 3 | function fakeC () { 4 | return '% webpack fakeC % - depends on - ' + fakeB(); 5 | } 6 | 7 | module.exports = { 8 | fakeC: fakeC 9 | }; 10 | -------------------------------------------------------------------------------- /spec/fixtures/node_modules/fake-c-package/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fake-c-package", 3 | "version": "1.0.0", 4 | "main": "fake-c-entry.js", 5 | "dependencies": { 6 | "fake-b-package": "1.0.0" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /spec/fixtures/node_modules/fake-other-package/fake-other-bundle.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : 3 | typeof define === 'function' && define.amd ? define(['exports'], factory) : 4 | (global = global || self, factory(global.FakeOther = {})); 5 | }(this, function (exports) { 'use strict'; 6 | 7 | const rootElement = document.getElementById('external-root'); 8 | 9 | rootElement.innerHTML = '
% fakeOther %
'; 10 | 11 | })); 12 | //# sourceMappingURL=fake-b-bundle.js.map 13 | -------------------------------------------------------------------------------- /spec/fixtures/node_modules/fake-other-package/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fake-other-package", 3 | "version": "1.0.0", 4 | "main": "fake-other-bundle.js" 5 | } 6 | -------------------------------------------------------------------------------- /spec/fixtures/node_modules/no-version/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "no-version" 3 | } -------------------------------------------------------------------------------- /spec/fixtures/node_modules/the-package/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "the-package", 3 | "version": "1.2.3" 4 | } -------------------------------------------------------------------------------- /spec/option_validation.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jasmine */ 2 | const path = require('path'); 3 | require('jasmine-expect'); 4 | 5 | const HtmlWebpackDeployPlugin = require('../'); 6 | 7 | const FIXTURES_PATH = path.join(__dirname, './fixtures'); 8 | 9 | describe('option validation', () => { 10 | describe('options', () => { 11 | it('should throw an error if no options are provided', done => { 12 | const theFunction = () => { 13 | return new HtmlWebpackDeployPlugin(); 14 | }; 15 | 16 | expect(theFunction).toThrowError(/(options should be an object)/); 17 | done(); 18 | }); 19 | 20 | it('should throw an error if the options are not an object', done => { 21 | const theFunction = () => { 22 | return new HtmlWebpackDeployPlugin('hello'); 23 | }; 24 | 25 | expect(theFunction).toThrowError(/(options should be an object)/); 26 | done(); 27 | }); 28 | 29 | it('should not throw an error if the options is an empty object', done => { 30 | const theFunction = () => { 31 | return new HtmlWebpackDeployPlugin({}); 32 | }; 33 | 34 | expect(theFunction).not.toThrowError(); 35 | done(); 36 | }); 37 | }); 38 | 39 | describe('options.assets', () => { 40 | const savedCwd = process.cwd(); 41 | beforeEach(done => { 42 | process.chdir(path.join(savedCwd, 'spec', 'fixtures')); 43 | done(); 44 | }); 45 | 46 | afterEach(done => { 47 | process.chdir(savedCwd); 48 | done(); 49 | }); 50 | 51 | it('should throw an error if the assets is not an object', done => { 52 | const theFunction = () => { 53 | return new HtmlWebpackDeployPlugin({ assets: '123' }); 54 | }; 55 | expect(theFunction).toThrowError(/(options\.assets should be an object)/); 56 | done(); 57 | }); 58 | 59 | it('should not throw an error if the assets is an empty object', done => { 60 | const theFunction = () => { 61 | return new HtmlWebpackDeployPlugin({ assets: {} }); 62 | }; 63 | expect(theFunction).not.toThrowError(); 64 | done(); 65 | }); 66 | 67 | it('should not throw an error if there are assets with empty copy array', done => { 68 | const theFunction = () => { 69 | return new HtmlWebpackDeployPlugin({ assets: { copy: [] } }); 70 | }; 71 | expect(theFunction).not.toThrowError(); 72 | done(); 73 | }); 74 | 75 | it('should throw an error if there are assets with copy that is not an array or object', done => { 76 | const theFunction = () => { 77 | return new HtmlWebpackDeployPlugin({ assets: { copy: '123' } }); 78 | }; 79 | expect(theFunction).toThrowError(/(options.assets.copy should be an object or array of objects)/); 80 | done(); 81 | }); 82 | 83 | it('should throw an error if there are assets with copy that is an object with non string from', done => { 84 | const theFunction = () => { 85 | return new HtmlWebpackDeployPlugin({ assets: { copy: { from: 123, to: 'dest' } } }); 86 | }; 87 | expect(theFunction).toThrowError(/(options.assets.copy should be an object with string properties from & to)/); 88 | done(); 89 | }); 90 | 91 | it('should throw an error if there are assets with copy that is an object with non string to', done => { 92 | const theFunction = () => { 93 | return new HtmlWebpackDeployPlugin({ assets: { copy: { from: 'src', to: 123 } } }); 94 | }; 95 | expect(theFunction).toThrowError(/(options.assets.copy should be an object with string properties from & to)/); 96 | done(); 97 | }); 98 | 99 | it('should not throw an error for an assets with copy that is an object with string from & to', done => { 100 | const theFunction = () => { 101 | return new HtmlWebpackDeployPlugin({ assets: { copy: { from: 'src', to: 'dest' } } }); 102 | }; 103 | expect(theFunction).not.toThrowError(); 104 | done(); 105 | }); 106 | 107 | runTestsForOption(['assets', 'links']); 108 | runTestsForOption(['assets', 'scripts']); 109 | 110 | it('should throw an error for assets with scripts that have a non-string devPath', done => { 111 | const theFunction = () => { 112 | return new HtmlWebpackDeployPlugin({ assets: { scripts: { path: 'a-path', devPath: 123 } } }); 113 | }; 114 | expect(theFunction).toThrowError(/(options.assets.scripts.devPath should be a string)/); 115 | done(); 116 | }); 117 | 118 | it('should not throw an error for assets with scripts that have a string devPath', done => { 119 | const theFunction = () => { 120 | return new HtmlWebpackDeployPlugin({ assets: { scripts: { path: 'a-path', devPath: 'dev-path' } } }); 121 | }; 122 | expect(theFunction).not.toThrowError(); 123 | done(); 124 | }); 125 | 126 | it('should throw an error for assets with links that have a non-string devPath', done => { 127 | const theFunction = () => { 128 | return new HtmlWebpackDeployPlugin({ assets: { links: { path: 'a-path', devPath: 123 } } }); 129 | }; 130 | expect(theFunction).toThrowError(/(options.assets.links.devPath should be a string)/); 131 | done(); 132 | }); 133 | 134 | it('should not throw an error for assets with links that have a string devPath', done => { 135 | const theFunction = () => { 136 | return new HtmlWebpackDeployPlugin({ assets: { links: { path: 'a-path', devPath: 'dev-path' } } }); 137 | }; 138 | expect(theFunction).not.toThrowError(); 139 | done(); 140 | }); 141 | }); 142 | 143 | describe('options.packages', () => { 144 | const savedCwd = process.cwd(); 145 | beforeEach(done => { 146 | process.chdir(path.join(savedCwd, 'spec', 'fixtures')); 147 | done(); 148 | }); 149 | 150 | afterEach(done => { 151 | process.chdir(savedCwd); 152 | done(); 153 | }); 154 | 155 | it('should throw an error if the packages is not an object', done => { 156 | const theFunction = () => { 157 | return new HtmlWebpackDeployPlugin({ packages: '123' }); 158 | }; 159 | expect(theFunction).toThrowError(/(options\.packages should be an object)/); 160 | done(); 161 | }); 162 | 163 | it('should throw an error if any of the packages is not an object', done => { 164 | const theFunction = () => { 165 | return new HtmlWebpackDeployPlugin({ packages: { 'the-package': 'abc' } }); 166 | }; 167 | expect(theFunction).toThrowError(/(options\.packages.the-package should be an object)/); 168 | done(); 169 | }); 170 | 171 | it('should not throw an error if the packages is an empty object', done => { 172 | const theFunction = () => { 173 | return new HtmlWebpackDeployPlugin({ packages: {} }); 174 | }; 175 | expect(theFunction).not.toThrowError(); 176 | done(); 177 | }); 178 | 179 | it('should not throw an error if there is an empty object package', done => { 180 | const theFunction = () => { 181 | return new HtmlWebpackDeployPlugin({ packages: { 'the-package': {} } }); 182 | }; 183 | expect(theFunction).not.toThrowError(); 184 | done(); 185 | }); 186 | 187 | it('should not throw an error if there is an object package with empty copy array', done => { 188 | const theFunction = () => { 189 | return new HtmlWebpackDeployPlugin({ packages: { 'the-package': { copy: [] } } }); 190 | }; 191 | expect(theFunction).not.toThrowError(); 192 | done(); 193 | }); 194 | 195 | it('should throw an error if there is a package with copy that is not an array or object', done => { 196 | const theFunction = () => { 197 | return new HtmlWebpackDeployPlugin({ packages: { 'the-package': { copy: '123' } } }); 198 | }; 199 | expect(theFunction).toThrowError(/(options.packages.the-package.copy should be an object or array of objects)/); 200 | done(); 201 | }); 202 | 203 | it('should throw an error if there is a package with copy that is an object with non string from', done => { 204 | const theFunction = () => { 205 | return new HtmlWebpackDeployPlugin({ packages: { 'the-package': { copy: { from: 123, to: 'dest' } } } }); 206 | }; 207 | expect(theFunction).toThrowError(/(options.packages.the-package.copy should be an object with string properties from & to)/); 208 | done(); 209 | }); 210 | 211 | it('should throw an error if there is a package with copy that is an object with non string to', done => { 212 | const theFunction = () => { 213 | return new HtmlWebpackDeployPlugin({ packages: { 'the-package': { copy: { from: 'src', to: 123 } } } }); 214 | }; 215 | expect(theFunction).toThrowError(/(options.packages.the-package.copy should be an object with string properties from & to)/); 216 | done(); 217 | }); 218 | 219 | it('should not throw an error for a package that exists with copy that is an object with string from & to', done => { 220 | const theFunction = () => { 221 | return new HtmlWebpackDeployPlugin({ packages: { 'the-package': { copy: { from: 'src', to: 'dest' } } } }); 222 | }; 223 | expect(theFunction).not.toThrowError(); 224 | done(); 225 | }); 226 | 227 | it('should not throw an error if the package.copy.fromAbsolute is true', done => { 228 | const theFunction = () => { 229 | return new HtmlWebpackDeployPlugin({ packages: { 'the-package': { copy: { from: 'src', to: 'dest' } } } }); 230 | }; 231 | 232 | expect(theFunction).not.toThrowError(); 233 | done(); 234 | }); 235 | 236 | it('should not throw an error if the package.copy.fromAbsolute is false', done => { 237 | const theFunction = () => { 238 | return new HtmlWebpackDeployPlugin({ packages: { 'the-package': { copy: { fromAbsolute: false, from: 'src', to: 'dest' } } } }); 239 | }; 240 | 241 | expect(theFunction).not.toThrowError(); 242 | done(); 243 | }); 244 | 245 | it('should throw an error if the package.copy.fromAbsolute flag is not a boolean', done => { 246 | const theFunction = () => { 247 | return new HtmlWebpackDeployPlugin({ packages: { 'the-package': { copy: { fromAbsolute: 123, from: 'src', to: 'dest' } } } }); 248 | }; 249 | 250 | expect(theFunction).toThrowError(/(options.packages.the-package.copy.fromAbsolute should be a boolean)/); 251 | done(); 252 | }); 253 | 254 | it('should throw an error for a package that does not exist', done => { 255 | const theFunction = () => { 256 | return new HtmlWebpackDeployPlugin({ packages: { 'package-does-not-exist': { copy: { from: 'src', to: 'dest' } } } }); 257 | }; 258 | expect(theFunction).toThrowError(/(options.packages.package-does-not-exist package path could not be found)/); 259 | done(); 260 | }); 261 | 262 | it('should throw an error for a package that does not have a version', done => { 263 | const theFunction = () => { 264 | return new HtmlWebpackDeployPlugin({ packages: { 'no-version': { copy: { from: 'src', to: 'dest' } } } }); 265 | }; 266 | expect(theFunction).toThrowError(/(options.packages.no-version package version could not be found)/); 267 | done(); 268 | }); 269 | 270 | it('should throw an error for a package that has a malformed package.json', done => { 271 | const theFunction = () => { 272 | return new HtmlWebpackDeployPlugin({ packages: { 'bad-package': { copy: { from: 'src', to: 'dest' } } } }); 273 | }; 274 | expect(theFunction).toThrowError(/(options.packages.bad-package package.json was malformed)/); 275 | done(); 276 | }); 277 | 278 | it('should throw an error for a package that has a non boolean useCdn', done => { 279 | const theFunction = () => { 280 | return new HtmlWebpackDeployPlugin({ packages: { 'the-package': { useCdn: '123', links: 'the-file' } } }); 281 | }; 282 | expect(theFunction).toThrowError(/(options.packages.the-package.useCdn should be a boolean)/); 283 | done(); 284 | }); 285 | 286 | it('should not throw an error for a package that has a boolean useCdn', done => { 287 | const theFunction = () => { 288 | return new HtmlWebpackDeployPlugin({ packages: { 'the-package': { useCdn: true, links: 'the-file' } } }); 289 | }; 290 | expect(theFunction).not.toThrowError(); 291 | done(); 292 | }); 293 | 294 | it('should throw an error for a package that has a non function getCdnPath', done => { 295 | const theFunction = () => { 296 | return new HtmlWebpackDeployPlugin({ packages: { 'the-package': { getCdnPath: '123', links: 'the-file' } } }); 297 | }; 298 | expect(theFunction).toThrowError(/(options.packages.the-package.getCdnPath should be a function)/); 299 | done(); 300 | }); 301 | 302 | it('should throw an error for a package that has a function getCdnPath the returns a non string', done => { 303 | const theFunction = () => { 304 | return new HtmlWebpackDeployPlugin({ packages: { 'the-package': { getCdnPath: () => 123, links: 'the-file' } } }); 305 | }; 306 | expect(theFunction).toThrowError(/(options.packages.the-package.getCdnPath should be a function)/); 307 | done(); 308 | }); 309 | 310 | it('should not throw an error for a package that has a function getCdnPath', done => { 311 | const theFunction = () => { 312 | return new HtmlWebpackDeployPlugin({ packages: { 'the-package': { getCdnPath: () => '', links: 'the-file' } } }); 313 | }; 314 | expect(theFunction).not.toThrowError(); 315 | done(); 316 | }); 317 | 318 | runTestsForOption(['packages', 'bootstrap', 'links']); 319 | runTestsForOption(['packages', 'bootstrap', 'scripts']); 320 | 321 | it('should throw an error for a package with scripts that are objects with string variableName and object external', done => { 322 | const theFunction = () => { 323 | return new HtmlWebpackDeployPlugin({ 324 | packages: { 325 | 'the-package': { 326 | scripts: { 327 | path: 'a-path', 328 | variableName: 'shortcutVariableName', 329 | external: { 330 | packageName: 'packageName', 331 | variableName: 'variableName' 332 | } 333 | } 334 | } 335 | } 336 | }); 337 | }; 338 | expect(theFunction).toThrowError(/(options.packages.the-package.scripts object variableName and external cannot be used together)/); 339 | done(); 340 | }); 341 | 342 | it('should throw an error for a package with scripts that are objects with non string variableName', done => { 343 | const theFunction = () => { 344 | return new HtmlWebpackDeployPlugin({ packages: { 'the-package': { scripts: { path: 'a-path', variableName: 123 } } } }); 345 | }; 346 | expect(theFunction).toThrowError(/(options.packages.the-package.scripts object variableName should be a string)/); 347 | done(); 348 | }); 349 | 350 | it('should not throw an error for a package with scripts that are objects with string variableName', done => { 351 | const theFunction = () => { 352 | return new HtmlWebpackDeployPlugin({ packages: { 'the-package': { scripts: { path: 'a-path', variableName: 'the-variable-name' } } } }); 353 | }; 354 | expect(theFunction).not.toThrowError(); 355 | done(); 356 | }); 357 | 358 | it('should throw an error for a package with scripts that have a non-string cdnPath', done => { 359 | const theFunction = () => { 360 | return new HtmlWebpackDeployPlugin({ packages: { 'the-package': { scripts: { path: 'a-path', cdnPath: 123 } } } }); 361 | }; 362 | expect(theFunction).toThrowError(/(options.packages.the-package.scripts.cdnPath should be a string)/); 363 | done(); 364 | }); 365 | 366 | it('should not throw an error for a package with scripts that have a string cdnPath', done => { 367 | const theFunction = () => { 368 | return new HtmlWebpackDeployPlugin({ packages: { 'the-package': { scripts: { path: 'a-path', cdnPath: 'cdn-path' } } } }); 369 | }; 370 | expect(theFunction).not.toThrowError(); 371 | done(); 372 | }); 373 | 374 | it('should throw an error for a package with links that have a non-string cdnPath', done => { 375 | const theFunction = () => { 376 | return new HtmlWebpackDeployPlugin({ packages: { 'the-package': { links: { path: 'a-path', cdnPath: 123 } } } }); 377 | }; 378 | expect(theFunction).toThrowError(/(options.packages.the-package.links.cdnPath should be a string)/); 379 | done(); 380 | }); 381 | 382 | it('should not throw an error for a package with links that have a string cdnPath', done => { 383 | const theFunction = () => { 384 | return new HtmlWebpackDeployPlugin({ packages: { 'the-package': { links: { path: 'a-path', cdnPath: 'cdn-path' } } } }); 385 | }; 386 | expect(theFunction).not.toThrowError(); 387 | done(); 388 | }); 389 | 390 | it('should throw an error for a package with scripts that have a non-string devPath', done => { 391 | const theFunction = () => { 392 | return new HtmlWebpackDeployPlugin({ packages: { 'the-package': { scripts: { path: 'a-path', devPath: 123 } } } }); 393 | }; 394 | expect(theFunction).toThrowError(/(options.packages.the-package.scripts.devPath should be a string)/); 395 | done(); 396 | }); 397 | 398 | it('should not throw an error for a package with scripts that have a string devPath', done => { 399 | const theFunction = () => { 400 | return new HtmlWebpackDeployPlugin({ packages: { 'the-package': { scripts: { path: 'a-path', devPath: 'dev-path' } } } }); 401 | }; 402 | expect(theFunction).not.toThrowError(); 403 | done(); 404 | }); 405 | 406 | it('should throw an error for a package with links that have a non-string devPath', done => { 407 | const theFunction = () => { 408 | return new HtmlWebpackDeployPlugin({ packages: { 'the-package': { links: { path: 'a-path', devPath: 123 } } } }); 409 | }; 410 | expect(theFunction).toThrowError(/(options.packages.the-package.links.devPath should be a string)/); 411 | done(); 412 | }); 413 | 414 | it('should not throw an error for a package with links that have a string devPath', done => { 415 | const theFunction = () => { 416 | return new HtmlWebpackDeployPlugin({ packages: { 'the-package': { links: { path: 'a-path', devPath: 'dev-path' } } } }); 417 | }; 418 | expect(theFunction).not.toThrowError(); 419 | done(); 420 | }); 421 | }); 422 | 423 | describe('options.addAssetsPath', () => { 424 | it('should throw an error if the addAssetsPath is not a function', done => { 425 | const theFunction = () => { 426 | return new HtmlWebpackDeployPlugin({ addAssetsPath: 'hello' }); 427 | }; 428 | 429 | expect(theFunction).toThrowError(/(options.addAssetsPath should be a function)/); 430 | done(); 431 | }); 432 | 433 | it('should throw an error if the addAssetsPath is not a function that returns a string', done => { 434 | const theFunction = () => { 435 | return new HtmlWebpackDeployPlugin({ addAssetsPath: () => null }); 436 | }; 437 | 438 | expect(theFunction).toThrowError(/(options.addAssetsPath should be a function that returns a string)/); 439 | done(); 440 | }); 441 | }); 442 | 443 | describe('options.addPackagesPath', () => { 444 | it('should throw an error if the addPackagesPath is not a function', done => { 445 | const theFunction = () => { 446 | return new HtmlWebpackDeployPlugin({ addPackagesPath: 'hello' }); 447 | }; 448 | 449 | expect(theFunction).toThrowError(/(options.addPackagesPath should be a function)/); 450 | done(); 451 | }); 452 | 453 | it('should throw an error if the addPackagesPath is not a function that returns a string', done => { 454 | const theFunction = () => { 455 | return new HtmlWebpackDeployPlugin({ addPackagesPath: () => null }); 456 | }; 457 | 458 | expect(theFunction).toThrowError(/(options.addPackagesPath should be a function that returns a string)/); 459 | done(); 460 | }); 461 | }); 462 | 463 | describe('options.findNodeModulesPath', () => { 464 | it('should throw an error if the findNodeModulesPath is not a function', done => { 465 | const theFunction = () => { 466 | return new HtmlWebpackDeployPlugin({ findNodeModulesPath: 'hello' }); 467 | }; 468 | 469 | expect(theFunction).toThrowError(/(options.findNodeModulesPath should be a function)/); 470 | done(); 471 | }); 472 | 473 | it('should throw an error if the findNodeModulesPath is not a function that returns a string', done => { 474 | const theFunction = () => { 475 | return new HtmlWebpackDeployPlugin({ findNodeModulesPath: () => null }); 476 | }; 477 | 478 | expect(theFunction).toThrowError(/(options.findNodeModulesPath should be a function that returns a string)/); 479 | done(); 480 | }); 481 | }); 482 | 483 | describe('options.useCdn', () => { 484 | it('should throw an error if the useCdn is not a boolean', done => { 485 | const theFunction = () => { 486 | return new HtmlWebpackDeployPlugin({ useCdn: 'hello' }); 487 | }; 488 | 489 | expect(theFunction).toThrowError(/(options.useCdn should be a boolean)/); 490 | done(); 491 | }); 492 | }); 493 | 494 | describe('options.getCdnPath', () => { 495 | it('should throw an error if the getCdnPath is not a function', done => { 496 | const theFunction = () => { 497 | return new HtmlWebpackDeployPlugin({ getCdnPath: 'hello' }); 498 | }; 499 | 500 | expect(theFunction).toThrowError(/(options.getCdnPath should be a function)/); 501 | done(); 502 | }); 503 | 504 | it('should throw an error if the getCdnPath is not a function that returns a string', done => { 505 | const theFunction = () => { 506 | return new HtmlWebpackDeployPlugin({ getCdnPath: () => null }); 507 | }; 508 | 509 | expect(theFunction).toThrowError(/(options.getCdnPath should be a function that returns a string)/); 510 | done(); 511 | }); 512 | }); 513 | 514 | describe('options.files', () => { 515 | it('should not throw an error if the files options is a string', done => { 516 | const theFunction = () => { 517 | return new HtmlWebpackDeployPlugin({ files: 'a-file' }); 518 | }; 519 | expect(theFunction).not.toThrowError(); 520 | done(); 521 | }); 522 | 523 | it('should not throw an error if the files options are strings', done => { 524 | const theFunction = () => { 525 | return new HtmlWebpackDeployPlugin({ files: ['a-file', 'b-file'] }); 526 | }; 527 | expect(theFunction).not.toThrowError(); 528 | done(); 529 | }); 530 | 531 | it('should throw an error if the files option is not a string', done => { 532 | const nonStringCheck = [123, true, /regex/, {}]; 533 | 534 | nonStringCheck.forEach(val => { 535 | const theCheck = () => { 536 | return new HtmlWebpackDeployPlugin({ files: val }); 537 | }; 538 | 539 | expect(theCheck).toThrowError(/(options\.files should be a string or array of strings)/); 540 | }); 541 | 542 | done(); 543 | }); 544 | 545 | it('should throw an error if any of the files options are not strings', done => { 546 | const theFunction = () => { 547 | return new HtmlWebpackDeployPlugin({ files: ['abc', true, 'def'] }); 548 | }; 549 | expect(theFunction).toThrowError(/(options\.files should be a string or array of strings)/); 550 | done(); 551 | }); 552 | }); 553 | 554 | describe('options.assets.tags', () => { 555 | it('should not throw an error if the tags option is specified for assets and is invalid for the tags plugin', done => { 556 | const theFunction = () => { 557 | return new HtmlWebpackDeployPlugin({ assets: { tags: 123 } }); 558 | }; 559 | 560 | expect(theFunction).toThrowError(/(options.assets.tags is not supported)/); 561 | done(); 562 | }); 563 | }); 564 | 565 | describe('options.packages.tags', () => { 566 | it('should not throw an error if the tags option is specified for a package and is invalid for the tags plugin', done => { 567 | const theFunction = () => { 568 | return new HtmlWebpackDeployPlugin({ packages: { p: { tags: 123 } } }); 569 | }; 570 | 571 | expect(theFunction).toThrowError(/(options.packages.p.tags is not supported)/); 572 | done(); 573 | }); 574 | }); 575 | 576 | describe('options.append', () => { 577 | it('should not throw an error if the append flag is not provided', done => { 578 | const theFunction = () => { 579 | return new HtmlWebpackDeployPlugin({}); 580 | }; 581 | 582 | expect(theFunction).not.toThrowError(); 583 | done(); 584 | }); 585 | 586 | it('should throw an error if the append flag is not a boolean', done => { 587 | const theFunction = () => { 588 | return new HtmlWebpackDeployPlugin({ append: 'hello' }); 589 | }; 590 | 591 | expect(theFunction).toThrowError(/(options.append should be a boolean)/); 592 | done(); 593 | }); 594 | }); 595 | 596 | describe('options.publicPath', () => { 597 | it('should throw an error if the publicPath flag is not a boolean or string or a function', done => { 598 | const theFunction = () => { 599 | return new HtmlWebpackDeployPlugin({ publicPath: 123 }); 600 | }; 601 | 602 | expect(theFunction).toThrowError(/(options.publicPath should be a boolean or a string or a function that returns a string)/); 603 | done(); 604 | }); 605 | 606 | it('should throw an error if the usePublicPath flag is not a boolean', done => { 607 | const theFunction = () => { 608 | return new HtmlWebpackDeployPlugin({ usePublicPath: 123 }); 609 | }; 610 | 611 | expect(theFunction).toThrowError(/(options.usePublicPath should be a boolean)/); 612 | done(); 613 | }); 614 | 615 | it('should throw an error if the addPublicPath option is not a function', done => { 616 | const theFunction = () => { 617 | return new HtmlWebpackDeployPlugin({ addPublicPath: 123 }); 618 | }; 619 | 620 | expect(theFunction).toThrowError(/(options.addPublicPath should be a function that returns a string)/); 621 | done(); 622 | }); 623 | 624 | it('should throw an error if publicPath and usePublicPath are specified together', done => { 625 | const theFunction = () => { 626 | return new HtmlWebpackDeployPlugin({ publicPath: true, usePublicPath: false }); 627 | }; 628 | 629 | expect(theFunction).toThrowError(/(options.publicPath should not be used with either usePublicPath or addPublicPath)/); 630 | done(); 631 | }); 632 | 633 | it('should throw an error if publicPath and addPublicPath are specified together', done => { 634 | const theFunction = () => { 635 | return new HtmlWebpackDeployPlugin({ publicPath: true, addPublicPath: () => '' }); 636 | }; 637 | 638 | expect(theFunction).toThrowError(/(options.publicPath should not be used with either usePublicPath or addPublicPath)/); 639 | done(); 640 | }); 641 | }); 642 | 643 | describe('options.hash', () => { 644 | it('should throw an error if the hash option is not a boolean or function', done => { 645 | const nonBooleanCheck = [123, { 'not a boolean': true }, /regex/, [], {}]; 646 | 647 | nonBooleanCheck.forEach(val => { 648 | const theCheck = () => { 649 | return new HtmlWebpackDeployPlugin({ append: true, publicPath: true, hash: val }); 650 | }; 651 | expect(theCheck).toThrowError(/(options.hash should be a boolean or a string or a function that returns a string)/); 652 | }); 653 | done(); 654 | }); 655 | 656 | it('should not throw an error if the hash is a string', done => { 657 | const theFunction = () => { 658 | return new HtmlWebpackDeployPlugin({ hash: 'my-hash' }); 659 | }; 660 | 661 | expect(theFunction).not.toThrowError(); 662 | done(); 663 | }); 664 | 665 | it('should throw an error if the useHash flag is not a boolean', done => { 666 | const theFunction = () => { 667 | return new HtmlWebpackDeployPlugin({ useHash: 123 }); 668 | }; 669 | 670 | expect(theFunction).toThrowError(/(options.useHash should be a boolean)/); 671 | done(); 672 | }); 673 | 674 | it('should throw an error if the addHash option is not a function', done => { 675 | const theFunction = () => { 676 | return new HtmlWebpackDeployPlugin({ addHash: 123 }); 677 | }; 678 | 679 | expect(theFunction).toThrowError(/(options.addHash should be a function that returns a string)/); 680 | done(); 681 | }); 682 | 683 | it('should throw an error if hash and useHash are specified together', done => { 684 | const theFunction = () => { 685 | return new HtmlWebpackDeployPlugin({ hash: true, useHash: false }); 686 | }; 687 | 688 | expect(theFunction).toThrowError(/(options.hash should not be used with either useHash or addHash)/); 689 | done(); 690 | }); 691 | 692 | it('should throw an error if hash and addHash are specified together', done => { 693 | const theFunction = () => { 694 | return new HtmlWebpackDeployPlugin({ hash: true, addHash: () => '' }); 695 | }; 696 | 697 | expect(theFunction).toThrowError(/(options.hash should not be used with either useHash or addHash)/); 698 | done(); 699 | }); 700 | }); 701 | }); 702 | 703 | /* 704 | * SAMPLE PARAMETERS: 705 | * ['assets', 'links'] 706 | * ['assets', 'scripts'] 707 | * ['packages', 'bootstrap', 'links'] 708 | * ['packages', 'bootstrap', 'scripts'] 709 | */ 710 | function runTestsForOption (optionNamePath) { 711 | const optionName = optionNamePath[optionNamePath.length - 1]; 712 | const isScript = optionName === 'scripts'; 713 | 714 | const optionPath = optionNamePath.join('.'); 715 | 716 | function createPlugin (value, pluginOptionsRoot = {}) { 717 | let pluginOptions = pluginOptionsRoot; 718 | optionNamePath.slice(0, optionNamePath.length - 1).forEach(pathName => { 719 | pluginOptions = pluginOptions[pathName] = {}; 720 | }); 721 | pluginOptions[optionNamePath[optionNamePath.length - 1]] = value; 722 | 723 | return new HtmlWebpackDeployPlugin(pluginOptionsRoot); 724 | } 725 | 726 | describe(`${optionPath}`, () => { 727 | it(`should throw an error if the ${optionPath} are not an array or string or object`, done => { 728 | const theFunction = () => { 729 | return createPlugin(123); 730 | }; 731 | 732 | expect(theFunction).toThrowError(new RegExp(`(options.${optionPath} should be a string, object, or array)`)); 733 | done(); 734 | }); 735 | 736 | it(`should throw an error if the ${optionPath} contains objects and a boolean`, done => { 737 | const theFunction = () => { 738 | return createPlugin([{ path: 'a' }, false, { path: 'b' }]); 739 | }; 740 | 741 | expect(theFunction).toThrowError(new RegExp(`(options.${optionPath} items must be an object or string)`)); 742 | done(); 743 | }); 744 | 745 | it(`should throw an error if the ${optionPath} contains string and a boolean`, done => { 746 | const theFunction = () => { 747 | return createPlugin(['foo.js', true, 'bar.css']); 748 | }; 749 | 750 | expect(theFunction).toThrowError(new RegExp(`(options.${optionPath} items must be an object or string)`)); 751 | done(); 752 | }); 753 | 754 | it(`should not throw an error if the ${optionPath} contains strings and objects`, done => { 755 | const theFunction = () => { 756 | return createPlugin(['foo.js', { path: 'file.js' }, 'bar.css']); 757 | }; 758 | 759 | expect(theFunction).not.toThrowError(); 760 | done(); 761 | }); 762 | }); 763 | 764 | describe(`options.${optionPath} path`, () => { 765 | it(`should throw an error if the ${optionPath} contains an element that is an empty object`, done => { 766 | const theFunction = () => { 767 | return createPlugin([{ path: 'a' }, {}, { path: 'b' }]); 768 | }; 769 | 770 | expect(theFunction).toThrowError(new RegExp(`(options.${optionPath} object must have a string path property)`)); 771 | done(); 772 | }); 773 | 774 | it(`should throw an error if the ${optionPath} contains an element that is an object with a non string path`, done => { 775 | const theFunction = () => { 776 | return createPlugin([{ path: 'a' }, { path: 123, type: 'js' }, { path: 'c.css' }]); 777 | }; 778 | 779 | expect(theFunction).toThrowError(new RegExp(`(options.${optionPath} object must have a string path property)`)); 780 | done(); 781 | }); 782 | 783 | it(`should not throw an error if the ${optionPath} contains elements that are all objects that have a path`, done => { 784 | const theFunction = () => { 785 | return createPlugin([{ path: 'a' }, { path: 'b' }, { path: 'c' }]); 786 | }; 787 | 788 | expect(theFunction).not.toThrowError(); 789 | done(); 790 | }); 791 | }); 792 | 793 | describe(`options.${optionPath} append`, () => { 794 | it(`should throw an error if the ${optionPath} contains an element that is an object with a non boolean append`, done => { 795 | const theFunction = () => { 796 | return createPlugin([{ path: 'a' }, { path: 'b', append: 123 }, { path: 'c' }]); 797 | }; 798 | 799 | expect(theFunction).toThrowError(new RegExp(`(options.${optionPath}.append should be a boolean)`)); 800 | done(); 801 | }); 802 | 803 | it(`should not throw an error if the ${optionPath} contains elements that are all objects that have a boolean append`, done => { 804 | const theFunction = () => { 805 | return createPlugin([{ path: 'a', append: true }, { path: 'b', append: false }, { path: 'c', append: true }]); 806 | }; 807 | 808 | expect(theFunction).not.toThrowError(); 809 | done(); 810 | }); 811 | }); 812 | 813 | describe(`options.${optionPath} publicPath`, () => { 814 | it(`should not throw an error if the ${optionPath} contains an element that is an object with publicPath set to string`, done => { 815 | const theFunction = () => { 816 | return createPlugin([{ path: 'a' }, { path: 'b', publicPath: 'string' }, { path: 'c' }]); 817 | }; 818 | 819 | expect(theFunction).not.toThrowError(); 820 | done(); 821 | }); 822 | 823 | it(`should throw an error if the ${optionPath} contains an element that is an object with publicPath set to object`, done => { 824 | const theFunction = () => { 825 | return createPlugin([{ path: 'a' }, { path: 'b', publicPath: {} }, { path: 'c' }]); 826 | }; 827 | 828 | expect(theFunction).toThrowError(new RegExp(`(options.${optionPath}.publicPath should be a boolean or a string or a function that returns a string)`)); 829 | done(); 830 | }); 831 | 832 | it(`should throw an error if the ${optionPath} contains an element that is an object with publicPath set to number`, done => { 833 | const theFunction = () => { 834 | return createPlugin([{ path: 'a' }, { path: 'b', publicPath: 0 }, { path: 'c' }]); 835 | }; 836 | 837 | expect(theFunction).toThrowError(new RegExp(`(options.${optionPath}.publicPath should be a boolean or a string or a function that returns a string)`)); 838 | done(); 839 | }); 840 | 841 | it(`should throw an error if the ${optionPath} contains an element that is an object with publicPath set to array`, done => { 842 | const theFunction = () => { 843 | return createPlugin([{ path: 'a' }, { path: 'b', publicPath: [] }, { path: 'c' }]); 844 | }; 845 | 846 | expect(theFunction).toThrowError(new RegExp(`(options.${optionPath}.publicPath should be a boolean or a string or a function that returns a string)`)); 847 | done(); 848 | }); 849 | 850 | it(`should not throw an error if the ${optionPath} contains an element that is an object with publicPath set to true`, done => { 851 | const theFunction = () => { 852 | return createPlugin([{ path: 'a', publicPath: true }, { path: 'b' }, { path: 'c' }]); 853 | }; 854 | 855 | expect(theFunction).not.toThrowError(); 856 | done(); 857 | }); 858 | }); 859 | 860 | describe(`options.${optionPath} attributes`, () => { 861 | it(`should throw an error if the ${optionPath} contains an element that is an object with non object string attributes`, done => { 862 | const theFunction = () => { 863 | return createPlugin([{ path: 'a' }, { path: 'b', attributes: '' }, { path: 'c' }]); 864 | }; 865 | 866 | expect(theFunction).toThrowError(new RegExp(`(options.${optionPath} object should have an object attributes property)`)); 867 | done(); 868 | }); 869 | 870 | it(`should throw an error if the ${optionPath} contains an element that is an object with array attributes`, done => { 871 | const theFunction = () => { 872 | return createPlugin([{ path: 'a' }, { path: 'b', attributes: [] }, { path: 'c' }]); 873 | }; 874 | 875 | expect(theFunction).toThrowError(new RegExp(`(options.${optionPath} object should have an object attributes property)`)); 876 | done(); 877 | }); 878 | 879 | it(`should throw an error if the ${optionPath} contains an element that is an object with number attributes`, done => { 880 | const theFunction = () => { 881 | return createPlugin([{ path: 'a' }, { path: 'b', attributes: 0 }, { path: 'c' }]); 882 | }; 883 | 884 | expect(theFunction).toThrowError(new RegExp(`(options.${optionPath} object should have an object attributes property)`)); 885 | done(); 886 | }); 887 | 888 | it(`should throw an error if the ${optionPath} contains an element that is an object with boolean attributes`, done => { 889 | const theFunction = () => { 890 | return createPlugin([{ path: 'a' }, { path: 'b', attributes: true }, { path: 'c' }]); 891 | }; 892 | 893 | expect(theFunction).toThrowError(new RegExp(`(options.${optionPath} object should have an object attributes property)`)); 894 | done(); 895 | }); 896 | 897 | it(`should not throw an error if the ${optionPath} contains an element that is an object with empty object attributes`, done => { 898 | const theFunction = () => { 899 | return createPlugin([{ path: 'a' }, { path: 'b', attributes: {} }, { path: 'c' }]); 900 | }; 901 | 902 | expect(theFunction).not.toThrowError(); 903 | done(); 904 | }); 905 | }); 906 | 907 | describe(`options.${optionPath} glob`, () => { 908 | it(`should throw an error if any of the ${optionPath} options are objects with a glob property that is not a string`, done => { 909 | const theFunction = () => { 910 | return createPlugin(['foo', { path: 'a', glob: 123, type: 'js' }, 'bar']); 911 | }; 912 | 913 | expect(theFunction).toThrowError(new RegExp(`(options.${optionPath} object should have a string glob property)`)); 914 | done(); 915 | }); 916 | 917 | it(`should throw an error if any of the ${optionPath} options are objects with a globPath property that is not a string`, done => { 918 | const theFunction = () => { 919 | return createPlugin(['foo', { path: 'a', globPath: 123, type: 'js' }, 'bar']); 920 | }; 921 | 922 | expect(theFunction).toThrowError(new RegExp(`(options.${optionPath} object should have a string glob property)`)); 923 | done(); 924 | }); 925 | 926 | it(`should throw an error if any of the ${optionPath} options are objects with glob specified but globPath missing`, done => { 927 | const theFunction = () => { 928 | return createPlugin(['foo', { path: 'a-path', glob: 'withoutExtensions*' }, 'bar'], { append: false }); 929 | }; 930 | expect(theFunction).toThrowError(new RegExp(`(options.${optionPath} object should have a string globPath property)`)); 931 | done(); 932 | }); 933 | 934 | it(`should throw an error if any of the ${optionPath} options are objects with globPath specified but glob missing`, done => { 935 | const theFunction = () => { 936 | return createPlugin(['foo', { path: 'a-path', globPath: 'withoutExtensions*' }, 'bar'], { append: false }); 937 | }; 938 | expect(theFunction).toThrowError(new RegExp(`(options.${optionPath} object should have a string glob property)`)); 939 | done(); 940 | }); 941 | 942 | it(`should throw an error if any of the ${optionPath} options are objects with glob that does not match any files`, done => { 943 | const theFunction = () => { 944 | return createPlugin([{ path: 'assets/', globPath: FIXTURES_PATH, glob: 'nonexistant*.js' }, { path: 'assets/', globPath: FIXTURES_PATH, glob: 'nonexistant*.css' }], { append: true }); 945 | }; 946 | 947 | expect(theFunction).toThrowError(new RegExp(`(options.${optionPath} object glob found no files)`)); 948 | done(); 949 | }); 950 | }); 951 | 952 | describe(`options.${optionPath} sourcePath`, () => { 953 | it(`should throw an error if any of the ${optionPath} options are objects with an sourcePath property that is not a string`, done => { 954 | const theFunction = () => { 955 | return createPlugin(['foo', { path: 'a', sourcePath: 123, type: 'js' }, 'bar']); 956 | }; 957 | 958 | expect(theFunction).toThrowError(new RegExp(`(options.${optionPath} object should have a string sourcePath property)`)); 959 | done(); 960 | }); 961 | }); 962 | 963 | describe(`options.${optionPath} external`, () => { 964 | it(`should throw an error if any of the ${optionPath} options are objects with external property that is not an object`, done => { 965 | const theFunction = () => { 966 | return createPlugin(['foo', { path: 'a', external: 123 }, 'bar']); 967 | }; 968 | if (isScript) { 969 | expect(theFunction).toThrowError(new RegExp(`(options.${optionPath}.external should be an object)`)); 970 | } else { 971 | expect(theFunction).toThrowError(new RegExp(`(options.${optionPath}.external should not be used on non script tags)`)); 972 | } 973 | done(); 974 | }); 975 | 976 | if (isScript) { 977 | it(`should not throw an error if any of the ${optionPath} options are objects with valid external objects`, done => { 978 | const theFunction = () => { 979 | return createPlugin(['foo', { path: 'a', external: { packageName: 'a', variableName: 'A' } }, 'bar']); 980 | }; 981 | expect(theFunction).not.toThrowError(); 982 | done(); 983 | }); 984 | 985 | it(`should throw an error if any of the ${optionPath} options are objects with external that is an empty object`, done => { 986 | const theFunction = () => { 987 | return createPlugin(['foo', { path: 'a', external: { } }, 'bar']); 988 | }; 989 | expect(theFunction).toThrowError(new RegExp(`(options.${optionPath}.external should have a string packageName and variableName property)`)); 990 | done(); 991 | }); 992 | 993 | it(`should throw an error if any of the ${optionPath} options are objects with external that has packageName but not variableName string properties`, done => { 994 | const theFunction = () => { 995 | return createPlugin(['foo', { path: 'a', external: { packageName: 'a' } }, 'bar']); 996 | }; 997 | expect(theFunction).toThrowError(new RegExp(`(options.${optionPath}.external should have a string variableName property)`)); 998 | done(); 999 | }); 1000 | 1001 | it(`should throw an error if any of the ${optionPath} options are objects with external that has variableName but not packageName string properties`, done => { 1002 | const theFunction = () => { 1003 | return createPlugin(['foo', { path: 'a', external: { variableName: 'A' } }, 'bar']); 1004 | }; 1005 | expect(theFunction).toThrowError(new RegExp(`(options.${optionPath}.external should have a string packageName property)`)); 1006 | done(); 1007 | }); 1008 | } 1009 | }); 1010 | } 1011 | -------------------------------------------------------------------------------- /spec/support/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "spec", 3 | "spec_files": [ 4 | "**/*[sS]pec.js" 5 | ], 6 | "helpers": ["../node_modules/jasmine-expect/index.js"], 7 | "stopSpecOnExpectationFailure": false, 8 | "random": true 9 | } 10 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const path = require('path'); 3 | const fs = require('fs'); 4 | 5 | const assert = require('assert'); 6 | const findUp = require('find-up'); 7 | const slash = require('slash'); // fixes slashes in file paths for windows 8 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 9 | const HtmlWebpackTagsPlugin = require('html-webpack-tags-plugin'); 10 | 11 | const PLUGIN_NAME = 'HtmlWebpackDeployPlugin'; 12 | 13 | const { getValidatedOptions, IS } = HtmlWebpackTagsPlugin.api; 14 | 15 | const DEFAULT_ROOT_OPTIONS = { 16 | assets: {}, 17 | packages: {}, 18 | useAssetsPath: true, 19 | addAssetsPath: assetPath => path.join('assets', assetPath), 20 | usePackagesPath: true, 21 | addPackagesPath: packagePath => path.join('packages', packagePath), 22 | getPackagePath: (packageName, packageVersion, packagePath) => path.join(packageName + '-' + packageVersion, packagePath), 23 | findNodeModulesPath: (cwd, packageName) => findUp.sync(slash(path.join('node_modules', packageName)), { cwd, type: 'directory' }) 24 | }; 25 | 26 | const DEFAULT_MAIN_OPTIONS = { 27 | useCdn: false, 28 | getCdnPath: (packageName, packageVersion, packagePath) => `https://unpkg.com/${packageName}@${packageVersion}/${packagePath}` 29 | }; 30 | 31 | const { isDefined, isArray, isObject, isString, isBoolean, isFunction } = IS; 32 | 33 | const isFunctionReturningString = v => isFunction(v) && isString(v('', '', '')); // 3rd string needed or this throws 34 | 35 | const isArrayOfString = v => isArray(v) && v.every(i => isString(i)); 36 | 37 | const processShortcuts = (options, optionPath, keyShortcut, keyUse, keyAdd) => { 38 | const processedOptions = {}; 39 | if (isDefined(options[keyUse]) || isDefined(options[keyAdd])) { 40 | assert(!isDefined(options[keyShortcut]), `${optionPath}.${keyShortcut} should not be used with either ${keyUse} or ${keyAdd}`); 41 | if (isDefined(options[keyUse])) { 42 | assert(isBoolean(options[keyUse]), `${optionPath}.${keyUse} should be a boolean`); 43 | processedOptions[keyUse] = options[keyUse]; 44 | } 45 | if (isDefined(options[keyAdd])) { 46 | assert(isFunctionReturningString(options[keyAdd]), `${optionPath}.${keyAdd} should be a function that returns a string`); 47 | processedOptions[keyAdd] = options[keyAdd]; 48 | } 49 | } else if (isDefined(options[keyShortcut])) { 50 | const shortcut = options[keyShortcut]; 51 | assert(isBoolean(shortcut) || isString(shortcut) || isFunctionReturningString(shortcut), 52 | `${optionPath}.${keyShortcut} should be a boolean or a string or a function that returns a string`); 53 | if (isBoolean(shortcut)) { 54 | processedOptions[keyUse] = shortcut; 55 | } else if (isString(shortcut)) { 56 | processedOptions[keyUse] = true; 57 | processedOptions[keyAdd] = thePath => path.join(shortcut, thePath); 58 | } else { 59 | processedOptions[keyUse] = true; 60 | processedOptions[keyAdd] = shortcut; 61 | } 62 | } 63 | return processedOptions; 64 | }; 65 | 66 | const getGroupLevelOptions = (options, optionPath, defaultOptions = {}) => { 67 | if (isObject(options)) { 68 | const { assets, packages, ...otherOptions } = options; 69 | return getValidatedOptions(otherOptions, optionPath, defaultOptions); 70 | } else { 71 | return getValidatedOptions(options, optionPath, defaultOptions); 72 | } 73 | }; 74 | 75 | const getTagsLevelOptions = (options, optionPath, useFromAbsolute = false) => { 76 | assert(!(isObject(options) && isDefined(options.tags)), `${optionPath}.tags is not supported`); 77 | const validatedOptions = getValidatedOptions(options, optionPath, {}); 78 | if (isDefined(validatedOptions.copy)) { 79 | const { copy } = validatedOptions; 80 | assert(isArray(copy) || isObject(copy), `${optionPath}.copy should be an object or array of objects`); 81 | if (isObject(copy)) { 82 | assert(isString(copy.from) && isString(copy.to), `${optionPath}.copy should be an object with string properties from & to`); 83 | if (useFromAbsolute && isDefined(copy.fromAbsolute)) { 84 | assert(isBoolean(copy.fromAbsolute), `${optionPath}.copy.fromAbsolute should be a boolean`); 85 | } 86 | validatedOptions.copy = [copy]; 87 | } else { 88 | const copyList = []; 89 | copy.forEach(copyItem => { 90 | assert(isObject(copyItem), `${optionPath}.copy should be an array of objects`); 91 | assert(isString(copyItem.from) && isString(copyItem.to), `${optionPath}.copy should be an array of objects with string properties from & to`); 92 | copyList.push(copyItem); 93 | }); 94 | validatedOptions.copy = copyList; 95 | } 96 | } 97 | return validatedOptions; 98 | }; 99 | 100 | const getValidatedMainOptions = (options, optionPath, defaultMainOptions) => { 101 | return getValidatedCdnOptions(getGroupLevelOptions(options, optionPath, defaultMainOptions), optionPath); 102 | }; 103 | 104 | const getValidatedRootOptions = (options, optionPath, defaultRootOptions = DEFAULT_ROOT_OPTIONS, defaultMainOptions = DEFAULT_MAIN_OPTIONS) => { 105 | const validatedRootOptions = { 106 | ...defaultRootOptions 107 | }; 108 | const validatedMainOptions = getValidatedMainOptions(options, optionPath, defaultMainOptions); 109 | const { assets, packages, files, getPackagePath, findNodeModulesPath, prependExternals } = options; 110 | 111 | const assetsPathOptions = processShortcuts(options, optionPath, 'assetsPath', 'useAssetsPath', 'addAssetsPath'); 112 | if (isDefined(assetsPathOptions.useAssetsPath)) { 113 | validatedRootOptions.useAssetsPath = assetsPathOptions.useAssetsPath; 114 | } 115 | if (isDefined(assetsPathOptions.addAssetsPath)) { 116 | validatedRootOptions.addAssetsPath = assetsPathOptions.addAssetsPath; 117 | } 118 | const packagesPathOptions = processShortcuts(options, optionPath, 'packagesPath', 'usePackagesPath', 'addPackagesPath'); 119 | if (isDefined(packagesPathOptions.usePackagesPath)) { 120 | validatedRootOptions.usePackagesPath = packagesPathOptions.usePackagesPath; 121 | } 122 | if (isDefined(packagesPathOptions.addPackagesPath)) { 123 | validatedRootOptions.addPackagesPath = packagesPathOptions.addPackagesPath; 124 | } 125 | if (isDefined(getPackagePath)) { 126 | assert(isFunctionReturningString(getPackagePath), `${optionPath}.getPackagePath should be a function that returns a string`); 127 | validatedRootOptions.getPackagePath = getPackagePath; 128 | } 129 | if (isDefined(findNodeModulesPath)) { 130 | assert(isFunctionReturningString(findNodeModulesPath), `${optionPath}.findNodeModulesPath should be a function that returns a string`); 131 | validatedRootOptions.findNodeModulesPath = findNodeModulesPath; 132 | } 133 | if (isDefined(prependExternals)) { 134 | assert(isBoolean(prependExternals), `${optionPath}.prependExternals should be a boolean`); 135 | validatedRootOptions.prependExternals = prependExternals; 136 | } 137 | if (isDefined(files)) { 138 | assert((isString(files) || isArrayOfString(files)), `${optionPath}.files should be a string or array of strings`); 139 | validatedRootOptions.files = files; 140 | } 141 | if (isDefined(assets)) { 142 | validatedRootOptions.assets = getValidatedAssetsOptions(assets, validatedRootOptions, validatedMainOptions, `${optionPath}.assets`); 143 | } 144 | if (isDefined(packages)) { 145 | validatedRootOptions.packages = getValidatedPackagesOptions(packages, validatedRootOptions, validatedMainOptions, `${optionPath}.packages`); 146 | } 147 | 148 | return validatedRootOptions; 149 | }; 150 | 151 | const getValidatedAssetsOptions = (assets, rootOptions, mainOptions, optionPath) => { 152 | const validatedAssets = getTagsLevelOptions(assets, optionPath); 153 | const { useAssetsPath, addAssetsPath } = rootOptions; 154 | const { copy, links, scripts, ...assetsOptions } = validatedAssets; 155 | 156 | const baseOptions = { ...mainOptions, ...assetsOptions }; 157 | 158 | const addTagAssetsPaths = (tag, optionName) => { 159 | const newTag = { 160 | ...baseOptions, 161 | ...tag, 162 | path: useAssetsPath ? addAssetsPath(tag.path) : tag.path 163 | }; 164 | if (isDefined(tag.devPath)) { 165 | assert(isString(tag.devPath), `${optionPath}.${optionName}.devPath should be a string`); 166 | newTag.devPath = useAssetsPath ? addAssetsPath(tag.devPath) : tag.devPath; 167 | } 168 | return newTag; 169 | }; 170 | const getAddTagAssetsPaths = optionName => tag => addTagAssetsPaths(tag, optionName); 171 | if (isDefined(copy)) { 172 | validatedAssets.copy = copy.map(copy => ({ ...copy, to: useAssetsPath ? addAssetsPath(copy.to) : copy.to })); 173 | } 174 | if (isDefined(links)) { 175 | validatedAssets.links = links.map(getAddTagAssetsPaths('links')); 176 | } 177 | if (isDefined(scripts)) { 178 | validatedAssets.scripts = scripts.map(getAddTagAssetsPaths('scripts')); 179 | } 180 | return validatedAssets; 181 | }; 182 | 183 | const getValidatedCdnOptions = (options, optionPath, defaultOptions = {}) => { 184 | const { useCdn, getCdnPath, ...otherOptions } = options; 185 | const validatedOptions = { 186 | ...defaultOptions, 187 | ...otherOptions 188 | }; 189 | if (isDefined(useCdn)) { 190 | assert(isBoolean(useCdn), `${optionPath}.useCdn should be a boolean`); 191 | validatedOptions.useCdn = useCdn; 192 | } 193 | if (isDefined(getCdnPath)) { 194 | assert(isFunctionReturningString(getCdnPath), `${optionPath}.getCdnPath should be a function that returns a string`); 195 | validatedOptions.getCdnPath = getCdnPath; 196 | } 197 | return validatedOptions; 198 | }; 199 | 200 | const getValidatedPackagesOptions = (packages, rootOptions, mainOptions, optionPath) => { 201 | assert(isObject(packages), `${optionPath} should be an object`); 202 | const validatedPackages = {}; 203 | Object.keys(packages).forEach(packageName => { 204 | validatedPackages[packageName] = getValidatedPackageOptions(packages[packageName], packageName, rootOptions, mainOptions, optionPath); 205 | }); 206 | return validatedPackages; 207 | }; 208 | 209 | const getValidatedPackageOptions = (thePackage, packageName, rootOptions, mainOptions, optionPath) => { 210 | optionPath = `${optionPath}.${packageName}`; 211 | const validatedPackage = getValidatedCdnOptions(getTagsLevelOptions(thePackage, optionPath, true), optionPath, mainOptions); 212 | const { copy, links, scripts, ...packageOptions } = validatedPackage; 213 | const { findNodeModulesPath, getPackagePath, usePackagesPath, addPackagesPath } = rootOptions; 214 | const packagePath = findNodeModulesPath(process.cwd(), packageName); 215 | assert(isString(packagePath), `${optionPath} package path could not be found`); 216 | const packageFilePath = path.join(packagePath, 'package.json'); 217 | let packageNpmPackage; 218 | try { 219 | packageNpmPackage = JSON.parse(fs.readFileSync(packageFilePath, 'utf8')); 220 | } catch (error) { 221 | assert(false, `${optionPath} package.json not found in ${packageFilePath}`); 222 | } 223 | assert(isObject(packageNpmPackage), `${optionPath} package.json was malformed: ${packageFilePath}/package.json`); 224 | const packageVersion = packageNpmPackage.version; 225 | assert(isString(packageNpmPackage.version), `${optionPath} package version could not be found`); 226 | 227 | validatedPackage.version = packageNpmPackage.version; 228 | 229 | // always copy even when using cdn 230 | if (isDefined(copy)) { 231 | const processCopyItem = copyItem => { 232 | let { fromAbsolute = false, from, to, ...otherOptions } = copyItem; 233 | to = getPackagePath(packageName, packageVersion, to); 234 | to = usePackagesPath ? addPackagesPath(to) : to; 235 | from = fromAbsolute ? from : path.join(packagePath, from); 236 | return { 237 | ...otherOptions, 238 | from, 239 | to 240 | }; 241 | }; 242 | validatedPackage.copy = copy.map(processCopyItem); 243 | } 244 | 245 | const applyExternal = script => { 246 | if (isDefined(script.variableName)) { 247 | assert(!isDefined(script.external), `${optionPath}.scripts object variableName and external cannot be used together`); 248 | const { variableName } = script; 249 | assert(isString(variableName), `${optionPath}.scripts object variableName should be a string`); 250 | script = { 251 | ...script, 252 | external: { 253 | packageName, 254 | variableName 255 | } 256 | }; 257 | } 258 | return script; 259 | }; 260 | 261 | const baseOptions = { ...mainOptions, ...packageOptions }; 262 | 263 | const applyCdnDevPackagePath = (tag, optionName) => { 264 | const { useCdn, getCdnPath } = { ...baseOptions, ...getValidatedCdnOptions(tag, optionPath) }; 265 | 266 | let cdnPath = tag.path; 267 | if (isDefined(tag.cdnPath)) { 268 | assert(isString(tag.cdnPath), `${optionPath}.${optionName}.cdnPath should be a string`); 269 | cdnPath = tag.cdnPath; 270 | } 271 | if (isDefined(tag.devPath)) { 272 | assert(isString(tag.devPath), `${optionPath}.${optionName}.devPath should be a string`); 273 | } 274 | let newTag = { 275 | ...validatedPackage, 276 | ...tag, 277 | packageName 278 | }; 279 | if (useCdn) { 280 | newTag = { 281 | ...newTag, 282 | path: getCdnPath(packageName, packageVersion, cdnPath), 283 | publicPath: false, 284 | hash: false, 285 | useCdn: true 286 | }; 287 | } else { 288 | const packagePath = getPackagePath(packageName, packageVersion, tag.path); 289 | newTag = { 290 | ...newTag, 291 | path: usePackagesPath ? addPackagesPath(packagePath) : packagePath 292 | }; 293 | if (isDefined(tag.devPath)) { 294 | const devPackagePath = getPackagePath(packageName, packageVersion, tag.devPath); 295 | newTag.devPath = usePackagesPath ? addPackagesPath(devPackagePath) : devPackagePath; 296 | } 297 | } 298 | return newTag; 299 | }; 300 | 301 | if (isDefined(links)) { 302 | validatedPackage.links = links.map(tag => applyCdnDevPackagePath(tag, 'links')); 303 | } 304 | if (isDefined(scripts)) { 305 | validatedPackage.scripts = scripts.map(tag => applyCdnDevPackagePath(tag, 'scripts')); 306 | validatedPackage.scripts = validatedPackage.scripts.map(applyExternal); 307 | } 308 | return validatedPackage; 309 | }; 310 | 311 | function HtmlWebpackDeployPlugin (options) { 312 | const copyList = []; 313 | const linkList = []; 314 | const scriptList = []; 315 | const validatedOptions = getValidatedRootOptions(options, `${PLUGIN_NAME}.options`); 316 | const { assets, packages, files, prependExternals } = validatedOptions; 317 | const addSection = section => { 318 | const { copy, links, scripts } = section; 319 | if (isDefined(copy)) { 320 | copyList.push(...copy); 321 | } 322 | if (isDefined(links)) { 323 | linkList.push(...links); 324 | } 325 | if (isDefined(scripts)) { 326 | scriptList.push(...scripts); 327 | } 328 | }; 329 | if (isDefined(assets)) { 330 | addSection(assets); 331 | } 332 | if (isDefined(packages)) { 333 | Object.keys(packages).forEach(packageName => { 334 | addSection(packages[packageName]); 335 | }); 336 | } 337 | 338 | this.options = { 339 | copy: copyList, 340 | links: linkList, 341 | scripts: scriptList, 342 | files, 343 | prependExternals 344 | }; 345 | } 346 | 347 | HtmlWebpackDeployPlugin.prototype.apply = function (compiler) { 348 | let { copy, links, scripts, files, prependExternals } = this.options; 349 | if (compiler.options.mode === 'development') { 350 | const applyDevPath = tag => { 351 | if (isDefined(tag.devPath) && !tag.useCdn) { 352 | tag = { 353 | ...tag, 354 | path: tag.devPath 355 | }; 356 | } 357 | return tag; 358 | }; 359 | 360 | links = links.map(applyDevPath); 361 | scripts = scripts.map(applyDevPath); 362 | } 363 | if (copy && (Array.isArray(copy) ? copy.length > 0 : true)) { 364 | new CopyWebpackPlugin({ patterns: copy }).apply(compiler); 365 | } 366 | new HtmlWebpackTagsPlugin({ links, scripts, files, prependExternals }).apply(compiler); 367 | }; 368 | 369 | module.exports = HtmlWebpackDeployPlugin; 370 | --------------------------------------------------------------------------------