11import { 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' ;
35import * as path from 'path' ;
6+ import * as semver from 'semver' ;
47
58import { CommandInstanceInfo , CommandLineInputs , CommandLineOptions , IonicCapacitorOptions , ProjectIntegration } from '../../definitions' ;
6- import { input , strong } from '../../lib/color' ;
9+ import { input , weak } from '../../lib/color' ;
710import { Command } from '../../lib/command' ;
811import { FatalException , RunnerException } from '../../lib/errors' ;
912import { 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' ;
1116import { generateOptionsForCapacitorBuild } from '../../lib/integrations/capacitor/utils' ;
17+ import { createPrefixedWriteStream } from '../../lib/utils/logger' ;
18+ import { pkgManagerArgs } from '../../lib/utils/npm' ;
1219
1320export 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}
0 commit comments