Skip to content

Commit b071fcb

Browse files
nphyattimhoffd
authored andcommitted
feat(integrations): ionic enterprise integration (#3905)
1 parent 5311138 commit b071fcb

11 files changed

Lines changed: 465 additions & 154 deletions

File tree

packages/ionic/src/commands/integrations/disable.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,12 @@ Integrations, such as Cordova, can be disabled with this command.
3838
}
3939

4040
const integration = await this.project.createIntegration(name);
41-
const integrationsConfig = this.project.config.get('integrations');
42-
const integrationConfig = integrationsConfig[name];
4341

4442
try {
45-
if (!integrationConfig || integrationConfig.enabled === false) {
43+
if (!integration.isAdded() || !integration.isEnabled()) {
4644
this.env.log.info(`Integration ${chalk.green(name)} already disabled.`);
4745
} else {
4846
await integration.disable();
49-
integrationConfig.enabled = false;
5047
this.env.log.ok(`Integration ${chalk.green(name)} disabled!`);
5148
}
5249
} catch (e) {
@@ -56,7 +53,5 @@ Integrations, such as Cordova, can be disabled with this command.
5653

5754
throw e;
5855
}
59-
60-
this.project.config.set('integrations', integrationsConfig);
6156
}
6257
}

packages/ionic/src/commands/integrations/enable.ts

Lines changed: 5 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -61,51 +61,26 @@ Integrations can be re-added with the ${chalk.green('--add')} option.
6161
}
6262

6363
const integration = await this.project.createIntegration(name);
64-
const integrationsConfig = this.project.config.get('integrations');
65-
const integrationConfig = integrationsConfig[name];
6664

6765
try {
68-
if (!integrationConfig || add) {
66+
if (!integration.isAdded() || add) {
6967
await integration.add({
7068
root,
7169
enableArgs: options['--'] ? options['--'] : undefined,
72-
}, {
73-
conflictHandler: async (f, stats) => {
74-
const isDirectory = stats.isDirectory();
75-
const filename = `${path.basename(f)}${isDirectory ? '/' : ''}`;
76-
const type = isDirectory ? 'directory' : 'file';
77-
78-
const confirm = await this.env.prompt({
79-
type: 'confirm',
80-
name: 'confirm',
81-
message: `The ${chalk.cyan(filename)} ${type} exists in project. Overwrite?`,
82-
default: false,
83-
});
84-
85-
return confirm;
86-
},
87-
onFileCreate: f => {
88-
if (!quiet) {
89-
this.env.log.msg(`${chalk.green('CREATE')} ${f}`);
90-
}
91-
},
70+
quiet: Boolean(quiet),
71+
env: this.env,
9272
});
9373

94-
integrationsConfig[name] = {
95-
root: root !== this.project.directory ? path.relative(this.project.rootDirectory, root) : undefined,
96-
};
97-
9874
this.env.log.ok(`Integration ${chalk.green(integration.name)} added!`);
9975
} else {
100-
const wasEnabled = integrationConfig.enabled !== false;
76+
const wasEnabled = integration.config.get('enabled') !== false;
10177

10278
// We still need to run this whenever this command is run to make sure
10379
// everything is good with the integration.
10480
await integration.enable();
105-
delete integrationConfig.enabled;
10681

10782
if (wasEnabled) {
108-
this.env.log.info(`Integration ${chalk.green(integration.name)} enabled.`);
83+
this.env.log.info(`Integration ${chalk.green(integration.name)} already enabled.`);
10984
} else {
11085
this.env.log.ok(`Integration ${chalk.green(integration.name)} enabled!`);
11186
}
@@ -117,7 +92,5 @@ Integrations can be re-added with the ${chalk.green('--add')} option.
11792

11893
throw e;
11994
}
120-
121-
this.project.config.set('integrations', integrationsConfig);
12295
}
12396
}

