From 8c46e4691ffea0b0c4594972e04e6e0913fdf79d Mon Sep 17 00:00:00 2001 From: Colum Ferry Date: Wed, 22 May 2024 11:17:26 +0100 Subject: [PATCH] feat(vite): add support for incremental builds on serve --- e2e/vite/src/vite-crystal.test.ts | 73 +++++++++++++++++++ .../vite/plugins/nx-tsconfig-paths.plugin.ts | 10 +++ .../nx-vite-build-coordination.plugin.ts | 71 ++++++++++++++++++ 3 files changed, 154 insertions(+) create mode 100644 packages/vite/plugins/nx-vite-build-coordination.plugin.ts diff --git a/e2e/vite/src/vite-crystal.test.ts b/e2e/vite/src/vite-crystal.test.ts index 0f34bf43778fcd..8cc48ccbad0b08 100644 --- a/e2e/vite/src/vite-crystal.test.ts +++ b/e2e/vite/src/vite-crystal.test.ts @@ -6,8 +6,10 @@ import { runCLI, runCommandUntil, uniq, + updateFile, } from '@nx/e2e/utils'; import { ChildProcess } from 'child_process'; +import { names } from '@nx/devkit'; const myApp = uniq('my-app'); const myVueApp = uniq('my-vue-app'); @@ -63,6 +65,77 @@ describe('@nx/vite/plugin', () => { }, 200_000); }); + describe('should support buildable libraries', () => { + it('should build the library and application successfully', () => { + const myApp = uniq('myapp'); + runCLI( + `generate @nx/react:app ${myApp} --bundler=vite --unitTestRunner=vitest` + ); + + const myBuildableLib = uniq('mybuildablelib'); + runCLI( + `generate @nx/react:library ${myBuildableLib} --bundler=vite --unitTestRunner=vitest --buildable` + ); + + const exportedLibraryComponent = names(myBuildableLib).className; + + updateFile( + `${myApp}/src/app/App.tsx`, + `import NxWelcome from './nx-welcome'; + import { ${exportedLibraryComponent} } from '@proj/${myBuildableLib}'; + export function App() { + return ( +
+ <${exportedLibraryComponent} /> + +
+ ); + } + export default App;` + ); + + updateFile( + `${myApp}/vite.config.ts`, + `/// + import { defineConfig } from 'vite'; + import react from '@vitejs/plugin-react'; + import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; + + export default defineConfig({ + root: __dirname, + cacheDir: '../../node_modules/.vite/${myApp}', + + server: { + port: 4200, + host: 'localhost', + }, + + preview: { + port: 4300, + host: 'localhost', + }, + + plugins: [react(), nxViteTsPaths({buildLibsFromSource: false})], + + build: { + outDir: '../../dist/${myApp}', + emptyOutDir: true, + reportCompressedSize: true, + commonjsOptions: { + transformMixedEsModules: true, + }, + }, + });` + ); + + const result = runCLI(`build ${myApp}`); + expect(result).toContain('1/1 dependent project tasks succeeded'); + expect(result).toContain( + `Successfully ran target build for project ${myApp}` + ); + }); + }); + it('should run serve-static', async () => { let process: ChildProcess; const port = 8081; diff --git a/packages/vite/plugins/nx-tsconfig-paths.plugin.ts b/packages/vite/plugins/nx-tsconfig-paths.plugin.ts index 246cad44de603d..040de1dc7a273e 100644 --- a/packages/vite/plugins/nx-tsconfig-paths.plugin.ts +++ b/packages/vite/plugins/nx-tsconfig-paths.plugin.ts @@ -17,6 +17,7 @@ import { createTmpTsConfig, } from '@nx/js/src/utils/buildable-libs-utils'; import { Plugin } from 'vite'; +import { nxViteBuildCoordinationPlugin } from './nx-vite-build-coordination.plugin'; export interface nxViteTsPathsOptions { /** @@ -107,6 +108,15 @@ There should at least be a tsconfig.base.json or tsconfig.json in the root of th relative(workspaceRoot, projectRoot), dependencies ); + + 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); diff --git a/packages/vite/plugins/nx-vite-build-coordination.plugin.ts b/packages/vite/plugins/nx-vite-build-coordination.plugin.ts new file mode 100644 index 00000000000000..2f124118ab52b3 --- /dev/null +++ b/packages/vite/plugins/nx-vite-build-coordination.plugin.ts @@ -0,0 +1,71 @@ +import { type Plugin } from 'vite'; +import { BatchFunctionRunner } from 'nx/src/command-line/watch/watch'; +import { exec, type ChildProcess } from 'child_process'; +import { + daemonClient, + type UnregisterCallback, +} from 'nx/src/daemon/client/client'; +import { output } from 'nx/src/utils/output'; + +export interface NxViteBuildCoordinationPluginOptions { + buildCommand: string; +} +export function nxViteBuildCoordinationPlugin( + options: NxViteBuildCoordinationPluginOptions +): Plugin { + let activeBuildProcess: ChildProcess | undefined; + let unregisterFileWatcher: UnregisterCallback | undefined; + + async function buildChangedProjects() { + await new Promise((res) => { + activeBuildProcess = exec(options.buildCommand); + activeBuildProcess.stdout.pipe(process.stdout); + activeBuildProcess.stderr.pipe(process.stderr); + activeBuildProcess.on('exit', () => { + res(); + }); + activeBuildProcess.on('error', () => { + res(); + }); + }); + activeBuildProcess = undefined; + } + + function createFileWatcher() { + const runner = new BatchFunctionRunner(() => buildChangedProjects()); + return daemonClient.registerFileWatcher( + { watchProjects: 'all' }, + (err, { changedProjects, changedFiles }) => { + if (err === 'closed') { + output.error({ + title: 'Watch connection closed', + bodyLines: [ + 'The daemon had closed the connection to this watch process.', + 'Please restart your watch command.', + ], + }); + process.exit(1); + } + + if (activeBuildProcess) { + activeBuildProcess.kill(2); + activeBuildProcess = undefined; + } + + runner.enqueue(changedProjects, changedFiles); + } + ); + } + + return { + name: 'nx-vite-build-coordination-plugin', + async buildStart() { + if (!unregisterFileWatcher) { + await buildChangedProjects(); + unregisterFileWatcher = await createFileWatcher(); + process.on('exit', () => unregisterFileWatcher()); + process.on('SIGINT', () => process.exit()); + } + }, + }; +}