diff --git a/e2e/vite/src/vite.test.ts b/e2e/vite/src/vite.test.ts index d1087fd689919f..40bbdb626e1321 100644 --- a/e2e/vite/src/vite.test.ts +++ b/e2e/vite/src/vite.test.ts @@ -1,3 +1,4 @@ +import { names } from '@nx/devkit'; import { cleanupProject, createFile, @@ -240,6 +241,74 @@ describe('Vite Plugin', () => { 100_000; }); + describe.only('incremental building', () => { + const app = uniq('demo'); + const lib = uniq('my-lib'); + beforeAll(() => { + proj = newProject({ name: uniq('vite-incr-build') }); + runCLI(`generate @nx/react:app ${app} --bundler=vite --no-interactive`); + + // only this project will be directly used from dist + runCLI( + `generate @nx/react:lib ${lib}-buildable --unitTestRunner=none --bundler=vite --importPath="@acme/buildable" --no-interactive` + ); + + runCLI( + `generate @nx/react:lib ${lib} --unitTestRunner=none --bundler=none --importPath="@acme/non-buildable" --no-interactive` + ); + + // because the default js lib builds as cjs it cannot be loaded from dist + // so the paths plugin should always resolve to the libs source + runCLI( + `generate @nx/js:lib ${lib}-js --bundler=tsc --importPath="@acme/js-lib" --no-interactive` + ); + const buildableLibCmp = names(`${lib}-buildable`).className; + const nonBuildableLibCmp = names(lib).className; + const buildableJsLibFn = names(`${lib}-js`).propertyName; + + updateFile(`apps/${app}/src/app/app.tsx`, () => { + return `// eslint-disable-next-line @typescript-eslint/no-unused-vars +import styles from './app.module.css'; + +import NxWelcome from './nx-welcome'; +import { ${buildableLibCmp} } from '@acme/buildable'; +import { ${buildableJsLibFn} } from '@acme/js-lib'; +import { ${nonBuildableLibCmp} } from '@acme/non-buildable'; + +export function App() { + return ( +
+ <${buildableLibCmp} /> + <${nonBuildableLibCmp} /> +

{${buildableJsLibFn}()}

+ +
+ ); +} +export default App; +`; + }); + }); + + afterAll(() => { + cleanupProject(); + }); + + it('should build app from libs source', () => { + const results = runCLI(`build ${app} --buildLibsFromSource=true`); + expect(results).toContain('Successfully ran target build for project'); + // this should be more modules than build from dist + expect(results).toContain('40 modules transformed'); + }); + + it('should build app from libs dist', () => { + const results = runCLI(`build ${app} --buildLibsFromSource=false`); + expect(results).toContain('Successfully ran target build for project'); + // this should be less modules than building from source + expect(results).toContain('38 modules transformed'); + }); + }); + describe('should be able to create libs that use vitest', () => { const lib = uniq('my-lib'); beforeEach(() => { @@ -255,7 +324,6 @@ describe('Vite Plugin', () => { `Successfully ran target test for project ${lib}` ); - // TODO(caleb): run tests from project root and make sure they still work const nestedResults = await runCLIAsync(`test ${lib} --skip-nx-cache`, { cwd: `${tmpProjPath()}/libs/${lib}`, }); diff --git a/packages/vite/package.json b/packages/vite/package.json index 4a4eb6a9324dfd..59a2e6e432f2ec 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -34,7 +34,8 @@ "dotenv": "~10.0.0", "enquirer": "~2.3.6", "@nx/devkit": "file:../devkit", - "@nx/js": "file:../js" + "@nx/js": "file:../js", + "tsconfig-paths": "^4.1.2" }, "peerDependencies": { "vite": "^4.3.4", @@ -53,6 +54,7 @@ "./executors": "./executors.js", "./src/executors/*/schema.json": "./src/executors/*/schema.json", "./src/executors/*.impl": "./src/executors/*.impl.js", - "./src/executors/*/compat": "./src/executors/*/compat.js" + "./src/executors/*/compat": "./src/executors/*/compat.js", + "./plugins/nx-tsconfig-paths.plugin": "./plugins/nx-tsconfig-paths.plugin.js" } } diff --git a/packages/vite/plugins/nx-tsconfig-paths.plugin.ts b/packages/vite/plugins/nx-tsconfig-paths.plugin.ts new file mode 100644 index 00000000000000..3bbc0c91929bd0 --- /dev/null +++ b/packages/vite/plugins/nx-tsconfig-paths.plugin.ts @@ -0,0 +1,91 @@ +import { stripIndents, workspaceRoot } from '@nx/devkit'; +import { existsSync } from 'node:fs'; +import { relative, join, resolve, posix } from 'node:path'; +import { loadConfig, createMatchPath, MatchPath } from 'tsconfig-paths'; + +// TODO(caleb): should we provide a way to override anything for the plugin? + +export function nxViteTsPaths() { + let matchTsPathEsm: MatchPath; + let matchTsPathFallback: MatchPath | undefined; + + return { + name: 'nx-vite-ts-paths', + configResolved(config: any) { + const projectRoot = config.root; + // TODO(caleb): verify on windows to see what type of paths vite returns posix vs win32 + const projectRootFromWorkspaceRoot = relative(workspaceRoot, projectRoot); + + const foundTsConfigPath = getTsConfig( + join( + workspaceRoot, + 'tmp', + projectRootFromWorkspaceRoot, + 'tsconfig.generated.json' + ) + ); + if (!foundTsConfigPath) { + throw new Error(stripIndents`Unable to find a tsconfig in the workspace! +There should at least be a tsconfig.base.json or tsconfig.json in the root of the workspace ${workspaceRoot}`); + } + const parsed = loadConfig(foundTsConfigPath); + + logIt('first parsed tsconfig: ', parsed); + if (parsed.resultType === 'failed') { + throw new Error(`Failed loading tsonfig at ${foundTsConfigPath}`); + } + + matchTsPathEsm = createMatchPath(parsed.absoluteBaseUrl, parsed.paths, [ + ['exports', '.', 'import'], + 'main', + ]); + + const rootLevelTsConfig = getTsConfig( + join(workspaceRoot, 'tsconfig.base.json') + ); + const rootLevelParsed = loadConfig(rootLevelTsConfig); + logIt('fallback parsed tsconfig: ', rootLevelParsed); + if (rootLevelParsed.resultType === 'success') { + matchTsPathFallback = createMatchPath( + rootLevelParsed.absoluteBaseUrl, + rootLevelParsed.paths, + ['main', 'module'] + ); + } + }, + resolveId(source: string) { + let resolvedFile: string; + try { + resolvedFile = matchTsPathEsm(source); + } catch (e) { + logIt('Using fallback path matching.'); + resolvedFile = matchTsPathFallback?.(source); + } + + if (!resolvedFile) { + logIt(`Unable to resolve ${source} with tsconfig paths`); + } + + return resolvedFile; + }, + }; +} + +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') { + console.debug('[Nx Vite TsPaths]', ...msg); + } +} diff --git a/packages/vite/src/executors/build/build.impl.ts b/packages/vite/src/executors/build/build.impl.ts index 015177879c8dda..8f3313c46c91a0 100644 --- a/packages/vite/src/executors/build/build.impl.ts +++ b/packages/vite/src/executors/build/build.impl.ts @@ -25,6 +25,7 @@ export async function* viteBuildExecutor( const projectRoot = context.projectsConfigurations.projects[context.projectName].root; + // TODO(caleb): do we need to register the paths anymore or just make tsconfig? registerPaths(projectRoot, options, context); const normalizedOptions = normalizeOptions(options); diff --git a/packages/vite/src/executors/dev-server/dev-server.impl.ts b/packages/vite/src/executors/dev-server/dev-server.impl.ts index f43342fa396627..7507400e57a240 100644 --- a/packages/vite/src/executors/dev-server/dev-server.impl.ts +++ b/packages/vite/src/executors/dev-server/dev-server.impl.ts @@ -20,6 +20,7 @@ export async function* viteDevServerExecutor( const projectRoot = context.projectsConfigurations.projects[context.projectName].root; + // TODO(caleb): do we need to register the paths anymore or just make tsconfig? registerPaths(projectRoot, options, context); // Retrieve the option for the configured buildTarget. diff --git a/packages/vite/src/utils/executor-utils.ts b/packages/vite/src/utils/executor-utils.ts index 470f1ff73d8038..11308e1e2f0d45 100644 --- a/packages/vite/src/utils/executor-utils.ts +++ b/packages/vite/src/utils/executor-utils.ts @@ -35,7 +35,9 @@ export function registerPaths( const tsConfig = resolve(projectRoot, 'tsconfig.json'); options.buildLibsFromSource ??= true; - if (!options.buildLibsFromSource) { + if (options.buildLibsFromSource) { + registerTsConfigPaths(tsConfig); + } else { const { dependencies } = calculateProjectDependencies( context.projectGraph, context.root, @@ -49,9 +51,6 @@ export function registerPaths( projectRoot, dependencies ); - registerTsConfigPaths(tmpTsConfig); - } else { - registerTsConfigPaths(tsConfig); } } diff --git a/packages/vite/src/utils/generator-utils.ts b/packages/vite/src/utils/generator-utils.ts index 7c5ee1deec5703..4430ec762888d8 100644 --- a/packages/vite/src/utils/generator-utils.ts +++ b/packages/vite/src/utils/generator-utils.ts @@ -583,13 +583,14 @@ export function createOrEditViteConfig( host: 'localhost', },`; + // viteTsConfigPaths({ + // root: '${offsetFromRoot(projectConfig.root)}', + // }), const pluginOption = ` plugins: [ ${dtsPlugin} ${reactPlugin} - viteTsConfigPaths({ - root: '${offsetFromRoot(projectConfig.root)}', - }), + nxViteTsPaths(), ], `; @@ -628,7 +629,7 @@ export function createOrEditViteConfig( /// import { defineConfig } from 'vite'; ${reactPluginImportLine} - import viteTsConfigPaths from 'vite-tsconfig-paths'; + import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; ${dtsImportLine} export default defineConfig({