├── spec ├── fixtures │ ├── simplescript2.js │ ├── simplescript1.js │ ├── stylesheet.css │ ├── chunk1.js │ ├── chunk2.js │ ├── chunk3.js │ ├── script1.js │ ├── script2.js │ ├── script3.js │ ├── index1.js │ ├── index2.js │ ├── index3.js │ ├── script1_with_style.js │ ├── async_script.js │ └── handlebars_template.hbs ├── support │ └── jasmine.json ├── helpers │ ├── core-test.js │ ├── jasmineSetup.js │ ├── versions.js │ └── compilation-test.js ├── config-spec.js ├── async-spec.js └── core-spec.js ├── .gitignore ├── index.js ├── dynavers.json ├── lib ├── constants.js ├── resource-hints.js ├── initial-chunk-resource-hints.js ├── custom-attributes.js ├── async-chunk-resource-hints.js ├── common.js ├── elements.js ├── config.js └── plugin.js ├── .travis.yml ├── LICENSE ├── package.json └── README.md /spec/fixtures/simplescript2.js: -------------------------------------------------------------------------------- 1 | Date.now(); 2 | -------------------------------------------------------------------------------- /spec/fixtures/simplescript1.js: -------------------------------------------------------------------------------- 1 | console.log('it works!'); 2 | -------------------------------------------------------------------------------- /spec/fixtures/stylesheet.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: snow; 3 | } -------------------------------------------------------------------------------- /spec/fixtures/chunk1.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = 'chunk1'; 4 | -------------------------------------------------------------------------------- /spec/fixtures/chunk2.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = 'chunk2'; 4 | -------------------------------------------------------------------------------- /spec/fixtures/chunk3.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = 'chunk3'; 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /dynavers_modules/ 3 | /dist/ 4 | npm-debug.log 5 | .idea 6 | -------------------------------------------------------------------------------- /spec/fixtures/script1.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./chunk1.js'); 4 | require('./index1.js'); 5 | -------------------------------------------------------------------------------- /spec/fixtures/script2.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./chunk2.js'); 4 | require('./index2.js'); 5 | -------------------------------------------------------------------------------- /spec/fixtures/script3.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./chunk3.js'); 4 | require('./index3.js'); 5 | -------------------------------------------------------------------------------- /spec/fixtures/index1.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | document.body.innerHTML = document.body.innerHTML + '

index1.js

'; 4 | -------------------------------------------------------------------------------- /spec/fixtures/index2.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | document.body.innerHTML = document.body.innerHTML + '

index2.js

'; 4 | -------------------------------------------------------------------------------- /spec/fixtures/index3.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | document.body.innerHTML = document.body.innerHTML + '

index3.js