packages/ionic/src/definitions.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -75,16 +75,25 @@ export type HookContext = BaseHookContext & HookInput;
7575

7676
export type HookFn = (ctx: HookContext) => Promise<void>;
7777

78-
export type IntegrationName = 'capacitor' | 'cordova';
78+
export type IntegrationName = 'capacitor' | 'cordova' | 'enterprise';
7979

8080
export interface ProjectIntegration {
8181
enabled?: boolean;
8282
root?: string;
8383
}
8484

85+
export interface EnterpriseProjectIntegration extends ProjectIntegration {
86+
productKey?: string;
87+
registries?: string[];
88+
appId?: string;
89+
orgId?: string;
90+
keyId?: number;
91+
}
92+
8593
export interface ProjectIntegrations {
8694
cordova?: ProjectIntegration;
8795
capacitor?: ProjectIntegration;
96+
enterprise?: EnterpriseProjectIntegration;
8897
}
8998

9099
export interface Response<T extends object> extends APIResponseSuccess {
@@ -261,7 +270,7 @@ export interface IProject {
261270
getDistDir(): Promise<string>;
262271
getInfo(): Promise<InfoItem[]>;
263272
detected(): Promise<boolean>;
264-
createIntegration(name: IntegrationName): Promise<IIntegration>;
273+
createIntegration(name: IntegrationName): Promise<IIntegration<ProjectIntegration>>;
265274
getIntegration(name: IntegrationName): Required<ProjectIntegration> | undefined;
266275
requireIntegration(name: IntegrationName): Required<ProjectIntegration>;
267276
requireAppflowId(): Promise<string>;
@@ -278,6 +287,8 @@ export interface IProject {
278287
}
279288

280289
export interface IntegrationAddDetails {
290+
env: IonicEnvironment;
291+
quiet?: boolean;
281292
root: string;
282293
enableArgs?: string[];
283294
}
@@ -287,13 +298,16 @@ export interface IntegrationAddHandlers {
287298
onFileCreate?: (f: string) => void;
288299
}
289300

290-
export interface IIntegration {
301+
export interface IIntegration<T extends ProjectIntegration> {
291302
readonly name: IntegrationName;
292303
readonly summary: string;
293304
readonly archiveUrl?: string;
305+
readonly config: ζframework.BaseConfig<T>;
294306

295-
add(details: IntegrationAddDetails, handlers?: IntegrationAddHandlers): Promise<void>;
296-
enable(): Promise<void>;
307+
add(details: IntegrationAddDetails): Promise<void>;
308+
isAdded(): boolean;
309+
enable(config?: T): Promise<void>;
310+
isEnabled(): boolean;
297311
disable(): Promise<void>;
298312
getInfo(): Promise<InfoItem[]>;
299313
personalize(details: ProjectPersonalizationDetails): Promise<void>;

packages/ionic/src/guards.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { APIResponse, APIResponseError, APIResponseSuccess, App, AppAssociation, BitbucketCloudRepoAssociation, BitbucketServerRepoAssociation, CommandPreRun, CordovaPackageJson, ExitCodeException, GithubBranch, GithubRepo, GithubRepoAssociation, ICommand, IMultiProjectConfig, IProjectConfig, IntegrationName, Login, Org, Response, SSHKey, SecurityProfile, Snapshot, StarterManifest, SuperAgentError, TreatableAilment, User } from './definitions';
22
import { AuthConnection } from './lib/auth';
33

4-
export const INTEGRATION_NAMES: IntegrationName[] = ['capacitor', 'cordova'];
4+
export const INTEGRATION_NAMES: IntegrationName[] = ['capacitor', 'cordova', 'enterprise'];
55

66
export function isCommand(cmd: any): cmd is ICommand {
77
return cmd && typeof cmd.run === 'function';

packages/ionic/src/lib/app.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,14 @@ export class AppClient extends ResourceClient implements ResourceClientLoad<App>
5252
return res.data;
5353
}
5454

55-
paginate(args: Partial<PaginateArgs<Response<App[]>>> = {}): IPaginator<Response<App[]>, PaginatorState> {
55+
paginate(args: Partial<PaginateArgs<Response<App[]>>> = {}, orgId?: string): IPaginator<Response<App[]>, PaginatorState> {
5656
return this.e.client.paginate({
5757
reqgen: async () => {
5858
const { req } = await this.e.client.make('GET', '/apps');
5959
this.applyAuthentication(req, this.token);
60+
if (orgId) {
61+
req.send({ org_id: orgId });
62+
}
6063
return { req };
6164
},
6265
guard: isAppsResponse,

packages/ionic/src/lib/integrations/capacitor/index.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,28 @@ import { parseArgs } from '@ionic/cli-framework';
22
import { mkdirp } from '@ionic/utils-fs';
33
import * as path from 'path';
44

5-
import { BaseIntegration } from '../';
6-
import { InfoItem, IntegrationAddDetails, IntegrationAddHandlers, IntegrationName, ProjectPersonalizationDetails } from '../../../definitions';
5+
import { BaseIntegration, IntegrationConfig } from '../';
6+
import {
7+
InfoItem,
8+
IntegrationAddDetails,
9+
IntegrationName,
10+
ProjectIntegration,
11+
ProjectPersonalizationDetails
12+
} from '../../../definitions';
713
import { pkgManagerArgs } from '../../utils/npm';
814

915
import { CAPACITOR_CONFIG_FILE, CapacitorConfig } from './config';
1016

11-
export class Integration extends BaseIntegration {
17+
export class Integration extends BaseIntegration<ProjectIntegration> {
1218
readonly name: IntegrationName = 'capacitor';
1319
readonly summary = `Target native iOS and Android with Capacitor, Ionic's new native layer`;
1420
readonly archiveUrl = undefined;
1521

16-
async add(details: IntegrationAddDetails, handlers: IntegrationAddHandlers = {}): Promise<void> {
22+
get config(): IntegrationConfig {
23+
return new IntegrationConfig(this.e.project.filePath, { pathPrefix: ['integrations', this.name] });
24+
}
25+
26+
async add(details: IntegrationAddDetails): Promise<void> {
1727
let name = this.e.project.config.get('name');
1828
let packageId = 'io.ionic.starter';
1929
const options: string[] = [];
@@ -34,7 +44,7 @@ export class Integration extends BaseIntegration {
3444
await mkdirp(details.root);
3545
await this.e.shell.run('capacitor', ['init', name, packageId, ...options], { cwd: details.root });
3646

37-
await super.add(details, handlers);
47+
await super.add(details);
3848
}
3949

4050
async installCapacitorCore() {

packages/ionic/src/lib/integrations/cordova/index.ts

Lines changed: 114 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,126 @@
1+
import { copy, mkdirp, pathExists, readdirSafe, remove, stat } from '@ionic/utils-fs';
2+
import chalk from 'chalk';
13
import * as Debug from 'debug';
4+
import * as lodash from 'lodash';
5+
import * as os from 'os';
6+
import * as path from 'path';
27

3-
import { BaseIntegration } from '../';
4-
import { InfoItem, IntegrationName, ProjectPersonalizationDetails } from '../../../definitions';
8+
import { BaseIntegration, IntegrationConfig } from '../';
9+
import {
10+
InfoItem,
11+
IntegrationAddDetails, IntegrationAddHandlers,
12+
IntegrationName,
13+
ProjectIntegration,
14+
ProjectPersonalizationDetails
15+
} from '../../../definitions';
516

617
const debug = Debug('ionic:lib:integrations:cordova');
718

8-
export class Integration extends BaseIntegration {
19+
export class Integration extends BaseIntegration<ProjectIntegration> {
920
readonly name: IntegrationName = 'cordova';
1021
readonly summary = 'Target native iOS and Android with Apache Cordova';
1122
readonly archiveUrl = 'https://d2ql0qc7j8u4b2.cloudfront.net/integration-cordova.tar.gz';
1223

24+
get config(): IntegrationConfig {
25+
return new IntegrationConfig(this.e.project.filePath, { pathPrefix: ['integrations', this.name] });
26+
}
27+
28+
async add(details: IntegrationAddDetails): Promise<void> {
29+
const handlers: IntegrationAddHandlers = {
30+
conflictHandler: async (f, stats) => {
31+
const isDirectory = stats.isDirectory();
32+
const filename = `${path.basename(f)}${isDirectory ? '/' : ''}`;
33+
const type = isDirectory ? 'directory' : 'file';
34+
35+
const confirm = await details.env.prompt({
36+
type: 'confirm',
37+
name: 'confirm',
38+
message: `The ${chalk.cyan(filename)} ${type} exists in project. Overwrite?`,
39+
default: false,
40+
});
41+
42+
return confirm;
43+
},
44+
onFileCreate: f => {
45+
if (!details.quiet) {
46+
details.env.log.msg(`${chalk.green('CREATE')} ${f}`);
47+
}
48+
},
49+
};
50+
const onFileCreate = handlers.onFileCreate ? handlers.onFileCreate : lodash.noop;
51+
const conflictHandler = handlers.conflictHandler ? handlers.conflictHandler : async () => false;
52+
53+
const { createRequest, download } = await import('../../utils/http');
54+
const { tar } = await import('../../utils/archive');
55+
56+
this.e.log.info(`Downloading integration ${chalk.green(this.name)}`);
57+
const tmpdir = path.resolve(os.tmpdir(), `ionic-integration-${this.name}`);
58+
59+
// TODO: etag
60+
61+
if (await pathExists(tmpdir)) {
62+
await remove(tmpdir);
63+
}
64+
65+
await mkdirp(tmpdir);
66+
67+
const ws = tar.extract({ cwd: tmpdir });
68+
const { req } = await createRequest('GET', this.archiveUrl, this.e.config.getHTTPConfig());
69+
await download(req, ws, {});
70+
71+
const contents = await readdirSafe(tmpdir);
72+
const blacklist: string[] = [];
73+
74+
debug(`Integration files downloaded to ${chalk.bold(tmpdir)} (files: ${contents.map(f => chalk.bold(f)).join(', ')})`);
75+
76+
for (const f of contents) {
77+
const projectf = path.resolve(this.e.project.directory, f);
78+
79+
try {
80+
const stats = await stat(projectf);
81+
const overwrite = await conflictHandler(projectf, stats);
82+
83+
if (!overwrite) {
84+
blacklist.push(f);
85+
}
86+
} catch (e) {
87+
if (e.code !== 'ENOENT') {
88+
throw e;
89+
}
90+
}
91+
}
92+
93+
this.e.log.info(`Copying integrations files to project`);
94+
debug(`Blacklist: ${blacklist.map(f => chalk.bold(f)).join(', ')}`);
95+
96+
await mkdirp(details.root);
97+
98+
await copy(tmpdir, details.root, {
99+
filter: f => {
100+
if (f === tmpdir) {
101+
return true;
102+
}
103+
104+
const projectf = f.substring(tmpdir.length + 1);
105+
106+
for (const item of blacklist) {
107+
if (item.slice(-1) === '/' && `${projectf}/` === item) {
108+
return false;
109+
}
110+
111+
if (projectf.startsWith(item)) {
112+
return false;
113+
}
114+
}
115+
116+
onFileCreate(projectf);
117+
118+
return true;
119+
},
120+
});
121+
await super.add(details);
122+
}
123+
13124
async getInfo(): Promise<InfoItem[]> {
14125
const { getAndroidSdkToolsVersion } = await import('./android');
15126

0 commit comments

Comments
 (0)