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",
"default": false
},
"skipBabelConfig": {
"description": "Do not generate a root babel.config.json (if babel is not needed).",
"type": "boolean",
"default": false
},
"js": {
"type": "boolean",
"default": false,
@ -244,7 +249,7 @@
"description": "The bundler to use.",
"type": "string",
"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"
}
},
@ -339,8 +344,7 @@
"unitTestRunner": {
"type": "string",
"enum": ["jest", "vitest", "none"],
"description": "Test runner to use for unit tests.",
"default": "jest"
"description": "Test runner to use for unit tests."
},
"inSourceTests": {
"type": "boolean",
@ -384,7 +388,7 @@
"buildable": {
"type": "boolean",
"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": {
"type": "string",
@ -419,11 +423,17 @@
"description": "Split the project configuration into `<projectRoot>/project.json` rather than including it inside `workspace.json`.",
"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": {
"type": "string",
"enum": ["babel", "swc"],
"default": "babel",
"description": "Which compiler to use."
"description": "Which compiler to use. Does not apply if bundler is set to Vite."
},
"skipPackageJson": {
"description": "Do not add dependencies to `package.json`.",

View File

@ -31,6 +31,11 @@
"enum": ["react", "none"],
"default": "react",
"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",
@ -60,6 +65,12 @@
"x-dropdown": "project",
"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": {
"type": "string",
"description": "UI Framework to use for Vite.",

View File

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

View File

@ -97,7 +97,7 @@ describe('Build React libraries and apps', () => {
afterEach(() => {
killPorts();
cleanupProject();
// cleanupProject();
});
describe('Buildable libraries', () => {
@ -252,4 +252,21 @@ export async function h() { return 'c'; }
}).toThrow();
}, 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 {
checkFilesExist,
cleanupProject,
createFile,
exists,
@ -32,38 +33,23 @@ describe('Vite Plugin', () => {
`apps/${myApp}/index.html`,
`
<!DOCTYPE html>
<html lang="en">
<html lang='en'>
<head>
<meta charset="utf-8" />
<meta charset='utf-8' />
<title>My App</title>
<base href="/" />
<base href='/' />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
<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/main.tsx"></script>
<div id='root'></div>
<script type='module' src='src/main.tsx'></script>
</body>
</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(
`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 () => {
const port = 4212;
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', () => {
beforeEach(() => {
proj = newProject();
runCLI(`generate @nrwl/react:app ${myApp} --bundler=webpack`);
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(
`apps/${myApp}/src/app/app.tsx`,
@ -294,18 +201,6 @@ describe('Vite Plugin', () => {
);
});
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 () => {
const port = 4212;
@ -348,7 +243,7 @@ describe('Vite Plugin', () => {
readFile(`dist/apps/${myApp}/assets/${mainBundle}`)
).toBeDefined();
rmDist();
}, 200000);
}, 200_000);
});
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}`
);
});
});
}),
100_000;
});
describe('should be able to create libs that use vitest', () => {
const lib = uniq('my-lib');
beforeEach(() => {
proj = newProject();
});
}),
100_000;
it('should be able to run tests', async () => {
runCLI(`generate @nrwl/react:lib ${lib} --unitTestRunner=vitest`);
@ -408,7 +305,8 @@ describe('Vite Plugin', () => {
expect(result.combinedOutput).toContain(
`Successfully ran target test for project ${lib}`
);
});
}),
100_000;
it('should be able to run tests with inSourceTests set to true', async () => {
runCLI(
@ -432,6 +330,6 @@ describe('Vite Plugin', () => {
const result = await runCLIAsync(`test ${lib}`);
expect(result.combinedOutput).toContain(`1 passed`);
});
}, 100_000);
});
});

View File

@ -19,7 +19,6 @@ describe('app', () => {
compiler: 'babel',
e2eTestRunner: 'cypress',
skipFormat: false,
unitTestRunner: 'jest',
name: 'myApp',
linter: Linter.EsLint,
style: 'css',
@ -381,6 +380,12 @@ describe('app', () => {
expect(targetConfig.build.options).toEqual({
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 () => {

View File

@ -80,6 +80,7 @@ export async function applicationGenerator(host: Tree, schema: Schema) {
const initTask = await reactInitGenerator(host, {
...options,
skipFormat: true,
skipBabelConfig: options.bundler === 'vite',
});
tasks.push(initTask);
@ -88,6 +89,10 @@ export async function applicationGenerator(host: Tree, schema: Schema) {
addProject(host, options);
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, {
uiFramework: 'react',
project: options.projectName,
@ -111,9 +116,11 @@ export async function applicationGenerator(host: Tree, schema: Schema) {
const cypressTask = await addCypress(host, options);
tasks.push(cypressTask);
const jestTask = await addJest(host, options);
tasks.push(jestTask);
updateSpecConfig(host, options);
if (options.unitTestRunner === 'jest') {
const jestTask = await addJest(host, options);
tasks.push(jestTask);
updateSpecConfig(host, options);
}
const styledTask = addStyledModuleDependencies(host, options.styledModule);
tasks.push(styledTask);
const routingTask = addRouting(host, options);

View File

@ -40,20 +40,7 @@ export function normalizeOptions(
assertValidStyle(options.style);
if (options.bundler === 'vite') {
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 {
const normalized = {
...options,
name: names(options.name).fileName,
projectName: appProjectName,
@ -63,5 +50,18 @@ export function normalizeOptions(
fileName,
styledModule,
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,
application: {
style: options.style,
unitTestRunner: options.unitTestRunner,
linter: options.linter,
bundler: options.bundler,
...prev.application,
},
component: {
@ -38,7 +38,6 @@ export function setDefaults(host: Tree, options: NormalizedSchema) {
},
library: {
style: options.style,
unitTestRunner: options.unitTestRunner,
linter: options.linter,
...prev.library,
},

View File

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

View File

@ -185,7 +185,7 @@
"description": "The bundler to use.",
"type": "string",
"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"
}
},

View File

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

View File

@ -28,6 +28,11 @@
"type": "boolean",
"default": false
},
"skipBabelConfig": {
"description": "Do not generate a root babel.config.json (if babel is not needed).",
"type": "boolean",
"default": false
},
"js": {
"type": "boolean",
"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 () => {
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(
"['babel-jest', { presets: ['@nrwl/react/babel'] }]"
);
@ -280,6 +284,7 @@ describe('lib', () => {
await libraryGenerator(appTree, {
...defaultSchema,
directory: 'myDir',
buildable: true,
compiler: 'babel',
});
expect(
@ -725,6 +730,7 @@ describe('lib', () => {
it('should install swc dependencies if needed', async () => {
await libraryGenerator(appTree, {
...defaultSchema,
buildable: true,
compiler: 'swc',
});
const packageJson = readJson(appTree, 'package.json');
@ -764,6 +770,7 @@ describe('lib', () => {
await libraryGenerator(appTree, {
...defaultSchema,
style,
compiler: 'babel',
name: 'myLib',
});

View File

@ -44,10 +44,12 @@ import {
typesReactRouterDomVersion,
} from '../../utils/versions';
import componentGenerator from '../component/component';
import init from '../init/init';
import initGenerator from '../init/init';
import { Schema } from './schema';
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 {
name: string;
fileName: string;
@ -57,6 +59,7 @@ export interface NormalizedSchema extends Schema {
parsedTags: string[];
appMain?: string;
appSourceRoot?: string;
unitTestRunner: 'jest' | 'vitest' | 'none';
}
export async function libraryGenerator(host: Tree, schema: Schema) {
@ -72,10 +75,11 @@ export async function libraryGenerator(host: Tree, schema: Schema) {
options.style = 'none';
}
const initTask = await init(host, {
const initTask = await initGenerator(host, {
...options,
e2eTestRunner: 'none',
skipFormat: true,
skipBabelConfig: options.bundler === 'vite',
});
tasks.push(initTask);
@ -90,6 +94,18 @@ export async function libraryGenerator(host: Tree, schema: Schema) {
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') {
const jestTask = await jestProjectGenerator(host, {
...options,
@ -110,7 +126,10 @@ export async function libraryGenerator(host: Tree, schema: Schema) {
);
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, {
uiFramework: 'react',
project: options.name,
@ -299,22 +318,34 @@ function updateBaseTsConfig(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(
host,
joinPathFragments(__dirname, './files/lib'),
joinPathFragments(__dirname, './files/common'),
options.projectRoot,
{
...options,
...names(options.name),
tmpl: '',
offsetFromRoot: offsetFromRoot(options.projectRoot),
rootTsConfigPath: getRelativePathToRootTsConfig(
host,
options.projectRoot
),
}
substitutions
);
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) {
host.delete(`${options.projectRoot}/package.json`);
}
@ -415,59 +446,6 @@ function readComponent(
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) {
return updateJson(host, `${options.projectRoot}/package.json`, (json) => {
json.name = options.importPath;

View File

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

View File

@ -81,8 +81,7 @@
"unitTestRunner": {
"type": "string",
"enum": ["jest", "vitest", "none"],
"description": "Test runner to use for unit tests.",
"default": "jest"
"description": "Test runner to use for unit tests."
},
"inSourceTests": {
"type": "boolean",
@ -126,7 +125,7 @@
"buildable": {
"type": "boolean",
"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": {
"type": "string",
@ -161,11 +160,17 @@
"description": "Split the project configuration into `<projectRoot>/project.json` rather than including it inside `workspace.json`.",
"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": {
"type": "string",
"enum": ["babel", "swc"],
"default": "babel",
"description": "Which compiler to use."
"description": "Which compiler to use. Does not apply if bundler is set to Vite."
},
"skipPackageJson": {
"description": "Do not add dependencies to `package.json`.",

View File

@ -4,25 +4,42 @@ import 'dotenv/config';
import { getBuildConfig } from '../../utils/options-utils';
import { ViteBuildExecutorOptions } from './schema';
import { copyAssets } from '@nrwl/js';
import { existsSync } from 'fs';
import { join } from 'path';
export default async function viteBuildExecutor(
options: ViteBuildExecutorOptions,
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(
{
outputPath: options.outputPath,
assets: options.assets,
assets: assets,
},
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 };
}

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 { nxVersion } from '../../utils/versions';
@ -116,4 +121,27 @@ describe('@nrwl/vite:configuration', () => {
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, {
uiFramework: schema.uiFramework,
includeLib: schema.includeLib,
});
tasks.push(initTask);

View File

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

View File

@ -16,6 +16,12 @@
"x-dropdown": "project",
"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": {
"type": "string",
"description": "UI Framework to use for Vite.",

View File

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

View File

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

View File

@ -11,6 +11,11 @@
"enum": ["react", "none"],
"default": "react",
"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"

View File

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

View File

@ -150,21 +150,23 @@ export function addOrChangeBuildTarget(
options: buildOptions,
configurations: {
development: {},
production: {
fileReplacements: [
{
replace: joinPathFragments(
project.sourceRoot,
'environments/environment.ts'
),
with: joinPathFragments(
project.sourceRoot,
'environments/environment.prod.ts'
),
production: options.includeLib
? {}
: {
fileReplacements: [
{
replace: joinPathFragments(
project.sourceRoot,
'environments/environment.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 = '';
const testOption = `test: {
const testOption = options.includeVitest
? `test: {
globals: true,
environment: 'jsdom',
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}']`
: ''
}
},`;
},`
: '';
const defineOption = `define: {
const defineOption = options.inSourceTests
? `define: {
'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) {
case 'react':
@ -378,17 +415,20 @@ ${options.includeVitest ? '/// <reference types="vitest" />' : ''}
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import ViteTsConfigPathsPlugin from 'vite-tsconfig-paths';
${options.includeLib ? "import dts from 'vite-plugin-dts';" : ''}
export default defineConfig({
plugins: [
${options.includeLib ? dtsPlugin : ''}
react(),
ViteTsConfigPathsPlugin({
root: '${offsetFromRoot(projectConfig.root)}',
projects: ['tsconfig.base.json'],
}),
],
${options.inSourceTests ? defineOption : ''}
${options.includeVitest ? testOption : ''}
${buildOption}
${defineOption}
${testOption}
});`;
break;
case 'none':
@ -396,16 +436,19 @@ ${options.includeVitest ? '/// <reference types="vitest" />' : ''}
${options.includeVitest ? '/// <reference types="vitest" />' : ''}
import { defineConfig } from 'vite';
import ViteTsConfigPathsPlugin from 'vite-tsconfig-paths';
${options.includeLib ? "import dts from 'vite-plugin-dts';" : ''}
export default defineConfig({
plugins: [
${options.includeLib ? dtsPlugin : ''}
ViteTsConfigPathsPlugin({
root: '${offsetFromRoot(projectConfig.root)}',
projects: ['tsconfig.base.json'],
}),
],
${options.inSourceTests ? defineOption : ''}
${options.includeVitest ? testOption : ''}
${buildOption}
${defineOption}
${testOption}
});`;
break;
default:

View File

@ -8,3 +8,4 @@ export const vitePluginVueVersion = '^3.2.0';
export const vitePluginVueJsxVersion = '^2.1.1';
export const viteTsConfigPathsVersion = '^3.5.2';
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/index.html')).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 () => {

View File

@ -192,6 +192,8 @@ export async function applicationGenerator(host: Tree, schema: Schema) {
const webTask = await webInitGenerator(host, {
...options,
skipFormat: true,
// Vite does not use babel by default
skipBabelConfig: options.bundler === 'vite',
});
tasks.push(webTask);
@ -199,6 +201,10 @@ export async function applicationGenerator(host: Tree, schema: Schema) {
await addProject(host, options);
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, {
uiFramework: 'react',
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')) {
return;
}
writeJson(tree, '/babel.config.json', {
babelrcRoots: ['*'], // Make sure .babelrc files other than root can be loaded in a monorepo
});
if (!schema.skipBabelConfig) {
writeJson(tree, '/babel.config.json', {
babelrcRoots: ['*'], // Make sure .babelrc files other than root can be loaded in a monorepo
});
}
const workspaceConfiguration = readWorkspaceConfiguration(tree);
@ -80,7 +82,7 @@ export async function webInitGenerator(tree: Tree, schema: Schema) {
const installTask = updateDependencies(tree, schema);
tasks.push(installTask);
}
initRootBabelConfig(tree);
initRootBabelConfig(tree, schema);
if (!schema.skipFormat) {
await formatFiles(tree);
}

View File

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

View File

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