├── .editorconfig ├── .eslintrc.json ├── .gitignore ├── .nycrc ├── .travis.yml ├── LICENSE ├── README.md ├── appveyor.yml ├── lib ├── loader.cjs └── loader.raw.cjs ├── package-lock.json ├── package.json ├── raw.cjs └── test ├── fixtures └── entry.js ├── helpers └── compile.js ├── loader.raw.test.js └── loader.test.js /.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 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "peerigon" 4 | ], 5 | "env": { 6 | "node": true 7 | }, 8 | "root": true 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | # Test coverage data 40 | .nyc_output 41 | coverage 42 | 43 | # Webpack output 44 | test/output/bundle 45 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "reporter": [ 3 | "lcov", 4 | "text" 5 | ], 6 | "include": [ 7 | "lib/**/*.js" 8 | ], 9 | "lines": 100, 10 | "statements": 100, 11 | "functions": 100, 12 | "branches": 100, 13 | "check-coverage": true 14 | } 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: 2 | - linux 3 | language: node_js 4 | node_js: 5 | - "14" 6 | - "15" 7 | 8 | script: 9 | - npm test 10 | 11 | after_success: 12 | - bash <(curl -s https://codecov.io/bash) 13 | -------------------------------------------------------------------------------- /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 | [![npm][npm]][npm-url] 2 | [![node][node]][node-url] 3 | [![npm-stats][npm-stats]][npm-url] 4 | [![deps][deps]][deps-url] 5 | [![travis][travis]][travis-url] 6 | [![appveyor][appveyor]][appveyor-url] 7 | [![coverage][coverage]][coverage-url] 8 | 9 |
10 | 11 | 13 | 14 |
15 | 16 |

inspect-loader

17 | 18 |

Webpack loader designed for loader testing and debugging. Calls a function with the received input.

19 | 20 | 21 |

Install

22 | 23 | ```bash 24 | npm install --save-dev inspect-loader 25 | ``` 26 | 27 |

Example

28 | 29 | Put the **inspect-loader** in front of the loader you want to test and pass in a callback function. The callback function will be called with useful information about the given inputs (`arguments`). It also exposes the internal loader context for further inspection: 30 | 31 | ```js 32 | webpack({ 33 | ... 34 | module: { 35 | rules: [{ 36 | test: /\.js$/, 37 | use: [{ 38 | loader: "inspect-loader", 39 | options: { 40 | callback(inspect) { 41 | console.log(inspect.arguments); 42 | console.log(inspect.context); 43 | console.log(inspect.options); 44 | } 45 | } 46 | }, { 47 | loader: "my-loader" // loader that you want to test/debug 48 | }] 49 | }] 50 | } 51 | }); 52 | ``` 53 | 54 | The loader returns the received arguments, which means that you can place the **inspect-loader** in the middle of your loader pipeline. You can even inspect multiple loaders: 55 | 56 | ```js 57 | webpack({ 58 | ... 59 | use: [{ 60 | loader: "inspect-loader", 61 | options: { 62 | callback: inspectALoader 63 | } 64 | }, { 65 | loader: "a-loader" 66 | }, { 67 | loader: "inspect-loader", 68 | options: { 69 | callback: inspectBLoader 70 | } 71 | }, { 72 | loader: "b-loader" 73 | }] 74 | ... 75 | }); 76 | ``` 77 | 78 | ### Raw 79 | 80 | This package exposes also a raw version that can be used to test [raw loaders](https://webpack.js.org/api/loaders/#-raw-loader): 81 | 82 | ```js 83 | webpack({ 84 | ... 85 | module: { 86 | rules: [{ 87 | test: /\.js$/, 88 | use: [{ 89 | loader: "inspect-loader/raw", 90 | options: { 91 | callback(inspect) { 92 | console.log(inspect.arguments[0] instanceof Buffer); // true 93 | } 94 | } 95 | }, { 96 | loader: "my-raw-loader" // raw loader that you want to test/debug 97 | }] 98 | }] 99 | } 100 | }); 101 | ``` 102 | 103 |

Options

