├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .github └── workflows │ └── release.yml ├── .gitignore ├── .nycrc ├── .releaserc.json ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── examples ├── index-html │ ├── app │ │ ├── hi.jpg │ │ ├── index.html │ │ ├── main.css │ │ └── main.js │ ├── package.json │ └── webpack.config.js └── main-css │ ├── app │ └── main.css │ ├── index.html │ ├── package.json │ └── webpack.config.js ├── package-lock.json ├── package.json ├── src └── extractLoader.js └── test ├── .eslintrc.json ├── extractLoader.test.js ├── mocha.opts ├── modules ├── deep.css ├── error-resolve-loader.js ├── error-resolve.js ├── error-syntax.js ├── error-to-string.js ├── hi.jpg ├── img.css ├── img.html ├── img.js ├── loader.html ├── simple-css-with-query-params-and-loader.js ├── simple-css-with-query-params.js ├── simple.css ├── simple.html ├── simple.js └── stylesheet.html └── support └── compile.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "env", 5 | { 6 | "targets": { 7 | "node": 6 8 | } 9 | } 10 | ] 11 | ], 12 | "plugins": [ 13 | "transform-runtime" 14 | ], 15 | "sourceMaps": true, 16 | "retainLines": true, 17 | "env": { 18 | "test": { 19 | "plugins": ["istanbul"] 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs. 2 | # More information at http://EditorConfig.org 3 | 4 | # No .editorconfig files above the root directory 5 | root = true 6 | 7 | [*] 8 | indent_style = space 9 | indent_size = 4 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | 14 | [package.json] 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # Compiled by babel 2 | lib 3 | 4 | # Compiled by webpack 5 | test/dist 6 | 7 | # Syntax error on purpose 8 | test/modules/error-syntax.js 9 | 10 | examples 11 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "peerigon/base" 4 | ], 5 | "env": { 6 | "node": true 7 | }, 8 | "root": true 9 | } 10 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - master 6 | - next 7 | jobs: 8 | prepare: 9 | runs-on: ubuntu-latest 10 | if: "! contains(github.event.head_commit.message, '[skip ci]')" 11 | steps: 12 | - run: echo "${{ github.event.head_commit.message }}" 13 | publish: 14 | needs: prepare 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v1 18 | - run: npm ci --ignore-scripts 19 | - run: npm test 20 | - run: npm run build 21 | - run: npm install @semantic-release/changelog@3 @semantic-release/git@7 --ignore-scripts --no-save 22 | - uses: codfish/semantic-release-action@v1 23 | env: 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Project specific 2 | lib 3 | test/dist 4 | examples/**/dist 5 | 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | 11 | # Runtime data 12 | pids 13 | *.pid 14 | *.seed 15 | 16 | # Directory for instrumented libs generated by jscoverage/JSCover 17 | lib-cov 18 | 19 | # Coverage directory used by tools like istanbul 20 | coverage 21 | 22 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 23 | .grunt 24 | 25 | # node-waf configuration 26 | .lock-wscript 27 | 28 | # Compiled binary addons (http://nodejs.org/api/addons.html) 29 | build/Release 30 | 31 | # Dependency directory 32 | node_modules 33 | 34 | # Optional npm cache directory 35 | .npm 36 | 37 | # Optional REPL history 38 | .node_repl_history 39 | 40 | # NYC covergae information 41 | .nyc_output -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "reporter": [ 3 | "lcov", 4 | "text" 5 | ], 6 | "include": [ 7 | "src" 8 | ], 9 | "lines": 97, 10 | "statements": 97, 11 | "functions": 93, 12 | "branches": 87, 13 | "check-coverage": true, 14 | "sourceMap": false, 15 | "instrument": false 16 | } -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "branches": ["master", "next"], 3 | "plugins": [ 4 | "@semantic-release/commit-analyzer", 5 | "@semantic-release/release-notes-generator", 6 | "@semantic-release/changelog", 7 | ["@semantic-release/git", { 8 | "assets": ["CHANGELOG.md"] 9 | }], 10 | "@semantic-release/github", 11 | "@semantic-release/npm" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: 2 | - linux 3 | language: node_js 4 | node_js: 5 | - "node" 6 | - "12" 7 | - "10" 8 | - "8" 9 | 10 | script: 11 | - npm test 12 | 13 | after_success: 14 | - npm install coveralls 15 | - npm run coverage && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [5.1.0](https://github.com/peerigon/extract-loader/compare/v5.0.1...v5.1.0) (2020-05-26) 2 | 3 | 4 | ### Features 5 | 6 | * unique placeholders for each match ([#83](https://github.com/peerigon/extract-loader/issues/83)) ([5e61f0c](https://github.com/peerigon/extract-loader/commit/5e61f0c345763d18d3f1be3622cabc86dac8077d)) 7 | 8 | # Change Log 9 | 10 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 11 | 12 | 13 | # [3.2.0](https://github.com/peerigon/extract-loader/compare/v5.0.1...v3.2.0) (2020-05-26) 14 | 15 | 16 | ### Features 17 | 18 | * unique placeholders for each match ([#83](https://github.com/peerigon/extract-loader/issues/83)) ([5e61f0c](https://github.com/peerigon/extract-loader/commit/5e61f0c)) 19 | 20 | 21 | 22 | 23 | # [3.1.0](https://github.com/peerigon/extract-loader/compare/v3.0.0...v3.1.0) (2018-11-26) 24 | 25 | 26 | ### Features 27 | 28 | * Accept function as publicPath option ([#51](https://github.com/peerigon/extract-loader/issues/51)) ([678933e](https://github.com/peerigon/extract-loader/commit/678933e)) 29 | 30 | 31 | 32 | 33 | # [3.0.0](https://github.com/peerigon/extract-loader/compare/v2.0.1...v3.0.0) (2018-08-31) 34 | 35 | 36 | ### Features 37 | 38 | * Add source map support ([#43](https://github.com/peerigon/extract-loader/issues/43)) ([8f56c2f](https://github.com/peerigon/extract-loader/commit/8f56c2f)), closes [#1](https://github.com/peerigon/extract-loader/issues/1) 39 | * Enable deep evaluation of dependency graph ([#42](https://github.com/peerigon/extract-loader/issues/42)) ([c5aff66](https://github.com/peerigon/extract-loader/commit/c5aff66)) 40 | 41 | 42 | ### BREAKING CHANGES 43 | 44 | * Although the change is not breaking according to our tests, we assume that there could be problems in certain projects. 45 | 46 | 47 | 48 | 49 | ## [2.0.1](https://github.com/peerigon/extract-loader/compare/v2.0.0...v2.0.1) (2018-03-20) 50 | 51 | Re-Release, because v2.0.0 was missing the `lib/extractLoader.js` file [#37](https://github.com/peerigon/extract-loader/issues/37) 52 | 53 | ### Bug Fixes 54 | * Update package.json `engines` field to properly state Node.js 6+ support 55 | 56 | 57 | 58 | # [2.0.0](https://github.com/peerigon/extract-loader/compare/v1.0.2...v2.0.0) (2018-03-19) 59 | 60 | ### Features 61 | 62 | * Add support for webpack 4 ([77f1a670eea87a7adea05cf66a4d54b2995be0e6](https://github.com/peerigon/extract-loader/commit/77f1a670eea87a7adea05cf66a4d54b2995be0e6)) 63 | 64 | ### Bug Fixes 65 | 66 | * TypeError require(...) is not a function ([050f189](https://github.com/peerigon/extract-loader/commit/050f189)) 67 | 68 | ### BREAKING CHANGES 69 | 70 | * extract-loader does now officially only support node >= 6. No guarantee for older node versions. 71 | 72 | 73 | 74 | ## [1.0.2](https://github.com/peerigon/extract-loader/compare/v1.0.1...v1.0.2) (2018-01-11) 75 | 76 | 77 | 78 | ## [1.0.1](https://github.com/peerigon/extract-loader/compare/v1.0.0...v1.0.1) (2017-08-19) 79 | 80 | ### Bug Fixes 81 | 82 | * Fix problems with aliased paths ([f5a1946a7b54ef962e5af56aaf29d318efaabf66](https://github.com/peerigon/extract-loader/commit/f5a1946a7b54ef962e5af56aaf29d318efaabf66)) 83 | 84 | 85 | 86 | # [1.0.0](https://github.com/peerigon/extract-loader/compare/v0.1.0...v1.0.0) (2017-05-24) 87 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | extract-loader 2 | ============== 3 | **webpack loader to extract HTML and CSS from the bundle.** 4 | 5 | [![](https://img.shields.io/npm/v/extract-loader.svg)](https://www.npmjs.com/package/extract-loader) 6 | [![](https://img.shields.io/npm/dm/extract-loader.svg)](https://www.npmjs.com/package/extract-loader) 7 | [![Dependency Status](https://david-dm.org/peerigon/extract-loader.svg)](https://david-dm.org/peerigon/extract-loader) 8 | [![Build Status](https://travis-ci.org/peerigon/extract-loader.svg?branch=master)](https://travis-ci.org/peerigon/extract-loader) 9 | [![Coverage Status](https://img.shields.io/coveralls/peerigon/extract-loader.svg)](https://coveralls.io/r/peerigon/extract-loader?branch=master) 10 | 11 | The extract-loader evaluates the given source code on the fly and returns the result as string. Its main use-case is to resolve urls within HTML and CSS coming from their respective loaders. Use the [file-loader](https://github.com/webpack/file-loader) to emit the extract-loader's result as separate file. 12 | 13 | ```javascript 14 | import stylesheetUrl from "file-loader!extract-loader!css-loader!main.css"; 15 | // stylesheetUrl will now be the hashed url to the final stylesheet 16 | ``` 17 | 18 | The extract-loader works similar to the [extract-text-webpack-plugin](https://github.com/webpack/extract-text-webpack-plugin) and the [mini-css-extract-plugin](https://github.com/webpack-contrib/mini-css-extract-plugin) and is meant as a lean alternative to it. When evaluating the source code, it provides a fake context which was especially designed to cope with the code generated by the [html-](https://github.com/webpack/html-loader) or the [css-loader](https://github.com/webpack/css-loader). Thus it might not work in other situations. 19 | 20 |
21 | 22 | Installation 23 | ------------------------------------------------------------------------ 24 | 25 | ```bash 26 | $ npm install extract-loader --save-dev 27 | ``` 28 | 29 |
30 | 31 | Examples 32 | ------------------------------------------------------------------------ 33 | 34 | ### [Extracting a main.css](https://github.com/peerigon/extract-loader/tree/master/examples/main-css) 35 | 36 | Bundling CSS with webpack has some nice advantages like referencing images and fonts with hashed urls or [hot module replacement](https://webpack.js.org/concepts/hot-module-replacement) in development. In production, on the other hand, it's not a good idea to apply your stylesheets depending on JS execution. Rendering may be delayed or even a [FOUC](https://en.wikipedia.org/wiki/Flash_of_unstyled_content) might be visible. Thus it's still better to have them as separate files in your final production build. 37 | 38 | With the extract-loader, you are able to reference your `main.css` as regular `entry`. The following `webpack.config.js` shows how to load your styles with the [style-loader](https://github.com/webpack/style-loader) in development and as separate file in production. 39 | 40 | ```js 41 | module.exports = ({ mode }) => { 42 | const pathToMainCss = require.resolve("./app/main.css"); 43 | const loaders = [{ 44 | loader: "css-loader", 45 | options: { 46 | sourceMap: true 47 | } 48 | }]; 49 | 50 | if (mode === "production") { 51 | loaders.unshift( 52 | "file-loader", 53 | "extract-loader" 54 | ); 55 | } else { 56 | loaders.unshift("style-loader"); 57 | } 58 | 59 | return { 60 | mode, 61 | entry: pathToMainCss, 62 | module: { 63 | rules: [ 64 | { 65 | test: pathToMainCss, 66 | loaders: loaders 67 | }, 68 | ] 69 | } 70 | }; 71 | }; 72 | ``` 73 | 74 | ### [Extracting the index.html](https://github.com/peerigon/extract-loader/tree/master/examples/index-html) 75 | 76 | You can even add your `index.html` as `entry` and reference your stylesheets from there. In that case, tell the html-loader to also pick up `link:href`: 77 | 78 | ```js 79 | module.exports = ({ mode }) => { 80 | const pathToMainJs = require.resolve("./app/main.js"); 81 | const pathToIndexHtml = require.resolve("./app/index.html"); 82 | 83 | return { 84 | mode, 85 | entry: [ 86 | pathToMainJs, 87 | pathToIndexHtml 88 | ], 89 | module: { 90 | rules: [ 91 | { 92 | test: pathToIndexHtml, 93 | use: [ 94 | "file-loader", 95 | "extract-loader", 96 | { 97 | loader: "html-loader", 98 | options: { 99 | attrs: ["img:src", "link:href"] 100 | } 101 | } 102 | ] 103 | }, 104 | { 105 | test: /\.css$/, 106 | use: [ 107 | "file-loader", 108 | "extract-loader", 109 | { 110 | loader: "css-loader", 111 | options: { 112 | sourceMap: true 113 | } 114 | } 115 | ] 116 | }, 117 | { 118 | test: /\.jpg$/, 119 | use: "file-loader" 120 | } 121 | ] 122 | } 123 | }; 124 | } 125 | ``` 126 | 127 | turns 128 | 129 | ```html 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | ``` 139 | 140 | into 141 | 142 | 143 | ```html 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | ``` 153 | 154 |
155 | 156 | Source Maps 157 | ------------------------------------------------------------------------ 158 | 159 | If you want source maps in your extracted CSS files, you need to set the [`sourceMap` option](https://github.com/webpack-contrib/css-loader#sourcemap) of the **css-loader**: 160 | 161 | ```js 162 | { 163 | loader: "css-loader", 164 | options: { 165 | sourceMap: true 166 | } 167 | } 168 | ``` 169 | 170 |
171 | 172 | Options 173 | ------------------------------------------------------------------------ 174 | 175 | There is currently exactly one option: `publicPath`. 176 | If you are using a relative `publicPath` in webpack's [output options](https://webpack.js.org/configuration/output/#output-publicpath) and extracting to a file with the `file-loader`, you might need this to account for the location of your extracted file. `publicPath` may be defined as a string or a function that accepts current [loader context](https://webpack.js.org/api/loaders/#the-loader-context) as single argument. 177 | 178 | Example with publicPath option as a string: 179 | 180 | ```js 181 | module.exports = { 182 | output: { 183 | path: path.resolve("./dist"), 184 | publicPath: "dist/" 185 | }, 186 | module: { 187 | rules: [ 188 | { 189 | test: /\.css$/, 190 | use: [ 191 | { 192 | loader: "file-loader", 193 | options: { 194 | name: "assets/[name].[ext]", 195 | }, 196 | }, 197 | { 198 | loader: "extract-loader", 199 | options: { 200 | publicPath: "../", 201 | } 202 | }, 203 | { 204 | loader: "css-loader", 205 | }, 206 | ], 207 | } 208 | ] 209 | } 210 | }; 211 | ``` 212 | 213 | Example with publicPath option as a function: 214 | 215 | ```js 216 | module.exports = { 217 | output: { 218 | path: path.resolve("./dist"), 219 | publicPath: "dist/" 220 | }, 221 | module: { 222 | rules: [ 223 | { 224 | test: /\.css$/, 225 | use: [ 226 | { 227 | loader: "file-loader", 228 | options: { 229 | name: "assets/[name].[ext]", 230 | }, 231 | }, 232 | { 233 | loader: "extract-loader", 234 | options: { 235 | // dynamically return a relative publicPath based on how deep in directory structure the loaded file is in /src/ directory 236 | publicPath: (context) => '../'.repeat(path.relative(path.resolve('src'), context.context).split('/').length), 237 | } 238 | }, 239 | { 240 | loader: "css-loader", 241 | }, 242 | ], 243 | } 244 | ] 245 | } 246 | }; 247 | ``` 248 | 249 | You need another option? Then you should think about: 250 | 251 |
252 | 253 | Contributing 254 | ------------------------------------------------------------------------ 255 | 256 | From opening a bug report to creating a pull request: **every contribution is appreciated and welcome**. If you're planning to implement a new feature or change the api please create an issue first. This way we can ensure that your precious work is not in vain. 257 | 258 | All pull requests should have 100% test coverage (with notable exceptions) and need to pass all tests. 259 | 260 | - Call `npm test` to run the unit tests 261 | - Call `npm run coverage` to check the test coverage (using [istanbul](https://github.com/gotwarlost/istanbul)) 262 | 263 |
264 | 265 | License 266 | ------------------------------------------------------------------------ 267 | 268 | Unlicense 269 | 270 | Sponsors 271 | ------------------------------------------------------------------------ 272 | 273 | [](https://peerigon.com) 274 | -------------------------------------------------------------------------------- /examples/index-html/app/hi.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peerigon/extract-loader/85008407e266ef7d7513d10a68ae9d03bddff7b8/examples/index-html/app/hi.jpg -------------------------------------------------------------------------------- /examples/index-html/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hello World 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/index-html/app/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: url(hi.jpg); 3 | } 4 | -------------------------------------------------------------------------------- /examples/index-html/app/main.js: -------------------------------------------------------------------------------- 1 | console.log("hi"); 2 | -------------------------------------------------------------------------------- /examples/index-html/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "index-html", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "webpack.config.js", 6 | "scripts": { 7 | "build": "../../node_modules/.bin/webpack --mode production" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC" 12 | } 13 | -------------------------------------------------------------------------------- /examples/index-html/webpack.config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = ({ mode }) => { 4 | const pathToMainJs = require.resolve("./app/main.js"); 5 | const pathToIndexHtml = require.resolve("./app/index.html"); 6 | 7 | return { 8 | mode, 9 | entry: [ 10 | pathToMainJs, 11 | pathToIndexHtml 12 | ], 13 | module: { 14 | rules: [ 15 | { 16 | test: pathToIndexHtml, 17 | use: [ 18 | "file-loader", 19 | // should be just "extract-loader" in your case 20 | require.resolve("../../lib/extractLoader.js"), 21 | { 22 | loader: "html-loader", 23 | options: { 24 | attrs: ["img:src", "link:href"] 25 | } 26 | } 27 | ] 28 | }, 29 | { 30 | test: /\.css$/, 31 | use: [ 32 | "file-loader", 33 | // should be just "extract-loader" in your case 34 | require.resolve("../../lib/extractLoader.js"), 35 | { 36 | loader: "css-loader", 37 | options: { 38 | sourceMap: true 39 | } 40 | } 41 | ] 42 | }, 43 | { 44 | test: /\.jpg$/, 45 | use: "file-loader" 46 | } 47 | ] 48 | } 49 | }; 50 | }; 51 | -------------------------------------------------------------------------------- /examples/main-css/app/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: hotpink; 3 | } 4 | -------------------------------------------------------------------------------- /examples/main-css/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Main CSS Example 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /examples/main-css/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "main-css", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "webpack.config.js", 6 | "scripts": { 7 | "build": "../../node_modules/.bin/webpack --mode production" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC" 12 | } 13 | -------------------------------------------------------------------------------- /examples/main-css/webpack.config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = ({ mode }) => { 4 | const pathToMainCss = require.resolve("./app/main.css"); 5 | const loaders = [{ 6 | loader: "css-loader", 7 | options: { 8 | sourceMap: true 9 | } 10 | }]; 11 | 12 | if (mode === "production") { 13 | loaders.unshift( 14 | "file-loader", 15 | // should be just "extract-loader" in your case 16 | require.resolve("../../lib/extractLoader.js"), 17 | ); 18 | } else { 19 | loaders.unshift("style-loader"); 20 | } 21 | 22 | return { 23 | mode, 24 | entry: pathToMainCss, 25 | module: { 26 | rules: [ 27 | { 28 | test: pathToMainCss, 29 | loaders: loaders 30 | }, 31 | ] 32 | } 33 | }; 34 | }; 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "extract-loader", 3 | "version": "3.2.0", 4 | "description": "webpack loader to extract HTML and CSS from the bundle", 5 | "main": "lib/extractLoader.js", 6 | "scripts": { 7 | "build": "babel src -d lib", 8 | "test": "cross-env NODE_ENV=test nyc mocha -R spec", 9 | "posttest": "eslint src test", 10 | "release": "standard-version", 11 | "prepublishOnly": "npm run build" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/peerigon/extract-loader.git" 16 | }, 17 | "keywords": [ 18 | "webpack", 19 | "loader", 20 | "extract", 21 | "html", 22 | "css" 23 | ], 24 | "author": "peerigon ", 25 | "license": "Unlicense", 26 | "bugs": { 27 | "url": "https://github.com/peerigon/extract-loader/issues" 28 | }, 29 | "homepage": "https://github.com/peerigon/extract-loader#readme", 30 | "engines": { 31 | "node": ">= 6.0.0" 32 | }, 33 | "devDependencies": { 34 | "babel-cli": "^6.26.0", 35 | "babel-plugin-istanbul": "^5.0.1", 36 | "babel-plugin-transform-runtime": "^6.23.0", 37 | "babel-register": "^6.26.0", 38 | "chai": "^4.1.0", 39 | "chai-fs": "^2.0.0", 40 | "cross-env": "^5.2.0", 41 | "css-loader": "^1.0.0", 42 | "eslint": "^5.4.0", 43 | "eslint-config-peerigon": "^15.0.2", 44 | "file-loader": "^5.0.2", 45 | "html-loader": "^0.5.5", 46 | "mocha": "^5.2.0", 47 | "nyc": "^13.0.1", 48 | "rimraf": "^2.6.2", 49 | "standard-version": "^4.4.0", 50 | "style-loader": "^0.23.0", 51 | "webpack": "^4.17.1", 52 | "webpack-command": "^0.4.1" 53 | }, 54 | "dependencies": { 55 | "babel-core": "^6.26.3", 56 | "babel-plugin-add-module-exports": "^1.0.2", 57 | "babel-preset-env": "^1.7.0", 58 | "babel-runtime": "^6.26.0", 59 | "btoa": "^1.2.1", 60 | "loader-utils": "^1.1.0", 61 | "resolve": "^1.8.1" 62 | }, 63 | "files": [ 64 | "lib" 65 | ] 66 | } 67 | -------------------------------------------------------------------------------- /src/extractLoader.js: -------------------------------------------------------------------------------- 1 | import vm from "vm"; 2 | import path from "path"; 3 | import {getOptions} from "loader-utils"; 4 | import resolve from "resolve"; 5 | import btoa from "btoa"; 6 | import * as babel from "babel-core"; 7 | 8 | /** 9 | * @typedef {Object} LoaderContext 10 | * @property {function} cacheable 11 | * @property {function} async 12 | * @property {function} addDependency 13 | * @property {function} loadModule 14 | * @property {string} resourcePath 15 | * @property {object} options 16 | */ 17 | 18 | /** 19 | * Executes the given module's src in a fake context in order to get the resulting string. 20 | * 21 | * @this LoaderContext 22 | * @param {string} src 23 | * @throws Error 24 | */ 25 | async function extractLoader(src) { 26 | const done = this.async(); 27 | const options = getOptions(this) || {}; 28 | const publicPath = getPublicPath(options, this); 29 | 30 | this.cacheable(); 31 | 32 | try { 33 | done(null, await evalDependencyGraph({ 34 | loaderContext: this, 35 | src, 36 | filename: this.resourcePath, 37 | publicPath, 38 | })); 39 | } catch (error) { 40 | done(error); 41 | } 42 | } 43 | 44 | function evalDependencyGraph({loaderContext, src, filename, publicPath = ""}) { 45 | const moduleCache = new Map(); 46 | 47 | function loadModule(filename) { 48 | return new Promise((resolve, reject) => { 49 | // loaderContext.loadModule automatically calls loaderContext.addDependency for all requested modules 50 | loaderContext.loadModule(filename, (error, src) => { 51 | if (error) { 52 | reject(error); 53 | } else { 54 | resolve(src); 55 | } 56 | }); 57 | }); 58 | } 59 | 60 | function extractExports(exports) { 61 | const hasBtoa = "btoa" in global; 62 | const previousBtoa = global.btoa; 63 | 64 | global.btoa = btoa; 65 | 66 | try { 67 | return exports.toString(); 68 | } catch (error) { 69 | throw error; 70 | } finally { 71 | if (hasBtoa) { 72 | global.btoa = previousBtoa; 73 | } else { 74 | delete global.btoa; 75 | } 76 | } 77 | } 78 | 79 | function extractQueryFromPath(givenRelativePath) { 80 | const indexOfLastExclMark = givenRelativePath.lastIndexOf("!"); 81 | const indexOfQuery = givenRelativePath.lastIndexOf("?"); 82 | 83 | if (indexOfQuery !== -1 && indexOfQuery > indexOfLastExclMark) { 84 | return { 85 | relativePathWithoutQuery: givenRelativePath.slice(0, indexOfQuery), 86 | query: givenRelativePath.slice(indexOfQuery), 87 | }; 88 | } 89 | 90 | return { 91 | relativePathWithoutQuery: givenRelativePath, 92 | query: "", 93 | }; 94 | } 95 | 96 | async function evalModule(src, filename) { 97 | src = babel.transform(src, { 98 | babelrc: false, 99 | presets: [ 100 | [ 101 | require("babel-preset-env"), { 102 | modules: "commonjs", 103 | targets: {nodejs: "current"}, 104 | }, 105 | ], 106 | ], 107 | plugins: [require("babel-plugin-add-module-exports")], 108 | }).code; 109 | 110 | const script = new vm.Script(src, { 111 | filename, 112 | displayErrors: true, 113 | }); 114 | const newDependencies = []; 115 | const exports = {}; 116 | const sandbox = Object.assign({}, global, { 117 | module: { 118 | exports, 119 | }, 120 | exports, 121 | __webpack_public_path__: publicPath, // eslint-disable-line camelcase 122 | require: givenRelativePath => { 123 | const {relativePathWithoutQuery, query} = extractQueryFromPath(givenRelativePath); 124 | const indexOfLastExclMark = relativePathWithoutQuery.lastIndexOf("!"); 125 | const loaders = givenRelativePath.slice(0, indexOfLastExclMark + 1); 126 | const relativePath = relativePathWithoutQuery.slice(indexOfLastExclMark + 1); 127 | const absolutePath = resolve.sync(relativePath, { 128 | basedir: path.dirname(filename), 129 | }); 130 | const ext = path.extname(absolutePath); 131 | 132 | if (moduleCache.has(absolutePath)) { 133 | return moduleCache.get(absolutePath); 134 | } 135 | 136 | // If the required file is a js file, we just require it with node's require. 137 | // If the required file should be processed by a loader we do not touch it (even if it is a .js file). 138 | if (loaders === "" && ext === ".js") { 139 | // Mark the file as dependency so webpack's watcher is working for the css-loader helper. 140 | // Other dependencies are automatically added by loadModule() below 141 | loaderContext.addDependency(absolutePath); 142 | 143 | const exports = require(absolutePath); // eslint-disable-line import/no-dynamic-require 144 | 145 | moduleCache.set(absolutePath, exports); 146 | 147 | return exports; 148 | } 149 | 150 | const rndPlaceholder = "__EXTRACT_LOADER_PLACEHOLDER__" + rndNumber() + rndNumber(); 151 | 152 | newDependencies.push({ 153 | absolutePath, 154 | absoluteRequest: loaders + absolutePath + query, 155 | rndPlaceholder, 156 | }); 157 | 158 | return rndPlaceholder; 159 | }, 160 | }); 161 | 162 | script.runInNewContext(sandbox); 163 | 164 | const extractedDependencyContent = await Promise.all( 165 | newDependencies.map(async ({absolutePath, absoluteRequest}) => { 166 | const src = await loadModule(absoluteRequest); 167 | 168 | return evalModule(src, absolutePath); 169 | }) 170 | ); 171 | const contentWithPlaceholders = extractExports(sandbox.module.exports); 172 | const extractedContent = extractedDependencyContent.reduce((content, dependencyContent, idx) => { 173 | const pattern = new RegExp(newDependencies[idx].rndPlaceholder, "g"); 174 | 175 | return content.replace(pattern, dependencyContent); 176 | }, contentWithPlaceholders); 177 | 178 | moduleCache.set(filename, extractedContent); 179 | 180 | return extractedContent; 181 | } 182 | 183 | return evalModule(src, filename); 184 | } 185 | 186 | /** 187 | * @returns {string} 188 | */ 189 | function rndNumber() { 190 | return Math.random() 191 | .toString() 192 | .slice(2); 193 | } 194 | 195 | // getPublicPath() encapsulates the complexity of reading the publicPath from the current 196 | // webpack config. Let's keep the complexity in this function. 197 | /* eslint-disable complexity */ 198 | /** 199 | * Retrieves the public path from the loader options, context.options (webpack <4) or context._compilation (webpack 4+). 200 | * context._compilation is likely to get removed in a future release, so this whole function should be removed then. 201 | * See: https://github.com/peerigon/extract-loader/issues/35 202 | * 203 | * @deprecated 204 | * @param {Object} options - Extract-loader options 205 | * @param {Object} context - Webpack loader context 206 | * @returns {string} 207 | */ 208 | function getPublicPath(options, context) { 209 | if ("publicPath" in options) { 210 | return typeof options.publicPath === "function" ? options.publicPath(context) : options.publicPath; 211 | } 212 | 213 | if (context.options && context.options.output && "publicPath" in context.options.output) { 214 | return context.options.output.publicPath; 215 | } 216 | 217 | if (context._compilation && context._compilation.outputOptions && "publicPath" in context._compilation.outputOptions) { 218 | return context._compilation.outputOptions.publicPath; 219 | } 220 | 221 | return ""; 222 | } 223 | /* eslint-enable complexity */ 224 | 225 | // For CommonJS interoperability 226 | module.exports = extractLoader; 227 | export default extractLoader; 228 | -------------------------------------------------------------------------------- /test/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "peerigon/tests" 4 | ], 5 | "env": { 6 | "node": true, 7 | "mocha": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/extractLoader.test.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable promise/always-return, promise/prefer-await-to-then */ 2 | import path from "path"; 3 | import fs from "fs"; 4 | import rimRaf from "rimraf"; 5 | import chai, {expect} from "chai"; 6 | import chaiFs from "chai-fs"; 7 | import extractLoader from "../src/extractLoader"; 8 | import compile from "./support/compile"; 9 | 10 | chai.use(chaiFs); 11 | 12 | describe("extractLoader", () => { 13 | // Using beforeEach so that we can inspect the test compilation afterwards 14 | beforeEach(() => { 15 | rimRaf.sync(path.resolve(__dirname, "dist")); 16 | }); 17 | it("should extract 'hello' into simple.js", () => 18 | compile({testModule: "simple.js"}).then(() => { 19 | const simpleJs = path.resolve(__dirname, "dist/simple-dist.js"); 20 | 21 | expect(simpleJs).to.be.a.file(); 22 | expect(simpleJs).to.have.content("hello"); 23 | })); 24 | it("should extract resource with query params into simple-css-with-query-param.js", () => 25 | compile({testModule: "simple-css-with-query-params.js"}).then(() => { 26 | const simpleJs = path.resolve(__dirname, "dist/simple-css-with-query-params-dist.js"); 27 | 28 | expect(simpleJs).to.be.a.file(); 29 | expect(simpleJs).to.have.content("simple-dist.css"); 30 | })); 31 | it("should extract resource with query params and loader into simple-css-with-query-param-and-loader.js", () => 32 | compile({testModule: "simple-css-with-query-params-and-loader.js"}).then(() => { 33 | const simpleJs = path.resolve(__dirname, "dist/simple-css-with-query-params-and-loader-dist.js"); 34 | 35 | expect(simpleJs).to.be.a.file(); 36 | expect(simpleJs).to.have.content("renamed-simple.css"); 37 | })); 38 | it("should extract the html of modules/simple.html into simple.html", () => 39 | compile({testModule: "simple.html"}).then(() => { 40 | const simpleHtml = path.resolve(__dirname, "dist/simple-dist.html"); 41 | 42 | expect(simpleHtml).to.be.a.file(); 43 | expect(simpleHtml).to.have.content( 44 | fs.readFileSync( 45 | path.resolve(__dirname, "modules/simple.html"), 46 | "utf8" 47 | ) 48 | ); 49 | })); 50 | it("should extract the css of modules/simple.css into simple.css", () => 51 | compile({testModule: "simple.css"}).then(() => { 52 | const originalContent = fs.readFileSync( 53 | path.resolve(__dirname, "modules/simple.css"), 54 | "utf8" 55 | ); 56 | const simpleCss = path.resolve(__dirname, "dist/simple-dist.css"); 57 | 58 | expect(simpleCss).to.be.a.file() 59 | .with.contents.that.match(new RegExp(originalContent)); 60 | })); 61 | it("should extract the source maps", () => 62 | compile({testModule: "simple.css"}).then(() => { 63 | const simpleCss = path.resolve(__dirname, "dist/simple-dist.css"); 64 | 65 | expect(simpleCss).to.be.a.file() 66 | .with.contents.that.match(/\/\*# sourceMappingURL=data:application\/json;charset=utf-8;base64,/); 67 | })); 68 | it("should extract the img url into img.js", () => compile({testModule: "img.js"}).then(() => { 69 | const imgJs = path.resolve(__dirname, "dist/img-dist.js"); 70 | 71 | expect(imgJs).to.be.a.file(); 72 | expect(imgJs).to.have.content("hi-dist.jpg"); 73 | })); 74 | it("should extract the img.html as file, emit the referenced img and rewrite the url", () => 75 | compile({testModule: "img.html"}).then(() => { 76 | const imgHtml = path.resolve(__dirname, "dist/img-dist.html"); 77 | const imgJpg = path.resolve(__dirname, "dist/hi-dist.jpg"); 78 | 79 | expect(imgHtml).to.be.a.file(); 80 | expect(imgJpg).to.be.a.file(); 81 | expect(imgHtml).to.have.content.that.match( 82 | // 83 | ); 84 | })); 85 | it("should extract the img.css as file, emit the referenced img and rewrite the url", () => 86 | compile({testModule: "img.css"}).then(() => { 87 | const imgCss = path.resolve(__dirname, "dist/img-dist.css"); 88 | const imgJpg = path.resolve(__dirname, "dist/hi-dist.jpg"); 89 | 90 | expect(imgCss).to.be.a.file(); 91 | expect(imgJpg).to.be.a.file(); 92 | expect(imgCss).to.have.content.that.match(/ url\(hi-dist\.jpg\);/); 93 | })); 94 | it("should extract the stylesheet.html and the referenced img.css as file, emit the files and rewrite all urls", () => 95 | compile({testModule: "stylesheet.html"}).then(() => { 96 | const stylesheetHtml = path.resolve( 97 | __dirname, 98 | "dist/stylesheet-dist.html" 99 | ); 100 | const imgCss = path.resolve(__dirname, "dist/img-dist.css"); 101 | const imgJpg = path.resolve(__dirname, "dist/hi-dist.jpg"); 102 | 103 | expect(stylesheetHtml).to.be.a.file(); 104 | expect(imgCss).to.be.a.file(); 105 | expect(imgJpg).to.be.a.file(); 106 | expect(stylesheetHtml).to.have.content.that.match( 107 | // 111 | ); 112 | expect(imgCss).to.have.content.that.match(/ url\(hi-dist\.jpg\);/); 113 | })); 114 | it("should extract css files with dependencies", () => 115 | compile({testModule: "deep.css"}).then(() => { 116 | const deepCss = path.resolve( 117 | __dirname, 118 | "dist/deep-dist.css" 119 | ); 120 | // const imgCss = path.resolve(__dirname, "dist/img-dist.css"); 121 | const imgJpg = path.resolve(__dirname, "dist/hi-dist.jpg"); 122 | 123 | expect(deepCss).to.be.a.file(); 124 | // expect(imgCss).to.not.be.a.file(); 125 | expect(imgJpg).to.be.a.file(); 126 | expect(deepCss).to.have.content.that.match(/ url\(hi-dist\.jpg\);/); 127 | })); 128 | it("should track all dependencies", () => 129 | compile({testModule: "stylesheet.html"}).then(stats => { 130 | const basePath = path.dirname(__dirname); // returns the parent dirname 131 | const dependencies = Array.from( 132 | stats.compilation.fileDependencies, 133 | dependency => dependency.slice(basePath.length) 134 | ); 135 | 136 | expect(dependencies.sort()).to.eql( 137 | [ 138 | "/node_modules/css-loader/lib/css-base.js", 139 | "/node_modules/css-loader/lib/url/escape.js", 140 | "/test/modules/hi.jpg", 141 | "/test/modules/img.css", 142 | "/test/modules/stylesheet.html", 143 | ].sort() 144 | ); 145 | })); 146 | it("should reference the img with the given publicPath", () => 147 | compile({testModule: "img.html", publicPath: "/test/"}).then(() => { 148 | const imgHtml = path.resolve(__dirname, "dist/img-dist.html"); 149 | const imgJpg = path.resolve(__dirname, "dist/hi-dist.jpg"); 150 | 151 | expect(imgHtml).to.be.a.file(); 152 | expect(imgJpg).to.be.a.file(); 153 | expect(imgHtml).to.have.content.that.match( 154 | // 155 | ); 156 | })); 157 | it("should override the configured publicPath with the publicPath query option", () => 158 | compile({ 159 | testModule: "img.html", 160 | publicPath: "/test/", 161 | loaderOptions: {publicPath: "/other/"}, 162 | }).then(() => { 163 | const imgHtml = path.resolve(__dirname, "dist/img-dist.html"); 164 | const imgJpg = path.resolve(__dirname, "dist/hi-dist.jpg"); 165 | 166 | expect(imgHtml).to.be.a.file(); 167 | expect(imgJpg).to.be.a.file(); 168 | expect(imgHtml).to.have.content.that.match( 169 | // 170 | ); 171 | })); 172 | it("should execute options.publicPath if it's defined as a function", done => { 173 | let publicPathCalledWithContext = false; 174 | const loaderContext = { 175 | async: () => () => done(), 176 | cacheable() {}, 177 | query: { 178 | publicPath: context => { 179 | publicPathCalledWithContext = context === loaderContext; 180 | 181 | return ""; 182 | }, 183 | }, 184 | }; 185 | 186 | extractLoader.call(loaderContext, ""); 187 | 188 | expect(publicPathCalledWithContext).to.equal(true); 189 | }); 190 | it("should support explicit loader chains", () => compile({testModule: "loader.html"}).then(() => { 191 | const loaderHtml = path.resolve(__dirname, "dist/loader-dist.html"); 192 | const errJs = path.resolve(__dirname, "dist/err.js"); 193 | 194 | expect(loaderHtml).to.be.a.file(); 195 | expect(errJs).to.have.content("this is a syntax error\n"); 196 | })); 197 | it("should report syntax errors", () => 198 | compile({testModule: "error-syntax.js"}).then( 199 | () => { 200 | throw new Error("Did not throw expected error"); 201 | }, 202 | message => { 203 | expect(message).to.match(/SyntaxError: unknown: Unexpected token/); 204 | } 205 | )); 206 | it("should report resolve errors", () => 207 | compile({testModule: "error-resolve.js"}).then( 208 | () => { 209 | throw new Error("Did not throw expected error"); 210 | }, 211 | message => { 212 | expect(message).to.match(/Error: Cannot find module '\.\/does-not-exist\.jpg'/); 213 | } 214 | )); 215 | it("should report resolve loader errors", () => 216 | compile({testModule: "error-resolve-loader.js"}).then( 217 | () => { 218 | throw new Error("Did not throw expected error"); 219 | }, 220 | message => { 221 | expect(message).to.match(/Error: Can't resolve 'does-not-exist'/); 222 | } 223 | )); 224 | it("should not leak globals when there is an error during toString()", () => 225 | compile({testModule: "error-to-string.js"}).then( 226 | () => { 227 | throw new Error("Did not throw expected error"); 228 | }, 229 | () => { 230 | expect("btoa" in global).to.be.false; 231 | } 232 | )); 233 | it("should restore the original globals when there is an error during toString()", () => { 234 | const myBtoa = {}; 235 | 236 | global.btoa = myBtoa; 237 | 238 | return compile({testModule: "error-to-string.js"}).then( 239 | () => { 240 | throw new Error("Did not throw expected error"); 241 | }, 242 | () => { 243 | expect(global.btoa).to.equal(myBtoa); 244 | } 245 | ); 246 | }); 247 | it("should flag itself as cacheable", done => { 248 | const loaderContext = { 249 | async() { 250 | return () => { 251 | expect(cacheableCalled).to.equal( 252 | true, 253 | "cacheable() has not been called" 254 | ); 255 | done(); 256 | }; 257 | }, 258 | cacheable() { 259 | cacheableCalled = true; 260 | }, 261 | options: {output: {}}, 262 | }; 263 | let cacheableCalled = false; 264 | 265 | extractLoader.call(loaderContext, ""); 266 | }); 267 | }); 268 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require babel-register 2 | --timeout 5000 3 | -------------------------------------------------------------------------------- /test/modules/deep.css: -------------------------------------------------------------------------------- 1 | @import "./img.css"; 2 | -------------------------------------------------------------------------------- /test/modules/error-resolve-loader.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable 2 | import/unambiguous, 3 | import/no-unresolved, 4 | import/no-extraneous-dependencies, 5 | import/no-webpack-loader-syntax 6 | */ 7 | module.exports = require("does-not-exist!./hi.jpg"); 8 | -------------------------------------------------------------------------------- /test/modules/error-resolve.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/unambiguous, import/no-unresolved */ 2 | module.exports = require("./does-not-exist.jpg"); 3 | -------------------------------------------------------------------------------- /test/modules/error-syntax.js: -------------------------------------------------------------------------------- 1 | this is a syntax error 2 | -------------------------------------------------------------------------------- /test/modules/error-to-string.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/unambiguous */ 2 | 3 | module.exports = { 4 | toString() { 5 | throw new Error("Error during toString()"); 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /test/modules/hi.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peerigon/extract-loader/85008407e266ef7d7513d10a68ae9d03bddff7b8/test/modules/hi.jpg -------------------------------------------------------------------------------- /test/modules/img.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: url(hi.jpg); 3 | } 4 | -------------------------------------------------------------------------------- /test/modules/img.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hello World 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /test/modules/img.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/unambiguous */ 2 | module.exports = require("./hi.jpg"); 3 | -------------------------------------------------------------------------------- /test/modules/loader.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Hello World 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /test/modules/simple-css-with-query-params-and-loader.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/unambiguous, import/no-unresolved, import/no-webpack-loader-syntax */ 2 | module.exports = require("!!file-loader?name=renamed-simple.css!./simple.css?v=1.2"); 3 | -------------------------------------------------------------------------------- /test/modules/simple-css-with-query-params.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/unambiguous, import/no-unresolved */ 2 | module.exports = require("./simple.css?v=1.2"); 3 | -------------------------------------------------------------------------------- /test/modules/simple.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: hotpink; 3 | } 4 | -------------------------------------------------------------------------------- /test/modules/simple.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hello World 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/modules/simple.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/unambiguous */ 2 | module.exports = "hello"; 3 | -------------------------------------------------------------------------------- /test/modules/stylesheet.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hello World 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /test/support/compile.js: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import webpack from "webpack"; 3 | 4 | const pathToExtractLoader = path.resolve( 5 | __dirname, 6 | "../../src/extractLoader.js" 7 | ); 8 | 9 | function compile({testModule, publicPath, loaderOptions}) { 10 | const testModulePath = path.resolve(__dirname, "../modules/", testModule); 11 | 12 | return new Promise((resolve, reject) => { 13 | webpack( 14 | { 15 | mode: "development", 16 | entry: testModulePath, 17 | output: { 18 | path: path.resolve(__dirname, "../dist"), 19 | filename: "bundle.js", 20 | publicPath, 21 | }, 22 | module: { 23 | rules: [ 24 | { 25 | test: /\.js$/, 26 | use: [ 27 | { 28 | loader: "file-loader", 29 | options: { 30 | // appending -dist so we can check if url rewriting is working 31 | name: "[name]-dist.[ext]", 32 | }, 33 | }, 34 | { 35 | loader: pathToExtractLoader, 36 | options: loaderOptions, 37 | }, 38 | ], 39 | }, 40 | { 41 | test: /\.html$/, 42 | use: [ 43 | { 44 | loader: "file-loader", 45 | options: { 46 | name: "[name]-dist.[ext]", 47 | }, 48 | }, 49 | { 50 | loader: pathToExtractLoader, 51 | options: loaderOptions, 52 | }, 53 | { 54 | loader: "html-loader", 55 | options: { 56 | attrs: ["img:src", "link:href"], 57 | interpolate: true, 58 | }, 59 | }, 60 | ], 61 | }, 62 | { 63 | test: /\.css$/, 64 | loaders: [ 65 | { 66 | loader: "file-loader", 67 | options: { 68 | name: "[name]-dist.[ext]", 69 | }, 70 | }, 71 | { 72 | loader: pathToExtractLoader, 73 | options: loaderOptions, 74 | }, 75 | { 76 | loader: "css-loader", 77 | options: { 78 | sourceMap: true, 79 | }, 80 | }, 81 | ], 82 | }, 83 | { 84 | test: /\.jpg$/, 85 | loaders: [ 86 | { 87 | loader: "file-loader", 88 | options: { 89 | name: "[name]-dist.[ext]", 90 | }, 91 | }, 92 | ], 93 | }, 94 | ], 95 | }, 96 | }, 97 | (err, stats) => { // eslint-disable-line promise/prefer-await-to-callbacks 98 | if (err || stats.hasErrors() || stats.hasWarnings()) { 99 | reject(err || stats.toString("minimal")); 100 | } else { 101 | resolve(stats); 102 | } 103 | } 104 | ); 105 | }); 106 | } 107 | 108 | export default compile; 109 | --------------------------------------------------------------------------------