## Current Behavior The Nx Vite TsConfig paths plugin incorrectly resolves to workspace root tsconfig files instead of project-specific ones, causing path aliases like `@/contexts` to fail resolution. ## Expected Behavior The plugin should check for project-specific tsconfig files (`tsconfig.app.json`, `tsconfig.lib.json`, `tsconfig.json`) before falling back to workspace root configurations. ## Related Issue(s) Fixes #28945 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com> Co-authored-by: Coly010 <Coly010@users.noreply.github.com>
282 lines
8.9 KiB
TypeScript
282 lines
8.9 KiB
TypeScript
import {
|
|
createProjectGraphAsync,
|
|
joinPathFragments,
|
|
stripIndents,
|
|
workspaceRoot,
|
|
} from '@nx/devkit';
|
|
import { copyFileSync, existsSync } from 'node:fs';
|
|
import { join, relative, resolve } from 'node:path';
|
|
import {
|
|
loadConfig,
|
|
createMatchPath,
|
|
MatchPath,
|
|
ConfigLoaderSuccessResult,
|
|
} from 'tsconfig-paths';
|
|
import {
|
|
calculateProjectBuildableDependencies,
|
|
createTmpTsConfig,
|
|
} from '@nx/js/src/utils/buildable-libs-utils';
|
|
import { Plugin } from 'vite';
|
|
import { nxViteBuildCoordinationPlugin } from './nx-vite-build-coordination.plugin';
|
|
import { findFile } from '../src/utils/nx-tsconfig-paths-find-file';
|
|
import { isUsingTsSolutionSetup } from '@nx/js/src/utils/typescript/ts-solution-setup';
|
|
import { getProjectTsConfigPath } from '../src/utils/options-utils';
|
|
|
|
export interface nxViteTsPathsOptions {
|
|
/**
|
|
* Enable debug logging
|
|
* If set to false, it will always ignore the debug logging even when `--verbose` or `NX_VERBOSE_LOGGING` is set to true.
|
|
* @default undefined
|
|
**/
|
|
debug?: boolean;
|
|
/**
|
|
* export fields in package.json to use for resolving
|
|
* @default [['exports', '.', 'import'], 'module', 'main']
|
|
*
|
|
* fallback resolution will use ['main', 'module']
|
|
**/
|
|
mainFields?: (string | string[])[];
|
|
/**
|
|
* extensions to check when resolving files when package.json resolution fails
|
|
* @default ['.ts', '.tsx', '.js', '.jsx', '.json', '.mjs', '.cjs']
|
|
**/
|
|
extensions?: string[];
|
|
/**
|
|
* Inform Nx whether to use the raw source or to use the built output for buildable dependencies.
|
|
* Set to `false` to use incremental builds.
|
|
* @default true
|
|
*/
|
|
buildLibsFromSource?: boolean;
|
|
}
|
|
|
|
export function nxViteTsPaths(options: nxViteTsPathsOptions = {}) {
|
|
let foundTsConfigPath: string;
|
|
let matchTsPathEsm: MatchPath;
|
|
let matchTsPathFallback: MatchPath | undefined;
|
|
let tsConfigPathsEsm: ConfigLoaderSuccessResult;
|
|
let tsConfigPathsFallback: ConfigLoaderSuccessResult;
|
|
|
|
options.extensions ??= [
|
|
'.ts',
|
|
'.tsx',
|
|
'.js',
|
|
'.jsx',
|
|
'.json',
|
|
'.mts',
|
|
'.mjs',
|
|
'.cts',
|
|
'.cjs',
|
|
'.css',
|
|
'.scss',
|
|
'.less',
|
|
];
|
|
options.mainFields ??= [['exports', '.', 'import'], 'module', 'main'];
|
|
options.buildLibsFromSource ??= true;
|
|
let projectRoot = '';
|
|
|
|
return {
|
|
name: 'nx-vite-ts-paths',
|
|
// Ensure the resolveId aspect of the plugin is called before vite's internal resolver
|
|
// Otherwise, issues can arise with Yarn Workspaces and Pnpm Workspaces
|
|
enforce: 'pre',
|
|
async configResolved(config: any) {
|
|
projectRoot = config.root;
|
|
const projectRootFromWorkspaceRoot = relative(workspaceRoot, projectRoot);
|
|
foundTsConfigPath = getTsConfig(
|
|
process.env.NX_TSCONFIG_PATH ??
|
|
join(
|
|
workspaceRoot,
|
|
'tmp',
|
|
projectRootFromWorkspaceRoot,
|
|
process.env.NX_TASK_TARGET_TARGET ?? 'build',
|
|
'tsconfig.generated.json'
|
|
)
|
|
);
|
|
|
|
if (!foundTsConfigPath) return;
|
|
|
|
if (
|
|
!options.buildLibsFromSource &&
|
|
!global.NX_GRAPH_CREATION &&
|
|
config.mode !== 'test'
|
|
) {
|
|
const projectGraph = await createProjectGraphAsync({
|
|
exitOnError: false,
|
|
resetDaemonClient: true,
|
|
});
|
|
const { dependencies } = calculateProjectBuildableDependencies(
|
|
undefined,
|
|
projectGraph,
|
|
workspaceRoot,
|
|
process.env.NX_TASK_TARGET_PROJECT,
|
|
// When using incremental building and the serve target is called
|
|
// we need to get the deps for the 'build' target instead.
|
|
process.env.NX_TASK_TARGET_TARGET === 'serve'
|
|
? 'build'
|
|
: process.env.NX_TASK_TARGET_TARGET,
|
|
process.env.NX_TASK_TARGET_CONFIGURATION
|
|
);
|
|
// This tsconfig is used via the Vite ts paths plugin.
|
|
// It can be also used by other user-defined Vite plugins (e.g. for creating type declaration files).
|
|
foundTsConfigPath = createTmpTsConfig(
|
|
foundTsConfigPath,
|
|
workspaceRoot,
|
|
relative(workspaceRoot, projectRoot),
|
|
dependencies,
|
|
true
|
|
);
|
|
|
|
if (config.command === 'serve') {
|
|
const buildableLibraryDependencies = dependencies
|
|
.filter((dep) => dep.node.type === 'lib')
|
|
.map((dep) => dep.node.name)
|
|
.join(',');
|
|
const buildCommand = `npx nx run-many --target=${process.env.NX_TASK_TARGET_TARGET} --projects=${buildableLibraryDependencies}`;
|
|
config.plugins.push(nxViteBuildCoordinationPlugin({ buildCommand }));
|
|
}
|
|
}
|
|
|
|
const parsed = loadConfig(foundTsConfigPath);
|
|
|
|
logIt('first parsed tsconfig: ', parsed);
|
|
if (parsed.resultType === 'failed') {
|
|
throw new Error(`Failed loading tsconfig at ${foundTsConfigPath}`);
|
|
}
|
|
tsConfigPathsEsm = parsed;
|
|
|
|
matchTsPathEsm = createMatchPath(
|
|
parsed.absoluteBaseUrl,
|
|
parsed.paths,
|
|
options.mainFields
|
|
);
|
|
|
|
const rootLevelTsConfig = getTsConfig(
|
|
join(workspaceRoot, 'tsconfig.base.json')
|
|
);
|
|
const rootLevelParsed = loadConfig(rootLevelTsConfig);
|
|
logIt('fallback parsed tsconfig: ', rootLevelParsed);
|
|
if (rootLevelParsed.resultType === 'success') {
|
|
tsConfigPathsFallback = rootLevelParsed;
|
|
matchTsPathFallback = createMatchPath(
|
|
rootLevelParsed.absoluteBaseUrl,
|
|
rootLevelParsed.paths,
|
|
['main', 'module']
|
|
);
|
|
}
|
|
},
|
|
resolveId(importPath: string) {
|
|
// Let other resolvers handle this path.
|
|
if (!foundTsConfigPath) return null;
|
|
|
|
let resolvedFile: string;
|
|
try {
|
|
resolvedFile = matchTsPathEsm(importPath);
|
|
} catch (e) {
|
|
logIt('Using fallback path matching.');
|
|
resolvedFile = matchTsPathFallback?.(importPath);
|
|
}
|
|
|
|
if (!resolvedFile || !existsSync(resolvedFile)) {
|
|
if (tsConfigPathsEsm || tsConfigPathsFallback) {
|
|
logIt(
|
|
`Unable to resolve ${importPath} with tsconfig paths. Using fallback file matching.`
|
|
);
|
|
resolvedFile =
|
|
loadFileFromPaths(tsConfigPathsEsm, importPath) ||
|
|
loadFileFromPaths(tsConfigPathsFallback, importPath);
|
|
} else {
|
|
logIt(`Unable to resolve ${importPath} with tsconfig paths`);
|
|
}
|
|
}
|
|
|
|
logIt(`Resolved ${importPath} to ${resolvedFile}`);
|
|
// Returning null defers to other resolveId functions and eventually the default resolution behavior
|
|
// https://rollupjs.org/plugin-development/#resolveid
|
|
return resolvedFile || null;
|
|
},
|
|
async writeBundle(options) {
|
|
if (isUsingTsSolutionSetup()) return;
|
|
const outDir = options.dir || 'dist';
|
|
const src = resolve(projectRoot, 'package.json');
|
|
if (existsSync(src)) {
|
|
const dest = join(outDir, 'package.json');
|
|
|
|
try {
|
|
copyFileSync(src, dest);
|
|
} catch (err) {
|
|
console.error('Error copying package.json:', err);
|
|
}
|
|
}
|
|
},
|
|
} as Plugin;
|
|
|
|
function getTsConfig(preferredTsConfigPath: string): string {
|
|
const projectTsConfigPath = getProjectTsConfigPath(projectRoot);
|
|
return [
|
|
resolve(preferredTsConfigPath),
|
|
projectTsConfigPath ? resolve(projectTsConfigPath) : null,
|
|
resolve(join(workspaceRoot, 'tsconfig.base.json')),
|
|
resolve(join(workspaceRoot, 'tsconfig.json')),
|
|
resolve(join(workspaceRoot, 'jsconfig.json')),
|
|
]
|
|
.filter(Boolean)
|
|
.find((tsPath) => {
|
|
if (existsSync(tsPath)) {
|
|
logIt('Found tsconfig at', tsPath);
|
|
return tsPath;
|
|
}
|
|
});
|
|
}
|
|
|
|
function logIt(...msg: any[]) {
|
|
if (process.env.NX_VERBOSE_LOGGING === 'true' && options?.debug !== false) {
|
|
console.debug('\n[Nx Vite TsPaths]', ...msg);
|
|
}
|
|
}
|
|
|
|
function loadFileFromPaths(
|
|
tsconfig: ConfigLoaderSuccessResult,
|
|
importPath: string
|
|
) {
|
|
logIt(
|
|
`Trying to resolve file from config in ${tsconfig.configFileAbsolutePath}`
|
|
);
|
|
let resolvedFile: string;
|
|
for (const alias in tsconfig.paths) {
|
|
const paths = tsconfig.paths[alias];
|
|
|
|
const normalizedImport = alias.replace(/\/\*$/, '');
|
|
|
|
if (
|
|
importPath === normalizedImport ||
|
|
importPath.startsWith(normalizedImport + '/')
|
|
) {
|
|
const joinedPath = joinPathFragments(
|
|
tsconfig.absoluteBaseUrl,
|
|
paths[0].replace(/\/\*$/, '')
|
|
);
|
|
|
|
resolvedFile = findFile(
|
|
importPath.replace(normalizedImport, joinedPath),
|
|
options.extensions
|
|
);
|
|
|
|
if (
|
|
resolvedFile === undefined &&
|
|
options.extensions.some((ext) => importPath.endsWith(ext))
|
|
) {
|
|
const foundExtension = options.extensions.find((ext) =>
|
|
importPath.endsWith(ext)
|
|
);
|
|
const pathWithoutExtension = importPath
|
|
.replace(normalizedImport, joinedPath)
|
|
.slice(0, -foundExtension.length);
|
|
resolvedFile = findFile(pathWithoutExtension, options.extensions);
|
|
}
|
|
}
|
|
}
|
|
|
|
return resolvedFile;
|
|
}
|
|
}
|