| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480 |
- "use strict";
- Object.defineProperty(exports, "__esModule", { value: true });
- exports.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_SERVER_PORT = exports.DEFAULT_LIVERELOAD_PORT = exports.DEFAULT_DEV_LOGGER_PORT = void 0;
- const tslib_1 = require("tslib");
- const cli_framework_1 = require("@ionic/cli-framework");
- const cli_framework_output_1 = require("@ionic/cli-framework-output");
- const string_1 = require("@ionic/cli-framework/utils/string");
- const utils_network_1 = require("@ionic/utils-network");
- const utils_process_1 = require("@ionic/utils-process");
- const chalk_1 = tslib_1.__importDefault(require("chalk"));
- const debug_1 = require("debug");
- const events_1 = require("events");
- const lodash = tslib_1.__importStar(require("lodash"));
- const split2_1 = tslib_1.__importDefault(require("split2"));
- const stream = tslib_1.__importStar(require("stream"));
- const color_1 = require("./color");
- const errors_1 = require("./errors");
- const events_2 = require("./events");
- const hooks_1 = require("./hooks");
- const open_1 = require("./open");
- const logger_1 = require("./utils/logger");
- const debug = (0, debug_1.debug)('ionic:lib:serve');
- exports.DEFAULT_DEV_LOGGER_PORT = 53703;
- exports.DEFAULT_LIVERELOAD_PORT = 35729;
- exports.DEFAULT_SERVER_PORT = 8100;
- exports.DEFAULT_DEVAPP_COMM_PORT = 53233;
- exports.DEFAULT_ADDRESS = 'localhost';
- exports.BIND_ALL_ADDRESS = '0.0.0.0';
- exports.LOCAL_ADDRESSES = ['localhost', '127.0.0.1'];
- exports.BROWSERS = ['safari', 'firefox', process.platform === 'win32' ? 'chrome' : (process.platform === 'darwin' ? 'google chrome' : 'google-chrome')];
- // npm script name
- exports.SERVE_SCRIPT = 'ionic:serve';
- exports.COMMON_SERVE_COMMAND_OPTIONS = [
- {
- name: 'external',
- summary: `Host dev server on all network interfaces (i.e. ${(0, color_1.input)('--host=0.0.0.0')})`,
- type: Boolean,
- },
- {
- name: 'address',
- summary: '',
- groups: ["hidden" /* MetadataGroup.HIDDEN */],
- },
- {
- name: 'host',
- summary: 'Use specific host for the dev server',
- default: exports.DEFAULT_ADDRESS,
- groups: ["advanced" /* MetadataGroup.ADVANCED */],
- },
- {
- name: 'port',
- summary: 'Use specific port for the dev server',
- default: exports.DEFAULT_SERVER_PORT.toString(),
- aliases: ['p'],
- groups: ["advanced" /* MetadataGroup.ADVANCED */],
- },
- {
- name: 'public-host',
- summary: 'The host used for the browser or web view',
- groups: ["advanced" /* MetadataGroup.ADVANCED */],
- spec: { value: 'host' },
- },
- {
- name: 'livereload',
- summary: 'Do not spin up dev server--just serve files',
- type: Boolean,
- default: true,
- },
- {
- name: 'engine',
- summary: `Target engine (e.g. ${['browser', 'cordova'].map(e => (0, color_1.input)(e)).join(', ')})`,
- groups: ["hidden" /* MetadataGroup.HIDDEN */, "advanced" /* MetadataGroup.ADVANCED */],
- },
- {
- name: 'platform',
- summary: `Target platform on chosen engine (e.g. ${['ios', 'android'].map(e => (0, color_1.input)(e)).join(', ')})`,
- groups: ["hidden" /* MetadataGroup.HIDDEN */, "advanced" /* MetadataGroup.ADVANCED */],
- },
- ];
- class ServeRunner {
- constructor() {
- this.devAppConnectionMade = false;
- }
- getPkgManagerServeCLI() {
- const pkgManagerCLIs = {
- npm: NpmServeCLI,
- pnpm: PnpmServeCLI,
- yarn: YarnServeCLI,
- };
- const client = this.e.config.get('npmClient');
- const CLI = pkgManagerCLIs[client];
- if (CLI) {
- return new CLI(this.e);
- }
- throw new errors_1.ServeCLIProgramNotFoundException('Unknown CLI client: ' + client);
- }
- createOptionsFromCommandLine(inputs, options) {
- const separatedArgs = options['--'];
- if (options['external'] && options['host'] === exports.DEFAULT_ADDRESS) {
- options['host'] = '0.0.0.0';
- }
- if (options['address'] && options['host'] === exports.DEFAULT_ADDRESS) {
- this.e.log.warn(`The ${(0, color_1.input)('--address')} option is deprecated in favor of ${(0, color_1.input)('--host')}.\n` +
- `Please use the ${(0, color_1.input)('--host')} option (e.g. ${(0, color_1.input)(`--host=${options['address']}`)}) to specify the host of the dev server.\n`);
- options['host'] = options['address'];
- }
- const engine = this.determineEngineFromCommandLine(options);
- const host = options['host'] ? String(options['host']) : exports.DEFAULT_ADDRESS;
- const port = (0, string_1.str2num)(options['port'], exports.DEFAULT_SERVER_PORT);
- const [platform] = options['platform'] ? [String(options['platform'])] : inputs;
- return {
- '--': separatedArgs ? separatedArgs : [],
- host,
- browser: options['browser'] ? String(options['browser']) : undefined,
- browserOption: options['browseroption'] ? String(options['browseroption']) : undefined,
- engine,
- externalAddressRequired: !!options['externalAddressRequired'],
- livereload: typeof options['livereload'] === 'boolean' ? Boolean(options['livereload']) : true,
- open: !!options['open'],
- platform,
- port,
- proxy: typeof options['proxy'] === 'boolean' ? Boolean(options['proxy']) : true,
- project: options['project'] ? String(options['project']) : undefined,
- publicHost: options['public-host'] ? String(options['public-host']) : undefined,
- verbose: !!options['verbose'],
- };
- }
- determineEngineFromCommandLine(options) {
- if (options['engine']) {
- return String(options['engine']);
- }
- if (options['cordova']) {
- return 'cordova';
- }
- return 'browser';
- }
- async beforeServe(options) {
- const hook = new ServeBeforeHook(this.e);
- try {
- await hook.run({ name: hook.name, serve: options });
- }
- catch (e) {
- if (e instanceof cli_framework_1.BaseError) {
- throw new errors_1.FatalException(e.message);
- }
- throw e;
- }
- }
- async run(options) {
- debug('serve options: %O', options);
- await this.beforeServe(options);
- const details = await this.serveProject(options);
- const localAddress = `${details.protocol}://${options.publicHost ? options.publicHost : 'localhost'}:${details.port}`;
- const fmtExternalAddress = (host) => `${details.protocol}://${host}:${details.port}`;
- this.e.log.nl();
- this.e.log.info(`Development server running!` +
- `\nLocal: ${(0, color_1.strong)(localAddress)}` +
- (details.externalNetworkInterfaces.length > 0 ? `\nExternal: ${details.externalNetworkInterfaces.map(v => (0, color_1.strong)(fmtExternalAddress(v.address))).join(', ')}` : '') +
- `\n\n${chalk_1.default.yellow('Use Ctrl+C to quit this process')}`);
- this.e.log.nl();
- if (options.open) {
- const openAddress = localAddress;
- const url = this.modifyOpenUrl(openAddress, options);
- await (0, open_1.openUrl)(url, { app: options.browser });
- this.e.log.info(`Browser window opened to ${(0, color_1.strong)(url)}!`);
- this.e.log.nl();
- }
- (0, events_2.emit)('serve:ready', details);
- debug('serve details: %O', details);
- this.scheduleAfterServe(options, details);
- return details;
- }
- async afterServe(options, details) {
- const hook = new ServeAfterHook(this.e);
- try {
- await hook.run({ name: hook.name, serve: lodash.assign({}, options, details) });
- }
- catch (e) {
- if (e instanceof cli_framework_1.BaseError) {
- throw new errors_1.FatalException(e.message);
- }
- throw e;
- }
- }
- scheduleAfterServe(options, details) {
- (0, utils_process_1.onBeforeExit)(async () => this.afterServe(options, details));
- }
- getUsedPorts(options, details) {
- return [details.port];
- }
- async selectExternalIP(options) {
- let availableInterfaces = [];
- let chosenIP = options.host;
- if (options.host === exports.BIND_ALL_ADDRESS) {
- // ignore link-local addresses
- availableInterfaces = (0, utils_network_1.getExternalIPv4Interfaces)().filter(i => !i.address.startsWith('169.254'));
- if (options.publicHost) {
- chosenIP = options.publicHost;
- }
- else {
- if (availableInterfaces.length === 0) {
- if (options.externalAddressRequired) {
- throw new errors_1.FatalException(`No external network interfaces detected. In order to use the dev server externally you will need one.\n` +
- `Are you connected to a local network?\n`);
- }
- }
- else if (availableInterfaces.length === 1) {
- chosenIP = availableInterfaces[0].address;
- }
- else if (availableInterfaces.length > 1) {
- if (options.externalAddressRequired) {
- if (this.e.flags.interactive) {
- this.e.log.warn('Multiple network interfaces detected!\n' +
- `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${(0, color_1.ancillary)('[1]')}.\n\n` +
- `To bypass this prompt, use the ${(0, color_1.input)('--public-host')} option (e.g. ${(0, color_1.input)(`--public-host=${availableInterfaces[0].address}`)}). You can alternatively bind the dev server to a specific IP (e.g. ${(0, color_1.input)(`--host=${availableInterfaces[0].address}`)}).\n\n` +
- `${(0, color_1.ancillary)('[1]')}: ${(0, color_1.strong)('https://ion.link/livereload-docs')}\n`);
- const promptedIp = await this.e.prompt({
- type: 'list',
- name: 'promptedIp',
- message: 'Please select which IP to use:',
- choices: availableInterfaces.map(i => ({
- name: `${i.address} ${(0, color_1.weak)(`(${i.device})`)}`,
- value: i.address,
- })),
- });
- chosenIP = promptedIp;
- }
- else {
- throw new errors_1.FatalException(`Multiple network interfaces detected!\n` +
- `You must select an external-facing IP for the dev server that your device or emulator can access with the ${(0, color_1.input)('--public-host')} option.`);
- }
- }
- }
- }
- }
- else if (options.externalAddressRequired && exports.LOCAL_ADDRESSES.includes(options.host)) {
- this.e.log.warn('An external host may be required to serve for this target device/platform.\n' +
- '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' +
- `You can use ${(0, color_1.input)('--external')} to run the dev server on all network interfaces, in which case an external address will be selected.\n`);
- }
- return [chosenIP, availableInterfaces];
- }
- }
- exports.ServeRunner = ServeRunner;
- class ServeBeforeHook extends hooks_1.Hook {
- constructor() {
- super(...arguments);
- this.name = 'serve:before';
- }
- }
- class ServeAfterHook extends hooks_1.Hook {
- constructor() {
- super(...arguments);
- this.name = 'serve:after';
- }
- }
- class ServeCLI extends events_1.EventEmitter {
- constructor(e) {
- super();
- this.e = e;
- /**
- * If true, the Serve CLI will not prompt to be installed.
- */
- this.global = false;
- }
- get resolvedProgram() {
- if (this._resolvedProgram) {
- return this._resolvedProgram;
- }
- return this.program;
- }
- /**
- * Build the environment variables to be passed to the Serve CLI. Called by `this.start()`;
- */
- async buildEnvVars(options) {
- return process.env;
- }
- /**
- * Called whenever a line of stdout is received.
- *
- * If `false` is returned, the line is not emitted to the log.
- *
- * By default, the CLI is considered ready whenever stdout is emitted. This
- * method should be overridden to more accurately portray readiness.
- *
- * @param line A line of stdout.
- */
- stdoutFilter(line) {
- this.emit('ready');
- return true;
- }
- /**
- * Called whenever a line of stderr is received.
- *
- * If `false` is returned, the line is not emitted to the log.
- */
- stderrFilter(line) {
- return true;
- }
- async resolveScript() {
- if (typeof this.script === 'undefined') {
- return;
- }
- const [pkg] = await this.e.project.getPackageJson(undefined, { logErrors: false });
- if (!pkg) {
- return;
- }
- return pkg.scripts && pkg.scripts[this.script];
- }
- async serve(options) {
- this._resolvedProgram = await this.resolveProgram();
- await this.spawnWrapper(options);
- const interval = setInterval(() => {
- this.e.log.info(`Waiting for connectivity with ${(0, color_1.input)(this.resolvedProgram)}...`);
- }, 5000);
- debug('awaiting TCP connection to %s:%d', options.host, options.port);
- await (0, utils_network_1.isHostConnectable)(options.host, options.port);
- clearInterval(interval);
- }
- async spawnWrapper(options) {
- try {
- return await this.spawn(options);
- }
- catch (e) {
- if (!(e instanceof errors_1.ServeCLIProgramNotFoundException)) {
- throw e;
- }
- if (this.global) {
- this.e.log.nl();
- throw new errors_1.FatalException(`${(0, color_1.input)(this.pkg)} is required for this command to work properly.`);
- }
- this.e.log.nl();
- this.e.log.info(`Looks like ${(0, color_1.input)(this.pkg)} isn't installed in this project.\n` +
- `This package is required for this command to work properly. The package provides a CLI utility, but the ${(0, color_1.input)(this.resolvedProgram)} binary was not found in your PATH.`);
- const installed = await this.promptToInstall();
- if (!installed) {
- this.e.log.nl();
- throw new errors_1.FatalException(`${(0, color_1.input)(this.pkg)} is required for this command to work properly.`);
- }
- return this.spawn(options);
- }
- }
- async spawn(options) {
- const args = await this.buildArgs(options);
- const env = await this.buildEnvVars(options);
- const p = await this.e.shell.spawn(this.resolvedProgram, args, { stdio: ['inherit', 'pipe', 'pipe'], cwd: this.e.project.directory, env: (0, utils_process_1.createProcessEnv)(env) });
- return new Promise((resolve, reject) => {
- const errorHandler = (err) => {
- debug('received error for %s: %o', this.resolvedProgram, err);
- if (this.resolvedProgram === this.program && err.code === 'ENOENT') {
- p.removeListener('close', closeHandler); // do not exit Ionic CLI, we can gracefully ask to install this CLI
- reject(new errors_1.ServeCLIProgramNotFoundException(`${(0, color_1.strong)(this.resolvedProgram)} command not found.`));
- }
- else {
- reject(err);
- }
- };
- const closeHandler = (code) => {
- if (code !== null) {
- debug('received unexpected close for %s (code: %d)', this.resolvedProgram, code);
- this.e.log.nl();
- this.e.log.error(`${(0, color_1.input)(this.resolvedProgram)} has unexpectedly closed (exit code ${code}).\n` +
- 'The Ionic CLI will exit. Please check any output above for error details.');
- (0, utils_process_1.processExit)(1);
- }
- };
- p.on('error', errorHandler);
- p.on('close', closeHandler);
- (0, utils_process_1.onBeforeExit)(async () => {
- p.removeListener('close', closeHandler);
- if (p.pid) {
- await (0, utils_process_1.killProcessTree)(p.pid);
- }
- });
- const ws = this.createLoggerStream();
- p.stdout?.pipe((0, split2_1.default)()).pipe(this.createStreamFilter(line => this.stdoutFilter(line))).pipe(ws);
- p.stderr?.pipe((0, split2_1.default)()).pipe(this.createStreamFilter(line => this.stderrFilter(line))).pipe(ws);
- this.once('ready', () => {
- resolve();
- });
- });
- }
- createLoggerStream() {
- const log = this.e.log.clone();
- log.handlers = (0, logger_1.createDefaultLoggerHandlers)((0, cli_framework_output_1.createPrefixedFormatter)((0, color_1.weak)(`[${this.resolvedProgram === this.program ? this.prefix : this.resolvedProgram}]`)));
- return log.createWriteStream(cli_framework_output_1.LOGGER_LEVELS.INFO);
- }
- async resolveProgram() {
- if (typeof this.script !== 'undefined') {
- debug(`Looking for ${(0, color_1.ancillary)(this.script)} npm script.`);
- if (await this.resolveScript()) {
- debug(`Using ${(0, color_1.ancillary)(this.script)} npm script.`);
- return this.e.config.get('npmClient');
- }
- }
- return this.program;
- }
- createStreamFilter(filter) {
- return new stream.Transform({
- transform(chunk, enc, callback) {
- const str = chunk.toString();
- if (filter(str)) {
- this.push(chunk);
- }
- callback();
- },
- });
- }
- async promptToInstall() {
- const { pkgManagerArgs } = await Promise.resolve().then(() => tslib_1.__importStar(require('./utils/npm')));
- const [manager, ...managerArgs] = await pkgManagerArgs(this.e.config.get('npmClient'), { command: 'install', pkg: this.pkg, saveDev: true, saveExact: true });
- this.e.log.nl();
- const confirm = await this.e.prompt({
- name: 'confirm',
- message: `Install ${(0, color_1.input)(this.pkg)}?`,
- type: 'confirm',
- });
- if (!confirm) {
- this.e.log.warn(`Not installing--here's how to install manually: ${(0, color_1.input)(`${manager} ${managerArgs.join(' ')}`)}`);
- return false;
- }
- await this.e.shell.run(manager, managerArgs, { cwd: this.e.project.directory });
- return true;
- }
- }
- exports.ServeCLI = ServeCLI;
- class PkgManagerServeCLI extends ServeCLI {
- constructor() {
- super(...arguments);
- this.global = true;
- this.script = exports.SERVE_SCRIPT;
- }
- async resolveProgram() {
- return this.program;
- }
- async buildArgs(options) {
- const { pkgManagerArgs } = await Promise.resolve().then(() => tslib_1.__importStar(require('./utils/npm')));
- // The Ionic CLI decides the host/port of the dev server, so --host and
- // --port are provided to the downstream npm script as a best-effort
- // attempt.
- const args = {
- _: [],
- host: options.host,
- port: options.port.toString(),
- };
- const scriptArgs = [...(0, cli_framework_1.unparseArgs)(args), ...options['--'] || []];
- const [, ...pkgArgs] = await pkgManagerArgs(this.program, { command: 'run', script: this.script, scriptArgs });
- return pkgArgs;
- }
- }
- class NpmServeCLI extends PkgManagerServeCLI {
- constructor() {
- super(...arguments);
- this.name = 'npm CLI';
- this.pkg = 'npm';
- this.program = 'npm';
- this.prefix = 'npm';
- }
- }
- exports.NpmServeCLI = NpmServeCLI;
- class PnpmServeCLI extends PkgManagerServeCLI {
- constructor() {
- super(...arguments);
- this.name = 'pnpm CLI';
- this.pkg = 'pnpm';
- this.program = 'pnpm';
- this.prefix = 'pnpm';
- }
- }
- exports.PnpmServeCLI = PnpmServeCLI;
- class YarnServeCLI extends PkgManagerServeCLI {
- constructor() {
- super(...arguments);
- this.name = 'Yarn';
- this.pkg = 'yarn';
- this.program = 'yarn';
- this.prefix = 'yarn';
- }
- }
- exports.YarnServeCLI = YarnServeCLI;
|