diff --git a/docs/generated/packages/react.json b/docs/generated/packages/react.json index da3d285d6c3e40..25baffbb285df9 100644 --- a/docs/generated/packages/react.json +++ b/docs/generated/packages/react.json @@ -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 `/project.json` rather than including it inside `workspace.json`.", "type": "boolean" }, + "bundler": { + "type": "string", + "description": "The bundler to use.", + "enum": ["vite", "rollup"], + "default": "rollup" + }, "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`.", diff --git a/docs/generated/packages/vite.json b/docs/generated/packages/vite.json index b489b5f889f34e..d2b8b07285ce60 100644 --- a/docs/generated/packages/vite.json +++ b/docs/generated/packages/vite.json @@ -60,6 +60,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.", diff --git a/docs/generated/packages/web.json b/docs/generated/packages/web.json index aba85caa4b0e63..d614412071f40a 100644 --- a/docs/generated/packages/web.json +++ b/docs/generated/packages/web.json @@ -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": [], diff --git a/e2e/react/src/react-package.test.ts b/e2e/react/src/react-package.test.ts index 89e39cf70a6492..76c19c8b2f4bb1 100644 --- a/e2e/react/src/react-package.test.ts +++ b/e2e/react/src/react-package.test.ts @@ -252,4 +252,22 @@ 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} --buildable --bundler=vite --no-interactive` + ); + + const result = await runCLIAsync(`build ${libName}`); + + expect(result).toMatch(/Vite builder finished/); + + checkFilesExist( + `dist/libs/${libName}/package.json`, + `dist/libs/${libName}/index.js`, + `dist/libs/${libName}/index.mjs` + ); + }); }); diff --git a/e2e/vite/src/vite.test.ts b/e2e/vite/src/vite.test.ts index 498e509250cd28..a94d2da0308780 100644 --- a/e2e/vite/src/vite.test.ts +++ b/e2e/vite/src/vite.test.ts @@ -1,4 +1,5 @@ import { + checkFilesExist, cleanupProject, createFile, exists, @@ -32,38 +33,23 @@ describe('Vite Plugin', () => { `apps/${myApp}/index.html`, ` - + - + My App - + - - + + -
- +
+ ` ); - 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`, ` @@ -169,20 +155,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( @@ -207,76 +179,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 ( - <> -

{environment.myTestVar}

-

Welcome ${myApp}!

- - ); - } - 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`, @@ -295,18 +202,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; diff --git a/packages/react/src/generators/application/application.spec.ts b/packages/react/src/generators/application/application.spec.ts index 8c612c41d1736a..f34524051f3c27 100644 --- a/packages/react/src/generators/application/application.spec.ts +++ b/packages/react/src/generators/application/application.spec.ts @@ -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 () => { diff --git a/packages/react/src/generators/application/application.ts b/packages/react/src/generators/application/application.ts index f50c528fb79189..278383a3177521 100644 --- a/packages/react/src/generators/application/application.ts +++ b/packages/react/src/generators/application/application.ts @@ -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); diff --git a/packages/react/src/generators/application/lib/normalize-options.ts b/packages/react/src/generators/application/lib/normalize-options.ts index 27e3a60ddd3cbb..cffc5efb7f8646 100644 --- a/packages/react/src/generators/application/lib/normalize-options.ts +++ b/packages/react/src/generators/application/lib/normalize-options.ts @@ -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; } diff --git a/packages/react/src/generators/application/lib/set-defaults.ts b/packages/react/src/generators/application/lib/set-defaults.ts index 435b9b130753bc..a9b71ee6a9710b 100644 --- a/packages/react/src/generators/application/lib/set-defaults.ts +++ b/packages/react/src/generators/application/lib/set-defaults.ts @@ -30,6 +30,7 @@ export function setDefaults(host: Tree, options: NormalizedSchema) { style: options.style, unitTestRunner: options.unitTestRunner, linter: options.linter, + bundler: options.bundler, ...prev.application, }, component: { diff --git a/packages/react/src/generators/application/schema.d.ts b/packages/react/src/generators/application/schema.d.ts index 7d5f4e49c6b07a..ead0831abf0917 100644 --- a/packages/react/src/generators/application/schema.d.ts +++ b/packages/react/src/generators/application/schema.d.ts @@ -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'; } diff --git a/packages/react/src/generators/application/schema.json b/packages/react/src/generators/application/schema.json index dc0ff922fdd636..63b40df77f0327 100644 --- a/packages/react/src/generators/application/schema.json +++ b/packages/react/src/generators/application/schema.json @@ -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" } }, diff --git a/packages/react/src/generators/init/schema.d.ts b/packages/react/src/generators/init/schema.d.ts index 7fccc4697c21c4..65431d3fac869c 100644 --- a/packages/react/src/generators/init/schema.d.ts +++ b/packages/react/src/generators/init/schema.d.ts @@ -1,6 +1,7 @@ export interface InitSchema { unitTestRunner?: 'jest' | 'vitest' | 'none'; e2eTestRunner?: 'cypress' | 'none'; + skipBabelConfig?: boolean; skipFormat?: boolean; skipPackageJson?: boolean; js?: boolean; diff --git a/packages/react/src/generators/init/schema.json b/packages/react/src/generators/init/schema.json index 7fe840439b54f9..458ee719ee2b6b 100644 --- a/packages/react/src/generators/init/schema.json +++ b/packages/react/src/generators/init/schema.json @@ -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, diff --git a/packages/react/src/generators/library/files/lib/.babelrc__tmpl__ b/packages/react/src/generators/library/files/common/.babelrc__tmpl__ similarity index 100% rename from packages/react/src/generators/library/files/lib/.babelrc__tmpl__ rename to packages/react/src/generators/library/files/common/.babelrc__tmpl__ diff --git a/packages/react/src/generators/library/files/lib/README.md b/packages/react/src/generators/library/files/common/README.md similarity index 100% rename from packages/react/src/generators/library/files/lib/README.md rename to packages/react/src/generators/library/files/common/README.md diff --git a/packages/react/src/generators/library/files/common/package.json__tmpl__ b/packages/react/src/generators/library/files/common/package.json__tmpl__ new file mode 100644 index 00000000000000..12d37480dfcb9e --- /dev/null +++ b/packages/react/src/generators/library/files/common/package.json__tmpl__ @@ -0,0 +1,12 @@ +{ + "name": "<%= name %>", + "version": "0.0.1", + "main": "./index.js", + "module": "./index.mjs", + "exports": { + ".": { + "import": "./index.mjs", + "require": "./index.js" + } + } +} diff --git a/packages/react/src/generators/library/files/lib/src/index.ts__tmpl__ b/packages/react/src/generators/library/files/common/src/index.ts__tmpl__ similarity index 100% rename from packages/react/src/generators/library/files/lib/src/index.ts__tmpl__ rename to packages/react/src/generators/library/files/common/src/index.ts__tmpl__ diff --git a/packages/react/src/generators/library/files/lib/tsconfig.json__tmpl__ b/packages/react/src/generators/library/files/common/tsconfig.json__tmpl__ similarity index 100% rename from packages/react/src/generators/library/files/lib/tsconfig.json__tmpl__ rename to packages/react/src/generators/library/files/common/tsconfig.json__tmpl__ diff --git a/packages/react/src/generators/library/files/lib/tsconfig.lib.json__tmpl__ b/packages/react/src/generators/library/files/common/tsconfig.lib.json__tmpl__ similarity index 100% rename from packages/react/src/generators/library/files/lib/tsconfig.lib.json__tmpl__ rename to packages/react/src/generators/library/files/common/tsconfig.lib.json__tmpl__ diff --git a/packages/react/src/generators/library/files/lib/package.json__tmpl__ b/packages/react/src/generators/library/files/lib/package.json__tmpl__ deleted file mode 100644 index fa518765a372fc..00000000000000 --- a/packages/react/src/generators/library/files/lib/package.json__tmpl__ +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "<%= name %>", - "version": "0.0.1" -} diff --git a/packages/react/src/generators/library/files/vite/index.html__tmpl__ b/packages/react/src/generators/library/files/vite/index.html__tmpl__ new file mode 100644 index 00000000000000..4c4c81ceb583c2 --- /dev/null +++ b/packages/react/src/generators/library/files/vite/index.html__tmpl__ @@ -0,0 +1,15 @@ + + + + + <%= className %> Demo + + + + + + +
+ + + diff --git a/packages/react/src/generators/library/files/vite/package.json__tmpl__ b/packages/react/src/generators/library/files/vite/package.json__tmpl__ new file mode 100644 index 00000000000000..28c580ee8ec3a6 --- /dev/null +++ b/packages/react/src/generators/library/files/vite/package.json__tmpl__ @@ -0,0 +1,11 @@ +{ + "name": "<%= name %>", + "version": "0.0.1", + "main": "./index.js", + "exports": { + ".": { + "import": "./index.mjs", + "require": "./index.js" + } + } +} diff --git a/packages/react/src/generators/library/files/vite/src/demo.tsx__tmpl__ b/packages/react/src/generators/library/files/vite/src/demo.tsx__tmpl__ new file mode 100644 index 00000000000000..617f6a880e9acf --- /dev/null +++ b/packages/react/src/generators/library/files/vite/src/demo.tsx__tmpl__ @@ -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( +

<%= className %> Demo

+); +<% } %> \ No newline at end of file diff --git a/packages/react/src/generators/library/library.spec.ts b/packages/react/src/generators/library/library.spec.ts index ed3952176b2811..1fe21bf25a706b 100644 --- a/packages/react/src/generators/library/library.spec.ts +++ b/packages/react/src/generators/library/library.spec.ts @@ -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', }); diff --git a/packages/react/src/generators/library/library.ts b/packages/react/src/generators/library/library.ts index 25d4c356b7fcc3..ad010cb68f8f30 100644 --- a/packages/react/src/generators/library/library.ts +++ b/packages/react/src/generators/library/library.ts @@ -44,10 +44,11 @@ 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'; + export interface NormalizedSchema extends Schema { name: string; fileName: string; @@ -57,6 +58,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 +74,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 +93,17 @@ 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, + includeVitest: true, + }); + tasks.push(viteTask); + } + if (options.unitTestRunner === 'jest') { const jestTask = await jestProjectGenerator(host, { ...options, @@ -110,7 +124,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 +316,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`); } @@ -433,8 +462,10 @@ function normalizeOptions(host: Tree, options: Schema): NormalizedSchema { const importPath = options.importPath || getImportPath(npmScope, projectDirectory); - const normalized: NormalizedSchema = { + const normalized = { ...options, + compiler: options.compiler ?? 'babel', + bundler: options.bundler ?? 'rollup', fileName, routePath: `/${name}`, name: projectName, @@ -442,7 +473,16 @@ function normalizeOptions(host: Tree, options: Schema): NormalizedSchema { 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); diff --git a/packages/react/src/generators/library/schema.d.ts b/packages/react/src/generators/library/schema.d.ts index 9adde64ac382d8..3aac2d46fa4296 100644 --- a/packages/react/src/generators/library/schema.d.ts +++ b/packages/react/src/generators/library/schema.d.ts @@ -11,7 +11,7 @@ export interface Schema { pascalCaseFiles?: boolean; routing?: boolean; appProject?: string; - unitTestRunner: 'jest' | 'vitest' | 'none'; + unitTestRunner?: 'jest' | 'vitest' | 'none'; inSourceTests?: boolean; linter: Linter; component?: boolean; @@ -24,5 +24,6 @@ export interface Schema { setParserOptionsProject?: boolean; standaloneConfig?: boolean; compiler?: 'babel' | 'swc'; + bundler?: 'rollup' | 'vite'; skipPackageJson?: boolean; } diff --git a/packages/react/src/generators/library/schema.json b/packages/react/src/generators/library/schema.json index ff4ade44655507..f5d12c1c79f7ac 100644 --- a/packages/react/src/generators/library/schema.json +++ b/packages/react/src/generators/library/schema.json @@ -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 `/project.json` rather than including it inside `workspace.json`.", "type": "boolean" }, + "bundler": { + "type": "string", + "description": "The bundler to use.", + "enum": ["vite", "rollup"], + "default": "rollup" + }, "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`.", diff --git a/packages/vite/src/executors/build/build.impl.ts b/packages/vite/src/executors/build/build.impl.ts index f9d1c3fc14569d..69b24b052c6a9e 100644 --- a/packages/vite/src/executors/build/build.impl.ts +++ b/packages/vite/src/executors/build/build.impl.ts @@ -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 builder starting ...`); + const buildResult = await runInstance(await getBuildConfig(options, context)); + logger.info(`NX Vite builder 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 builder starting ...`); - await runInstance(await getBuildConfig(options, context)); - logger.info(`NX Vite builder finished ...`); - logger.info(`NX Vite files available in ${options.outputPath}`); return { success: true }; } diff --git a/packages/vite/src/generators/configuration/configuration.spec.ts b/packages/vite/src/generators/configuration/configuration.spec.ts index c95a8b30d49205..fce8b9718829b3 100644 --- a/packages/vite/src/generators/configuration/configuration.spec.ts +++ b/packages/vite/src/generators/configuration/configuration.spec.ts @@ -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'"); + }); + }); }); diff --git a/packages/vite/src/generators/configuration/schema.d.ts b/packages/vite/src/generators/configuration/schema.d.ts index 40b05c200d841c..9533d025cff33e 100644 --- a/packages/vite/src/generators/configuration/schema.d.ts +++ b/packages/vite/src/generators/configuration/schema.d.ts @@ -4,4 +4,5 @@ export interface Schema { newProject?: boolean; includeVitest?: boolean; inSourceTests?: boolean; + includeLib?: boolean; } diff --git a/packages/vite/src/generators/configuration/schema.json b/packages/vite/src/generators/configuration/schema.json index 1aab0f1df1336b..c48db6c5e745a4 100644 --- a/packages/vite/src/generators/configuration/schema.json +++ b/packages/vite/src/generators/configuration/schema.json @@ -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.", diff --git a/packages/vite/src/generators/vitest/vitest.spec.ts b/packages/vite/src/generators/vitest/vitest.spec.ts index 17a8feff06830a..13b40713e11e59 100644 --- a/packages/vite/src/generators/vitest/vitest.spec.ts +++ b/packages/vite/src/generators/vitest/vitest.spec.ts @@ -117,6 +117,7 @@ describe('vitest generator', () => { }), ], + test: { globals: true, environment: 'jsdom', @@ -150,6 +151,7 @@ describe('vitest generator', () => { projects: [join(__dirname, 'tsconfig.json')], }), ], + define: { 'import.meta.vitest': undefined }, diff --git a/packages/vite/src/utils/generator-utils.ts b/packages/vite/src/utils/generator-utils.ts index d97b13a4506b83..e4e6e6e0dbf9e1 100644 --- a/packages/vite/src/utils/generator-utils.ts +++ b/packages/vite/src/utils/generator-utils.ts @@ -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,39 @@ 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 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': @@ -388,8 +419,9 @@ ${options.includeVitest ? '/// ' : ''} projects: [join(__dirname, 'tsconfig.json')], }), ], - ${options.inSourceTests ? defineOption : ''} - ${options.includeVitest ? testOption : ''} + ${buildOption} + ${defineOption} + ${testOption} });`; break; case 'none': @@ -406,8 +438,9 @@ ${options.includeVitest ? '/// ' : ''} projects: [join(__dirname, 'tsconfig.json')], }), ], - ${options.inSourceTests ? defineOption : ''} - ${options.includeVitest ? testOption : ''} + ${buildOption} + ${defineOption} + ${testOption} });`; break; default: diff --git a/packages/web/src/generators/application/application.spec.ts b/packages/web/src/generators/application/application.spec.ts index 097c9ad0185bc4..9a6c258f1f39ab 100644 --- a/packages/web/src/generators/application/application.spec.ts +++ b/packages/web/src/generators/application/application.spec.ts @@ -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 () => { diff --git a/packages/web/src/generators/application/application.ts b/packages/web/src/generators/application/application.ts index 3e658531b5cde1..bd7cced40f496a 100644 --- a/packages/web/src/generators/application/application.ts +++ b/packages/web/src/generators/application/application.ts @@ -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, diff --git a/packages/web/src/generators/init/init.ts b/packages/web/src/generators/init/init.ts index 3df5faf8b426c9..dfa1ba21492eab 100644 --- a/packages/web/src/generators/init/init.ts +++ b/packages/web/src/generators/init/init.ts @@ -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); } diff --git a/packages/web/src/generators/init/schema.d.ts b/packages/web/src/generators/init/schema.d.ts index 00c2cc6a747670..7276adf217146c 100644 --- a/packages/web/src/generators/init/schema.d.ts +++ b/packages/web/src/generators/init/schema.d.ts @@ -4,4 +4,5 @@ export interface Schema { e2eTestRunner?: 'cypress' | 'none'; skipFormat?: boolean; skipPackageJson?: boolean; + skipBabelConfig?: boolean; } diff --git a/packages/web/src/generators/init/schema.json b/packages/web/src/generators/init/schema.json index 93f02a32fe7afa..8e7a8daab5367f 100644 --- a/packages/web/src/generators/init/schema.json +++ b/packages/web/src/generators/init/schema.json @@ -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": []