Skip to content

Commit ef852fa

Browse files
jthoms1imhoffd
authored andcommitted
feat: Add React project type (#3936)
1 parent 0b7342e commit ef852fa

File tree

16 files changed

+568
-11
lines changed

16 files changed

+568
-11
lines changed

packages/ionic/src/commands/capacitor/run.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Footnote, MetadataGroup, validators } from '@ionic/cli-framework';
22
import { onBeforeExit, sleepForever } from '@ionic/utils-process';
33
import chalk from 'chalk';
4+
import * as lodash from 'lodash';
45
import * as path from 'path';
56

67
import { CommandInstanceInfo, CommandLineInputs, CommandLineOptions, CommandMetadata, CommandMetadataOption, CommandPreRun } from '../../definitions';
@@ -24,7 +25,7 @@ export class RunCommand extends CapacitorCommand implements CommandPreRun {
2425
'ios --livereload-url=http://localhost:8100',
2526
].sort();
2627

27-
const options: CommandMetadataOption[] = [
28+
let options: CommandMetadataOption[] = [
2829
// Build Options
2930
{
3031
name: 'build',
@@ -68,7 +69,10 @@ export class RunCommand extends CapacitorCommand implements CommandPreRun {
6869
const libmetadata = await serveRunner.getCommandMetadata();
6970
const existingOpts = options.map(o => o.name);
7071
groups = libmetadata.groups || [];
71-
options.push(...(libmetadata.options || []).filter(o => !existingOpts.includes(o.name)).map(o => ({ ...o, hint: `${o.hint ? `${o.hint} ` : ''}${weak('(--livereload)')}` })));
72+
const runnerOpts = (libmetadata.options || [])
73+
.filter(o => !existingOpts.includes(o.name))
74+
.map(o => ({ ...o, hint: `${o.hint ? `${o.hint} ` : ''}${weak('(--livereload)')}` }));
75+
options = lodash.uniqWith([...runnerOpts, ...options], (optionA, optionB) => optionA.name === optionB.name);
7276
footnotes.push(...libmetadata.footnotes || []);
7377
}
7478

packages/ionic/src/commands/cordova/run.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Footnote, MetadataGroup } from '@ionic/cli-framework';
22
import { onBeforeExit, sleepForever } from '@ionic/utils-process';
33
import * as Debug from 'debug';
4+
import * as lodash from 'lodash';
45
import * as url from 'url';
56

67
import { CommandInstanceInfo, CommandLineInputs, CommandLineOptions, CommandMetadata, CommandMetadataOption, CommandPreRun, IShellRunOptions } from '../../definitions';
@@ -54,7 +55,7 @@ export class RunCommand extends CordovaCommand implements CommandPreRun {
5455
'ios --livereload-url=http://localhost:8100',
5556
].sort();
5657

57-
const options: CommandMetadataOption[] = [
58+
let options: CommandMetadataOption[] = [
5859
{
5960
name: 'list',
6061
summary: 'List all available targets',
@@ -110,9 +111,10 @@ export class RunCommand extends CordovaCommand implements CommandPreRun {
110111
const libmetadata = await serveRunner.getCommandMetadata();
111112
const existingOpts = options.map(o => o.name);
112113
groups = libmetadata.groups || [];
113-
options.push(...(libmetadata.options || [])
114+
const runnerOpts = (libmetadata.options || [])
114115
.filter(o => !existingOpts.includes(o.name) && o.groups && o.groups.includes('cordova'))
115-
.map(o => ({ ...o, hint: `${o.hint ? `${o.hint} ` : ''}${weak('(--livereload)')}` })));
116+
.map(o => ({ ...o, hint: `${o.hint ? `${o.hint} ` : ''}${weak('(--livereload)')}` }));
117+
options = lodash.uniqWith([...runnerOpts, ...options], (optionA, optionB) => optionA.name === optionB.name);
116118
footnotes.push(...libmetadata.footnotes || []);
117119
}
118120

packages/ionic/src/commands/serve.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export class ServeCommand extends Command implements CommandPreRun {
1212
async getMetadata(): Promise<CommandMetadata> {
1313
let groups: string[] = [];
1414

15-
const options: CommandMetadataOption[] = [
15+
let options: CommandMetadataOption[] = [
1616
...COMMON_SERVE_COMMAND_OPTIONS,
1717
{
1818
name: 'lab-host',
@@ -71,7 +71,7 @@ Try the ${input('--lab')} option to see multiple platforms at once.`;
7171
if (runner) {
7272
const libmetadata = await runner.getCommandMetadata();
7373
groups = libmetadata.groups || [];
74-
options.push(...libmetadata.options || []);
74+
options = lodash.uniqWith([...libmetadata.options || [], ...options], (optionA, optionB) => optionA.name === optionB.name);
7575
description += `\n\n${(libmetadata.description || '').trim()}`;
7676
footnotes.push(...libmetadata.footnotes || []);
7777
exampleCommands.push(...libmetadata.exampleCommands || []);

packages/ionic/src/commands/start.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,8 @@ Use the ${input('--type')} option to start projects using older versions of Ioni
163163
}
164164
}
165165

166+
// TODO: currently defaults to angular as the project type if a type is not provided
167+
// we might want to make them select a type instead
166168
const projectType = options['type'] ? String(options['type']) : 'angular';
167169
const appflowId = options['id'] ? String(options['id']) : undefined;
168170

packages/ionic/src/constants.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@ import { ProjectType } from './definitions';
55
export const ASSETS_DIRECTORY = path.resolve(__dirname, 'assets');
66

77
export const PROJECT_FILE = 'ionic.config.json';
8-
export const PROJECT_TYPES: ProjectType[] = ['angular', 'ionic-angular', 'ionic1', 'custom'];
8+
export const PROJECT_TYPES: ProjectType[] = ['angular', 'ionic-angular', 'ionic1', 'custom', 'react', 'vue'];

packages/ionic/src/definitions.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export interface Runner<T extends object, U> {
5858
run(options: T): Promise<U>;
5959
}
6060

61-
export type ProjectType = 'angular' | 'ionic-angular' | 'ionic1' | 'custom' | 'bare';
61+
export type ProjectType = 'angular' | 'ionic-angular' | 'ionic1' | 'custom' | 'bare' | 'react' | 'vue';
6262
export type HookName = 'build:before' | 'build:after' | 'serve:before' | 'serve:after';
6363

6464
export interface BaseHookContext {
@@ -547,6 +547,19 @@ export interface AngularBuildOptions extends BuildOptions<'angular'> {
547547
cordovaAssets?: boolean;
548548
}
549549

550+
export interface ReactBuildOptions extends BuildOptions<'react'> {
551+
publicUrl?: string;
552+
ci?: boolean;
553+
sourceMap?: boolean;
554+
inlineRuntimeChunk?: boolean;
555+
}
556+
557+
export interface VueBuildOptions extends BuildOptions<'vue'> {
558+
configuration?: string;
559+
sourcemaps?: boolean;
560+
cordovaAssets?: boolean;
561+
}
562+
550563
export interface IonicAngularBuildOptions extends BuildOptions<'ionic-angular'> {
551564
prod: boolean;
552565
sourcemaps?: boolean;
@@ -605,6 +618,18 @@ export interface AngularServeOptions extends ServeOptions {
605618
sourcemaps?: boolean;
606619
}
607620

621+
export interface ReactServeOptions extends ServeOptions {
622+
https?: boolean;
623+
ci?: boolean;
624+
reactEditor?: string;
625+
}
626+
627+
export interface VueServeOptions extends ServeOptions {
628+
ssl?: boolean;
629+
configuration?: string;
630+
sourcemaps?: boolean;
631+
}
632+
608633
export interface IonicAngularServeOptions extends ServeOptions {
609634
sourcemaps?: boolean;
610635
consolelogs: boolean;

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ export class Integration extends BaseIntegration<ProjectIntegration> {
2828
let packageId = 'io.ionic.starter';
2929
const options: string[] = [];
3030

31+
if (this.e.project.type === 'react') {
32+
options.push('--web-dir', 'public');
33+
}
34+
3135
if (details.enableArgs) {
3236
const parsedArgs = parseArgs(details.enableArgs);
3337

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,12 @@ export async function createProjectFromDetails(details: ProjectDetailsResult, de
292292
case 'angular':
293293
const { AngularProject } = await import('./angular');
294294
return new AngularProject(details, deps);
295+
case 'react':
296+
const { ReactProject } = await import('./react');
297+
return new ReactProject(details, deps);
298+
case 'vue':
299+
const { VueProject } = await import('./vue');
300+
return new VueProject(details, deps);
295301
case 'ionic-angular':
296302
const { IonicAngularProject } = await import('./ionic-angular');
297303
return new IonicAngularProject(details, deps);
@@ -624,6 +630,10 @@ export function prettyProjectName(type?: string): string {
624630

625631
if (type === 'angular') {
626632
return '@ionic/angular';
633+
} else if (type === 'react') {
634+
return '@ionic/react';
635+
} else if (type === 'vue') {
636+
return '@ionic/vue';
627637
} else if (type === 'ionic-angular') {
628638
return 'Ionic 2/3';
629639
} else if (type === 'ionic1') {
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { MetadataGroup } from '@ionic/cli-framework';
2+
3+
import { CommandLineInputs, CommandLineOptions, CommandMetadata, ReactBuildOptions } from '../../../definitions';
4+
import { BUILD_SCRIPT, BuildCLI, BuildRunner, BuildRunnerDeps } from '../../build';
5+
import { input } from '../../color';
6+
7+
import { ReactProject } from './';
8+
9+
export interface ReactBuildRunnerDeps extends BuildRunnerDeps {
10+
readonly project: ReactProject;
11+
}
12+
13+
export class ReactBuildRunner extends BuildRunner<ReactBuildOptions> {
14+
constructor(protected readonly e: ReactBuildRunnerDeps) {
15+
super();
16+
}
17+
18+
async getCommandMetadata(): Promise<Partial<CommandMetadata>> {
19+
return {
20+
description: `
21+
${input('ionic build')} uses React Scripts. See the ${input('create-react-app')} docs[^cra-build-docs] for explanations. This command interprets the arguments to environment variables supported by React Scripts.
22+
`,
23+
footnotes: [
24+
{
25+
id: 'cra-build-docs',
26+
url: 'https://facebook.github.io/create-react-app/docs/advanced-configuration',
27+
},
28+
],
29+
options: [
30+
{
31+
name: 'public-url',
32+
summary: `You may use this variable to force assets to be referenced verbatim to the url you provide (hostname included). `,
33+
type: String,
34+
},
35+
{
36+
name: 'ci',
37+
summary: `Treat all warnings as build failures. Also makes the test runner non-watching.`,
38+
type: Boolean,
39+
},
40+
{
41+
name: 'source-map',
42+
summary: `When set to false, source maps are not generated.`,
43+
type: Boolean,
44+
},
45+
{
46+
name: 'inline-runtime-chunk',
47+
summary: `By default a runtime script is included in index.html. When set to false, the script will not be embedded and will be imported as usual. This is normally required when dealing with CSP.`,
48+
type: Boolean,
49+
},
50+
],
51+
groups: [MetadataGroup.BETA],
52+
};
53+
}
54+
55+
createOptionsFromCommandLine(inputs: CommandLineInputs, options: CommandLineOptions): ReactBuildOptions {
56+
const baseOptions = super.createBaseOptionsFromCommandLine(inputs, options);
57+
const publicUrl = options['public-url'] ? String(options['public-url']) : undefined;
58+
const ci = options['ci'] ? Boolean(options['ci']) : undefined;
59+
const sourceMap = options['source-map'] ? Boolean(options['source-map']) : undefined;
60+
const inlineRuntimeChunk = options['inline-runtime-check'] ? Boolean(options['inline-runtime-check']) : undefined;
61+
62+
return {
63+
...baseOptions,
64+
type: 'react',
65+
publicUrl,
66+
ci,
67+
sourceMap,
68+
inlineRuntimeChunk,
69+
};
70+
}
71+
72+
async buildProject(options: ReactBuildOptions): Promise<void> {
73+
const reactScripts = new ReactBuildCLI(this.e);
74+
await reactScripts.build(options);
75+
}
76+
}
77+
78+
export class ReactBuildCLI extends BuildCLI<ReactBuildOptions> {
79+
readonly name = 'React Scripts';
80+
readonly pkg = 'react-scripts';
81+
readonly program = 'react-scripts';
82+
readonly prefix = 'react-scripts';
83+
readonly script = BUILD_SCRIPT;
84+
85+
protected async buildArgs(options: ReactBuildOptions): Promise<string[]> {
86+
const { pkgManagerArgs } = await import('../../utils/npm');
87+
88+
if (this.resolvedProgram === this.program) {
89+
return ['build'];
90+
} else {
91+
const [ , ...pkgArgs ] = await pkgManagerArgs(this.e.config.get('npmClient'), { command: 'run', script: this.script });
92+
return pkgArgs;
93+
}
94+
}
95+
96+
protected async buildEnvVars(options: ReactBuildOptions): Promise<NodeJS.ProcessEnv> {
97+
const envVars: NodeJS.ProcessEnv = {};
98+
99+
if (options.publicUrl) {
100+
envVars.PUBLIC_URL = options.publicUrl;
101+
}
102+
envVars.CI = String(options.ci);
103+
envVars.GENERATE_SOURCEMAP = String(options.sourceMap);
104+
envVars.INLINE_RUNTIME_CHUNK = String(options.inlineRuntimeChunk);
105+
106+
return envVars;
107+
}
108+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import chalk from 'chalk';
2+
import * as Debug from 'debug';
3+
import * as lodash from 'lodash';
4+
5+
import { Project } from '../';
6+
import { InfoItem } from '../../../definitions';
7+
import { RunnerNotFoundException } from '../../errors';
8+
9+
const debug = Debug('ionic:lib:project:React');
10+
11+
export class ReactProject extends Project {
12+
readonly type: 'react' = 'react';
13+
14+
async getInfo(): Promise<InfoItem[]> {
15+
const [
16+
[ ionicReactPkg, ionicReactPkgPath ],
17+
] = await Promise.all([
18+
this.getPackageJson('@ionic/react'),
19+
]);
20+
21+
return [
22+
...(await super.getInfo()),
23+
{ group: 'ionic', key: 'Ionic Framework', value: ionicReactPkg ? `@ionic/react ${ionicReactPkg.version}` : 'not installed', path: ionicReactPkgPath },
24+
];
25+
}
26+
27+
/**
28+
* We can't detect React project types. We don't know what they look like!
29+
*/
30+
async detected() {
31+
try {
32+
const pkg = await this.requirePackageJson();
33+
const deps = lodash.assign({}, pkg.dependencies, pkg.devDependencies);
34+
35+
if (typeof deps['@ionic/React'] === 'string') {
36+
debug(`${chalk.bold('@ionic/React')} detected in ${chalk.bold('package.json')}`);
37+
return true;
38+
}
39+
} catch (e) {
40+
// ignore
41+
}
42+
43+
return false;
44+
}
45+
46+
async requireBuildRunner(): Promise<import('./build').ReactBuildRunner> {
47+
const { ReactBuildRunner } = await import('./build');
48+
const deps = { ...this.e, project: this };
49+
return new ReactBuildRunner(deps);
50+
}
51+
52+
async requireServeRunner(): Promise<import('./serve').ReactServeRunner> {
53+
const { ReactServeRunner } = await import('./serve');
54+
const deps = { ...this.e, project: this };
55+
return new ReactServeRunner(deps);
56+
}
57+
58+
async requireGenerateRunner(): Promise<never> {
59+
throw new RunnerNotFoundException(
60+
`Cannot perform generate for React projects.\n` +
61+
`Since you're using the ${chalk.bold('React')} project type, this command won't work. The Ionic CLI doesn't know how to generate framework components for React projects.`
62+
);
63+
}
64+
}

0 commit comments

Comments
 (0)