-
Notifications
You must be signed in to change notification settings - Fork 56
/
WriteFileWebpackPlugin.js
200 lines (156 loc) · 6.51 KB
/
WriteFileWebpackPlugin.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
import fs from 'fs';
import {createHash} from 'crypto';
import path from 'path';
import _ from 'lodash';
import mkdirp from 'mkdirp';
import chalk from 'chalk';
import moment from 'moment';
import filesize from 'filesize';
import createDebug from 'debug';
import {sync as writeFileAtomicSync} from 'write-file-atomic';
const debug = createDebug('write-file-webpack-plugin');
/**
* When 'webpack' program is used, constructor name is equal to 'NodeOutputFileSystem'.
* When 'webpack-dev-server' program is used, constructor name is equal to 'MemoryFileSystem'.
*/
const isMemoryFileSystem = (outputFileSystem: Object): boolean => {
return outputFileSystem.constructor.name === 'MemoryFileSystem';
};
/**
* @typedef {Object} options
* @property {boolean} atomicReplace Atomically replace files content (i.e., to prevent programs like test watchers from seeing partial files) (default: true).
* @property {boolean} exitOnErrors Stop writing files on webpack errors (default: true).
* @property {boolean} force Forces the execution of the plugin regardless of being using `webpack-dev-server` or not (default: false).
* @property {boolean} log Logs names of the files that are being written (or skipped because they have not changed) (default: true).
* @property {RegExp} test A regular expression or function used to test if file should be written. When not present, all bundle will be written.
* @property {boolean} useHashIndex Use hash index to write only files that have changed since the last iteration (default: true).
*/
type UserOptionsType = {
atomicReplace: ?boolean,
exitOnErrors: ?boolean,
test: ?RegExp,
useHashIndex: ?boolean,
log: ?boolean,
force: ?boolean
};
export default function WriteFileWebpackPlugin (userOptions: UserOptionsType = {}): Object {
const options = _.assign({}, {
atomicReplace: true,
exitOnErrors: true,
force: false,
log: true,
test: null,
useHashIndex: true
}, userOptions);
if (!_.isBoolean(options.exitOnErrors)) {
throw new TypeError('options.exitOnErrors value must be of boolean type.');
}
if (!_.isBoolean(options.force)) {
throw new TypeError('options.force value must be of boolean type.');
}
if (!_.isBoolean(options.log)) {
throw new TypeError('options.log value must be of boolean type.');
}
if (!_.isNull(options.test) && !(_.isRegExp(options.test) || _.isFunction(options.test))) {
throw new TypeError('options.test value must be an instance of RegExp or Function.');
}
if (!_.isBoolean(options.useHashIndex)) {
throw new TypeError('options.useHashIndex value must be of boolean type.');
}
if (!_.isBoolean(options.atomicReplace)) {
throw new TypeError('options.atomicReplace value must be of boolean type.');
}
const writeFileMethod = options.atomicReplace ? writeFileAtomicSync : fs.writeFileSync;
const log = (...append) => {
if (!options.log) {
return;
}
debug(chalk.dim('[' + moment().format('HH:mm:ss') + '] [write-file-webpack-plugin]'), ...append);
};
const assetSourceHashIndex = {};
log('options', options);
const apply = (compiler) => {
let outputPath;
let setupDone;
let setupStatus;
const setup = (): boolean => {
if (setupDone) {
return setupStatus;
}
setupDone = true;
log('compiler.outputFileSystem is "' + chalk.cyan(compiler.outputFileSystem.constructor.name) + '".');
if (!isMemoryFileSystem(compiler.outputFileSystem) && !options.force) {
return false;
}
if (_.has(compiler, 'options.output.path') && compiler.options.output.path !== '/') {
outputPath = compiler.options.output.path;
}
if (!outputPath) {
throw new Error('output.path is not defined. Define output.path.');
}
log('outputPath is "' + chalk.cyan(outputPath) + '".');
setupStatus = true;
return setupStatus;
};
// eslint-disable-next-line promise/prefer-await-to-callbacks
const handleAfterEmit = (compilation, callback) => {
if (!setup()) {
// eslint-disable-next-line promise/prefer-await-to-callbacks
callback();
return;
}
if (options.exitOnErrors && compilation.errors.length) {
// eslint-disable-next-line promise/prefer-await-to-callbacks
callback();
return;
}
log('compilation.errors.length is "' + chalk.cyan(compilation.errors.length) + '".');
_.forEach(compilation.assets, (asset, assetPath) => {
const outputFilePath = path.isAbsolute(assetPath) ? assetPath : path.join(outputPath, assetPath);
const relativeOutputPath = path.relative(process.cwd(), outputFilePath);
const targetDefinition = 'asset: ' + chalk.cyan('./' + assetPath) + '; destination: ' + chalk.cyan('./' + relativeOutputPath);
const test = options.test;
if (test) {
const skip = _.isRegExp(test) ? !test.test(assetPath) : !test(assetPath);
if (skip) {
log(targetDefinition, chalk.yellow('[skipped; does not match test]'));
return;
}
}
const assetSize = asset.size();
const assetSource = Array.isArray(asset.source()) ? asset.source().join('\n') : asset.source();
if (options.useHashIndex) {
const assetSourceHash = createHash('sha256').update(assetSource).digest('hex');
if (assetSourceHashIndex[relativeOutputPath] && assetSourceHashIndex[relativeOutputPath] === assetSourceHash) {
log(targetDefinition, chalk.yellow('[skipped; matched hash index]'));
return;
}
assetSourceHashIndex[relativeOutputPath] = assetSourceHash;
}
mkdirp.sync(path.dirname(relativeOutputPath));
try {
writeFileMethod(relativeOutputPath.split('?')[0], assetSource);
log(targetDefinition, chalk.green('[written]'), chalk.magenta('(' + filesize(assetSize) + ')'));
} catch (error) {
log(targetDefinition, chalk.bold.red('[is not written]'), chalk.magenta('(' + filesize(assetSize) + ')'));
log(chalk.bold.bgRed('Exception:'), chalk.bold.red(error.message));
}
});
// eslint-disable-next-line promise/prefer-await-to-callbacks
callback();
};
/**
* webpack 4+ comes with a new plugin system.
*
* Check for hooks in-order to support old plugin system
*/
if (compiler.hooks) {
compiler.hooks.afterEmit.tapAsync('write-file-webpack-plugin', handleAfterEmit);
} else {
compiler.plugin('after-emit', handleAfterEmit);
}
};
return {
apply
};
}