Skip to content

Commit 62a2918

Browse files
committed
feat(capacitor): add run command
`ionic capacitor run` is similar to `ionic cordova run` in that it invokes an Ionic build or serve (depending on the `--livereload` option) and copies assets and configuration to native projects. It differs in that it does not programmatically run the native project in a device or emulator/simulator. Capacitor projects must still use the Native IDE (Xcode or Android Studio) to run apps natively (for now 😉). Resolves #3130
1 parent 2a312ab commit 62a2918

4 files changed

Lines changed: 209 additions & 0 deletions

File tree

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { BaseConfig } from '@ionic/cli-framework';
2+
import * as lodash from 'lodash';
3+
4+
export const CAPACITOR_CONFIG_FILE = 'capacitor.config.json';
5+
6+
export interface CapacitorConfigFile {
7+
webDir?: string;
8+
server?: {
9+
url?: string;
10+
originalUrl?: string;
11+
};
12+
}
13+
14+
export class CapacitorConfig extends BaseConfig<CapacitorConfigFile> {
15+
provideDefaults(config: CapacitorConfigFile): CapacitorConfigFile {
16+
return config;
17+
}
18+
19+
setServerUrl(url: string): void {
20+
const serverConfig = this.get('server') || {};
21+
22+
if (serverConfig.url) {
23+
serverConfig.originalUrl = serverConfig.url;
24+
}
25+
26+
serverConfig.url = url;
27+
28+
this.set('server', serverConfig);
29+
}
30+
31+
resetServerUrl(): void {
32+
const serverConfig = this.get('server') || {};
33+
34+
delete serverConfig.url;
35+
36+
if (serverConfig.originalUrl) {
37+
serverConfig.url = serverConfig.originalUrl;
38+
delete serverConfig.originalUrl;
39+
}
40+
41+
if (lodash.isEmpty(serverConfig)) {
42+
this.unset('server');
43+
} else {
44+
this.set('server', serverConfig);
45+
}
46+
}
47+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { CommandLineInputs, CommandLineOptions } from '../../../definitions';
2+
3+
export function generateOptionsForCapacitorBuild(inputs: CommandLineInputs, options: CommandLineOptions): CommandLineOptions {
4+
const [ platform ] = inputs;
5+
6+
return {
7+
...options,
8+
externalAddressRequired: true,
9+
engine: 'capacitor',
10+
platform: platform ? platform : (options['platform'] ? String(options['platform']) : undefined),
11+
};
12+
}

packages/ionic/src/commands/capacitor/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ Learn more about Capacitor:
2929
['add', async () => { const { AddCommand } = await import('./add'); return new AddCommand(this); }],
3030
['copy', async () => { const { CopyCommand } = await import('./copy'); return new CopyCommand(this); }],
3131
['open', async () => { const { OpenCommand } = await import('./open'); return new OpenCommand(this); }],
32+
['run', async () => { const { RunCommand } = await import('./run'); return new RunCommand(this); }],
3233
['sync', async () => { const { SyncCommand } = await import('./sync'); return new SyncCommand(this); }],
3334
['update', async () => { const { UpdateCommand } = await import('./update'); return new UpdateCommand(this); }],
3435
]);
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import chalk from 'chalk';
2+
import * as path from 'path';
3+
4+
import { validators } from '@ionic/cli-framework';
5+
import { onBeforeExit, sleepForever } from '@ionic/cli-framework/utils/process';
6+
import { CommandInstanceInfo, CommandLineInputs, CommandLineOptions, CommandMetadata, CommandMetadataOption, CommandPreRun } from '@ionic/cli-utils';
7+
import { build } from '@ionic/cli-utils/lib/build';
8+
import { FatalException } from '@ionic/cli-utils/lib/errors';
9+
import { CAPACITOR_CONFIG_FILE, CapacitorConfig } from '@ionic/cli-utils/lib/integrations/capacitor/config';
10+
import { generateOptionsForCapacitorBuild } from '@ionic/cli-utils/lib/integrations/capacitor/utils';
11+
import { COMMON_SERVE_COMMAND_OPTIONS, LOCAL_ADDRESSES, serve } from '@ionic/cli-utils/lib/serve';
12+
13+
import { CapacitorCommand } from './base';
14+
15+
export class RunCommand extends CapacitorCommand implements CommandPreRun {
16+
async getMetadata(): Promise<CommandMetadata> {
17+
let groups: string[] = [];
18+
const options: CommandMetadataOption[] = [
19+
// Build Options
20+
{
21+
name: 'build',
22+
summary: 'Do not invoke Ionic build',
23+
type: Boolean,
24+
default: true,
25+
},
26+
...COMMON_SERVE_COMMAND_OPTIONS.filter(o => !['livereload'].includes(o.name)),
27+
{
28+
name: 'livereload',
29+
summary: 'Spin up dev server to live-reload www files',
30+
type: Boolean,
31+
aliases: ['l'],
32+
},
33+
];
34+
35+
const serveRunner = this.project && await this.project.getServeRunner();
36+
const buildRunner = this.project && await this.project.getBuildRunner();
37+
38+
if (buildRunner) {
39+
const libmetadata = await buildRunner.getCommandMetadata();
40+
groups = libmetadata.groups || [];
41+
options.push(...libmetadata.options || []);
42+
}
43+
44+
if (serveRunner) {
45+
const libmetadata = await serveRunner.getCommandMetadata();
46+
const existingOpts = options.map(o => o.name);
47+
groups = libmetadata.groups || [];
48+
options.push(...(libmetadata.options || []).filter(o => !existingOpts.includes(o.name)).map(o => ({ ...o, hint: `${o.hint ? `${o.hint} ` : ''}${chalk.dim('(--livereload)')}` })));
49+
}
50+
51+
return {
52+
name: 'run',
53+
type: 'project',
54+
summary: 'Copies web assets to each Capacitor native platform',
55+
description: `
56+
${chalk.green('ionic capacitor run')} will do the following:
57+
- Perform ${chalk.green('ionic build')} (or run the dev server from ${chalk.green('ionic serve')} with the ${chalk.green('--livereload')} option)
58+
- Copy web assets into the specified native platform
59+
60+
For Android and iOS, you can setup Remote Debugging on your device with browser development tools using these docs${chalk.cyan('[1]')}.
61+
62+
${chalk.cyan('[1]')}: ${chalk.bold('https://ionicframework.com/docs/developer-resources/developer-tips/')}
63+
`,
64+
exampleCommands: [
65+
'',
66+
'-l',
67+
],
68+
inputs: [
69+
{
70+
name: 'platform',
71+
summary: `The platform to run (e.g. ${['android', 'ios'].map(v => chalk.green(v)).join(', ')})`,
72+
validators: [validators.required],
73+
},
74+
],
75+
options,
76+
groups,
77+
};
78+
}
79+
80+
async preRun(inputs: CommandLineInputs, options: CommandLineOptions, runinfo: CommandInstanceInfo): Promise<void> {
81+
await this.preRunChecks(runinfo);
82+
83+
if (!inputs[0]) {
84+
const platform = await this.env.prompt({
85+
type: 'list',
86+
name: 'platform',
87+
message: 'What platform would you like to run?',
88+
choices: ['android', 'ios'],
89+
});
90+
91+
inputs[0] = platform.trim();
92+
}
93+
}
94+
95+
async run(inputs: CommandLineInputs, options: CommandLineOptions): Promise<void> {
96+
if (!this.project) {
97+
throw new FatalException(`Cannot run ${chalk.green('ionic cordova run/emulate')} outside a project directory.`);
98+
}
99+
100+
const [ platform ] = inputs;
101+
102+
if (options['livereload']) {
103+
const conf = new CapacitorConfig(path.resolve(this.project.directory, CAPACITOR_CONFIG_FILE));
104+
105+
// TODO: use runner directly
106+
const details = await serve({ flags: this.env.flags, config: this.env.config, log: this.env.log, prompt: this.env.prompt, shell: this.env.shell, project: this.project }, inputs, generateOptionsForCapacitorBuild(inputs, options));
107+
108+
if (details.externallyAccessible === false) {
109+
const extra = LOCAL_ADDRESSES.includes(details.externalAddress) ? '\nEnsure you have proper port forwarding setup from your device to your computer.' : '';
110+
this.env.log.warn(`Your device or emulator may not be able to access ${chalk.bold(details.externalAddress)}.${extra}\n\n`);
111+
}
112+
113+
conf.setServerUrl(`${details.protocol || 'http'}://${details.externalAddress}:${details.port}`);
114+
115+
onBeforeExit(async () => {
116+
conf.resetServerUrl();
117+
});
118+
} else {
119+
if (options['build']) {
120+
// TODO: use runner directly
121+
await build({ config: this.env.config, log: this.env.log, shell: this.env.shell, prompt: this.env.prompt, project: this.project }, inputs, generateOptionsForCapacitorBuild(inputs, options));
122+
}
123+
}
124+
125+
// copy assets and capacitor.config.json into place
126+
await this.runCapacitor(['copy', platform]);
127+
128+
// TODO: native-run
129+
130+
this.env.log.nl();
131+
this.env.log.info(
132+
'Ready for use in your Native IDE!\n' +
133+
`To continue, run your project on a device or ${platform === 'ios' ? 'simulator' : 'emulator'} using ${platform === 'ios' ? 'Xcode' : 'Android Studio'}!`
134+
);
135+
136+
this.env.log.nl();
137+
138+
await this.runCapacitor(['open', platform]);
139+
140+
if (options['livereload']) {
141+
this.env.log.nl();
142+
this.env.log.info(
143+
'Development server will continue running until manually stopped.\n' +
144+
chalk.yellow('Use Ctrl+C to quit this process')
145+
);
146+
await sleepForever();
147+
}
148+
}
149+
}

0 commit comments

Comments
 (0)