Skip to content

Commit 4a12b17

Browse files
committed
feat(init): add ionic init command
`ionic init` is a command for initializing an existing project with Ionic by creating an `ionic.config.json` file. Additionally, provide some friendliness around config file errors, and a suggestion for `ionic init`. fixes #3740 resolves #3732
1 parent 13298f1 commit 4a12b17

File tree

11 files changed

+255
-97
lines changed

11 files changed

+255
-97
lines changed

packages/ionic/src/commands/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export class IonicNamespace extends Namespace {
6262
['generate', async () => { const { GenerateCommand } = await import('./generate'); return new GenerateCommand(this); }],
6363
['help', async () => { const { HelpCommand } = await import('./help'); return new HelpCommand(this); }],
6464
['info', async () => { const { InfoCommand } = await import('./info'); return new InfoCommand(this); }],
65+
['init', async () => { const { InitCommand } = await import('./init'); return new InitCommand(this); }],
6566
['ionitron', async () => { const { IonitronCommand } = await import('./ionitron'); return new IonitronCommand(this); }],
6667
['link', async () => { const { LinkCommand } = await import('./link'); return new LinkCommand(this); }],
6768
['login', async () => { const { LoginCommand } = await import('./login'); return new LoginCommand(this); }],

packages/ionic/src/commands/init.ts

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import { CommandGroup, OptionGroup, validators } from '@ionic/cli-framework';
2+
import { prettyPath } from '@ionic/cli-framework/utils/format';
3+
import { slugify } from '@ionic/cli-framework/utils/string';
4+
import chalk from 'chalk';
5+
import * as path from 'path';
6+
7+
import { PROJECT_FILE, PROJECT_TYPES } from '../constants';
8+
import { CommandLineInputs, CommandLineOptions, CommandMetadata, IProject, ProjectType } from '../definitions';
9+
import { Command } from '../lib/command';
10+
import { FatalException } from '../lib/errors';
11+
import { ProjectDetails, createProjectFromDetails, prettyProjectName } from '../lib/project';
12+
13+
export class InitCommand extends Command {
14+
async getMetadata(): Promise<CommandMetadata> {
15+
return {
16+
name: 'init',
17+
type: 'global',
18+
summary: 'Initialize existing projects with Ionic',
19+
description: `
20+
This command will initialize the current directory with an ${chalk.bold(PROJECT_FILE)} file.
21+
22+
${chalk.green('ionic init')} will prompt for a project name and then proceed to determine the type of your project. You can specify the ${chalk.green('name')} argument and ${chalk.green('--type')} option to provide these values via command-line.
23+
`,
24+
exampleCommands: [
25+
'',
26+
'"My App"',
27+
'"My App" --type=angular',
28+
],
29+
inputs: [
30+
{
31+
name: 'name',
32+
summary: `The name of your project (e.g. ${chalk.green('myApp')}, ${chalk.green('"My App"')})`,
33+
validators: [validators.required],
34+
},
35+
],
36+
options: [
37+
{
38+
name: 'type',
39+
summary: `Type of project (e.g. ${PROJECT_TYPES.map(type => chalk.green(type)).join(', ')})`,
40+
},
41+
{
42+
name: 'force',
43+
summary: 'Initialize even if a project already exists',
44+
type: Boolean,
45+
aliases: ['f'],
46+
default: false,
47+
},
48+
{
49+
name: 'project-id',
50+
summary: 'Specify a slug for your app',
51+
groups: [OptionGroup.Advanced],
52+
},
53+
],
54+
groups: [CommandGroup.Beta],
55+
};
56+
}
57+
58+
async preRun(inputs: CommandLineInputs, options: CommandLineOptions): Promise<void> {
59+
const force = options['force'] ? true : false;
60+
61+
if (this.project && this.project.details.context === 'app' && !force) {
62+
// TODO: check for existing project config in multi-app
63+
throw new FatalException(
64+
`Existing Ionic project file found: ${chalk.bold(prettyPath(this.project.filePath))}\n` +
65+
`You can re-initialize your project using the ${chalk.green('--force')} option.`
66+
);
67+
}
68+
69+
if (!inputs[0]) {
70+
const name = await this.env.prompt({
71+
type: 'input',
72+
name: 'name',
73+
message: 'Project name:',
74+
validate: v => validators.required(v),
75+
});
76+
77+
inputs[0] = name;
78+
}
79+
80+
if (!options['type']) {
81+
const details = new ProjectDetails({ rootDirectory: this.env.ctx.execPath, e: this.env });
82+
options['type'] = await details.getTypeFromDetection();
83+
}
84+
85+
if (!options['type']) {
86+
if (this.env.flags.interactive) {
87+
this.env.log.warn(
88+
`Could not determine project type.\n` +
89+
`Please choose a project type from the list.`
90+
);
91+
this.env.log.nl();
92+
}
93+
94+
const type = await this.env.prompt({
95+
type: 'list',
96+
name: 'type',
97+
message: 'Project type:',
98+
choices: PROJECT_TYPES.map(t => ({
99+
name: `${prettyProjectName(t)} (${chalk.green(t)})`,
100+
value: t,
101+
})),
102+
});
103+
104+
options['type'] = type;
105+
}
106+
}
107+
108+
async run(inputs: CommandLineInputs, options: CommandLineOptions): Promise<void> {
109+
const name = inputs[0].trim();
110+
const type = options['type'] ? String(options['type']) as ProjectType : undefined;
111+
const projectId = options['project-id'] ? String(options['project-id']) : slugify(name); // TODO validate --project-id
112+
113+
if (!type) {
114+
throw new FatalException(
115+
`Could not determine project type.\n` +
116+
`Please specify ${chalk.green('--type')}. See ${chalk.green('ionic init --help')} for details.`
117+
);
118+
}
119+
120+
let project: IProject | undefined;
121+
122+
if (this.project && this.project.details.context === 'multiapp') {
123+
project = await createProjectFromDetails({ context: 'multiapp', configPath: path.resolve(this.project.rootDirectory, PROJECT_FILE), id: projectId, type, errors: [] }, this.env);
124+
project.config.set('root', path.relative(this.project.rootDirectory, this.env.ctx.execPath));
125+
} else {
126+
project = await createProjectFromDetails({ context: 'app', configPath: path.resolve(this.env.ctx.execPath, PROJECT_FILE), type, errors: [] }, this.env);
127+
}
128+
129+
project.config.set('name', name);
130+
project.config.set('type', type);
131+
132+
this.env.log.ok('Your Ionic project has been initialized!');
133+
}
134+
}

packages/ionic/src/commands/start.ts

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { OptionGroup, validators } from '@ionic/cli-framework';
22
import { columnar, prettyPath } from '@ionic/cli-framework/utils/format';
3-
import { isValidPackageName } from '@ionic/cli-framework/utils/node';
43
import { isValidURL, slugify } from '@ionic/cli-framework/utils/string';
54
import { mkdir, pathExists, removeDirectory, unlink } from '@ionic/utils-fs';
65
import chalk from 'chalk';
@@ -13,7 +12,7 @@ import { CommandInstanceInfo, CommandLineInputs, CommandLineOptions, CommandMeta
1312
import { Command } from '../lib/command';
1413
import { FatalException } from '../lib/errors';
1514
import { runCommand } from '../lib/executor';
16-
import { createProjectFromDirectory, createProjectFromType } from '../lib/project';
15+
import { createProjectFromDetails, createProjectFromDirectory, isValidProjectId } from '../lib/project';
1716
import { prependNodeModulesBinToPath } from '../lib/shell';
1817
import { emoji } from '../lib/utils/emoji';
1918

@@ -199,7 +198,7 @@ ${chalk.cyan('[1]')}: ${chalk.bold('https://ionicframework.com/docs/cli/starters
199198
options['git'] = true;
200199
}
201200

202-
if (this.project && !this.project.name) {
201+
if (this.project && this.project.details.context === 'app') {
203202
const confirm = await this.env.prompt({
204203
type: 'confirm',
205204
name: 'confirm',
@@ -304,7 +303,7 @@ ${chalk.cyan('[1]')}: ${chalk.bold('https://ionicframework.com/docs/cli/starters
304303
if (projectId) {
305304
await this.validateProjectId(projectId);
306305
} else {
307-
projectId = options['project-id'] = this.isValidProjectId(inputs[0]) ? inputs[0] : slugify(inputs[0]);
306+
projectId = options['project-id'] = isValidProjectId(inputs[0]) ? inputs[0] : slugify(inputs[0]);
308307
}
309308

310309
const projectDir = path.resolve(projectId);
@@ -431,11 +430,11 @@ ${chalk.cyan('[1]')}: ${chalk.bold('https://ionicframework.com/docs/cli/starters
431430

432431
let project: IProject | undefined;
433432

434-
if (this.project && this.project.name && !this.schema.cloned) {
433+
if (this.project && this.project.details.context === 'multiapp' && !this.schema.cloned) {
435434
// We're in a multi-app setup, so the new config file isn't wanted.
436435
await unlink(path.resolve(projectDir, 'ionic.config.json'));
437436

438-
project = await createProjectFromType(path.resolve(this.project.rootDirectory, PROJECT_FILE), projectId, this.env, this.schema.type);
437+
project = await createProjectFromDetails({ context: 'multiapp', configPath: path.resolve(this.project.rootDirectory, PROJECT_FILE), id: projectId, type: this.schema.type, errors: [] }, this.env);
439438
project.config.set('type', this.schema.type);
440439
project.config.set('root', path.relative(this.project.rootDirectory, projectDir));
441440
} else {
@@ -634,18 +633,14 @@ ${chalk.cyan('[1]')}: ${chalk.bold('https://ionicframework.com/docs/cli/starters
634633
}
635634

636635
async validateProjectId(projectId: string) {
637-
if (!this.isValidProjectId(projectId)) {
636+
if (!isValidProjectId(projectId)) {
638637
throw new FatalException(
639638
`${chalk.green(projectId)} is not a valid package or directory name.\n` +
640639
`Please choose a different ${chalk.green('--project-id')}. Alphanumeric characters are always safe.`
641640
);
642641
}
643642
}
644643

645-
isValidProjectId(projectId: string): boolean {
646-
return projectId !== '.' && isValidPackageName(projectId) && projectId === path.basename(projectId);
647-
}
648-
649644
async loadManifest(manifestPath: string): Promise<StarterManifest | undefined> {
650645
const { readStarterManifest } = await import('../lib/start');
651646

packages/ionic/src/definitions.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import * as ζsuperagent from 'superagent';
77

88
import * as ζbuild from './lib/build';
99
import * as ζgenerate from './lib/generate';
10+
import * as ζproject from './lib/project';
1011
import * as ζserve from './lib/serve';
1112

1213
export { CommandLineInputs, CommandLineOptions, CommandMetadataInput, NamespaceMetadata, PackageJson } from '@ionic/cli-framework';
@@ -256,9 +257,9 @@ export interface IProject {
256257
readonly rootDirectory: string;
257258
readonly directory: string;
258259
readonly filePath: string;
259-
readonly name?: string;
260260
readonly type: ProjectType;
261261
readonly config: ζframework.BaseConfig<IProjectConfig>;
262+
readonly details: ζproject.ProjectDetailsResult;
262263

263264
getDocsUrl(): Promise<string>;
264265
getSourceDir(sourceRoot?: string): Promise<string>;

packages/ionic/src/lib/build.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -235,8 +235,8 @@ export async function build(deps: BuildRunnerDeps, inputs: CommandLineInputs, op
235235
try {
236236
const runner = await deps.project.requireBuildRunner();
237237

238-
if (deps.project.name) {
239-
options['project'] = deps.project.name;
238+
if (deps.project.details.context === 'multiapp') {
239+
options['project'] = deps.project.details.id;
240240
}
241241

242242
const opts = runner.createOptionsFromCommandLine(inputs, options);

packages/ionic/src/lib/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ export async function generateIonicEnvironment(ctx: IonicContext, pargv: string[
9191
log.warn(`${chalk.green('--yarn')} / ${chalk.green('--no-yarn')} has been removed. Use ${chalk.green(`ionic config set -g npmClient ${argv['yarn'] ? 'yarn' : 'npm'}`)}.`);
9292
}
9393

94-
const project = projectDir ? await createProjectFromDirectory(projectDir, argv, deps, { logErrors: argv._[0] !== 'start' }) : undefined;
94+
const project = projectDir ? await createProjectFromDirectory(projectDir, argv, deps, { logErrors: !['start', 'init'].includes(argv._[0]) }) : undefined;
9595

9696
if (project) {
9797
shell.alterPath = p => prependNodeModulesBinToPath(project.directory, p);

packages/ionic/src/lib/project/angular/__tests__/index.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,21 @@ describe('ionic', () => {
77

88
describe('AngularProject', () => {
99

10+
let p: AngularProject;
11+
12+
beforeEach(() => {
13+
p = new AngularProject({ context: 'app', configPath: '/path/to/proj/file' } as any, {} as any);
14+
jest.spyOn(p, 'config', 'get').mockImplementation(() => ({ get: () => undefined }));
15+
});
16+
1017
it('should set directory attribute', async () => {
11-
const p = new AngularProject('/path/to/proj/file', undefined, {} as any);
18+
debugger;
1219
expect(p.directory).toEqual(path.resolve('/path/to/proj'));
1320
});
1421

1522
describe('getSourceDir', () => {
1623

1724
it('should default to src', async () => {
18-
const p = new AngularProject('/path/to/proj/file', undefined, {} as any);
1925
const result = await p.getSourceDir();
2026
expect(result).toEqual(path.resolve('/path/to/proj/src'));
2127
});

0 commit comments

Comments
 (0)