Skip to content

Commit 359cdec

Browse files
imhoffdmhartingtonelylucasjcesarmobile
authored
feat(capacitor): capacitor 3 support (#4610)
* feat(capacitor): Capacitor 3 support * dynamic config wip * specific version * android cleartext * use integration root * wip * better names * cleartext not necessary in generated config * use srcMainDir * redundant message * optomize in integration itself * print targets with Ionic CLI * cleanup * finalize preRun * do the thing * words * add checks for options that don't work with old capacitor * capacitor prefix * fix --open and --list usage * refactor to clear up the flows * only print for open flow * message for run as well * better error management when getting version * install package for Cap 3 * rm electron * fix installed platforms for v2 * fix extConfig usage for backwards compat * use integration root for web dir * add ios and android to info * new apps use @next * only load manifest for android??? * fix(capacitor): dont create capacitor.config.json if it doesn't exist Closes #4690 * feat(capacitor): add core capacitor plugins for ionic * chore(): update the latest tags Co-authored-by: Mike Hartington <mhartington@users.noreply.github.com> Co-authored-by: Ely Lucas <ely@meta-tek.net> Co-authored-by: jcesarmobile <jcesarmobile@gmail.com> Co-authored-by: Mike Hartington <mikehartington@gmail.com>
1 parent 99a875c commit 359cdec

File tree

17 files changed

+634
-245
lines changed

17 files changed

+634
-245
lines changed

packages/@ionic/cli/src/commands/capacitor/add.ts

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,13 @@ export class AddCommand extends CapacitorCommand implements CommandPreRun {
1313
summary: 'Add a native platform to your Ionic project',
1414
description: `
1515
${input('ionic capacitor add')} will do the following:
16-
- Add a new platform specific folder to your project (ios, android, or electron)
16+
- Install the Capacitor platform package
17+
- Copy the native platform template into your project
1718
`,
1819
inputs: [
1920
{
2021
name: 'platform',
21-
summary: `The platform to add (e.g. ${['android', 'ios', 'electron'].map(v => input(v)).join(', ')})`,
22+
summary: `The platform to add (e.g. ${['android', 'ios'].map(v => input(v)).join(', ')})`,
2223
validators: [validators.required],
2324
},
2425
],
@@ -33,7 +34,7 @@ ${input('ionic capacitor add')} will do the following:
3334
type: 'list',
3435
name: 'platform',
3536
message: 'What platform would you like to add?',
36-
choices: ['android', 'ios', 'electron'],
37+
choices: ['android', 'ios'],
3738
});
3839

3940
inputs[0] = platform.trim();
@@ -42,12 +43,7 @@ ${input('ionic capacitor add')} will do the following:
4243

4344
async run(inputs: CommandLineInputs, options: CommandLineOptions): Promise<void> {
4445
const [ platform ] = inputs;
45-
const args = ['add'];
4646

47-
if (platform) {
48-
args.push(platform);
49-
}
50-
51-
await this.runCapacitor(args);
47+
await this.installPlatform(platform);
5248
}
5349
}
Lines changed: 188 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
11
import { pathExists } from '@ionic/utils-fs';
2-
import { ERROR_COMMAND_NOT_FOUND, ERROR_SIGNAL_EXIT, SubprocessError } from '@ionic/utils-subprocess';
2+
import { onBeforeExit } from '@ionic/utils-process';
3+
import { ERROR_COMMAND_NOT_FOUND, SubprocessError } from '@ionic/utils-subprocess';
4+
import * as lodash from 'lodash';
35
import * as path from 'path';
6+
import * as semver from 'semver';
47

58
import { CommandInstanceInfo, CommandLineInputs, CommandLineOptions, IonicCapacitorOptions, ProjectIntegration } from '../../definitions';
6-
import { input, strong } from '../../lib/color';
9+
import { input, weak } from '../../lib/color';
710
import { Command } from '../../lib/command';
811
import { FatalException, RunnerException } from '../../lib/errors';
912
import { runCommand } from '../../lib/executor';
10-
import { CAPACITOR_CONFIG_FILE, CapacitorConfig } from '../../lib/integrations/capacitor/config';
13+
import type { CapacitorCLIConfig, Integration as CapacitorIntegration } from '../../lib/integrations/capacitor'
14+
import { ANDROID_MANIFEST_FILE, CapacitorAndroidManifest } from '../../lib/integrations/capacitor/android';
15+
import { CAPACITOR_CONFIG_JSON_FILE, CapacitorJSONConfig, CapacitorConfig } from '../../lib/integrations/capacitor/config';
1116
import { generateOptionsForCapacitorBuild } from '../../lib/integrations/capacitor/utils';
17+
import { createPrefixedWriteStream } from '../../lib/utils/logger';
18+
import { pkgManagerArgs } from '../../lib/utils/npm';
1219

