Compare commits

...

12 Commits

Author SHA1 Message Date
Katerina Skroumpelou d0173c49fa fix(vite): better extra args resolution (#20708) 2023-12-12 09:46:47 -05:00
Katerina Skroumpelou 718e730783 cleanup(misc): add packages option to e2e newProject function (#20713) 2023-12-12 09:46:47 -05:00
Jonathan Cammisuli 55327f79e4 fix(core): properly handle negated paths in cache outputs (#20661) 2023-12-12 09:46:47 -05:00
Jonathan Cammisuli f61667509f fix(core): show warning if workspaceRoot starts with ! (#20705) 2023-12-12 09:46:47 -05:00
Jack Hsu 4dc3c8f462 fix(webpack): handle both nx and nrwl scoped executors when migrating config (#20714) 2023-12-12 09:46:47 -05:00
Katerina Skroumpelou d4ff686c10 fix(vite): ignore CJS build deprecated warning (#20719) 2023-12-12 09:46:47 -05:00
Jack Hsu 214907af2a fix(webpack): add standardWebpackConfigFunction option when users opts for a standard config function (#20702) 2023-12-12 08:16:02 -05:00
Tycho Bokdam 57543dba1d fix(webpack): fixed isolatedConfig: false option not composing plugins (#20678) 2023-12-11 16:28:34 -05:00
Leosvel Pérez Espinosa 2ea07b9be9 fix(angular): add missing package update for @angular/pwa (#20690) 2023-12-11 16:11:25 -05:00
Katerina Skroumpelou 8d4d5754eb fix(vite): config migration account for other syntaxes (#20693) 2023-12-11 16:11:25 -05:00
Jack Hsu fb4e280993 fix(react): webpack backwards compat for @nx/react/plugin/webpack (#20697) 2023-12-11 16:11:25 -05:00
Jack Hsu 20fe50b75b fix(webpack): migrate projects without webpackConfig to use webpack.config.js (#20699) 2023-12-11 16:11:25 -05:00
45 changed files with 1053 additions and 528 deletions
+9 -9
View File
@@ -1943,9 +1943,9 @@
"isExternal": false,
"children": [
{
"name": "Manually set up your project to use Vite.js",
"path": "/recipes/vite/set-up-vite-manually",
"id": "set-up-vite-manually",
"name": "Configure Vite on your Nx workspace",
"path": "/recipes/vite/configure-vite",
"id": "configure-vite",
"isExternal": false,
"children": [],
"disableCollapsible": false
@@ -3382,9 +3382,9 @@
"isExternal": false,
"children": [
{
"name": "Manually set up your project to use Vite.js",
"path": "/recipes/vite/set-up-vite-manually",
"id": "set-up-vite-manually",
"name": "Configure Vite on your Nx workspace",
"path": "/recipes/vite/configure-vite",
"id": "configure-vite",
"isExternal": false,
"children": [],
"disableCollapsible": false
@@ -3393,9 +3393,9 @@
"disableCollapsible": false
},
{
"name": "Manually set up your project to use Vite.js",
"path": "/recipes/vite/set-up-vite-manually",
"id": "set-up-vite-manually",
"name": "Configure Vite on your Nx workspace",
"path": "/recipes/vite/configure-vite",
"id": "configure-vite",
"isExternal": false,
"children": [],
"disableCollapsible": false
+16 -16
View File
@@ -2661,14 +2661,14 @@
"file": "",
"itemList": [
{
"id": "set-up-vite-manually",
"name": "Manually set up your project to use Vite.js",
"description": "Manually set up your project to use Vite.js",
"id": "configure-vite",
"name": "Configure Vite on your Nx workspace",
"description": "Configure Vite on your Nx workspace",
"mediaImage": "",
"file": "shared/packages/vite/set-up-vite-manually",
"file": "shared/packages/vite/configure-vite",
"itemList": [],
"isExternal": false,
"path": "/recipes/vite/set-up-vite-manually",
"path": "/recipes/vite/configure-vite",
"tags": []
}
],
@@ -4633,14 +4633,14 @@
"file": "",
"itemList": [
{
"id": "set-up-vite-manually",
"name": "Manually set up your project to use Vite.js",
"description": "Manually set up your project to use Vite.js",
"id": "configure-vite",
"name": "Configure Vite on your Nx workspace",
"description": "Configure Vite on your Nx workspace",
"mediaImage": "",
"file": "shared/packages/vite/set-up-vite-manually",
"file": "shared/packages/vite/configure-vite",
"itemList": [],
"isExternal": false,
"path": "/recipes/vite/set-up-vite-manually",
"path": "/recipes/vite/configure-vite",
"tags": []
}
],
@@ -4648,15 +4648,15 @@
"path": "/recipes/vite",
"tags": []
},
"/recipes/vite/set-up-vite-manually": {
"id": "set-up-vite-manually",
"name": "Manually set up your project to use Vite.js",
"description": "Manually set up your project to use Vite.js",
"/recipes/vite/configure-vite": {
"id": "configure-vite",
"name": "Configure Vite on your Nx workspace",
"description": "Configure Vite on your Nx workspace",
"mediaImage": "",
"file": "shared/packages/vite/set-up-vite-manually",
"file": "shared/packages/vite/configure-vite",
"itemList": [],
"isExternal": false,
"path": "/recipes/vite/set-up-vite-manually",
"path": "/recipes/vite/configure-vite",
"tags": []
},
"/recipes/webpack": {
File diff suppressed because one or more lines are too long
@@ -313,6 +313,11 @@
"default": true,
"x-deprecated": "Automatic configuration of Webpack is deprecated in favor of an explicit 'webpack.config.js' file. This option will be removed in Nx 18. See https://nx.dev/recipes/webpack/webpack-config-setup."
},
"standardWebpackConfigFunction": {
"type": "boolean",
"description": "Set to true if the webpack config exports a standard webpack function, not an Nx-specific one. See: https://webpack.js.org/configuration/configuration-types/#exporting-a-function",
"default": false
},
"extractLicenses": {
"type": "boolean",
"description": "Extract all licenses in a separate file, in the case of production builds only."
+4 -4
View File
@@ -848,10 +848,10 @@
"description": "Vite related recipes",
"itemList": [
{
"id": "set-up-vite-manually",
"name": "Manually set up your project to use Vite.js",
"description": "Manually set up your project to use Vite.js",
"file": "shared/packages/vite/set-up-vite-manually"
"id": "configure-vite",
"name": "Configure Vite on your Nx workspace",
"description": "Configure Vite on your Nx workspace",
"file": "shared/packages/vite/configure-vite"
}
]
},
+218
View File
@@ -0,0 +1,218 @@
---
title: Configure Vite on your Nx workspace
description: This guide explains how you can configure Vite in your Nx workspace.
---
# Configure Vite on your Nx workspace
{% callout type="note" title="Use our generator!" %}
It is recommended that you use the [`@nx/vite:configuration`](/nx-api/vite/generators/configuration) generator to set up [Vite](https://vitejs.dev/) for your new or existing projects.
{% /callout %}
The `@nx/vite` plugin generators take care of configuring Vite for you. However, you may need to set up Vite manually in some cases. This guide explains how you can configure Vite in your Nx workspace.
## TypeScript paths
You need to use the `nxViteTsPaths()` plugin to make sure that your TypeScript paths are resolved correctly in your monorepo. This
## Framework plugins
If you are using React, you need to use the [`@vitejs/plugin-react` plugin](https://www.npmjs.com/package/@vitejs/plugin-react). If you're using Vue, you need to use the [`@vitejs/plugin-vue` plugin](https://www.npmjs.com/package/@vitejs/plugin-vue).
## Set the `root` path
Make sure to set the `root: __dirname,` property on your config object. This is necessary to make sure that the paths are resolved correctly in your monorepo.
## Set the build `outDir` path
Make sure you set the `outDir` property on your `build` object. Set the path as relative to the workspace root, so for example if your project is located in `apps/my-app`, set the `outDir` to `../../dist/apps/my-app`. If your project is located in `my-app`, set the `outDir` to `../dist/my-app`, etc.
## DTS plugin
If you are building a library, you need to use the [`vite-plugin-dts` plugin](https://www.npmjs.com/package/vite-plugin-dts) to generate the `.d.ts` files for your library.
### Skip diagnostics
If you are building a library, you can set the `skipDiagnostics` option to `true` to speed up the build. This means that type diagnostic will be skipped during the build process. However, if there are some files with type errors which interrupt the build process, these files will not be emitted and `.d.ts` declaration files will not be generated.
If you choose to skip diagnostics, here is what your `'vite-plugin-dts'` plugin setup will look like:
```ts {% fileName="libs/my-lib/vite.config.ts" %}
...
import dts from 'vite-plugin-dts';
import { join } from 'path';
...
...
export default defineConfig({
plugins: [
...,
dts({
entryRoot: 'src',
tsConfigFilePath: join(__dirname, 'tsconfig.lib.json'),
skipDiagnostics: true,
}),
```
### Do not skip diagnostics
If you are building a library, and you want to make sure that all the files are type checked, you can set the `skipDiagnostics` option to `false` to make sure that all the files are type checked. This means that type diagnostic will be run during the build process.
If you choose to enable diagnostics, here is what your `'vite-plugin-dts'` plugin setup will look like:
```ts {% fileName="libs/my-lib/vite.config.ts" %}
...
import dts from 'vite-plugin-dts';
...
...
export default defineConfig({
plugins: [
...,
dts({
root: '../../',
entryRoot: 'libs/my-lib/src',
tsConfigFilePath: 'libs/my-lib/tsconfig.lib.json',
include: ['libs/my-lib/src/**/*.ts'],
outputDir: 'dist/libs/my-lib',
skipDiagnostics: false,
}),
```
You can read more about the configuration options in the [`vite-plugin-dts` plugin documentation](https://www.npmjs.com/package/vite-plugin-dts).
## For testing
If you're using `vitest`, make sure your `test` object in your `vite.config.ts` file looks like this:
```ts
...
test: {
globals: true,
cache: {
dir: '../node_modules/.vitest',
},
environment: 'jsdom',
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
reporters: ['default'],
coverage: {
reportsDirectory: '../coverage/<project-root>',
provider: 'v8',
},
},
...
```
Note how we're specifying `reporters` and `environment`.
## How your `vite.config.ts` looks like
### For applications
Add a `vite.config.ts` file to the root of your project. If you are not using React, you can skip adding the `react` plugin, of course.
```ts {% fileName="apps/my-app/vite.config.ts" %}
/// <reference types='vitest' />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
export default defineConfig({
root: __dirname,
build: {
outDir: '../../dist/apps/my-app',
reportCompressedSize: true,
commonjsOptions: {
transformMixedEsModules: true,
},
},
cacheDir: '../../node_modules/.vite/my-app',
server: {
port: 4200,
host: 'localhost',
},
preview: {
port: 4300,
host: 'localhost',
},
plugins: [react(), nxViteTsPaths()],
test: {
reporters: ['default'],
coverage: {
reportsDirectory: '../../coverage/apps/my-app',
provider: 'v8',
},
globals: true,
cache: {
dir: '../../node_modules/.vitest',
},
environment: 'jsdom',
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
},
});
```
### For libraries
If you are setting up a library (rather than an application) to use Vite, your `vite.config.ts` file should look like this:
```ts {% fileName="libs/my-lib/vite.config.ts" %}
/// <reference types='vitest' />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import dts from 'vite-plugin-dts';
import * as path from 'path';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
export default defineConfig({
root: __dirname,
cacheDir: '../node_modules/.vite/my-lib',
plugins: [
react(),
nxViteTsPaths(),
dts({
entryRoot: 'src',
tsConfigFilePath: path.join(__dirname, 'tsconfig.lib.json'),
skipDiagnostics: true,
}),
],
// Configuration for building your library.
// See: https://vitejs.dev/guide/build.html#library-mode
build: {
outDir: '../dist/my-lib',
reportCompressedSize: true,
commonjsOptions: {
transformMixedEsModules: true,
},
lib: {
// Could also be a dictionary or array of multiple entry points.
entry: 'src/index.ts',
name: 'my-lib',
fileName: 'index',
// Change this to the formats you want to support.
// Don't forget to update your package.json as well.
formats: ['es', 'cjs'],
},
rollupOptions: {
// External packages that should not be bundled into your library.
external: ['react', 'react-dom', 'react/jsx-runtime'],
},
},
test: {
globals: true,
cache: {
dir: '../node_modules/.vitest',
},
environment: 'jsdom',
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
reporters: ['default'],
coverage: {
reportsDirectory: '../coverage/my-lib',
provider: 'v8',
},
},
});
```
In that config file, you can configure Vite as you would normally do. For more information, see the [Vite.js documentation](https://vitejs.dev/config/).
@@ -1,336 +0,0 @@
---
title: Manually set up your project to use Vite.js
description: This guide explains how you can manually set up your project to use Vite.js in your Nx workspace.
---
# Manually set up your project to use Vite.js
{% callout type="note" title="Use our generator!" %}
It is recommended that you use the [`@nx/vite:configuration`](/nx-api/vite/generators/configuration) generator to do convert an existing project to use [Vite](https://vitejs.dev/).
{% /callout %}
You can use the `@nx/vite:dev-server`,`@nx/vite:build` and `@nx/vite:test` executors to serve, build and test your project using [Vite](https://vitejs.dev/) and [Vitest](https://vitest.dev/). To do this, you need to make a few adjustments to your project. It is recommended that you use the [`@nx/vite:configuration`](/nx-api/vite/generators/configuration) generator to do this, but you can also do it manually.
A reason you may need to do this manually, is if our generator does not support conversion for your project, or if you want to experiment with custom options.
The list of steps below assumes that your project can be converted to use the `@nx/vite` executors. However, if it's not supported by the [`@nx/vite:configuration`](/nx-api/vite/generators/configuration) generator, it's likely that your project will not work as expected when converted. So, proceed with caution and always commit your code before making any changes.
## 1. Change the executors in your `project.json`
### The `serve` target
This applies to applications, not libraries.
In your app's `project.json` file, change the executor of your `serve` target to use `@nx/vite:dev-server` and set it up with the following options:
```json
//...
"my-app": {
"targets": {
//...
"serve": {
"executor": "@nx/vite:dev-server",
"defaultConfiguration": "development",
"options": {
"buildTarget": "my-app:build",
},
"configurations": {
...
}
},
}
}
```
{% callout type="note" title="Other options" %}
Any extra options that you may need to add to your server's configuration can be added in your project's `vite.config.ts` file. You can find all the options that are supported in the [Vite.js documentation](https://vitejs.dev/config/). You can see which of these options you can add in your `project.json` in the [`@nx/vite:dev-server`](/nx-api/vite/executors/dev-server#options) documentation.
{% /callout %}
### The `build` target
In your project's `project.json` file, change the executor of your `build` target to use `@nx/vite:build` and set it up with the following options:
```json
//...
"my-app": {
"targets": {
//...
"build": {
"executor": "@nx/vite:build",
...
"options": {
"outputPath": "dist/apps/my-app"
},
"configurations": {
...
}
},
}
}
```
{% callout type="note" title="Other options" %}
You can specify more options in the `vite.config.ts` file (see **Step 2** below).
{% /callout %}
## 2. Configure Vite.js
### TypeScript paths
You need to use the [`vite-tsconfig-paths` plugin](https://www.npmjs.com/package/vite-tsconfig-paths) to make sure that your TypeScript paths are resolved correctly in your monorepo.
### React plugin
If you are using React, you need to use the [`@vitejs/plugin-react` plugin](https://www.npmjs.com/package/@vitejs/plugin-react).
### DTS plugin
If you are building a library, you need to use the [`vite-plugin-dts` plugin](https://www.npmjs.com/package/vite-plugin-dts) to generate the `.d.ts` files for your library.
#### Skip diagnostics
If you are building a library, you can set the `skipDiagnostics` option to `true` to speed up the build. This means that type diagnostic will be skipped during the build process. However, if there are some files with type errors which interrupt the build process, these files will not be emitted and `.d.ts` declaration files will not be generated.
If you choose to skip diagnostics, here is what your `'vite-plugin-dts'` plugin setup will look like:
```ts {% fileName="libs/my-lib/vite.config.ts" %}
...
import dts from 'vite-plugin-dts';
import { join } from 'path';
...
...
export default defineConfig({
plugins: [
...,
dts({
entryRoot: 'src',
tsConfigFilePath: join(__dirname, 'tsconfig.lib.json'),
skipDiagnostics: true,
}),
```
#### Do not skip diagnostics
If you are building a library, and you want to make sure that all the files are type checked, you can set the `skipDiagnostics` option to `false` to make sure that all the files are type checked. This means that type diagnostic will be run during the build process.
If you choose to enable diagnostics, here is what your `'vite-plugin-dts'` plugin setup will look like:
```ts {% fileName="libs/my-lib/vite.config.ts" %}
...
import dts from 'vite-plugin-dts';
...
...
export default defineConfig({
plugins: [
...,
dts({
root: '../../',
entryRoot: 'libs/my-lib/src',
tsConfigFilePath: 'libs/my-lib/tsconfig.lib.json',
include: ['libs/my-lib/src/**/*.ts'],
outputDir: 'dist/libs/my-lib',
skipDiagnostics: false,
}),
```
You can read more about the configuration options in the [`vite-plugin-dts` plugin documentation](https://www.npmjs.com/package/vite-plugin-dts)).
### How your `vite.config.ts` looks like
#### For applications
Add a `vite.config.ts` file to the root of your project. If you are not using React, you can skip adding the `react` plugin, of course.
```ts {% fileName="apps/my-app/vite.config.ts" %}
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import ViteTsConfigPathsPlugin from 'vite-tsconfig-paths';
export default defineConfig({
plugins: [
react(),
ViteTsConfigPathsPlugin({
root: '../../',
}),
],
});
```
#### For libraries
If you are setting up a library (rather than an application) to use vite, your `vite.config.ts` file should look like this:
```ts {% fileName="libs/my-lib/vite.config.ts" %}
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import viteTsConfigPaths from 'vite-tsconfig-paths';
import dts from 'vite-plugin-dts';
import { join } from 'path';
export default defineConfig({
plugins: [
dts({
entryRoot: 'src',
tsConfigFilePath: join(__dirname, 'tsconfig.lib.json'),
skipDiagnostics: true,
}),
react(),
viteTsConfigPaths({
root: '../../',
}),
],
// 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: 'pure-libs-rlv1',
fileName: 'index',
// Change this to the formats you want to support.
// Don't forget to update your package.json as well.
formats: ['es', 'cjs'],
},
rollupOptions: {
// External packages that should not be bundled into your library.
external: ['react', 'react-dom', 'react/jsx-runtime'],
},
},
});
```
{% callout type="note" title="The `root` path" %}
Make sure the `root` path in the `ViteTsConfigPathsPlugin` options is correct. It should be the path to the root of your workspace.
{% /callout %}
In that config file, you can configure Vite.js as you would normally do. For more information, see the [Vite.js documentation](https://vitejs.dev/config/).
## 3. Move `index.html` and point it to your app's entrypoint
This applies to applications, not libraries.
First of all, move your `index.html` file to the root of your app (e.g. from `apps/my-app/src/index.html` to `apps/my-app/index.html`).
Then, add a module `script` tag pointing to the `main.tsx` (or `main.ts`) file of your app:
```html {% fileName="apps/my-app/index.html" %}
...
<body>
<div id="root"></div>
<script type="module" src="src/main.tsx"></script>
</body>
</html>
```
## 4. Add a `public` folder
You can add a `public` folder to the root of your project. You can read more about the public folder in the [Vite.js documentation](https://vitejs.dev/guide/assets.html#the-public-directory).
```treeview
myorg/
├── apps/
│ ├── my-app/
│ │ ├── src/
│ │ │ ├── app/
│ │ │ ├── assets/
│ │ │ ├── ...
│ │ │ └── main.tsx
│ │ ├── index.html
│ │ ├── public/
| . | . | ├── favicon.ico
│ │ │ └── my-page.md
│ │ ├── project.json
│ │ ├── ...
│ │ ├── tsconfig.app.json
│ │ ├── tsconfig.json
│ │ └── tsconfig.spec.json
```
You can use the `public` folder to store static **assets**, such as images, fonts, and so on. You can also use it to store Markdown files, which you can then import in your app and use as a source of content.
## 5. Adjust your project's tsconfig.json
Change your app's `tsconfig.json` (e.g. `apps/my-app/tsconfig.json`) `compilerOptions` to the following:
### For React apps
```json {% fileName="apps/my-app/tsconfig.json" %}
...
"compilerOptions": {
"jsx": "react-jsx",
"allowJs": false,
"esModuleInterop": false,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"module": "ESNext",
"moduleResolution": "Node",
"noEmit": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"strict": true,
"target": "ESNext",
"types": ["vite/client"],
"useDefineForClassFields": true
},
...
```
### For Web apps
```json {% fileName="apps/my-app/tsconfig.json" %}
...
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ESNext", "DOM"],
"moduleResolution": "Node",
"strict": true,
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"noEmit": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"skipLibCheck": true,
"types": ["vite/client"]
},
"include": ["src"],
...
```
You can read more about the TypeScript compiler options in the [Vite.js documentation](https://vitejs.dev/guide/features.html#typescript-compiler-options).
## 6. Use Vite.js
Now you can finally serve and build your app using Vite.js:
### Serve the app
```bash
nx serve my-app
```
or
```bash
nx run my-app:serve
```
Now, visit [http://localhost:4200](http://localhost:4200) to see your app running!
### Build the app
```bash
nx build my-app
```
or
```bash
nx run my-app:build
```
+1 -1
View File
@@ -139,7 +139,7 @@
- [Next](/recipes/next)
- [How to configure Next.js plugins](/recipes/next/next-config-setup)
- [Vite](/recipes/vite)
- [Manually set up your project to use Vite.js](/recipes/vite/set-up-vite-manually)
- [Configure Vite on your Nx workspace](/recipes/vite/configure-vite)
- [Webpack](/recipes/webpack)
- [How to configure webpack on your Nx workspace](/recipes/webpack/webpack-config-setup)
- [Webpack plugins](/recipes/webpack/webpack-plugins)
+1
View File
@@ -13,6 +13,7 @@ describe('Nuxt Plugin', () => {
beforeAll(() => {
proj = newProject({
packages: ['@nx/nuxt', '@nx/storybook'],
unsetProjectNameAndRootFormat: false,
});
runCLI(`generate @nx/nuxt:app ${app} --unitTestRunner=vitest`);
+3 -1
View File
@@ -17,7 +17,9 @@ describe('Storybook generators and executors for monorepos', () => {
const reactStorybookApp = uniq('react-app');
let proj;
beforeAll(async () => {
proj = newProject();
proj = newProject({
packages: ['@nx/react', '@nx/storybook'],
});
runCLI(
`generate @nx/react:app ${reactStorybookApp} --bundler=webpack --project-name-and-root-format=as-provided --no-interactive`
);
+6 -1
View File
@@ -11,7 +11,12 @@ import {
// This test ensures that when CJS is gone from the published `vite` package, Nx will continue to work.
describe('Vite ESM tests', () => {
beforeAll(() => newProject({ unsetProjectNameAndRootFormat: false }));
beforeAll(() =>
newProject({
unsetProjectNameAndRootFormat: false,
packages: ['@nx/react'],
})
);
it('should build with Vite when it is ESM-only', async () => {
const appName = uniq('viteapp');
+12 -22
View File
@@ -1,6 +1,7 @@
import { cleanupProject, newProject, runCLI, uniq } from '@nx/e2e/utils';
const myApp = uniq('my-app');
const myVueApp = uniq('my-vue-app');
describe('@nx/vite/plugin', () => {
let proj: string;
@@ -9,22 +10,21 @@ describe('@nx/vite/plugin', () => {
beforeAll(() => {
originalEnv = process.env.NX_PCV3;
process.env.NX_PCV3 = 'true';
proj = newProject({
packages: ['@nx/react', '@nx/vue'],
});
runCLI(
`generate @nx/react:app ${myApp} --bundler=vite --unitTestRunner=vitest`
);
runCLI(`generate @nx/vue:app ${myVueApp} --unitTestRunner=vitest`);
});
afterAll(() => {
process.env.NODE_ENV = originalEnv;
cleanupProject();
});
describe('build and test React Vite app', () => {
beforeAll(() => {
proj = newProject();
runCLI(
`generate @nx/react:app ${myApp} --bundler=vite --unitTestRunner=vitest`
);
});
afterAll(() => cleanupProject());
describe('build and test React app', () => {
it('should build application', () => {
const result = runCLI(`build ${myApp}`);
expect(result).toContain('Successfully ran target build');
@@ -35,24 +35,14 @@ describe('@nx/vite/plugin', () => {
expect(result).toContain('Successfully ran target test');
}, 200_000);
});
describe('build and test Vue app', () => {
beforeAll(() => {
proj = newProject();
runCLI(`generate @nx/vue:app ${myApp} --unitTestRunner=vitest`);
});
afterAll(() => {
cleanupProject();
});
it('should build application', () => {
const result = runCLI(`build ${myApp}`);
const result = runCLI(`build ${myVueApp}`);
expect(result).toContain('Successfully ran target build');
}, 200_000);
it('should test application', () => {
const result = runCLI(`test ${myApp}`);
const result = runCLI(`test ${myVueApp}`);
expect(result).toContain('Successfully ran target test');
}, 200_000);
});
+15 -5
View File
@@ -31,7 +31,9 @@ describe('Vite Plugin', () => {
describe('Vite on React apps', () => {
describe('set up new React app with --bundler=vite option', () => {
beforeEach(async () => {
proj = newProject();
proj = newProject({
packages: ['@nx/react'],
});
runCLI(`generate @nx/react:app ${myApp} --bundler=vite`);
createFile(`apps/${myApp}/public/hello.md`, `# Hello World`);
});
@@ -49,7 +51,9 @@ describe('Vite Plugin', () => {
describe('Vite on Web apps', () => {
describe('set up new @nx/web app with --bundler=vite option', () => {
beforeEach(() => {
proj = newProject();
proj = newProject({
packages: ['@nx/web'],
});
runCLI(`generate @nx/web:app ${myApp} --bundler=vite`);
});
afterEach(() => cleanupProject());
@@ -122,7 +126,10 @@ describe('Vite Plugin', () => {
const app = uniq('demo');
const lib = uniq('my-lib');
beforeAll(() => {
proj = newProject({ name: uniq('vite-incr-build') });
proj = newProject({
name: uniq('vite-incr-build'),
packages: ['@nx/react'],
});
runCLI(`generate @nx/react:app ${app} --bundler=vite --no-interactive`);
// only this project will be directly used from dist
@@ -206,7 +213,7 @@ export default App;
describe('should be able to create libs that use vitest', () => {
const lib = uniq('my-lib');
beforeEach(() => {
proj = newProject({ name: uniq('vite-proj') });
proj = newProject({ name: uniq('vite-proj'), packages: ['@nx/react'] });
});
it('should be able to run tests', async () => {
@@ -359,7 +366,10 @@ export default defineConfig({
describe('ESM-only apps', () => {
beforeAll(() => {
newProject({ unsetProjectNameAndRootFormat: false });
newProject({
unsetProjectNameAndRootFormat: false,
packages: ['@nx/react'],
});
});
it('should support ESM-only plugins in vite.config.ts for root apps (#NXP-168)', () => {
+3 -1
View File
@@ -12,7 +12,9 @@ describe('Storybook generators and executors for Vue projects', () => {
const vueStorybookApp = uniq('vue-app');
let proj;
beforeAll(async () => {
proj = newProject();
proj = newProject({
packages: ['@nx/vue', '@nx/storybook'],
});
runCLI(
`generate @nx/vue:app ${vueStorybookApp} --project-name-and-root-format=as-provided --no-interactive`
);
+1 -1
View File
@@ -10,7 +10,7 @@ import {
describe('vue tailwind support', () => {
beforeAll(() => {
newProject({ unsetProjectNameAndRootFormat: false });
newProject({ unsetProjectNameAndRootFormat: false, packages: ['@nx/vue'] });
});
afterAll(() => {
+1
View File
@@ -12,6 +12,7 @@ describe('Vue Plugin', () => {
beforeAll(() => {
proj = newProject({
packages: ['@nx/vue'],
unsetProjectNameAndRootFormat: false,
});
});
+1
View File
@@ -381,6 +381,7 @@ const recipesUrls = {
'/recipes/next/next-config-setup',
'/packages/vite/documents/set-up-vite-manually':
'/recipes/vite/set-up-vite-manually',
'/recipes/vite/set-up-vite-manually': '/recipes/vite/configure-vite',
'/packages/webpack/documents/webpack-plugins':
'/recipes/webpack/webpack-plugins',
'/packages/webpack/documents/webpack-config-setup':
+4
View File
@@ -1433,6 +1433,10 @@
"version": "~17.0.0",
"alwaysAddToPackageJson": false
},
"@angular/pwa": {
"version": "~17.0.0",
"alwaysAddToPackageJson": false
},
"@angular/core": {
"version": "~17.0.0",
"alwaysAddToPackageJson": true
+71 -13
View File
@@ -1,6 +1,7 @@
use std::path::PathBuf;
use tracing::trace;
use crate::native::glob::build_glob_set;
use crate::native::glob::{build_glob_set, contains_glob_pattern};
use crate::native::utils::Normalize;
use crate::native::walker::nx_walker_sync;
@@ -10,17 +11,46 @@ use crate::native::walker::nx_walker_sync;
pub fn expand_outputs(directory: String, entries: Vec<String>) -> anyhow::Result<Vec<String>> {
let directory: PathBuf = directory.into();
let (existing_paths, not_found): (Vec<_>, Vec<_>) = entries.into_iter().partition(|entry| {
let path = directory.join(entry);
path.exists()
});
let has_glob_pattern = entries.iter().any(|entry| contains_glob_pattern(entry));
if not_found.is_empty() {
return Ok(existing_paths);
if !has_glob_pattern {
trace!("No glob patterns found, checking if entries exist");
let existing_directories = entries
.into_iter()
.filter(|entry| {
let path = directory.join(entry);
path.exists()
})
.collect();
return Ok(existing_directories);
}
let glob_set = build_glob_set(&not_found)?;
let found_paths = nx_walker_sync(directory)
let (regular_globs, negated_globs): (Vec<_>, Vec<_>) = entries
.into_iter()
.partition(|entry| !entry.starts_with('!'));
let negated_globs = negated_globs
.into_iter()
.map(|s| s[1..].to_string())
.collect::<Vec<_>>();
let regular_globs = regular_globs
.into_iter()
.map(|s| {
if !s.ends_with('/') {
let path = directory.join(&s);
if path.is_dir() {
return format!("{}/", s);
}
}
s
})
.collect::<Vec<_>>();
trace!(?negated_globs, ?regular_globs, "Expanding globs");
let glob_set = build_glob_set(&regular_globs)?;
let found_paths = nx_walker_sync(directory, Some(&negated_globs))
.filter_map(|path| {
if glob_set.is_match(&path) {
Some(path.to_normalized_string())
@@ -28,9 +58,9 @@ pub fn expand_outputs(directory: String, entries: Vec<String>) -> anyhow::Result
None
}
})
.chain(existing_paths);
.collect();
Ok(found_paths.collect())
Ok(found_paths)
}
#[napi]
@@ -60,7 +90,7 @@ pub fn get_files_for_outputs(
if !globs.is_empty() {
// todo(jcammisuli): optimize this as nx_walker_sync is very slow on the root directory. We need to change this to only search smaller directories
let glob_set = build_glob_set(&globs)?;
let found_paths = nx_walker_sync(&directory).filter_map(|path| {
let found_paths = nx_walker_sync(&directory, None).filter_map(|path| {
if glob_set.is_match(&path) {
Some(path.to_normalized_string())
} else {
@@ -75,7 +105,7 @@ pub fn get_files_for_outputs(
for dir in directories {
let dir = PathBuf::from(dir);
let dir_path = directory.join(&dir);
let files_in_dir = nx_walker_sync(&dir_path).filter_map(|e| {
let files_in_dir = nx_walker_sync(&dir_path, None).filter_map(|e| {
let path = dir_path.join(&e);
if path.is_file() {
@@ -123,6 +153,15 @@ mod test {
temp.child("multi").child("src.ts").touch().unwrap();
temp.child("multi").child("file.map").touch().unwrap();
temp.child("multi").child("file.txt").touch().unwrap();
temp.child("apps/web/.next/cache")
.child("contents")
.touch()
.unwrap();
temp.child("apps/web/.next/static")
.child("contents")
.touch()
.unwrap();
temp.child("apps/web/.next/content-file").touch().unwrap();
temp
}
#[test]
@@ -156,4 +195,23 @@ mod test {
vec!["multi/file.js", "multi/file.map", "multi/src.ts"]
);
}
#[test]
fn should_handle_multiple_outputs_with_negation() {
let temp = setup_fs();
let entries = vec![
"apps/web/.next".to_string(),
"!apps/web/.next/cache".to_string(),
];
let mut result = expand_outputs(temp.display().to_string(), entries).unwrap();
result.sort();
assert_eq!(
result,
vec![
"apps/web/.next/content-file",
"apps/web/.next/static",
"apps/web/.next/static/contents"
]
);
}
}
+16
View File
@@ -107,6 +107,22 @@ pub(crate) fn build_glob_set<S: AsRef<str> + Debug>(globs: &[S]) -> anyhow::Resu
NxGlobSetBuilder::new(&result)?.build()
}
pub(crate) fn contains_glob_pattern(value: &str) -> bool {
value.contains('!')
|| value.contains('?')
|| value.contains('@')
|| value.contains('+')
|| value.contains('*')
|| value.contains('|')
|| value.contains(',')
|| value.contains('{')
|| value.contains('}')
|| value.contains('[')
|| value.contains(']')
|| value.contains('(')
|| value.contains(')')
}
#[cfg(test)]
mod test {
use super::*;
@@ -2,24 +2,25 @@ use std::sync::Arc;
use anyhow::*;
use dashmap::DashMap;
use tracing::trace;
use tracing::{trace, warn};
use crate::native::glob::build_glob_set;
use crate::native::types::FileData;
use crate::native::{glob::build_glob_set, hasher::hash};
pub fn hash_workspace_files(
workspace_file_set: &str,
all_workspace_files: &[FileData],
cache: Arc<DashMap<String, String>>,
) -> Result<String> {
let file_set = workspace_file_set
.strip_prefix("{workspaceRoot}/")
.ok_or_else(|| {
anyhow!(
"{workspace_file_set} does not start with {}",
"{workspaceRoot}/"
)
})?;
let file_set = workspace_file_set.strip_prefix("{workspaceRoot}/");
let Some(file_set) = file_set else {
warn!(
"{workspace_file_set} does not start with {}. This will throw an error in Nx 18.",
"{workspaceRoot}/"
);
return Ok(hash(b""));
};
if let Some(cache_results) = cache.get(file_set) {
return Ok(cache_results.clone());
@@ -50,9 +51,10 @@ mod test {
use std::sync::Arc;
#[test]
fn test_hash_workspace_files_error() {
let result = hash_workspace_files("packages/{package}", &[], Arc::new(DashMap::new()));
assert!(result.is_err());
fn invalid_workspace_input_is_just_empty_hash() {
let result =
hash_workspace_files("packages/{package}", &[], Arc::new(DashMap::new())).unwrap();
assert_eq!(result, hash(b""));
}
#[test]
+19 -5
View File
@@ -20,14 +20,28 @@ pub struct NxFile {
/// Walks the directory in a single thread and does not ignore any files
/// Should only be used for small directories, and not traversing the whole workspace
pub fn nx_walker_sync<'a, P>(directory: P) -> impl Iterator<Item = PathBuf>
///
/// The `ignores` argument is used to filter entries. This is important to make sure that any ignore globs are applied on the `filter_entry` function
pub fn nx_walker_sync<'a, P>(
directory: P,
ignores: Option<&[String]>,
) -> impl Iterator<Item = PathBuf>
where
P: AsRef<Path> + 'a,
{
let base_dir: PathBuf = directory.as_ref().into();
let ignore_glob_set =
build_glob_set(&["**/node_modules", "**/.git"]).expect("These static ignores always build");
let mut base_ignores: Vec<String> = vec![
"**/node_modules".into(),
"**/.git".into(),
"**/.nx/cache".into(),
];
if let Some(additional_ignores) = ignores {
base_ignores.extend(additional_ignores.iter().map(|s| format!("**/{}", s)));
};
let ignore_glob_set = build_glob_set(&base_ignores).expect("Should be valid globs");
// Use WalkDir instead of ignore::WalkBuilder because it's faster
WalkDir::new(&base_dir)
@@ -50,8 +64,8 @@ where
{
let directory = directory.as_ref();
let ignore_glob_set =
build_glob_set(&["**/node_modules", "**/.git"]).expect("These static ignores always build");
let ignore_glob_set = build_glob_set(&["**/node_modules", "**/.git", "**/.nx/cache"])
.expect("These static ignores always build");
let mut walker = WalkBuilder::new(directory);
walker.hidden(false);
+3 -13
View File
@@ -1,16 +1,6 @@
import { composePlugins, withNx } from '@nx/webpack';
import { withReact } from './with-react';
// Support existing default exports as well as new named export.
const legacyExport: any = withReact();
legacyExport.withReact = withReact;
const plugin = composePlugins(withNx(), withReact());
/** @deprecated use `import { withReact } from '@nx/react'` */
// This is here for backward compatibility if anyone imports {getWebpackConfig} directly.
// TODO(jack): Remove in Nx 16
const getWebpackConfig = withReact();
legacyExport.getWebpackConfig = getWebpackConfig;
module.exports = legacyExport;
export { getWebpackConfig };
module.exports = plugin;
+1 -1
View File
@@ -74,7 +74,7 @@ The list of executors for building, testing and serving that can be converted to
We **cannot** guarantee that projects using unsupported executors - _or any executor that is NOT listed in the list of "supported executors"_ - for either building, testing or serving will work correctly when converted to use the `@nx/vite` executors.
If you have a project that does _not_ use one of the supported executors you can try to [configure it to use the `@nx/vite` executors manually](/recipes/vite/set-up-vite-manually), but it may not work properly.
If you have a project that does _not_ use one of the supported executors you can try to [configure it to use the `@nx/vite` executors manually](/recipes/vite/configure-vite), but it may not work properly.
You can read more in the [Vite package overview page](/packages/vite).
@@ -31,6 +31,7 @@ export async function* viteBuildExecutor(
options: Record<string, any> & ViteBuildExecutorOptions,
context: ExecutorContext
) {
process.env.VITE_CJS_IGNORE_WARNING = 'true';
// Allows ESM to be required in CJS modules. Vite will be published as ESM in the future.
const { mergeConfig, build, loadConfigFromFile } = await (Function(
'return import("vite")'
@@ -49,7 +50,7 @@ export async function* viteBuildExecutor(
? process.cwd()
: relative(context.cwd, joinPathFragments(context.root, projectRoot));
const { buildOptions, otherOptions } = await getExtraArgs(options);
const { buildOptions, otherOptions } = await getBuildExtraArgs(options);
const resolved = await loadConfigFromFile(
{
@@ -188,7 +189,9 @@ export async function* viteBuildExecutor(
}
}
async function getExtraArgs(options: ViteBuildExecutorOptions): Promise<{
export async function getBuildExtraArgs(
options: ViteBuildExecutorOptions
): Promise<{
buildOptions: BuildOptions;
otherOptions: Record<string, any>;
}> {
@@ -3,6 +3,7 @@ import {
loadConfigFromFile,
type InlineConfig,
type ViteDevServer,
ServerOptions,
} from 'vite';
import {
@@ -20,6 +21,7 @@ export async function* viteDevServerExecutor(
options: ViteDevServerExecutorOptions,
context: ExecutorContext
): AsyncGenerator<{ success: boolean; baseUrl: string }> {
process.env.VITE_CJS_IGNORE_WARNING = 'true';
// Allows ESM to be required in CJS modules. Vite will be published as ESM in the future.
const { mergeConfig, createServer } = await (Function(
'return import("vite")'
@@ -43,11 +45,11 @@ export async function* viteDevServerExecutor(
projectRoot,
buildTargetOptions.configFile
);
const extraArgs = await getExtraArgs(options);
const { serverOptions, otherOptions } = await getServerExtraArgs(options);
const resolved = await loadConfigFromFile(
{
mode: extraArgs?.mode ?? 'production',
command: 'build',
mode: otherOptions?.mode ?? 'development',
command: 'serve',
},
viteConfigPath
);
@@ -62,9 +64,9 @@ export async function* viteDevServerExecutor(
{
server: {
...(await getViteServerOptions(options, context)),
...extraArgs,
...serverOptions,
},
...extraArgs,
...otherOptions,
}
);
@@ -111,9 +113,12 @@ async function runViteDevServer(server: ViteDevServer): Promise<void> {
export default viteDevServerExecutor;
async function getExtraArgs(
async function getServerExtraArgs(
options: ViteDevServerExecutorOptions
): Promise<InlineConfig> {
): Promise<{
serverOptions: ServerOptions;
otherOptions: Record<string, any>;
}> {
// support passing extra args to vite cli
const schema = await import('./schema.json');
const extraArgs = {};
@@ -123,5 +128,37 @@ async function getExtraArgs(
}
}
return extraArgs as InlineConfig;
const serverOptions = {} as ServerOptions;
const serverSchemaKeys = [
'hmr',
'warmup',
'watch',
'middlewareMode',
'fs',
'origin',
'preTransformRequests',
'sourcemapIgnoreList',
'port',
'strictPort',
'host',
'https',
'open',
'proxy',
'cors',
'headers',
];
const otherOptions = {};
for (const key of Object.keys(extraArgs)) {
if (serverSchemaKeys.includes(key)) {
serverOptions[key] = extraArgs[key];
} else {
otherOptions[key] = extraArgs[key];
}
}
return {
serverOptions,
otherOptions,
};
}
@@ -5,20 +5,22 @@ import {
parseTargetString,
runExecutor,
} from '@nx/devkit';
import type { InlineConfig, PreviewServer } from 'vite';
import type { InlineConfig, PreviewOptions, PreviewServer } from 'vite';
import {
getNxTargetOptions,
getVitePreviewOptions,
getProxyConfig,
normalizeViteConfigFilePath,
} from '../../utils/options-utils';
import { ViteBuildExecutorOptions } from '../build/schema';
import { VitePreviewServerExecutorOptions } from './schema';
import { relative } from 'path';
import { getBuildExtraArgs } from '../build/build.impl';
export async function* vitePreviewServerExecutor(
options: VitePreviewServerExecutorOptions,
context: ExecutorContext
) {
process.env.VITE_CJS_IGNORE_WARNING = 'true';
// Allows ESM to be required in CJS modules. Vite will be published as ESM in the future.
const { mergeConfig, preview, loadConfigFromFile } = await (Function(
'return import("vite")'
@@ -43,15 +45,20 @@ export async function* vitePreviewServerExecutor(
options.buildTarget,
context
);
const viteConfigPath = normalizeViteConfigFilePath(
context.root,
projectRoot,
buildTargetOptions.configFile
);
const extraArgs = await getExtraArgs(options);
const { buildOptions, otherOptions: otherOptionsFromBuild } =
await getBuildExtraArgs(buildTargetOptions);
const { previewOptions, otherOptions } = await getExtraArgs(options);
const resolved = await loadConfigFromFile(
{
mode: extraArgs?.mode ?? 'production',
mode: otherOptions?.mode ?? 'production',
command: 'build',
},
viteConfigPath
@@ -81,12 +88,16 @@ export async function* vitePreviewServerExecutor(
...{ watch: {} },
build: {
outDir,
...(isCustomBuildTarget ? {} : buildOptions),
},
...(isCustomBuildTarget ? {} : otherOptionsFromBuild),
...otherOptions,
preview: {
...getProxyConfig(context, otherOptions.proxyConfig),
...previewOptions,
},
...(isCustomBuildTarget ? {} : buildTargetOptions),
...extraArgs,
};
// Retrieve the server configuration.
const serverConfig: InlineConfig = mergeConfig(
{
// This should not be needed as it's going to be set in vite.config.ts
@@ -96,7 +107,6 @@ export async function* vitePreviewServerExecutor(
},
{
...mergedOptions,
preview: getVitePreviewOptions(mergedOptions, context),
}
);
@@ -180,7 +190,10 @@ export default vitePreviewServerExecutor;
async function getExtraArgs(
options: VitePreviewServerExecutorOptions
): Promise<InlineConfig> {
): Promise<{
previewOptions: PreviewOptions;
otherOptions: Record<string, any>;
}> {
// support passing extra args to vite cli
const schema = await import('./schema.json');
const extraArgs = {};
@@ -190,5 +203,29 @@ async function getExtraArgs(
}
}
return extraArgs as InlineConfig;
const previewOptions = {} as PreviewOptions;
const previewSchemaKeys = [
'port',
'strictPort',
'host',
'https',
'open',
'proxy',
'cors',
'headers',
];
const otherOptions = {};
for (const key of Object.keys(extraArgs)) {
if (previewSchemaKeys.includes(key)) {
previewOptions[key] = extraArgs[key];
} else {
otherOptions[key] = extraArgs[key];
}
}
return {
previewOptions,
otherOptions,
};
}
@@ -7,7 +7,6 @@ import {
import { VitestExecutorOptions } from '../schema';
import { normalizeViteConfigFilePath } from '../../../utils/options-utils';
import { relative } from 'path';
import { NxReporter } from './nx-reporter';
export async function getOptions(
options: VitestExecutorOptions,
@@ -76,12 +75,9 @@ export async function getExtraArgs(
options: VitestExecutorOptions
): Promise<Record<string, any>> {
// support passing extra args to vite cli
const schema = await import('../schema.json');
const extraArgs: Record<string, any> = {};
for (const key of Object.keys(options)) {
if (!schema.properties[key]) {
extraArgs[key] = options[key];
}
extraArgs[key] = options[key];
}
return extraArgs;
@@ -14,6 +14,8 @@ export async function* vitestExecutor(
registerTsConfigPaths(resolve(workspaceRoot, projectRoot, 'tsconfig.json'));
process.env.VITE_CJS_IGNORE_WARNING = 'true';
// Allows ESM to be required in CJS modules. Vite will be published as ESM in the future.
const { startVitest } = await (Function(
'return import("vitest/node")'
)() as Promise<typeof import('vitest/node')>);
@@ -165,3 +165,112 @@ export default defineConfig({
});
"
`;
exports[`change-vite-ts-paths-plugin migration should convert the file correctly 1`] = `
"/// <reference types='vitest' />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
export default defineConfig(({ mode }) => {
return {
root: __dirname,
build: {
outDir: '../dist/demo4',
reportCompressedSize: true,
commonjsOptions: {
transformMixedEsModules: true,
},
},
cacheDir: '../../node_modules/.vite/demo4',
server: {
port: 4200,
host: 'localhost',
},
preview: {
port: 4300,
host: 'localhost',
},
plugins: [react(), nxViteTsPaths()],
// Uncomment this if you are using workers.
// worker: {
// plugins: [ nxViteTsPaths() ],
// },
test: {
reporters: ['default'],
coverage: {
reportsDirectory: '../coverage/demo4',
provider: 'v8',
},
globals: true,
cache: {
dir: '../../node_modules/.vitest',
},
environment: 'jsdom',
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
},
};
});
"
`;
exports[`change-vite-ts-paths-plugin migration should convert the file correctly 2`] = `
"/// <reference types='vitest' />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
export default defineConfig(({ mode }) => {
return {
root: __dirname,
build: {
outDir: '../dist/demo4',
reportCompressedSize: true,
commonjsOptions: {
transformMixedEsModules: true,
},
},
cacheDir: '../../node_modules/.vite/demo4',
server: {
port: 4200,
host: 'localhost',
},
preview: {
port: 4300,
host: 'localhost',
},
plugins: [react(), nxViteTsPaths()],
// Uncomment this if you are using workers.
// worker: {
// plugins: [ nxViteTsPaths() ],
// },
test: {
reporters: ['default'],
coverage: {
reportsDirectory: '../coverage/demo4',
provider: 'v8',
},
globals: true,
cache: {
dir: '../../node_modules/.vitest',
},
environment: 'jsdom',
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
},
};
});
"
`;
exports[`change-vite-ts-paths-plugin migration should show warning to the user if could not recognize config 1`] = `
"// some invalid config
"
`;
@@ -1,17 +1,24 @@
import { ChangeType, applyChangesToString } from '@nx/devkit';
import { FileReplacement } from '../../../../plugins/rollup-replace-files.plugin';
import { tsquery } from '@phenomnomnominal/tsquery';
import { getConfigNode, notFoundWarning } from '../update-vite-config';
export function addFileReplacements(
configContents: string,
fileReplacements: FileReplacement[]
fileReplacements: FileReplacement[],
configPath: string
): string {
const configNode = getConfigNode(configContents);
if (!configNode) {
notFoundWarning(configPath);
return configContents;
}
const pluginsObject = tsquery.query(
configContents,
configNode,
`PropertyAssignment:has(Identifier[name="plugins"])`
)?.[0];
const replaceFilesPlugin = tsquery.query(
configContents,
configNode,
`PropertyAssignment:has(Identifier[name="plugins"]) CallExpression:has(Identifier[name="replaceFiles"])`
)?.[0];
@@ -55,7 +62,7 @@ export function addFileReplacements(
return applyChangesToString(configContents, [
{
type: ChangeType.Insert,
index: foundDefineConfig.getStart() + 14,
index: configNode.getStart() + 1,
text: `plugins: [replaceFiles(${JSON.stringify(fileReplacements)})],`,
},
firstImportDeclaration
@@ -4,12 +4,12 @@ import {
Tree,
applyChangesToString,
joinPathFragments,
logger,
offsetFromRoot,
updateProjectConfiguration,
} from '@nx/devkit';
import { tsquery } from '@phenomnomnominal/tsquery';
import ts = require('typescript');
import { getConfigNode, notFoundWarning } from '../update-vite-config';
export function updateBuildOutDirAndRoot(
options: Record<string, any>,
@@ -17,7 +17,8 @@ export function updateBuildOutDirAndRoot(
projectConfig: ProjectConfiguration,
targetName: string,
tree: Tree,
projectName: string
projectName: string,
configPath: string
): string {
const foundDefineConfig = tsquery.query(
configContents,
@@ -25,10 +26,8 @@ export function updateBuildOutDirAndRoot(
)?.[0];
if (!foundDefineConfig) {
logger.warn(`
Could not find defineConfig in your vite.config file.
Please add the build.outDir and root options to your vite.config file.
`);
notFoundWarning(configPath);
return;
}
configContents = fixBuild(
@@ -38,10 +37,10 @@ export function updateBuildOutDirAndRoot(
targetName,
tree,
projectName,
foundDefineConfig
configPath
);
configContents = addRoot(configContents, foundDefineConfig);
configContents = addRoot(configContents, configPath);
return configContents;
}
@@ -53,8 +52,14 @@ function fixBuild(
targetName: string,
tree: Tree,
projectName: string,
foundDefineConfig?: ts.Node
configPath: string
) {
const configNode = getConfigNode(configContents);
if (!configNode) {
notFoundWarning(configPath);
return configContents;
}
let outputPath = '';
// In vite.config.ts, we want to keep the path relative to workspace root
@@ -78,7 +83,7 @@ function fixBuild(
updateProjectConfiguration(tree, projectName, projectConfig);
const buildObject = tsquery.query(
configContents,
configNode,
`PropertyAssignment:has(Identifier[name="build"])`
)?.[0];
@@ -134,28 +139,32 @@ function fixBuild(
return applyChangesToString(configContents, changes);
}
} else {
return addBuildProperty(configContents, outputPath, foundDefineConfig);
return addBuildProperty(configContents, outputPath, configPath);
}
return configContents;
}
function addRoot(
configFileContents: string,
foundDefineConfig?: ts.Node
): string {
function addRoot(configFileContents: string, configPath: string): string {
const configNode = getConfigNode(configFileContents);
if (!configNode) {
notFoundWarning(configPath);
return configFileContents;
}
const rootOption = tsquery.query(
configFileContents,
configNode,
`PropertyAssignment:has(Identifier[name="root"]) Identifier[name="__dirname"]`
)?.[0];
if (rootOption || !foundDefineConfig) {
if (rootOption) {
return configFileContents;
} else {
return applyChangesToString(configFileContents, [
{
type: ChangeType.Insert,
index: foundDefineConfig.getStart() + 14,
index: configNode.getStart() + 1,
text: `root: __dirname,`,
},
]);
@@ -165,23 +174,25 @@ function addRoot(
function addBuildProperty(
configFileContents: string,
outputPath: string,
foundDefineConfig: ts.Node
configPath: string
): string {
if (foundDefineConfig) {
return applyChangesToString(configFileContents, [
{
type: ChangeType.Insert,
index: foundDefineConfig.getStart() + 14,
text: `build: {
const configNode = getConfigNode(configFileContents);
if (!configNode) {
notFoundWarning(configPath);
return configFileContents;
}
return applyChangesToString(configFileContents, [
{
type: ChangeType.Insert,
index: configNode.getStart() + 1,
text: `build: {
outDir: '${outputPath}',
reportCompressedSize: true,
commonjsOptions: {
transformMixedEsModules: true,
},
},`,
},
]);
} else {
return configFileContents;
}
},
]);
}
@@ -7,13 +7,21 @@ import {
} from '@nx/devkit';
import { tsquery } from '@phenomnomnominal/tsquery';
import ts = require('typescript');
import { getConfigNode, notFoundWarning } from '../update-vite-config';
export function updateTestConfig(
configContents: string,
projectConfig: ProjectConfiguration
projectConfig: ProjectConfiguration,
configPath: string
): string {
const configNode = getConfigNode(configContents);
if (!configNode) {
notFoundWarning(configPath);
return configContents;
}
const testObject = tsquery.query(
configContents,
configNode,
`PropertyAssignment:has(Identifier[name="test"])`
)?.[0];
let testCoverageDir: ts.Node;
@@ -2,6 +2,7 @@ import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import {
Tree,
addProjectConfiguration,
logger,
readProjectConfiguration,
} from '@nx/devkit';
@@ -40,6 +41,36 @@ describe('change-vite-ts-paths-plugin migration', () => {
readProjectConfiguration(tree, 'demo3').targets.build.options.outputPath
).toBe('dist/demo3');
});
it('should convert the file correctly', async () => {
addProject4(tree, 'demo4');
await updateBuildDir(tree);
expect(tree.read('demo4/vite.config.ts', 'utf-8')).toMatchSnapshot();
expect(
readProjectConfiguration(tree, 'demo4').targets.build.options.outputPath
).toBe('dist/demo4');
});
it('should convert the file correctly', async () => {
addProject4(tree, 'demo4');
await updateBuildDir(tree);
expect(tree.read('demo4/vite.config.ts', 'utf-8')).toMatchSnapshot();
expect(
readProjectConfiguration(tree, 'demo4').targets.build.options.outputPath
).toBe('dist/demo4');
});
it('should show warning to the user if could not recognize config', async () => {
jest.spyOn(logger, 'warn');
addProject5(tree, 'demo5');
await updateBuildDir(tree);
expect(tree.read('demo5/vite.config.ts', 'utf-8')).toMatchSnapshot();
expect(logger.warn).toHaveBeenCalledWith(
expect.stringContaining(
`Could not migrate your demo5/vite.config.ts file.`
)
);
});
});
function addProject1(tree: Tree, name: string) {
@@ -260,3 +291,103 @@ export default defineConfig({
`
);
}
function addProject4(tree: Tree, name: string) {
addProjectConfiguration(tree, name, {
root: `${name}`,
sourceRoot: `${name}/src`,
targets: {
build: {
executor: '@nx/vite:build',
outputs: ['{options.outputPath}'],
defaultConfiguration: 'production',
options: {
outputPath: `dist/${name}`,
buildLibsFromSource: false,
},
configurations: {
development: {
mode: 'development',
},
production: {
mode: 'production',
},
},
},
},
});
tree.write(
`${name}/vite.config.ts`,
`
/// <reference types='vitest' />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
export default defineConfig(({ mode }) => {
return {
cacheDir: '../../node_modules/.vite/demo4',
server: {
port: 4200,
host: 'localhost',
},
preview: {
port: 4300,
host: 'localhost',
},
plugins: [react(), nxViteTsPaths()],
// Uncomment this if you are using workers.
// worker: {
// plugins: [ nxViteTsPaths() ],
// },
test: {
globals: true,
cache: {
dir: '../../node_modules/.vitest',
},
environment: 'jsdom',
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
},
};
});
`
);
}
function addProject5(tree: Tree, name: string) {
addProjectConfiguration(tree, name, {
root: `${name}`,
sourceRoot: `${name}/src`,
targets: {
build: {
executor: '@nx/vite:build',
outputs: ['{options.outputPath}'],
defaultConfiguration: 'production',
options: {
outputPath: `dist/${name}`,
buildLibsFromSource: false,
},
configurations: {
development: {
mode: 'development',
},
production: {
mode: 'production',
},
},
},
},
});
tree.write(
`${name}/vite.config.ts`,
`
// some invalid config
`
);
}
@@ -1,9 +1,17 @@
import { Tree, formatFiles, getProjects, joinPathFragments } from '@nx/devkit';
import {
Tree,
formatFiles,
getProjects,
joinPathFragments,
logger,
} from '@nx/devkit';
import { forEachExecutorOptions } from '@nx/devkit/src/generators/executor-options-utils';
import { ViteBuildExecutorOptions } from '../../executors/build/schema';
import { updateBuildOutDirAndRoot } from './lib/edit-build-config';
import { updateTestConfig } from './lib/edit-test-config';
import { addFileReplacements } from './lib/add-file-replacements';
import { tsquery } from '@phenomnomnominal/tsquery';
import ts = require('typescript');
export default async function updateBuildDir(tree: Tree) {
const projects = getProjects(tree);
@@ -25,15 +33,17 @@ export default async function updateBuildDir(tree: Tree) {
projectConfig,
targetName,
tree,
projectName
projectName,
config
);
configContents = updateTestConfig(configContents, projectConfig);
configContents = updateTestConfig(configContents, projectConfig, config);
if (options.fileReplacements?.length > 0) {
configContents = addFileReplacements(
configContents,
options.fileReplacements
options.fileReplacements,
config
);
}
@@ -53,3 +63,34 @@ function findViteConfig(tree: Tree, searchRoot: string) {
}
}
}
export function getConfigNode(configFileContents: string): ts.Node | undefined {
if (!configFileContents) {
return;
}
let configNode = tsquery.query(
configFileContents,
`ObjectLiteralExpression`
)?.[0];
const arrowFunctionReturnStatement = tsquery.query(
configFileContents,
`ArrowFunction Block ReturnStatement ObjectLiteralExpression`
)?.[0];
if (arrowFunctionReturnStatement) {
configNode = arrowFunctionReturnStatement;
}
return configNode;
}
export function notFoundWarning(configPath: string) {
logger.warn(`
Could not migrate your ${configPath} file.
Please add the build.outDir and root options in your ${configPath} file.
You can find more information on how to configure vite for Nx here:
https://nx.dev/recipes/vite/configure-vite
`);
}
+8 -16
View File
@@ -6,7 +6,7 @@ import {
readTargetOptions,
} from '@nx/devkit';
import { existsSync } from 'fs';
import { PreviewOptions, ServerOptions } from 'vite';
import { ProxyOptions, ServerOptions } from 'vite';
import { ViteDevServerExecutorOptions } from '../executors/dev-server/schema';
/**
@@ -117,24 +117,16 @@ export async function getViteServerOptions(
return serverOptions;
}
/**
* Builds the options for the vite preview server.
*/
export function getVitePreviewOptions(
options: Record<string, any>,
context: ExecutorContext
): PreviewOptions {
const serverOptions: ServerOptions = {};
const proxyConfigPath = getViteServerProxyConfigPath(
options.proxyConfig,
context
);
export function getProxyConfig(
context: ExecutorContext,
proxyConfig?: string
): Record<string, string | ProxyOptions> | undefined {
const proxyConfigPath = getViteServerProxyConfigPath(proxyConfig, context);
if (proxyConfigPath) {
logger.info(`Loading proxy configuration from: ${proxyConfigPath}`);
serverOptions.proxy = require(proxyConfigPath);
return require(proxyConfigPath);
}
return serverOptions;
return;
}
export function getNxTargetOptions(target: string, context: ExecutorContext) {
+6
View File
@@ -29,6 +29,12 @@
"version": "16.0.0-beta.1",
"description": "Replace @nrwl/webpack with @nx/webpack",
"implementation": "./src/migrations/update-16-0-0-add-nx-packages/update-16-0-0-add-nx-packages"
},
"update-17-2-1-webpack-config-setup": {
"cli": "nx",
"version": "17.2.1-beta.0",
"description": "Add webpack.config.js file when webpackConfig is not defined",
"implementation": "./src/migrations/update-17-2-1/webpack-config-setup"
}
},
"packageJsonUpdates": {
@@ -81,7 +81,11 @@ export async function* devServerExecutor(
// Only add the dev server option if user is composable plugin.
// Otherwise, user should define `devServer` option directly in their webpack config.
if (isNxWebpackComposablePlugin(userDefinedWebpackConfig)) {
if (
typeof userDefinedWebpackConfig === 'function' &&
isNxWebpackComposablePlugin(userDefinedWebpackConfig) &&
!buildOptions.standardWebpackConfigFunction
) {
config = await userDefinedWebpackConfig(
{ devServer },
{
+1
View File
@@ -50,6 +50,7 @@ export interface WebpackExecutorOptions {
// TODO(v18): Remove this option
/** @deprecated set webpackConfig and provide an explicit webpack.config.js file (See: https://nx.dev/recipes/webpack/webpack-config-setup) */
isolatedConfig?: boolean;
standardWebpackConfigFunction?: boolean;
main?: string;
memoryLimit?: number;
namedChunks?: boolean;
@@ -237,6 +237,11 @@
"default": true,
"x-deprecated": "Automatic configuration of Webpack is deprecated in favor of an explicit 'webpack.config.js' file. This option will be removed in Nx 18. See https://nx.dev/recipes/webpack/webpack-config-setup."
},
"standardWebpackConfigFunction": {
"type": "boolean",
"description": "Set to true if the webpack config exports a standard webpack function, not an Nx-specific one. See: https://webpack.js.org/configuration/configuration-types/#exporting-a-function",
"default": false
},
"extractLicenses": {
"type": "boolean",
"description": "Extract all licenses in a separate file, in the case of production builds only."
@@ -23,7 +23,7 @@ import type {
} from './schema';
import { normalizeOptions } from './lib/normalize-options';
import {
composePlugins,
composePluginsSync,
isNxWebpackComposablePlugin,
} from '../../utils/config';
import { withNx } from '../../utils/with-nx';
@@ -54,9 +54,15 @@ async function getWebpackConfigs(
const config = options.isolatedConfig
? {}
: composePlugins(withNx(options), withWeb(options));
: (options.target === 'web'
? composePluginsSync(withNx(options), withWeb(options))
: withNx(options))({}, { options, context });
if (isNxWebpackComposablePlugin(userDefinedWebpackConfig)) {
if (
typeof userDefinedWebpackConfig === 'function' &&
isNxWebpackComposablePlugin(userDefinedWebpackConfig) &&
!options.standardWebpackConfigFunction
) {
// Old behavior, call the Nx-specific webpack config function that user exports
return await userDefinedWebpackConfig(config, {
options,
@@ -0,0 +1,89 @@
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { addProjectConfiguration, Tree } from '@nx/devkit';
import webpackConfigSetup from './webpack-config-setup';
describe('17.2.1 migration (setup webpack.config file)', () => {
let tree: Tree;
beforeEach(async () => {
tree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
});
it.each`
executor
${'@nx/webpack:webpack'}
${'@nrwl/webpack:webpack'}
`(
'should create webpack.config.js for projects that did not set webpackConfig',
async ({ executor }) => {
addProjectConfiguration(tree, 'myapp', {
root: 'apps/myapp',
targets: {
build: {
executor,
options: {},
},
},
});
await webpackConfigSetup(tree);
expect(tree.read('apps/myapp/webpack.config.js', 'utf-8'))
.toEqual(`const { composePlugins, withNx } = require('@nx/webpack');
// Nx plugins for webpack.
module.exports = composePlugins(withNx(), (config) => {
// Note: This was added by an Nx migration. Webpack builds are required to have a corresponding Webpack config file.
// See: https://nx.dev/recipes/webpack/webpack-config-setup
return config;
});
`);
}
);
it('should not create webpack.config.js when webpackConfig is already set', async () => {
tree.write(
`apps/myapp/webpack.config.js`,
`
module.exports = { /* CUSTOM */ };
`
);
addProjectConfiguration(tree, 'myapp', {
root: 'apps/myapp',
targets: {
build: {
executor: '@nrwl/webpack:webpack',
options: {
webpackConfig: 'apps/myapp/webpack.config.js',
},
},
},
});
await webpackConfigSetup(tree);
expect(tree.read('apps/myapp/webpack.config.js', 'utf-8')).toContain(
'/* CUSTOM */'
);
});
it('should not create webpack.config.js when isolatedConfig is set to false', async () => {
addProjectConfiguration(tree, 'myapp', {
root: 'apps/myapp',
targets: {
build: {
executor: '@nrwl/webpack:webpack',
options: {
// Technically this is not possible, since isolatedConfig without webpackConfig does not work
// Handling this edge-case anyway
isolatedConfig: false,
},
},
},
});
await webpackConfigSetup(tree);
expect(tree.exists('apps/myapp/webpack.config.js')).toBeFalsy();
});
});
@@ -0,0 +1,55 @@
import {
formatFiles,
readProjectConfiguration,
Tree,
updateProjectConfiguration,
} from '@nx/devkit';
import { forEachExecutorOptions } from '@nx/devkit/src/generators/executor-options-utils';
import { WebpackExecutorOptions } from '../../executors/webpack/schema';
export default async function (tree: Tree) {
const update = (
options: WebpackExecutorOptions,
projectName: string,
targetName: string,
configurationName: string
) => {
// Only handle webpack config for default configuration
if (configurationName) return;
const projectConfiguration = readProjectConfiguration(tree, projectName);
if (!options.webpackConfig && options.isolatedConfig !== false) {
options.webpackConfig = `${projectConfiguration.root}/webpack.config.js`;
tree.write(
options.webpackConfig,
`
const { composePlugins, withNx } = require('@nx/webpack');
// Nx plugins for webpack.
module.exports = composePlugins(withNx(), (config) => {
// Note: This was added by an Nx migration. Webpack builds are required to have a corresponding Webpack config file.
// See: https://nx.dev/recipes/webpack/webpack-config-setup
return config;
});
`
);
projectConfiguration.targets[targetName].options = options;
updateProjectConfiguration(tree, projectName, projectConfiguration);
}
};
forEachExecutorOptions<WebpackExecutorOptions>(
tree,
'@nx/webpack:webpack',
update
);
forEachExecutorOptions<WebpackExecutorOptions>(
tree,
'@nrwl/webpack:webpack',
update
);
await formatFiles(tree);
}
@@ -43,6 +43,7 @@ async function addMigrationPackageGroup(
'@angular/material',
'@angular/cdk',
'@angular/ssr',
'@angular/pwa',
].includes(pkgName)
) {
continue;
@@ -9,6 +9,7 @@ const packagesToUpdate: PackageSpec[] = [
'@angular-devkit/build-angular',
'@angular-devkit/core',
'@angular-devkit/schematics',
'@angular/pwa',
'@angular/ssr',
'@schematics/angular',
],