feat(vite): add convert-to-inferred migration generator (#26249)

- feat(vite): add convert-to-inferred generator for migrating to
inference
- feat(vite): add build postTargetTransformer
- feat(vite): add serve, preview, test postTargetTransformer
- feat(vite): convert-to-inferred should clean up inputs and outputs
- docs(vite): add convert-to-inferred
- feat(vite): update outDir correctly

<!-- Please make sure you have read the submission guidelines before
posting an PR -->
<!--
https://github.com/nrwl/nx/blob/master/CONTRIBUTING.md#-submitting-a-pr
-->

<!-- Please make sure that your commit message follows our format -->
<!-- Example: `fix(nx): must begin with lowercase` -->

## Current Behavior
<!-- This is the behavior we have today -->
There is currently no generator that can migrate projects that use
`@nx/vite:*` executors to use Inference plugins.

## Expected Behavior
<!-- This is the behavior we should expect with the changes in this PR
-->
Add `@nx/vite:convert-to-inferred` generator

## Related Issue(s)
<!-- Please link the issue being fixed so it gets closed when this is
merged. -->

Fixes #

---------

Co-authored-by: Jack Hsu <jack.hsu@gmail.com>
This commit is contained in:
Colum Ferry 2024-06-14 20:53:25 +02:00 committed by GitHub
parent e9b7439ce2
commit b36c39e331
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 2077 additions and 4 deletions

View File

@ -9593,6 +9593,14 @@
"isExternal": false,
"disableCollapsible": false
},
{
"id": "convert-to-inferred",
"path": "/nx-api/vite/generators/convert-to-inferred",
"name": "convert-to-inferred",
"children": [],
"isExternal": false,
"disableCollapsible": false
},
{
"id": "vitest",
"path": "/nx-api/vite/generators/vitest",

View File

@ -2968,6 +2968,15 @@
"path": "/nx-api/vite/generators/setup-paths-plugin",
"type": "generator"
},
"/nx-api/vite/generators/convert-to-inferred": {
"description": "Convert existing Vite project(s) using `@nx/vite:*` executors to use `@nx/vite/plugin`. Defaults to migrating all projects. Pass '--project' to migrate only one target.",
"file": "generated/packages/vite/generators/convert-to-inferred.json",
"hidden": false,
"name": "convert-to-inferred",
"originalFilePath": "/packages/vite/src/generators/convert-to-inferred/schema.json",
"path": "/nx-api/vite/generators/convert-to-inferred",
"type": "generator"
},
"/nx-api/vite/generators/vitest": {
"description": "Generate a vitest configuration.",
"file": "generated/packages/vite/generators/vitest.json",

View File

@ -2936,6 +2936,15 @@
"path": "vite/generators/setup-paths-plugin",
"type": "generator"
},
{
"description": "Convert existing Vite project(s) using `@nx/vite:*` executors to use `@nx/vite/plugin`. Defaults to migrating all projects. Pass '--project' to migrate only one target.",
"file": "generated/packages/vite/generators/convert-to-inferred.json",
"hidden": false,
"name": "convert-to-inferred",
"originalFilePath": "/packages/vite/src/generators/convert-to-inferred/schema.json",
"path": "vite/generators/convert-to-inferred",
"type": "generator"
},
{
"description": "Generate a vitest configuration.",
"file": "generated/packages/vite/generators/vitest.json",

View File

@ -0,0 +1,30 @@
{
"name": "convert-to-inferred",
"factory": "./src/generators/convert-to-inferred/convert-to-inferred",
"schema": {
"$schema": "https://json-schema.org/schema",
"$id": "NxViteConvertToInferred",
"description": "Convert existing Vite project(s) using `@nx/vite:*` executors to use `@nx/vite/plugin`. Defaults to migrating all projects. Pass '--project' to migrate only one target.",
"title": "Convert Vite project from executor to plugin",
"type": "object",
"properties": {
"project": {
"type": "string",
"description": "The project to convert from using the `@nx/vite:*` executors to use `@nx/vite/plugin`.",
"x-priority": "important"
},
"skipFormat": {
"type": "boolean",
"description": "Whether to format files at the end of the migration.",
"default": false
}
},
"presets": []
},
"description": "Convert existing Vite project(s) using `@nx/vite:*` executors to use `@nx/vite/plugin`. Defaults to migrating all projects. Pass '--project' to migrate only one target.",
"implementation": "/packages/vite/src/generators/convert-to-inferred/convert-to-inferred.ts",
"aliases": [],
"hidden": false,
"path": "/packages/vite/src/generators/convert-to-inferred/schema.json",
"type": "generator"
}

View File

@ -676,6 +676,7 @@
- [init](/nx-api/vite/generators/init)
- [configuration](/nx-api/vite/generators/configuration)
- [setup-paths-plugin](/nx-api/vite/generators/setup-paths-plugin)
- [convert-to-inferred](/nx-api/vite/generators/convert-to-inferred)
- [vitest](/nx-api/vite/generators/vitest)
- [vue](/nx-api/vue)
- [documents](/nx-api/vue/documents)

View File

@ -21,6 +21,11 @@
"schema": "./src/generators/setup-paths-plugin/schema.json",
"description": "Sets up the nxViteTsPaths plugin to enable support for workspace libraries."
},
"convert-to-inferred": {
"factory": "./src/generators/convert-to-inferred/convert-to-inferred",
"schema": "./src/generators/convert-to-inferred/schema.json",
"description": "Convert existing Vite project(s) using `@nx/vite:*` executors to use `@nx/vite/plugin`. Defaults to migrating all projects. Pass '--project' to migrate only one target."
},
"vitest": {
"factory": "./src/generators/vitest/vitest-generator#vitestGeneratorInternal",
"schema": "./src/generators/vitest/schema.json",

View File

@ -0,0 +1,634 @@
import {
getRelativeProjectJsonSchemaPath,
updateProjectConfiguration,
} from 'nx/src/generators/utils/project-configuration';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { convertToInferred } from './convert-to-inferred';
import {
addProjectConfiguration as _addProjectConfiguration,
type ExpandedPluginConfiguration,
joinPathFragments,
type ProjectConfiguration,
type ProjectGraph,
readNxJson,
readProjectConfiguration,
type Tree,
updateNxJson,
writeJson,
} from '@nx/devkit';
import { TempFs } from '@nx/devkit/internal-testing-utils';
import { join } from 'node:path';
let fs: TempFs;
let projectGraph: ProjectGraph;
let mockedConfigs = {};
const getMockedConfig = (
opts: { configFile: string; mode: 'development' },
target: string
) => {
const relativeConfigFile = opts.configFile.replace(`${fs.tempDir}/`, '');
return Promise.resolve({
path: opts.configFile,
config: mockedConfigs[relativeConfigFile],
build: mockedConfigs[relativeConfigFile]['build'],
test: mockedConfigs[relativeConfigFile]['test'],
dependencies: [],
});
};
jest.mock('vite', () => ({
resolveConfig: jest.fn().mockImplementation(getMockedConfig),
}));
jest.mock('../../utils/executor-utils', () => ({
loadViteDynamicImport: jest.fn().mockImplementation(() => ({
resolveConfig: jest.fn().mockImplementation(getMockedConfig),
})),
}));
jest.mock('@nx/devkit', () => ({
...jest.requireActual<any>('@nx/devkit'),
createProjectGraphAsync: jest.fn().mockImplementation(async () => {
return projectGraph;
}),
updateProjectConfiguration: jest
.fn()
.mockImplementation((tree, projectName, projectConfiguration) => {
function handleEmptyTargets(
projectName: string,
projectConfiguration: ProjectConfiguration
): void {
if (
projectConfiguration.targets &&
!Object.keys(projectConfiguration.targets).length
) {
// Re-order `targets` to appear after the `// target` comment.
delete projectConfiguration.targets;
projectConfiguration[
'// targets'
] = `to see all targets run: nx show project ${projectName} --web`;
projectConfiguration.targets = {};
} else {
delete projectConfiguration['// targets'];
}
}
const projectConfigFile = joinPathFragments(
projectConfiguration.root,
'project.json'
);
if (!tree.exists(projectConfigFile)) {
throw new Error(
`Cannot update Project ${projectName} at ${projectConfiguration.root}. It either doesn't exist yet, or may not use project.json for configuration. Use \`addProjectConfiguration()\` instead if you want to create a new project.`
);
}
handleEmptyTargets(projectName, projectConfiguration);
writeJson(tree, projectConfigFile, {
name: projectConfiguration.name ?? projectName,
$schema: getRelativeProjectJsonSchemaPath(tree, projectConfiguration),
...projectConfiguration,
root: undefined,
});
projectGraph.nodes[projectName].data = projectConfiguration;
}),
}));
function addProjectConfiguration(
tree: Tree,
name: string,
project: ProjectConfiguration
) {
_addProjectConfiguration(tree, name, project);
projectGraph.nodes[name] = {
name: name,
type: project.projectType === 'application' ? 'app' : 'lib',
data: {
projectType: project.projectType,
root: project.root,
targets: project.targets,
},
};
}
interface CreateViteTestProjectOptions {
appName: string;
appRoot: string;
buildTargetName: string;
serveTargetName: string;
previewTargetName: string;
testTargetName: string;
outputPath: string;
}
const defaultCreateViteTestProjectOptions: CreateViteTestProjectOptions = {
appName: 'myapp',
appRoot: 'myapp',
buildTargetName: 'build',
serveTargetName: 'serve',
previewTargetName: 'preview',
testTargetName: 'test',
outputPath: '{workspaceRoot}/dist/myapp',
};
function createTestProject(
tree: Tree,
opts: Partial<CreateViteTestProjectOptions> = defaultCreateViteTestProjectOptions
) {
let projectOpts = { ...defaultCreateViteTestProjectOptions, ...opts };
const project: ProjectConfiguration = {
name: projectOpts.appName,
root: projectOpts.appRoot,
projectType: 'application',
targets: {
[projectOpts.buildTargetName]: {
executor: '@nx/vite:build',
outputs: [projectOpts.outputPath],
options: {
configFile: `${projectOpts.appRoot}/vite.config.ts`,
},
},
[projectOpts.serveTargetName]: {
executor: '@nx/vite:dev-server',
options: {
buildTarget: `${projectOpts.appName}:${projectOpts.buildTargetName}`,
},
},
[projectOpts.previewTargetName]: {
executor: '@nx/vite:preview-server',
options: {
buildTarget: `${projectOpts.appName}:${projectOpts.buildTargetName}`,
},
},
[projectOpts.testTargetName]: {
executor: '@nx/vite:test',
options: {
configFile: `${projectOpts.appRoot}/vite.config.ts`,
},
},
},
};
const viteConfigContents = `/// <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,
cacheDir: '../../node_modules/.vite/apps/myapp',
server: {
port: 4200,
host: 'localhost',
},
preview: {
port: 4300,
host: 'localhost',
},
plugins: [react(), nxViteTsPaths()],
// Uncomment this if you are using workers.
// worker: {
// plugins: [ nxViteTsPaths() ],
// },
build: {
outDir: '../../dist/apps/myapp',
reportCompressedSize: true,
commonjsOptions: {
transformMixedEsModules: true,
},
},
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/apps/myapp',
provider: 'v8',
},
},
});`;
tree.write(`${projectOpts.appRoot}/vite.config.ts`, viteConfigContents);
fs.createFileSync(
`${projectOpts.appRoot}/vite.config.ts`,
viteConfigContents
);
tree.write(`${projectOpts.appRoot}/index.html`, `<html></html>`);
fs.createFileSync(`${projectOpts.appRoot}/index.html`, `<html></html>`);
mockedConfigs[`${projectOpts.appRoot}/vite.config.ts`] = {
root: `${projectOpts.appRoot}`,
cacheDir: `../../node_modules/.vite/${projectOpts.appName}`,
server: {
port: 4200,
host: 'localhost',
},
preview: {
port: 4300,
host: 'localhost',
},
build: {
outDir: `../../dist/${projectOpts.appRoot}`,
reportCompressedSize: true,
commonjsOptions: {
transformMixedEsModules: true,
},
},
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/${projectOpts.appRoot}`,
provider: 'v8',
},
},
};
jest.doMock(
join(fs.tempDir, `${projectOpts.appRoot}/vite.config.ts`),
() => ({
default: mockedConfigs[`${projectOpts.appRoot}/vite.config.ts`],
}),
{
virtual: true,
}
);
addProjectConfiguration(tree, project.name, project);
fs.createFileSync(
`${projectOpts.appRoot}/project.json`,
JSON.stringify(project)
);
return project;
}
describe('Vite - Convert Executors To Plugin', () => {
let tree: Tree;
beforeEach(() => {
fs = new TempFs('vite');
tree = createTreeWithEmptyWorkspace();
tree.root = fs.tempDir;
mockedConfigs = {};
projectGraph = {
nodes: {},
dependencies: {},
externalNodes: {},
};
});
afterEach(() => {
fs.reset();
});
describe('--project', () => {
it('should setup a new Vite plugin and only migrate one specific project', async () => {
// ARRANGE
const existingProject = createTestProject(tree, {
appRoot: 'existing',
appName: 'existing',
buildTargetName: 'build',
});
const project = createTestProject(tree, {
buildTargetName: 'build-base',
});
const secondProject = createTestProject(tree, {
appRoot: 'second',
appName: 'second',
buildTargetName: 'build-base',
});
const thirdProject = createTestProject(tree, {
appRoot: 'third',
appName: 'third',
buildTargetName: 'package',
});
const nxJson = readNxJson(tree);
nxJson.plugins ??= [];
nxJson.plugins.push({
plugin: '@nx/vite/plugin',
options: {
buildTargetName: 'build',
testTargetName: 'test',
previewTargetName: 'preview',
serveTargetName: 'serve',
},
});
updateNxJson(tree, nxJson);
// ACT
await convertToInferred(tree, { project: 'myapp', skipFormat: true });
// ASSERT
// project.json modifications
const updatedProject = readProjectConfiguration(tree, project.name);
expect(updatedProject.targets).toMatchInlineSnapshot(`
{
"build-base": {
"options": {
"config": "./vite.config.ts",
},
},
"test": {
"options": {
"config": "./vite.config.ts",
},
},
}
`);
// nx.json modifications
const nxJsonPlugins = readNxJson(tree).plugins;
const addedTestVitePlugin = nxJsonPlugins.find((plugin) => {
if (
typeof plugin !== 'string' &&
plugin.plugin === '@nx/vite/plugin' &&
plugin.include?.length === 1
) {
return true;
}
});
expect(addedTestVitePlugin).toBeTruthy();
expect(
(addedTestVitePlugin as ExpandedPluginConfiguration).include
).toEqual(['myapp/**/*']);
});
});
describe('--all', () => {
it('should successfully migrate a project using Vite executors to plugin', async () => {
const project = createTestProject(tree);
// ACT
await convertToInferred(tree, { skipFormat: true });
// ASSERT
// project.json modifications
const updatedProject = readProjectConfiguration(tree, project.name);
const targetKeys = Object.keys(updatedProject.targets);
expect(targetKeys).not.toContain('e2e');
// nx.json modifications
const nxJsonPlugins = readNxJson(tree).plugins;
const hasVitePlugin = nxJsonPlugins.find((plugin) =>
typeof plugin === 'string'
? plugin === '@nx/vite/plugin'
: plugin.plugin === '@nx/vite/plugin'
);
expect(hasVitePlugin).toBeTruthy();
if (typeof hasVitePlugin !== 'string') {
[
['buildTargetName', 'build'],
['serveTargetName', 'serve'],
['previewTargetName', 'preview'],
['testTargetName', 'test'],
].forEach(([targetOptionName, targetName]) => {
expect(hasVitePlugin.options[targetOptionName]).toEqual(targetName);
});
}
});
it('should setup Vite plugin to match projects', async () => {
// ARRANGE
const project = createTestProject(tree, {
buildTargetName: 'bundle',
});
// ACT
await convertToInferred(tree, { skipFormat: true });
// ASSERT
// project.json modifications
const updatedProject = readProjectConfiguration(tree, project.name);
expect(updatedProject.targets).toMatchInlineSnapshot(`
{
"bundle": {
"options": {
"config": "./vite.config.ts",
},
},
"test": {
"options": {
"config": "./vite.config.ts",
},
},
}
`);
// nx.json modifications
const nxJsonPlugins = readNxJson(tree).plugins;
const hasVitePlugin = nxJsonPlugins.find((plugin) =>
typeof plugin === 'string'
? plugin === '@nx/vite/plugin'
: plugin.plugin === '@nx/vite/plugin'
);
expect(hasVitePlugin).toBeTruthy();
if (typeof hasVitePlugin !== 'string') {
[
['buildTargetName', 'bundle'],
['serveTargetName', 'serve'],
['previewTargetName', 'preview'],
['testTargetName', 'test'],
].forEach(([targetOptionName, targetName]) => {
expect(hasVitePlugin.options[targetOptionName]).toEqual(targetName);
});
}
});
it('should setup a new Vite plugin to match only projects migrated', async () => {
// ARRANGE
const existingProject = createTestProject(tree, {
appRoot: 'existing',
appName: 'existing',
buildTargetName: 'build',
});
const project = createTestProject(tree, {
buildTargetName: 'bundle',
});
const secondProject = createTestProject(tree, {
appRoot: 'second',
appName: 'second',
buildTargetName: 'bundle',
});
const thirdProject = createTestProject(tree, {
appRoot: 'third',
appName: 'third',
buildTargetName: 'build-base',
});
const nxJson = readNxJson(tree);
nxJson.plugins ??= [];
nxJson.plugins.push({
plugin: '@nx/vite/plugin',
options: {
buildTargetName: 'build',
serveTargetName: 'serve',
previewTargetName: 'preview',
testTargetName: 'test',
},
});
updateNxJson(tree, nxJson);
// ACT
await convertToInferred(tree, { skipFormat: true });
// ASSERT
// project.json modifications
const updatedProject = readProjectConfiguration(tree, project.name);
expect(updatedProject.targets).toMatchInlineSnapshot(`
{
"bundle": {
"options": {
"config": "./vite.config.ts",
},
},
"test": {
"options": {
"config": "./vite.config.ts",
},
},
}
`);
// nx.json modifications
const nxJsonPlugins = readNxJson(tree).plugins;
const addedTestVitePlugin = nxJsonPlugins.find((plugin) => {
if (
typeof plugin !== 'string' &&
plugin.plugin === '@nx/vite/plugin' &&
plugin.include?.length === 2
) {
return true;
}
});
expect(addedTestVitePlugin).toBeTruthy();
expect(
(addedTestVitePlugin as ExpandedPluginConfiguration).include
).toEqual(['myapp/**/*', 'second/**/*']);
const addedIntegrationVitePlugin = nxJsonPlugins.find((plugin) => {
if (
typeof plugin !== 'string' &&
plugin.plugin === '@nx/vite/plugin' &&
plugin.include?.length === 1
) {
return true;
}
});
expect(addedIntegrationVitePlugin).toBeTruthy();
expect(
(addedIntegrationVitePlugin as ExpandedPluginConfiguration).include
).toEqual(['third/**/*']);
});
it('should keep Vite options in project.json', async () => {
// ARRANGE
const project = createTestProject(tree);
project.targets.build.options.mode = 'development';
updateProjectConfiguration(tree, project.name, project);
// ACT
await convertToInferred(tree, { skipFormat: true });
// ASSERT
// project.json modifications
const updatedProject = readProjectConfiguration(tree, project.name);
expect(updatedProject.targets.build).toMatchInlineSnapshot(`
{
"options": {
"config": "./vite.config.ts",
"mode": "development",
},
}
`);
// nx.json modifications
const nxJsonPlugins = readNxJson(tree).plugins;
const hasVitePlugin = nxJsonPlugins.find((plugin) =>
typeof plugin === 'string'
? plugin === '@nx/vite/plugin'
: plugin.plugin === '@nx/vite/plugin'
);
expect(hasVitePlugin).toBeTruthy();
if (typeof hasVitePlugin !== 'string') {
[
['buildTargetName', 'build'],
['serveTargetName', 'serve'],
['previewTargetName', 'preview'],
['testTargetName', 'test'],
].forEach(([targetOptionName, targetName]) => {
expect(hasVitePlugin.options[targetOptionName]).toEqual(targetName);
});
}
});
it('should add Vite options found in targetDefaults for the executor to the project.json', async () => {
// ARRANGE
const nxJson = readNxJson(tree);
nxJson.targetDefaults ??= {};
nxJson.targetDefaults['@nx/vite:build'] = {
options: {
mode: 'production',
},
};
updateNxJson(tree, nxJson);
const project = createTestProject(tree);
// ACT
await convertToInferred(tree, { skipFormat: true });
// ASSERT
// project.json modifications
const updatedProject = readProjectConfiguration(tree, project.name);
expect(updatedProject.targets.build).toMatchInlineSnapshot(`
{
"options": {
"config": "./vite.config.ts",
"mode": "production",
},
}
`);
// nx.json modifications
const nxJsonPlugins = readNxJson(tree).plugins;
const hasVitePlugin = nxJsonPlugins.find((plugin) =>
typeof plugin === 'string'
? plugin === '@nx/vite/plugin'
: plugin.plugin === '@nx/vite/plugin'
);
expect(hasVitePlugin).toBeTruthy();
if (typeof hasVitePlugin !== 'string') {
[
['buildTargetName', 'build'],
['serveTargetName', 'serve'],
['previewTargetName', 'preview'],
['testTargetName', 'test'],
].forEach(([targetOptionName, targetName]) => {
expect(hasVitePlugin.options[targetOptionName]).toEqual(targetName);
});
}
});
});
});

View File

@ -0,0 +1,105 @@
import { createProjectGraphAsync, formatFiles, type Tree } from '@nx/devkit';
import { migrateExecutorToPlugin } from '@nx/devkit/src/generators/plugin-migrations/executor-to-plugin-migrator';
import { createNodesV2, VitePluginOptions } from '../../plugins/plugin';
import { buildPostTargetTransformer } from './lib/build-post-target-transformer';
import { servePostTargetTransformer } from './lib/serve-post-target-transformer';
import { previewPostTargetTransformer } from './lib/preview-post-target-transformer';
import { testPostTargetTransformer } from './lib/test-post-target-transformer';
import { AggregatedLog } from '@nx/devkit/src/generators/plugin-migrations/aggregate-log-util';
interface Schema {
project?: string;
skipFormat?: boolean;
}
export async function convertToInferred(tree: Tree, options: Schema) {
const projectGraph = await createProjectGraphAsync();
const migrationLogs = new AggregatedLog();
const migratedBuildProjects =
await migrateExecutorToPlugin<VitePluginOptions>(
tree,
projectGraph,
'@nx/vite:build',
'@nx/vite/plugin',
(targetName) => ({
buildTargetName: targetName,
serveTargetName: 'serve',
previewTargetName: 'preview',
testTargetName: 'test',
serveStaticTargetName: 'serve-static',
}),
buildPostTargetTransformer,
createNodesV2,
options.project
);
const migratedServeProjects =
await migrateExecutorToPlugin<VitePluginOptions>(
tree,
projectGraph,
'@nx/vite:dev-server',
'@nx/vite/plugin',
(targetName) => ({
buildTargetName: 'build',
serveTargetName: targetName,
previewTargetName: 'preview',
testTargetName: 'test',
serveStaticTargetName: 'serve-static',
}),
servePostTargetTransformer(migrationLogs),
createNodesV2,
options.project
);
const migratedPreviewProjects =
await migrateExecutorToPlugin<VitePluginOptions>(
tree,
projectGraph,
'@nx/vite:preview-server',
'@nx/vite/plugin',
(targetName) => ({
buildTargetName: 'build',
serveTargetName: 'serve',
previewTargetName: targetName,
testTargetName: 'test',
serveStaticTargetName: 'serve-static',
}),
previewPostTargetTransformer(migrationLogs),
createNodesV2,
options.project
);
const migratedTestProjects = await migrateExecutorToPlugin<VitePluginOptions>(
tree,
projectGraph,
'@nx/vite:test',
'@nx/vite/plugin',
(targetName) => ({
buildTargetName: 'build',
serveTargetName: 'serve',
previewTargetName: 'preview',
testTargetName: targetName,
serveStaticTargetName: 'serve-static',
}),
testPostTargetTransformer,
createNodesV2,
options.project
);
const migratedProjects =
migratedBuildProjects.size +
migratedServeProjects.size +
migratedPreviewProjects.size +
migratedTestProjects.size;
if (migratedProjects === 0) {
throw new Error('Could not find any targets to migrate.');
}
if (!options.skipFormat) {
await formatFiles(tree);
}
return () => {
migrationLogs.flushLogs();
};
}
export default convertToInferred;

View File

@ -0,0 +1,271 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`buildPostTargetTransformer moveBuildLibsFromSourceToViteConfig should add buildLibsFromSource to existing nxViteTsPaths plugin with existing options 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({
root: __dirname,
cacheDir: '../../node_modules/.vite/apps/myapp',
server: {
port: 4200,
host: 'localhost',
},
preview: {
port: 4300,
host: 'localhost',
},
plugins: [react(), nxViteTsPaths({buildLibsFromSource: options.buildLibsFromSource, debug: true })],
// Uncomment this if you are using workers.
// worker: {
// plugins: [ nxViteTsPaths() ],
// },
build: {
outDir: '../../dist/apps/myapp',
reportCompressedSize: true,
commonjsOptions: {
transformMixedEsModules: true,
},
},
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/apps/myapp',
provider: 'v8',
},
},
});"
`;
exports[`buildPostTargetTransformer moveBuildLibsFromSourceToViteConfig should add buildLibsFromSource to existing nxViteTsPaths plugin with no existing options 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({
root: __dirname,
cacheDir: '../../node_modules/.vite/apps/myapp',
server: {
port: 4200,
host: 'localhost',
},
preview: {
port: 4300,
host: 'localhost',
},
plugins: [react(), nxViteTsPaths({ buildLibsFromSource: options.buildLibsFromSource }),],
// Uncomment this if you are using workers.
// worker: {
// plugins: [ nxViteTsPaths() ],
// },
build: {
outDir: '../../dist/apps/myapp',
reportCompressedSize: true,
commonjsOptions: {
transformMixedEsModules: true,
},
},
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/apps/myapp',
provider: 'v8',
},
},
});"
`;
exports[`buildPostTargetTransformer moveBuildLibsFromSourceToViteConfig should add buildLibsFromSource to new nxViteTsPaths plugin when the plugin is not added 1`] = `
"import { nxViteTsPaths } from "@nx/vite/plugins/nx-tsconfig-paths.plugin";
/// <reference types='vitest' />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
root: __dirname,
cacheDir: '../../node_modules/.vite/apps/myapp',
server: {
port: 4200,
host: 'localhost',
},
preview: {
port: 4300,
host: 'localhost',
},
plugins: [nxViteTsPaths({ buildLibsFromSource: options.buildLibsFromSource }),react()],
// Uncomment this if you are using workers.
// worker: {
// plugins: [ nxViteTsPaths() ],
// },
build: {
outDir: '../../dist/apps/myapp',
reportCompressedSize: true,
commonjsOptions: {
transformMixedEsModules: true,
},
},
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/apps/myapp',
provider: 'v8',
},
},
});"
`;
exports[`buildPostTargetTransformer moveBuildLibsFromSourceToViteConfig should add buildLibsFromSource to new nxViteTsPaths plugin when the plugins property does not exist 1`] = `
"import { nxViteTsPaths } from "@nx/vite/plugins/nx-tsconfig-paths.plugin";
/// <reference types='vitest' />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({plugins: [nxViteTsPaths({ buildLibsFromSource: options.buildLibsFromSource }),],
root: __dirname,
cacheDir: '../../node_modules/.vite/apps/myapp',
server: {
port: 4200,
host: 'localhost',
},
preview: {
port: 4300,
host: 'localhost',
},
// Uncomment this if you are using workers.
// worker: {
// plugins: [ nxViteTsPaths() ],
// },
build: {
outDir: '../../dist/apps/myapp',
reportCompressedSize: true,
commonjsOptions: {
transformMixedEsModules: true,
},
},
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/apps/myapp',
provider: 'v8',
},
},
});"
`;
exports[`buildPostTargetTransformer should remove the correct options and move the AST options to the vite config file correctly and remove outputs when they match inferred 1`] = `
"/// <reference types='vitest' />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
// These options were migrated by @nx/vite:convert-to-inferred from the project.json file.
const configValues = {"default":{"buildLibsFromSource":true}};
// Determine the correct configValue to use based on the configuration
const nxConfiguration = process.env.NX_TASK_TARGET_CONFIGURATION ?? 'default';
const options = {
...configValues.default,
...(configValues[nxConfiguration] ?? {})
}
export default defineConfig({
root: __dirname,
cacheDir: '../../node_modules/.vite/apps/myapp',
server: {
port: 4200,
host: 'localhost',
},
preview: {
port: 4300,
host: 'localhost',
},
plugins: [react(), nxViteTsPaths({ buildLibsFromSource: options.buildLibsFromSource }),],
// Uncomment this if you are using workers.
// worker: {
// plugins: [ nxViteTsPaths() ],
// },
build: {
outDir: '../../dist/apps/myapp',
reportCompressedSize: true,
commonjsOptions: {
transformMixedEsModules: true,
},
},
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/apps/myapp',
provider: 'v8',
},
},
});"
`;

View File

@ -0,0 +1,414 @@
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import {
buildPostTargetTransformer,
moveBuildLibsFromSourceToViteConfig,
} from './build-post-target-transformer';
describe('buildPostTargetTransformer', () => {
it('should remove the correct options and move the AST options to the vite config file correctly and remove outputs when they match inferred', () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace();
const targetConfiguration = {
outputs: ['{options.outputPath}'],
options: {
outputPath: 'build/apps/myapp',
configFile: 'vite.config.ts',
buildLibsFromSource: true,
skipTypeCheck: false,
watch: true,
generatePackageJson: true,
includeDevDependenciesInPackageJson: false,
tsConfig: 'apps/myapp/tsconfig.json',
},
};
const inferredTargetConfiguration = {
outputs: ['{projectRoot}/{options.outDir}'],
};
tree.write('vite.config.ts', viteConfigFileV17);
// ACT
const target = buildPostTargetTransformer(
targetConfiguration,
tree,
{
projectName: 'myapp',
root: 'apps/myapp',
},
inferredTargetConfiguration
);
// ASSERT
const configFile = tree.read('vite.config.ts', 'utf-8');
expect(configFile).toMatchSnapshot();
expect(target).toMatchInlineSnapshot(`
{
"options": {
"config": "../../vite.config.ts",
"outDir": "../../build/apps/myapp",
"watch": true,
},
}
`);
});
it('should add inferred outputs when a custom output exists', () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace();
const targetConfiguration = {
outputs: ['{options.outputPath}', '{workspaceRoot}/my/custom/path'],
options: {
outputPath: 'build/apps/myapp',
configFile: 'vite.config.ts',
buildLibsFromSource: true,
skipTypeCheck: false,
watch: true,
generatePackageJson: true,
includeDevDependenciesInPackageJson: false,
tsConfig: 'apps/myapp/tsconfig.json',
},
};
const inferredTargetConfiguration = {
outputs: ['{projectRoot}/{options.outDir}'],
};
tree.write('vite.config.ts', viteConfigFileV17);
// ACT
const target = buildPostTargetTransformer(
targetConfiguration,
tree,
{
projectName: 'myapp',
root: 'apps/myapp',
},
inferredTargetConfiguration
);
// ASSERT
expect(target).toMatchInlineSnapshot(`
{
"options": {
"config": "../../vite.config.ts",
"outDir": "../../build/apps/myapp",
"watch": true,
},
"outputs": [
"{projectRoot}/{options.outDir}",
"{workspaceRoot}/my/custom/path",
],
}
`);
});
describe('moveBuildLibsFromSourceToViteConfig', () => {
it('should add buildLibsFromSource to existing nxViteTsPaths plugin with no existing options', () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace();
tree.write('vite.config.ts', viteConfigFileV17);
// ACT
moveBuildLibsFromSourceToViteConfig(tree, 'vite.config.ts');
// ASSERT
const newContents = tree.read('vite.config.ts', 'utf-8');
expect(newContents).toContain(
'nxViteTsPaths({ buildLibsFromSource: options.buildLibsFromSource })'
);
expect(newContents).toMatchSnapshot();
});
it('should add buildLibsFromSource to existing nxViteTsPaths plugin with existing options', () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace();
tree.write('vite.config.ts', viteConfigFileV17NxViteTsPathsOpts);
// ACT
moveBuildLibsFromSourceToViteConfig(tree, 'vite.config.ts');
// ASSERT
const newContents = tree.read('vite.config.ts', 'utf-8');
expect(newContents).toContain(
'nxViteTsPaths({buildLibsFromSource: options.buildLibsFromSource, debug: true })'
);
expect(newContents).toMatchSnapshot();
});
it('should add buildLibsFromSource to new nxViteTsPaths plugin when the plugin is not added', () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace();
tree.write('vite.config.ts', viteConfigFileV17NoNxViteTsPaths);
// ACT
moveBuildLibsFromSourceToViteConfig(tree, 'vite.config.ts');
// ASSERT
const newContents = tree.read('vite.config.ts', 'utf-8');
expect(newContents).toContain(
'nxViteTsPaths({ buildLibsFromSource: options.buildLibsFromSource })'
);
expect(newContents).toMatchSnapshot();
});
it('should add buildLibsFromSource to new nxViteTsPaths plugin when the plugins property does not exist', () => {
// ARRANGE
const tree = createTreeWithEmptyWorkspace();
tree.write('vite.config.ts', viteConfigFileV17NoPlugins);
// ACT
moveBuildLibsFromSourceToViteConfig(tree, 'vite.config.ts');
// ASSERT
const newContents = tree.read('vite.config.ts', 'utf-8');
expect(newContents).toContain(
'nxViteTsPaths({ buildLibsFromSource: options.buildLibsFromSource })'
);
expect(newContents).toMatchSnapshot();
});
});
});
const viteConfigFileV17 = `/// <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,
cacheDir: '../../node_modules/.vite/apps/myapp',
server: {
port: 4200,
host: 'localhost',
},
preview: {
port: 4300,
host: 'localhost',
},
plugins: [react(), nxViteTsPaths()],
// Uncomment this if you are using workers.
// worker: {
// plugins: [ nxViteTsPaths() ],
// },
build: {
outDir: '../../dist/apps/myapp',
reportCompressedSize: true,
commonjsOptions: {
transformMixedEsModules: true,
},
},
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/apps/myapp',
provider: 'v8',
},
},
});`;
const viteConfigFileV17NoOutDir = `/// <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,
cacheDir: '../../node_modules/.vite/apps/myapp',
server: {
port: 4200,
host: 'localhost',
},
preview: {
port: 4300,
host: 'localhost',
},
plugins: [react(), nxViteTsPaths()],
// Uncomment this if you are using workers.
// worker: {
// plugins: [ nxViteTsPaths() ],
// },
build: {
reportCompressedSize: true,
commonjsOptions: {
transformMixedEsModules: true,
},
},
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/apps/myapp',
provider: 'v8',
},
},
});`;
const viteConfigFileV17NxViteTsPathsOpts = `/// <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,
cacheDir: '../../node_modules/.vite/apps/myapp',
server: {
port: 4200,
host: 'localhost',
},
preview: {
port: 4300,
host: 'localhost',
},
plugins: [react(), nxViteTsPaths({ debug: true })],
// Uncomment this if you are using workers.
// worker: {
// plugins: [ nxViteTsPaths() ],
// },
build: {
outDir: '../../dist/apps/myapp',
reportCompressedSize: true,
commonjsOptions: {
transformMixedEsModules: true,
},
},
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/apps/myapp',
provider: 'v8',
},
},
});`;
const viteConfigFileV17NoNxViteTsPaths = `/// <reference types='vitest' />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
root: __dirname,
cacheDir: '../../node_modules/.vite/apps/myapp',
server: {
port: 4200,
host: 'localhost',
},
preview: {
port: 4300,
host: 'localhost',
},
plugins: [react()],
// Uncomment this if you are using workers.
// worker: {
// plugins: [ nxViteTsPaths() ],
// },
build: {
outDir: '../../dist/apps/myapp',
reportCompressedSize: true,
commonjsOptions: {
transformMixedEsModules: true,
},
},
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/apps/myapp',
provider: 'v8',
},
},
});`;
const viteConfigFileV17NoPlugins = `/// <reference types='vitest' />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
root: __dirname,
cacheDir: '../../node_modules/.vite/apps/myapp',
server: {
port: 4200,
host: 'localhost',
},
preview: {
port: 4300,
host: 'localhost',
},
// Uncomment this if you are using workers.
// worker: {
// plugins: [ nxViteTsPaths() ],
// },
build: {
outDir: '../../dist/apps/myapp',
reportCompressedSize: true,
commonjsOptions: {
transformMixedEsModules: true,
},
},
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/apps/myapp',
provider: 'v8',
},
},
});`;

View File

@ -0,0 +1,234 @@
import { type TargetConfiguration, type Tree } from '@nx/devkit';
import { tsquery } from '@phenomnomnominal/tsquery';
import { extname } from 'path/posix';
import {
addConfigValuesToViteConfig,
getViteConfigPath,
toProjectRelativePath,
} from './utils';
import { processTargetOutputs } from '@nx/devkit/src/generators/plugin-migrations/plugin-migration-utils';
export function buildPostTargetTransformer(
target: TargetConfiguration,
tree: Tree,
projectDetails: { projectName: string; root: string },
inferredTargetConfiguration: TargetConfiguration
) {
let viteConfigPath = getViteConfigPath(tree, projectDetails.root);
const configValues: Record<string, Record<string, unknown>> = {
default: {},
};
if (target.options) {
if (target.options.configFile) {
viteConfigPath = target.options.configFile;
}
removePropertiesFromTargetOptions(
tree,
target.options,
viteConfigPath,
projectDetails.root,
configValues['default'],
true
);
}
if (target.configurations) {
for (const configurationName in target.configurations) {
const configuration = target.configurations[configurationName];
configValues[configuration] = {};
removePropertiesFromTargetOptions(
tree,
configuration,
viteConfigPath,
projectDetails.root,
configValues[configuration]
);
if (Object.keys(configuration).length === 0) {
delete target.configurations[configurationName];
}
}
if (Object.keys(target.configurations).length === 0) {
if ('defaultConfiguration' in target) {
delete target.defaultConfiguration;
}
delete target.configurations;
}
if (
'defaultConfiguration' in target &&
!target.configurations[target.defaultConfiguration]
) {
delete target.defaultConfiguration;
}
}
if (target.outputs) {
processTargetOutputs(
target,
[{ newName: 'outDir', oldName: 'outputPath' }],
inferredTargetConfiguration,
{
projectName: projectDetails.projectName,
projectRoot: projectDetails.root,
}
);
}
if (
target.inputs &&
target.inputs.every((i) => i === 'production' || i === '^production')
) {
delete target.inputs;
}
addConfigValuesToViteConfig(tree, viteConfigPath, configValues);
return target;
}
function removePropertiesFromTargetOptions(
tree: Tree,
targetOptions: any,
viteConfigPath: string,
projectRoot: string,
configValues: Record<string, unknown>,
defaultOptions = false
) {
if ('configFile' in targetOptions) {
targetOptions.config = toProjectRelativePath(
targetOptions.configFile,
projectRoot
);
delete targetOptions.configFile;
}
if (targetOptions.outputPath) {
targetOptions.outDir = toProjectRelativePath(
targetOptions.outputPath,
projectRoot
);
delete targetOptions.outputPath;
}
if ('buildLibsFromSource' in targetOptions) {
configValues['buildLibsFromSource'] = targetOptions.buildLibsFromSource;
if (defaultOptions) {
moveBuildLibsFromSourceToViteConfig(tree, viteConfigPath);
}
delete targetOptions.buildLibsFromSource;
}
if ('skipTypeCheck' in targetOptions) {
delete targetOptions.skipTypeCheck;
}
if ('generatePackageJson' in targetOptions) {
delete targetOptions.generatePackageJson;
}
if ('includeDevDependenciesInPackageJson' in targetOptions) {
delete targetOptions.includeDevDependenciesInPackageJson;
}
if ('tsConfig' in targetOptions) {
delete targetOptions.tsConfig;
}
}
export function moveBuildLibsFromSourceToViteConfig(
tree: Tree,
configPath: string
) {
const PLUGINS_PROPERTY_SELECTOR =
'PropertyAssignment:has(Identifier[name=plugins])';
const PLUGINS_NX_VITE_TS_PATHS_SELECTOR =
'PropertyAssignment:has(Identifier[name=plugins]) CallExpression:has(Identifier[name=nxViteTsPaths])';
const BUILD_LIBS_FROM_SOURCE_SELECTOR =
'PropertyAssignment:has(Identifier[name=plugins]) CallExpression:has(Identifier[name=nxViteTsPaths]) ObjectLiteralExpression > PropertyAssignment:has(Identifier[name=buildLibsFromSource])';
const nxViteTsPathsImport =
extname(configPath) === 'js'
? 'const {nxViteTsPaths} = require("@nx/vite/plugins/nx-tsconfig-paths.plugin");'
: 'import { nxViteTsPaths } from "@nx/vite/plugins/nx-tsconfig-paths.plugin";';
const plugin = `nxViteTsPaths({ buildLibsFromSource: options.buildLibsFromSource }),`;
const viteConfigContents = tree.read(configPath, 'utf-8');
let newViteConfigContents = viteConfigContents;
const ast = tsquery.ast(viteConfigContents);
const buildLibsFromSourceNodes = tsquery(
ast,
BUILD_LIBS_FROM_SOURCE_SELECTOR,
{ visitAllChildren: true }
);
if (buildLibsFromSourceNodes.length > 0) {
return;
}
const nxViteTsPathsNodes = tsquery(ast, PLUGINS_NX_VITE_TS_PATHS_SELECTOR, {
visitAllChildren: true,
});
if (nxViteTsPathsNodes.length === 0) {
const pluginsNodes = tsquery(ast, PLUGINS_PROPERTY_SELECTOR, {
visitAllChildren: true,
});
if (pluginsNodes.length === 0) {
// Add plugin property
const configNodes = tsquery(
ast,
'CallExpression:has(Identifier[name=defineConfig]) > ObjectLiteralExpression',
{ visitAllChildren: true }
);
if (configNodes.length === 0) {
return;
}
newViteConfigContents = `${nxViteTsPathsImport}\n${viteConfigContents.slice(
0,
configNodes[0].getStart() + 1
)}plugins: [${plugin}],${viteConfigContents.slice(
configNodes[0].getStart() + 1
)}`;
} else {
// Add nxViteTsPaths plugin
const pluginsArrayNodes = tsquery(
pluginsNodes[0],
'ArrayLiteralExpression'
);
if (pluginsArrayNodes.length === 0) {
return;
}
newViteConfigContents = `${nxViteTsPathsImport}\n${viteConfigContents.slice(
0,
pluginsArrayNodes[0].getStart() + 1
)}${plugin}${viteConfigContents.slice(
pluginsArrayNodes[0].getStart() + 1
)}`;
}
} else {
const pluginOptionsNodes = tsquery(
nxViteTsPathsNodes[0],
'ObjectLiteralExpression'
);
if (pluginOptionsNodes.length === 0) {
// Add the options
newViteConfigContents = `${viteConfigContents.slice(
0,
nxViteTsPathsNodes[0].getStart()
)}${plugin}${viteConfigContents.slice(nxViteTsPathsNodes[0].getEnd())}`;
} else {
// update the object
newViteConfigContents = `${viteConfigContents.slice(
0,
pluginOptionsNodes[0].getStart() + 1
)}buildLibsFromSource: options.buildLibsFromSource, ${viteConfigContents.slice(
pluginOptionsNodes[0].getStart() + 1
)}`;
}
}
tree.write(configPath, newViteConfigContents);
}

View File

@ -0,0 +1,76 @@
import { type TargetConfiguration, type Tree } from '@nx/devkit';
import { getViteConfigPath } from './utils';
import { AggregatedLog } from '@nx/devkit/src/generators/plugin-migrations/aggregate-log-util';
export function previewPostTargetTransformer(migrationLogs: AggregatedLog) {
return (
target: TargetConfiguration,
tree: Tree,
projectDetails: { projectName: string; root: string },
inferredTargetConfiguration: TargetConfiguration
) => {
const viteConfigPath = getViteConfigPath(tree, projectDetails.root);
if (target.options) {
removePropertiesFromTargetOptions(
target.options,
projectDetails.projectName,
migrationLogs
);
}
if (target.configurations) {
for (const configurationName in target.configurations) {
const configuration = target.configurations[configurationName];
removePropertiesFromTargetOptions(
configuration,
projectDetails.projectName,
migrationLogs
);
if (Object.keys(configuration).length === 0) {
delete target.configurations[configurationName];
}
}
if (Object.keys(target.configurations).length === 0) {
if ('defaultConfiguration' in target) {
delete target.defaultConfiguration;
}
delete target.configurations;
}
if (
'defaultConfiguration' in target &&
!target.configurations[target.defaultConfiguration]
) {
delete target.defaultConfiguration;
}
}
return target;
};
}
function removePropertiesFromTargetOptions(
targetOptions: any,
projectName: string,
migrationLogs: AggregatedLog
) {
if ('buildTarget' in targetOptions) {
delete targetOptions.buildTarget;
}
if ('staticFilePath' in targetOptions) {
delete targetOptions.staticFilePath;
}
if ('proxyConfig' in targetOptions) {
migrationLogs.addLog({
executorName: '@nx/vite:preview-server',
project: projectName,
log: `Encountered 'proxyConfig' in project.json. You will need to copy the contents of this file to the 'server.proxy' property in your Vite config file.`,
});
delete targetOptions.proxyConfig;
}
}

View File

@ -0,0 +1,96 @@
import { type TargetConfiguration, type Tree } from '@nx/devkit';
import { getViteConfigPath } from './utils';
import { AggregatedLog } from '@nx/devkit/src/generators/plugin-migrations/aggregate-log-util';
export function servePostTargetTransformer(migrationLogs: AggregatedLog) {
return (
target: TargetConfiguration,
tree: Tree,
projectDetails: { projectName: string; root: string },
inferredTargetConfiguration: TargetConfiguration
) => {
const viteConfigPath = getViteConfigPath(tree, projectDetails.root);
if (target.options) {
removePropertiesFromTargetOptions(
tree,
target.options,
viteConfigPath,
projectDetails.root,
projectDetails.projectName,
migrationLogs,
true
);
}
if (target.configurations) {
for (const configurationName in target.configurations) {
const configuration = target.configurations[configurationName];
removePropertiesFromTargetOptions(
tree,
configuration,
viteConfigPath,
projectDetails.root,
projectDetails.projectName,
migrationLogs
);
if (Object.keys(configuration).length === 0) {
delete target.configurations[configurationName];
}
}
if (Object.keys(target.configurations).length === 0) {
if ('defaultConfiguration' in target) {
delete target.defaultConfiguration;
}
delete target.configurations;
}
if (
'defaultConfiguration' in target &&
!target.configurations[target.defaultConfiguration]
) {
delete target.defaultConfiguration;
}
}
return target;
};
}
function removePropertiesFromTargetOptions(
tree: Tree,
targetOptions: any,
viteConfigPath: string,
projectRoot: string,
projectName: string,
migrationLogs: AggregatedLog,
defaultOptions = false
) {
if ('buildTarget' in targetOptions) {
delete targetOptions.buildTarget;
}
if ('buildLibsFromSource' in targetOptions) {
migrationLogs.addLog({
executorName: '@nx/vite:dev-server',
project: projectName,
log: `Encountered 'buildLibsFromSource' in project.json. This property will be added to your Vite config file via the '@nx/vite:build' executor migration.`,
});
delete targetOptions.buildLibsFromSource;
}
if ('hmr' in targetOptions) {
delete targetOptions.hmr;
}
if ('proxyConfig' in targetOptions) {
migrationLogs.addLog({
executorName: '@nx/vite:dev-server',
project: projectName,
log: `Encountered 'proxyConfig' in project.json. You will need to copy the contents of this file to the 'server.proxy' property in your Vite config file.`,
});
delete targetOptions.proxyConfig;
}
}

View File

@ -0,0 +1,94 @@
import { type TargetConfiguration, type Tree } from '@nx/devkit';
import { toProjectRelativePath } from './utils';
import { processTargetOutputs } from '@nx/devkit/src/generators/plugin-migrations/plugin-migration-utils';
export function testPostTargetTransformer(
target: TargetConfiguration,
tree: Tree,
projectDetails: { projectName: string; root: string },
inferredTargetConfiguration: TargetConfiguration
) {
if (target.options) {
removePropertiesFromTargetOptions(target.options, projectDetails.root);
}
if (target.configurations) {
for (const configurationName in target.configurations) {
const configuration = target.configurations[configurationName];
removePropertiesFromTargetOptions(configuration, projectDetails.root);
if (Object.keys(configuration).length === 0) {
delete target.configurations[configurationName];
}
}
if (Object.keys(target.configurations).length === 0) {
if ('defaultConfiguration' in target) {
delete target.defaultConfiguration;
}
delete target.configurations;
}
if (
'defaultConfiguration' in target &&
!target.configurations[target.defaultConfiguration]
) {
delete target.defaultConfiguration;
}
}
if (target.outputs) {
processTargetOutputs(
target,
[{ newName: 'coverage.reportsDirectory', oldName: 'reportsDirectory' }],
inferredTargetConfiguration,
{
projectName: projectDetails.projectName,
projectRoot: projectDetails.root,
}
);
}
if (
target.inputs &&
target.inputs.every((i) => i === 'default' || i === '^production')
) {
delete target.inputs;
}
return target;
}
function removePropertiesFromTargetOptions(
targetOptions: any,
projectRoot: string
) {
if ('configFile' in targetOptions) {
targetOptions.config = toProjectRelativePath(
targetOptions.configFile,
projectRoot
);
delete targetOptions.configFile;
}
if ('reportsDirectory' in targetOptions) {
if (targetOptions.reportsDirectory.startsWith('../')) {
targetOptions.reportsDirectory = targetOptions.reportsDirectory.replace(
/(\.\.\/)+/,
''
);
}
targetOptions['coverage.reportsDirectory'] = toProjectRelativePath(
targetOptions.reportsDirectory,
projectRoot
);
delete targetOptions.reportsDirectory;
}
if ('testFiles' in targetOptions) {
targetOptions.testNamePattern = `"/(${targetOptions.testFiles
.map((f) => f.replace('.', '\\.'))
.join('|')})/"`;
delete targetOptions.testFiles;
}
}

