feat(react): add Vite bundler option for buildable libraries (#13382)

This commit is contained in:
Jack Hsu 2022-11-25 15:25:37 -05:00 committed by GitHub
parent c2db462992
commit a63a25d2e2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 556 additions and 280 deletions

View File

@ -48,6 +48,11 @@
"type": "boolean", "type": "boolean",
"default": false "default": false
}, },
"skipBabelConfig": {
"description": "Do not generate a root babel.config.json (if babel is not needed).",
"type": "boolean",
"default": false
},
"js": { "js": {
"type": "boolean", "type": "boolean",
"default": false, "default": false,
@ -244,7 +249,7 @@
"description": "The bundler to use.", "description": "The bundler to use.",
"type": "string", "type": "string",
"enum": ["vite", "webpack"], "enum": ["vite", "webpack"],
"x-prompt": "Which bundler do you want to use?", "x-prompt": "Which bundler do you want to use to build the application?",
"default": "webpack" "default": "webpack"
} }
}, },
@ -339,8 +344,7 @@
"unitTestRunner": { "unitTestRunner": {
"type": "string", "type": "string",
"enum": ["jest", "vitest", "none"], "enum": ["jest", "vitest", "none"],
"description": "Test runner to use for unit tests.", "description": "Test runner to use for unit tests."
"default": "jest"
}, },
"inSourceTests": { "inSourceTests": {
"type": "boolean", "type": "boolean",
@ -384,7 +388,7 @@
"buildable": { "buildable": {
"type": "boolean", "type": "boolean",
"default": false, "default": false,
"description": "Generate a buildable library." "description": "Generate a buildable library. If a bundler is set then the library is buildable by default."
}, },
"importPath": { "importPath": {
"type": "string", "type": "string",
@ -419,11 +423,17 @@
"description": "Split the project configuration into `<projectRoot>/project.json` rather than including it inside `workspace.json`.", "description": "Split the project configuration into `<projectRoot>/project.json` rather than including it inside `workspace.json`.",
"type": "boolean" "type": "boolean"
}, },
"bundler": {
"type": "string",
"description": "The bundler to use.",
"enum": ["vite", "rollup"],
"x-prompt": "Which bundler would you like to use to build the library?"
},
"compiler": { "compiler": {
"type": "string", "type": "string",
"enum": ["babel", "swc"], "enum": ["babel", "swc"],
"default": "babel", "default": "babel",
"description": "Which compiler to use." "description": "Which compiler to use. Does not apply if bundler is set to Vite."
}, },
"skipPackageJson": { "skipPackageJson": {
"description": "Do not add dependencies to `package.json`.", "description": "Do not add dependencies to `package.json`.",

View File

@ -31,6 +31,11 @@
"enum": ["react", "none"], "enum": ["react", "none"],
"default": "react", "default": "react",
"x-prompt": "What UI framework plugin should Vite use?" "x-prompt": "What UI framework plugin should Vite use?"
},
"includeLib": {
"type": "boolean",
"description": "Add dependencies needed to build libraries.",
"default": false
} }
}, },
"examplesFile": "This is a generator will initialize Vite.js in your workspace. It will install all the necessary dependencies. You can read more about how this generator works, in the [Vite package overview page](/packages/vite).\n\nYou can use it on its own like this:\n\n```bash\nnx g @nrwl/vite:configuration\n```\n\nHowever, this generator will be called when you are either converting an existing React or Web app to use Vite, using the [`@nrwl/vite:configuration` generator](/packages/vite/generators/configuration), or when you are creating a new React or Web app using the [`@nrwl/react:app`](/packages/react/generators/application) or [`@nrwl/web:app`](<(/packages/web/generators/application)>) generators, if you choose `vite` as the `bundler`.\n\n## Examples\n\n### Install all the necessary dependencies for Vite and the React plugin\n\n```bash\nnx g @nrwl/vite:init --uiFramework=react\n```\n\n### Install all the necessary dependencies for Vite\n\n```bash\nnx g @nrwl/vite:init --uiFramework=none\n```\n", "examplesFile": "This is a generator will initialize Vite.js in your workspace. It will install all the necessary dependencies. You can read more about how this generator works, in the [Vite package overview page](/packages/vite).\n\nYou can use it on its own like this:\n\n```bash\nnx g @nrwl/vite:configuration\n```\n\nHowever, this generator will be called when you are either converting an existing React or Web app to use Vite, using the [`@nrwl/vite:configuration` generator](/packages/vite/generators/configuration), or when you are creating a new React or Web app using the [`@nrwl/react:app`](/packages/react/generators/application) or [`@nrwl/web:app`](<(/packages/web/generators/application)>) generators, if you choose `vite` as the `bundler`.\n\n## Examples\n\n### Install all the necessary dependencies for Vite and the React plugin\n\n```bash\nnx g @nrwl/vite:init --uiFramework=react\n```\n\n### Install all the necessary dependencies for Vite\n\n```bash\nnx g @nrwl/vite:init --uiFramework=none\n```\n",
@ -60,6 +65,12 @@
"x-dropdown": "project", "x-dropdown": "project",
"x-prompt": "What is the name of the project to set up a webpack for?" "x-prompt": "What is the name of the project to set up a webpack for?"
}, },
"includeLib": {
"type": "boolean",
"description": "Add a library build option.",
"default": false,
"x-prompt": "Does this project contain a buildable library?"
},
"uiFramework": { "uiFramework": {
"type": "string", "type": "string",
"description": "UI Framework to use for Vite.", "description": "UI Framework to use for Vite.",

View File

@ -53,6 +53,11 @@
"description": "Do not add dependencies to `package.json`.", "description": "Do not add dependencies to `package.json`.",
"type": "boolean", "type": "boolean",
"default": false "default": false
},
"skipBabelConfig": {
"description": "Do not generate a root babel.config.json (if babel is not needed).",
"type": "boolean",
"default": false
} }
}, },
"required": [], "required": [],

View File

@ -97,7 +97,7 @@ describe('Build React libraries and apps', () => {
afterEach(() => { afterEach(() => {
killPorts(); killPorts();
cleanupProject(); // cleanupProject();
}); });
describe('Buildable libraries', () => { describe('Buildable libraries', () => {
@ -252,4 +252,21 @@ export async function h() { return 'c'; }
}).toThrow(); }).toThrow();
}, 250000); }, 250000);
}); });
it('should support bundling with Vite', async () => {
const libName = uniq('lib');
runCLI(
`generate @nrwl/react:lib ${libName} --bundler=vite --no-interactive`
);
await runCLIAsync(`build ${libName}`);
checkFilesExist(
`dist/libs/${libName}/package.json`,
`dist/libs/${libName}/index.d.ts`,
`dist/libs/${libName}/index.js`,
`dist/libs/${libName}/index.mjs`
);
});
}); });

