1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 | const EventEmitter = require('events');
|
12 | const { existsSync } = require('fs');
|
13 | const { join } = require('path');
|
14 |
|
15 | const chalk = require('chalk');
|
16 | const globby = require('globby');
|
17 | const Koa = require('koa');
|
18 | const { customAlphabet } = require('nanoid');
|
19 | const { DefinePlugin, ProgressPlugin } = require('webpack');
|
20 |
|
21 | const { init: initHmrPlugin } = require('./plugins/hmr');
|
22 | const { init: initRamdiskPlugin } = require('./plugins/ramdisk');
|
23 | const { forceError, getLogger } = require('./log');
|
24 | const { start } = require('./server');
|
25 | const { validate } = require('./validate');
|
26 |
|
27 | const defaults = {
|
28 |
|
29 |
|
30 | compress: null,
|
31 | headers: null,
|
32 | historyFallback: false,
|
33 | hmr: true,
|
34 | host: null,
|
35 | liveReload: false,
|
36 | log: { level: 'info' },
|
37 | middleware: () => {},
|
38 | open: false,
|
39 | port: 55555,
|
40 | progress: true,
|
41 | publicPath: null,
|
42 | ramdisk: false,
|
43 | secure: false,
|
44 | static: null,
|
45 | status: true
|
46 | };
|
47 |
|
48 | const key = 'webpack-plugin-serve';
|
49 | const newline = () => console.log();
|
50 | const nanoid = customAlphabet('1234567890abcdef', 7);
|
51 |
|
52 | let instance = null;
|
53 |
|
54 |
|
55 | class WebpackPluginServe extends EventEmitter {
|
56 | constructor(opts = {}) {
|
57 | super();
|
58 |
|
59 | const valid = validate(opts);
|
60 |
|
61 | if (valid.error) {
|
62 | forceError('An option was passed to WebpackPluginServe that is not valid');
|
63 | throw valid.error;
|
64 | }
|
65 |
|
66 |
|
67 |
|
68 |
|
69 |
|
70 | if (!opts.allowMany && instance) {
|
71 | instance.log.error(
|
72 | 'Duplicate instances created. Only the first instance of this plugin will be active.'
|
73 | );
|
74 | return;
|
75 | }
|
76 |
|
77 | instance = this;
|
78 |
|
79 | const options = Object.assign({}, defaults, opts);
|
80 |
|
81 | if (options.compress === true) {
|
82 | options.compress = {};
|
83 | }
|
84 |
|
85 | if (options.historyFallback === true) {
|
86 | options.historyFallback = {};
|
87 | }
|
88 |
|
89 |
|
90 |
|
91 |
|
92 | if (typeof options.host === 'string') {
|
93 | const { host } = options;
|
94 | options.host = {
|
95 | then(r) {
|
96 | r(host);
|
97 | }
|
98 | };
|
99 | }
|
100 |
|
101 | if (Number.isInteger(options.port)) {
|
102 | const { port } = options;
|
103 | options.port = {
|
104 | then(r) {
|
105 | r(port);
|
106 | }
|
107 | };
|
108 | }
|
109 |
|
110 | if (!options.static) {
|
111 | options.static = [];
|
112 | } else if (options.static.glob) {
|
113 | const { glob, options: globOptions = {} } = options.static;
|
114 | options.static = globby.sync(glob, globOptions);
|
115 | }
|
116 |
|
117 | this.app = new Koa();
|
118 | this.log = getLogger(options.log || {});
|
119 | this.options = options;
|
120 | this.compilers = [];
|
121 | this.state = {};
|
122 | }
|
123 |
|
124 | apply(compiler) {
|
125 | this.compiler = compiler;
|
126 |
|
127 |
|
128 |
|
129 | if (instance !== this) {
|
130 | return;
|
131 | }
|
132 |
|
133 | this.hook(compiler);
|
134 | }
|
135 |
|
136 |
|
137 | attach() {
|
138 | const self = this;
|
139 | const result = {
|
140 | apply(compiler) {
|
141 | return self.hook(compiler);
|
142 | }
|
143 | };
|
144 | return result;
|
145 | }
|
146 |
|
147 |
|
148 | emit(eventName, ...args) {
|
149 | const listeners = this.eventNames();
|
150 |
|
151 | if (listeners.includes(eventName)) {
|
152 | super.emit(eventName, ...args);
|
153 | } else {
|
154 |
|
155 | if (eventName === 'close') {
|
156 | return;
|
157 | }
|
158 | const [data] = args;
|
159 | super.emit('unhandled', { eventName, data });
|
160 | }
|
161 | }
|
162 |
|
163 | hook(compiler) {
|
164 | const { done, invalid, watchClose, watchRun } = compiler.hooks;
|
165 |
|
166 | if (!compiler.wpsId) {
|
167 |
|
168 | compiler.wpsId = nanoid();
|
169 | }
|
170 |
|
171 | if (!compiler.name && !compiler.options.name) {
|
172 |
|
173 | compiler.options.name = this.compilers.length.toString();
|
174 | this.compilers.push(compiler);
|
175 | }
|
176 |
|
177 | if (this.options.hmr) {
|
178 | initHmrPlugin(compiler, this.log);
|
179 | }
|
180 |
|
181 | if (this.options.ramdisk) {
|
182 | initRamdiskPlugin.call(this, compiler, this.options.ramdisk);
|
183 | }
|
184 |
|
185 | if (!this.options.static.length) {
|
186 | this.options.static.push(compiler.context);
|
187 | }
|
188 |
|
189 |
|
190 | const publicPath =
|
191 | this.options.publicPath === null
|
192 | ? compiler.options.output.publicPath
|
193 | : this.options.publicPath;
|
194 | if (publicPath) {
|
195 | let foundPath = false;
|
196 | for (const path of this.options.static) {
|
197 | const joined = join(path, publicPath);
|
198 | if (existsSync(joined)) {
|
199 | foundPath = true;
|
200 | break;
|
201 | }
|
202 | }
|
203 |
|
204 |
|
205 | if (!foundPath) {
|
206 | this.log.warn(
|
207 | chalk`{bold {yellow Warning}} The value of {yellow \`publicPath\`} was not found on the filesystem in any static paths specified\n`
|
208 | );
|
209 | }
|
210 | }
|
211 |
|
212 |
|
213 |
|
214 | done.tap(key, (stats) => this.emit('done', stats, compiler));
|
215 | invalid.tap(key, (filePath) => this.emit('invalid', filePath, compiler));
|
216 | watchClose.tap(key, () => this.emit('close', compiler));
|
217 |
|
218 | if (this.options.waitForBuild) {
|
219 |
|
220 | this.state.compiling = new Promise((resolve) => {
|
221 | this.once('done', () => resolve());
|
222 | });
|
223 |
|
224 |
|
225 | this.on('invalid', () => {
|
226 |
|
227 | this.state.compiling = new Promise((resolve) => {
|
228 | this.once('done', () => resolve());
|
229 | });
|
230 | });
|
231 | }
|
232 |
|
233 | compiler.hooks.compilation.tap(key, (compilation) => {
|
234 | compilation.hooks.afterHash.tap(key, () => {
|
235 |
|
236 |
|
237 |
|
238 |
|
239 | if (this.lastHash === compilation.hash) {
|
240 | return;
|
241 | }
|
242 | this.lastHash = compilation.hash;
|
243 | this.emit('build', compiler.name, compiler);
|
244 | });
|
245 | });
|
246 |
|
247 | watchRun.tapPromise(key, async () => {
|
248 | if (!this.state.starting) {
|
249 |
|
250 | this.state.starting = start.bind(this)();
|
251 | this.state.starting.then(() => newline());
|
252 | }
|
253 |
|
254 |
|
255 | await this.state.starting;
|
256 |
|
257 | const compilerData = {
|
258 |
|
259 |
|
260 | compilerName: this.compilers.length > 1 ? compiler.options.name : null,
|
261 | wpsId: compiler.wpsId
|
262 | };
|
263 |
|
264 | const defineObject = Object.assign({}, this.options, compilerData);
|
265 | const defineData = { ʎɐɹɔosǝʌɹǝs: JSON.stringify(defineObject) };
|
266 | const definePlugin = new DefinePlugin(defineData);
|
267 |
|
268 | definePlugin.apply(compiler);
|
269 |
|
270 | if (this.options.progress) {
|
271 | const progressPlugin = new ProgressPlugin((percent, message, misc) => {
|
272 |
|
273 |
|
274 | this.emit('progress', { percent, message, misc }, compiler);
|
275 | });
|
276 |
|
277 | progressPlugin.apply(compiler);
|
278 | }
|
279 | });
|
280 | }
|
281 | }
|
282 |
|
283 | module.exports = { defaults, WebpackPluginServe };
|