1320
export abstract class CapacitorCommand extends Command {
1421
private _integration?: Required<ProjectIntegration>;
@@ -25,73 +32,156 @@ export abstract class CapacitorCommand extends Command {
2532
return this._integration;
2633
}
2734

28-
getCapacitorConfig(): CapacitorConfig {
35+
async getGeneratedConfig(platform: string): Promise<CapacitorJSONConfig> {
2936
if (!this.project) {
3037
throw new FatalException(`Cannot use Capacitor outside a project directory.`);
3138
}
3239

33-
return new CapacitorConfig(path.resolve(this.project.directory, CAPACITOR_CONFIG_FILE));
40+
const p = await this.getGeneratedConfigPath(platform);
41+
42+
return new CapacitorJSONConfig(p);
3443
}
3544

36-
async checkCapacitor(runinfo: CommandInstanceInfo) {
45+
async getGeneratedConfigPath(platform: string): Promise<string> {
3746
if (!this.project) {
3847
throw new FatalException(`Cannot use Capacitor outside a project directory.`);
3948
}
4049

41-
const capacitor = this.project.getIntegration('capacitor');
50+
const p = await this.getGeneratedConfigDir(platform);
4251

43-
if (!capacitor) {
44-
await runCommand(runinfo, ['integrations', 'enable', 'capacitor']);
52+
return path.resolve(this.integration.root, p, CAPACITOR_CONFIG_JSON_FILE);
53+
}
54+
55+
async getAndroidManifest(): Promise<CapacitorAndroidManifest> {
56+
const p = await this.getAndroidManifestPath();
57+
58+
return CapacitorAndroidManifest.load(p);
59+
}
60+
61+
async getAndroidManifestPath(): Promise<string> {
62+
const cli = await this.getCapacitorCLIConfig();
63+
const srcDir = cli?.android.srcMainDirAbs ?? 'android/app/src/main';
64+
65+
return path.resolve(this.integration.root, srcDir, ANDROID_MANIFEST_FILE);
66+
}
67+
68+
async getGeneratedConfigDir(platform: string): Promise<string> {
69+
const cli = await this.getCapacitorCLIConfig();
70+
71+
switch (platform) {
72+
case 'android':
73+
return cli?.android.assetsDirAbs ?? 'android/app/src/main/assets';
74+
case 'ios':
75+
return cli?.ios.nativeTargetDirAbs ?? 'ios/App/App';
4576
}
77+
78+
throw new FatalException(`Could not determine generated Capacitor config path for ${input(platform)} platform.`);
4679
}
4780

48-
async preRunChecks(runinfo: CommandInstanceInfo) {
49-
await this.checkCapacitor(runinfo);
81+
async getCapacitorCLIConfig(): Promise<CapacitorCLIConfig | undefined> {
82+
const capacitor = await this.getCapacitorIntegration();
83+
84+
return capacitor.getCapacitorCLIConfig();
85+
}
86+
87+
async getCapacitorConfig(): Promise<CapacitorConfig | undefined> {
88+
const capacitor = await this.getCapacitorIntegration();
89+
90+
return capacitor.getCapacitorConfig();
5091
}
5192

52-
async runCapacitor(argList: string[]): Promise<void> {
93+
getCapacitorIntegration = lodash.memoize(async (): Promise<CapacitorIntegration> => {
94+
if (!this.project) {
95+
throw new FatalException(`Cannot use Capacitor outside a project directory.`);
96+
}
97+
98+
return this.project.createIntegration('capacitor');
99+
});
100+
101+
getCapacitorVersion = lodash.memoize(async (): Promise<semver.SemVer> => {
53102
try {
54-
return await this._runCapacitor(argList);
103+
const proc = await this.env.shell.createSubprocess('capacitor', ['--version'], { cwd: this.integration.root });
104+
const version = semver.parse((await proc.output()).trim());
105+
106+
if (!version) {
107+
throw new FatalException('Error while parsing Capacitor CLI version.');
108+
}
109+
110+
return version;
55111
} catch (e) {
56112
if (e instanceof SubprocessError) {
57113
if (e.code === ERROR_COMMAND_NOT_FOUND) {
58-
const pkg = '@capacitor/cli';
59-
const requiredMsg = `The Capacitor CLI is required for Capacitor projects.`;
60-
this.env.log.nl();
61-
this.env.log.info(`Looks like ${input(pkg)} isn't installed in this project.\n` + requiredMsg);
62-
this.env.log.nl();
63-
64-
const installed = await this.promptToInstallCapacitor();
65-
66-
if (!installed) {
67-
throw new FatalException(`${input(pkg)} is required for Capacitor projects.`);
68-
}
69-
70-
return this.runCapacitor(argList);
114+
throw new FatalException('Error while getting Capacitor CLI version. Is Capacitor installed?');
71115
}
72116

73-
if (e.code === ERROR_SIGNAL_EXIT) {
74-
return;
75-
}
117+
throw new FatalException('Error while getting Capacitor CLI version.\n' + (e.output ? e.output : e.code));
76118
}
77119

78120
throw e;
79121
}
122+
});
123+
124+
async getInstalledPlatforms(): Promise<string[]> {
125+
const cli = await this.getCapacitorCLIConfig();
126+
const androidPlatformDirAbs = cli?.android.platformDirAbs ?? path.resolve(this.integration.root, 'android');
127+
const iosPlatformDirAbs = cli?.ios.platformDirAbs ?? path.resolve(this.integration.root, 'ios');
128+
const platforms: string[] = [];
129+
130+
if (await pathExists(androidPlatformDirAbs)) {
131+
platforms.push('android');
132+
}
133+
134+
if (await pathExists(iosPlatformDirAbs)) {
135+
platforms.push('ios');
136+
}
137+
138+
return platforms;
139+
}
140+
141+
async isPlatformInstalled(platform: string): Promise<boolean> {
142+
const platforms = await this.getInstalledPlatforms();
143+
144+
return platforms.includes(platform);
145+
}
146+
147+
async checkCapacitor(runinfo: CommandInstanceInfo) {
148+
if (!this.project) {
149+
throw new FatalException(`Cannot use Capacitor outside a project directory.`);
150+
}
151+
152+
const capacitor = this.project.getIntegration('capacitor');
153+
154+
if (!capacitor) {
155+
await runCommand(runinfo, ['integrations', 'enable', 'capacitor']);
156+
}
157+
}
158+
159+
async preRunChecks(runinfo: CommandInstanceInfo) {
160+
await this.checkCapacitor(runinfo);
161+
}
162+
163+
async runCapacitor(argList: string[]) {
164+
if (!this.project) {
165+
throw new FatalException(`Cannot use Capacitor outside a project directory.`);
166+
}
167+
168+
const stream = createPrefixedWriteStream(this.env.log, weak(`[capacitor]`));
169+
170+
await this.env.shell.run('capacitor', argList, { stream, fatalOnNotFound: false, truncateErrorOutput: 5000, cwd: this.integration.root });
80171
}
81172

82173
async runBuild(inputs: CommandLineInputs, options: CommandLineOptions): Promise<void> {
83174
if (!this.project) {
84175
throw new FatalException(`Cannot use Capacitor outside a project directory.`);
85176
}
86177

87-
const conf = this.getCapacitorConfig();
88-
const serverConfig = conf.get('server');
178+
const conf = await this.getCapacitorConfig();
89179

90-
if (serverConfig && serverConfig.url) {
180+
if (conf?.server?.url) {
91181
this.env.log.warn(
92182
`Capacitor server URL is in use.\n` +
93183
`This may result in unexpected behavior for this build, where an external server is used in the Web View instead of your app. This likely occurred because of ${input('--livereload')} usage in the past and the CLI improperly exiting without cleaning up.\n\n` +
94-
`Delete the ${input('server')} key in the ${strong(CAPACITOR_CONFIG_FILE)} file if you did not intend to use an external server.`
184+
`Delete the ${input('server')} key in the Capacitor config file if you did not intend to use an external server.`
95185
);
96186
this.env.log.nl();
97187
}
@@ -111,6 +201,51 @@ export abstract class CapacitorCommand extends Command {
111201
}
112202
}
113203

204+
async runServe(inputs: CommandLineInputs, options: CommandLineOptions): Promise<void> {
205+
if (!this.project) {
206+
throw new FatalException(`Cannot run ${input('ionic capacitor run')} outside a project directory.`);
207+
}
208+
209+
const [ platform ] = inputs;
210+
211+
try {
212+
const runner = await this.project.requireServeRunner();
213+
const runnerOpts = runner.createOptionsFromCommandLine(inputs, generateOptionsForCapacitorBuild(inputs, options));
214+
215+
let serverUrl = options['livereload-url'] ? String(options['livereload-url']) : undefined;
216+
217+
if (!serverUrl) {
218+
const details = await runner.run(runnerOpts);
219+
serverUrl = `${details.protocol || 'http'}://${details.externalAddress}:${details.port}`;
220+
}
221+
222+
const conf = await this.getGeneratedConfig(platform);
223+
224+
onBeforeExit(async () => {
225+
conf.resetServerUrl();
226+
});
227+
228+
conf.setServerUrl(serverUrl);
229+
230+
if (platform === 'android') {
231+
const manifest = await this.getAndroidManifest();
232+
233+
onBeforeExit(async () => {
234+
await manifest.reset();
235+
});
236+
237+
manifest.enableCleartextTraffic();
238+
await manifest.save();
239+
}
240+
} catch (e) {
241+
if (e instanceof RunnerException) {
242+
throw new FatalException(e.message);
243+
}
244+
245+
throw e;
246+
}
247+
}
248+
114249
async checkForPlatformInstallation(platform: string) {
115250
if (!this.project) {
116251
throw new FatalException('Cannot use Capacitor outside a project directory.');
@@ -123,66 +258,37 @@ export abstract class CapacitorCommand extends Command {
123258
throw new FatalException('Cannot check platform installations--Capacitor not yet integrated.');
124259
}
125260

126-
const integrationRoot = capacitor.root;
127-
const platformsToCheck = ['android', 'ios', 'electron'];
128-
const platforms = (await Promise.all(platformsToCheck.map(async (p): Promise<[string, boolean]> => [p, await pathExists(path.resolve(integrationRoot, p))])))
129-
.filter(([, e]) => e)
130-
.map(([p]) => p);
131-
132-
if (!platforms.includes(platform)) {
133-
await this._runCapacitor(['add', platform]);
261+
if (!(await this.isPlatformInstalled(platform))) {
262+
await this.installPlatform(platform);
134263
}
135264
}
136265
}
137266

138-
protected createOptionsFromCommandLine(inputs: CommandLineInputs, options: CommandLineOptions): IonicCapacitorOptions {
139-
const separatedArgs = options['--'];
140-
const verbose = !!options['verbose'];
141-
const conf = this.getCapacitorConfig();
142-
const server = conf.get('server');
143-
144-
return {
145-
'--': separatedArgs ? separatedArgs : [],
146-
appId: conf.get('appId'),
147-
appName: conf.get('appName'),
148-
server: {
149-
url: server?.url,
150-
},
151-
verbose,
152-
};
153-
}
267+
async installPlatform(platform: string): Promise<void> {
268+
const version = await this.getCapacitorVersion();
269+
const installedPlatforms = await this.getInstalledPlatforms();
154270

155-
private async promptToInstallCapacitor(): Promise<boolean> {
156-
if (!this.project) {
157-
throw new FatalException(`Cannot use Capacitor outside a project directory.`);
271+
if (installedPlatforms.includes(platform)) {
272+
throw new FatalException(`The ${input(platform)} platform is already installed!`);
158273
}
159274

160-
const { pkgManagerArgs } = await import('../../lib/utils/npm');
161-
162-
const pkg = '@capacitor/cli';
163-
const [ manager, ...managerArgs ] = await pkgManagerArgs(this.env.config.get('npmClient'), { pkg, command: 'install', saveDev: true });
164-
165-
const confirm = await this.env.prompt({
166-
name: 'confirm',
167-
message: `Install ${input(pkg)}?`,
168-
type: 'confirm',
169-
});
170-
171-
if (!confirm) {
172-
this.env.log.warn(`Not installing--here's how to install manually: ${input(`${manager} ${managerArgs.join(' ')}`)}`);
173-
return false;
275+
if (semver.gte(version, '3.0.0-alpha.1')) {
276+
const [ manager, ...managerArgs ] = await pkgManagerArgs(this.env.config.get('npmClient'), { command: 'install', pkg: `@capacitor/${platform}@next`, saveDev: true });
277+
await this.env.shell.run(manager, managerArgs, { cwd: this.integration.root });
174278
}
175279

176-
await this.env.shell.run(manager, managerArgs, { cwd: this.project.directory });
177-
178-
return true;
280+
await this.runCapacitor(['add', platform]);
179281
}
180282

181-
private async _runCapacitor(argList: string[]) {
182-
if (!this.project) {
183-
throw new FatalException(`Cannot use Capacitor outside a project directory.`);
184-
}
283+
protected async createOptionsFromCommandLine(inputs: CommandLineInputs, options: CommandLineOptions): Promise<IonicCapacitorOptions> {
284+
const separatedArgs = options['--'];
285+
const verbose = !!options['verbose'];
286+
const conf = await this.getCapacitorConfig();
185287

186-
await this.env.shell.run('capacitor', argList, { fatalOnNotFound: false, truncateErrorOutput: 5000, stdio: 'inherit', cwd: this.integration.root });
288+
return {
289+
'--': separatedArgs ? separatedArgs : [],
290+
verbose,
291+
...conf,
292+
};
187293
}
188294
}

packages/@ionic/cli/src/commands/capacitor/build.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ To configure your native project, see the common configuration docs[^capacitor-n
160160
await hook.run({
161161
name: hook.name,
162162
build: buildRunner.createOptionsFromCommandLine(inputs, options),
163-
capacitor: this.createOptionsFromCommandLine(inputs, options),
163+
capacitor: await this.createOptionsFromCommandLine(inputs, options),
164164
});
165165
} catch (e) {
166166
if (e instanceof BaseError) {

0 commit comments

Comments
 (0)