View File

@ -1,4 +1,5 @@
import { import {
checkFilesExist,
cleanupProject, cleanupProject,
createFile, createFile,
exists, exists,
@ -32,38 +33,23 @@ describe('Vite Plugin', () => {
`apps/${myApp}/index.html`, `apps/${myApp}/index.html`,
` `
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang='en'>
<head> <head>
<meta charset="utf-8" /> <meta charset='utf-8' />
<title>My App</title> <title>My App</title>
<base href="/" /> <base href='/' />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name='viewport' content='width=device-width, initial-scale=1' />
<link rel="icon" type="image/x-icon" href="favicon.ico" /> <link rel='icon' type='image/x-icon' href='favicon.ico' />
</head> </head>
<body> <body>
<div id="root"></div> <div id='root'></div>
<script type="module" src="src/main.tsx"></script> <script type='module' src='src/main.tsx'></script>
</body> </body>
</html> </html>
` `
); );
createFile(
`apps/${myApp}/src/environments/environment.prod.ts`,
`export const environment = {
production: true,
myTestVar: 'MyProductionValue',
};`
);
createFile(
`apps/${myApp}/src/environments/environment.ts`,
`export const environment = {
production: false,
myTestVar: 'MyDevelopmentValue',
};`
);
updateFile( updateFile(
`apps/${myApp}/src/app/app.tsx`, `apps/${myApp}/src/app/app.tsx`,
` `
@ -168,20 +154,6 @@ describe('Vite Plugin', () => {
}); });
}); });
it('should build application and replace files', async () => {
runCLI(`build ${myApp}`);
expect(readFile(`dist/apps/${myApp}/index.html`)).toBeDefined();
const fileArray = listFiles(`dist/apps/${myApp}/assets`);
const mainBundle = fileArray.find((file) => file.endsWith('.js'));
expect(readFile(`dist/apps/${myApp}/assets/${mainBundle}`)).toContain(
'MyProductionValue'
);
expect(
readFile(`dist/apps/${myApp}/assets/${mainBundle}`)
).not.toContain('MyDevelopmentValue');
rmDist();
}, 200000);
it('should serve application in dev mode', async () => { it('should serve application in dev mode', async () => {
const port = 4212; const port = 4212;
const p = await runCommandUntil( const p = await runCommandUntil(
@ -206,76 +178,11 @@ describe('Vite Plugin', () => {
}); });
}); });
describe('set up new React app with --bundler=vite option', () => {
beforeEach(() => {
proj = newProject();
runCLI(`generate @nrwl/react:app ${myApp} --bundler=vite`);
updateFile(
`apps/${myApp}/src/environments/environment.prod.ts`,
`export const environment = {
production: true,
myTestVar: 'MyProductionValue',
};`
);
updateFile(
`apps/${myApp}/src/environments/environment.ts`,
`export const environment = {
production: false,
myTestVar: 'MyDevelopmentValue',
};`
);
updateFile(
`apps/${myApp}/src/app/app.tsx`,
`
import { environment } from './../environments/environment';
export function App() {
return (
<>
<h1>{environment.myTestVar}</h1>
<p>Welcome ${myApp}!</p>
</>
);
}
export default App;
`
);
});
afterEach(() => cleanupProject());
it('should build application and replace files', async () => {
runCLI(`build ${myApp}`);
expect(readFile(`dist/apps/${myApp}/index.html`)).toBeDefined();
const fileArray = listFiles(`dist/apps/${myApp}/assets`);
const mainBundle = fileArray.find((file) => file.endsWith('.js'));
expect(readFile(`dist/apps/${myApp}/assets/${mainBundle}`)).toContain(
'MyProductionValue'
);
expect(
readFile(`dist/apps/${myApp}/assets/${mainBundle}`)
).not.toContain('MyDevelopmentValue');
rmDist();
}, 200000);
});
describe('convert React webpack app to vite using the vite:configuration generator', () => { describe('convert React webpack app to vite using the vite:configuration generator', () => {
beforeEach(() => { beforeEach(() => {
proj = newProject(); proj = newProject();
runCLI(`generate @nrwl/react:app ${myApp} --bundler=webpack`); runCLI(`generate @nrwl/react:app ${myApp} --bundler=webpack`);
runCLI(`generate @nrwl/vite:configuration ${myApp}`); runCLI(`generate @nrwl/vite:configuration ${myApp}`);
updateFile(
`apps/${myApp}/src/environments/environment.prod.ts`,
`export const environment = {
production: true,
myTestVar: 'MyProductionValue',
};`
);
updateFile(
`apps/${myApp}/src/environments/environment.ts`,
`export const environment = {
production: false,
myTestVar: 'MyDevelopmentValue',
};`
);
updateFile( updateFile(
`apps/${myApp}/src/app/app.tsx`, `apps/${myApp}/src/app/app.tsx`,
@ -294,18 +201,6 @@ describe('Vite Plugin', () => {
); );
}); });
afterEach(() => cleanupProject()); afterEach(() => cleanupProject());
it('should build application and replace files', async () => {
runCLI(`build ${myApp}`);
expect(readFile(`dist/apps/${myApp}/index.html`)).toBeDefined();
const fileArray = listFiles(`dist/apps/${myApp}/assets`);
const mainBundle = fileArray.find((file) => file.endsWith('.js'));
expect(readFile(`dist/apps/${myApp}/assets/${mainBundle}`)).toContain(
'MyProductionValue'
);
expect(
readFile(`dist/apps/${myApp}/assets/${mainBundle}`)
).not.toContain('MyDevelopmentValue');
}, 200000);
it('should serve application in dev mode', async () => { it('should serve application in dev mode', async () => {
const port = 4212; const port = 4212;
@ -348,7 +243,7 @@ describe('Vite Plugin', () => {
readFile(`dist/apps/${myApp}/assets/${mainBundle}`) readFile(`dist/apps/${myApp}/assets/${mainBundle}`)
).toBeDefined(); ).toBeDefined();
rmDist(); rmDist();
}, 200000); }, 200_000);
}); });
describe('convert @nrwl/web webpack app to vite using the vite:configuration generator', () => { describe('convert @nrwl/web webpack app to vite using the vite:configuration generator', () => {
@ -391,14 +286,16 @@ describe('Vite Plugin', () => {
`Successfully ran target test for project ${myApp}` `Successfully ran target test for project ${myApp}`
); );
}); });
}); }),
100_000;
}); });
describe('should be able to create libs that use vitest', () => { describe('should be able to create libs that use vitest', () => {
const lib = uniq('my-lib'); const lib = uniq('my-lib');
beforeEach(() => { beforeEach(() => {
proj = newProject(); proj = newProject();
}); }),
100_000;
it('should be able to run tests', async () => { it('should be able to run tests', async () => {
runCLI(`generate @nrwl/react:lib ${lib} --unitTestRunner=vitest`); runCLI(`generate @nrwl/react:lib ${lib} --unitTestRunner=vitest`);
@ -408,7 +305,8 @@ describe('Vite Plugin', () => {
expect(result.combinedOutput).toContain( expect(result.combinedOutput).toContain(
`Successfully ran target test for project ${lib}` `Successfully ran target test for project ${lib}`
); );
}); }),
100_000;
it('should be able to run tests with inSourceTests set to true', async () => { it('should be able to run tests with inSourceTests set to true', async () => {
runCLI( runCLI(
@ -432,6 +330,6 @@ describe('Vite Plugin', () => {
const result = await runCLIAsync(`test ${lib}`); const result = await runCLIAsync(`test ${lib}`);
expect(result.combinedOutput).toContain(`1 passed`); expect(result.combinedOutput).toContain(`1 passed`);
}); }, 100_000);
}); });
}); });

View File

@ -19,7 +19,6 @@ describe('app', () => {
compiler: 'babel', compiler: 'babel',
e2eTestRunner: 'cypress', e2eTestRunner: 'cypress',
skipFormat: false, skipFormat: false,
unitTestRunner: 'jest',
name: 'myApp', name: 'myApp',
linter: Linter.EsLint, linter: Linter.EsLint,
style: 'css', style: 'css',
@ -381,6 +380,12 @@ describe('app', () => {
expect(targetConfig.build.options).toEqual({ expect(targetConfig.build.options).toEqual({
outputPath: 'dist/apps/my-app', outputPath: 'dist/apps/my-app',
}); });
expect(
appTree.exists(`apps/my-app/environments/environment.ts`)
).toBeFalsy();
expect(
appTree.exists(`apps/my-app/environments/environment.prod.ts`)
).toBeFalsy();
}); });
it('should setup the nrwl web dev server builder', async () => { it('should setup the nrwl web dev server builder', async () => {

View File

@ -80,6 +80,7 @@ export async function applicationGenerator(host: Tree, schema: Schema) {
const initTask = await reactInitGenerator(host, { const initTask = await reactInitGenerator(host, {
...options, ...options,
skipFormat: true, skipFormat: true,
skipBabelConfig: options.bundler === 'vite',
}); });
tasks.push(initTask); tasks.push(initTask);
@ -88,6 +89,10 @@ export async function applicationGenerator(host: Tree, schema: Schema) {
addProject(host, options); addProject(host, options);
if (options.bundler === 'vite') { if (options.bundler === 'vite') {
// We recommend users use `import.meta.env.MODE` and other variables in their code to differentiate between production and development.
// See: https://vitejs.dev/guide/env-and-mode.html
host.delete(joinPathFragments(options.appProjectRoot, 'src/environments'));
const viteTask = await viteConfigurationGenerator(host, { const viteTask = await viteConfigurationGenerator(host, {
uiFramework: 'react', uiFramework: 'react',
project: options.projectName, project: options.projectName,
@ -111,9 +116,11 @@ export async function applicationGenerator(host: Tree, schema: Schema) {
const cypressTask = await addCypress(host, options); const cypressTask = await addCypress(host, options);
tasks.push(cypressTask); tasks.push(cypressTask);
const jestTask = await addJest(host, options); if (options.unitTestRunner === 'jest') {
tasks.push(jestTask); const jestTask = await addJest(host, options);
updateSpecConfig(host, options); tasks.push(jestTask);
updateSpecConfig(host, options);
}
const styledTask = addStyledModuleDependencies(host, options.styledModule); const styledTask = addStyledModuleDependencies(host, options.styledModule);
tasks.push(styledTask); tasks.push(styledTask);
const routingTask = addRouting(host, options); const routingTask = addRouting(host, options);

View File

@ -40,20 +40,7 @@ export function normalizeOptions(
assertValidStyle(options.style); assertValidStyle(options.style);
if (options.bundler === 'vite') { const normalized = {
options.unitTestRunner = 'vitest';
}
options.routing = options.routing ?? false;
options.strict = options.strict ?? true;
options.classComponent = options.classComponent ?? false;
options.unitTestRunner = options.unitTestRunner ?? 'jest';
options.e2eTestRunner = options.e2eTestRunner ?? 'cypress';
options.compiler = options.compiler ?? 'babel';
options.bundler = options.bundler ?? 'webpack';
options.devServerPort ??= findFreePort(host);
return {
...options, ...options,
name: names(options.name).fileName, name: names(options.name).fileName,
projectName: appProjectName, projectName: appProjectName,
@ -63,5 +50,18 @@ export function normalizeOptions(
fileName, fileName,
styledModule, styledModule,
hasStyles: options.style !== 'none', hasStyles: options.style !== 'none',
}; } as NormalizedSchema;
normalized.routing = normalized.routing ?? false;
normalized.strict = normalized.strict ?? true;
normalized.classComponent = normalized.classComponent ?? false;
normalized.compiler = normalized.compiler ?? 'babel';
normalized.bundler = normalized.bundler ?? 'webpack';
normalized.unitTestRunner =
normalized.unitTestRunner ??
(normalized.bundler === 'vite' ? 'vitest' : 'jest');
normalized.e2eTestRunner = normalized.e2eTestRunner ?? 'cypress';
normalized.devServerPort ??= findFreePort(host);
return normalized;
} }

View File

@ -28,8 +28,8 @@ export function setDefaults(host: Tree, options: NormalizedSchema) {
...prev, ...prev,
application: { application: {
style: options.style, style: options.style,
unitTestRunner: options.unitTestRunner,
linter: options.linter, linter: options.linter,
bundler: options.bundler,
...prev.application, ...prev.application,
}, },
component: { component: {
@ -38,7 +38,6 @@ export function setDefaults(host: Tree, options: NormalizedSchema) {
}, },
library: { library: {
style: options.style, style: options.style,
unitTestRunner: options.unitTestRunner,
linter: options.linter, linter: options.linter,
...prev.library, ...prev.library,
}, },

View File

@ -7,7 +7,7 @@ export interface Schema {
skipFormat: boolean; skipFormat: boolean;
directory?: string; directory?: string;
tags?: string; tags?: string;
unitTestRunner: 'jest' | 'vitest' | 'none'; unitTestRunner?: 'jest' | 'vitest' | 'none';
inSourceTests?: boolean; inSourceTests?: boolean;
/** /**
* @deprecated * @deprecated
@ -41,4 +41,5 @@ export interface NormalizedSchema extends Schema {
fileName: string; fileName: string;
styledModule: null | SupportedStyles; styledModule: null | SupportedStyles;
hasStyles: boolean; hasStyles: boolean;
unitTestRunner: 'jest' | 'vitest' | 'none';
} }

View File

@ -185,7 +185,7 @@
"description": "The bundler to use.", "description": "The bundler to use.",
"type": "string", "type": "string",
"enum": ["vite", "webpack"], "enum": ["vite", "webpack"],
"x-prompt": "Which bundler do you want to use?", "x-prompt": "Which bundler do you want to use to build the application?",
"default": "webpack" "default": "webpack"
} }
}, },

View File

@ -1,6 +1,7 @@
export interface InitSchema { export interface InitSchema {
unitTestRunner?: 'jest' | 'vitest' | 'none'; unitTestRunner?: 'jest' | 'vitest' | 'none';
e2eTestRunner?: 'cypress' | 'none'; e2eTestRunner?: 'cypress' | 'none';
skipBabelConfig?: boolean;
skipFormat?: boolean; skipFormat?: boolean;
skipPackageJson?: boolean; skipPackageJson?: boolean;
js?: boolean; js?: boolean;

View File

@ -28,6 +28,11 @@
"type": "boolean", "type": "boolean",
"default": false "default": false
}, },
"skipBabelConfig": {
"description": "Do not generate a root babel.config.json (if babel is not needed).",
"type": "boolean",
"default": false
},
"js": { "js": {
"type": "boolean", "type": "boolean",
"default": false, "default": false,

View File

@ -0,0 +1,12 @@
{
"name": "<%= name %>",
"version": "0.0.1",
"main": "./index.js",
"module": "./index.mjs",
"exports": {
".": {
"import": "./index.mjs",
"require": "./index.js"
}
}
}

View File

@ -1,4 +0,0 @@
{
"name": "<%= name %>",
"version": "0.0.1"
}

View File

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title><%= className %> Demo</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
</head>
<body>
<div id="root"></div>
<script type="module" src="./src/demo.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,12 @@
{
"name": "<%= name %>",
"version": "0.0.1",
"main": "./index.js",
"types": "./index.d.ts",
"exports": {
".": {
"import": "./index.mjs",
"require": "./index.js"
}
}
}

View File

@ -0,0 +1,19 @@
/*
* This a a demo file that can be helpful when developing components by serving and interacting with them in the browser.
*/
<% if (component) { %>
import * as ReactDOM from 'react-dom/client';
import { <%= className %> } from './index';
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(
<<%= className %> />
);
<% } else { %>
import * as ReactDOM from 'react-dom/client';
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(
<h1><%= className %> Demo</h1>
);
<% } %>

View File

@ -0,0 +1,60 @@
import type { Tree } from '@nrwl/devkit';
import { Linter } from '@nrwl/linter';
import { createTreeWithEmptyWorkspace } from '@nrwl/devkit/testing';
import { normalizeOptions } from './normalize-options';
describe('normalizeOptions', () => {
let tree: Tree;
beforeEach(() => {
tree = createTreeWithEmptyWorkspace();
});
it('should set unitTestRunner=jest and bundler=rollup by default', async () => {
const options = normalizeOptions(tree, {
name: 'test',
style: 'css',
linter: Linter.None,
});
expect(options).toMatchObject({
buildable: false,
bundler: 'rollup',
compiler: 'babel',
unitTestRunner: 'jest',
});
});
it('should set unitTestRunner=vitest by default when bundler is vite', async () => {
const options = normalizeOptions(tree, {
name: 'test',
style: 'css',
linter: Linter.None,
bundler: 'vite',
});
expect(options).toMatchObject({
buildable: true,
bundler: 'vite',
compiler: 'babel',
unitTestRunner: 'vitest',
});
});
it('should set maintain unitTestRunner when bundler is vite', async () => {
const options = normalizeOptions(tree, {
name: 'test',
style: 'css',
linter: Linter.None,
bundler: 'vite',
unitTestRunner: 'jest',
});
expect(options).toMatchObject({
buildable: true,
bundler: 'vite',
compiler: 'babel',
unitTestRunner: 'jest',
});
});
});

View File

@ -0,0 +1,79 @@
import {
getImportPath,
getProjects,
getWorkspaceLayout,
joinPathFragments,
names,
normalizePath,
Tree,
} from '@nrwl/devkit';
import { assertValidStyle } from '../../../utils/assertion';
import { NormalizedSchema } from '../library';
import { Schema } from '../schema';
export function normalizeOptions(
host: Tree,
options: Schema
): NormalizedSchema {
const name = names(options.name).fileName;
const projectDirectory = options.directory
? `${names(options.directory).fileName}/${name}`
: name;
const projectName = projectDirectory.replace(new RegExp('/', 'g'), '-');
const fileName = projectName;
const { libsDir, npmScope } = getWorkspaceLayout(host);
const projectRoot = joinPathFragments(libsDir, projectDirectory);
const parsedTags = options.tags
? options.tags.split(',').map((s) => s.trim())
: [];
const importPath =
options.importPath || getImportPath(npmScope, projectDirectory);
const normalized = {
...options,
compiler: options.compiler ?? 'babel',
bundler: options.bundler ?? 'rollup',
fileName,
routePath: `/${name}`,
name: projectName,
projectRoot,
projectDirectory,
parsedTags,
importPath,
} as NormalizedSchema;
// Libraries with a bundler or is publishable must also be buildable.
normalized.buildable = Boolean(
options.bundler || options.buildable || options.publishable
);
normalized.unitTestRunner =
normalized.unitTestRunner ??
(normalized.bundler === 'vite' ? 'vitest' : 'jest');
if (options.appProject) {
const appProjectConfig = getProjects(host).get(options.appProject);
if (appProjectConfig.projectType !== 'application') {
throw new Error(
`appProject expected type of "application" but got "${appProjectConfig.projectType}"`
);
}
try {
normalized.appMain = appProjectConfig.targets.build.options.main;
normalized.appSourceRoot = normalizePath(appProjectConfig.sourceRoot);
} catch (e) {
throw new Error(
`Could not locate project main for ${options.appProject}`
);
}
}
assertValidStyle(normalized.style);
return normalized;
}

View File

@ -225,7 +225,11 @@ describe('lib', () => {
`); `);
}); });
it('should update jest.config.ts for babel', async () => { it('should update jest.config.ts for babel', async () => {
await libraryGenerator(appTree, { ...defaultSchema, compiler: 'babel' }); await libraryGenerator(appTree, {
...defaultSchema,
buildable: true,
compiler: 'babel',
});
expect(appTree.read('libs/my-lib/jest.config.ts', 'utf-8')).toContain( expect(appTree.read('libs/my-lib/jest.config.ts', 'utf-8')).toContain(
"['babel-jest', { presets: ['@nrwl/react/babel'] }]" "['babel-jest', { presets: ['@nrwl/react/babel'] }]"
); );
@ -280,6 +284,7 @@ describe('lib', () => {
await libraryGenerator(appTree, { await libraryGenerator(appTree, {
...defaultSchema, ...defaultSchema,
directory: 'myDir', directory: 'myDir',
buildable: true,
compiler: 'babel', compiler: 'babel',
}); });
expect( expect(
@ -725,6 +730,7 @@ describe('lib', () => {
it('should install swc dependencies if needed', async () => { it('should install swc dependencies if needed', async () => {
await libraryGenerator(appTree, { await libraryGenerator(appTree, {
...defaultSchema, ...defaultSchema,
buildable: true,
compiler: 'swc', compiler: 'swc',
}); });
const packageJson = readJson(appTree, 'package.json'); const packageJson = readJson(appTree, 'package.json');
@ -764,6 +770,7 @@ describe('lib', () => {
await libraryGenerator(appTree, { await libraryGenerator(appTree, {
...defaultSchema, ...defaultSchema,
style, style,
compiler: 'babel',
name: 'myLib', name: 'myLib',
}); });

View File

@ -44,10 +44,12 @@ import {
typesReactRouterDomVersion, typesReactRouterDomVersion,
} from '../../utils/versions'; } from '../../utils/versions';
import componentGenerator from '../component/component'; import componentGenerator from '../component/component';
import init from '../init/init'; import initGenerator from '../init/init';
import { Schema } from './schema'; import { Schema } from './schema';
import { updateJestConfigContent } from '../../utils/jest-utils'; import { updateJestConfigContent } from '../../utils/jest-utils';
import { vitestGenerator } from '@nrwl/vite'; import { viteConfigurationGenerator, vitestGenerator } from '@nrwl/vite';
import { normalizeOptions } from './lib/normalize-options';
export interface NormalizedSchema extends Schema { export interface NormalizedSchema extends Schema {
name: string; name: string;
fileName: string; fileName: string;
@ -57,6 +59,7 @@ export interface NormalizedSchema extends Schema {
parsedTags: string[]; parsedTags: string[];
appMain?: string; appMain?: string;
appSourceRoot?: string; appSourceRoot?: string;
unitTestRunner: 'jest' | 'vitest' | 'none';
} }
export async function libraryGenerator(host: Tree, schema: Schema) { export async function libraryGenerator(host: Tree, schema: Schema) {
@ -72,10 +75,11 @@ export async function libraryGenerator(host: Tree, schema: Schema) {
options.style = 'none'; options.style = 'none';
} }
const initTask = await init(host, { const initTask = await initGenerator(host, {
...options, ...options,
e2eTestRunner: 'none', e2eTestRunner: 'none',
skipFormat: true, skipFormat: true,
skipBabelConfig: options.bundler === 'vite',
}); });
tasks.push(initTask); tasks.push(initTask);
@ -90,6 +94,18 @@ export async function libraryGenerator(host: Tree, schema: Schema) {
updateBaseTsConfig(host, options); updateBaseTsConfig(host, options);
} }
if (options.buildable && options.bundler === 'vite') {
const viteTask = await viteConfigurationGenerator(host, {
uiFramework: 'react',
project: options.name,
newProject: true,
includeLib: true,
inSourceTests: options.inSourceTests,
includeVitest: true,
});
tasks.push(viteTask);
}
if (options.unitTestRunner === 'jest') { if (options.unitTestRunner === 'jest') {
const jestTask = await jestProjectGenerator(host, { const jestTask = await jestProjectGenerator(host, {
...options, ...options,
@ -110,7 +126,10 @@ export async function libraryGenerator(host: Tree, schema: Schema) {
); );
host.write(jestConfigPath, updatedContent); host.write(jestConfigPath, updatedContent);
} }
} else if (options.unitTestRunner === 'vitest') { } else if (
options.unitTestRunner === 'vitest' &&
options.bundler !== 'vite' // tests are already configured if bundler is vite
) {
const vitestTask = await vitestGenerator(host, { const vitestTask = await vitestGenerator(host, {
uiFramework: 'react', uiFramework: 'react',
project: options.name, project: options.name,
@ -299,22 +318,34 @@ function updateBaseTsConfig(host: Tree, options: NormalizedSchema) {
} }
function createFiles(host: Tree, options: NormalizedSchema) { function createFiles(host: Tree, options: NormalizedSchema) {
const substitutions = {
...options,
...names(options.name),
tmpl: '',
offsetFromRoot: offsetFromRoot(options.projectRoot),
rootTsConfigPath: getRelativePathToRootTsConfig(host, options.projectRoot),
};
generateFiles( generateFiles(
host, host,
joinPathFragments(__dirname, './files/lib'), joinPathFragments(__dirname, './files/common'),
options.projectRoot, options.projectRoot,
{ substitutions
...options,
...names(options.name),
tmpl: '',
offsetFromRoot: offsetFromRoot(options.projectRoot),
rootTsConfigPath: getRelativePathToRootTsConfig(
host,
options.projectRoot
),
}
); );
if (options.bundler === 'vite') {
generateFiles(
host,
joinPathFragments(__dirname, './files/vite'),
options.projectRoot,
substitutions
);
if (host.exists(joinPathFragments(options.projectRoot, '.babelrc'))) {
host.delete(joinPathFragments(options.projectRoot, '.babelrc'));
}
}
if (!options.publishable && !options.buildable) { if (!options.publishable && !options.buildable) {
host.delete(`${options.projectRoot}/package.json`); host.delete(`${options.projectRoot}/package.json`);
} }
@ -415,59 +446,6 @@ function readComponent(
return { content, source }; return { content, source };
} }
function normalizeOptions(host: Tree, options: Schema): NormalizedSchema {
const name = names(options.name).fileName;
const projectDirectory = options.directory
? `${names(options.directory).fileName}/${name}`
: name;
const projectName = projectDirectory.replace(new RegExp('/', 'g'), '-');
const fileName = projectName;
const { libsDir, npmScope } = getWorkspaceLayout(host);
const projectRoot = joinPathFragments(libsDir, projectDirectory);
const parsedTags = options.tags
? options.tags.split(',').map((s) => s.trim())
: [];
const importPath =
options.importPath || getImportPath(npmScope, projectDirectory);
const normalized: NormalizedSchema = {
...options,
fileName,
routePath: `/${name}`,
name: projectName,
projectRoot,
projectDirectory,
parsedTags,
importPath,
};
if (options.appProject) {
const appProjectConfig = getProjects(host).get(options.appProject);
if (appProjectConfig.projectType !== 'application') {
throw new Error(
`appProject expected type of "application" but got "${appProjectConfig.projectType}"`
);
}
try {
normalized.appMain = appProjectConfig.targets.build.options.main;
normalized.appSourceRoot = normalizePath(appProjectConfig.sourceRoot);
} catch (e) {
throw new Error(
`Could not locate project main for ${options.appProject}`
);
}
}
assertValidStyle(normalized.style);
return normalized;
}
function updateLibPackageNpmScope(host: Tree, options: NormalizedSchema) { function updateLibPackageNpmScope(host: Tree, options: NormalizedSchema) {
return updateJson(host, `${options.projectRoot}/package.json`, (json) => { return updateJson(host, `${options.projectRoot}/package.json`, (json) => {
json.name = options.importPath; json.name = options.importPath;

View File

@ -2,27 +2,28 @@ import { Linter } from '@nrwl/linter';
import { SupportedStyles } from '../../../typings/style'; import { SupportedStyles } from '../../../typings/style';
export interface Schema { export interface Schema {
name: string;
directory?: string;
style: SupportedStyles;
skipTsConfig: boolean;
skipFormat: boolean;
tags?: string;
pascalCaseFiles?: boolean;
routing?: boolean;
appProject?: string; appProject?: string;
unitTestRunner: 'jest' | 'vitest' | 'none';
inSourceTests?: boolean;
linter: Linter;
component?: boolean;
publishable?: boolean;
buildable?: boolean; buildable?: boolean;
importPath?: string; bundler?: 'rollup' | 'vite';
js?: boolean;
globalCss?: boolean;
strict?: boolean;
setParserOptionsProject?: boolean;
standaloneConfig?: boolean;
compiler?: 'babel' | 'swc'; compiler?: 'babel' | 'swc';
component?: boolean;
directory?: string;
globalCss?: boolean;
importPath?: string;
inSourceTests?: boolean;
js?: boolean;
linter: Linter;
name: string;
pascalCaseFiles?: boolean;
publishable?: boolean;
routing?: boolean;
setParserOptionsProject?: boolean;
skipFormat?: boolean;
skipPackageJson?: boolean; skipPackageJson?: boolean;
skipTsConfig?: boolean;
standaloneConfig?: boolean;
strict?: boolean;
style: SupportedStyles;
tags?: string;
unitTestRunner?: 'jest' | 'vitest' | 'none';
} }

View File

@ -81,8 +81,7 @@
"unitTestRunner": { "unitTestRunner": {
"type": "string", "type": "string",
"enum": ["jest", "vitest", "none"], "enum": ["jest", "vitest", "none"],
"description": "Test runner to use for unit tests.", "description": "Test runner to use for unit tests."
"default": "jest"
}, },
"inSourceTests": { "inSourceTests": {
"type": "boolean", "type": "boolean",
@ -126,7 +125,7 @@
"buildable": { "buildable": {
"type": "boolean", "type": "boolean",
"default": false, "default": false,
"description": "Generate a buildable library." "description": "Generate a buildable library. If a bundler is set then the library is buildable by default."
}, },
"importPath": { "importPath": {
"type": "string", "type": "string",
@ -161,11 +160,17 @@
"description": "Split the project configuration into `<projectRoot>/project.json` rather than including it inside `workspace.json`.", "description": "Split the project configuration into `<projectRoot>/project.json` rather than including it inside `workspace.json`.",
"type": "boolean" "type": "boolean"
}, },
"bundler": {
"type": "string",
"description": "The bundler to use.",
"enum": ["vite", "rollup"],
"x-prompt": "Which bundler would you like to use to build the library?"
},
"compiler": { "compiler": {
"type": "string", "type": "string",
"enum": ["babel", "swc"], "enum": ["babel", "swc"],
"default": "babel", "default": "babel",
"description": "Which compiler to use." "description": "Which compiler to use. Does not apply if bundler is set to Vite."
}, },
"skipPackageJson": { "skipPackageJson": {
"description": "Do not add dependencies to `package.json`.", "description": "Do not add dependencies to `package.json`.",

View File

@ -4,25 +4,42 @@ import 'dotenv/config';
import { getBuildConfig } from '../../utils/options-utils'; import { getBuildConfig } from '../../utils/options-utils';
import { ViteBuildExecutorOptions } from './schema'; import { ViteBuildExecutorOptions } from './schema';
import { copyAssets } from '@nrwl/js'; import { copyAssets } from '@nrwl/js';
import { existsSync } from 'fs';
import { join } from 'path';
export default async function viteBuildExecutor( export default async function viteBuildExecutor(
options: ViteBuildExecutorOptions, options: ViteBuildExecutorOptions,
context: ExecutorContext context: ExecutorContext
) { ) {
if (options.assets) { const projectRoot = context.workspace.projects[context.projectName].root;
let assets = options.assets;
// Copy package.json as an asset if it exists
if (existsSync(join(projectRoot, 'package.json'))) {
assets ??= [];
assets.push({
input: '.',
output: '.',
glob: 'package.json',
});
}
logger.info(`NX Vite build starting ...`);
const buildResult = await runInstance(await getBuildConfig(options, context));
logger.info(`NX Vite build finished ...`);
logger.info(`NX Vite files available in ${options.outputPath}`);
// TODO(jack): handle watch once we add that option
if (assets) {
await copyAssets( await copyAssets(
{ {
outputPath: options.outputPath, outputPath: options.outputPath,
assets: options.assets, assets: assets,
}, },
context context
); );
} }
logger.info(`NX Vite build starting ...`);
await runInstance(await getBuildConfig(options, context));
logger.info(`NX Vite build finished ...`);
logger.info(`NX Vite files available in ${options.outputPath}`);
return { success: true }; return { success: true };
} }

View File

@ -1,4 +1,9 @@
import { addDependenciesToPackageJson, readJson, Tree } from '@nrwl/devkit'; import {
addDependenciesToPackageJson,
addProjectConfiguration,
readJson,
Tree,
} from '@nrwl/devkit';
import { createTreeWithEmptyV1Workspace } from '@nrwl/devkit/testing'; import { createTreeWithEmptyV1Workspace } from '@nrwl/devkit/testing';
import { nxVersion } from '../../utils/versions'; import { nxVersion } from '../../utils/versions';
@ -116,4 +121,27 @@ describe('@nrwl/vite:configuration', () => {
expect(viteConfig).toContain('test'); expect(viteConfig).toContain('test');
}); });
}); });
describe('library mode', () => {
beforeEach(async () => {
tree = createTreeWithEmptyV1Workspace();
addProjectConfiguration(tree, 'my-lib', {
root: 'my-lib',
});
});
it('should add config for building library', async () => {
await viteConfigurationGenerator(tree, {
uiFramework: 'react',
includeLib: true,
project: 'my-lib',
newProject: true,
});
const viteConfig = tree.read('my-lib/vite.config.ts').toString();
expect(viteConfig).toMatch('build: {');
expect(viteConfig).toMatch("external: ['react'");
});
});
}); });

View File

@ -35,6 +35,7 @@ export async function viteConfigurationGenerator(tree: Tree, schema: Schema) {
const initTask = await initGenerator(tree, { const initTask = await initGenerator(tree, {
uiFramework: schema.uiFramework, uiFramework: schema.uiFramework,
includeLib: schema.includeLib,
}); });
tasks.push(initTask); tasks.push(initTask);

View File

@ -4,4 +4,5 @@ export interface Schema {
newProject?: boolean; newProject?: boolean;
includeVitest?: boolean; includeVitest?: boolean;
inSourceTests?: boolean; inSourceTests?: boolean;
includeLib?: boolean;
} }

View File

@ -16,6 +16,12 @@
"x-dropdown": "project", "x-dropdown": "project",
"x-prompt": "What is the name of the project to set up a webpack for?" "x-prompt": "What is the name of the project to set up a webpack for?"
}, },
"includeLib": {
"type": "boolean",
"description": "Add a library build option.",
"default": false,
"x-prompt": "Does this project contain a buildable library?"
},
"uiFramework": { "uiFramework": {
"type": "string", "type": "string",
"description": "UI Framework to use for Vite.", "description": "UI Framework to use for Vite.",

View File

@ -5,17 +5,17 @@ import {
Tree, Tree,
updateJson, updateJson,
} from '@nrwl/devkit'; } from '@nrwl/devkit';
import { runTasksInSerial } from '@nrwl/workspace/src/utilities/run-tasks-in-serial';
import { import {
jsdomVersion,
nxVersion, nxVersion,
vitePluginDtsVersion,
vitePluginEslintVersion, vitePluginEslintVersion,
vitePluginReactVersion, vitePluginReactVersion,
viteVersion,
vitestUiVersion, vitestUiVersion,
vitestVersion, vitestVersion,
viteTsConfigPathsVersion, viteTsConfigPathsVersion,
jsdomVersion, viteVersion,
} from '../../utils/versions'; } from '../../utils/versions';
import { Schema } from './schema'; import { Schema } from './schema';
@ -39,6 +39,10 @@ function checkDependenciesInstalled(host: Tree, schema: Schema) {
devDependencies['@vitejs/plugin-react'] = vitePluginReactVersion; devDependencies['@vitejs/plugin-react'] = vitePluginReactVersion;
} }
if (schema.includeLib) {
devDependencies['vite-plugin-dts'] = vitePluginDtsVersion;
}
return addDependenciesToPackageJson(host, dependencies, devDependencies); return addDependenciesToPackageJson(host, dependencies, devDependencies);
} }

View File

@ -1,3 +1,4 @@
export interface Schema { export interface Schema {
uiFramework: 'react' | 'none'; uiFramework: 'react' | 'none';
includeLib?: boolean;
} }

View File

@ -11,6 +11,11 @@
"enum": ["react", "none"], "enum": ["react", "none"],
"default": "react", "default": "react",
"x-prompt": "What UI framework plugin should Vite use?" "x-prompt": "What UI framework plugin should Vite use?"
},
"includeLib": {
"type": "boolean",
"description": "Add dependencies needed to build libraries.",
"default": false
} }
}, },
"examplesFile": "../../../docs/init-examples.md" "examplesFile": "../../../docs/init-examples.md"

View File

@ -107,8 +107,10 @@ describe('vitest generator', () => {
import react from '@vitejs/plugin-react'; import react from '@vitejs/plugin-react';
import ViteTsConfigPathsPlugin from 'vite-tsconfig-paths'; import ViteTsConfigPathsPlugin from 'vite-tsconfig-paths';
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
react(), react(),
ViteTsConfigPathsPlugin({ ViteTsConfigPathsPlugin({
root: '../../', root: '../../',
@ -116,6 +118,7 @@ describe('vitest generator', () => {
}), }),
], ],
test: { test: {
globals: true, globals: true,
environment: 'jsdom', environment: 'jsdom',
@ -140,14 +143,17 @@ describe('vitest generator', () => {
import react from '@vitejs/plugin-react'; import react from '@vitejs/plugin-react';
import ViteTsConfigPathsPlugin from 'vite-tsconfig-paths'; import ViteTsConfigPathsPlugin from 'vite-tsconfig-paths';
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
react(), react(),
ViteTsConfigPathsPlugin({ ViteTsConfigPathsPlugin({
root: '../../', root: '../../',
projects: ['tsconfig.base.json'], projects: ['tsconfig.base.json'],
}), }),
], ],
define: { define: {
'import.meta.vitest': undefined 'import.meta.vitest': undefined
}, },

View File

@ -150,21 +150,23 @@ export function addOrChangeBuildTarget(
options: buildOptions, options: buildOptions,
configurations: { configurations: {
development: {}, development: {},
production: { production: options.includeLib
fileReplacements: [ ? {}
{ : {
replace: joinPathFragments( fileReplacements: [
project.sourceRoot, {
'environments/environment.ts' replace: joinPathFragments(
), project.sourceRoot,
with: joinPathFragments( 'environments/environment.ts'
project.sourceRoot, ),
'environments/environment.prod.ts' with: joinPathFragments(
), project.sourceRoot,
'environments/environment.prod.ts'
),
},
],
sourceMap: false,
}, },
],
sourceMap: false,
},
}, },
}; };
} }
@ -356,7 +358,8 @@ export function writeViteConfig(tree: Tree, options: Schema) {
let viteConfigContent = ''; let viteConfigContent = '';
const testOption = `test: { const testOption = options.includeVitest
? `test: {
globals: true, globals: true,
environment: 'jsdom', environment: 'jsdom',
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
@ -365,11 +368,45 @@ export function writeViteConfig(tree: Tree, options: Schema) {
? `includeSource: ['src/**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}']` ? `includeSource: ['src/**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}']`
: '' : ''
} }
},`; },`
: '';
const defineOption = `define: { const defineOption = options.inSourceTests
? `define: {
'import.meta.vitest': undefined 'import.meta.vitest': undefined
},`; },`
: '';
const dtsPlugin = `dts({
tsConfigFilePath: join(__dirname, 'tsconfig.lib.json'),
// Faster builds by skipping tests. Set this to false to enable type checking.
skipDiagnostics: true,
}),`;
const buildOption = options.includeLib
? `
// Configuration for building your library.
// See: https://vitejs.dev/guide/build.html#library-mode
build: {
lib: {
// Could also be a dictionary or array of multiple entry points.
entry: 'src/index.ts',
name: '${options.project}',
fileName: 'index',
// Change this to the formats you want to support.
// Don't forgot to update your package.json as well.
formats: ['es', 'cjs']
},
rollupOptions: {
// External packages that should not be bundled into your library.
external: [${
options.uiFramework === 'react'
? "'react', 'react-dom', 'react/jsx-runtime'"
: ''
}]
}
},`
: '';
switch (options.uiFramework) { switch (options.uiFramework) {
case 'react': case 'react':
@ -378,17 +415,20 @@ ${options.includeVitest ? '/// <reference types="vitest" />' : ''}
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react'; import react from '@vitejs/plugin-react';
import ViteTsConfigPathsPlugin from 'vite-tsconfig-paths'; import ViteTsConfigPathsPlugin from 'vite-tsconfig-paths';
${options.includeLib ? "import dts from 'vite-plugin-dts';" : ''}
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
${options.includeLib ? dtsPlugin : ''}
react(), react(),
ViteTsConfigPathsPlugin({ ViteTsConfigPathsPlugin({
root: '${offsetFromRoot(projectConfig.root)}', root: '${offsetFromRoot(projectConfig.root)}',
projects: ['tsconfig.base.json'], projects: ['tsconfig.base.json'],
}), }),
], ],
${options.inSourceTests ? defineOption : ''} ${buildOption}
${options.includeVitest ? testOption : ''} ${defineOption}
${testOption}
});`; });`;
break; break;
case 'none': case 'none':
@ -396,16 +436,19 @@ ${options.includeVitest ? '/// <reference types="vitest" />' : ''}
${options.includeVitest ? '/// <reference types="vitest" />' : ''} ${options.includeVitest ? '/// <reference types="vitest" />' : ''}
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
import ViteTsConfigPathsPlugin from 'vite-tsconfig-paths'; import ViteTsConfigPathsPlugin from 'vite-tsconfig-paths';
${options.includeLib ? "import dts from 'vite-plugin-dts';" : ''}
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
${options.includeLib ? dtsPlugin : ''}
ViteTsConfigPathsPlugin({ ViteTsConfigPathsPlugin({
root: '${offsetFromRoot(projectConfig.root)}', root: '${offsetFromRoot(projectConfig.root)}',
projects: ['tsconfig.base.json'], projects: ['tsconfig.base.json'],
}), }),
], ],
${options.inSourceTests ? defineOption : ''} ${buildOption}
${options.includeVitest ? testOption : ''} ${defineOption}
${testOption}
});`; });`;
break; break;
default: default:

View File

@ -8,3 +8,4 @@ export const vitePluginVueVersion = '^3.2.0';
export const vitePluginVueJsxVersion = '^2.1.1'; export const vitePluginVueJsxVersion = '^2.1.1';
export const viteTsConfigPathsVersion = '^3.5.2'; export const viteTsConfigPathsVersion = '^3.5.2';
export const jsdomVersion = '~20.0.3'; export const jsdomVersion = '~20.0.3';
export const vitePluginDtsVersion = '~1.7.1';

View File

@ -168,6 +168,12 @@ describe('app', () => {
expect(tree.exists('apps/my-app-e2e/cypress.config.ts')).toBeTruthy(); expect(tree.exists('apps/my-app-e2e/cypress.config.ts')).toBeTruthy();
expect(tree.exists('apps/my-app/index.html')).toBeTruthy(); expect(tree.exists('apps/my-app/index.html')).toBeTruthy();
expect(tree.exists('apps/my-app/vite.config.ts')).toBeTruthy(); expect(tree.exists('apps/my-app/vite.config.ts')).toBeTruthy();
expect(
tree.exists(`apps/my-app/environments/environment.ts`)
).toBeFalsy();
expect(
tree.exists(`apps/my-app/environments/environment.prod.ts`)
).toBeFalsy();
}); });
it('should extend from root tsconfig.json when no tsconfig.base.json', async () => { it('should extend from root tsconfig.json when no tsconfig.base.json', async () => {

View File

@ -192,6 +192,8 @@ export async function applicationGenerator(host: Tree, schema: Schema) {
const webTask = await webInitGenerator(host, { const webTask = await webInitGenerator(host, {
...options, ...options,
skipFormat: true, skipFormat: true,
// Vite does not use babel by default
skipBabelConfig: options.bundler === 'vite',
}); });
tasks.push(webTask); tasks.push(webTask);
@ -199,6 +201,10 @@ export async function applicationGenerator(host: Tree, schema: Schema) {
await addProject(host, options); await addProject(host, options);
if (options.bundler === 'vite') { if (options.bundler === 'vite') {
// We recommend users use `import.meta.env.MODE` and other variables in their code to differentiate between production and development.
// See: https://vitejs.dev/guide/env-and-mode.html
host.delete(joinPathFragments(options.appProjectRoot, 'src/environments'));
const viteTask = await viteConfigurationGenerator(host, { const viteTask = await viteConfigurationGenerator(host, {
uiFramework: 'react', uiFramework: 'react',
project: options.projectName, project: options.projectName,

View File

@ -42,14 +42,16 @@ function updateDependencies(tree: Tree, schema: Schema) {
); );
} }
function initRootBabelConfig(tree: Tree) { function initRootBabelConfig(tree: Tree, schema: Schema) {
if (tree.exists('/babel.config.json') || tree.exists('/babel.config.js')) { if (tree.exists('/babel.config.json') || tree.exists('/babel.config.js')) {
return; return;
} }
writeJson(tree, '/babel.config.json', { if (!schema.skipBabelConfig) {
babelrcRoots: ['*'], // Make sure .babelrc files other than root can be loaded in a monorepo writeJson(tree, '/babel.config.json', {
}); babelrcRoots: ['*'], // Make sure .babelrc files other than root can be loaded in a monorepo
});
}
const workspaceConfiguration = readWorkspaceConfiguration(tree); const workspaceConfiguration = readWorkspaceConfiguration(tree);
@ -80,7 +82,7 @@ export async function webInitGenerator(tree: Tree, schema: Schema) {
const installTask = updateDependencies(tree, schema); const installTask = updateDependencies(tree, schema);
tasks.push(installTask); tasks.push(installTask);
} }
initRootBabelConfig(tree); initRootBabelConfig(tree, schema);
if (!schema.skipFormat) { if (!schema.skipFormat) {
await formatFiles(tree); await formatFiles(tree);
} }

View File

@ -4,4 +4,5 @@ export interface Schema {
e2eTestRunner?: 'cypress' | 'none'; e2eTestRunner?: 'cypress' | 'none';
skipFormat?: boolean; skipFormat?: boolean;
skipPackageJson?: boolean; skipPackageJson?: boolean;
skipBabelConfig?: boolean;
} }

View File

@ -33,6 +33,11 @@
"description": "Do not add dependencies to `package.json`.", "description": "Do not add dependencies to `package.json`.",
"type": "boolean", "type": "boolean",
"default": false "default": false
},
"skipBabelConfig": {
"description": "Do not generate a root babel.config.json (if babel is not needed).",
"type": "boolean",
"default": false
} }
}, },
"required": [] "required": []