├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── TODO.md ├── dynavers.json ├── index.js ├── lib ├── common.js ├── config.js ├── findFile.js ├── insertStyle.js ├── plugin.js ├── removeFile.js └── replaceTag.js ├── package-lock.json ├── package.json └── spec ├── config-spec.js ├── core-spec.js ├── css-reg-exp-spec.js ├── expectations.js ├── fixtures ├── Indie-Flower.woff2 ├── hot-reload │ ├── entry.js │ ├── index.js │ ├── stylesheet1.css │ └── stylesheet2.css ├── html_template.ejs ├── html_template_with_style.ejs ├── index.js ├── nested_stylesheets.js ├── one_stylesheet.js ├── one_stylesheet_with_web_font.js ├── one_tricky_stylesheet.js ├── page1 │ ├── script.js │ └── stylesheet.css ├── page2 │ ├── script.js │ └── stylesheet.css ├── stylesheet1.css ├── stylesheet2.css ├── stylesheet3.css └── two_stylesheets.js ├── helpers ├── compilation-test.js ├── configs.js ├── core-test.js ├── hot-reload-test.js ├── jasmineSetup.js ├── main-tests.js ├── multi-entry-test.js └── versions.js ├── hot-reload-spec.js ├── postcss.config.js ├── set-position-spec.js └── support └── jasmine.json /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /dynavers_modules/ 3 | /dist/ 4 | /issue/ 5 | npm-debug.log 6 | .swp 7 | .idea 8 | 9 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![npm version](https://badge.fury.io/js/style-ext-html-webpack-plugin.svg)](http://badge.fury.io/js/style-ext-html-webpack-plugin) [![Dependency Status](https://david-dm.org/numical/style-ext-html-webpack-plugin.svg)](https://david-dm.org/numical/style-ext-html-webpack-plugin) [![Build status](https://travis-ci.org/numical/style-ext-html-webpack-plugin.svg)](https://travis-ci.org/numical/style-ext-html-webpack-plugin) [![js-semistandard-style](https://img.shields.io/badge/code%20style-semistandard-brightgreen.svg?style=flat-square)](https://github.com/Flet/semistandard) 2 | 3 | [![NPM](https://nodei.co/npm/style-ext-html-webpack-plugin.png?downloads=true&downloadRank=true&stars=true)](https://nodei.co/npm/style-ext-html-webpack-plugin/) 4 | 5 | ## Deprecation Warning 6 | 7 | **tl;dr** 8 | This project is no longer maintained. It does not support Webpack 5. 9 | 10 | **A bit more detail** 11 | Any look at the [project activity](https://github.com/numical/style-ext-html-webpack-plugin/pulse) will show that I have not been able to maintain this project adequately. 12 | The advent of version 5 of Webpack requires another bout of refactoring that I simply have no time for. 13 | Consequently v4.1.3 will be the last version of this plugin. 14 | My thanks to all users, and especially to all contributors, of this plugin over the years. 15 | My apologies to all those whose webpack 5 migration has been made more complicated by this decision. 16 | 17 | **But I still want to use the plugin...** 18 | Feel free! 19 | My last update works with versions of v4.x of webpack and v4.x of html-webpack-plugin. 20 | Forkers feel free! That's what the licence is for. 21 | In fact, if you fork with an intention to support on-going development, let me know! 22 | I'll happily link to your repository here and offer some tips (main one: ditch backward compatibility - it's a pain). 23 | I will formally archive this repository at the end of the 2020. 24 | 25 | ## Summary 26 | > 27 | > If you use HtmlWebpackPlugin and ExtractTextPlugin or MiniCssExtractPlugin to `` to external stylesheet files, add this plugin to convert the links into `
' to match //. 7 | - Expected '
' to match /
/. 8 | 9 | 2) Core Functionality (webpack v4.35.2, htmlWebpackPlugin v4.0.0-beta.5, mini-css-extract-plugin v0.7.0) plays happily with other plugins using same html plugin event 10 | - Expected '
' to match /
' to match //. 17 | - Expected '
' to match /
/. 18 | 19 | 4) Custom css RegExp (webpack v4.35.2, htmlWebpackPlugin v4.0.0-beta.5, mini-css-extract-plugin v0.7.0) plays happily with other plugins using same html plugin event 20 | - Expected '
' to match /
' to match //. 27 | - Expected '
' to match /
/. 28 | ``` 29 | 1. Multi-entry config for mini-css-extract-plugin 30 | 1. Enable HMR tests for mini-css-extract-plugin 31 | 1. Update README for mini-css-extract-plugin 32 | -------------------------------------------------------------------------------- /dynavers.json: -------------------------------------------------------------------------------- 1 | { 2 | "path": "dynavers_modules", 3 | "versions": { 4 | "webpack" : ["3.12.0", "4.44.2"], 5 | "html-webpack-plugin" : ["3.2.0", "4.0.0-beta.5"], 6 | "extract-text-webpack-plugin" : ["3.0.2", "4.0.0-beta.0"], 7 | "mini-css-extract-plugin": ["0.7.0"] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const StyleExtHtmlWebpackPlugin = require('./lib/plugin.js'); 4 | 5 | module.exports = StyleExtHtmlWebpackPlugin; 6 | -------------------------------------------------------------------------------- /lib/common.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const debug = require('debug')('StyleExt'); 4 | 5 | const PLUGIN = 'StyleExtHtmlWebpackPlugin'; 6 | 7 | const error = msg => { 8 | const err = new Error(`${PLUGIN}: ${msg}`); 9 | err.name = PLUGIN + 'Error'; 10 | debug(`${PLUGIN} error: ${msg}`); 11 | throw err; 12 | }; 13 | 14 | const extractCss = (cssFilename, compilation, minifier) => { 15 | let css = compilation.assets[cssFilename].source(); 16 | debug(`CSS in compilation: ${css}`); 17 | if (minifier) { 18 | css = minifier.minify(css).styles; 19 | debug(`Minified CSS: ${css}`); 20 | } 21 | return css; 22 | }; 23 | 24 | exports.debug = debug; 25 | exports.error = error; 26 | exports.extractCss = extractCss; 27 | -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | 'use-strict'; 2 | 3 | const common = require('./common.js'); 4 | const error = common.error; 5 | 6 | const defaultOptions = Object.freeze({ 7 | enabled: true, 8 | position: 'plugin', 9 | minify: false 10 | }); 11 | const errorMsg = 'invalid args - please see https://github.com/numical/style-ext-html-webpack-plugin for configuration options'; 12 | 13 | const hasProperty = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop); 14 | 15 | const denormaliseOptions = options => { 16 | const denormalised = Object.assign({}, defaultOptions); 17 | 18 | switch (typeof options) { 19 | case 'undefined': 20 | break; 21 | case 'boolean': 22 | denormalised.enabled = options; 23 | break; 24 | case 'string': 25 | denormalised.enabled = true; 26 | denormalised.cssFilename = options; 27 | break; 28 | case 'object': 29 | if (hasProperty(options, 'enabled')) { 30 | denormalised.enabled = options.enabled; 31 | } 32 | if (hasProperty(options, 'file')) { 33 | denormalised.cssFilename = options.file; 34 | } 35 | if (hasProperty(options, 'chunks')) { 36 | denormalised.chunks = options.chunks; 37 | } 38 | if (hasProperty(options, 'position')) { 39 | denormalised.position = options.position; 40 | switch (denormalised.position) { 41 | case 'plugin': 42 | case 'head-top': 43 | case 'head-bottom': 44 | case 'body-top': 45 | case 'body-bottom': 46 | break; 47 | default: 48 | error(errorMsg); 49 | } 50 | } 51 | if (hasProperty(options, 'minify')) { 52 | if (options.minify === true) { 53 | denormalised.minify = {}; 54 | } else { 55 | denormalised.minify = options.minify; 56 | } 57 | } 58 | if (hasProperty(options, 'cssRegExp')) { 59 | denormalised.cssRegExp = options.cssRegExp; 60 | } 61 | break; 62 | default: 63 | error(errorMsg); 64 | } 65 | return denormalised; 66 | }; 67 | 68 | module.exports = denormaliseOptions; 69 | module.exports.defaultOptions = defaultOptions; 70 | -------------------------------------------------------------------------------- /lib/findFile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const common = require('./common.js'); 4 | 5 | const debug = common.debug; 6 | const error = common.error; 7 | 8 | const CSS_REGEX = /\.css$/; 9 | const NO_FILES = {}; 10 | 11 | const validateCssFile = cssFilename => filename => filename === cssFilename; 12 | 13 | const identifyCssFile = cssRegex => filename => (cssRegex || CSS_REGEX).test(filename); 14 | 15 | const allFiles = () => true; 16 | 17 | const onlyChunkFiles = (chunkNames, htmlWebpackPluginChunks, compilation) => { 18 | // cannot use Array.prototype.includes < node v6 19 | let matchingChunks = compilation.chunks.filter(chunk => chunkNames.indexOf(chunk.name) > -1); 20 | if (htmlWebpackPluginChunks) { 21 | matchingChunks = matchingChunks.filter(chunk => htmlWebpackPluginChunks.indexOf(chunk.name) > -1); 22 | } 23 | return matchingChunks.length > 0 24 | ? filename => matchingChunks.some(chunk => chunk.files.indexOf(filename) > -1) 25 | : NO_FILES; 26 | }; 27 | 28 | const findGeneratedCssFile = (fileMatcher, fileFilter, compilation) => { 29 | const filenames = Object.keys(compilation.assets).filter(fileFilter); 30 | for (const filename of filenames) { 31 | if (fileMatcher(filename)) { 32 | debug(`CSS file in compilation: '${filename}'`); 33 | return filename; 34 | } 35 | } 36 | error(`could not find ExtractTextWebpackPlugin's generated .css file; available files: '${filenames.join()}'`); 37 | }; 38 | 39 | const findCssFile = (options, htmlWebpackPluginOptions, compilation) => { 40 | const fileMatcher = (options.cssFilename) 41 | ? validateCssFile(options.cssFilename) 42 | : identifyCssFile(options.cssRegExp); 43 | const fileFilter = (options.chunks) 44 | ? onlyChunkFiles(options.chunks, htmlWebpackPluginOptions.chunks, compilation) 45 | : allFiles; 46 | if (fileFilter === NO_FILES) { 47 | return null; 48 | } else { 49 | return findGeneratedCssFile(fileMatcher, fileFilter, compilation); 50 | } 51 | }; 52 | 53 | module.exports = findCssFile; 54 | -------------------------------------------------------------------------------- /lib/insertStyle.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const common = require('./common.js'); 4 | const extractCss = common.extractCss; 5 | const debug = common.debug; 6 | const error = common.error; 7 | 8 | const errorMsg = 'invalid args - please see https://github.com/numical/style-ext-html-webpack-plugin for configuration options'; 9 | 10 | const createStyleTag = (cssFilename, compilation, minifier) => { 11 | const css = extractCss(cssFilename, compilation, minifier); 12 | const styleTag = ``; 13 | debug('added new 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /spec/fixtures/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | document.body.innerHTML = document.body.innerHTML + '

