UNPKG

14.6 kBJavaScriptView Raw
1// @flow
2
3import type {InitialParcelOptions} from '@parcel/types';
4import {BuildError} from '@parcel/core';
5import {NodeFS} from '@parcel/fs';
6import ThrowableDiagnostic from '@parcel/diagnostic';
7import {prettyDiagnostic, openInBrowser} from '@parcel/utils';
8import {Disposable} from '@parcel/events';
9import {INTERNAL_ORIGINAL_CONSOLE} from '@parcel/logger';
10import chalk from 'chalk';
11import commander from 'commander';
12import path from 'path';
13import getPort from 'get-port';
14import {version} from '../package.json';
15
16require('v8-compile-cache');
17
18const program = new commander.Command();
19
20// Exit codes in response to signals are traditionally
21// 128 + signal value
22// https://tldp.org/LDP/abs/html/exitcodes.html
23const SIGINT_EXIT_CODE = 130;
24
25async function logUncaughtError(e: mixed) {
26 if (e instanceof ThrowableDiagnostic) {
27 for (let diagnostic of e.diagnostics) {
28 let {message, codeframe, stack, hints, documentation} =
29 await prettyDiagnostic(diagnostic);
30 INTERNAL_ORIGINAL_CONSOLE.error(chalk.red(message));
31 if (codeframe || stack) {
32 INTERNAL_ORIGINAL_CONSOLE.error('');
33 }
34 INTERNAL_ORIGINAL_CONSOLE.error(codeframe);
35 INTERNAL_ORIGINAL_CONSOLE.error(stack);
36 if ((stack || codeframe) && hints.length > 0) {
37 INTERNAL_ORIGINAL_CONSOLE.error('');
38 }
39 for (let h of hints) {
40 INTERNAL_ORIGINAL_CONSOLE.error(chalk.blue(h));
41 }
42 if (documentation) {
43 INTERNAL_ORIGINAL_CONSOLE.error(chalk.magenta.bold(documentation));
44 }
45 }
46 } else {
47 INTERNAL_ORIGINAL_CONSOLE.error(e);
48 }
49
50 // A hack to definitely ensure we logged the uncaught exception
51 await new Promise(resolve => setTimeout(resolve, 100));
52}
53
54const handleUncaughtException = async exception => {
55 try {
56 await logUncaughtError(exception);
57 } catch (err) {
58 console.error(exception);
59 console.error(err);
60 }
61
62 process.exit(1);
63};
64
65process.on('unhandledRejection', handleUncaughtException);
66
67program.storeOptionsAsProperties();
68program.version(version);
69
70// --no-cache, --cache-dir, --no-source-maps, --no-autoinstall, --global?, --public-url, --log-level
71// --no-content-hash, --experimental-scope-hoisting, --detailed-report
72
73const commonOptions = {
74 '--no-cache': 'disable the filesystem cache',
75 '--config <path>':
76 'specify which config to use. can be a path or a package name',
77 '--cache-dir <path>': 'set the cache directory. defaults to ".parcel-cache"',
78 '--no-source-maps': 'disable sourcemaps',
79 '--target [name]': [
80 'only build given target(s)',
81 (val, list) => list.concat([val]),
82 [],
83 ],
84 '--log-level <level>': new commander.Option(
85 '--log-level <level>',
86 'set the log level',
87 ).choices(['none', 'error', 'warn', 'info', 'verbose']),
88 '--dist-dir <dir>':
89 'output directory to write to when unspecified by targets',
90 '--no-autoinstall': 'disable autoinstall',
91 '--profile': 'enable build profiling',
92 '-V, --version': 'output the version number',
93 '--detailed-report [count]': [
94 'print the asset timings and sizes in the build report',
95 parseOptionInt,
96 ],
97 '--reporter <name>': [
98 'additional reporters to run',
99 (val, acc) => {
100 acc.push(val);
101 return acc;
102 },
103 [],
104 ],
105};
106
107var hmrOptions = {
108 '--no-hmr': 'disable hot module replacement',
109 '-p, --port <port>': [
110 'set the port to serve on. defaults to $PORT or 1234',
111 process.env.PORT,
112 ],
113 '--host <host>':
114 'set the host to listen on, defaults to listening on all interfaces',
115 '--https': 'serves files over HTTPS',
116 '--cert <path>': 'path to certificate to use with HTTPS',
117 '--key <path>': 'path to private key to use with HTTPS',
118 '--hmr-port <port>': ['hot module replacement port', process.env.HMR_PORT],
119};
120
121function applyOptions(cmd, options) {
122 for (let opt in options) {
123 const option = options[opt];
124 if (option instanceof commander.Option) {
125 cmd.addOption(option);
126 } else {
127 cmd.option(opt, ...(Array.isArray(option) ? option : [option]));
128 }
129 }
130}
131
132let serve = program
133 .command('serve [input...]')
134 .description('starts a development server')
135 .option('--public-url <url>', 'the path prefix for absolute urls')
136 .option(
137 '--open [browser]',
138 'automatically open in specified browser, defaults to default browser',
139 )
140 .option('--watch-for-stdin', 'exit when stdin closes')
141 .option(
142 '--lazy',
143 'Build async bundles on demand, when requested in the browser',
144 )
145 .action(runCommand);
146
147applyOptions(serve, hmrOptions);
148applyOptions(serve, commonOptions);
149
150let watch = program
151 .command('watch [input...]')
152 .description('starts the bundler in watch mode')
153 .option('--public-url <url>', 'the path prefix for absolute urls')
154 .option('--no-content-hash', 'disable content hashing')
155 .option('--watch-for-stdin', 'exit when stdin closes')
156 .action(runCommand);
157
158applyOptions(watch, hmrOptions);
159applyOptions(watch, commonOptions);
160
161let build = program
162 .command('build [input...]')
163 .description('bundles for production')
164 .option('--no-optimize', 'disable minification')
165 .option('--no-scope-hoist', 'disable scope-hoisting')
166 .option('--public-url <url>', 'the path prefix for absolute urls')
167 .option('--no-content-hash', 'disable content hashing')
168 .action(runCommand);
169
170applyOptions(build, commonOptions);
171
172program
173 .command('help [command]')
174 .description('display help information for a command')
175 .action(function (command) {
176 let cmd = program.commands.find(c => c.name() === command) || program;
177 cmd.help();
178 });
179
180program.on('--help', function () {
181 INTERNAL_ORIGINAL_CONSOLE.log('');
182 INTERNAL_ORIGINAL_CONSOLE.log(
183 ' Run `' +
184 chalk.bold('parcel help <command>') +
185 '` for more information on specific commands',
186 );
187 INTERNAL_ORIGINAL_CONSOLE.log('');
188});
189
190// Override to output option description if argument was missing
191commander.Command.prototype.optionMissingArgument = function (option) {
192 INTERNAL_ORIGINAL_CONSOLE.error(
193 "error: option `%s' argument missing",
194 option.flags,
195 );
196 INTERNAL_ORIGINAL_CONSOLE.log(program.createHelp().optionDescription(option));
197 process.exit(1);
198};
199
200// Make serve the default command except for --help
201var args = process.argv;
202if (args[2] === '--help' || args[2] === '-h') args[2] = 'help';
203
204if (!args[2] || !program.commands.some(c => c.name() === args[2])) {
205 args.splice(2, 0, 'serve');
206}
207
208program.parse(args);
209
210function runCommand(...args) {
211 run(...args).catch(handleUncaughtException);
212}
213
214async function run(
215 entries: Array<string>,
216 _opts: any, // using pre v7 Commander options as properties
217 command: any,
218) {
219 if (entries.length === 0) {
220 entries = ['.'];
221 }
222
223 entries = entries.map(entry => path.resolve(entry));
224
225 let Parcel = require('@parcel/core').default;
226 let fs = new NodeFS();
227 let options = await normalizeOptions(command, fs);
228 let parcel = new Parcel({
229 entries,
230 // $FlowFixMe[extra-arg] - flow doesn't know about the `paths` option (added in Node v8.9.0)
231 defaultConfig: require.resolve('@parcel/config-default', {
232 paths: [fs.cwd(), __dirname],
233 }),
234 shouldPatchConsole: true,
235 ...options,
236 });
237
238 let disposable = new Disposable();
239 let unsubscribe: () => Promise<mixed>;
240 let isExiting;
241 async function exit(exitCode: number = 0) {
242 if (isExiting) {
243 return;
244 }
245
246 isExiting = true;
247 if (unsubscribe != null) {
248 await unsubscribe();
249 } else if (parcel.isProfiling) {
250 await parcel.stopProfiling();
251 }
252
253 if (process.stdin.isTTY && process.stdin.isRaw) {
254 // $FlowFixMe
255 process.stdin.setRawMode(false);
256 }
257
258 disposable.dispose();
259 process.exit(exitCode);
260 }
261
262 const isWatching = command.name() === 'watch' || command.name() === 'serve';
263 if (process.stdin.isTTY) {
264 // $FlowFixMe
265 process.stdin.setRawMode(true);
266 require('readline').emitKeypressEvents(process.stdin);
267
268 let stream = process.stdin.on('keypress', async (char, key) => {
269 if (!key.ctrl) {
270 return;
271 }
272
273 switch (key.name) {
274 case 'c':
275 // Detect the ctrl+c key, and gracefully exit after writing the asset graph to the cache.
276 // This is mostly for tools that wrap Parcel as a child process like yarn and npm.
277 //
278 // Setting raw mode prevents SIGINT from being sent in response to ctrl-c:
279 // https://nodejs.org/api/tty.html#tty_readstream_setrawmode_mode
280 //
281 // We don't use the SIGINT event for this because when run inside yarn, the parent
282 // yarn process ends before Parcel and it appears that Parcel has ended while it may still
283 // be cleaning up. Handling events from stdin prevents this impression.
284
285 // Enqueue a busy message to be shown if Parcel doesn't shut down
286 // within the timeout.
287 setTimeout(
288 () =>
289 INTERNAL_ORIGINAL_CONSOLE.log(
290 chalk.bold.yellowBright('Parcel is shutting down...'),
291 ),
292 500,
293 );
294 // When watching, a 0 success code is acceptable when Parcel is interrupted with ctrl-c.
295 // When building, fail with a code as if we received a SIGINT.
296 await exit(isWatching ? 0 : SIGINT_EXIT_CODE);
297 break;
298 case 'e':
299 await (parcel.isProfiling
300 ? parcel.stopProfiling()
301 : parcel.startProfiling());
302 break;
303 case 'y':
304 await parcel.takeHeapSnapshot();
305 break;
306 }
307 });
308
309 disposable.add(() => {
310 stream.destroy();
311 });
312 }
313
314 if (isWatching) {
315 ({unsubscribe} = await parcel.watch(err => {
316 if (err) {
317 throw err;
318 }
319 }));
320
321 if (command.open && options.serveOptions) {
322 await openInBrowser(
323 `${options.serveOptions.https ? 'https' : 'http'}://${
324 options.serveOptions.host || 'localhost'
325 }:${options.serveOptions.port}`,
326 command.open,
327 );
328 }
329
330 if (command.watchForStdin) {
331 process.stdin.on('end', async () => {
332 INTERNAL_ORIGINAL_CONSOLE.log('STDIN closed, ending');
333
334 await exit();
335 });
336 process.stdin.resume();
337 }
338
339 // In non-tty cases, respond to SIGINT by cleaning up. Since we're watching,
340 // a 0 success code is acceptable.
341 process.on('SIGINT', exit);
342 process.on('SIGTERM', exit);
343 } else {
344 try {
345 await parcel.run();
346 } catch (err) {
347 // If an exception is thrown during Parcel.build, it is given to reporters in a
348 // buildFailure event, and has been shown to the user.
349 if (!(err instanceof BuildError)) {
350 await logUncaughtError(err);
351 }
352 await exit(1);
353 }
354
355 await exit();
356 }
357}
358
359function parsePort(portValue: string): number {
360 let parsedPort = Number(portValue);
361
362 // Throw an error if port value is invalid...
363 if (!Number.isInteger(parsedPort)) {
364 throw new Error(`Port ${portValue} is not a valid integer.`);
365 }
366
367 return parsedPort;
368}
369
370function parseOptionInt(value) {
371 const parsedValue = parseInt(value, 10);
372 if (isNaN(parsedValue)) {
373 throw new commander.InvalidOptionArgumentError('Must be an integer.');
374 }
375 return parsedValue;
376}
377
378async function normalizeOptions(
379 command,
380 inputFS,
381): Promise<InitialParcelOptions> {
382 let nodeEnv;
383 if (command.name() === 'build') {
384 nodeEnv = process.env.NODE_ENV || 'production';
385 // Autoinstall unless explicitly disabled or we detect a CI environment.
386 command.autoinstall = !(command.autoinstall === false || process.env.CI);
387 } else {
388 nodeEnv = process.env.NODE_ENV || 'development';
389 }
390
391 // Set process.env.NODE_ENV to a default if undefined so that it is
392 // available in JS configs and plugins.
393 process.env.NODE_ENV = nodeEnv;
394
395 let https = !!command.https;
396 if (command.cert && command.key) {
397 https = {
398 cert: command.cert,
399 key: command.key,
400 };
401 }
402
403 let serveOptions = false;
404 let {host} = command;
405
406 // Ensure port is valid and available
407 let port = parsePort(command.port || '1234');
408 let originalPort = port;
409 if (command.name() === 'serve' || command.hmr) {
410 try {
411 port = await getPort({port, host});
412 } catch (err) {
413 throw new ThrowableDiagnostic({
414 diagnostic: {
415 message: `Could not get available port: ${err.message}`,
416 origin: 'parcel',
417 stack: err.stack,
418 },
419 });
420 }
421
422 if (port !== originalPort) {
423 let errorMessage = `Port "${originalPort}" could not be used`;
424 if (command.port != null) {
425 // Throw the error if the user defined a custom port
426 throw new Error(errorMessage);
427 } else {
428 // Parcel logger is not set up at this point, so just use native INTERNAL_ORIGINAL_CONSOLE
429 INTERNAL_ORIGINAL_CONSOLE.warn(errorMessage);
430 }
431 }
432 }
433
434 if (command.name() === 'serve') {
435 let {publicUrl} = command;
436
437 serveOptions = {
438 https,
439 port,
440 host,
441 publicUrl,
442 };
443 }
444
445 let hmrOptions = null;
446 if (command.name() !== 'build' && command.hmr !== false) {
447 let hmrport = command.hmrPort ? parsePort(command.hmrPort) : port;
448
449 hmrOptions = {port: hmrport, host};
450 }
451
452 if (command.detailedReport === true) {
453 command.detailedReport = '10';
454 }
455
456 let additionalReporters = [
457 {packageName: '@parcel/reporter-cli', resolveFrom: __filename},
458 ...(command.reporter: Array<string>).map(packageName => ({
459 packageName,
460 resolveFrom: path.join(inputFS.cwd(), 'index'),
461 })),
462 ];
463
464 let mode = command.name() === 'build' ? 'production' : 'development';
465 return {
466 shouldDisableCache: command.cache === false,
467 cacheDir: command.cacheDir,
468 config: command.config,
469 mode,
470 hmrOptions,
471 shouldContentHash: hmrOptions ? false : command.contentHash,
472 serveOptions,
473 targets: command.target.length > 0 ? command.target : null,
474 shouldAutoInstall: command.autoinstall ?? true,
475 logLevel: command.logLevel,
476 shouldProfile: command.profile,
477 shouldBuildLazily: command.lazy,
478 detailedReport:
479 command.detailedReport != null
480 ? {
481 assetsPerBundle: parseInt(command.detailedReport, 10),
482 }
483 : null,
484 env: {
485 NODE_ENV: nodeEnv,
486 },
487 additionalReporters,
488 defaultTargetOptions: {
489 shouldOptimize:
490 command.optimize != null ? command.optimize : mode === 'production',
491 sourceMaps: command.sourceMaps ?? true,
492 shouldScopeHoist: command.scopeHoist,
493 publicUrl: command.publicUrl,
494 distDir: command.distDir,
495 },
496 };
497}