nx/packages/vite/src/utils/generator-utils.ts
2023-01-04 11:42:47 +00:00

808 lines
22 KiB
TypeScript

import {
joinPathFragments,
logger,
offsetFromRoot,
readJson,
readProjectConfiguration,
TargetConfiguration,
Tree,
updateProjectConfiguration,
writeJson,
} from '@nrwl/devkit';
import { ViteBuildExecutorOptions } from '../executors/build/schema';
import { ViteDevServerExecutorOptions } from '../executors/dev-server/schema';
import { VitestExecutorOptions } from '../executors/test/schema';
import { Schema } from '../generators/configuration/schema';
import { ensureBuildOptionsInViteConfig } from './vite-config-edit-utils';
export interface TargetFlags {
build?: boolean;
serve?: boolean;
test?: boolean;
}
export interface UserProvidedTargetName {
build?: string;
serve?: string;
test?: string;
}
export interface ValidFoundTargetName {
build?: string;
serve?: string;
test?: string;
}
export function findExistingTargetsInProject(
targets: {
[targetName: string]: TargetConfiguration;
},
userProvidedTargets?: UserProvidedTargetName
): {
validFoundTargetName: ValidFoundTargetName;
projectContainsUnsupportedExecutor?: boolean;
userProvidedTargetIsUnsupported?: TargetFlags;
alreadyHasNxViteTargets?: TargetFlags;
} {
let validFoundBuildTarget: string | undefined,
validFoundServeTarget: string | undefined,
validFoundTestTarget: string | undefined,
projectContainsUnsupportedExecutor: boolean | undefined,
unsupportedUserProvidedTargetBuild: boolean | undefined,
unsupportedUserProvidedTargetServe: boolean | undefined,
unsupportedUserProvidedTargetTest: boolean | undefined,
alreadyHasNxViteTargetBuild: boolean | undefined,
alreadyHasNxViteTargetServe: boolean | undefined,
alreadyHasNxViteTargetTest: boolean | undefined;
const arrayOfSupportedBuilders = [
'@nxext/vite:build',
'@nrwl/js:babel',
'@nrwl/js:swc',
'@nrwl/webpack:webpack',
'@nrwl/rollup:rollup',
'@nrwl/web:rollup',
];
const arrayOfSupportedServers = [
'@nxext/vite:dev',
'@nrwl/webpack:dev-server',
];
const arrayOfSupportedTesters = ['@nrwl/jest:jest', '@nxext/vitest:vitest'];
const arrayofUnsupportedExecutors = [
'@nrwl/angular:ng-packagr-lite',
'@nrwl/angular:package',
'@nrwl/angular:webpack-browser',
'@angular-devkit/build-angular:browser',
'@angular-devkit/build-angular:dev-server',
'@nrwl/esbuild:esbuild',
'@nrwl/react-native:run-ios',
'@nrwl/react-native:start',
'@nrwl/react-native:run-android',
'@nrwl/react-native:bundle',
'@nrwl/react-native:build-android',
'@nrwl/react-native:bundle',
'@nrwl/next:build',
'@nrwl/next:server',
'@nrwl/js:tsc',
];
const arrayOfNxViteExecutors = [
'@nrwl/vite:build',
'@nrwl/vite:dev-server',
'@nrwl/vite:test',
];
// First, we check if the user has provided a target
// If they have, we check if the executor the target is using is supported
// If it's not supported, then we set the unsupported flag to true for that target
if (userProvidedTargets?.build) {
if (
arrayOfSupportedBuilders.includes(
targets[userProvidedTargets.build]?.executor
)
) {
validFoundBuildTarget = userProvidedTargets.build;
} else {
unsupportedUserProvidedTargetBuild = true;
}
}
if (userProvidedTargets?.serve) {
if (
arrayOfSupportedServers.includes(
targets[userProvidedTargets.serve]?.executor
)
) {
validFoundServeTarget = userProvidedTargets.serve;
} else {
unsupportedUserProvidedTargetServe = true;
}
}
if (userProvidedTargets?.test) {
if (
arrayOfSupportedServers.includes(
targets[userProvidedTargets.test]?.executor
)
) {
validFoundTestTarget = userProvidedTargets.test;
} else {
unsupportedUserProvidedTargetTest = true;
}
}
// Then, we try to find the targets that are using the supported executors
// for build, serve and test, since these are the ones we will be converting
for (const target in targets) {
// If we have a value for each one of the targets, we can break out of the loop
if (
validFoundBuildTarget &&
validFoundServeTarget &&
validFoundTestTarget
) {
break;
}
if (targets[target].executor === '@nrwl/vite:build') {
alreadyHasNxViteTargetBuild = true;
}
if (targets[target].executor === '@nrwl/vite:dev-server') {
alreadyHasNxViteTargetServe = true;
}
if (targets[target].executor === '@nrwl/vite:test') {
alreadyHasNxViteTargetTest = true;
}
if (
!validFoundBuildTarget &&
arrayOfSupportedBuilders.includes(targets[target].executor)
) {
validFoundBuildTarget = target;
}
if (
!validFoundServeTarget &&
arrayOfSupportedServers.includes(targets[target].executor)
) {
validFoundServeTarget = target;
}
if (
!validFoundTestTarget &&
arrayOfSupportedTesters.includes(targets[target].executor)
) {
validFoundTestTarget = target;
}
if (
!arrayOfNxViteExecutors.includes(targets[target].executor) &&
arrayofUnsupportedExecutors.includes(targets[target].executor)
) {
projectContainsUnsupportedExecutor = true;
}
}
return {
validFoundTargetName: {
build: validFoundBuildTarget,
serve: validFoundServeTarget,
test: validFoundTestTarget,
},
projectContainsUnsupportedExecutor,
userProvidedTargetIsUnsupported: {
build: unsupportedUserProvidedTargetBuild,
serve: unsupportedUserProvidedTargetServe,
test: unsupportedUserProvidedTargetTest,
},
alreadyHasNxViteTargets: {
build: alreadyHasNxViteTargetBuild,
serve: alreadyHasNxViteTargetServe,
test: alreadyHasNxViteTargetTest,
},
};
}
export function addOrChangeTestTarget(
tree: Tree,
options: Schema,
target: string
) {
const project = readProjectConfiguration(tree, options.project);
const coveragePath = joinPathFragments(
'coverage',
project.root === '.' ? options.project : project.root
);
const testOptions: VitestExecutorOptions = {
passWithNoTests: true,
// vitest runs in the project root so we have to offset to the workspaceRoot
reportsDirectory: joinPathFragments(
offsetFromRoot(project.root),
coveragePath
),
};
if (project.targets?.[target]) {
project.targets[target].executor = '@nrwl/vite:test';
delete project.targets[target].options?.jestConfig;
} else {
if (!project.targets) {
project.targets = {};
}
project.targets[target] = {
executor: '@nrwl/vite:test',
outputs: [coveragePath],
options: testOptions,
};
}
updateProjectConfiguration(tree, options.project, project);
}
export function addOrChangeBuildTarget(
tree: Tree,
options: Schema,
target: string
) {
const project = readProjectConfiguration(tree, options.project);
const buildOptions: ViteBuildExecutorOptions = {
outputPath: joinPathFragments(
'dist',
project.root != '.' ? project.root : options.project
),
};
if (project.targets?.[target]) {
buildOptions.fileReplacements =
project.targets[target].options?.fileReplacements;
if (project.targets[target].executor === '@nxext/vite:build') {
buildOptions.base = project.targets[target].options?.baseHref;
buildOptions.sourcemap = project.targets[target].options?.sourcemaps;
}
project.targets[target].options = {
...buildOptions,
};
project.targets[target].executor = '@nrwl/vite:build';
} else {
if (!project.targets) {
project.targets = {};
}
project.targets[`${target}`] = {
executor: '@nrwl/vite:build',
outputs: ['{options.outputPath}'],
defaultConfiguration: 'production',
options: buildOptions,
configurations: {
development: {
mode: 'development',
},
production: {
mode: 'production',
},
},
};
}
updateProjectConfiguration(tree, options.project, project);
}
export function addOrChangeServeTarget(
tree: Tree,
options: Schema,
target: string
) {
const project = readProjectConfiguration(tree, options.project);
const serveOptions: ViteDevServerExecutorOptions = {
buildTarget: `${options.project}:build`,
};
if (project.targets?.[target]) {
if (target === '@nxext/vite:dev') {
serveOptions.proxyConfig = project.targets[target].options.proxyConfig;
}
project.targets[target].options = {
...serveOptions,
https: project.targets[target].options?.https,
hmr: project.targets[target].options?.hmr,
open: project.targets[target].options?.open,
};
project.targets[target].executor = '@nrwl/vite:dev-server';
} else {
if (!project.targets) {
project.targets = {};
}
project.targets[`${target}`] = {
executor: '@nrwl/vite:dev-server',
defaultConfiguration: 'development',
options: {
buildTarget: `${options.project}:build`,
},
configurations: {
development: {
buildTarget: `${options.project}:build:development`,
hmr: true,
},
production: {
buildTarget: `${options.project}:build:production`,
hmr: false,
},
},
};
}
updateProjectConfiguration(tree, options.project, project);
}
export function editTsConfig(tree: Tree, options: Schema) {
const projectConfig = readProjectConfiguration(tree, options.project);
const config = readJson(tree, `${projectConfig.root}/tsconfig.json`);
switch (options.uiFramework) {
case 'react':
config.compilerOptions = {
target: 'ESNext',
useDefineForClassFields: true,
module: 'ESNext',
lib: ['DOM', 'DOM.Iterable', 'ESNext'],
allowJs: false,
skipLibCheck: true,
esModuleInterop: false,
allowSyntheticDefaultImports: true,
strict: true,
forceConsistentCasingInFileNames: true,
moduleResolution: 'Node',
resolveJsonModule: true,
isolatedModules: true,
noEmit: true,
jsx: 'react-jsx',
types: options.includeVitest
? ['vite/client', 'vitest']
: ['vite/client'],
};
config.include = [...config.include, 'src'];
break;
case 'none':
config.compilerOptions = {
target: 'ESNext',
useDefineForClassFields: true,
module: 'ESNext',
lib: ['ESNext', 'DOM'],
skipLibCheck: true,
esModuleInterop: true,
strict: true,
moduleResolution: 'Node',
resolveJsonModule: true,
isolatedModules: true,
noEmit: true,
noUnusedLocals: true,
noUnusedParameters: true,
noImplicitReturns: true,
types: options.includeVitest
? ['vite/client', 'vitest']
: ['vite/client'],
};
config.include = [...config.include, 'src'];
break;
default:
break;
}
writeJson(tree, `${projectConfig.root}/tsconfig.json`, config);
}
export function moveAndEditIndexHtml(
tree: Tree,
options: Schema,
buildTarget: string
) {
const projectConfig = readProjectConfiguration(tree, options.project);
let indexHtmlPath =
projectConfig.targets[buildTarget].options?.index ??
`${projectConfig.root}/src/index.html`;
const mainPath = (
projectConfig.targets[buildTarget].options?.main ??
`${projectConfig.root}/src/main.ts${
options.uiFramework === 'react' ? 'x' : ''
}`
).replace(projectConfig.root, '');
if (
!tree.exists(indexHtmlPath) &&
tree.exists(`${projectConfig.root}/index.html`)
) {
indexHtmlPath = `${projectConfig.root}/index.html`;
}
if (tree.exists(indexHtmlPath)) {
const indexHtmlContent = tree.read(indexHtmlPath, 'utf8');
if (
!indexHtmlContent.includes(
`<script type="module" src="${mainPath}"></script>`
)
) {
tree.write(
`${projectConfig.root}/index.html`,
indexHtmlContent.replace(
'</body>',
`<script type="module" src="${mainPath}"></script>
</body>`
)
);
if (tree.exists(`${projectConfig.root}/src/index.html`))
tree.delete(`${projectConfig.root}/src/index.html`);
}
} else {
tree.write(
`${projectConfig.root}/index.html`,
`<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="${mainPath}"></script>
</body>
</html>`
);
}
}
export function createOrEditViteConfig(
tree: Tree,
options: Schema,
onlyVitest?: boolean,
projectAlreadyHasViteTargets?: TargetFlags
) {
const projectConfig = readProjectConfiguration(tree, options.project);
const viteConfigPath = `${projectConfig.root}/vite.config.ts`;
const buildOption = onlyVitest
? ''
: options.includeLib
? `
// Configuration for building your library.
// See: https://vitejs.dev/guide/build.html#library-mode
build: {
lib: {
// Could also be a dictionary or array of multiple entry points.
entry: 'src/index.ts',
name: '${options.project}',
fileName: 'index',
// Change this to the formats you want to support.
// Don't forgot to update your package.json as well.
formats: ['es', 'cjs']
},
rollupOptions: {
// External packages that should not be bundled into your library.
external: [${
options.uiFramework === 'react'
? "'react', 'react-dom', 'react/jsx-runtime'"
: ''
}]
}
},`
: ``;
const dtsPlugin = onlyVitest
? ''
: options.includeLib
? `dts({
tsConfigFilePath: join(__dirname, 'tsconfig.lib.json'),
// Faster builds by skipping tests. Set this to false to enable type checking.
skipDiagnostics: true,
}),`
: '';
const dtsImportLine = onlyVitest
? ''
: options.includeLib
? `import dts from 'vite-plugin-dts';\nimport { join } from 'path';`
: '';
let viteConfigContent = '';
const testOption = options.includeVitest
? `test: {
globals: true,
cache: {
dir: '${offsetFromRoot(projectConfig.root)}node_modules/.vitest'
},
environment: 'jsdom',
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
${
options.inSourceTests
? `includeSource: ['src/**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}']`
: ''
}
},`
: '';
const defineOption = options.inSourceTests
? `define: {
'import.meta.vitest': undefined
},`
: '';
const reactPluginImportLine =
options.uiFramework === 'react'
? `import react from '@vitejs/plugin-react';`
: '';
const reactPlugin = options.uiFramework === 'react' ? `react(),` : '';
const serverOption = onlyVitest
? ''
: options.includeLib
? ''
: `
server:{
port: 4200,
host: 'localhost',
},`;
const pluginOption = `
plugins: [
${dtsPlugin}
${reactPlugin}
viteTsConfigPaths({
root: '${offsetFromRoot(projectConfig.root)}',
}),
],
`;
const workerOption = `
// Uncomment this if you are using workers.
// worker: {
// viteTsConfigPaths({
// root: '${offsetFromRoot(projectConfig.root)}',
// }),
// },`;
if (tree.exists(viteConfigPath)) {
handleViteConfigFileExists(
tree,
viteConfigPath,
options,
buildOption,
dtsPlugin,
dtsImportLine,
pluginOption,
testOption,
offsetFromRoot(projectConfig.root),
projectAlreadyHasViteTargets
);
return;
}
viteConfigContent = `
import { defineConfig } from 'vite';
${reactPluginImportLine}
import viteTsConfigPaths from 'vite-tsconfig-paths';
${dtsImportLine}
export default defineConfig({
${serverOption}
${pluginOption}
${workerOption}
${buildOption}
${defineOption}
${testOption}
});`;
tree.write(viteConfigPath, viteConfigContent);
}
export function normalizeViteConfigFilePathWithTree(
tree: Tree,
projectRoot: string,
configFile?: string
): string {
return configFile && tree.exists(configFile)
? configFile
: tree.exists(joinPathFragments(`${projectRoot}/vite.config.ts`))
? joinPathFragments(`${projectRoot}/vite.config.ts`)
: tree.exists(joinPathFragments(`${projectRoot}/vite.config.js`))
? joinPathFragments(`${projectRoot}/vite.config.js`)
: undefined;
}
export function getViteConfigPathForProject(
tree: Tree,
projectName: string,
target?: string
) {
let viteConfigPath: string | undefined;
const { targets, root } = readProjectConfiguration(tree, projectName);
if (target) {
viteConfigPath = targets[target]?.options?.configFile;
} else {
const buildTarget = Object.entries(targets).find(
([_targetName, targetConfig]) => {
return targetConfig.executor === '@nrwl/vite:build';
}
);
viteConfigPath = buildTarget?.[1]?.options?.configFile;
}
return normalizeViteConfigFilePathWithTree(tree, root, viteConfigPath);
}
export async function handleUnsupportedUserProvidedTargets(
userProvidedTargetIsUnsupported: TargetFlags,
userProvidedTargetName: UserProvidedTargetName,
validFoundTargetName: ValidFoundTargetName
) {
if (userProvidedTargetIsUnsupported.build && validFoundTargetName.build) {
await handleUnsupportedUserProvidedTargetsErrors(
userProvidedTargetName.build,
validFoundTargetName.build,
'build',
'build'
);
}
if (userProvidedTargetIsUnsupported.serve && validFoundTargetName.serve) {
await handleUnsupportedUserProvidedTargetsErrors(
userProvidedTargetName.serve,
validFoundTargetName.serve,
'serve',
'dev-server'
);
}
if (userProvidedTargetIsUnsupported.test && validFoundTargetName.test) {
await handleUnsupportedUserProvidedTargetsErrors(
userProvidedTargetName.test,
validFoundTargetName.test,
'test',
'test'
);
}
}
async function handleUnsupportedUserProvidedTargetsErrors(
userProvidedTargetName: string,
validFoundTargetName: string,
action: 'build' | 'serve' | 'test',
executor: 'build' | 'dev-server' | 'test'
) {
logger.warn(
`The custom ${action} target you provided (${userProvidedTargetName}) cannot be converted to use the @nrwl/vite:${executor} executor.
However, we found the following ${action} target in your project that can be converted: ${validFoundTargetName}
Please note that converting a potentially non-compatible project to use Vite.js may result in unexpected behavior. Always commit
your changes before converting a project to use Vite.js, and test the converted project thoroughly before deploying it.
`
);
const { Confirm } = require('enquirer');
const prompt = new Confirm({
name: 'question',
message: `Should we convert the ${validFoundTargetName} target to use the @nrwl/vite:${executor} executor?`,
initial: true,
});
const shouldConvert = await prompt.run();
if (!shouldConvert) {
throw new Error(
`The ${action} target ${userProvidedTargetName} cannot be converted to use the @nrwl/vite:${executor} executor.
Please try again, either by providing a different ${action} target or by not providing a target at all (Nx will
convert the first one it finds, most probably this one: ${validFoundTargetName})
Please note that converting a potentially non-compatible project to use Vite.js may result in unexpected behavior. Always commit
your changes before converting a project to use Vite.js, and test the converted project thoroughly before deploying it.
`
);
}
}
export async function handleUnknownExecutors(projectName: string) {
logger.warn(
`
We could not find any targets in project ${projectName} that use executors which
can be converted to the @nrwl/vite executors.
This either means that your project may not have a target
for building, serving, or testing at all, or that your targets are
using executors that are not known to Nx.
If you still want to convert your project to use the @nrwl/vite executors,
please make sure to commit your changes before running this generator.
`
);
const { Confirm } = require('enquirer');
const prompt = new Confirm({
name: 'question',
message: `Should Nx convert your project to use the @nrwl/vite executors?`,
initial: true,
});
const shouldConvert = await prompt.run();
if (!shouldConvert) {
throw new Error(`
Nx could not verify that the executors you are using can be converted to the @nrwl/vite executors.
Please try again with a different project.
`);
}
}
function handleViteConfigFileExists(
tree: Tree,
viteConfigPath: string,
options: Schema,
buildOption: string,
dtsPlugin: string,
dtsImportLine: string,
pluginOption: string,
testOption: string,
offsetFromRoot: string,
projectAlreadyHasViteTargets?: TargetFlags
) {
if (projectAlreadyHasViteTargets.build && projectAlreadyHasViteTargets.test) {
return;
}
logger.info(`vite.config.ts already exists for project ${options.project}.`);
const buildOptionObject = {
lib: {
entry: 'src/index.ts',
name: options.project,
fileName: 'index',
formats: ['es', 'cjs'],
},
rollupOptions: {
external: [
options.uiFramework === 'react'
? "'react', 'react-dom', 'react/jsx-runtime'"
: '',
],
},
};
const testOptionObject = {
globals: true,
cache: {
dir: `${offsetFromRoot}node_modules/.vitest`,
},
environment: 'jsdom',
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
};
const changed = ensureBuildOptionsInViteConfig(
tree,
viteConfigPath,
buildOption,
buildOptionObject,
dtsPlugin,
dtsImportLine,
pluginOption,
testOption,
testOptionObject,
projectAlreadyHasViteTargets
);
if (!changed) {
logger.warn(
`Make sure the following setting exists in your Vite configuration file (${viteConfigPath}):
${buildOption}
`
);
} else {
logger.info(`
Vite configuration file (${viteConfigPath}) has been updated with the required settings for the new target(s).
`);
}
}