index.js

'; 4 | -------------------------------------------------------------------------------- /spec/fixtures/nested_stylesheets.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./stylesheet1.css'); 4 | require('./one_tricky_stylesheet.js'); 5 | require('./index.js'); 6 | -------------------------------------------------------------------------------- /spec/fixtures/one_stylesheet.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./stylesheet1.css'); 4 | require('./index.js'); 5 | -------------------------------------------------------------------------------- /spec/fixtures/one_stylesheet_with_web_font.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./stylesheet3.css'); 4 | require('./index.js'); 5 | -------------------------------------------------------------------------------- /spec/fixtures/one_tricky_stylesheet.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./stylesheet2.css'); 4 | require('./index.js'); 5 | -------------------------------------------------------------------------------- /spec/fixtures/page1/script.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./stylesheet.css'); 4 | require('../index.js'); 5 | -------------------------------------------------------------------------------- /spec/fixtures/page1/stylesheet.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: snow; 3 | } -------------------------------------------------------------------------------- /spec/fixtures/page2/script.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./stylesheet.css'); 4 | require('../index.js'); 5 | -------------------------------------------------------------------------------- /spec/fixtures/page2/stylesheet.css: -------------------------------------------------------------------------------- 1 | /* import statements */ 2 | @import url("https://fonts.googleapis.com/css?family=Indie+Flower"); 3 | 4 | body { 5 | /* deliberate British spelling to be corrected by postcss processing */ 6 | colour: grey; 7 | } 8 | 9 | /* quoted attributes with ' */ 10 | [contenteditable='true']:active, [contenteditable='true']:focus { 11 | border:none; 12 | } 13 | 14 | -------------------------------------------------------------------------------- /spec/fixtures/stylesheet1.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: snow; 3 | } -------------------------------------------------------------------------------- /spec/fixtures/stylesheet2.css: -------------------------------------------------------------------------------- 1 | /* import statements */ 2 | @import url("https://fonts.googleapis.com/css?family=Indie+Flower"); 3 | 4 | body { 5 | /* deliberate British spelling to be corrected by postcss processing */ 6 | colour: grey; 7 | } 8 | 9 | /* quoted attributes with ' */ 10 | [contenteditable='true']:active, [contenteditable='true']:focus { 11 | border:none; 12 | } 13 | 14 | -------------------------------------------------------------------------------- /spec/fixtures/stylesheet3.css: -------------------------------------------------------------------------------- 1 | /* fonts */ 2 | @font-face { 3 | font-family: 'Indie-Flower'; 4 | src: url(./Indie-Flower.woff2); 5 | } 6 | 7 | body { 8 | colour: blue; 9 | } 10 | 11 | -------------------------------------------------------------------------------- /spec/fixtures/two_stylesheets.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./stylesheet1.css'); 4 | require('./stylesheet2.css'); 5 | require('./index.js'); 6 | -------------------------------------------------------------------------------- /spec/helpers/compilation-test.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jasmine */ 2 | /* global since:false */ 3 | 'use strict'; 4 | 5 | const path = require('path'); 6 | const fs = require('fs'); 7 | const OUTPUT_DIR = path.join(__dirname, '../../dist'); 8 | 9 | module.exports = (err, stats, htmlFile, jsFile, expected) => { 10 | testError(err); 11 | testCompilation(stats.compilation.errors); 12 | testCompilation(stats.compilation.warnings); 13 | testFilesExistence(expected.files, true); 14 | testFilesExistence(expected.not.files, false); 15 | testFileContent(htmlFile, expected.html, true); 16 | testFileContent(htmlFile, expected.not.html, false); 17 | testFileContent(jsFile, expected.js, true); 18 | testFileContent(jsFile, expected.not.js, false); 19 | }; 20 | 21 | function testError (err) { 22 | expect(err).toBeFalsy(); 23 | } 24 | 25 | function testCompilation (msgs) { 26 | msgs = (msgs || []).join('\n'); 27 | expect(msgs).toBe(''); 28 | } 29 | 30 | function testFilesExistence (expectedFiles, expectedToExist) { 31 | expectedFiles.forEach((filename) => { 32 | testFileExistence(filename, expectedToExist); 33 | }); 34 | } 35 | 36 | function testFileExistence (filename, expectedToExist) { 37 | const fileExists = fs.existsSync(path.join(OUTPUT_DIR, filename)); 38 | const msg = expectedToExist 39 | ? `file ${filename} should exist` 40 | : `file ${filename} should not exist`; 41 | since(msg).expect(fileExists).toBe(expectedToExist); 42 | return fileExists; 43 | } 44 | 45 | function testFileContent (filename, expectedContents, expectedToExist) { 46 | if (expectedContents.length > 0) { 47 | const content = getFileContent(filename); 48 | since(`file ${filename} should have content`).expect(content).not.toBeNull(); 49 | expectedContents.forEach((expectedContent) => { 50 | if (expectedToExist) { 51 | const msg = `file ${filename} should include ${expectedContent}`; 52 | testContentExists(content, expectedContent, msg); 53 | } else { 54 | const msg = `file ${filename} should not include ${expectedContent}`; 55 | testContentDoesNotExist(content, expectedContent, msg); 56 | } 57 | }); 58 | } 59 | } 60 | 61 | function getFileContent (filename) { 62 | const fileExists = testFileExistence(filename, true); 63 | return fileExists ? fs.readFileSync(path.join(OUTPUT_DIR, filename)).toString() : null; 64 | } 65 | 66 | function testContentExists (content, expectedContent, msg) { 67 | if (expectedContent instanceof RegExp) { 68 | since(msg).expect(content).toMatch(expectedContent); 69 | } else { 70 | since(msg).expect(content).toContain(expectedContent); 71 | } 72 | } 73 | 74 | function testContentDoesNotExist (content, expectedContent, msg) { 75 | if (expectedContent instanceof RegExp) { 76 | since(msg).expect(content).not.toMatch(expectedContent); 77 | } else { 78 | since(msg).expect(content).not.toContain(expectedContent); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /spec/helpers/configs.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jasmine */ 2 | 'use strict'; 3 | 4 | const path = require('path'); 5 | const version = require('./versions'); 6 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 7 | const StyleExtHtmlWebpackPlugin = require('../../index.js'); 8 | 9 | const baseOptions = Object.freeze({ 10 | cssFilename: 'styles.css', 11 | cssLoaders: ['css-loader'], 12 | entry: 'one_stylesheet', 13 | htmlWebpackOptions: { 14 | hash: true, 15 | template: path.join(__dirname, '../fixtures/html_template.ejs') 16 | }, 17 | outputDir: path.join(__dirname, '../../dist'), 18 | position: null, 19 | styleExtOptions: {} 20 | }); 21 | 22 | const populateOptions = (options, defaultOptions) => { 23 | const defOptions = defaultOptions || {}; 24 | switch (typeof options) { 25 | case 'undefined': 26 | return Object.assign({}, baseOptions, defOptions); 27 | case 'string': 28 | return Object.assign({}, baseOptions, defOptions, { entry: options }); 29 | case 'object': 30 | return Object.assign({}, baseOptions, defOptions, options); 31 | default: 32 | throw new Error(`Invalid options ${options}`); 33 | } 34 | }; 35 | 36 | const adaptForVersion = (config) => { 37 | switch (version.major) { 38 | case 4: 39 | config.mode = 'production'; 40 | config.module.rules = config.module.loaders; 41 | delete config.module.loaders; 42 | return config; 43 | default: 44 | return config; 45 | } 46 | }; 47 | 48 | const baseConfig = (options, defaultOptions) => { 49 | const opts = populateOptions(options, defaultOptions); 50 | const config = { 51 | entry: path.join(__dirname, `../fixtures/${opts.entry}.js`), 52 | output: { 53 | path: opts.outputDir, 54 | filename: 'index_bundle.js' 55 | }, 56 | plugins: [ 57 | new HtmlWebpackPlugin(opts.htmlWebpackOptions), 58 | version.extractPlugin.create(opts.cssFilename), 59 | new StyleExtHtmlWebpackPlugin(opts.styleExtOptions) 60 | ], 61 | module: { 62 | loaders: [ 63 | { 64 | test: /\.css$/, 65 | loader: version.extractPlugin.loader(opts.cssLoaders) 66 | } 67 | ] 68 | } 69 | }; 70 | return adaptForVersion(config); 71 | }; 72 | 73 | const multiEntryConfig = () => { 74 | const { create, loader } = version.extractPlugin; 75 | const page1Extract = create('page1.css'); 76 | const page2Extract = create('page2.css'); 77 | const page1Loader = loader(['css-loader'], page1Extract); 78 | const page2Loader = loader(['css-loader'], page2Extract); 79 | const config = baseConfig(''); 80 | config.entry = { 81 | page1: path.join(__dirname, '../fixtures/page1/script.js'), 82 | page2: path.join(__dirname, '../fixtures/page2/script.js') 83 | }; 84 | config.output.filename = '[name].js'; 85 | config.module.loaders = [ 86 | { 87 | test: /\.css$/, 88 | loader: page1Loader, 89 | include: [ 90 | path.resolve(__dirname, '../fixtures/page1') 91 | ] 92 | }, 93 | { 94 | test: /\.css$/, 95 | loader: page2Loader, 96 | include: [ 97 | path.resolve(__dirname, '../fixtures/page2') 98 | ] 99 | } 100 | ]; 101 | config.plugins = [ 102 | new HtmlWebpackPlugin({ 103 | hash: true, 104 | chunks: ['page1'], 105 | filename: 'page1.html' 106 | }), 107 | new HtmlWebpackPlugin({ 108 | hash: true, 109 | chunks: ['page2'], 110 | filename: 'page2.html' 111 | }), 112 | page1Extract, 113 | page2Extract, 114 | new StyleExtHtmlWebpackPlugin({ 115 | chunks: ['page1'] 116 | }), 117 | new StyleExtHtmlWebpackPlugin({ 118 | chunks: ['page2'] 119 | }) 120 | ]; 121 | return adaptForVersion(config); 122 | }; 123 | 124 | module.exports = { 125 | baseConfig, 126 | multiEntryConfig 127 | }; 128 | -------------------------------------------------------------------------------- /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, 'index.html', 'index_bundle.js', expected); 10 | done(); 11 | }); 12 | }; 13 | -------------------------------------------------------------------------------- /spec/helpers/hot-reload-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /* eslint-env jasmine */ 3 | 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const version = require('./versions.js'); 7 | const createTestDirectory = require('fs-temp/promise').mkdir; 8 | const copyDir = require('ncp'); 9 | const makePromise = require('denodeify'); 10 | const writeFile = makePromise(fs.writeFile); 11 | const testCompilation = require('./compilation-test.js'); 12 | const webpack = require('webpack'); 13 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 14 | const StyleExtHtmlWebpackPlugin = require('../../index.js'); 15 | const debug = require('debug')('StyleExt:hot-reload'); 16 | 17 | const OUTPUT_DIR = path.join(__dirname, '../../dist'); 18 | const FIXTURES_DIR = path.join(__dirname, '../fixtures/hot-reload'); 19 | 20 | module.exports = (expectations, testIterations, done) => { 21 | createTestDirectory() 22 | .then(setup) 23 | .then((setupResults) => { 24 | return new Promise((resolve, reject) => { 25 | try { 26 | // constants for the tests - a mess of interdependencies between the functions 27 | // so references passed around in the 'metadata' envelope 28 | const metaData = { 29 | iterationCount: 0, 30 | watcher: null, 31 | v1StartupHack: null, 32 | testFn: null 33 | }; 34 | const testDir = setupResults[0]; 35 | const compiler = setupResults[1]; 36 | const iterate = iterateFn.bind(null, testDir, testIterations, metaData, done); 37 | const test = (err, stats) => { 38 | testCompilation(err, stats, 'index.html', 'index_bundle.js', expectations.shift()); 39 | iterate(); 40 | }; 41 | metaData.testFn = test; 42 | // see funcion's doc below 43 | addStartupIterations(expectations, testIterations); 44 | // main test loop - the 'iterate' function will cause the 'watch' event to fire again 45 | metaData.watcher = compiler.watch({}, test); 46 | // promise faff to keep the error chain 47 | resolve(); 48 | } catch (err) { 49 | reject(err); 50 | } 51 | }); 52 | }) 53 | .catch((err) => { 54 | done.fail(err); 55 | }); 56 | }; 57 | 58 | const setup = (testDir) => { 59 | return Promise.all([ 60 | copyTestFixtures(testDir), 61 | createCompiler(testDir) 62 | ]); 63 | }; 64 | 65 | const copyTestFixtures = (testDir) => { 66 | return new Promise((resolve, reject) => { 67 | copyDir(FIXTURES_DIR, testDir, (err) => { 68 | (err) ? reject(err) : resolve(testDir); 69 | }); 70 | }); 71 | }; 72 | 73 | const createCompiler = (testDir) => { 74 | return new Promise((resolve, reject) => { 75 | try { 76 | const compiler = webpack(createConfig(testDir)); 77 | resolve(compiler); 78 | } catch (err) { 79 | reject(err); 80 | } 81 | }); 82 | }; 83 | 84 | const createConfig = (testDir) => { 85 | return { 86 | entry: path.join(testDir, 'entry.js'), 87 | output: { 88 | path: OUTPUT_DIR, 89 | filename: 'index_bundle.js' 90 | }, 91 | plugins: [ 92 | // note: cacheing must be OFF 93 | new HtmlWebpackPlugin({ cache: false }), 94 | version.extractPlugin.create('styles.css'), 95 | new StyleExtHtmlWebpackPlugin() 96 | ], 97 | module: { 98 | loaders: [ 99 | { 100 | test: /\.css$/, 101 | loader: version.extractPlugin.loader(['css-loader']) 102 | } 103 | ] 104 | } 105 | }; 106 | }; 107 | 108 | /* 109 | * Main loop logic for test-change-recompile-retest loop. 110 | * The main complication is the webpack v1 startup hack - see 'WTF' below' 111 | */ 112 | const iterateFn = (testDir, testIterations, metaData, done) => { 113 | // startup faff - see WTF below 114 | if (metaData.v1StartupHack) { 115 | clearTimeout(metaData.v1StartupHack); 116 | metaData.v1StartupHack = null; 117 | debug('v1 startup hack cleared'); 118 | } 119 | metaData.iterationCount += 1; 120 | debug(`watch iteration ${metaData.iterationCount}, remaining iterations = ${testIterations.length}`); 121 | // close watcher and test? 122 | if (testIterations.length === 0) { 123 | debug(`closing watcher after ${metaData.iterationCount} iterations`); 124 | metaData.watcher.close(done); 125 | } else { 126 | const testIteration = testIterations.shift(); 127 | if (testIteration.fileToChange) { 128 | // write a change that causes 'watch' event to fire again 129 | debug(`About to write to file ${testIteration.fileToChange}`); 130 | return writeFile( 131 | path.join(testDir, testIteration.fileToChange), 132 | testIteration.fileContents 133 | ); 134 | } else { 135 | // more startup hack faff 136 | debug('no file to write but test iterations left'); 137 | if (version.major === 1 && metaData.iterationCount === 1) { 138 | debug('adding v1 startup hack'); 139 | metaData.v1StartupHack = addv1StartupHack(metaData); 140 | } 141 | } 142 | } 143 | }; 144 | 145 | /* 146 | * Webpack v.2 always calls the 'watch' function's callback TWICE on startup. 147 | * Hence this function duplicates the first expectation. 148 | * However there is a gotcha with Webpack v.1 - see WTF below. 149 | */ 150 | const addStartupIterations = (expectations, testIterations) => { 151 | // expectations.unshift(expectations[0]); 152 | // testIterations.unshift({}); 153 | }; 154 | 155 | /* WTF!? 156 | * What/why 'addv1StartupHack'? 157 | * Webpack v.1 can call the 'watch' function's callback once OR twice on startup. 158 | * This non-determinism is sort-of hinted at in 159 | * https://github.com/webpack/docs/wiki/node.js-api. 160 | * This hack gives Webpack v1 the chance to call the callback a second time, but 161 | * if it does not, the hack simulates that second call. 162 | * Note that, to prevent too many calls, this setTimeout must be cleared in the 163 | * callback function itself. 164 | * Yuk, yuk, yuk. 165 | */ 166 | const addv1StartupHack = (testMetaData) => { 167 | const callback = () => { 168 | debug('v1 startup hack forcing test callback...'); 169 | testMetaData.testFn(); 170 | }; 171 | return setTimeout(callback, jasmine.DEFAULT_TIMEOUT_INTERVAL / 2); 172 | }; 173 | -------------------------------------------------------------------------------- /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/helpers/main-tests.js: -------------------------------------------------------------------------------- 1 | /* eslint-env jasmine */ 2 | 'use strict'; 3 | 4 | const path = require('path'); 5 | const version = require('./versions'); 6 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 7 | const StyleExtHtmlWebpackPlugin = require('../../index.js'); 8 | const testPlugin = require('./core-test.js'); 9 | const testMultiEntry = require('./multi-entry-test.js'); 10 | const { baseConfig, multiEntryConfig } = require('./configs.js'); 11 | 12 | const mainTests = (defaultOptions, baseExpectations, yyy, multiEntryExpectations) => { 13 | it('inlines a single stylesheet', done => { 14 | const config = baseConfig(defaultOptions); 15 | const expected = baseExpectations(); 16 | expected.html = [ 17 | /