UNPKG

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