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 { VitePreviewServerExecutorOptions } from '../executors/preview-server/schema';
import { VitestExecutorOptions } from '../executors/test/schema';
import { Schema } from '../generators/configuration/schema';
import { ensureBuildOptionsInViteConfig } from './vite-config-edit-utils';
export type Target = 'build' | 'serve' | 'test' | 'preview';
export type TargetFlags = Partial>;
export type UserProvidedTargetName = Partial>;
export type ValidFoundTargetName = Partial>;
export function findExistingTargetsInProject(
targets: {
[targetName: string]: TargetConfiguration;
},
userProvidedTargets?: UserProvidedTargetName
): {
validFoundTargetName: ValidFoundTargetName;
projectContainsUnsupportedExecutor: boolean;
userProvidedTargetIsUnsupported: TargetFlags;
alreadyHasNxViteTargets: TargetFlags;
} {
const output: ReturnType = {
validFoundTargetName: {},
projectContainsUnsupportedExecutor: false,
userProvidedTargetIsUnsupported: {},
alreadyHasNxViteTargets: {},
};
const supportedExecutors = {
build: [
'@nxext/vite:build',
'@nrwl/js:babel',
'@nrwl/js:swc',
'@nrwl/webpack:webpack',
'@nrwl/rollup:rollup',
'@nrwl/web:rollup',
],
serve: ['@nxext/vite:dev', '@nrwl/webpack:dev-server'],
test: ['@nrwl/jest:jest', '@nxext/vitest:vitest'],
};
const unsupportedExecutors = [
'@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',
];
// 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
function checkUserProvidedTarget(target: Target) {
if (userProvidedTargets?.[target]) {
if (
supportedExecutors[target].includes(
targets[userProvidedTargets[target]]?.executor
)
) {
output.validFoundTargetName[target] = userProvidedTargets[target];
} else {
output.userProvidedTargetIsUnsupported[target] = true;
}
}
}
checkUserProvidedTarget('build');
checkUserProvidedTarget('serve');
checkUserProvidedTarget('test');
// Returns early when we have a build, serve, and test targets.
if (
output.validFoundTargetName.build &&
output.validFoundTargetName.serve &&
output.validFoundTargetName.test
) {
return output;
}
// 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) {
const executorName = targets[target].executor;
const hasViteTargets = output.alreadyHasNxViteTargets;
hasViteTargets.build ||= executorName === '@nrwl/vite:build';
hasViteTargets.serve ||= executorName === '@nrwl/vite:dev-server';
hasViteTargets.test ||= executorName === '@nrwl/vite:test';
hasViteTargets.preview ||= executorName === '@nrwl/vite:preview-server';
const foundTargets = output.validFoundTargetName;
if (
!foundTargets.build &&
supportedExecutors.build.includes(executorName)
) {
foundTargets.build = target;
}
if (
!foundTargets.serve &&
supportedExecutors.serve.includes(executorName)
) {
foundTargets.serve = target;
}
if (!foundTargets.test && supportedExecutors.test.includes(executorName)) {
foundTargets.test = target;
}
output.projectContainsUnsupportedExecutor ||=
unsupportedExecutors.includes(executorName);
}
return output;
}
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
),
};
project.targets ??= {};
if (project.targets[target]) {
project.targets[target].executor = '@nrwl/vite:test';
delete project.targets[target].options?.jestConfig;
} else {
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
),
};
project.targets ??= {};
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 {
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);
project.targets ??= {};
if (project.targets[target]) {
const serveTarget = project.targets[target];
const serveOptions: ViteDevServerExecutorOptions = {
buildTarget: `${options.project}:build`,
https: project.targets[target].options?.https,
hmr: project.targets[target].options?.hmr,
open: project.targets[target].options?.open,
};
if (serveTarget.executor === '@nxext/vite:dev') {
serveOptions.proxyConfig = project.targets[target].options.proxyConfig;
}
serveTarget.executor = '@nrwl/vite:dev-server';
serveTarget.options = serveOptions;
} else {
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);
}
/**
* Adds a target for the preview server.
*
* @param tree
* @param options
* @param serveTarget An existing serve target.
* @param previewTarget The preview target to create.
*/
export function addPreviewTarget(
tree: Tree,
options: Schema,
serveTarget: string
) {
const project = readProjectConfiguration(tree, options.project);
const previewOptions: VitePreviewServerExecutorOptions = {
buildTarget: `${options.project}:build`,
};
project.targets ??= {};
// Update the options from the passed serve target.
if (project.targets[serveTarget]) {
const target = project.targets[serveTarget];
if (target.executor === '@nxext/vite:dev') {
previewOptions.proxyConfig = target.options.proxyConfig;
}
previewOptions.https = target.options?.https;
previewOptions.open = target.options?.open;
}
// Adds a preview target.
project.targets.preview = {
executor: '@nrwl/vite:preview-server',
defaultConfiguration: 'development',
options: previewOptions,
configurations: {
development: {
buildTarget: `${options.project}:build:development`,
},
production: {
buildTarget: `${options.project}:build:production`,
},
},
};
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`);
const commonCompilerOptions = {
target: 'ESNext',
useDefineForClassFields: true,
module: 'ESNext',
strict: true,
moduleResolution: 'Node',
resolveJsonModule: true,
isolatedModules: true,
types: ['vite/client'],
noEmit: true,
};
switch (options.uiFramework) {
case 'react':
config.compilerOptions = {
...commonCompilerOptions,
lib: ['DOM', 'DOM.Iterable', 'ESNext'],
allowJs: false,
esModuleInterop: false,
skipLibCheck: true,
allowSyntheticDefaultImports: true,
forceConsistentCasingInFileNames: true,
jsx: 'react-jsx',
};
config.include = [...config.include, 'src'];
break;
case 'none':
config.compilerOptions = {
...commonCompilerOptions,
lib: ['ESNext', 'DOM'],
skipLibCheck: true,
esModuleInterop: true,
strict: true,
noUnusedLocals: true,
noUnusedParameters: true,
noImplicitReturns: true,
};
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(
``
)
) {
tree.write(
`${projectConfig.root}/index.html`,
indexHtmlContent.replace(
'
',
`
`
)
);
if (tree.exists(`${projectConfig.root}/src/index.html`)) {
tree.delete(`${projectConfig.root}/src/index.html`);
}
}
} else {
tree.write(
`${projectConfig.root}/index.html`,
`