├── .nvmrc ├── .npmrc ├── example ├── app.js └── webpack.config.js ├── .gitignore ├── spec ├── support │ └── jasmine.json ├── helpers │ └── reporter.js └── webpack.spec.js ├── .editorconfig ├── .travis.yml ├── .eslintrc.json ├── codecov.yml ├── .nycrc ├── LICENSE ├── package.json ├── CODE_OF_CONDUCT.md ├── module.js └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | lts/* -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ -------------------------------------------------------------------------------- /example/app.js: -------------------------------------------------------------------------------- 1 | require('archy'); 2 | require('jasmine'); 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.idea/ 3 | /.nyc_output/ 4 | /coverage/ 5 | /node_modules/ 6 | dist/ 7 | main.js -------------------------------------------------------------------------------- /spec/support/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "spec", 3 | "spec_files": [ 4 | "**/*.spec.js" 5 | ], 6 | "helpers": [ 7 | "helpers/**/*.js" 8 | ] 9 | } -------------------------------------------------------------------------------- /spec/helpers/reporter.js: -------------------------------------------------------------------------------- 1 | const { SpecReporter } = require('jasmine-spec-reporter'); 2 | 3 | const env = jasmine.getEnv(); 4 | env.clearReporters(); 5 | env.addReporter(new SpecReporter()); 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | trim_trailing_whitespace = true 9 | insert_final_newline = false 10 | 11 | [*.js] 12 | insert_final_newline = true -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - stable 4 | - lts/* 5 | # - 6.9 6 | cache: yarn 7 | install: 8 | - travis_retry yarn --ignore-engines 9 | script: 10 | - travis_retry yarn test 11 | after_success: 12 | - yarn global add codecov 13 | - codecov -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jasmine": true 4 | }, 5 | "rules": { 6 | "global-require": 0, 7 | "no-cond-assign": 0, 8 | "no-param-reassign": 0, 9 | "no-underscore-dangle": 0, 10 | "import/no-dynamic-require": 0 11 | }, 12 | "extends": "airbnb-base" 13 | } -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | precision: 2 3 | round: down 4 | range: 90...100 5 | 6 | status: 7 | project: 8 | default: 9 | target: auto 10 | if_not_found: success 11 | patch: true 12 | changes: false 13 | 14 | comment: 15 | layout: "header, diff" 16 | behavior: default -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "check-coverage": true, 3 | "lines": 100, 4 | "statements": 100, 5 | "functions": 100, 6 | "branches": 100, 7 | "exclude": [ 8 | "spec/**" 9 | ], 10 | "reporter": [ 11 | "lcov", 12 | "text" 13 | ], 14 | "cache": true, 15 | "watermarks": { 16 | "lines": [ 90, 100 ], 17 | "functions": [ 90, 100 ], 18 | "branches": [ 90, 100 ], 19 | "statements": [ 90, 100 ] 20 | } 21 | } -------------------------------------------------------------------------------- /example/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const WebpackCdnPlugin = require('../'); 4 | 5 | const mode = process.env.NODE_ENV || 'development'; 6 | 7 | module.exports = { 8 | entry: path.join(__dirname, 'app.js'), 9 | mode, 10 | output: { 11 | path: path.join(__dirname, 'dist/assets'), 12 | publicPath: '/assets', 13 | filename: 'app.js', 14 | }, 15 | plugins: [ 16 | new HtmlWebpackPlugin({ filename: '../index.html' }), // output file relative to output.path 17 | new WebpackCdnPlugin({ 18 | modules: [ 19 | { name: 'archy' }, 20 | { name: 'jasmine' }, 21 | ], 22 | prod: mode === 'production', 23 | publicPath: '/node_modules', // override when prod is false 24 | }), 25 | ], 26 | }; 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Van Nguyen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack-cdn-plugin", 3 | "version": "3.3.1", 4 | "description": "A webpack plugin that use externals of CDN urls for production and local node_modules for development", 5 | "main": "module.js", 6 | "module": "module.js", 7 | "scripts": { 8 | "lint": "eslint module.js spec/", 9 | "test": "npm run lint && nyc jasmine", 10 | "commitmsg": "validate-commit-msg", 11 | "precommit": "npm test", 12 | "start": "webpack --config example/webpack.config.js" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/shirotech/webpack-cdn-plugin.git" 17 | }, 18 | "files": [ 19 | "module.js", 20 | "README.md" 21 | ], 22 | "keywords": [ 23 | "webpack", 24 | "html", 25 | "externals", 26 | "script", 27 | "include", 28 | "assets", 29 | "plugin", 30 | "cdn", 31 | "production", 32 | "development" 33 | ], 34 | "author": { 35 | "name": "Van Nguyen", 36 | "email": "van@shirotech.com", 37 | "url": "https://shirotech.com/" 38 | }, 39 | "contributors": [ 40 | { 41 | "name": "Van Nguyen", 42 | "email": "van@shirotech.com", 43 | "url": "https://shirotech.com/" 44 | } 45 | ], 46 | "engines": { 47 | "node": ">=6.9" 48 | }, 49 | "license": "MIT", 50 | "bugs": { 51 | "url": "https://github.com/shirotech/webpack-cdn-plugin/issues" 52 | }, 53 | "homepage": "https://github.com/shirotech/webpack-cdn-plugin", 54 | "devDependencies": { 55 | "archy": "^1.0.0", 56 | "bootstrap-css-only": "^4.4.1", 57 | "eslint": "6.8.0", 58 | "eslint-config-airbnb-base": "14.1.0", 59 | "eslint-plugin-import": "^2.20.2", 60 | "html-webpack-plugin": "^4.0.4", 61 | "husky": "^4.2.3", 62 | "jasmine": "^3.5.0", 63 | "jasmine-spec-reporter": "^5.0.1", 64 | "nyc": "^15.0.1", 65 | "validate-commit-msg": "^2.14.0", 66 | "webpack": "4.42.1", 67 | "webpack-cli": "^3.3.4" 68 | }, 69 | "peerDependencies": { 70 | "html-webpack-plugin": ">=3" 71 | }, 72 | "dependencies": { 73 | "sri-create": "^0.0.3" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at van@shirotech.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /module.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 3 | const createSri = require('sri-create'); 4 | 5 | const empty = ''; 6 | const slash = '/'; 7 | const packageJson = 'package.json'; 8 | const paramsRegex = /:([a-z]+)/gi; 9 | const assetEmptyPrefix = /^\.\//; 10 | const backSlashes = /\\/g; 11 | const nodeModulesRegex = /[\\/]node_modules[\\/].+?[\\/](.*)/; 12 | const DEFAULT_MODULE_KEY = 'defaultCdnModuleKey____'; 13 | 14 | class WebpackCdnPlugin { 15 | constructor({ 16 | modules, 17 | prod, 18 | prodUrl = 'https://unpkg.com/:name@:version/:path', 19 | devUrl = ':name/:path', 20 | publicPath, 21 | optimize = false, 22 | crossOrigin = false, 23 | sri = false, 24 | pathToNodeModules = process.cwd(), 25 | }) { 26 | this.modules = Array.isArray(modules) ? { [DEFAULT_MODULE_KEY]: modules } : modules; 27 | this.prod = prod !== false; 28 | this.prefix = publicPath; 29 | this.url = this.prod ? prodUrl : devUrl; 30 | this.optimize = optimize; 31 | this.crossOrigin = crossOrigin; 32 | this.sri = sri; 33 | this.pathToNodeModules = pathToNodeModules; 34 | } 35 | 36 | apply(compiler) { 37 | const { output } = compiler.options; 38 | if (this.prefix === empty) { 39 | output.publicPath = './'; 40 | } else { 41 | output.publicPath = output.publicPath || '/'; 42 | 43 | if (output.publicPath.slice(-1) !== slash) { 44 | output.publicPath += slash; 45 | } 46 | 47 | this.prefix = this.prod ? empty : this.prefix || output.publicPath; 48 | 49 | if (!this.prod && this.prefix.slice(-1) !== slash) { 50 | this.prefix += slash; 51 | } 52 | } 53 | 54 | const getArgs = [this.url, this.prefix, this.prod, output.publicPath]; 55 | 56 | compiler.hooks.compilation.tap('WebpackCdnPlugin', (compilation) => { 57 | WebpackCdnPlugin._getHtmlHook(compilation, 'beforeAssetTagGeneration', 'htmlWebpackPluginBeforeHtmlGeneration').tapAsync( 58 | 'WebpackCdnPlugin', 59 | (data, callback) => { 60 | const moduleId = data.plugin.options.cdnModule; 61 | if (moduleId !== false) { 62 | let modules = this.modules[moduleId || Reflect.ownKeys(this.modules)[0]]; 63 | if (modules) { 64 | if (this.optimize) { 65 | const usedModules = WebpackCdnPlugin._getUsedModules(compilation); 66 | modules = modules.filter((p) => usedModules[p.name]); 67 | } 68 | 69 | WebpackCdnPlugin._cleanModules(modules, this.pathToNodeModules); 70 | 71 | modules = modules.filter((module) => module.version); 72 | 73 | data.assets.js = WebpackCdnPlugin._getJs(modules, ...getArgs).concat(data.assets.js); 74 | data.assets.css = WebpackCdnPlugin._getCss(modules, ...getArgs).concat( 75 | data.assets.css, 76 | ); 77 | 78 | if (this.prefix === empty) { 79 | WebpackCdnPlugin._assetNormalize(data.assets.js); 80 | WebpackCdnPlugin._assetNormalize(data.assets.css); 81 | } 82 | } 83 | } 84 | callback(null, data); 85 | }, 86 | ); 87 | }); 88 | const externals = compiler.options.externals || {}; 89 | 90 | Reflect.ownKeys(this.modules).forEach((key) => { 91 | const mods = this.modules[key]; 92 | mods 93 | .filter((m) => !m.cssOnly) 94 | .forEach((p) => { 95 | externals[p.name] = p.var || p.name; 96 | }); 97 | }); 98 | 99 | compiler.options.externals = externals; 100 | 101 | if (this.prod && (this.crossOrigin || this.sri)) { 102 | compiler.hooks.afterPlugins.tap('WebpackCdnPlugin', () => { 103 | compiler.hooks.thisCompilation.tap('WebpackCdnPlugin', () => { 104 | compiler.hooks.compilation.tap('HtmlWebpackPluginHooks', (compilation) => { 105 | WebpackCdnPlugin._getHtmlHook(compilation, 'alterAssetTags', 'htmlWebpackPluginAlterAssetTags').tapPromise( 106 | 'WebpackCdnPlugin', 107 | this.alterAssetTags.bind(this), 108 | ); 109 | }); 110 | }); 111 | }); 112 | } 113 | } 114 | 115 | async alterAssetTags(pluginArgs) { 116 | const getProdUrlPrefixes = () => { 117 | const urls = this.modules[Reflect.ownKeys(this.modules)[0]] 118 | .filter((m) => m.prodUrl).map((m) => m.prodUrl); 119 | urls.push(this.url); 120 | return [...new Set(urls)].map((url) => url.split('/:')[0]); 121 | }; 122 | 123 | const prefixes = getProdUrlPrefixes(); 124 | 125 | const filterTag = (tag) => { 126 | const url = (tag.tagName === 'script' && tag.attributes.src) 127 | || (tag.tagName === 'link' && tag.attributes.href); 128 | return url && prefixes.filter((prefix) => url.indexOf(prefix) === 0).length !== 0; 129 | }; 130 | 131 | const processTag = async (tag) => { 132 | if (this.crossOrigin) { 133 | tag.attributes.crossorigin = this.crossOrigin; 134 | } 135 | if (this.sri) { 136 | let url; 137 | if (tag.tagName === 'link') { 138 | url = tag.attributes.href; 139 | } 140 | if (tag.tagName === 'script') { 141 | url = tag.attributes.src; 142 | } 143 | try { 144 | tag.attributes.integrity = await createSri(url); 145 | } catch (e) { 146 | throw new Error(`Failed to generate hash for resource ${url}.\n${e}`); 147 | } 148 | } 149 | }; 150 | 151 | /* istanbul ignore next */ 152 | if (pluginArgs.assetTags) { 153 | await Promise.all(pluginArgs.assetTags.scripts.filter(filterTag).map(processTag)); 154 | await Promise.all(pluginArgs.assetTags.styles.filter(filterTag).map(processTag)); 155 | } else { 156 | await Promise.all(pluginArgs.head.filter(filterTag).map(processTag)); 157 | await Promise.all(pluginArgs.body.filter(filterTag).map(processTag)); 158 | } 159 | } 160 | 161 | /** 162 | * Returns the version of a package in the root of the `node_modules` folder. 163 | * 164 | * If `pathToNodeModules` param is not provided, the current working directory is used instead. 165 | * Note that the path should not end with `node_modules`. 166 | * 167 | * @param {string} name name of the package whose version to get. 168 | * @param {string} [pathToNodeModules=process.cwd()] 169 | */ 170 | static getVersionInNodeModules(name, pathToNodeModules = process.cwd()) { 171 | try { 172 | return require(path.join(pathToNodeModules, 'node_modules', name, packageJson)).version; 173 | } catch (e) { 174 | /* istanbul ignore next */ 175 | return null; 176 | } 177 | } 178 | 179 | /** 180 | * Returns the list of all modules in the bundle 181 | */ 182 | static _getUsedModules(compilation) { 183 | const usedModules = {}; 184 | 185 | compilation 186 | .getStats() 187 | .toJson() 188 | .chunks.forEach((c) => { 189 | c.modules.forEach((m) => { 190 | m.reasons.forEach((r) => { 191 | usedModules[r.userRequest] = true; 192 | }); 193 | }); 194 | }); 195 | 196 | return usedModules; 197 | } 198 | 199 | /** 200 | * - populate the "version" property of each module 201 | * - construct the "paths" and "styles" arrays 202 | * - add a default path if none provided 203 | * 204 | * If `pathToNodeModules` param is not provided, the current working directory is used instead. 205 | * Note that the path should not end with `node_modules`. 206 | * 207 | * @param {Array} modules the modules to clean 208 | * @param {string} [pathToNodeModules] 209 | * @private 210 | */ 211 | static _cleanModules(modules, pathToNodeModules) { 212 | modules.forEach((p) => { 213 | p.version = WebpackCdnPlugin.getVersionInNodeModules(p.name, pathToNodeModules); 214 | 215 | if (!p.paths) { 216 | p.paths = []; 217 | } 218 | if (p.path) { 219 | p.paths.unshift(p.path); 220 | p.path = undefined; 221 | } 222 | if (p.paths.length === 0 && !p.cssOnly) { 223 | p.paths.push( 224 | require 225 | .resolve(p.name) 226 | .match(nodeModulesRegex)[1] 227 | .replace(backSlashes, slash), 228 | ); 229 | } 230 | 231 | if (!p.styles) { 232 | p.styles = []; 233 | } 234 | if (p.style) { 235 | p.styles.unshift(p.style); 236 | p.style = undefined; 237 | } 238 | }); 239 | } 240 | 241 | /** 242 | * Returns the list of CSS files for all modules 243 | * It is the concatenation of "localStyle" and all "styles" 244 | */ 245 | static _getCss(modules, url, prefix, prod, publicPath) { 246 | return WebpackCdnPlugin._get(modules, url, prefix, prod, publicPath, 'styles', 'localStyle'); 247 | } 248 | 249 | /** 250 | * Returns the list of JS files for all modules 251 | * It is the concatenation of "localScript" and all "paths" 252 | */ 253 | static _getJs(modules, url, prefix, prod, publicPath) { 254 | return WebpackCdnPlugin._get(modules, url, prefix, prod, publicPath, 'paths', 'localScript'); 255 | } 256 | 257 | /** 258 | * Generic method to construct the list of files 259 | */ 260 | static _get(modules, url, prefix, prod, publicPath, pathsKey, localKey) { 261 | prefix = prefix || empty; 262 | prod = prod !== false; 263 | 264 | const files = []; 265 | 266 | modules.filter((p) => p[localKey]).forEach((p) => files.push(publicPath + p[localKey])); 267 | 268 | modules.filter((p) => p[pathsKey].length > 0) 269 | .forEach((p) => { 270 | const moduleSpecificUrl = (prod ? p.prodUrl : p.devUrl); 271 | p[pathsKey].forEach((s) => files.push( 272 | prefix + (moduleSpecificUrl || url).replace(paramsRegex, (m, p1) => { 273 | if (prod && p.cdn && p1 === 'name') { 274 | return p.cdn; 275 | } 276 | 277 | return p1 === 'path' ? s : p[p1]; 278 | }), 279 | )); 280 | }); 281 | 282 | return files; 283 | } 284 | 285 | static _assetNormalize(assets) { 286 | assets.forEach((item, i) => { 287 | assets[i] = assets[i].replace(assetEmptyPrefix, empty); 288 | }); 289 | } 290 | 291 | static _getHtmlHook(compilation, v4Name, v3Name) { 292 | try { 293 | /* istanbul ignore next */ 294 | return HtmlWebpackPlugin.getHooks(compilation)[v4Name] || compilation.hooks[v3Name]; 295 | } catch (e) { 296 | /* istanbul ignore next */ 297 | return compilation.hooks[v3Name]; 298 | } 299 | } 300 | } 301 | 302 | module.exports = WebpackCdnPlugin; 303 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | *Note:* This only works on Webpack 4, if you're still on Webpack 3 or below please use version 1.x 2 | 3 | ## CDN extension for the HTML Webpack Plugin 4 | 5 | [![Build Status](https://travis-ci.org/shirotech/webpack-cdn-plugin.svg?branch=master)](https://travis-ci.org/shirotech/webpack-cdn-plugin) 6 | [![codecov](https://codecov.io/gh/shirotech/webpack-cdn-plugin/branch/master/graph/badge.svg)](https://codecov.io/gh/shirotech/webpack-cdn-plugin) 7 | 8 | Enhances [html-webpack-plugin](https://github.com/ampedandwired/html-webpack-plugin) functionality by allowing you to specify the modules you want to externalize from node_modules in development and a CDN in production. 9 | 10 | Basically this will allow you to greatly reduce build time when developing and improve page load performance on production. 11 | 12 | ### Installation 13 | 14 | It is recommended to run webpack on **node 5.x or higher** 15 | 16 | Install the plugin with npm: 17 | 18 | ```bash 19 | npm install webpack-cdn-plugin --save-dev 20 | ``` 21 | 22 | or yarn 23 | 24 | ```bash 25 | yarn add webpack-cdn-plugin --dev 26 | ``` 27 | 28 | ### Basic Usage 29 | 30 | Require the plugin in your webpack config: 31 | 32 | ```javascript 33 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 34 | const WebpackCdnPlugin = require('webpack-cdn-plugin'); 35 | ``` 36 | 37 | Add the plugin to your webpack config: 38 | 39 | ```javascript 40 | module.exports = { 41 | // ... 42 | plugins: [ 43 | new HtmlWebpackPlugin(), 44 | new WebpackCdnPlugin({ 45 | modules: [ 46 | { 47 | name: 'vue', 48 | var: 'Vue', 49 | path: 'dist/vue.runtime.min.js' 50 | }, 51 | { 52 | name: 'vue-router', 53 | var: 'VueRouter', 54 | path: 'dist/vue-router.min.js' 55 | }, 56 | { 57 | name: 'vuex', 58 | var: 'Vuex', 59 | path: 'dist/vuex.min.js' 60 | } 61 | ], 62 | publicPath: '/node_modules' 63 | }) 64 | ] 65 | // ... 66 | }; 67 | ``` 68 | 69 | This will generate an `index.html` file with something like below: 70 | 71 | ```html 72 | 73 | 74 | 75 | 76 | Webpack App 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | ``` 87 | 88 | And u also need config in 89 | 90 | ```javascript 91 | # src/router 92 | import Vue from 'vue' 93 | import VueRouter from 'vue-router' 94 | 95 | if (!window.VueRouter) Vue.use(VueRouter) 96 | // ... 97 | // Any lib need Vue.use() just to do so 98 | ``` 99 | 100 | When you set `prod` to `false`, it will output urls using `publicPath`, so you might need to expose it as some sort of static route. 101 | 102 | ```html 103 | 104 | 105 | 106 | 107 | Webpack App 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | ``` 118 | 119 | You can also use your own custom html template, please refer to [html-webpack-plugin](https://github.com/ampedandwired/html-webpack-plugin). 120 | 121 | Please see the [example](example) folder for a basic working example. 122 | 123 | ### Configuration 124 | 125 | You can pass an object options to WebpackCdnPlugin. Allowed values are as follows: 126 | 127 | ##### `modules`:`array` or `object`(for multiple pages) 128 | 129 | The available options for each module, which is part of an array. 130 | If you want inject cdn for multiple pages, you can config like this: 131 | 132 | ```js 133 | plugins:[ 134 | // ...otherConfig 135 | new HtmlWebpackPlugin({ 136 | title: 'title', 137 | cdnModule: 'vue', 138 | favicon: 'path/to/favicon', 139 | template: 'path/to/template', 140 | filename: 'filename', 141 | // other config 142 | }), 143 | new HtmlWebpackPlugin({ 144 | title: 'title', 145 | cdnModule: 'react', 146 | favicon: 'path/to/favicon', 147 | template: 'path/to/template', 148 | filename: 'filename', 149 | // other config 150 | }), 151 | new WebpackCdnPlugin({ 152 | modules: { 153 | 'vue': [ 154 | { name: 'vue', var: 'Vue', path: 'dist/vue.min.js' }, 155 | ], 156 | 'react': [ 157 | { name: 'react', var: 'React', path: `umd/react.${process.env.NODE_ENV}.min.js` }, 158 | { name: 'react-dom', var: 'ReactDOM', path: `umd/react-dom.${process.env.NODE_ENV}.min.js` }, 159 | ] 160 | } 161 | }) 162 | ] 163 | ``` 164 | 165 | The extra `html-webpack-plugin` option `cdnModule` corresponds to the configuration __key__ that you config inside the `webpack-cdn-plugin` modules 166 | - If you do not give `cdnModule` this value, the default is to take the first one 167 | - If you set `cdnModule = false`, it will not inject cdn 168 | 169 | More detail to see [#13](https://github.com/shirotech/webpack-cdn-plugin/pull/13) 170 | 171 | `name`:`string` 172 | 173 | The name of the module you want to externalize 174 | 175 | `cdn`:`string` (optional) 176 | 177 | If the name from the CDN resource is different from npm, you can override with this i.e. `moment` is `moment.js` on cdnjs 178 | 179 | `var`:`string` (optional) 180 | 181 | A variable that will be assigned to the module in global scope, webpack requires this. If not supplied than it will be the same as the name. 182 | 183 | `path`:`string` (optional) 184 | 185 | You can specify a path to the main file that will be used, this is useful when you want the minified version for example if main does not point to it. 186 | 187 | `paths`:`string[]` (optional) 188 | 189 | You can alternatively specify multiple paths which will be loaded from the CDN. 190 | 191 | `style`:`string` (optional) 192 | 193 | If the module comes with style sheets, you can also specify it as a path. 194 | 195 | `styles`:`string[]` (optional) 196 | 197 | You can alternatively specify multiple style sheets which will be loaded from the CDN. 198 | 199 | `cssOnly`:`boolean` | `false` 200 | 201 | If the module is just a css library, you can specify `cssOnly` to `true`, it will ignore path. 202 | 203 | `localScript`:`string` (option) 204 | 205 | Useful when you wanted to use your own build version of the library for js files 206 | 207 | `localStyle`:`string` (option) 208 | 209 | Useful when you wanted to use your own build version of the library for css files 210 | 211 | `prodUrl`:`string` (option) 212 | 213 | Overrides the global prodUrl, allowing you to specify the CDN location for a specific module 214 | 215 | `devUrl`:`string` (option) 216 | 217 | Overrides the global devUrl, allowing you to specify the location for a specific module 218 | 219 | 220 | ##### `prod`:`boolean` | `true` 221 | 222 | `prod` flag defaults to `true`, which will output uri using the CDN, when `false` it will use the file from `node_modules` folder locally. 223 | 224 | ##### `prodUrl`:`string` | `//unpkg.com/:name@:version/:path` 225 | 226 | You can specify a custom template url with the following replacement strings: 227 | 228 | `:name`: The name of the module e.g. `vue` 229 | 230 | `:version`: The version of the module e.g. `1.0.0` 231 | 232 | `:path`: The path to the file e.g. `lib/app.min.js` 233 | 234 | A common example is you can use cdnjs e.g. `//cdnjs.cloudflare.com/ajax/libs/:name/:version/:path`. If not specified it will fallback to using unpkg.com. 235 | 236 | ##### `devUrl`:`string` | `/:name/:path` 237 | 238 | Similar to `prodUrl`, this option overrides the default template url for when `prod` is `false` 239 | 240 | ##### `publicPath`:`string` (optional) 241 | 242 | Prefixes the assets with this string, if none is provided it will fallback to the one set globally in `webpack.options.output.publicPath`, note that this is always empty when prod is `true` so that it makes use of the CDN location because it is a remote resource. 243 | 244 | ##### `optimize`:`boolean` | `false` 245 | 246 | Set to `true` to ignore every module not actually required in your bundle. 247 | 248 | ##### `crossOrigin`:`string` (optional) 249 | 250 | Allows you to specify a custom `crossorigin` attribute of either `"anonymous"` or `"use-credentials"`, to configure the CORS requests for the element's fetched data. Visit [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_settings_attributes) for more information. 251 | 252 | ##### `sri`:`boolean` | `false` 253 | 254 | Adds a Subresource Integrity (SRI) hash in the integrity attribute when generating tags for static files. See [MDN](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity) for more information. 255 | 256 | ##### `pathToNodeModules?: string` (optional) 257 | 258 | Path to the `node_modules` folder to "serve" packages from. This is used to determinate what version to request for packages from the CDN. 259 | 260 | If not provided, the value returned by `process.cwd()` is used. 261 | 262 | ### Contribution 263 | 264 | This is a pretty simple plugin and caters mostly for my needs. However, I have made it as flexible and customizable as possible. 265 | 266 | If you happen to find any bugs, do please report it in the [issues](/../../issues) or can help improve the codebase, [pull requests](/../../pulls) are always welcomed. 267 | 268 | ### Resources 269 | 270 | - [Webpack vs Gulp](https://shirotech.com/tutorial/webpack-vs-gulp) 271 | - [Managing your Node.js versions](https://shirotech.com/node-js/managing-your-node-js-versions) 272 | 273 | ### Contributors 274 | 275 | Many thanks to the following contributors: 276 | 277 | - [xiaoiver](https://github.com/xiaoiver) 278 | - [QingWei-Li](https://github.com/QingWei-Li) 279 | - [jikkai](https://github.com/jikkai) 280 | - [likun7981](https://github.com/likun7981) 281 | - [kagawagao](https://github.com/kagawagao) 282 | - [mahcloud](https://github.com/mahcloud) 283 | - [mistic100](https://github.com/mistic100) 284 | - [gaje](https://github.com/gaje) 285 | - [myst729](https://github.com/myst729) 286 | - [MrTreasure](https://github.com/MrTreasure) 287 | - [Neo-Zhixing](https://github.com/Neo-Zhixing) 288 | - [G-Rath](https://github.com/G-Rath) 289 | - [prsnca](https://github.com/prsnca) 290 | -------------------------------------------------------------------------------- /spec/webpack.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | const WebpackCdnPlugin = require('../module'); 5 | 6 | const cssMatcher = //g; 7 | const jsMatcher = //g; 8 | 9 | let cssAssets; 10 | let jsAssets; 11 | let cssAssets2; 12 | let jsAssets2; 13 | let cssSri; 14 | let cssSri2; 15 | let jsSri; 16 | let jsSri2; 17 | let cssCrossOrigin; 18 | let jsCrossOrigin; 19 | let cssCrossOrigin2; 20 | let jsCrossOrigin2; 21 | 22 | const versions = { 23 | jasmine: WebpackCdnPlugin.getVersionInNodeModules('jasmine'), 24 | jasmineSpecReporter: WebpackCdnPlugin.getVersionInNodeModules('jasmine-spec-reporter'), 25 | nyc: WebpackCdnPlugin.getVersionInNodeModules('nyc'), 26 | jasmineCore: WebpackCdnPlugin.getVersionInNodeModules('jasmine-core'), 27 | archy: WebpackCdnPlugin.getVersionInNodeModules('archy'), 28 | bootstrapCssOnly: WebpackCdnPlugin.getVersionInNodeModules('bootstrap-css-only'), 29 | }; 30 | 31 | const fs = new webpack.MemoryOutputFileSystem(); 32 | 33 | function runWebpack(callback, config) { 34 | cssAssets = []; 35 | jsAssets = []; 36 | cssAssets2 = []; 37 | jsAssets2 = []; 38 | cssSri = []; 39 | jsSri = []; 40 | cssSri2 = []; 41 | jsSri2 = []; 42 | cssCrossOrigin = []; 43 | jsCrossOrigin = []; 44 | cssCrossOrigin2 = []; 45 | jsCrossOrigin2 = []; 46 | 47 | const compiler = webpack(config); 48 | compiler.outputFileSystem = fs; 49 | 50 | compiler.run((err, stats) => { 51 | const html = stats.compilation.assets['../index.html'].source(); 52 | const html2 = stats.compilation.assets['../index2.html'].source(); 53 | 54 | let matches; 55 | let sriMatches; 56 | 57 | while ((matches = cssMatcher.exec(html))) { 58 | cssAssets.push(matches[1]); 59 | cssCrossOrigin.push(/crossorigin="anonymous"/.test(matches[2])); 60 | if (sriMatches = /integrity="(sha[^"]+)"/.exec(matches[3])) { 61 | cssSri.push(sriMatches[1]); 62 | } 63 | } 64 | while ((matches = cssMatcher.exec(html2))) { 65 | cssAssets2.push(matches[1]); 66 | cssCrossOrigin2.push(/crossorigin="anonymous"/.test(matches[2])); 67 | if (sriMatches = /integrity="(sha[^"]+)"/.exec(matches[3])) { 68 | cssSri2.push(sriMatches[1]); 69 | } 70 | } 71 | 72 | while ((matches = jsMatcher.exec(html))) { 73 | jsAssets.push(matches[1]); 74 | jsCrossOrigin.push(/crossorigin="anonymous"/.test(matches[2])); 75 | if (sriMatches = /integrity="(sha[^"]+)"/.exec(matches[3])) { 76 | jsSri.push(sriMatches[1]); 77 | } 78 | } 79 | while ((matches = jsMatcher.exec(html2))) { 80 | jsAssets2.push(matches[1]); 81 | jsCrossOrigin2.push(/crossorigin="anonymous"/.test(matches[2])); 82 | if (sriMatches = /integrity="(sha[^"]+)"/.exec(matches[3])) { 83 | jsSri2.push(sriMatches[1]); 84 | } 85 | } 86 | 87 | callback(); 88 | }); 89 | } 90 | 91 | function getConfig({ 92 | prod, 93 | publicPath = '/node_modules', 94 | publicPath2 = '/assets', 95 | prodUrl, 96 | moduleProdUrl, 97 | moduleDevUrl, 98 | multiple, 99 | multipleFiles, 100 | optimize, 101 | crossOrigin, 102 | sri, 103 | }) { 104 | const output = { 105 | path: path.join(__dirname, 'dist/assets'), 106 | filename: 'app.js', 107 | }; 108 | 109 | if (publicPath2) { 110 | output.publicPath = publicPath2; 111 | } 112 | 113 | let modules = [ 114 | { name: 'jasmine-spec-reporter', path: 'index.js' }, 115 | { 116 | name: 'nyc', 117 | style: 'style.css', 118 | localStyle: 'local.css', 119 | localScript: 'local.js', 120 | }, 121 | { name: 'jasmine', cdn: 'jasmine2', style: 'style.css' }, 122 | ]; 123 | if (sri) { 124 | if (sri === 'jasmine') { 125 | if (moduleProdUrl) { 126 | modules = [ 127 | { name: 'jasmine', path: 'lib/jasmine.js' }, 128 | ]; 129 | } else { 130 | modules = [ 131 | { name: 'jasmine', path: 'lib/jasmine.js' }, 132 | { name: 'bootstrap-css-only', style: 'css/bootstrap-grid.css', cssOnly: true }, 133 | ]; 134 | } 135 | } else { 136 | modules = [ 137 | { name: 'jasmine', path: 'notfound.js' }, 138 | ]; 139 | } 140 | } 141 | if (moduleProdUrl) { 142 | modules[modules.length - 1].prodUrl = moduleProdUrl; 143 | } 144 | if (moduleDevUrl) { 145 | modules[modules.length - 1].devUrl = moduleDevUrl; 146 | } 147 | if (multiple) { 148 | modules = { 149 | module1: modules, 150 | module2: [ 151 | { name: 'jasmine-core', path: 'index.js' }, 152 | { 153 | name: 'nyc', 154 | style: 'style.css', 155 | localStyle: 'local.css', 156 | localScript: 'local.js', 157 | }, 158 | { name: 'archy', cdn: 'archy', style: 'style.css' }, 159 | ], 160 | }; 161 | } 162 | if (multipleFiles) { 163 | modules = [ 164 | { 165 | name: 'jasmine', 166 | cdn: 'jasmine2', 167 | paths: ['index1.js', 'index2.js'], 168 | styles: ['style1.css', 'style2.css'], 169 | }, 170 | { 171 | name: 'archy', 172 | path: 'index1.js', 173 | paths: ['index2.js'], 174 | style: 'style1.css', 175 | styles: ['style2.css'], 176 | }, 177 | ]; 178 | } 179 | const options = { 180 | modules, 181 | prod, 182 | prodUrl, 183 | optimize, 184 | crossOrigin, 185 | sri, 186 | }; 187 | 188 | if (publicPath !== undefined) { 189 | options.publicPath = publicPath; 190 | } 191 | 192 | return { 193 | mode: prod ? 'production' : 'development', 194 | entry: path.join(__dirname, '../example/app.js'), 195 | output, 196 | optimization: { 197 | minimize: false, 198 | }, 199 | plugins: [ 200 | new HtmlWebpackPlugin({ filename: '../index.html' }), 201 | new HtmlWebpackPlugin({ filename: '../index2.html', cdnModule: 'module2' }), 202 | new HtmlWebpackPlugin({ filename: '../index3.html', cdnModule: false }), 203 | new WebpackCdnPlugin(options), 204 | ], 205 | }; 206 | } 207 | 208 | describe('Webpack Integration', () => { 209 | describe('When `prod` is true', () => { 210 | describe('When `prodUrl` is default', () => { 211 | beforeAll((done) => { 212 | runWebpack(done, getConfig({ prod: true })); 213 | }); 214 | 215 | it('should output the right assets (css)', () => { 216 | expect(cssAssets).toEqual([ 217 | '/assets/local.css', 218 | `https://unpkg.com/nyc@${versions.nyc}/style.css`, 219 | `https://unpkg.com/jasmine2@${versions.jasmine}/style.css`, 220 | ]); 221 | }); 222 | 223 | it('should output the right assets (js)', () => { 224 | expect(jsAssets).toEqual([ 225 | '/assets/local.js', 226 | `https://unpkg.com/jasmine-spec-reporter@${versions.jasmineSpecReporter}/index.js`, 227 | `https://unpkg.com/nyc@${versions.nyc}/index.js`, 228 | `https://unpkg.com/jasmine2@${versions.jasmine}/lib/jasmine.js`, 229 | '/assets/app.js', 230 | ]); 231 | }); 232 | }); 233 | 234 | describe('When `prodUrl` is set', () => { 235 | beforeAll((done) => { 236 | runWebpack( 237 | done, 238 | getConfig({ 239 | prod: true, 240 | prodUrl: '//cdnjs.cloudflare.com/ajax/libs/:name/:version/:path', 241 | }), 242 | ); 243 | }); 244 | 245 | it('should output the right assets (css)', () => { 246 | expect(cssAssets).toEqual([ 247 | '/assets/local.css', 248 | `//cdnjs.cloudflare.com/ajax/libs/nyc/${versions.nyc}/style.css`, 249 | `//cdnjs.cloudflare.com/ajax/libs/jasmine2/${versions.jasmine}/style.css`, 250 | ]); 251 | }); 252 | 253 | it('should output the right assets (js)', () => { 254 | expect(jsAssets).toEqual([ 255 | '/assets/local.js', 256 | `//cdnjs.cloudflare.com/ajax/libs/jasmine-spec-reporter/${ 257 | versions.jasmineSpecReporter 258 | }/index.js`, 259 | `//cdnjs.cloudflare.com/ajax/libs/nyc/${versions.nyc}/index.js`, 260 | `//cdnjs.cloudflare.com/ajax/libs/jasmine2/${versions.jasmine}/lib/jasmine.js`, 261 | '/assets/app.js', 262 | ]); 263 | }); 264 | }); 265 | 266 | describe('When module `prodUrl` is set', () => { 267 | beforeAll((done) => { 268 | runWebpack( 269 | done, 270 | getConfig({ 271 | prod: true, 272 | prodUrl: '//cdnjs.cloudflare.com/ajax/libs/:name/:version/:path', 273 | moduleProdUrl: '//cdn.jsdelivr.net/npm/:name@:version/:path', 274 | }), 275 | ); 276 | }); 277 | 278 | it('should output the right assets (css)', () => { 279 | expect(cssAssets).toEqual([ 280 | '/assets/local.css', 281 | `//cdnjs.cloudflare.com/ajax/libs/nyc/${versions.nyc}/style.css`, 282 | `//cdn.jsdelivr.net/npm/jasmine2@${versions.jasmine}/style.css`, 283 | ]); 284 | }); 285 | 286 | it('should output the right assets (js)', () => { 287 | expect(jsAssets).toEqual([ 288 | '/assets/local.js', 289 | `//cdnjs.cloudflare.com/ajax/libs/jasmine-spec-reporter/${ 290 | versions.jasmineSpecReporter 291 | }/index.js`, 292 | `//cdnjs.cloudflare.com/ajax/libs/nyc/${versions.nyc}/index.js`, 293 | `//cdn.jsdelivr.net/npm/jasmine2@${versions.jasmine}/lib/jasmine.js`, 294 | '/assets/app.js', 295 | ]); 296 | }); 297 | }); 298 | 299 | describe('When module `prodUrl` and `sri` are set', () => { 300 | beforeAll((done) => { 301 | runWebpack( 302 | done, 303 | getConfig({ 304 | prod: true, 305 | sri: 'jasmine', 306 | prodUrl: 'https://cdnjs.cloudflare.com/ajax/libs/:name/:version/:path', 307 | moduleProdUrl: 'https://cdn.jsdelivr.net/npm/:name@:version/:path', 308 | }), 309 | ); 310 | }); 311 | 312 | it('should output the right assets (js)', () => { 313 | expect(jsSri).toEqual(['sha384-GVSvp94Rbje0r89j7JfSj0QfDdJ9BkFy7YUaUZUgKNc4R6ibqFHWgv+eD1oufzAu']); 314 | }); 315 | 316 | it('should output the right assets (js)', () => { 317 | expect(jsAssets).toEqual([ 318 | `https://cdn.jsdelivr.net/npm/jasmine@${versions.jasmine}/lib/jasmine.js`, 319 | '/assets/app.js', 320 | ]); 321 | }); 322 | }); 323 | 324 | describe('When set `multiple` modules', () => { 325 | beforeAll((done) => { 326 | runWebpack( 327 | done, 328 | getConfig({ 329 | prod: true, 330 | multiple: true, 331 | }), 332 | ); 333 | }); 334 | 335 | it('should output the right assets (css)', () => { 336 | expect(cssAssets).toEqual([ 337 | '/assets/local.css', 338 | `https://unpkg.com/nyc@${versions.nyc}/style.css`, 339 | `https://unpkg.com/jasmine2@${versions.jasmine}/style.css`, 340 | ]); 341 | expect(cssAssets2).toEqual([ 342 | '/assets/local.css', 343 | `https://unpkg.com/nyc@${versions.nyc}/style.css`, 344 | `https://unpkg.com/archy@${versions.archy}/style.css`, 345 | ]); 346 | }); 347 | 348 | it('should output the right assets (js)', () => { 349 | expect(jsAssets).toEqual([ 350 | '/assets/local.js', 351 | `https://unpkg.com/jasmine-spec-reporter@${versions.jasmineSpecReporter}/index.js`, 352 | `https://unpkg.com/nyc@${versions.nyc}/index.js`, 353 | `https://unpkg.com/jasmine2@${versions.jasmine}/lib/jasmine.js`, 354 | '/assets/app.js', 355 | ]); 356 | expect(jsAssets2).toEqual([ 357 | '/assets/local.js', 358 | `https://unpkg.com/jasmine-core@${versions.jasmineCore}/index.js`, 359 | `https://unpkg.com/nyc@${versions.nyc}/index.js`, 360 | `https://unpkg.com/archy@${versions.archy}/index.js`, 361 | '/assets/app.js', 362 | ]); 363 | }); 364 | }); 365 | 366 | describe('With multiple files', () => { 367 | beforeAll((done) => { 368 | runWebpack( 369 | done, 370 | getConfig({ 371 | prod: true, publicPath: null, publicPath2: null, multipleFiles: true, 372 | }), 373 | ); 374 | }); 375 | 376 | it('should output the right assets (css)', () => { 377 | expect(cssAssets).toEqual([ 378 | `https://unpkg.com/jasmine2@${versions.jasmine}/style1.css`, 379 | `https://unpkg.com/jasmine2@${versions.jasmine}/style2.css`, 380 | `https://unpkg.com/archy@${versions.archy}/style1.css`, 381 | `https://unpkg.com/archy@${versions.archy}/style2.css`, 382 | ]); 383 | }); 384 | 385 | it('should output the right assets (js)', () => { 386 | expect(jsAssets).toEqual([ 387 | `https://unpkg.com/jasmine2@${versions.jasmine}/index1.js`, 388 | `https://unpkg.com/jasmine2@${versions.jasmine}/index2.js`, 389 | `https://unpkg.com/archy@${versions.archy}/index1.js`, 390 | `https://unpkg.com/archy@${versions.archy}/index2.js`, 391 | '/app.js', 392 | ]); 393 | }); 394 | }); 395 | 396 | describe('When `crossOrigin` is set', () => { 397 | beforeAll((done) => { 398 | runWebpack(done, getConfig({ prod: true, crossOrigin: 'anonymous' })); 399 | }); 400 | 401 | it('should output the right assets (css)', () => { 402 | expect(cssCrossOrigin).toEqual([false, true, true]); 403 | }); 404 | 405 | it('should output the right assets (js)', () => { 406 | expect(jsCrossOrigin).toEqual([false, true, true, true, false]); 407 | }); 408 | }); 409 | 410 | describe('When `sri` generates a hash in prod', () => { 411 | beforeAll((done) => { 412 | runWebpack(done, getConfig({ prod: true, sri: 'jasmine' })); 413 | }); 414 | 415 | it('should output the right assets (js)', () => { 416 | expect(jsSri).toEqual(['sha384-GVSvp94Rbje0r89j7JfSj0QfDdJ9BkFy7YUaUZUgKNc4R6ibqFHWgv+eD1oufzAu']); 417 | }); 418 | 419 | it('should output the right assets (css)', () => { 420 | expect(cssSri).toEqual(['sha384-2c0TqAkCN1roP60Rv0mi/hGc4f/Wcgf55C348nsOdphbp3YncSDjfSLBTO/IbRVh']); 421 | }); 422 | }); 423 | 424 | describe('When `sri` fails to generates a hash in prod', () => { 425 | beforeAll((done) => { 426 | runWebpack(done, getConfig({ prod: true, sri: 'notfound' })); 427 | }); 428 | 429 | it('should trigger exception (js)', () => { 430 | expect(jsSri).toEqual([]); 431 | }); 432 | }); 433 | }); 434 | 435 | describe('When `prod` is false', () => { 436 | describe('When `publicPath` is default', () => { 437 | beforeAll((done) => { 438 | runWebpack(done, getConfig({ prod: false, publicPath: null, publicPath2: null })); 439 | }); 440 | 441 | it('should output the right assets (css)', () => { 442 | expect(cssAssets).toEqual(['/local.css', '/nyc/style.css', '/jasmine/style.css']); 443 | }); 444 | 445 | it('should output the right assets (js)', () => { 446 | expect(jsAssets).toEqual([ 447 | '/local.js', 448 | '/jasmine-spec-reporter/index.js', 449 | '/nyc/index.js', 450 | '/jasmine/lib/jasmine.js', 451 | '/app.js', 452 | ]); 453 | }); 454 | }); 455 | 456 | describe('When `publicPath` is `/`', () => { 457 | beforeAll((done) => { 458 | runWebpack(done, getConfig({ prod: false, publicPath: null, publicPath2: '/' })); 459 | }); 460 | 461 | it('should output the right assets (css)', () => { 462 | expect(cssAssets).toEqual(['/local.css', '/nyc/style.css', '/jasmine/style.css']); 463 | }); 464 | 465 | it('should output the right assets (js)', () => { 466 | expect(jsAssets).toEqual([ 467 | '/local.js', 468 | '/jasmine-spec-reporter/index.js', 469 | '/nyc/index.js', 470 | '/jasmine/lib/jasmine.js', 471 | '/app.js', 472 | ]); 473 | }); 474 | }); 475 | 476 | describe('When `publicPath` is empty', () => { 477 | beforeAll((done) => { 478 | runWebpack(done, getConfig({ prod: false, publicPath: '' })); 479 | }); 480 | 481 | it('should output the right assets (css)', () => { 482 | expect(cssAssets).toEqual(['local.css', 'nyc/style.css', 'jasmine/style.css']); 483 | }); 484 | 485 | it('should output the right assets (js)', () => { 486 | expect(jsAssets).toEqual([ 487 | 'local.js', 488 | 'jasmine-spec-reporter/index.js', 489 | 'nyc/index.js', 490 | 'jasmine/lib/jasmine.js', 491 | 'app.js', 492 | ]); 493 | }); 494 | }); 495 | 496 | describe('When `publicPath` is set', () => { 497 | beforeAll((done) => { 498 | runWebpack(done, getConfig({ prod: false })); 499 | }); 500 | 501 | it('should output the right assets (css)', () => { 502 | expect(cssAssets).toEqual([ 503 | '/assets/local.css', 504 | '/node_modules/nyc/style.css', 505 | '/node_modules/jasmine/style.css', 506 | ]); 507 | }); 508 | 509 | it('should output the right assets (js)', () => { 510 | expect(jsAssets).toEqual([ 511 | '/assets/local.js', 512 | '/node_modules/jasmine-spec-reporter/index.js', 513 | '/node_modules/nyc/index.js', 514 | '/node_modules/jasmine/lib/jasmine.js', 515 | '/assets/app.js', 516 | ]); 517 | }); 518 | }); 519 | 520 | describe('When module `devUrl` is set', () => { 521 | beforeAll((done) => { 522 | runWebpack(done, getConfig({ 523 | prod: false, 524 | moduleDevUrl: ':name/dist/:path', 525 | })); 526 | }); 527 | 528 | it('should output the right assets (css)', () => { 529 | expect(cssAssets).toEqual([ 530 | '/assets/local.css', 531 | '/node_modules/nyc/style.css', 532 | '/node_modules/jasmine/dist/style.css', 533 | ]); 534 | }); 535 | 536 | it('should output the right assets (js)', () => { 537 | expect(jsAssets).toEqual([ 538 | '/assets/local.js', 539 | '/node_modules/jasmine-spec-reporter/index.js', 540 | '/node_modules/nyc/index.js', 541 | '/node_modules/jasmine/dist/lib/jasmine.js', 542 | '/assets/app.js', 543 | ]); 544 | }); 545 | }); 546 | 547 | describe('When set `multiple` modules', () => { 548 | beforeAll((done) => { 549 | runWebpack( 550 | done, 551 | getConfig({ 552 | prod: false, 553 | publicPath: null, 554 | publicPath2: null, 555 | multiple: true, 556 | }), 557 | ); 558 | }); 559 | 560 | it('should output the right assets (css)', () => { 561 | expect(cssAssets).toEqual(['/local.css', '/nyc/style.css', '/jasmine/style.css']); 562 | expect(cssAssets2).toEqual(['/local.css', '/nyc/style.css', '/archy/style.css']); 563 | }); 564 | 565 | it('should output the right assets (js)', () => { 566 | expect(jsAssets).toEqual([ 567 | '/local.js', 568 | '/jasmine-spec-reporter/index.js', 569 | '/nyc/index.js', 570 | '/jasmine/lib/jasmine.js', 571 | '/app.js', 572 | ]); 573 | expect(jsAssets2).toEqual([ 574 | '/local.js', 575 | '/jasmine-core/index.js', 576 | '/nyc/index.js', 577 | '/archy/index.js', 578 | '/app.js', 579 | ]); 580 | }); 581 | }); 582 | 583 | describe('With multiple files', () => { 584 | beforeAll((done) => { 585 | runWebpack( 586 | done, 587 | getConfig({ 588 | prod: false, publicPath: null, publicPath2: null, multipleFiles: true, 589 | }), 590 | ); 591 | }); 592 | 593 | it('should output the right assets (css)', () => { 594 | expect(cssAssets).toEqual([ 595 | '/jasmine/style1.css', 596 | '/jasmine/style2.css', 597 | '/archy/style1.css', 598 | '/archy/style2.css', 599 | ]); 600 | }); 601 | 602 | it('should output the right assets (js)', () => { 603 | expect(jsAssets).toEqual([ 604 | '/jasmine/index1.js', 605 | '/jasmine/index2.js', 606 | '/archy/index1.js', 607 | '/archy/index2.js', 608 | '/app.js', 609 | ]); 610 | }); 611 | }); 612 | 613 | describe('With `optimize`', () => { 614 | beforeAll((done) => { 615 | runWebpack( 616 | done, 617 | getConfig({ 618 | prod: false, publicPath: null, publicPath2: null, optimize: true, 619 | }), 620 | ); 621 | }); 622 | 623 | it('should output the right assets (css)', () => { 624 | expect(cssAssets).toEqual(['/jasmine/style.css']); 625 | }); 626 | 627 | it('should output the right assets (js)', () => { 628 | expect(jsAssets).toEqual(['/jasmine/lib/jasmine.js', '/app.js']); 629 | }); 630 | }); 631 | }); 632 | }); 633 | --------------------------------------------------------------------------------