Skip to content

Commit 94e9fa2

Browse files
authored
feat(): add vite support for react and vue (#4966)
* feat(): add vite support for react and vue This adds two additional project types, vue-vite and react-vite. This is to support the plan for migrating the react and vue stater projects to vite while also maintaining backwards compat for older react-scripts based projects and vue-cli based projects. * fix(): update based on feedback
1 parent e624258 commit 94e9fa2

9 files changed

Lines changed: 589 additions & 2 deletions

File tree

packages/@ionic/cli/src/constants.ts

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

99
export const PROJECT_FILE = process.env['IONIC_CONFIG_FILE'] ?? 'ionic.config.json';
10-
export const PROJECT_TYPES: ProjectType[] = ['angular', 'react', 'vue', 'ionic-angular', 'ionic1', 'custom'];
10+
export const PROJECT_TYPES: ProjectType[] = ['angular', 'react', 'vue', 'ionic-angular', 'ionic1', 'custom', 'vue-vite', 'react-vite'];
1111
export const LEGACY_PROJECT_TYPES: ProjectType[] = ['ionic-angular', 'ionic1'];
1212
export const MODERN_PROJECT_TYPES: ProjectType[] = lodash.difference(PROJECT_TYPES, LEGACY_PROJECT_TYPES);
1313

packages/@ionic/cli/src/definitions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export interface Runner<T extends object, U> {
7878
run(options: T): Promise<U>;
7979
}
8080

81-
export type ProjectType = 'angular' | 'ionic-angular' | 'ionic1' | 'custom' | 'bare' | 'react' | 'vue';
81+
export type ProjectType = 'angular' | 'ionic-angular' | 'ionic1' | 'custom' | 'bare' | 'react' | 'vue' | 'react-vite' | 'vue-vite';
8282
export type HookName = 'build:before' | 'build:after' | 'serve:before' | 'serve:after' | 'capacitor:run:before' | 'capacitor:build:before' | 'capacitor:sync:after';
8383

8484
export type CapacitorRunHookName = 'capacitor:run:before';

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,12 @@ export async function createProjectFromDetails(details: ProjectDetailsResult, de
307307
case 'vue':
308308
const { VueProject } = await import('./vue');
309309
return new VueProject(details, deps);
310+
case 'vue-vite':
311+
const { VueViteProject } = await import('./vue-vite');
312+
return new VueViteProject(details, deps);
313+
case 'react-vite':
314+
const { ReactViteProject } = await import('./react-vite');
315+
return new ReactViteProject(details, deps);
310316
case 'custom':
311317
const { CustomProject } = await import('./custom');
312318
return new CustomProject(details, deps);
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { CommandLineInputs, CommandLineOptions, CommandMetadata, ReactBuildOptions } from '../../../definitions';
2+
import { BUILD_SCRIPT, BuildCLI, BuildRunner, BuildRunnerDeps } from '../../build';
3+
4+
import { ReactViteProject } from './';
5+
6+
export interface ReactViteBuildRunnerDeps extends BuildRunnerDeps {
7+
readonly project: ReactViteProject;
8+
}
9+
export class ReactViteBuildRunner extends BuildRunner<ReactBuildOptions> {
10+
constructor(protected readonly e: ReactViteBuildRunnerDeps) {
11+
super();
12+
}
13+
14+
async getCommandMetadata(): Promise<Partial<CommandMetadata>> {
15+
return {};
16+
}
17+
18+
createOptionsFromCommandLine(inputs: CommandLineInputs, options: CommandLineOptions): ReactBuildOptions {
19+
const baseOptions = super.createBaseOptionsFromCommandLine(inputs, options);
20+
21+
return {
22+
...baseOptions,
23+
type: 'react',
24+
};
25+
}
26+
27+
async buildProject(options: ReactBuildOptions): Promise<void> {
28+
const reactVite = new ReactViteBuildCLI(this.e);
29+
await reactVite.build(options);
30+
}
31+
}
32+
33+
export class ReactViteBuildCLI extends BuildCLI<ReactBuildOptions> {
34+
readonly name = 'Vite CLI Service';
35+
readonly pkg = 'vite';
36+
readonly program = 'vite';
37+
readonly prefix = 'vite';
38+
readonly script = BUILD_SCRIPT;
39+
40+
protected async buildArgs(options: ReactBuildOptions): Promise<string[]> {
41+
const { pkgManagerArgs } = await import('../../utils/npm');
42+
43+
if (this.resolvedProgram === this.program) {
44+
return ['build', ...(options['--'] || [])];
45+
} else {
46+
const [ , ...pkgArgs ] = await pkgManagerArgs(this.e.config.get('npmClient'), { command: 'run', script: this.script, scriptArgs: options['--'] });
47+
return pkgArgs;
48+
}
49+
}
50+
51+
protected async buildEnvVars(options: ReactBuildOptions): Promise<NodeJS.ProcessEnv> {
52+
const env: NodeJS.ProcessEnv = {};
53+
return { ...await super.buildEnvVars(options), ...env };
54+
}
55+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import * as chalk from 'chalk';
2+
import * as Debug from 'debug';
3+
import * as lodash from 'lodash';
4+
import * as path from 'path';
5+
6+
import { Project } from '../';
7+
import { InfoItem } from '../../../definitions';
8+
import { RunnerNotFoundException } from '../../errors';
9+
10+
const debug = Debug('ionic:lib:project:react');
11+
12+
export class ReactViteProject extends Project {
13+
readonly type: 'react' = 'react';
14+
15+
async getInfo(): Promise<InfoItem[]> {
16+
const [
17+
[ionicReact, ionicReactPath],
18+
] = await Promise.all([
19+
this.getPackageJson('@ionic/react'),
20+
]);
21+
22+
return [
23+
...(await super.getInfo()),
24+
{
25+
group: 'ionic',
26+
name: 'Ionic Framework',
27+
key: 'framework',
28+
value: ionicReact ? `@ionic/react ${ionicReact.version}` : 'not installed',
29+
path: ionicReactPath,
30+
},
31+
];
32+
}
33+
34+
/**
35+
* We can't detect React project types. We don't know what they look like!
36+
*/
37+
async detected() {
38+
try {
39+
const pkg = await this.requirePackageJson();
40+
const deps = lodash.assign({}, pkg.dependencies, pkg.devDependencies);
41+
42+
if (typeof deps['@ionic/react'] === 'string') {
43+
debug(`${chalk.bold('@ionic/react')} detected in ${chalk.bold('package.json')}`);
44+
return true;
45+
}
46+
} catch (e) {
47+
// ignore
48+
}
49+
50+
return false;
51+
}
52+
53+
async getDefaultDistDir(): Promise<string> {
54+
return 'dist';
55+
}
56+
57+
async requireBuildRunner(): Promise<import('./build').ReactViteBuildRunner> {
58+
const { ReactViteBuildRunner } = await import('./build');
59+
const deps = { ...this.e, project: this };
60+
return new ReactViteBuildRunner(deps);
61+
}
62+
63+
async requireServeRunner(): Promise<import('./serve').ReactViteServeRunner> {
64+
const { ReactViteServeRunner } = await import('./serve');
65+
const deps = { ...this.e, project: this };
66+
return new ReactViteServeRunner(deps);
67+
}
68+
69+
async requireGenerateRunner(): Promise<never> {
70+
throw new RunnerNotFoundException(
71+
`Cannot perform generate for React projects.\n` +
72+
`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.`
73+
);
74+
}
75+
76+
setPrimaryTheme(themeColor: string): Promise<void> {
77+
const themePath = path.join(this.directory, 'src', 'theme', 'variables.css');
78+
return this.writeThemeColor(themePath, themeColor);
79+
}
80+
}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import { ParsedArgs, unparseArgs } from '@ionic/cli-framework';
2+
import { stripAnsi } from '@ionic/cli-framework-output';
3+
import { findClosestOpenPort } from '@ionic/utils-network';
4+
import { CommandMetadata, ServeDetails, ReactServeOptions, } from '../../../definitions';
5+
6+
import { strong } from '../../color';
7+
import { BIND_ALL_ADDRESS, DEFAULT_ADDRESS, LOCAL_ADDRESSES, SERVE_SCRIPT, ServeCLI, ServeRunner, ServeRunnerDeps, } from '../../serve';
8+
9+
export class ReactViteServeRunner extends ServeRunner<ReactServeOptions> {
10+
constructor(protected readonly e: ServeRunnerDeps) {
11+
super();
12+
}
13+
14+
async getCommandMetadata(): Promise<Partial<CommandMetadata>> {
15+
return {};
16+
}
17+
18+
modifyOpenUrl(url: string, _options: ReactServeOptions): string {
19+
return url;
20+
}
21+
22+
async serveProject(options: ReactServeOptions): Promise<ServeDetails> {
23+
const [externalIP, availableInterfaces] = await this.selectExternalIP(options);
24+
25+
const port = (options.port = await findClosestOpenPort(options.port));
26+
27+
const reactScripts = new ReactViteServeCLI(this.e);
28+
await reactScripts.serve(options);
29+
30+
return {
31+
custom: reactScripts.resolvedProgram !== reactScripts.program,
32+
protocol: options.https ? 'https' : 'http',
33+
localAddress: 'localhost',
34+
externalAddress: externalIP,
35+
externalNetworkInterfaces: availableInterfaces,
36+
port,
37+
externallyAccessible: ![BIND_ALL_ADDRESS, ...LOCAL_ADDRESSES].includes(
38+
externalIP
39+
),
40+
};
41+
}
42+
}
43+
44+
export class ReactViteServeCLI extends ServeCLI<ReactServeOptions> {
45+
readonly name = 'Vite CLI Service';
46+
readonly pkg = 'vite';
47+
readonly program = 'vite';
48+
readonly prefix = 'vite';
49+
readonly script = SERVE_SCRIPT;
50+
protected chunks = 0;
51+
52+
async serve(options: ReactServeOptions): Promise<void> {
53+
this.on('compile', (chunks) => {
54+
if (chunks > 0) {
55+
this.e.log.info(
56+
`... and ${strong(chunks.toString())} additional chunks`
57+
);
58+
}
59+
});
60+
61+
return super.serve(options);
62+
}
63+
64+
protected stdoutFilter(line: string): boolean {
65+
if (this.resolvedProgram !== this.program) {
66+
return super.stdoutFilter(line);
67+
}
68+
const strippedLine = stripAnsi(line);
69+
const compileMsgs = [
70+
'Compiled successfully',
71+
'Compiled with warnings',
72+
'Failed to compile',
73+
"ready in"
74+
];
75+
if (compileMsgs.some((msg) => strippedLine.includes(msg))) {
76+
this.emit('ready');
77+
return false;
78+
}
79+
80+
if (strippedLine.match(/.*chunk\s{\d+}.+/)) {
81+
this.chunks++;
82+
return false;
83+
}
84+
85+
if (strippedLine.includes('Compiled successfully')) {
86+
this.emit('compile', this.chunks);
87+
this.chunks = 0;
88+
}
89+
90+
if(strippedLine.includes('has unexpectedly closed')) {
91+
return false;
92+
}
93+
return true;
94+
}
95+
96+
protected stderrFilter(line: string): boolean {
97+
if (this.resolvedProgram !== this.program) {
98+
return super.stderrFilter(line);
99+
}
100+
const strippedLine = stripAnsi(line);
101+
if (strippedLine.includes('webpack.Progress')) {
102+
return false;
103+
}
104+
if(strippedLine.includes('has unexpectedly closed')) {
105+
return false;
106+
}
107+
108+
return true;
109+
}
110+
111+
protected async buildArgs(options: ReactServeOptions): Promise<string[]> {
112+
const args: ParsedArgs = {
113+
_: [],
114+
host: options.host,
115+
port: options.port ? options.port.toString() : undefined,
116+
};
117+
const { pkgManagerArgs } = await import('../../utils/npm');
118+
119+
const separatedArgs = options['--'];
120+
121+
if (this.resolvedProgram === this.program) {
122+
return [...unparseArgs(args), ...separatedArgs];
123+
} else {
124+
const [, ...pkgArgs] = await pkgManagerArgs(
125+
this.e.config.get('npmClient'),
126+
{
127+
command: 'run',
128+
script: this.script,
129+
scriptArgs: [...unparseArgs(args), ...separatedArgs],
130+
}
131+
);
132+
return pkgArgs;
133+
}
134+
}
135+
136+
protected async buildEnvVars(
137+
options: ReactServeOptions
138+
): Promise<NodeJS.ProcessEnv> {
139+
const env: NodeJS.ProcessEnv = {};
140+
// // Vite binds to `localhost` by default, but if specified it prints a
141+
// // warning, so don't set `HOST` if the host is set to `localhost`.
142+
if (options.host !== DEFAULT_ADDRESS) {
143+
env.HOST = options.host;
144+
}
145+
146+
env.PORT = String(options.port);
147+
148+
env.HTTPS = options.https ? 'true' : 'false';
149+
150+
return { ...(await super.buildEnvVars(options)), ...env };
151+
}
152+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { CommandLineInputs, CommandLineOptions, CommandMetadata, VueBuildOptions } from '../../../definitions';
2+
import { BUILD_SCRIPT, BuildCLI, BuildRunner, BuildRunnerDeps } from '../../build';
3+
4+
import { VueViteProject } from './';
5+
6+
export interface VueBuildRunnerDeps extends BuildRunnerDeps {
7+
readonly project: VueViteProject;
8+
}
9+
export class VueViteBuildRunner extends BuildRunner<VueBuildOptions> {
10+
constructor(protected readonly e: VueBuildRunnerDeps) {
11+
super();
12+
}
13+
14+
async getCommandMetadata(): Promise<Partial<CommandMetadata>> {
15+
return {};
16+
}
17+
18+
createOptionsFromCommandLine(inputs: CommandLineInputs, options: CommandLineOptions): VueBuildOptions {
19+
const baseOptions = super.createBaseOptionsFromCommandLine(inputs, options);
20+
21+
return {
22+
...baseOptions,
23+
type: 'vue',
24+
};
25+
}
26+
27+
async buildProject(options: VueBuildOptions): Promise<void> {
28+
const vueScripts = new VueViteBuildCLI(this.e);
29+
await vueScripts.build(options);
30+
}
31+
}
32+
33+
export class VueViteBuildCLI extends BuildCLI<VueBuildOptions> {
34+
readonly name = 'Vite CLI Service';
35+
readonly pkg = 'vite';
36+
readonly program = 'vite';
37+
readonly prefix = 'vite';
38+
readonly script = BUILD_SCRIPT;
39+
40+
protected async buildArgs(options: VueBuildOptions): Promise<string[]> {
41+
const { pkgManagerArgs } = await import('../../utils/npm');
42+
43+
if (this.resolvedProgram === this.program) {
44+
return ['build', ...(options['--'] || [])];
45+
} else {
46+
const [ , ...pkgArgs ] = await pkgManagerArgs(this.e.config.get('npmClient'), { command: 'run', script: this.script, scriptArgs: options['--'] });
47+
return pkgArgs;
48+
}
49+
}
50+
51+
protected async buildEnvVars(options: VueBuildOptions): Promise<NodeJS.ProcessEnv> {
52+
const env: NodeJS.ProcessEnv = {};
53+
return { ...await super.buildEnvVars(options), ...env };
54+
}
55+
}

0 commit comments

Comments
 (0)