104 | 105 | ### `callback: Function | string` 106 | 107 | Can be a `Function` (preferred) or a `string`. In case it's a string, it is treated as a string reference and will be invoked on the `inspectLoader.callbacks` object like this: 108 | 109 | ```js 110 | const inspectLoader = require("inspect-loader"); 111 | 112 | inspectLoader.callbacks.myCallback = function () { ... }; 113 | 114 | webpack({ 115 | ... 116 | loader: "inspect-loader", 117 | options: { 118 | callback: "myCallback" 119 | } 120 | ... 121 | }); 122 | ``` 123 | 124 | 125 | The callback passes an `inspect` object as single argument that exposes the internal loader state: 126 | 127 | ```js 128 | { 129 | arguments, // A true array that carries all the input arguments that were passed to the loader 130 | context, // A reference to the loaderContext of the inspect-loader 131 | options // A reference to the options object of the inspect-loader 132 | } 133 | ``` 134 | 135 | ```js 136 | function callback(inspect) { 137 | console.log(inspect.arguments); // ["loader contents from the previous loader"] 138 | console.log(inspect.context); // { resource: "...", ... } 139 | console.log(inspect.options); // { callback: [Function] } 140 | } 141 | ``` 142 | 143 | **Please note:** `context` and `options` are *not* references to the `loaderContext` of the loader you want to test. They just expose the internal state of the **inspect-loader**. This is useful if you have multiple callbacks and you want to find out which resource or loader pipeline has been invoked. 144 | 145 |

Usage

146 | 147 | ### Assertions 148 | 149 | Most of the time, you will probably want to do assertions on the `inspect` object. It is recommended to do this *after* the webpack compilation has finished, because otherwise the assertion error will be caught by webpack and reported as `Module build error`. 150 | 151 | Not so good: 152 | 153 | ```js 154 | ... 155 | loader: "inspect-loader", 156 | options: { 157 | callback(inspect) { 158 | // assertion errors will be caught as Module build error 159 | assert.deepEqual(inspect.arguments, [...]) 160 | } 161 | } 162 | ... 163 | ``` 164 | 165 | Better: 166 | 167 | ```js 168 | let args; 169 | 170 | webpack({ 171 | ... 172 | loader: "inspect-loader", 173 | options: { 174 | callback(inspect) { 175 | args = inspect.arguments; 176 | } 177 | } 178 | ... 179 | }, (err, stats) => { 180 | ... 181 | assert.deepEqual(args, [...]) 182 | }); 183 | 184 | ``` 185 | 186 |

License

187 | Unlicense 188 | 189 |

Sponsors

