diff --git a/e2e/vite/src/vite.test.ts b/e2e/vite/src/vite.test.ts index b946d883e499e..925a9104ea845 100644 --- a/e2e/vite/src/vite.test.ts +++ b/e2e/vite/src/vite.test.ts @@ -11,6 +11,7 @@ import { promisifiedTreeKill, readFile, readJson, + removeFile, rmDist, runCLI, runCLIAsync, @@ -267,9 +268,8 @@ describe('Vite Plugin', () => { const buildableJsLibFn = names(`${lib}-js`).propertyName; updateFile(`apps/${app}/src/app/app.tsx`, () => { - return `// eslint-disable-next-line @typescript-eslint/no-unused-vars + return ` import styles from './app.module.css'; - import NxWelcome from './nx-welcome'; import { ${buildableLibCmp} } from '@acme/buildable'; import { ${buildableJsLibFn} } from '@acme/js-lib'; @@ -277,12 +277,12 @@ import { ${nonBuildableLibCmp} } from '@acme/non-buildable'; export function App() { return ( -
- <${buildableLibCmp} /> - <${nonBuildableLibCmp} /> -

{${buildableJsLibFn}()}

- -
+
+ <${buildableLibCmp} /> + <${nonBuildableLibCmp} /> +

{${buildableJsLibFn}()}

+ +
); } export default App; @@ -307,6 +307,24 @@ export default App; // this should be less modules than building from source expect(results).toContain('38 modules transformed'); }); + + it('should build app from libs without package.json in lib', () => { + removeFile(`libs/${lib}-buildable/package.json`); + + const buildFromSourceResults = runCLI( + `build ${app} --buildLibsFromSource=true` + ); + expect(buildFromSourceResults).toContain( + 'Successfully ran target build for project' + ); + + const noBuildFromSourceResults = runCLI( + `build ${app} --buildLibsFromSource=false` + ); + expect(noBuildFromSourceResults).toContain( + 'Successfully ran target build for project' + ); + }); }); describe('should be able to create libs that use vitest', () => { diff --git a/packages/vite/plugins/nx-tsconfig-paths.plugin.ts b/packages/vite/plugins/nx-tsconfig-paths.plugin.ts index afe9f478c0773..987b2995b0ba6 100644 --- a/packages/vite/plugins/nx-tsconfig-paths.plugin.ts +++ b/packages/vite/plugins/nx-tsconfig-paths.plugin.ts @@ -1,11 +1,49 @@ import { stripIndents, workspaceRoot } from '@nx/devkit'; import { existsSync } from 'node:fs'; import { relative, join, resolve } from 'node:path'; -import { loadConfig, createMatchPath, MatchPath } from 'tsconfig-paths'; +import { + loadConfig, + createMatchPath, + MatchPath, + ConfigLoaderSuccessResult, +} from 'tsconfig-paths'; -export function nxViteTsPaths() { +export interface nxViteTsPathsOptions { + /** + * Enable debug logging + * @default false + **/ + 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[]; +} + +export function nxViteTsPaths(options: nxViteTsPathsOptions = {}) { let matchTsPathEsm: MatchPath; let matchTsPathFallback: MatchPath | undefined; + let tsConfigPathsEsm: ConfigLoaderSuccessResult; + let tsConfigPathsFallback: ConfigLoaderSuccessResult; + + options.extensions ??= [ + '.ts', + '.tsx', + '.js', + '.jsx', + '.json', + '.mjs', + '.cjs', + ]; + options.mainFields ??= [['exports', '.', 'import'], 'module', 'main']; return { name: 'nx-vite-ts-paths', @@ -31,12 +69,13 @@ There should at least be a tsconfig.base.json or tsconfig.json in the root of th if (parsed.resultType === 'failed') { throw new Error(`Failed loading tsonfig at ${foundTsConfigPath}`); } + tsConfigPathsEsm = parsed; - matchTsPathEsm = createMatchPath(parsed.absoluteBaseUrl, parsed.paths, [ - ['exports', '.', 'import'], - 'module', - 'main', - ]); + matchTsPathEsm = createMatchPath( + parsed.absoluteBaseUrl, + parsed.paths, + options.mainFields + ); const rootLevelTsConfig = getTsConfig( join(workspaceRoot, 'tsconfig.base.json') @@ -44,6 +83,7 @@ There should at least be a tsconfig.base.json or tsconfig.json in the root of th const rootLevelParsed = loadConfig(rootLevelTsConfig); logIt('fallback parsed tsconfig: ', rootLevelParsed); if (rootLevelParsed.resultType === 'success') { + tsConfigPathsFallback = rootLevelParsed; matchTsPathFallback = createMatchPath( rootLevelParsed.absoluteBaseUrl, rootLevelParsed.paths, @@ -51,39 +91,82 @@ There should at least be a tsconfig.base.json or tsconfig.json in the root of th ); } }, - resolveId(source: string) { + resolveId(importPath: string) { let resolvedFile: string; try { - resolvedFile = matchTsPathEsm(source); + resolvedFile = matchTsPathEsm(importPath); } catch (e) { logIt('Using fallback path matching.'); - resolvedFile = matchTsPathFallback?.(source); + resolvedFile = matchTsPathFallback?.(importPath); } if (!resolvedFile) { - logIt(`Unable to resolve ${source} with tsconfig paths`); + 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`); + } } - return resolvedFile; + 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; }, }; -} -function getTsConfig(preferredTsConfigPath: string): string { - return [ - resolve(preferredTsConfigPath), - resolve(join(workspaceRoot, 'tsconfig.base.json')), - resolve(join(workspaceRoot, 'tsconfig.json')), - ].find((tsPath) => { - if (existsSync(tsPath)) { - logIt('Found tsconfig at', tsPath); - return tsPath; + function getTsConfig(preferredTsConfigPath: string): string { + return [ + resolve(preferredTsConfigPath), + resolve(join(workspaceRoot, 'tsconfig.base.json')), + resolve(join(workspaceRoot, 'tsconfig.json')), + ].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) { + console.debug('\n[Nx Vite TsPaths]', ...msg); } - }); -} + } -function logIt(...msg: any[]) { - if (process.env.NX_VERBOSE_LOGGING === 'true') { - console.debug('[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.startsWith(normalizedImport)) { + const path = (tsconfig.absoluteBaseUrl, paths[0].replace(/\/\*$/, '')); + resolvedFile = findFile(importPath.replace(normalizedImport, path)); + } + } + + return resolvedFile; + } + + function findFile(path: string): string { + for (const ext of options.extensions) { + const r = resolve(path + ext); + if (existsSync(r)) { + return r; + } + } } }