├── .gitignore ├── .eslintrc.json ├── test ├── .eslintrc.json └── default.test.js ├── lib ├── .eslintrc.json └── skeleton-loader.js ├── package.json ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | node_modules 3 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": "../../_common/files/eslintrc.json", 4 | "env": {"node": true} 5 | } 6 | -------------------------------------------------------------------------------- /test/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": {"es6": true, "mocha": true}, 3 | "rules": { 4 | "no-unused-expressions": "off" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /lib/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-var": "off", 4 | "prefer-arrow-callback": "off", 5 | "no-unused-expressions": ["error", { "allowShortCircuit": true }], 6 | "consistent-return": "off" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "skeleton-loader", 3 | "version": "2.0.0", 4 | "title": "skeleton-loader", 5 | "description": "Loader module for webpack to execute your custom procedure. It works as your custom loader.", 6 | "keywords": [ 7 | "webpack", 8 | "loader", 9 | "custom", 10 | "custom-loader", 11 | "function", 12 | "instant", 13 | "edit", 14 | "content", 15 | "modify" 16 | ], 17 | "main": "./lib/skeleton-loader.js", 18 | "files": [ 19 | "lib/*.js" 20 | ], 21 | "dependencies": { 22 | "loader-utils": "^1.4.2" 23 | }, 24 | "devDependencies": { 25 | "chai": "^4.3.4", 26 | "mocha": "^10.1.0", 27 | "sinon": "^11.1.1" 28 | }, 29 | "scripts": { 30 | "test": "mocha" 31 | }, 32 | "homepage": "https://github.com/anseki/skeleton-loader", 33 | "repository": { 34 | "type": "git", 35 | "url": "git://github.com/anseki/skeleton-loader.git" 36 | }, 37 | "bugs": "https://github.com/anseki/skeleton-loader/issues", 38 | "license": "MIT", 39 | "author": { 40 | "name": "anseki", 41 | "url": "https://github.com/anseki" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 anseki 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. -------------------------------------------------------------------------------- /lib/skeleton-loader.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var loaderUtils = require('loader-utils'); 4 | 5 | module.exports = function(content, sourceMap, meta) { 6 | var context = this, 7 | options = loaderUtils.getOptions(context) || {}; 8 | 9 | function getResult(content) { 10 | return context.loaderIndex === 0 && options.toCode 11 | ? 'module.exports = ' + JSON.stringify(content) + ';' : content; 12 | } 13 | 14 | options.cacheable = typeof options.cacheable === 'boolean' ? options.cacheable : true; 15 | options.cacheable && context.cacheable && context.cacheable(); 16 | if (typeof context.resourceQuery === 'string' && context.resourceQuery) { 17 | options.resourceOptions = loaderUtils.parseQuery(context.resourceQuery); 18 | } 19 | options.sourceMap = sourceMap; 20 | options.meta = meta; 21 | 22 | if (typeof options.procedure === 'function') { 23 | content = options.procedure.call(context, content, options, 24 | function(error, content, sourceMap, meta) { 25 | if (error) { 26 | context.callback(error); 27 | } else { 28 | context.callback(null, getResult(content), sourceMap, meta); 29 | } 30 | }); 31 | 32 | if (options.procedure.length >= 3) { // async mode 33 | if (!context.async()) { throw new Error('Asynchronous mode is not allowed'); } 34 | return; 35 | } 36 | } 37 | 38 | return getResult(content); 39 | }; 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # skeleton-loader 2 | 3 | [![npm](https://img.shields.io/npm/v/skeleton-loader.svg)](https://www.npmjs.com/package/skeleton-loader) [![GitHub issues](https://img.shields.io/github/issues/anseki/skeleton-loader.svg)](https://github.com/anseki/skeleton-loader/issues) [![David](https://img.shields.io/david/anseki/skeleton-loader.svg)](package.json) [![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) 4 | 5 | Loader module for [webpack](https://webpack.js.org/) to execute your custom procedure. It works as your custom loader. 6 | 7 | By default, skeleton-loader only outputs the input content. When you specify a function, skeleton-loader executes your function with the input content, and outputs its result. The function does something, it might edit the content, it might parse the content and indicate something in a console, it might do anything else. 8 | 9 | That is, you can specify a function in webpack configuration instead of writing new custom loader. 10 | 11 | skeleton-loader is useful when: 12 | 13 | - You couldn't find a loader you want. 14 | - You don't want to write a special loader for your project. 15 | - You want to add something to the result of another loader. 16 | - You want to do additional editing. 17 | - etc. 18 | 19 | For example: 20 | 21 | ```js 22 | // webpack.config.js 23 | module.exports = { 24 | entry: './app.js', 25 | output: { 26 | filename: 'bundle.js' 27 | }, 28 | module: { 29 | rules: [{ 30 | test: /\.js$/, 31 | loader: 'skeleton-loader', 32 | options: { 33 | procedure: function(content) { 34 | // Change the input content, and output it. 35 | return (content + '').replace(/foo/g, 'bar'); 36 | } 37 | } 38 | }] 39 | } 40 | }; 41 | ``` 42 | 43 | ```js 44 | // webpack.config.js 45 | // ... 46 | test: /\.html$/, 47 | // ... 48 | // skeleton-loader options 49 | options: { 50 | procedure: function(content) { 51 | // Remove all elements for testing from HTML. 52 | return (content + '').replace(/
[^]*?<\/div>/g, ''); 53 | }, 54 | toCode: true 55 | } 56 | ``` 57 | 58 | ```js 59 | // webpack.config.js 60 | // ... 61 | test: /\.json$/, 62 | // ... 63 | // skeleton-loader options 64 | options: { 65 | procedure: function(content) { 66 | var appConfig = JSON.parse(content); 67 | // Check and change JSON. 68 | console.log(appConfig.foo); 69 | appConfig.bar = 'PUBLISH'; 70 | return appConfig; 71 | }, 72 | toCode: true 73 | } 74 | ``` 75 | 76 | ```js 77 | // webpack.config.js 78 | // ... 79 | // skeleton-loader options 80 | options: { 81 | // Asynchronous mode 82 | procedure: function(content, options, callback) { 83 | setTimeout(function() { 84 | callback(null, 'Edited: ' + content); 85 | }, 5000); 86 | } 87 | } 88 | ``` 89 | 90 | ## Installation 91 | 92 | ``` 93 | npm install --save-dev skeleton-loader 94 | ``` 95 | 96 | ## Usage 97 | 98 | Documentation: 99 | 100 | - [Loaders](https://webpack.js.org/concepts/loaders/) 101 | - [Using loaders](http://webpack.github.io/docs/using-loaders.html) (for webpack v1) 102 | 103 | ## Options 104 | 105 | You can specify options via query parameters or an `options` (or `skeletonLoader` for webpack v1) object in webpack configuration. 106 | 107 | ### `procedure` 108 | 109 | *Type:* function 110 | *Default:* `undefined` 111 | 112 | A function to do something with the input content. The result of the `procedure` is output. 113 | The following arguments are passed to the `procedure`: 114 | 115 | - `content` 116 | The content of the resource file as string, or something that is passed from previous loader. That is, if another loader is chained in `loaders` list, the `content` that is passed from that loader might not be string. 117 | - `options` 118 | Reference to current options. This might contain either or both of `sourceMap` and `meta` if those are passed from previous loader. Also, it might contain [`options.resourceOptions`](#optionsresourceoptions). 119 | - `callback` 120 | A callback function for asynchronous mode. If the `procedure` doesn't receive the `callback`, the loader works in synchronous mode. 121 | 122 | In the `procedure` function, `this` refers to the loader context. It has `resourcePath`, `query`, etc. See: https://webpack.js.org/api/loaders/#the-loader-context 123 | 124 | The result of the `procedure` can be any type such as `string`, `Object`, `null`, `undefined`, etc. 125 | For example: 126 | 127 | ```js 128 | // app.js 129 | var config = require('config.json'); 130 | ``` 131 | 132 | ```js 133 | // webpack.config.js 134 | // ... 135 | // skeleton-loader options 136 | options: { 137 | procedure: function(config) { 138 | if (initialize) { 139 | return; // make config be undefined 140 | } 141 | return process.env.NODE_ENV === 'production' ? config : {name: 'DUMMY'}; // data for test 142 | } 143 | } 144 | ``` 145 | 146 | In synchronous mode, the `procedure` has to return the content. The content is output as JavaScript code, or passed to next loader if it is chained. 147 | 148 | For example: 149 | 150 | ```js 151 | // webpack.config.js 152 | // ... 153 | // skeleton-loader options 154 | options: { 155 | procedure: function(content, options) { 156 | 157 | // Do something with content. 158 | console.log('Size: ' + content.length); 159 | content = (content + '').replace(/foo/g, 'bar'); // content might be not string. 160 | 161 | // Check the resource file by using context. 162 | if (this.resourcePath === '/abc/resource.js') { 163 | 164 | // Change current option. 165 | options.toCode = true; 166 | } 167 | 168 | // Return the content to output. 169 | return content; 170 | } 171 | } 172 | ``` 173 | 174 | If the `procedure` receives the `callback`, the loader works in asynchronous mode. To return either or both of SourceMap and meta data, it must be asynchronous mode. 175 | In asynchronous mode, the `procedure` has to call the `callback` when it finished. 176 | 177 | The `callback` accepts the following arguments: 178 | 179 | - `error` 180 | An error object, when your procedure failed. 181 | - `content` 182 | The content that is output as JavaScript code, or passed to next loader if it is chained. This can be any type such as `string`, `Object`, `null`, `undefined`, etc. 183 | - `sourceMap` 184 | An optional value SourceMap as JavaScript object that is output, or passed to next loader if it is chained. 185 | - `meta` 186 | An optional value that can be anything and is output, or passed to next loader if it is chained. 187 | 188 | For example: 189 | 190 | ```js 191 | // webpack.config.js 192 | // ... 193 | // skeleton-loader options 194 | options: { 195 | procedure: function(content, options, callback) { // Switches to asynchronous mode 196 | // Do something asynchronously. 197 | require('fs').readFile('data.txt', function(error, data) { 198 | if (error) { 199 | // Failed 200 | callback(error); 201 | } else { 202 | // Done 203 | callback(null, data + content); 204 | } 205 | }); 206 | } 207 | } 208 | ``` 209 | 210 | #### `options.resourceOptions` 211 | 212 | The `options` argument has `resourceOptions` property if a query string is specified with the resource file, and it is an object that is parsed query string. 213 | This is useful for specifying additional parameters when importing the resource files. For example, you can specify the behavior with resource files. 214 | 215 | ```js 216 | var 217 | all = require('file.html'), 218 | noHead = require('file.html?removeHead=yes'),; 219 | ``` 220 | 221 | ```js 222 | // webpack.config.js 223 | // ... 224 | // skeleton-loader options 225 | options: { 226 | procedure: function(content, options) { 227 | if (options.resourceOptions && options.resourceOptions.removeHead) { 228 | content = content.replace(//, ''); // Remove 229 | } 230 | return content; 231 | } 232 | } 233 | ``` 234 | 235 | The query string is parsed in the same way as [loader-utils](https://github.com/webpack/loader-utils#options-as-query-strings). 236 | 237 | ### `toCode` 238 | 239 | *Type:* boolean 240 | *Default:* `false` 241 | 242 | When the content is not JavaScript code (e.g. HTML, CSS, JSON, etc.), a loader that is specified as a final loader has to convert the content to JavaScript code and output it to allow another code to import the content. 243 | If `true` is specified for `toCode` option, the content is converted to JavaScript code. 244 | If the loader is specified as not a final loader, this option is ignored (i.e. the content is not converted, and it is passed to next loader). 245 | 246 | For example: 247 | 248 | ```js 249 | // webpack.config.js 250 | module.exports = { 251 | // ... 252 | module: { 253 | rules: [ 254 | // HTML code is converted to JavaScript string. 255 | // It works same as raw-loader. 256 | {test: /\.html$/, loader: 'skeleton-loader?toCode=true'}, 257 | 258 | // JSON data is converted to JavaScript object. 259 | // It works same as json-loader. 260 | { 261 | test: /\.json$/, 262 | loader: 'skeleton-loader', 263 | options: { 264 | procedure: function(content) { return JSON.parse(content); }, 265 | toCode: true 266 | } 267 | } 268 | ] 269 | } 270 | }; 271 | ``` 272 | 273 | ```js 274 | // app.js 275 | var html = require('file.html'); 276 | element.innerHTML = html; 277 | 278 | var obj = require('file.json'); 279 | console.log(obj.array1[3]); 280 | ``` 281 | 282 | ### `cacheable` 283 | 284 | *Type:* boolean 285 | *Default:* `true` 286 | 287 | Make the result cacheable. 288 | A cacheable loader must have a deterministic result, when inputs and dependencies haven't changed. This means the loader shouldn't have other dependencies than specified with [`context.addDependency`](https://webpack.js.org/api/loaders/#this-adddependency). 289 | Note that the default value is `true`. 290 | -------------------------------------------------------------------------------- /test/default.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const expect = require('chai').expect, 4 | sinon = require('sinon'), 5 | webpack = { 6 | cacheable: sinon.spy(), 7 | async: sinon.spy(() => webpack.callback), 8 | callback: sinon.spy(function() { 9 | if (webpack.next) { 10 | const args = Array.from(arguments); 11 | setTimeout(() => { webpack.next.apply(null, args); }, 0); 12 | } 13 | }) 14 | }, 15 | loader = require('../'); 16 | 17 | function resetAll(context) { 18 | const DEFAULT_PROPS = { 19 | query: {}, 20 | loaderIndex: 0, 21 | resourceQuery: null, 22 | next: null 23 | }; 24 | Object.keys(DEFAULT_PROPS).forEach(propName => { 25 | webpack[propName] = 26 | context.hasOwnProperty(propName) ? context[propName] : DEFAULT_PROPS[propName]; 27 | }); 28 | 29 | webpack.cacheable.resetHistory(); 30 | webpack.async.resetHistory(); 31 | webpack.callback.resetHistory(); 32 | } 33 | 34 | describe('flow for `procedure`', () => { 35 | 36 | describe('Synchronous mode', () => { 37 | 38 | describe('should return edited string', () => { 39 | 40 | function procedure(content, options) { 41 | expect(options.sourceMap).to.equal(''); 42 | expect(options.meta).to.equal(''); 43 | return `${content}`; 44 | } 45 | 46 | it('toCode: false', () => { 47 | resetAll({query: {procedure}}); 48 | expect(loader.call(webpack, 'INPUT', '', '')).to.equal('INPUT'); 49 | expect(webpack.async.notCalled).to.be.true; 50 | expect(webpack.callback.notCalled).to.be.true; 51 | }); 52 | 53 | it('toCode: true', () => { 54 | resetAll({query: {procedure, toCode: true}}); 55 | expect(loader.call(webpack, 'INPUT', '', '')) 56 | .to.equal('module.exports = "INPUT";'); 57 | expect(webpack.async.notCalled).to.be.true; 58 | expect(webpack.callback.notCalled).to.be.true; 59 | }); 60 | 61 | }); 62 | 63 | describe('should return a value even if it is not string (Array)', () => { 64 | 65 | const passedValue = [1, 2, 3]; 66 | function procedure(content, options) { 67 | expect(options.sourceMap).to.equal(''); 68 | expect(options.meta).to.equal(''); 69 | return passedValue; 70 | } 71 | 72 | it('toCode: false', () => { 73 | resetAll({query: {procedure}}); 74 | expect(loader.call(webpack, 'INPUT', '', '')).to.equal(passedValue); 75 | expect(webpack.async.notCalled).to.be.true; 76 | expect(webpack.callback.notCalled).to.be.true; 77 | }); 78 | 79 | it('toCode: true', () => { 80 | resetAll({query: {procedure, toCode: true}}); 81 | expect(loader.call(webpack, 'INPUT', '', '')) 82 | .to.equal('module.exports = [1,2,3];'); 83 | expect(webpack.async.notCalled).to.be.true; 84 | expect(webpack.callback.notCalled).to.be.true; 85 | }); 86 | 87 | }); 88 | 89 | describe('should return a value even if it is `null`', () => { 90 | 91 | function procedure(content, options) { 92 | expect(options.sourceMap).to.equal(''); 93 | expect(options.meta).to.equal(''); 94 | return null; 95 | } 96 | 97 | it('toCode: false', () => { 98 | resetAll({query: {procedure}}); 99 | expect(loader.call(webpack, 'INPUT', '', '')).to.be.null; 100 | expect(webpack.async.notCalled).to.be.true; 101 | expect(webpack.callback.notCalled).to.be.true; 102 | }); 103 | 104 | it('toCode: true', () => { 105 | resetAll({query: {procedure, toCode: true}}); 106 | expect(loader.call(webpack, 'INPUT', '', '')) 107 | .to.equal('module.exports = null;'); 108 | expect(webpack.async.notCalled).to.be.true; 109 | expect(webpack.callback.notCalled).to.be.true; 110 | }); 111 | 112 | }); 113 | 114 | describe('should return a value even if it is `undefined`', () => { 115 | 116 | function procedure(content, options) { 117 | expect(options.sourceMap).to.equal(''); 118 | expect(options.meta).to.equal(''); 119 | } 120 | 121 | it('toCode: false', () => { 122 | resetAll({query: {procedure}}); 123 | expect(loader.call(webpack, 'INPUT', '', '')).to.be.undefined; 124 | expect(webpack.async.notCalled).to.be.true; 125 | expect(webpack.callback.notCalled).to.be.true; 126 | }); 127 | 128 | it('toCode: true', () => { 129 | resetAll({query: {procedure, toCode: true}}); 130 | expect(loader.call(webpack, 'INPUT', '', '')) 131 | .to.equal('module.exports = undefined;'); 132 | expect(webpack.async.notCalled).to.be.true; 133 | expect(webpack.callback.notCalled).to.be.true; 134 | }); 135 | 136 | }); 137 | 138 | }); 139 | 140 | describe('Asynchronous mode', () => { 141 | 142 | describe('should return edited string', () => { 143 | 144 | function procedureSync(content, options) { 145 | expect(options.sourceMap).to.equal(''); 146 | expect(options.meta).to.equal(''); 147 | return `${content}`; 148 | } 149 | 150 | function procedure(content, options, procDone) { 151 | setTimeout(() => { 152 | procDone(null, procedureSync(content, options), options.sourceMap, options.meta); 153 | }, 0); 154 | return 'DUMMY'; // This is not returned by loader 155 | } 156 | 157 | it('toCode: false', done => { 158 | resetAll({ 159 | query: {procedure}, 160 | next: (error, content, map, meta) => { 161 | expect(error).to.be.null; 162 | expect(content).to.equal('INPUT'); 163 | expect(map).to.equal(''); 164 | expect(meta).to.equal(''); 165 | expect(webpack.async.calledOnce).to.be.true; 166 | expect(webpack.callback.calledOnceWithExactly( 167 | null, 'INPUT', '', '')).to.be.true; 168 | 169 | done(); 170 | } 171 | }); 172 | expect(loader.call(webpack, 'INPUT', '', '')).to.be.undefined; // always undefined 173 | }); 174 | 175 | it('toCode: true', done => { 176 | resetAll({ 177 | query: {procedure, toCode: true}, 178 | next: (error, content, map, meta) => { 179 | expect(error).to.be.null; 180 | expect(content).to.equal('module.exports = "INPUT";'); 181 | expect(map).to.equal(''); 182 | expect(meta).to.equal(''); 183 | expect(webpack.async.calledOnce).to.be.true; 184 | expect(webpack.callback.calledOnceWithExactly( 185 | null, 'module.exports = "INPUT";', '', '')).to.be.true; 186 | 187 | done(); 188 | } 189 | }); 190 | expect(loader.call(webpack, 'INPUT', '', '')).to.be.undefined; // always undefined 191 | }); 192 | 193 | }); 194 | 195 | describe('should return a value even if it is not string (Array)', () => { 196 | 197 | const passedValue = [1, 2, 3]; 198 | function procedureSync(content, options) { 199 | expect(options.sourceMap).to.equal(''); 200 | expect(options.meta).to.equal(''); 201 | return passedValue; 202 | } 203 | 204 | function procedure(content, options, procDone) { 205 | setTimeout(() => { 206 | procDone(null, procedureSync(content, options), options.sourceMap, options.meta); 207 | }, 0); 208 | return 'DUMMY'; // This is not returned by loader 209 | } 210 | 211 | it('toCode: false', done => { 212 | resetAll({ 213 | query: {procedure}, 214 | next: (error, content, map, meta) => { 215 | expect(error).to.be.null; 216 | expect(content).to.equal(passedValue); 217 | expect(map).to.equal(''); 218 | expect(meta).to.equal(''); 219 | expect(webpack.async.calledOnce).to.be.true; 220 | expect(webpack.callback.calledOnceWithExactly( 221 | null, passedValue, '', '')).to.be.true; 222 | 223 | done(); 224 | } 225 | }); 226 | expect(loader.call(webpack, 'INPUT', '', '')).to.be.undefined; // always undefined 227 | }); 228 | 229 | it('toCode: true', done => { 230 | resetAll({ 231 | query: {procedure, toCode: true}, 232 | next: (error, content, map, meta) => { 233 | expect(error).to.be.null; 234 | expect(content).to.equal('module.exports = [1,2,3];'); 235 | expect(map).to.equal(''); 236 | expect(meta).to.equal(''); 237 | expect(webpack.async.calledOnce).to.be.true; 238 | expect(webpack.callback.calledOnceWithExactly( 239 | null, 'module.exports = [1,2,3];', '', '')).to.be.true; 240 | 241 | done(); 242 | } 243 | }); 244 | expect(loader.call(webpack, 'INPUT', '', '')).to.be.undefined; // always undefined 245 | }); 246 | 247 | }); 248 | 249 | describe('should return a value even if it is `null`', () => { 250 | 251 | function procedureSync(content, options) { 252 | expect(options.sourceMap).to.equal(''); 253 | expect(options.meta).to.equal(''); 254 | return null; 255 | } 256 | 257 | function procedure(content, options, procDone) { 258 | setTimeout(() => { 259 | procDone(null, procedureSync(content, options), options.sourceMap, options.meta); 260 | }, 0); 261 | return 'DUMMY'; // This is not returned by loader 262 | } 263 | 264 | it('toCode: false', done => { 265 | resetAll({ 266 | query: {procedure}, 267 | next: (error, content, map, meta) => { 268 | expect(error).to.be.null; 269 | expect(content).to.be.null; 270 | expect(map).to.equal(''); 271 | expect(meta).to.equal(''); 272 | expect(webpack.async.calledOnce).to.be.true; 273 | expect(webpack.callback.calledOnceWithExactly( 274 | null, null, '', '')).to.be.true; 275 | 276 | done(); 277 | } 278 | }); 279 | expect(loader.call(webpack, 'INPUT', '', '')).to.be.undefined; // always undefined 280 | }); 281 | 282 | it('toCode: true', done => { 283 | resetAll({ 284 | query: {procedure, toCode: true}, 285 | next: (error, content, map, meta) => { 286 | expect(error).to.be.null; 287 | expect(content).to.equal('module.exports = null;'); 288 | expect(map).to.equal(''); 289 | expect(meta).to.equal(''); 290 | expect(webpack.async.calledOnce).to.be.true; 291 | expect(webpack.callback.calledOnceWithExactly( 292 | null, 'module.exports = null;', '', '')).to.be.true; 293 | 294 | done(); 295 | } 296 | }); 297 | expect(loader.call(webpack, 'INPUT', '', '')).to.be.undefined; // always undefined 298 | }); 299 | 300 | }); 301 | 302 | describe('should return a value even if it is `undefined`', () => { 303 | 304 | function procedureSync(content, options) { 305 | expect(options.sourceMap).to.equal(''); 306 | expect(options.meta).to.equal(''); 307 | } 308 | 309 | function procedure(content, options, procDone) { 310 | setTimeout(() => { 311 | procDone(null, procedureSync(content, options), options.sourceMap, options.meta); 312 | }, 0); 313 | return 'DUMMY'; // This is not returned by loader 314 | } 315 | 316 | it('toCode: false', done => { 317 | resetAll({ 318 | query: {procedure}, 319 | next: (error, content, map, meta) => { 320 | expect(error).to.be.null; 321 | expect(content).to.be.undefined; 322 | expect(map).to.equal(''); 323 | expect(meta).to.equal(''); 324 | expect(webpack.async.calledOnce).to.be.true; 325 | expect(webpack.callback.calledOnceWithExactly( 326 | null, void 0, '', '')).to.be.true; 327 | 328 | done(); 329 | } 330 | }); 331 | expect(loader.call(webpack, 'INPUT', '', '')).to.be.undefined; // always undefined 332 | }); 333 | 334 | it('toCode: true', done => { 335 | resetAll({ 336 | query: {procedure, toCode: true}, 337 | next: (error, content, map, meta) => { 338 | expect(error).to.be.null; 339 | expect(content).to.equal('module.exports = undefined;'); 340 | expect(map).to.equal(''); 341 | expect(meta).to.equal(''); 342 | expect(webpack.async.calledOnce).to.be.true; 343 | expect(webpack.callback.calledOnceWithExactly( 344 | null, 'module.exports = undefined;', '', '')).to.be.true; 345 | 346 | done(); 347 | } 348 | }); 349 | expect(loader.call(webpack, 'INPUT', '', '')).to.be.undefined; // always undefined 350 | }); 351 | 352 | }); 353 | 354 | it('should pass an error that was passed by procedure', done => { 355 | const error1 = new Error('error1'); 356 | resetAll({ 357 | query: { 358 | procedure: (content, options, procDone) => { 359 | setTimeout(() => { 360 | procDone(error1, `${content}`); 361 | }, 0); 362 | return 'DUMMY'; // This is not returned by loader 363 | } 364 | }, 365 | next: (error, content, map, meta) => { 366 | expect(error).to.equal(error1); 367 | expect(content).to.be.undefined; 368 | expect(map).to.be.undefined; 369 | expect(meta).to.be.undefined; 370 | expect(webpack.async.calledOnce).to.be.true; 371 | expect(webpack.callback.calledOnceWithExactly(error1)).to.be.true; 372 | 373 | done(); 374 | } 375 | }); 376 | expect(loader.call(webpack, 'INPUT')).to.be.undefined; // always undefined 377 | }); 378 | 379 | }); 380 | 381 | }); 382 | 383 | describe('converts output as code', () => { 384 | const RES = 'INPUT', 385 | RES_CNV = `module.exports = "${RES}";`; 386 | 387 | function procedure(content) { return `${content}`; } 388 | 389 | it('should not convert content when loaderIndex: 1 / toCode: false', () => { 390 | resetAll({loaderIndex: 1, query: {procedure, toCode: false}}); 391 | expect(loader.call(webpack, 'INPUT')).to.equal(RES); 392 | }); 393 | 394 | it('should not convert content when loaderIndex: 1 / toCode: true', () => { 395 | resetAll({loaderIndex: 1, query: {procedure, toCode: true}}); 396 | expect(loader.call(webpack, 'INPUT')).to.equal(RES); 397 | }); 398 | 399 | it('should not convert content when loaderIndex: 0 / toCode: false', () => { 400 | resetAll({loaderIndex: 0, query: {procedure, toCode: false}}); 401 | expect(loader.call(webpack, 'INPUT')).to.equal(RES); 402 | }); 403 | 404 | it('should convert content when loaderIndex: 0 / toCode: true', () => { 405 | resetAll({loaderIndex: 0, query: {procedure, toCode: true}}); 406 | expect(loader.call(webpack, 'INPUT')).to.equal(RES_CNV); 407 | }); 408 | 409 | }); 410 | 411 | describe('options.resourceOptions', () => { 412 | 413 | it('should not set options.resourceOptions when resourceQuery is not given', () => { 414 | resetAll({ 415 | query: { 416 | procedure: (content, options) => { 417 | expect(options.hasOwnProperty('resourceOptions')).to.be.false; 418 | return `${content}`; 419 | } 420 | } 421 | }); 422 | expect(loader.call(webpack, 'INPUT')).to.equal('INPUT'); 423 | }); 424 | 425 | it('should parse resourceQuery and set options.resourceOptions', () => { 426 | resetAll({ 427 | resourceQuery: '?p1=v1&p2=v2', 428 | query: { 429 | procedure: (content, options) => { 430 | expect(options.hasOwnProperty('resourceOptions')).to.be.true; 431 | expect(options.resourceOptions.p1).to.equal('v1'); 432 | expect(options.resourceOptions.p2).to.equal('v2'); 433 | return `${content}`; 434 | } 435 | } 436 | }); 437 | expect(loader.call(webpack, 'INPUT')).to.equal('INPUT'); 438 | }); 439 | 440 | }); 441 | --------------------------------------------------------------------------------