Skip to content

Commit

Permalink
feat(@ionic/cli): proof of concept for running multiple apps in a sin…
Browse files Browse the repository at this point in the history
…gle workspace. Related ionic-team#3281

Adding `--project` support for almost every Ionic command.

Create an `ionic.json` config in project root. This closely mirrors `angular.json` but doesn't depend on it.
```
{
  "defaultProject": "app",
  "projects": {
    "app": {
      "name": "Ionic App",
      "type": "angular",
      "integrations": {
        "cordova": {
          "root": "cordova"
        }
      },
      "hooks": {}
    }
  }
}
```

Example commands
```
# With project name
ionic cordova platform add android --project app

# With defaultProject
ionic cordova platform add android

# Other commands
ionic cordova resources --project app
ionic cordova plugin add cordova-plugin-splashscreen --project app
ionic cordova build --project app
ionic cordova run --project app
ionic cordova emulate --project app
ionic cordova requirements --project app
ionic cordova prepare --project app
```
  • Loading branch information
stupidawesome committed Jun 4, 2018
1 parent c53adf1 commit 51bd0eb
Show file tree
Hide file tree
Showing 26 changed files with 212 additions and 93 deletions.
17 changes: 17 additions & 0 deletions packages/@ionic/cli-framework/src/utils/fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,3 +250,20 @@ export async function findBaseDirectory(dir: string, file: string): Promise<stri
}
}
}

/**
* Find the base directory based on the path given and a marker file to look for.
*/
export async function findProjectDirectory(dir: string, file: string): Promise<string | undefined> {
if (!dir || !file) {
return;
}

for (const d of compilePaths(dir)) {
const results = await fsReadDir(d);

if (results.includes(file)) {
return d;
}
}
}
3 changes: 2 additions & 1 deletion packages/@ionic/cli-utils/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ import { ProjectType } from './definitions';

export const ASSETS_DIRECTORY = path.resolve(__dirname, 'assets');

export const PROJECT_FILE = 'ionic.config.json';
export const PROJECT_FILE = 'ionic.json';
export const DEPRECATED_PROJECT_FILE = 'ionic.config.json';
export const PROJECT_TYPES: ProjectType[] = ['angular', 'ionic-angular', 'ionic1', 'custom'];
17 changes: 15 additions & 2 deletions packages/@ionic/cli-utils/src/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,15 +79,17 @@ export type HookFn = (ctx: HookContext) => Promise<void>;
export type IntegrationName = 'capacitor' | 'cordova';

export interface ProjectIntegration {
enabled?: boolean;
enabled: boolean;
root: string;
outputPath: string;
}

export interface ProjectIntegrations {
cordova?: ProjectIntegration;
capacitor?: ProjectIntegration;
}