'; 4 | -------------------------------------------------------------------------------- /spec/fixtures/script1_with_style.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./stylesheet.css'); 4 | require('./chunk1.js'); 5 | require('./index1.js'); 6 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ScriptExtHtmlWebpackPlugin = require('./lib/plugin.js'); 4 | 5 | module.exports = ScriptExtHtmlWebpackPlugin; 6 | -------------------------------------------------------------------------------- /dynavers.json: -------------------------------------------------------------------------------- 1 | { 2 | "path": "dynavers_modules", 3 | "versions": { 4 | "webpack" : ["1.14.0", "2.7.0","3.12.0", "4.44.2"], 5 | "html-webpack-plugin" : ["3.2.0", "4.5.0"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /lib/constants.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const PLUGIN = 'ScriptExtHtmlWebpackPlugin'; 4 | const EVENT = 'html-webpack-plugin-alter-asset-tags'; 5 | 6 | module.exports = { 7 | PLUGIN, 8 | EVENT 9 | }; 10 | -------------------------------------------------------------------------------- /spec/fixtures/async_script.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./chunk1.js'); 4 | require('./index1.js'); 5 | 6 | require.ensure(['./chunk2.js'], () => {}, 'dynamic2'); 7 | require.ensure(['./chunk3.js'], () => {}, 'dynamic3'); 8 | -------------------------------------------------------------------------------- /spec/support/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "spec", 3 | "spec_files": [ 4 | "**/*[sS]pec.js" 5 | ], 6 | "helpers": [ 7 | "helpers/**/*.js" 8 | ], 9 | "stopSpecOnExpectationFailure": false, 10 | "random": false 11 | } 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "11" 4 | - "10" 5 | - "9" 6 | - "8" 7 | - "7" 8 | - "6" 9 | env: 10 | - CXX=g++-4.8 11 | addons: 12 | apt: 13 | sources: 14 | - ubuntu-toolchain-r-test 15 | packages: 16 | - g++-4.8 17 | 18 | -------------------------------------------------------------------------------- /spec/helpers/core-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jasmine */ 2 | 'use strict'; 3 | 4 | const compilationTest = require('./compilation-test.js'); 5 | 6 | module.exports = (config, expected, done) => { 7 | const webpack = require('webpack'); 8 | webpack(config, (err, stats) => { 9 | compilationTest(err, stats, expected, done); 10 | }); 11 | }; 12 | -------------------------------------------------------------------------------- /spec/helpers/jasmineSetup.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jasmine */ 2 | 'use strict'; 3 | 4 | // for debugging 5 | if (process.env.DEBUG) { 6 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 600000; 7 | } 8 | 9 | require('jasmine2-custom-message'); 10 | const SpecReporter = require('jasmine-spec-reporter').SpecReporter; 11 | jasmine.getEnv().addReporter(new SpecReporter()); 12 | -------------------------------------------------------------------------------- /spec/fixtures/handlebars_template.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /lib/resource-hints.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const shouldAddResourceHints = options => { 4 | return !(options.prefetch.test.length === 0 && 5 | options.preload.test.length === 0); 6 | }; 7 | 8 | const createResourceHint = (rel, href) => { 9 | return { 10 | tagName: 'link', 11 | selfClosingTag: true, 12 | attributes: { 13 | rel: rel, 14 | href: href, 15 | as: 'script' 16 | } 17 | }; 18 | }; 19 | 20 | module.exports = { 21 | shouldAddResourceHints, 22 | createResourceHint 23 | }; 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 numical 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 | -------------------------------------------------------------------------------- /lib/initial-chunk-resource-hints.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const CHUNK_OPTIONS = ['all', 'initial']; 4 | 5 | const createResourceHint = require('./resource-hints.js').createResourceHint; 6 | const common = require('./common.js'); 7 | const matches = common.matches; 8 | const getScriptName = common.getScriptName; 9 | const getRawScriptName = common.getRawScriptName; 10 | const hasScriptName = common.hasScriptName; 11 | 12 | const optionsMatch = (option, scriptName) => { 13 | return matches(option.chunks, CHUNK_OPTIONS) && matches(scriptName, option.test); 14 | }; 15 | 16 | const addInitialChunkResourceHints = (options, tags) => { 17 | return tags 18 | .filter(hasScriptName) 19 | .reduce((hints, tag) => { 20 | const scriptName = getScriptName(options, tag); 21 | if (optionsMatch(options.preload, scriptName)) { 22 | hints.push(createResourceHint('preload', getRawScriptName(tag))); 23 | } else if (optionsMatch(options.prefetch, scriptName)) { 24 | hints.push(createResourceHint('prefetch', getRawScriptName(tag))); 25 | } 26 | return hints; 27 | }, 28 | [] 29 | ); 30 | }; 31 | 32 | module.exports = addInitialChunkResourceHints; 33 | -------------------------------------------------------------------------------- /lib/custom-attributes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const CONSTANTS = require('./constants.js'); 4 | 5 | const common = require('./common.js'); 6 | const debug = common.debug; 7 | const getScriptName = common.getScriptName; 8 | const isResourceLink = common.isResourceLink; 9 | const isScript = common.isScript; 10 | const matches = common.matches; 11 | 12 | const shouldAdd = options => { 13 | return options.custom.length > 0; 14 | }; 15 | 16 | const add = (options, tags) => { 17 | const update = updateElement.bind(null, options); 18 | return tags.map(update); 19 | }; 20 | 21 | const updateElement = (options, tag) => { 22 | return (isScript(tag) || isResourceLink(tag)) 23 | ? updateScriptElement(options, tag) 24 | : tag; 25 | }; 26 | 27 | const updateScriptElement = (options, tag) => { 28 | const scriptName = getScriptName(options, tag); 29 | let updated = false; 30 | options.custom.forEach(customOption => { 31 | if (matches(scriptName, customOption.test)) { 32 | tag.attributes = tag.attributes || {}; 33 | tag.attributes[customOption.attribute] = customOption.value; 34 | updated = true; 35 | } 36 | }); 37 | if (updated) { 38 | debug(`${CONSTANTS.PLUGIN}: updated to: ${JSON.stringify(tag)}`); 39 | } 40 | return tag; 41 | }; 42 | 43 | module.exports = { 44 | shouldAdd, 45 | add 46 | }; 47 | -------------------------------------------------------------------------------- /spec/helpers/versions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const setModuleVersion = require('dynavers')('dynavers.json'); 4 | 5 | const VERSIONS = { 6 | webpack1: { 7 | major: 1, 8 | minor: 14, 9 | patch: 0, 10 | display: '1.14.0' 11 | }, 12 | webpack2: { 13 | major: 2, 14 | minor: 7, 15 | patch: 0, 16 | display: '2.7.0' 17 | }, 18 | webpack3: { 19 | major: 3, 20 | minor: 12, 21 | patch: 0, 22 | display: '3.12.0' 23 | }, 24 | webpack4: { 25 | major: 4, 26 | minor: 44, 27 | patch: 2, 28 | display: '4.44.2' 29 | } 30 | }; 31 | 32 | const HWP_VERSION = { 33 | hwp3: { 34 | major: 3, 35 | minor: 2, 36 | patch: 0, 37 | display: '3.2.0' 38 | }, 39 | hwp4: { 40 | major: 4, 41 | minor: 5, 42 | patch: 0, 43 | display: '4.5.0' 44 | } 45 | }; 46 | 47 | const selected = VERSIONS[process.env.VERSION]; 48 | if (selected) { 49 | setModuleVersion('webpack', selected.display, true); 50 | } else { 51 | throw new Error(`Unknown webpack version '${process.env.VERSION}'`); 52 | } 53 | 54 | const selectedForHwp = HWP_VERSION[process.env.HWP_VERSION]; 55 | if (selectedForHwp) { 56 | setModuleVersion('html-webpack-plugin', selectedForHwp.display, true); 57 | } else { 58 | throw new Error(`Unknown html-webpack-plugin version '${process.env.HWP_VERSION}'`); 59 | } 60 | 61 | selected['html-webpack-plugin'] = selectedForHwp; 62 | 63 | module.exports = selected; 64 | -------------------------------------------------------------------------------- /lib/async-chunk-resource-hints.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const CHUNK_OPTIONS = ['all', 'async']; 4 | 5 | const getPublicPath = require('./common.js').getPublicPath; 6 | const createResourceHint = require('./resource-hints.js').createResourceHint; 7 | const matches = require('./common.js').matches; 8 | 9 | const addAsyncChunkResourceHints = (chunks, options) => { 10 | const getRef = generateRef(options); 11 | const hints = []; 12 | chunks 13 | .filter(chunk => !isInitial(chunk)) 14 | .reduce( 15 | (files, chunk) => files.concat(chunk.files), 16 | []) 17 | .forEach(file => { 18 | if (optionsMatch(options.preload, file)) { 19 | hints.push(createResourceHint('preload', getRef(file))); 20 | } else if (optionsMatch(options.prefetch, file)) { 21 | hints.push(createResourceHint('prefetch', getRef(file))); 22 | } 23 | }); 24 | return hints; 25 | }; 26 | 27 | const isInitial = chunk => 28 | chunk.canBeInitial 29 | ? chunk.canBeInitial() 30 | : chunk.isInitial 31 | ? chunk.isInitial() 32 | : chunk.isInitial; 33 | 34 | const optionsMatch = (option, file) => { 35 | return matches(option.chunks, CHUNK_OPTIONS) && matches(file, option.test); 36 | }; 37 | 38 | const generateRef = options => { 39 | const publicPath = getPublicPath(options); 40 | return publicPath 41 | ? file => publicPath + file 42 | : file => file; 43 | }; 44 | 45 | module.exports = addAsyncChunkResourceHints; 46 | -------------------------------------------------------------------------------- /lib/common.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const debug = require('debug')('ScriptExt'); 4 | const separator = '/'; 5 | 6 | const isScript = (tag) => tag.tagName === 'script'; 7 | 8 | const isResourceLink = (tag) => tag.tagName === 'link' && tag.attributes && tag.attributes.as === 'script'; 9 | 10 | const hasScriptName = tag => { 11 | if (isScript(tag)) { 12 | return tag.attributes && tag.attributes.src; 13 | } else if (isResourceLink(tag)) { 14 | return tag.attributes && tag.attributes.href; 15 | } else { 16 | return false; 17 | } 18 | }; 19 | 20 | const getRawScriptName = tag => { 21 | if (isScript(tag)) { 22 | return (tag.attributes && tag.attributes.src) || ''; 23 | } else if (isResourceLink(tag)) { 24 | return (tag.attributes && tag.attributes.href) || ''; 25 | } else { 26 | return ''; 27 | } 28 | }; 29 | 30 | const getPublicPath = options => { 31 | const output = options.compilationOptions.output; 32 | if (output) { 33 | const publicPath = output.publicPath; 34 | if (publicPath) { 35 | return publicPath.endsWith(separator) ? publicPath : publicPath + separator; 36 | } 37 | } 38 | }; 39 | 40 | const getScriptName = (options, tag) => { 41 | let scriptName = getRawScriptName(tag); 42 | const publicPath = getPublicPath(options); 43 | if (publicPath) { 44 | scriptName = scriptName.replace(publicPath, ''); 45 | } 46 | if (options.htmlWebpackOptions.hash) { 47 | scriptName = scriptName.split('?', 1)[0]; 48 | } 49 | return scriptName; 50 | }; 51 | 52 | const matches = (toMatch, matchers) => { 53 | return matchers.some((matcher) => { 54 | if (matcher instanceof RegExp) { 55 | return matcher.test(toMatch); 56 | } else { 57 | return toMatch.includes(matcher); 58 | } 59 | }); 60 | }; 61 | 62 | module.exports = { 63 | debug, 64 | getPublicPath, 65 | getRawScriptName, 66 | getScriptName, 67 | hasScriptName, 68 | isResourceLink, 69 | isScript, 70 | matches 71 | }; 72 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "script-ext-html-webpack-plugin", 3 | "version": "2.1.5", 4 | "description": "Enhances html-webpack-plugin functionality with async and defer attributes for script elements", 5 | "main": "index.js", 6 | "files": [ 7 | "index.js", 8 | "lib/" 9 | ], 10 | "scripts": { 11 | "pretest": "semistandard & install-module-versions dynavers.json", 12 | "test": "npm run test:webpack1 && npm run test:webpack2 && npm run test:webpack3 && npm run test:webpack4 && npm run test:webpack4htmlPlugin4", 13 | "test:webpack1": "cross-env VERSION=webpack1 HWP_VERSION=hwp3 jasmine", 14 | "test:webpack2": "cross-env VERSION=webpack2 HWP_VERSION=hwp3 jasmine", 15 | "test:webpack3": "cross-env VERSION=webpack3 HWP_VERSION=hwp3 jasmine", 16 | "test:webpack4": "cross-env VERSION=webpack4 HWP_VERSION=hwp3 jasmine", 17 | "test:webpack4htmlPlugin4": "cross-env VERSION=webpack4 HWP_VERSION=hwp4 jasmine", 18 | "debug": "cross-env DEBUG=ScriptExt VERSION=webpack4 HWP_VERSION=hwp4 jasmine", 19 | "node-debug": "cross-env DEBUG=ScriptExt VERSION=webpack4 HWP_VERSION=hwp4 node-debug jasmine" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/numical/script-ext-html-webpack-plugin.git" 24 | }, 25 | "keywords": [ 26 | "webpack", 27 | "plugin", 28 | "html-webpack-plugin", 29 | "async", 30 | "defer", 31 | "inline", 32 | "script", 33 | "module", 34 | "resource hints", 35 | "prefetch", 36 | "preload", 37 | "dynamic script", 38 | "async script" 39 | ], 40 | "author": "Mike Evans (https://github.com/numical)", 41 | "license": "MIT", 42 | "css-loader": "^1.0.1", 43 | "bugs": { 44 | "url": "https://github.com/numical/script-ext-html-webpack-plugin/issues" 45 | }, 46 | "homepage": "https://github.com/numical/script-ext-html-webpack-plugin", 47 | "dependencies": { 48 | "debug": "^4.2.0" 49 | }, 50 | "devDependencies": { 51 | "cross-env": "^7.0.2", 52 | "dynavers": "^0.3.1", 53 | "handlebars": "^4.7.6", 54 | "handlebars-loader": "1.7.1", 55 | "jasmine": "^3.6.2", 56 | "jasmine-spec-reporter": "^6.0.0", 57 | "jasmine2-custom-message": "^0.9.3", 58 | "rimraf": "^3.0.2", 59 | "semistandard": "^14.2.3", 60 | "uglifyjs-webpack-plugin": "^2.2.0", 61 | "webpack-config": "7.5.0" 62 | }, 63 | "peerDependencies": { 64 | "webpack": "^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0", 65 | "html-webpack-plugin": "^3.0.0 || ^4.0.0" 66 | }, 67 | "engines": { 68 | "node": ">=6.11.5" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /lib/elements.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const CONSTANTS = require('./constants.js'); 4 | const SYNC = 'sync'; 5 | const ATTRIBUTE_PRIORITIES = [SYNC, 'async', 'defer']; 6 | 7 | const common = require('./common.js'); 8 | const debug = common.debug; 9 | const isScript = common.isScript; 10 | const matches = common.matches; 11 | const getScriptName = common.getScriptName; 12 | 13 | const shouldUpdate = (options) => { 14 | if (ATTRIBUTE_PRIORITIES.indexOf(options.defaultAttribute) < 0) { 15 | throw new Error(`${CONSTANTS.PLUGIN}: invalid default attribute`); 16 | } 17 | return !(options.defaultAttribute === SYNC && 18 | options.inline.test.length === 0 && 19 | options.async.test.length === 0 && 20 | options.defer.test.length === 0 && 21 | options.module.test.length === 0); 22 | }; 23 | 24 | const update = (assets, options, tags) => { 25 | const update = updateElement.bind(null, assets, options); 26 | return tags.map(update); 27 | }; 28 | 29 | const updateElement = (assets, options, tag) => { 30 | return (isScript(tag)) 31 | ? updateScriptElement(assets, options, tag) 32 | : tag; 33 | }; 34 | 35 | const updateScriptElement = (assets, options, tag) => { 36 | debug(`${CONSTANTS.EVENT}: processing