serve.js 21 KB

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