Context
As a modern tool for building web applications, Vite has been utilized by many developers for creating web applications (react, vue). Due to its ease of use and high performance, many web frameworks have even written official plugins (solid, astro), making it a successful challenger to Webpack in recent years. However, Vite has evolved to be more than just a tool for the web layer. Its surrounding ecosystem is flourishing, leading to the development of a series of peripheral tools.
Motivation
Why is Vite suitable for developing Node.js applications?
Firstly, even without Vite, tools like vitest for unit testing, tsx/ts-node for running source code for debugging, and tsup/esbuild for bundling code into the final js to be executed are likely needed. Thus, if Vite is used, these tasks can be accomplished within the same ecosystem.
- vitest: A unit testing tool that supports ESM and TypeScript.
- vite-node: A tool for running TypeScript code that supports various Vite features, such as
?raw
. - Vite: Bundles Node.js applications into the final JavaScript to be executed, with an option to bundle dependencies selectively.
Usage
Vitest and vite-node are ready to use out of the box, so the focus here is on Vite's building aspects.
First, install the dependencies
pnpm i -D vite vite-node vitest
vitest
Create a unit test file, for example, src/__tests__/index.test.ts
import { it } from 'vitest'
it('hello world', () => {
expect(1 + 1).eq(2)
})
Run vitest with the following command
pnpm vitest src/__tests__/index.test.ts
vite-node
It can replace the node command to run any file, offering more capabilities than the standard node command, including:
- Support for ts/tsx files and running esm/cjs modules.
- CJS polyfills in ESM, allowing direct use of
__dirname
, etc. - Support for watch mode execution.
- Support for Vite's own features, such as
?raw
. - Support for using Vite plugins.
For example, create a file src/main.ts
import { readFile } from 'fs/promises'
console.log(await readFile(__filename, 'utf-8'))
Then run it with vite-node
pnpm vite-node src/main.ts
Vite
To build Node.js applications with Vite, it's necessary to modify some configurations and plugins to address a few key issues:
- Implement CJS polyfills for ESM code, including
__dirname
,__filename
,require
, andself
. - Correctly bundle
devDependencies
while excludingnode
anddependencies
. - Provide an out-of-the-box default configuration.
Let's tackle these issues one by one.
Polyfilling CJS Features During Build
In Node.js, global variables like __dirname
are commonly used. Unfortunately, ESM does not support these. The recommended approach in Node.js is as follows:
import path from 'path'
import { fileURLToPath } from 'url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
You can use a Vite plugin to prepend some extra code to the beginning of the built code, automatically creating these variables.
Here's how to implement it:
- Install
magic-string
to modify the code while keeping the source map intact.
pnpm i -D magic-string
- Then, add the polyfill code in the
renderChunk
hook.
import MagicString from 'magic-string'
import { Plugin } from 'vite'
function shims(): Plugin {
return {
name: 'node-shims',
renderChunk(code, chunk) {
if (!chunk.fileName.endsWith('.js')) {
return null;
}
const s = new MagicString(code);
s.prepend(`
import __path from 'path';
import { fileURLToPath as __fileURLToPath } from 'url';
import { createRequire as __createRequire } from 'module';
const __getFilename = () => __fileURLToPath(import.meta.url);
const __getDirname = () => __path.dirname(__getFilename());
const __dirname = __getDirname();
const __filename = __getFilename();
const self = globalThis;
const require = __createRequire(import.meta.url);
`);
return {
code: s.toString(),
map: s.generateMap({ hires: true }),
};
},
apply: 'build',
};
}
By integrating this plugin into your Vite configuration, you'll automatically inject these shims into your JavaScript files during the build process, emulating Node.js's CommonJS environment within an ESM context. This solution streamlines the adaptation of existing Node.js codebases to the ESM module system.
Correctly Bundling Dependencies
It's common in Node.js applications to include modules such as fs
, which Vite will also try to bundle by default. Therefore, it's necessary to treat them as external dependencies. Fortunately, there is already a plugin called rollup-plugin-node-externals that can exclude Node.js and dependencies declared in the dependencies
field of package.json
. However, some minor compatibility adjustments are needed for it to work smoothly with Vite.
- Install the dependency:
pnpm i -D rollup-plugin-node-externals
- Wrap it to make it compatible with Vite:
import { nodeExternals } from 'rollup-plugin-node-externals'
import { Plugin } from 'vite'
function externals(): Plugin {
return {
...nodeExternals({
// Options here if needed
}),
name: 'node-externals',
enforce: 'pre', // The key is to run it before Vite's default dependency resolution plugin
apply: 'build',
}
}
Adding Default Configuration
Given the frequency of Node.js projects, it's preferable not to repeat the configuration setup for each project. Instead, a convention-over-configuration approach is adopted, supplemented with the option for customization. Thus, a simple implementation of a shared Vite plugin for common configurations can be created as follows:
import path from 'path'
import { Plugin } from 'vite'
function config(options?: { entry?: string }): Plugin {
const entry = options?.entry ?? 'src/main.ts'
return {
name: 'node-config',
config() {
return {
build: {
lib: {
entry: path.resolve(entry),
formats: ['es'],
fileName: (format) => `${path.basename(entry, path.extname(entry))}.${format}.js`,
},
rollupOptions: {
external: ['dependencies-to-exclude']
// Additional Rollup options here
},
},
resolve: {
// Change default resolution to node rather than browser
mainFields: ['module', 'jsnext:main', 'jsnext'],
conditions: ['node'],
},
}
},
apply: 'build',
}
}
This setup enables a default configuration tailored for Node.js projects, simplifying the development process. By specifying an entry file and the necessary adjustments for module resolution and build output, this plugin facilitates a straightforward build setup for a wide range of Node.js applications.
Combining Plugins
Finally, we combine these plugins into one, creating a new plugin setup:
import { Plugin } from 'vite'
export function node(): Plugin[] {
return [shims(), externals(), config()]
}
Then, use it in your vite.config.ts
file:
import { defineConfig } from 'vite'
import { node } from './path/to/your/plugin'
export default defineConfig({
plugins: [node()],
})
Now, you can build Node.js applications with Vite using the following command:
pnpm vite build
Enjoy everything Vite has to offer!
A Vite plugin @liuli-util/vite-plugin-node has been published, which is ready to use out of the box.
Limitations
Alright, there are still some issues here, including:
-
Vite does not officially support building node applications, and its main goal is not that-- At least it can be said that both vitest/nuxt depend on Vite at the node level. -
vite-plugin-node still has many issues, such as not automatically polyfilling-- Implemented.__dirname
and the like -
Vite's performance is still an order of magnitude worse compared to esbuild-- The performance issues are not significant for most libraries now, and human perception can hardly notice it. -
Building with zx fails to run due to incorrect configuration for chalk-- specifically, failure to recognize thenode
field inimports
. The maintainer's lack of interest suggests considering a switch to ansi-colors, as discussed in this issue. -
Issues after building with koa-bodyparser-- due to lack of proper ESM support. Awaiting the merge of this pull request.
No choice is perfect, but the decision to go all-in with Vite is deliberate.
Future Goals
- [x] Support for multiple entry points.
- [x] Generation of type definitions.
Choosing to focus on Vite involves weighing its current limitations against its potential and making a strategic decision based on the overall benefits it offers, including its growing ecosystem and the ability to streamline development workflows for Node.js applications.
Top comments (0)