1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 | 'use strict';
|
8 |
|
9 | const address = require('address');
|
10 | const fs = require('fs');
|
11 | const path = require('path');
|
12 | const url = require('url');
|
13 | const chalk = require('chalk');
|
14 | const detect = require('detect-port-alt');
|
15 | const isRoot = require('is-root');
|
16 | const prompts = require('prompts');
|
17 | const clearConsole = require('./clearConsole');
|
18 | const formatWebpackMessages = require('./formatWebpackMessages');
|
19 | const getProcessForPort = require('./getProcessForPort');
|
20 | const typescriptFormatter = require('./typescriptFormatter');
|
21 | const forkTsCheckerWebpackPlugin = require('./ForkTsCheckerWebpackPlugin');
|
22 |
|
23 | const isInteractive = process.stdout.isTTY;
|
24 |
|
25 | function prepareUrls(protocol, host, port, pathname = '/') {
|
26 | const formatUrl = hostname =>
|
27 | url.format({
|
28 | protocol,
|
29 | hostname,
|
30 | port,
|
31 | pathname,
|
32 | });
|
33 | const prettyPrintUrl = hostname =>
|
34 | url.format({
|
35 | protocol,
|
36 | hostname,
|
37 | port: chalk.bold(port),
|
38 | pathname,
|
39 | });
|
40 |
|
41 | const isUnspecifiedHost = host === '0.0.0.0' || host === '::';
|
42 | let prettyHost, lanUrlForConfig, lanUrlForTerminal;
|
43 | if (isUnspecifiedHost) {
|
44 | prettyHost = 'localhost';
|
45 | try {
|
46 |
|
47 | lanUrlForConfig = address.ip();
|
48 | if (lanUrlForConfig) {
|
49 |
|
50 |
|
51 | if (
|
52 | /^10[.]|^172[.](1[6-9]|2[0-9]|3[0-1])[.]|^192[.]168[.]/.test(
|
53 | lanUrlForConfig
|
54 | )
|
55 | ) {
|
56 |
|
57 | lanUrlForTerminal = prettyPrintUrl(lanUrlForConfig);
|
58 | } else {
|
59 |
|
60 | lanUrlForConfig = undefined;
|
61 | }
|
62 | }
|
63 | } catch (_e) {
|
64 |
|
65 | }
|
66 | } else {
|
67 | prettyHost = host;
|
68 | }
|
69 | const localUrlForTerminal = prettyPrintUrl(prettyHost);
|
70 | const localUrlForBrowser = formatUrl(prettyHost);
|
71 | return {
|
72 | lanUrlForConfig,
|
73 | lanUrlForTerminal,
|
74 | localUrlForTerminal,
|
75 | localUrlForBrowser,
|
76 | };
|
77 | }
|
78 |
|
79 | function printInstructions(appName, urls, useYarn) {
|
80 | console.log();
|
81 | console.log(`You can now view ${chalk.bold(appName)} in the browser.`);
|
82 | console.log();
|
83 |
|
84 | if (urls.lanUrlForTerminal) {
|
85 | console.log(
|
86 | ` ${chalk.bold('Local:')} ${urls.localUrlForTerminal}`
|
87 | );
|
88 | console.log(
|
89 | ` ${chalk.bold('On Your Network:')} ${urls.lanUrlForTerminal}`
|
90 | );
|
91 | } else {
|
92 | console.log(` ${urls.localUrlForTerminal}`);
|
93 | }
|
94 |
|
95 | console.log();
|
96 | console.log('Note that the development build is not optimized.');
|
97 | console.log(
|
98 | `To create a production build, use ` +
|
99 | `${chalk.cyan(`${useYarn ? 'yarn' : 'npm run'} build`)}.`
|
100 | );
|
101 | console.log();
|
102 | }
|
103 |
|
104 | function createCompiler({
|
105 | appName,
|
106 | config,
|
107 | devSocket,
|
108 | urls,
|
109 | useYarn,
|
110 | useTypeScript,
|
111 | tscCompileOnError,
|
112 | webpack,
|
113 | }) {
|
114 |
|
115 |
|
116 | let compiler;
|
117 | try {
|
118 | compiler = webpack(config);
|
119 | } catch (err) {
|
120 | console.log(chalk.red('Failed to compile.'));
|
121 | console.log();
|
122 | console.log(err.message || err);
|
123 | console.log();
|
124 | process.exit(1);
|
125 | }
|
126 |
|
127 |
|
128 |
|
129 |
|
130 |
|
131 | compiler.hooks.invalid.tap('invalid', () => {
|
132 | if (isInteractive) {
|
133 | clearConsole();
|
134 | }
|
135 | console.log('Compiling...');
|
136 | });
|
137 |
|
138 | let isFirstCompile = true;
|
139 | let tsMessagesPromise;
|
140 | let tsMessagesResolver;
|
141 |
|
142 | if (useTypeScript) {
|
143 | compiler.hooks.beforeCompile.tap('beforeCompile', () => {
|
144 | tsMessagesPromise = new Promise(resolve => {
|
145 | tsMessagesResolver = msgs => resolve(msgs);
|
146 | });
|
147 | });
|
148 |
|
149 | forkTsCheckerWebpackPlugin
|
150 | .getCompilerHooks(compiler)
|
151 | .receive.tap('afterTypeScriptCheck', (diagnostics, lints) => {
|
152 | const allMsgs = [...diagnostics, ...lints];
|
153 | const format = message =>
|
154 | `${message.file}\n${typescriptFormatter(message, true)}`;
|
155 |
|
156 | tsMessagesResolver({
|
157 | errors: allMsgs.filter(msg => msg.severity === 'error').map(format),
|
158 | warnings: allMsgs
|
159 | .filter(msg => msg.severity === 'warning')
|
160 | .map(format),
|
161 | });
|
162 | });
|
163 | }
|
164 |
|
165 |
|
166 |
|
167 | compiler.hooks.done.tap('done', async stats => {
|
168 | if (isInteractive) {
|
169 | clearConsole();
|
170 | }
|
171 |
|
172 |
|
173 |
|
174 |
|
175 |
|
176 |
|
177 | const statsData = stats.toJson({
|
178 | all: false,
|
179 | warnings: true,
|
180 | errors: true,
|
181 | });
|
182 |
|
183 | if (useTypeScript && statsData.errors.length === 0) {
|
184 | const delayedMsg = setTimeout(() => {
|
185 | console.log(
|
186 | chalk.yellow(
|
187 | 'Files successfully emitted, waiting for typecheck results...'
|
188 | )
|
189 | );
|
190 | }, 100);
|
191 |
|
192 | const messages = await tsMessagesPromise;
|
193 | clearTimeout(delayedMsg);
|
194 | if (tscCompileOnError) {
|
195 | statsData.warnings.push(...messages.errors);
|
196 | } else {
|
197 | statsData.errors.push(...messages.errors);
|
198 | }
|
199 | statsData.warnings.push(...messages.warnings);
|
200 |
|
201 |
|
202 |
|
203 | if (tscCompileOnError) {
|
204 | stats.compilation.warnings.push(...messages.errors);
|
205 | } else {
|
206 | stats.compilation.errors.push(...messages.errors);
|
207 | }
|
208 | stats.compilation.warnings.push(...messages.warnings);
|
209 |
|
210 | if (messages.errors.length > 0) {
|
211 | if (tscCompileOnError) {
|
212 | devSocket.warnings(messages.errors);
|
213 | } else {
|
214 | devSocket.errors(messages.errors);
|
215 | }
|
216 | } else if (messages.warnings.length > 0) {
|
217 | devSocket.warnings(messages.warnings);
|
218 | }
|
219 |
|
220 | if (isInteractive) {
|
221 | clearConsole();
|
222 | }
|
223 | }
|
224 |
|
225 | const messages = formatWebpackMessages(statsData);
|
226 | const isSuccessful = !messages.errors.length && !messages.warnings.length;
|
227 | if (isSuccessful) {
|
228 | console.log(chalk.green('Compiled successfully!'));
|
229 | }
|
230 | if (isSuccessful && (isInteractive || isFirstCompile)) {
|
231 | printInstructions(appName, urls, useYarn);
|
232 | }
|
233 | isFirstCompile = false;
|
234 |
|
235 |
|
236 | if (messages.errors.length) {
|
237 |
|
238 |
|
239 | if (messages.errors.length > 1) {
|
240 | messages.errors.length = 1;
|
241 | }
|
242 | console.log(chalk.red('Failed to compile.\n'));
|
243 | console.log(messages.errors.join('\n\n'));
|
244 | return;
|
245 | }
|
246 |
|
247 |
|
248 | if (messages.warnings.length) {
|
249 | console.log(chalk.yellow('Compiled with warnings.\n'));
|
250 | console.log(messages.warnings.join('\n\n'));
|
251 |
|
252 |
|
253 | console.log(
|
254 | '\nSearch for the ' +
|
255 | chalk.underline(chalk.yellow('keywords')) +
|
256 | ' to learn more about each warning.'
|
257 | );
|
258 | console.log(
|
259 | 'To ignore, add ' +
|
260 | chalk.cyan('// eslint-disable-next-line') +
|
261 | ' to the line before.\n'
|
262 | );
|
263 | }
|
264 | });
|
265 |
|
266 |
|
267 |
|
268 | const isSmokeTest = process.argv.some(
|
269 | arg => arg.indexOf('--smoke-test') > -1
|
270 | );
|
271 | if (isSmokeTest) {
|
272 | compiler.hooks.failed.tap('smokeTest', async () => {
|
273 | await tsMessagesPromise;
|
274 | process.exit(1);
|
275 | });
|
276 | compiler.hooks.done.tap('smokeTest', async stats => {
|
277 | await tsMessagesPromise;
|
278 | if (stats.hasErrors() || stats.hasWarnings()) {
|
279 | process.exit(1);
|
280 | } else {
|
281 | process.exit(0);
|
282 | }
|
283 | });
|
284 | }
|
285 |
|
286 | return compiler;
|
287 | }
|
288 |
|
289 | function resolveLoopback(proxy) {
|
290 | const o = url.parse(proxy);
|
291 | o.host = undefined;
|
292 | if (o.hostname !== 'localhost') {
|
293 | return proxy;
|
294 | }
|
295 |
|
296 |
|
297 |
|
298 |
|
299 | |
300 |
|
301 |
|
302 |
|
303 |
|
304 |
|
305 | try {
|
306 |
|
307 |
|
308 |
|
309 | if (!address.ip()) {
|
310 | o.hostname = '127.0.0.1';
|
311 | }
|
312 | } catch (_ignored) {
|
313 | o.hostname = '127.0.0.1';
|
314 | }
|
315 | return url.format(o);
|
316 | }
|
317 |
|
318 |
|
319 |
|
320 | function onProxyError(proxy) {
|
321 | return (err, req, res) => {
|
322 | const host = req.headers && req.headers.host;
|
323 | console.log(
|
324 | chalk.red('Proxy error:') +
|
325 | ' Could not proxy request ' +
|
326 | chalk.cyan(req.url) +
|
327 | ' from ' +
|
328 | chalk.cyan(host) +
|
329 | ' to ' +
|
330 | chalk.cyan(proxy) +
|
331 | '.'
|
332 | );
|
333 | console.log(
|
334 | 'See https://nodejs.org/api/errors.html#errors_common_system_errors for more information (' +
|
335 | chalk.cyan(err.code) +
|
336 | ').'
|
337 | );
|
338 | console.log();
|
339 |
|
340 |
|
341 |
|
342 | if (res.writeHead && !res.headersSent) {
|
343 | res.writeHead(500);
|
344 | }
|
345 | res.end(
|
346 | 'Proxy error: Could not proxy request ' +
|
347 | req.url +
|
348 | ' from ' +
|
349 | host +
|
350 | ' to ' +
|
351 | proxy +
|
352 | ' (' +
|
353 | err.code +
|
354 | ').'
|
355 | );
|
356 | };
|
357 | }
|
358 |
|
359 | function prepareProxy(proxy, appPublicFolder, servedPathname) {
|
360 |
|
361 | if (!proxy) {
|
362 | return undefined;
|
363 | }
|
364 | if (typeof proxy !== 'string') {
|
365 | console.log(
|
366 | chalk.red('When specified, "proxy" in package.json must be a string.')
|
367 | );
|
368 | console.log(
|
369 | chalk.red('Instead, the type of "proxy" was "' + typeof proxy + '".')
|
370 | );
|
371 | console.log(
|
372 | chalk.red('Either remove "proxy" from package.json, or make it a string.')
|
373 | );
|
374 | process.exit(1);
|
375 | }
|
376 |
|
377 |
|
378 |
|
379 |
|
380 | const sockPath = process.env.WDS_SOCKET_PATH || '/sockjs-node';
|
381 | const isDefaultSockHost = !process.env.WDS_SOCKET_HOST;
|
382 | function mayProxy(pathname) {
|
383 | const maybePublicPath = path.resolve(
|
384 | appPublicFolder,
|
385 | pathname.replace(new RegExp('^' + servedPathname), '')
|
386 | );
|
387 | const isPublicFileRequest = fs.existsSync(maybePublicPath);
|
388 |
|
389 | const isWdsEndpointRequest =
|
390 | isDefaultSockHost && pathname.startsWith(sockPath);
|
391 | return !(isPublicFileRequest || isWdsEndpointRequest);
|
392 | }
|
393 |
|
394 | if (!/^http(s)?:\/\//.test(proxy)) {
|
395 | console.log(
|
396 | chalk.red(
|
397 | 'When "proxy" is specified in package.json it must start with either http:// or https://'
|
398 | )
|
399 | );
|
400 | process.exit(1);
|
401 | }
|
402 |
|
403 | let target;
|
404 | if (process.platform === 'win32') {
|
405 | target = resolveLoopback(proxy);
|
406 | } else {
|
407 | target = proxy;
|
408 | }
|
409 | return [
|
410 | {
|
411 | target,
|
412 | logLevel: 'silent',
|
413 |
|
414 |
|
415 |
|
416 |
|
417 |
|
418 |
|
419 |
|
420 |
|
421 |
|
422 |
|
423 | context: function (pathname, req) {
|
424 | return (
|
425 | req.method !== 'GET' ||
|
426 | (mayProxy(pathname) &&
|
427 | req.headers.accept &&
|
428 | req.headers.accept.indexOf('text/html') === -1)
|
429 | );
|
430 | },
|
431 | onProxyReq: proxyReq => {
|
432 |
|
433 |
|
434 |
|
435 | if (proxyReq.getHeader('origin')) {
|
436 | proxyReq.setHeader('origin', target);
|
437 | }
|
438 | },
|
439 | onError: onProxyError(target),
|
440 | secure: false,
|
441 | changeOrigin: true,
|
442 | ws: true,
|
443 | xfwd: true,
|
444 | },
|
445 | ];
|
446 | }
|
447 |
|
448 | function choosePort(host, defaultPort) {
|
449 | return detect(defaultPort, host).then(
|
450 | port =>
|
451 | new Promise(resolve => {
|
452 | if (port === defaultPort) {
|
453 | return resolve(port);
|
454 | }
|
455 | const message =
|
456 | process.platform !== 'win32' && defaultPort < 1024 && !isRoot()
|
457 | ? `Admin permissions are required to run a server on a port below 1024.`
|
458 | : `Something is already running on port ${defaultPort}.`;
|
459 | if (isInteractive) {
|
460 | clearConsole();
|
461 | const existingProcess = getProcessForPort(defaultPort);
|
462 | const question = {
|
463 | type: 'confirm',
|
464 | name: 'shouldChangePort',
|
465 | message:
|
466 | chalk.yellow(
|
467 | message +
|
468 | `${existingProcess ? ` Probably:\n ${existingProcess}` : ''}`
|
469 | ) + '\n\nWould you like to run the app on another port instead?',
|
470 | initial: true,
|
471 | };
|
472 | prompts(question).then(answer => {
|
473 | if (answer.shouldChangePort) {
|
474 | resolve(port);
|
475 | } else {
|
476 | resolve(null);
|
477 | }
|
478 | });
|
479 | } else {
|
480 | console.log(chalk.red(message));
|
481 | resolve(null);
|
482 | }
|
483 | }),
|
484 | err => {
|
485 | throw new Error(
|
486 | chalk.red(`Could not find an open port at ${chalk.bold(host)}.`) +
|
487 | '\n' +
|
488 | ('Network error message: ' + err.message || err) +
|
489 | '\n'
|
490 | );
|
491 | }
|
492 | );
|
493 | }
|
494 |
|
495 | module.exports = {
|
496 | choosePort,
|
497 | createCompiler,
|
498 | prepareProxy,
|
499 | prepareUrls,
|
500 | };
|