190 | 191 | [](https://peerigon.com) 192 | 193 | [npm]: https://img.shields.io/npm/v/inspect-loader.svg 194 | [npm-stats]: https://img.shields.io/npm/dm/inspect-loader.svg 195 | [npm-url]: https://npmjs.com/package/inspect-loader 196 | 197 | [node]: https://img.shields.io/node/v/inspect-loader.svg 198 | [node-url]: https://nodejs.org 199 | 200 | [deps]: https://david-dm.org/peerigon/inspect-loader.svg 201 | [deps-url]: https://david-dm.org/peerigon/inspect-loader 202 | 203 | [travis]: http://img.shields.io/travis/peerigon/inspect-loader.svg 204 | [travis-url]: https://travis-ci.org/peerigon/inspect-loader 205 | 206 | [appveyor-url]: https://ci.appveyor.com/project/jhnns/inspect-loader/branch/master 207 | [appveyor]: https://ci.appveyor.com/api/projects/status/github/peerigon/inspect-loader?svg=true 208 | 209 | [coverage]: https://img.shields.io/codecov/c/github/peerigon/inspect-loader.svg 210 | [coverage-url]: https://codecov.io/gh/peerigon/inspect-loader 211 | 212 | [chat]: https://badges.gitter.im/peerigon/webpack.svg 213 | [chat-url]: https://gitter.im/peerigon/webpack 214 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # appveyor file 2 | # http://www.appveyor.com/docs/appveyor-yml 3 | 4 | init: 5 | - git config --global core.autocrlf input 6 | 7 | install: 8 | - ps: Install-Product node $env:nodejs_version x64 9 | - npm install 10 | 11 | build: off 12 | 13 | matrix: 14 | fast_finish: true 15 | 16 | test_script: 17 | - node --version 18 | - npm --version 19 | - cmd: npm run test 20 | -------------------------------------------------------------------------------- /lib/loader.cjs: -------------------------------------------------------------------------------- 1 | const loaderUtils = require("loader-utils"); 2 | 3 | function defaultLogger(inspect) { 4 | // Using console.error as default as we want to print to stderr 5 | console.error(inspect.arguments); 6 | } 7 | 8 | function getInspectCallback(callback = defaultLogger) { 9 | const type = typeof callback; 10 | 11 | switch (type) { 12 | case "function": 13 | return callback; 14 | case "string": { 15 | const callbackName = callback; 16 | 17 | callback = inspectLoader.callbacks[callbackName]; 18 | if (typeof callback !== "function") { 19 | throw new TypeError( 20 | `Expected the registered callback "${callbackName}" to be typeof "function", instead of "${typeof callback}".` 21 | ); 22 | } 23 | 24 | return callback; 25 | } 26 | default: 27 | throw new TypeError( 28 | `Unexpected callback type. Expected "string" or "function", but saw "${type}".` 29 | ); 30 | } 31 | } 32 | 33 | function inspectLoader(...args) { 34 | // eslint-disable-next-line @babel/no-invalid-this 35 | const context = this; 36 | const options = loaderUtils.getOptions(context) || {}; 37 | const inspectCallback = getInspectCallback(options.callback); 38 | 39 | inspectCallback({ 40 | arguments: args, 41 | context, 42 | options, 43 | }); 44 | 45 | // Just pass the all the arguments to the next loader because we want this loader to be as unobtrusive as possible 46 | context.callback(null, ...args); 47 | } 48 | 49 | inspectLoader.callbacks = Object.create(null); 50 | 51 | module.exports = inspectLoader; 52 | -------------------------------------------------------------------------------- /lib/loader.raw.cjs: -------------------------------------------------------------------------------- 1 | 2 | const inspectLoader = require("./loader.cjs"); 3 | 4 | function rawInspectLoader(...args) { 5 | // eslint-disable-next-line @babel/no-invalid-this 6 | return inspectLoader.apply(this, args); 7 | } 8 | 9 | Object.assign(rawInspectLoader, { 10 | raw: true, 11 | }, inspectLoader); 12 | 13 | module.exports = rawInspectLoader; 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "inspect-loader", 3 | "version": "1.0.0", 4 | "description": "Webpack loader designed for loader testing and debugging. Calls a function with the received input.", 5 | "main": "lib/loader.cjs", 6 | "type": "module", 7 | "scripts": { 8 | "test": "nyc ava", 9 | "posttest": "npm run lint", 10 | "lint": "eslint --ignore-path .gitignore lib test --ext js,cjs" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/peerigon/inspect-loader.git" 15 | }, 16 | "keywords": [ 17 | "webpack", 18 | "loader", 19 | "test", 20 | "testing", 21 | "debug" 22 | ], 23 | "engines": { 24 | "node": ">=14.0.0" 25 | }, 26 | "author": "developers@peerigon.com", 27 | "license": "Unlicense", 28 | "bugs": { 29 | "url": "https://github.com/peerigon/inspect-loader/issues" 30 | }, 31 | "homepage": "https://github.com/peerigon/inspect-loader#readme", 32 | "files": [ 33 | "lib", 34 | "raw.js", 35 | "README.md", 36 | "LICENSE" 37 | ], 38 | "devDependencies": { 39 | "ava": "^3.15.0", 40 | "eslint": "^7.23.0", 41 | "eslint-config-peerigon": "^30.0.2", 42 | "eslint-plugin-jsdoc": "^32.3.0", 43 | "nyc": "^15.1.0", 44 | "webpack": "^5.30.0" 45 | }, 46 | "dependencies": { 47 | "loader-utils": "^2.0.0" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /raw.cjs: -------------------------------------------------------------------------------- 1 | module.exports = require("./lib/loader.raw.cjs"); 2 | -------------------------------------------------------------------------------- /test/fixtures/entry.js: -------------------------------------------------------------------------------- 1 | /* This is an entry file */ 2 | -------------------------------------------------------------------------------- /test/helpers/compile.js: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import * as url from "url"; 3 | import webpack from "webpack"; 4 | 5 | const dirname = path.dirname(url.fileURLToPath(import.meta.url)); 6 | 7 | export default function compile(loaderPipeline) { 8 | return new Promise((resolve, reject) => { 9 | webpack({ 10 | mode: "none", 11 | entry: path.resolve(dirname, "../fixtures/entry.js"), 12 | output: { 13 | path: path.resolve(dirname, "..", "output"), 14 | filename: "bundle", // omitting js because we don't want to trigger ava in watch mode 15 | }, 16 | module: { 17 | rules: [ 18 | { 19 | test: /\.js$/, 20 | use: loaderPipeline, 21 | }, 22 | ], 23 | }, 24 | }, (error, stats) => { 25 | const problem = error || stats.compilation.errors[0] || stats.compilation.warnings[0]; 26 | 27 | if (problem) { 28 | const error = problem.message ? 29 | problem : 30 | new Error(typeof problem === "string" ? problem : "Unexpected error"); 31 | 32 | error.originalError = error; 33 | error.stats = stats; 34 | 35 | reject(error); 36 | 37 | return; 38 | } 39 | 40 | resolve(stats); 41 | }); 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /test/loader.raw.test.js: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import * as url from "url"; 3 | import test from "ava"; 4 | import compile from "./helpers/compile.js"; 5 | import rawInspectLoader from "../raw.cjs"; 6 | import inspectLoader from "../lib/loader.cjs"; 7 | 8 | const dirname = path.dirname(url.fileURLToPath(import.meta.url)); 9 | 10 | test("should work the same as the inspect-loader just for buffers", t => { 11 | let inspect; 12 | const options = { 13 | callback(i) { 14 | inspect = i; 15 | }, 16 | }; 17 | 18 | t.plan(1); 19 | 20 | return compile([ 21 | { 22 | loader: path.resolve(dirname, "../raw.cjs"), 23 | options, 24 | }, 25 | ]) 26 | .then(() => { 27 | t.true(inspect.arguments[0] instanceof Buffer); 28 | }); 29 | }); 30 | 31 | test("should also expose the same callbacks object", t => { 32 | t.is(inspectLoader.callbacks, rawInspectLoader.callbacks); 33 | }); 34 | -------------------------------------------------------------------------------- /test/loader.test.js: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import * as url from "url"; 3 | import { readFileSync } from "fs"; 4 | import test from "ava"; 5 | import inspectLoader from "../lib/loader.cjs"; 6 | import compile from "./helpers/compile.js"; 7 | 8 | const dirname = path.dirname(url.fileURLToPath(import.meta.url)); 9 | const entryContent = readFileSync(path.resolve(dirname, "./fixtures/entry.js"), "utf8"); 10 | const pathToInspectLoader = path.resolve(dirname, "../lib/loader.cjs"); 11 | 12 | /** 13 | * Applies dummy properties that must be defined on the context. 14 | * 15 | * @param {Object} context 16 | * @returns {Object} 17 | */ 18 | function mockContext(context) { 19 | context.callback = context.callback || Function.prototype; 20 | 21 | return context; 22 | } 23 | 24 | test("should call the query callback synchronously", t => { 25 | let called = false; 26 | const context = mockContext({ 27 | query: { 28 | callback() { 29 | called = true; 30 | }, 31 | }, 32 | }); 33 | 34 | inspectLoader.call(context); 35 | 36 | t.true(called); 37 | }); 38 | 39 | test("should call the loaderContext callback with the expected context and arguments", t => { 40 | const args = [1, 2, 3, 4, 5, 6]; 41 | const context = mockContext({ 42 | query: { 43 | callback() {}, 44 | }, 45 | }); 46 | 47 | context.callback = function (...actualArgs) { 48 | t.is(this, context); 49 | t.is(actualArgs[0], null); 50 | t.deepEqual(args, actualArgs.slice(1)); 51 | }; 52 | 53 | t.plan(3); 54 | 55 | inspectLoader.apply(context, args); 56 | }); 57 | 58 | test("should callback the query callback with inspectable arguments", t => { 59 | const args = [1, 2, 3, 4, 5, 6]; 60 | const options = { 61 | callback(inspect) { 62 | t.deepEqual(inspect.arguments, args); 63 | t.is(inspect.context, context); 64 | t.is(inspect.options, options); 65 | }, 66 | }; 67 | const context = mockContext({ 68 | query: options, 69 | }); 70 | 71 | t.plan(3); 72 | 73 | inspectLoader.apply(context, args); 74 | }); 75 | 76 | test("should also support string refs to previously registered callbacks", t => { 77 | const stringRef = "testCallback"; 78 | const args = [1, 2, 3, 4, 5, 6]; 79 | const options = { 80 | callback: stringRef, 81 | }; 82 | const context = mockContext({ 83 | query: options, 84 | }); 85 | 86 | t.plan(3); 87 | 88 | inspectLoader.callbacks[stringRef] = (inspect) => { 89 | t.deepEqual(inspect.arguments, args); 90 | t.is(inspect.context, context); 91 | t.is(inspect.options, options); 92 | }; 93 | 94 | inspectLoader.apply(context, args); 95 | }); 96 | 97 | // Rewrite test for default console.error 98 | test.skip("should throw a TypeError if there was no callback registered", t => { 99 | const context = mockContext({}); 100 | 101 | const err = t.throws(() => { 102 | inspectLoader.apply(context); 103 | }); 104 | 105 | t.true(err instanceof TypeError); 106 | t.is(err.message, 'Unexpected callback type. Expected "string" or "function", but saw "undefined".'); 107 | }); 108 | 109 | test("should throw a descriptive TypeError if an unknown callback string ref was given", t => { 110 | const stringRef = "notRegistered"; 111 | const context = mockContext({ 112 | query: { 113 | callback: "notRegistered", 114 | }, 115 | }); 116 | 117 | const err = t.throws(() => { 118 | inspectLoader.apply(context); 119 | }); 120 | 121 | t.true(err instanceof TypeError); 122 | t.is(err.message, `Expected the registered callback "${stringRef}" to be typeof "function", instead of "undefined".`); 123 | }); 124 | 125 | test("should also work with webpack's loader context", t => { 126 | let inspect; 127 | const options = { 128 | callback(i) { 129 | inspect = i; 130 | }, 131 | }; 132 | 133 | t.plan(5); 134 | 135 | return compile([ 136 | { 137 | loader: pathToInspectLoader, 138 | options, 139 | }, 140 | ]) 141 | .then(() => { 142 | t.truthy(inspect.arguments); 143 | t.deepEqual(inspect.arguments, [entryContent]); 144 | 145 | t.truthy(inspect.context); 146 | t.truthy(inspect.context.resourcePath); 147 | 148 | t.is(inspect.options, options); 149 | }); 150 | }); 151 | 152 | test("should be possible to inspect multiple times", t => { 153 | const loaderIndices = []; 154 | let inspect1; 155 | let inspect2; 156 | let inspect3; 157 | const options = [ 158 | { 159 | callback(i) { 160 | inspect1 = i; 161 | loaderIndices[0] = i.context.loaderIndex; 162 | }, 163 | }, { 164 | callback(i) { 165 | inspect2 = i; 166 | loaderIndices[1] = i.context.loaderIndex; 167 | }, 168 | }, { 169 | callback(i) { 170 | inspect3 = i; 171 | loaderIndices[2] = i.context.loaderIndex; 172 | }, 173 | }, 174 | ]; 175 | 176 | t.plan(1 + (options.length * 4)); 177 | 178 | return compile(options.map(options => ({ 179 | loader: pathToInspectLoader, 180 | options, 181 | }))) 182 | .then(() => { 183 | t.deepEqual(loaderIndices, [0, 1, 2]); 184 | [inspect1, inspect2, inspect3].forEach((inspect, i) => { 185 | t.truthy(inspect.arguments); 186 | t.deepEqual(inspect.arguments, [entryContent]); 187 | 188 | t.truthy(inspect.context); 189 | 190 | t.is(inspect.options, options[i]); 191 | }); 192 | }); 193 | }); 194 | --------------------------------------------------------------------------------