├── .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 | [](https://www.npmjs.com/package/skeleton-loader) [](https://github.com/anseki/skeleton-loader/issues) [](package.json) [](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('