export interface ProjectFile {
export interface ProjectConfig {
name: string;
pro_id?: string;

Expand All @@ -98,6 +100,8 @@ export interface ProjectFile {
key?: string;
cert?: string;
};

type: ProjectType
}

export interface Response<T extends object> extends APIResponseSuccess {
Expand Down Expand Up @@ -231,13 +235,15 @@ export interface ProjectPersonalizationDetails {

export interface IProject extends IBaseConfig<ProjectFile> {
type: ProjectType | undefined;
name: string;

getDocsUrl(): Promise<string>;
getSourceDir(sourceRoot?: string): Promise<string>;
getDistDir(): Promise<string>;
getInfo(): Promise<InfoItem[]>;
detected(): Promise<boolean>;
createIntegration(name: IntegrationName): Promise<IIntegration>;
getIntegration(name: IntegrationName): Promise<ProjectIntegration>;
requireProId(): Promise<string>;
getPackageJson(pkgName?: string): Promise<framework.PackageJson | undefined>;
requirePackageJson(): Promise<framework.PackageJson>;
Expand Down Expand Up @@ -776,3 +782,10 @@ export interface IPCMessage {
type: 'telemetry';
data: { command: string; args: string[]; };
}

export interface ProjectFile {
defaultProject: string;
projects: {
[key: string]: ProjectConfig;
}
}
44 changes: 35 additions & 9 deletions packages/@ionic/cli-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import { Logger } from './lib/utils/logger';
import { ProSession } from './lib/session';
import { Shell } from './lib/shell';
import { createOnFallback } from './lib/prompts';
import { fsReadJsonFile } from "@ionic/cli-framework/utils/fs";
import { FatalException } from "./lib/errors";

export * from './definitions';
export * from './constants';
Expand All @@ -33,18 +35,37 @@ const debug = Debug('ionic:cli-utils');

let _pkg: PackageJson | undefined;

export async function getProject(projectDir: string | undefined, deps: ProjectDeps): Promise<IProject> {
if (!projectDir) {
return new OutsideProject('', PROJECT_FILE);
export async function getProject(projectDir: string, projectName: string, deps: ProjectDeps): Promise<IProject> {
const projectFilePath = path.resolve(projectDir, PROJECT_FILE);
let projectFile: { [key: string]: any; } | undefined;
let projectConfig: any;

try {
projectFile = await fsReadJsonFile(projectFilePath);
} catch (e) {
debug('Attempted to load project config %s but got error: %O', projectFilePath, e);
}

if (projectFile) {
projectName = projectName === 'default' ? projectFile.defaultProject : projectName;
projectConfig = projectFile.projects[projectName];

debug(`Project name: ${chalk.bold(projectName)}`);

if (!projectConfig) {
throw projectFile.defaultProject
? new FatalException(`${chalk.bold(`projects.${projectName}`)} was not found in ${chalk.bold('ionic.json')}.`)
: new FatalException(`Please set a ${chalk.bold('defaultProject')} in ${chalk.bold('ionic.json')} or specify the project using ${chalk.bold('--project')}`);
}
}

const type = await Project.determineType(projectDir, deps);
const type = await Project.determineType(projectDir, projectName, projectConfig, deps);

if (!type) {
return new OutsideProject('', PROJECT_FILE);
return new OutsideProject('', PROJECT_FILE, projectName);
}

return Project.createFromProjectType(projectDir, PROJECT_FILE, deps, type);
return Project.createFromProjectType(projectDir, PROJECT_FILE, projectName, deps, type);
}

async function loadPackageJson(): Promise<PackageJson> {
Expand All @@ -58,8 +79,9 @@ async function loadPackageJson(): Promise<PackageJson> {
export async function generateIonicEnvironment(ctx: IonicContext, pargv: string[], env: { [key: string]: string; }): Promise<IonicEnvironment> {
process.chdir(ctx.execPath);

const argv = parseArgs(pargv, { boolean: true, string: '_' });
const config = new Config(env['IONIC_CONFIG_DIRECTORY'] || DEFAULT_CONFIG_DIRECTORY, CONFIG_FILE);
const argv = parseArgs(pargv, { boolean: ['quiet', 'interactive', 'confirm'], string: ['_', 'project'] });
const projectName = argv['project'] || 'default';
const config = new Config(env['IONIC_CONFIG_DIRECTORY'] || DEFAULT_CONFIG_DIRECTORY, CONFIG_FILE, projectName);
const flags = gatherFlags(argv);

const configData = await config.load();
Expand All @@ -84,6 +106,10 @@ export async function generateIonicEnvironment(ctx: IonicContext, pargv: string[
const projectDir = await findBaseDirectory(ctx.execPath, PROJECT_FILE);
const proxyVars = PROXY_ENVIRONMENT_VARIABLES.map(e => [e, env[e]]).filter(([e, v]) => !!v);

if (!projectDir) {
throw new FatalException(`Could not find ${chalk.green(PROJECT_FILE)}`)
}

const getInfo = async () => {
const pkg = await loadPackageJson();
const osName = await import('os-name');
Expand All @@ -107,7 +133,7 @@ export async function generateIonicEnvironment(ctx: IonicContext, pargv: string[
};

const shell = new Shell({ log, projectDir });
const project = await getProject(projectDir, { config, log, shell, tasks });
const project = await getProject(projectDir, projectName, { config, log, shell, tasks });
const client = new Client(config);
const session = new ProSession({ config, client, project });

Expand Down
6 changes: 3 additions & 3 deletions packages/@ionic/cli-utils/src/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ import {
import { FatalException } from './errors';

export abstract class BaseConfig<T> implements IBaseConfig<T> {
directory: string;
filePath: string;
readonly directory: string;
readonly filePath: string;
protected configFile?: T;
protected originalConfigFile?: { [key: string]: any };

constructor(directory: string, public fileName: string) {
constructor(directory: string, public readonly fileName: string, public readonly name: string) {
this.directory = directory ? path.resolve(directory) : ''; // TODO: better way to check if in project
this.filePath = path.resolve(this.directory, fileName);
}
Expand Down
12 changes: 8 additions & 4 deletions packages/@ionic/cli-utils/src/lib/doctor/ailments/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,8 @@ class GitConfigInvalid extends Ailment {
}

const p = await this.project.load();
const proId = p.pro_id;
const projectConfig = p.projects[this.project.name];
const proId = projectConfig.pro_id;

if (!proId) {
return false;
Expand Down Expand Up @@ -359,12 +360,14 @@ class UnsavedCordovaPlatforms extends Ailment {

async detected() {
const project = await this.project.load();
const projectConfig = project.projects[this.project.name];

if (!project.integrations.cordova) {
if (!projectConfig.integrations.cordova) {
return false;
}

const platforms = await getPlatforms(this.project.directory);
const cordovaRoot = path.resolve(this.project.directory, projectConfig.integrations.cordova.root);
const platforms = await getPlatforms(cordovaRoot);
const conf = await loadConfigXml({ project: this.project });
const engines = conf.getPlatformEngines();
const engineNames = new Set([...engines.map(e => e.name)]);
Expand Down Expand Up @@ -393,8 +396,9 @@ class DefaultCordovaBundleIdUsed extends Ailment {

async detected() {
const project = await this.project.load();
const projectConfig = project.projects[this.project.name];

if (!project.integrations.cordova) {
if (!projectConfig.integrations.cordova) {
return false;
}

Expand Down
3 changes: 2 additions & 1 deletion packages/@ionic/cli-utils/src/lib/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export abstract class Hook {
}

const project = await this.project.load();
const projectConfig = project.projects[this.project.name];
const pkg = await this.project.requirePackageJson();
const config = await this.config.load();
const { npmClient } = config;
Expand All @@ -57,7 +58,7 @@ export abstract class Hook {
await this.shell.run(pkgManager, pkgArgs, {});
}

const hooks = project.hooks ? conform(project.hooks[this.name]) : [];
const hooks = projectConfig.hooks ? conform(projectConfig.hooks[this.name]) : [];

for (const h of hooks) {
const p = path.resolve(this.project.directory, h);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ export class Integration extends BaseIntegration {

async add(options?: IIntegrationAddOptions): Promise<void> {
const project = await this.project.load();
const projectConfig = project.projects[this.project.name];

await this.installCapacitorCore();
await this.installCapacitorCLI();

await this.shell.run('capacitor', ['init', project.name, 'io.ionic.starter'], {});
await this.shell.run('capacitor', ['init', projectConfig.name, 'io.ionic.starter'], {});

await super.add(options);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,12 @@ export class ConfigXml {
}

export async function loadConfigXml({ project }: { project: IProject }): Promise<ConfigXml> {
const filePath = path.resolve(project.directory, 'config.xml');
const config = await project.getIntegration('cordova');
const directory = path.resolve(project.directory, config.root);

process.chdir(directory);

const filePath = path.resolve(directory, 'config.xml');
debug(`Using config.xml: ${filePath}`);

try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ export async function getPlatforms(projectDir: string): Promise<string[]> {
return dirContents.filter(f => f && f !== 'platforms.json' && !f.startsWith('.'));
}

export async function installPlatform(env: IonicEnvironment, platform: string, extraArgs: string[] = []): Promise<void> {
export async function installPlatform(env: IonicEnvironment, platform: string, cwd = process.cwd(), extraArgs: string[] = []): Promise<void> {
try {
await env.shell.run('cordova', ['platform', 'add', platform, '--save', ...extraArgs], { fatalOnError: false, showError: false });
await env.shell.run('cordova', ['platform', 'add', platform, '--save', ...extraArgs], { fatalOnError: false, showError: false, cwd });
} catch (e) {
const s = String(e);

Expand Down
3 changes: 1 addition & 2 deletions packages/@ionic/cli-utils/src/lib/project/angular/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ export const NG_BUILD_OPTIONS = [
summary: 'The name of the project',
type: String,
groups: [OptionGroup.Advanced],
default: 'app',
hint: chalk.dim('[ng]'),
},
{
Expand Down Expand Up @@ -60,7 +59,7 @@ ${chalk.cyan('[1]')}: ${chalk.bold('https://github.com/angular/angular-cli/wiki/
createOptionsFromCommandLine(inputs: CommandLineInputs, options: CommandLineOptions): AngularBuildOptions {
const baseOptions = super.createBaseOptionsFromCommandLine(inputs, options);
const prod = options['prod'] ? Boolean(options['prod']) : undefined;
const project = options['project'] ? String(options['project']) : 'app';
const project = this.project.name;
const configuration = options['configuration'] ? String(options['configuration']) : (prod ? 'production' : undefined);

return {
Expand Down
3 changes: 1 addition & 2 deletions packages/@ionic/cli-utils/src/lib/project/angular/serve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ const NG_SERVE_OPTIONS = [
summary: 'The name of the project',
type: String,
groups: [OptionGroup.Advanced],
default: 'app',
hint: chalk.dim('[ng]'),
},
{
Expand Down Expand Up @@ -59,7 +58,7 @@ ${chalk.cyan('[1]')}: ${chalk.bold('https://github.com/angular/angular-cli/wiki/
createOptionsFromCommandLine(inputs: CommandLineInputs, options: CommandLineOptions): AngularServeOptions {
const baseOptions = super.createOptionsFromCommandLine(inputs, options);
const prod = options['prod'] ? Boolean(options['prod']) : undefined;
const project = options['project'] ? String(options['project']) : 'app';
const project = this.project.name;
const configuration = options['configuration'] ? String(options['configuration']) : undefined;

return {
Expand Down
Loading

0 comments on commit 51bd0eb

Please sign in to comment.