View File

@ -0,0 +1,70 @@
import { relative, resolve } from 'path/posix';
import { workspaceRoot, type Tree, joinPathFragments } from '@nx/devkit';
import { tsquery } from '@phenomnomnominal/tsquery';
export function toProjectRelativePath(
path: string,
projectRoot: string
): string {
if (projectRoot === '.') {
// workspace and project root are the same, we normalize it to ensure it
// works with Jest since some paths only work when they start with `./`
return path.startsWith('.') ? path : `./${path}`;
}
const relativePath = relative(
resolve(workspaceRoot, projectRoot),
resolve(workspaceRoot, path)
);
return relativePath.startsWith('.') ? relativePath : `./${relativePath}`;
}
export function getViteConfigPath(tree: Tree, root: string) {
return [
joinPathFragments(root, `vite.config.ts`),
joinPathFragments(root, `vite.config.cts`),
joinPathFragments(root, `vite.config.mts`),
joinPathFragments(root, `vite.config.js`),
joinPathFragments(root, `vite.config.cjs`),
joinPathFragments(root, `vite.config.mjs`),
].find((f) => tree.exists(f));
}
export function addConfigValuesToViteConfig(
tree: Tree,
configFile: string,
configValues: Record<string, Record<string, unknown>>
) {
const IMPORT_PROPERTY_SELECTOR = 'ImportDeclaration';
const viteConfigContents = tree.read(configFile, 'utf-8');
const ast = tsquery.ast(viteConfigContents);
// AST TO GET SECTION TO APPEND TO
const importNodes = tsquery(ast, IMPORT_PROPERTY_SELECTOR, {
visitAllChildren: true,
});
if (importNodes.length === 0) {
return;
}
const lastImportNode = importNodes[importNodes.length - 1];
const configValuesString = `
// These options were migrated by @nx/vite:convert-to-inferred from the project.json file.
const configValues = ${JSON.stringify(configValues)};
// Determine the correct configValue to use based on the configuration
const nxConfiguration = process.env.NX_TASK_TARGET_CONFIGURATION ?? 'default';
const options = {
...configValues.default,
...(configValues[nxConfiguration] ?? {})
}`;
tree.write(
configFile,
`${viteConfigContents.slice(0, lastImportNode.getEnd())}
${configValuesString}
${viteConfigContents.slice(lastImportNode.getEnd())}`
);
}

View File

@ -0,0 +1,19 @@
{
"$schema": "https://json-schema.org/schema",
"$id": "NxViteConvertToInferred",
"description": "Convert existing Vite project(s) using `@nx/vite:*` executors to use `@nx/vite/plugin`. Defaults to migrating all projects. Pass '--project' to migrate only one target.",
"title": "Convert Vite project from executor to plugin",
"type": "object",
"properties": {
"project": {
"type": "string",
"description": "The project to convert from using the `@nx/vite:*` executors to use `@nx/vite/plugin`.",
"x-priority": "important"
},
"skipFormat": {
"type": "boolean",
"description": "Whether to format files at the end of the migration.",
"default": false
}
}
}

View File

@ -14,8 +14,7 @@ describe('@nx/vite/plugin', () => {
let context: CreateNodesContext;
describe('root project', () => {
let tempFs;
let tempFs: TempFs;
beforeEach(async () => {
tempFs = new TempFs('vite-plugin-tests');
context = {
@ -62,8 +61,7 @@ describe('@nx/vite/plugin', () => {
});
describe('not root project', () => {
let tempFs;
let tempFs: TempFs;
beforeEach(() => {
tempFs = new TempFs('test');
context = {