UNPKG

22.9 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3exports.YarnServeCLI = exports.PnpmServeCLI = exports.NpmServeCLI = exports.ServeCLI = exports.ServeRunner = exports.COMMON_SERVE_COMMAND_OPTIONS = exports.SERVE_SCRIPT = exports.BROWSERS = exports.LOCAL_ADDRESSES = exports.BIND_ALL_ADDRESS = exports.DEFAULT_ADDRESS = exports.DEFAULT_DEVAPP_COMM_PORT = exports.DEFAULT_LAB_PORT = exports.DEFAULT_SERVER_PORT = exports.DEFAULT_LIVERELOAD_PORT = exports.DEFAULT_DEV_LOGGER_PORT = void 0;
4const cli_framework_1 = require("@ionic/cli-framework");
5const cli_framework_output_1 = require("@ionic/cli-framework-output");
6const string_1 = require("@ionic/cli-framework/utils/string");
7const utils_network_1 = require("@ionic/utils-network");
8const utils_process_1 = require("@ionic/utils-process");
9const chalk = require("chalk");
10const Debug = require("debug");
11const events_1 = require("events");
12const lodash = require("lodash");
13const split2 = require("split2");
14const stream = require("stream");
15const color_1 = require("./color");
16const errors_1 = require("./errors");
17const events_2 = require("./events");
18const hooks_1 = require("./hooks");
19const open_1 = require("./open");
20const logger_1 = require("./utils/logger");
21const debug = Debug('ionic:lib:serve');
22exports.DEFAULT_DEV_LOGGER_PORT = 53703;
23exports.DEFAULT_LIVERELOAD_PORT = 35729;
24exports.DEFAULT_SERVER_PORT = 8100;
25exports.DEFAULT_LAB_PORT = 8200;
26exports.DEFAULT_DEVAPP_COMM_PORT = 53233;
27exports.DEFAULT_ADDRESS = 'localhost';
28exports.BIND_ALL_ADDRESS = '0.0.0.0';
29exports.LOCAL_ADDRESSES = ['localhost', '127.0.0.1'];
30exports.BROWSERS = ['safari', 'firefox', process.platform === 'win32' ? 'chrome' : (process.platform === 'darwin' ? 'google chrome' : 'google-chrome')];
31// npm script name
32exports.SERVE_SCRIPT = 'ionic:serve';
33exports.COMMON_SERVE_COMMAND_OPTIONS = [
34 {
35 name: 'external',
36 summary: `Host dev server on all network interfaces (i.e. ${color_1.input('--host=0.0.0.0')})`,
37 type: Boolean,
38 },
39 {
40 name: 'address',
41 summary: '',
42 groups: ["hidden" /* HIDDEN */],
43 },
44 {
45 name: 'host',
46 summary: 'Use specific host for the dev server',
47 default: exports.DEFAULT_ADDRESS,
48 groups: ["advanced" /* ADVANCED */],
49 },
50 {
51 name: 'port',
52 summary: 'Use specific port for the dev server',
53 default: exports.DEFAULT_SERVER_PORT.toString(),
54 aliases: ['p'],
55 groups: ["advanced" /* ADVANCED */],
56 },
57 {
58 name: 'public-host',
59 summary: 'The host used for the browser or web view',
60 groups: ["advanced" /* ADVANCED */],
61 spec: { value: 'host' },
62 },
63 {
64 name: 'livereload',
65 summary: 'Do not spin up dev server--just serve files',
66 type: Boolean,
67 default: true,
68 },
69 {
70 name: 'engine',
71 summary: `Target engine (e.g. ${['browser', 'cordova'].map(e => color_1.input(e)).join(', ')})`,
72 groups: ["hidden" /* HIDDEN */, "advanced" /* ADVANCED */],
73 },
74 {
75 name: 'platform',
76 summary: `Target platform on chosen engine (e.g. ${['ios', 'android'].map(e => color_1.input(e)).join(', ')})`,
77 groups: ["hidden" /* HIDDEN */, "advanced" /* ADVANCED */],
78 },
79];
80class ServeRunner {
81 constructor() {
82 this.devAppConnectionMade = false;
83 }
84 getPkgManagerServeCLI() {
85 const pkgManagerCLIs = {
86 npm: NpmServeCLI,
87 pnpm: PnpmServeCLI,
88 yarn: YarnServeCLI,
89 };
90 const client = this.e.config.get('npmClient');
91 const CLI = pkgManagerCLIs[client];
92 if (CLI) {
93 return new CLI(this.e);
94 }
95 throw new errors_1.ServeCLIProgramNotFoundException('Unknown CLI client: ' + client);
96 }
97 createOptionsFromCommandLine(inputs, options) {
98 const separatedArgs = options['--'];
99 if (options['external'] && options['host'] === exports.DEFAULT_ADDRESS) {
100 options['host'] = '0.0.0.0';
101 }
102 if (options['address'] && options['host'] === exports.DEFAULT_ADDRESS) {
103 this.e.log.warn(`The ${color_1.input('--address')} option is deprecated in favor of ${color_1.input('--host')}.\n` +
104 `Please use the ${color_1.input('--host')} option (e.g. ${color_1.input(`--host=${options['address']}`)}) to specify the host of the dev server.\n`);
105 options['host'] = options['address'];
106 }
107 const engine = this.determineEngineFromCommandLine(options);
108 const host = options['host'] ? String(options['host']) : exports.DEFAULT_ADDRESS;
109 const labPort = string_1.str2num(options['lab-port'], exports.DEFAULT_LAB_PORT);
110 const port = string_1.str2num(options['port'], exports.DEFAULT_SERVER_PORT);
111 const [platform] = options['platform'] ? [String(options['platform'])] : inputs;
112 return {
113 '--': separatedArgs ? separatedArgs : [],
114 host,
115 browser: options['browser'] ? String(options['browser']) : undefined,
116 browserOption: options['browseroption'] ? String(options['browseroption']) : undefined,
117 engine,
118 externalAddressRequired: !!options['externalAddressRequired'],
119 lab: !!options['lab'],
120 labHost: options['lab-host'] ? String(options['lab-host']) : 'localhost',
121 labPort,
122 livereload: typeof options['livereload'] === 'boolean' ? Boolean(options['livereload']) : true,
123 open: !!options['open'],
124 platform,
125 port,
126 proxy: typeof options['proxy'] === 'boolean' ? Boolean(options['proxy']) : true,
127 project: options['project'] ? String(options['project']) : undefined,
128 publicHost: options['public-host'] ? String(options['public-host']) : undefined,
129 verbose: !!options['verbose'],
130 };
131 }
132 determineEngineFromCommandLine(options) {
133 if (options['engine']) {
134 return String(options['engine']);
135 }
136 if (options['cordova']) {
137 return 'cordova';
138 }
139 return 'browser';
140 }
141 async beforeServe(options) {
142 const hook = new ServeBeforeHook(this.e);
143 try {
144 await hook.run({ name: hook.name, serve: options });
145 }
146 catch (e) {
147 if (e instanceof cli_framework_1.BaseError) {
148 throw new errors_1.FatalException(e.message);
149 }
150 throw e;
151 }
152 }
153 async run(options) {
154 debug('serve options: %O', options);
155 await this.beforeServe(options);
156 const details = await this.serveProject(options);
157 const labDetails = options.lab ? await this.runLab(options, details) : undefined;
158 const localAddress = `${details.protocol}://${options.publicHost ? options.publicHost : 'localhost'}:${details.port}`;
159 const fmtExternalAddress = (host) => `${details.protocol}://${host}:${details.port}`;
160 const labHost = labDetails ? `http://${labDetails.host}:${labDetails.port}` : undefined;
161 this.e.log.nl();
162 this.e.log.info(`Development server running!` +
163 (labHost ? `\nLab: ${color_1.strong(labHost)}` : '') +
164 `\nLocal: ${color_1.strong(localAddress)}` +
165 (details.externalNetworkInterfaces.length > 0 ? `\nExternal: ${details.externalNetworkInterfaces.map(v => color_1.strong(fmtExternalAddress(v.address))).join(', ')}` : '') +
166 `\n\n${chalk.yellow('Use Ctrl+C to quit this process')}`);
167 this.e.log.nl();
168 if (options.open) {
169 const openAddress = labHost ? labHost : localAddress;
170 const url = this.modifyOpenUrl(openAddress, options);
171 await open_1.openUrl(url, { app: options.browser });
172 this.e.log.info(`Browser window opened to ${color_1.strong(url)}!`);
173 this.e.log.nl();
174 }
175 events_2.emit('serve:ready', details);
176 debug('serve details: %O', details);
177 this.scheduleAfterServe(options, details);
178 return details;
179 }
180 async afterServe(options, details) {
181 const hook = new ServeAfterHook(this.e);
182 try {
183 await hook.run({ name: hook.name, serve: lodash.assign({}, options, details) });
184 }
185 catch (e) {
186 if (e instanceof cli_framework_1.BaseError) {
187 throw new errors_1.FatalException(e.message);
188 }
189 throw e;
190 }
191 }
192 scheduleAfterServe(options, details) {
193 utils_process_1.onBeforeExit(async () => this.afterServe(options, details));
194 }
195 getUsedPorts(options, details) {
196 return [details.port];
197 }
198 async runLab(options, serveDetails) {
199 const labDetails = {
200 projectType: this.e.project.type,
201 host: options.labHost,
202 port: await utils_network_1.findClosestOpenPort(options.labPort),
203 };
204 const lab = new IonicLabServeCLI(this.e);
205 await lab.serve({ serveDetails, ...labDetails });
206 return labDetails;
207 }
208 async selectExternalIP(options) {
209 let availableInterfaces = [];
210 let chosenIP = options.host;
211 if (options.host === exports.BIND_ALL_ADDRESS) {
212 // ignore link-local addresses
213 availableInterfaces = utils_network_1.getExternalIPv4Interfaces().filter(i => !i.address.startsWith('169.254'));
214 if (options.publicHost) {
215 chosenIP = options.publicHost;
216 }
217 else {
218 if (availableInterfaces.length === 0) {
219 if (options.externalAddressRequired) {
220 throw new errors_1.FatalException(`No external network interfaces detected. In order to use the dev server externally you will need one.\n` +
221 `Are you connected to a local network?\n`);
222 }
223 }
224 else if (availableInterfaces.length === 1) {
225 chosenIP = availableInterfaces[0].address;
226 }
227 else if (availableInterfaces.length > 1) {
228 if (options.externalAddressRequired) {
229 if (this.e.flags.interactive) {
230 this.e.log.warn('Multiple network interfaces detected!\n' +
231 `You will be prompted to select an external-facing IP for the dev server that your device or emulator can access. Make sure your device is on the same Wi-Fi network as your computer. Learn more about Live Reload in the docs${color_1.ancillary('[1]')}.\n\n` +
232 `To bypass this prompt, use the ${color_1.input('--public-host')} option (e.g. ${color_1.input(`--public-host=${availableInterfaces[0].address}`)}). You can alternatively bind the dev server to a specific IP (e.g. ${color_1.input(`--host=${availableInterfaces[0].address}`)}).\n\n` +
233 `${color_1.ancillary('[1]')}: ${color_1.strong('https://ion.link/livereload-docs')}\n`);
234 const promptedIp = await this.e.prompt({
235 type: 'list',
236 name: 'promptedIp',
237 message: 'Please select which IP to use:',
238 choices: availableInterfaces.map(i => ({
239 name: `${i.address} ${color_1.weak(`(${i.device})`)}`,
240 value: i.address,
241 })),
242 });
243 chosenIP = promptedIp;
244 }
245 else {
246 throw new errors_1.FatalException(`Multiple network interfaces detected!\n` +
247 `You must select an external-facing IP for the dev server that your device or emulator can access with the ${color_1.input('--public-host')} option.`);
248 }
249 }
250 }
251 }
252 }
253 else if (options.externalAddressRequired && exports.LOCAL_ADDRESSES.includes(options.host)) {
254 this.e.log.warn('An external host may be required to serve for this target device/platform.\n' +
255 'If you get connection issues on your device or emulator, try connecting the device to the same Wi-Fi network and selecting an accessible IP address for your computer on that network.\n\n' +
256 `You can use ${color_1.input('--external')} to run the dev server on all network interfaces, in which case an external address will be selected.\n`);
257 }
258 return [chosenIP, availableInterfaces];
259 }
260}
261exports.ServeRunner = ServeRunner;
262class ServeBeforeHook extends hooks_1.Hook {
263 constructor() {
264 super(...arguments);
265 this.name = 'serve:before';
266 }
267}
268class ServeAfterHook extends hooks_1.Hook {
269 constructor() {
270 super(...arguments);
271 this.name = 'serve:after';
272 }
273}
274class ServeCLI extends events_1.EventEmitter {
275 constructor(e) {
276 super();
277 this.e = e;
278 /**
279 * If true, the Serve CLI will not prompt to be installed.
280 */
281 this.global = false;
282 }
283 get resolvedProgram() {
284 if (this._resolvedProgram) {
285 return this._resolvedProgram;
286 }
287 return this.program;
288 }
289 /**
290 * Build the environment variables to be passed to the Serve CLI. Called by `this.start()`;
291 */
292 async buildEnvVars(options) {
293 return process.env;
294 }
295 /**
296 * Called whenever a line of stdout is received.
297 *
298 * If `false` is returned, the line is not emitted to the log.
299 *
300 * By default, the CLI is considered ready whenever stdout is emitted. This
301 * method should be overridden to more accurately portray readiness.
302 *
303 * @param line A line of stdout.
304 */
305 stdoutFilter(line) {
306 this.emit('ready');
307 return true;
308 }
309 /**
310 * Called whenever a line of stderr is received.
311 *
312 * If `false` is returned, the line is not emitted to the log.
313 */
314 stderrFilter(line) {
315 return true;
316 }
317 async resolveScript() {
318 if (typeof this.script === 'undefined') {
319 return;
320 }
321 const [pkg] = await this.e.project.getPackageJson(undefined, { logErrors: false });
322 if (!pkg) {
323 return;
324 }
325 return pkg.scripts && pkg.scripts[this.script];
326 }
327 async serve(options) {
328 this._resolvedProgram = await this.resolveProgram();
329 await this.spawnWrapper(options);
330 const interval = setInterval(() => {
331 this.e.log.info(`Waiting for connectivity with ${color_1.input(this.resolvedProgram)}...`);
332 }, 5000);
333 debug('awaiting TCP connection to %s:%d', options.host, options.port);
334 await utils_network_1.isHostConnectable(options.host, options.port);
335 clearInterval(interval);
336 }
337 async spawnWrapper(options) {
338 try {
339 return await this.spawn(options);
340 }
341 catch (e) {
342 if (!(e instanceof errors_1.ServeCLIProgramNotFoundException)) {
343 throw e;
344 }
345 if (this.global) {
346 this.e.log.nl();
347 throw new errors_1.FatalException(`${color_1.input(this.pkg)} is required for this command to work properly.`);
348 }
349 this.e.log.nl();
350 this.e.log.info(`Looks like ${color_1.input(this.pkg)} isn't installed in this project.\n` +
351 `This package is required for this command to work properly. The package provides a CLI utility, but the ${color_1.input(this.resolvedProgram)} binary was not found in your PATH.`);
352 const installed = await this.promptToInstall();
353 if (!installed) {
354 this.e.log.nl();
355 throw new errors_1.FatalException(`${color_1.input(this.pkg)} is required for this command to work properly.`);
356 }
357 return this.spawn(options);
358 }
359 }
360 async spawn(options) {
361 const args = await this.buildArgs(options);
362 const env = await this.buildEnvVars(options);
363 const p = await this.e.shell.spawn(this.resolvedProgram, args, { stdio: 'pipe', cwd: this.e.project.directory, env: utils_process_1.createProcessEnv(env) });
364 return new Promise((resolve, reject) => {
365 const errorHandler = (err) => {
366 debug('received error for %s: %o', this.resolvedProgram, err);
367 if (this.resolvedProgram === this.program && err.code === 'ENOENT') {
368 p.removeListener('close', closeHandler); // do not exit Ionic CLI, we can gracefully ask to install this CLI
369 reject(new errors_1.ServeCLIProgramNotFoundException(`${color_1.strong(this.resolvedProgram)} command not found.`));
370 }
371 else {
372 reject(err);
373 }
374 };
375 const closeHandler = (code) => {
376 if (code !== null) {
377 debug('received unexpected close for %s (code: %d)', this.resolvedProgram, code);
378 this.e.log.nl();
379 this.e.log.error(`${color_1.input(this.resolvedProgram)} has unexpectedly closed (exit code ${code}).\n` +
380 'The Ionic CLI will exit. Please check any output above for error details.');
381 utils_process_1.processExit(1);
382 }
383 };
384 p.on('error', errorHandler);
385 p.on('close', closeHandler);
386 utils_process_1.onBeforeExit(async () => {
387 p.removeListener('close', closeHandler);
388 if (p.pid) {
389 await utils_process_1.killProcessTree(p.pid);
390 }
391 });
392 const ws = this.createLoggerStream();
393 p.stdout.pipe(split2()).pipe(this.createStreamFilter(line => this.stdoutFilter(line))).pipe(ws);
394 p.stderr.pipe(split2()).pipe(this.createStreamFilter(line => this.stderrFilter(line))).pipe(ws);
395 this.once('ready', () => {
396 resolve();
397 });
398 });
399 }
400 createLoggerStream() {
401 const log = this.e.log.clone();
402 log.handlers = logger_1.createDefaultLoggerHandlers(cli_framework_output_1.createPrefixedFormatter(color_1.weak(`[${this.resolvedProgram === this.program ? this.prefix : this.resolvedProgram}]`)));
403 return log.createWriteStream(cli_framework_output_1.LOGGER_LEVELS.INFO);
404 }
405 async resolveProgram() {
406 if (typeof this.script !== 'undefined') {
407 debug(`Looking for ${color_1.ancillary(this.script)} npm script.`);
408 if (await this.resolveScript()) {
409 debug(`Using ${color_1.ancillary(this.script)} npm script.`);
410 return this.e.config.get('npmClient');
411 }
412 }
413 return this.program;
414 }
415 createStreamFilter(filter) {
416 return new stream.Transform({
417 transform(chunk, enc, callback) {
418 const str = chunk.toString();
419 if (filter(str)) {
420 this.push(chunk);
421 }
422 callback();
423 },
424 });
425 }
426 async promptToInstall() {
427 const { pkgManagerArgs } = await Promise.resolve().then(() => require('./utils/npm'));
428 const [manager, ...managerArgs] = await pkgManagerArgs(this.e.config.get('npmClient'), { command: 'install', pkg: this.pkg, saveDev: true, saveExact: true });
429 this.e.log.nl();
430 const confirm = await this.e.prompt({
431 name: 'confirm',
432 message: `Install ${color_1.input(this.pkg)}?`,
433 type: 'confirm',
434 });
435 if (!confirm) {
436 this.e.log.warn(`Not installing--here's how to install manually: ${color_1.input(`${manager} ${managerArgs.join(' ')}`)}`);
437 return false;
438 }
439 await this.e.shell.run(manager, managerArgs, { cwd: this.e.project.directory });
440 return true;
441 }
442}
443exports.ServeCLI = ServeCLI;
444class PkgManagerServeCLI extends ServeCLI {
445 constructor() {
446 super(...arguments);
447 this.global = true;
448 this.script = exports.SERVE_SCRIPT;
449 }
450 async resolveProgram() {
451 return this.program;
452 }
453 async buildArgs(options) {
454 const { pkgManagerArgs } = await Promise.resolve().then(() => require('./utils/npm'));
455 // The Ionic CLI decides the host/port of the dev server, so --host and
456 // --port are provided to the downstream npm script as a best-effort
457 // attempt.
458 const args = {
459 _: [],
460 host: options.host,
461 port: options.port.toString(),
462 };
463 const scriptArgs = [...cli_framework_1.unparseArgs(args), ...options['--'] || []];
464 const [, ...pkgArgs] = await pkgManagerArgs(this.program, { command: 'run', script: this.script, scriptArgs });
465 return pkgArgs;
466 }
467}
468class NpmServeCLI extends PkgManagerServeCLI {
469 constructor() {
470 super(...arguments);
471 this.name = 'npm CLI';
472 this.pkg = 'npm';
473 this.program = 'npm';
474 this.prefix = 'npm';
475 }
476}
477exports.NpmServeCLI = NpmServeCLI;
478class PnpmServeCLI extends PkgManagerServeCLI {
479 constructor() {
480 super(...arguments);
481 this.name = 'pnpm CLI';
482 this.pkg = 'pnpm';
483 this.program = 'pnpm';
484 this.prefix = 'pnpm';
485 }
486}
487exports.PnpmServeCLI = PnpmServeCLI;
488class YarnServeCLI extends PkgManagerServeCLI {
489 constructor() {
490 super(...arguments);
491 this.name = 'Yarn';
492 this.pkg = 'yarn';
493 this.program = 'yarn';
494 this.prefix = 'yarn';
495 }
496}
497exports.YarnServeCLI = YarnServeCLI;
498class IonicLabServeCLI extends ServeCLI {
499 constructor() {
500 super(...arguments);
501 this.name = 'Ionic Lab';
502 this.pkg = '@ionic/lab';
503 this.program = 'ionic-lab';
504 this.prefix = 'lab';
505 this.script = undefined;
506 }
507 stdoutFilter(line) {
508 if (line.includes('running')) {
509 this.emit('ready');
510 }
511 return false; // no stdout
512 }
513 async buildArgs(options) {
514 const { serveDetails, ...labDetails } = options;
515 const pkg = await this.e.project.requirePackageJson();
516 const url = `${serveDetails.protocol}://localhost:${serveDetails.port}`;
517 const appName = this.e.project.config.get('name');
518 const labArgs = [url, '--host', labDetails.host, '--port', String(labDetails.port), '--project-type', labDetails.projectType];
519 const nameArgs = appName ? ['--app-name', appName] : [];
520 const versionArgs = pkg.version ? ['--app-version', pkg.version] : [];
521 return [...labArgs, ...nameArgs, ...versionArgs];
522 }
523}