Skip to content

Commit

Permalink
feat(capacitor): capacitor 3 support (#4610)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
5 people committed May 18, 2021
1 parent 99a875c commit 359cdec
Show file tree
Hide file tree
Showing 17 changed files with 634 additions and 245 deletions.
14 changes: 5 additions & 9 deletions packages/@ionic/cli/src/commands/capacitor/add.ts
Expand Up @@ -13,12 +13,13 @@ export class AddCommand extends CapacitorCommand implements CommandPreRun {
summary: 'Add a native platform to your Ionic project',
description: `
${input('ionic capacitor add')} will do the following:
- Add a new platform specific folder to your project (ios, android, or electron)
- Install the Capacitor platform package
- Copy the native platform template into your project
`,
inputs: [
{
name: 'platform',
summary: `The platform to add (e.g. ${['android', 'ios', 'electron'].map(v => input(v)).join(', ')})`,
summary: `The platform to add (e.g. ${['android', 'ios'].map(v => input(v)).join(', ')})`,
validators: [validators.required],
},
],
Expand All @@ -33,7 +34,7 @@ ${input('ionic capacitor add')} will do the following:
type: 'list',
name: 'platform',
message: 'What platform would you like to add?',
choices: ['android', 'ios', 'electron'],
choices: ['android', 'ios'],
});

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

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

if (platform) {
args.push(platform);
}

await this.runCapacitor(args);
await this.installPlatform(platform);
}
}
270 changes: 188 additions & 82 deletions packages/@ionic/cli/src/commands/capacitor/base.ts
@@ -1,14 +1,21 @@
import { pathExists } from '@ionic/utils-fs';
import { ERROR_COMMAND_NOT_FOUND, ERROR_SIGNAL_EXIT, SubprocessError } from '@ionic/utils-subprocess';
import { onBeforeExit } from '@ionic/utils-process';
import { ERROR_COMMAND_NOT_FOUND, SubprocessError } from '@ionic/utils-subprocess';
import * as lodash from 'lodash';
import * as path from 'path';
import * as semver from 'semver';

import { CommandInstanceInfo, CommandLineInputs, CommandLineOptions, IonicCapacitorOptions, ProjectIntegration } from '../../definitions';
import { input, strong } from '../../lib/color';
import { input, weak } from '../../lib/color';
import { Command } from '../../lib/command';
import { FatalException, RunnerException } from '../../lib/errors';
import { runCommand } from '../../lib/executor';
import { CAPACITOR_CONFIG_FILE, CapacitorConfig } from '../../lib/integrations/capacitor/config';
import type { CapacitorCLIConfig, Integration as CapacitorIntegration } from '../../lib/integrations/capacitor'
import { ANDROID_MANIFEST_FILE, CapacitorAndroidManifest } from '../../lib/integrations/capacitor/android';
import { CAPACITOR_CONFIG_JSON_FILE, CapacitorJSONConfig, CapacitorConfig } from '../../lib/integrations/capacitor/config';
import { generateOptionsForCapacitorBuild } from '../../lib/integrations/capacitor/utils';
import { createPrefixedWriteStream } from '../../lib/utils/logger';
import { pkgManagerArgs } from '../../lib/utils/npm';

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

getCapacitorConfig(): CapacitorConfig {
async getGeneratedConfig(platform: string): Promise<CapacitorJSONConfig> {
if (!this.project) {
throw new FatalException(`Cannot use Capacitor outside a project directory.`);
}

return new CapacitorConfig(path.resolve(this.project.directory, CAPACITOR_CONFIG_FILE));
const p = await this.getGeneratedConfigPath(platform);

return new CapacitorJSONConfig(p);
}

async checkCapacitor(runinfo: CommandInstanceInfo) {
async getGeneratedConfigPath(platform: string): Promise<string> {
if (!this.project) {
throw new FatalException(`Cannot use Capacitor outside a project directory.`);
}

const capacitor = this.project.getIntegration('capacitor');
const p = await this.getGeneratedConfigDir(platform);

if (!capacitor) {
await runCommand(runinfo, ['integrations', 'enable', 'capacitor']);
return path.resolve(this.integration.root, p, CAPACITOR_CONFIG_JSON_FILE);
}

async getAndroidManifest(): Promise<CapacitorAndroidManifest> {
const p = await this.getAndroidManifestPath();

return CapacitorAndroidManifest.load(p);
}

async getAndroidManifestPath(): Promise<string> {
const cli = await this.getCapacitorCLIConfig();
const srcDir = cli?.android.srcMainDirAbs ?? 'android/app/src/main';

return path.resolve(this.integration.root, srcDir, ANDROID_MANIFEST_FILE);
}

async getGeneratedConfigDir(platform: string): Promise<string> {
const cli = await this.getCapacitorCLIConfig();

switch (platform) {
case 'android':
return cli?.android.assetsDirAbs ?? 'android/app/src/main/assets';
case 'ios':
return cli?.ios.nativeTargetDirAbs ?? 'ios/App/App';
}

throw new FatalException(`Could not determine generated Capacitor config path for ${input(platform)} platform.`);
}

async preRunChecks(runinfo: CommandInstanceInfo) {
await this.checkCapacitor(runinfo);
async getCapacitorCLIConfig(): Promise<CapacitorCLIConfig | undefined> {
const capacitor = await this.getCapacitorIntegration();

return capacitor.getCapacitorCLIConfig();
}

async getCapacitorConfig(): Promise<CapacitorConfig | undefined> {
const capacitor = await this.getCapacitorIntegration();

return capacitor.getCapacitorConfig();
}

async runCapacitor(argList: string[]): Promise<void> {
getCapacitorIntegration = lodash.memoize(async (): Promise<CapacitorIntegration> => {
if (!this.project) {
throw new FatalException(`Cannot use Capacitor outside a project directory.`);
}

return this.project.createIntegration('capacitor');
});

getCapacitorVersion = lodash.memoize(async (): Promise<semver.SemVer> => {
try {
return await this._runCapacitor(argList);
const proc = await this.env.shell.createSubprocess('capacitor', ['--version'], { cwd: this.integration.root });
const version = semver.parse((await proc.output()).trim());

if (!version) {
throw new FatalException('Error while parsing Capacitor CLI version.');
}

return version;
} catch (e) {
if (e instanceof SubprocessError) {
if (e.code === ERROR_COMMAND_NOT_FOUND) {
const pkg = '@capacitor/cli';
const requiredMsg = `The Capacitor CLI is required for Capacitor projects.`;
this.env.log.nl();
this.env.log.info(`Looks like ${input(pkg)} isn't installed in this project.\n` + requiredMsg);
this.env.log.nl();

const installed = await this.promptToInstallCapacitor();

if (!installed) {
throw new FatalException(`${input(pkg)} is required for Capacitor projects.`);
}

return this.runCapacitor(argList);
throw new FatalException('Error while getting Capacitor CLI version. Is Capacitor installed?');
}

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

throw e;
}
});

async getInstalledPlatforms(): Promise<string[]> {
const cli = await this.getCapacitorCLIConfig();
const androidPlatformDirAbs = cli?.android.platformDirAbs ?? path.resolve(this.integration.root, 'android');
const iosPlatformDirAbs = cli?.ios.platformDirAbs ?? path.resolve(this.integration.root, 'ios');
const platforms: string[] = [];

if (await pathExists(androidPlatformDirAbs)) {
platforms.push('android');
}

if (await pathExists(iosPlatformDirAbs)) {
platforms.push('ios');
}

return platforms;
}

async isPlatformInstalled(platform: string): Promise<boolean> {
const platforms = await this.getInstalledPlatforms();

return platforms.includes(platform);
}

async checkCapacitor(runinfo: CommandInstanceInfo) {
if (!this.project) {
throw new FatalException(`Cannot use Capacitor outside a project directory.`);
}

const capacitor = this.project.getIntegration('capacitor');

if (!capacitor) {
await runCommand(runinfo, ['integrations', 'enable', 'capacitor']);
}
}

async preRunChecks(runinfo: CommandInstanceInfo) {
await this.checkCapacitor(runinfo);
}

async runCapacitor(argList: string[]) {
if (!this.project) {
throw new FatalException(`Cannot use Capacitor outside a project directory.`);
}

const stream = createPrefixedWriteStream(this.env.log, weak(`[capacitor]`));

await this.env.shell.run('capacitor', argList, { stream, fatalOnNotFound: false, truncateErrorOutput: 5000, cwd: this.integration.root });
}

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

const conf = this.getCapacitorConfig();
const serverConfig = conf.get('server');
const conf = await this.getCapacitorConfig();

if (serverConfig && serverConfig.url) {
if (conf?.server?.url) {
this.env.log.warn(
`Capacitor server URL is in use.\n` +
`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` +
`Delete the ${input('server')} key in the ${strong(CAPACITOR_CONFIG_FILE)} file if you did not intend to use an external server.`
`Delete the ${input('server')} key in the Capacitor config file if you did not intend to use an external server.`
);
this.env.log.nl();
}
Expand All @@ -111,6 +201,51 @@ export abstract class CapacitorCommand extends Command {
}
}

async runServe(inputs: CommandLineInputs, options: CommandLineOptions): Promise<void> {
if (!this.project) {
throw new FatalException(`Cannot run ${input('ionic capacitor run')} outside a project directory.`);
}

const [ platform ] = inputs;

try {
const runner = await this.project.requireServeRunner();
const runnerOpts = runner.createOptionsFromCommandLine(inputs, generateOptionsForCapacitorBuild(inputs, options));

let serverUrl = options['livereload-url'] ? String(options['livereload-url']) : undefined;

if (!serverUrl) {
const details = await runner.run(runnerOpts);
serverUrl = `${details.protocol || 'http'}://${details.externalAddress}:${details.port}`;
}

const conf = await this.getGeneratedConfig(platform);

onBeforeExit(async () => {
conf.resetServerUrl();
});

conf.setServerUrl(serverUrl);

if (platform === 'android') {
const manifest = await this.getAndroidManifest();

onBeforeExit(async () => {
await manifest.reset();
});

manifest.enableCleartextTraffic();
await manifest.save();
}
} catch (e) {
if (e instanceof RunnerException) {
throw new FatalException(e.message);
}

throw e;
}
}

async checkForPlatformInstallation(platform: string) {
if (!this.project) {
throw new FatalException('Cannot use Capacitor outside a project directory.');
Expand All @@ -123,66 +258,37 @@ export abstract class CapacitorCommand extends Command {
throw new FatalException('Cannot check platform installations--Capacitor not yet integrated.');
}

const integrationRoot = capacitor.root;
const platformsToCheck = ['android', 'ios', 'electron'];
const platforms = (await Promise.all(platformsToCheck.map(async (p): Promise<[string, boolean]> => [p, await pathExists(path.resolve(integrationRoot, p))])))
.filter(([, e]) => e)
.map(([p]) => p);

if (!platforms.includes(platform)) {
await this._runCapacitor(['add', platform]);
if (!(await this.isPlatformInstalled(platform))) {
await this.installPlatform(platform);
}
}
}

protected createOptionsFromCommandLine(inputs: CommandLineInputs, options: CommandLineOptions): IonicCapacitorOptions {
const separatedArgs = options['--'];
const verbose = !!options['verbose'];
const conf = this.getCapacitorConfig();
const server = conf.get('server');

return {
'--': separatedArgs ? separatedArgs : [],
appId: conf.get('appId'),
appName: conf.get('appName'),
server: {
url: server?.url,
},
verbose,
};
}
async installPlatform(platform: string): Promise<void> {
const version = await this.getCapacitorVersion();
const installedPlatforms = await this.getInstalledPlatforms();

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

const { pkgManagerArgs } = await import('../../lib/utils/npm');

const pkg = '@capacitor/cli';
const [ manager, ...managerArgs ] = await pkgManagerArgs(this.env.config.get('npmClient'), { pkg, command: 'install', saveDev: true });

const confirm = await this.env.prompt({
name: 'confirm',
message: `Install ${input(pkg)}?`,
type: 'confirm',
});

if (!confirm) {
this.env.log.warn(`Not installing--here's how to install manually: ${input(`${manager} ${managerArgs.join(' ')}`)}`);
return false;
if (semver.gte(version, '3.0.0-alpha.1')) {
const [ manager, ...managerArgs ] = await pkgManagerArgs(this.env.config.get('npmClient'), { command: 'install', pkg: `@capacitor/${platform}@next`, saveDev: true });
await this.env.shell.run(manager, managerArgs, { cwd: this.integration.root });
}

await this.env.shell.run(manager, managerArgs, { cwd: this.project.directory });

return true;
await this.runCapacitor(['add', platform]);
}

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

await this.env.shell.run('capacitor', argList, { fatalOnNotFound: false, truncateErrorOutput: 5000, stdio: 'inherit', cwd: this.integration.root });
return {
'--': separatedArgs ? separatedArgs : [],
verbose,
...conf,
};
}
}
2 changes: 1 addition & 1 deletion packages/@ionic/cli/src/commands/capacitor/build.ts
Expand Up @@ -160,7 +160,7 @@ To configure your native project, see the common configuration docs[^capacitor-n
await hook.run({
name: hook.name,
build: buildRunner.createOptionsFromCommandLine(inputs, options),
capacitor: this.createOptionsFromCommandLine(inputs, options),
capacitor: await this.createOptionsFromCommandLine(inputs, options),
});
} catch (e) {
if (e instanceof BaseError) {
Expand Down

0 comments on commit 359cdec

Please sign in to comment.