Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Node.js Core Module Support #7821

Open
4 tasks done
ayuhito opened this issue Apr 20, 2022 · 24 comments
Open
4 tasks done

Node.js Core Module Support #7821

ayuhito opened this issue Apr 20, 2022 · 24 comments
Labels
p2-to-be-discussed Enhancement under consideration (priority)

Comments

@ayuhito
Copy link

ayuhito commented Apr 20, 2022

Clear and concise description of the problem

This might seem like a very weird use case for Vite, but I've been experimenting with developing a framework to develop normal Node.js libraries or even CLIs using Vite as the core. Hugely inspired by how Vitest works with vite-node, it aims to centralise all the previous build tools (tsc, rollup, testing etc.) into one convenient package using the same Vite config. Plain, simple and easy to setup a modern ESM-first TS project with far less dependencies to go around. So far, the watch mode uses the normal esbuild transforms, rollup for production etc. Essentially a modern TSDX replacement, I've been pleasantly surprised that Vite makes a good fit even if it isn't the original intended use case.

Which leads to one slight hiccup. Vite does not really support any libraries that call Node.js core modules since the intended target is browsers only.

For example, trying to develop a CLI using cac returns the following error when running vite build:

1: import { EventEmitter } from 'events';
            ^
error during build:
Error: 'EventEmitter' is not exported by __vite-browser-external, imported by ../../node_modules/cac/dist/index.mjs

A workaround is to update rollupOptions.external to include the "events" library (and also "node:events"). This isn't ideal considering there are many core Node.js modules, thus it'd be better to resolve this upstream.

Suggested solution

My proposal is to add an additional option in the Vite config that allows the resolver to ignore any imports that call the Node.js built-in modules. It could probably be a boolean value with the key node?

I feel an option like that could also let a different type of ecosystem to thrive and we may see more interesting Node-based projects using Vite, especially due to vite-node.

Alternative

#2694 (comment) is the most similar issue I've found, although the solution is not optimal in the above-given context as the libraries are not intended for the browser.

Additional context

No response

Validations

@bluwy
Copy link
Member

bluwy commented Apr 20, 2022

A workaround is to update rollupOptions.external to include the "events" library (and also "node:events"). This isn't ideal considering there are many core Node.js modules, thus it'd be better to resolve this upstream.

What about using import { builtinModules } from 'module' and pass the list of nodejs builtin modules to rolllupOptions.external? I'm not sure it's good to officially support this usecase yet since it's a niche one. And it may be better done externally first too to quickly make changes. Until it's stablized (e.g. vite-node), there may be a chance Vite could officially support it.

@ayuhito
Copy link
Author

ayuhito commented Apr 20, 2022

What about using import { builtinModules } from 'module' and pass the list of nodejs builtin modules to rolllupOptions.external?

I didn't know that was an option, thanks!

I'm not sure it's good to officially support this usecase yet since it's a niche one. And it may be better done externally first too to quickly make changes. Until it's stablized (e.g. vite-node), there may be a chance Vite could officially support it.

Understandable, I'll leave this issue open for future consideration. As a relevant topic, #7810 seems to suggest rather than adding an additional config key, it might be better to just use the build.target key with the value node12 or other versions instead. I feel that PR is closely related to the intent of this issue as well, which may highlight there is more interest for something like this than initially believed?

It would make sense to determine if there are any other things that are Node-specific that Vite currently doesn't handle properly such as process.env to help spec out what else needs to be supported.

@alex8088
Copy link

@bluwy As @DecliningLotus said is what I think.

@bluwy
Copy link
Member

bluwy commented Apr 22, 2022

It would make sense to determine if there are any other things that are Node-specific that Vite currently doesn't handle properly such as process.env to help spec out what else needs to be supported.

I think the same too, though the official way Vite is handling Node-stuff now is through the ssr config. Perhaps that can't be used for y'alls usecase?

Would love to hear how y'all are using Vite (with examples?) so that we can be more opinionated and handle all them through the smallest set of config @DecliningLotus @alex8088

@ayuhito
Copy link
Author

ayuhito commented Apr 22, 2022

@bluwy I'll be happy to share what I'm working on in the near future here since it's library starter for everyone to use. Just held up with some other commitments.

As for the SSR option, that might be ideal, I'll play around with it. Thanks!

@alex8088
Copy link

@bluwy , my usecase:

I use Vite in my Electron project to bundle my code. In Electron projects, node.js and browser environments need to be handled at the same time. In a node.js environment (the main process), developers will only use Vite's build mode instead of serve mode. In this case, process.env cannot be used in Electron's main process. I write a tool for Electron that integrated with Vite: electron-vite, having run into these issues, you can see if it helps you understand my usecase.

@bluwy
Copy link
Member

bluwy commented Apr 26, 2022

Note: I found another case where nodeXX is used for build.target (sveltejs/kit#4742). SvelteKit's using in with the ssr config too so it makes sense. Using build.target: "nodeXX" only still seems like a unique usecase, but maybe that's a fine enough heuristic to get it to work with Electron, or maybe we want to introduce ssr.target: "electron" 🤔

@ayuhito
Copy link
Author

ayuhito commented Apr 30, 2022

@bluwy, I'm not sure whether to bring this up in #6812, but I think SSR is a blocker for me as I need to offer an option to output both CJS and ES at the same time. Quite frankly the workarounds using the normal build config offer more flexibility than SSR.

My build.lib config for outputting both CJS and ES looks like:

formats: ["es", "cjs"],
fileName: (format) =>
    `${name}.${format}.${format === "es" ? "mjs" : "js"}`,

Creating two bundles of .cjs.js and .es.mjs in the dist folder. I don't know if that's something I should bring up to expand the scope of that PR.

I could just run it twice to handle both formats, but that feels like a clunkier workaround than without SSR.

@abarke
Copy link

abarke commented May 31, 2022

Having the same issue here. Having a clear target as previously mentioned would be the most rational and elegant solution IMO.

Here is the vite.config.ts we currently us for a node-polyfills package we use internally.

import { defineConfig } from "vite"

export const name = "node-polyfills"

export default defineConfig({
  build: {
    target: "ESNext",
    lib: {
      name,
      entry: "src/index.ts",
      fileName: (format) => `${name}.${format}.js`,
    },
    sourcemap: true,
    rollupOptions: {
      external: [
        "node:util",
        "node:buffer",
        "node:stream",
        "node:net",
        "node:url",
        "node:fs",
        "node:path",
        "perf_hooks",
      ],
      output: {
        globals: {
          "node:stream": "stream",
          "node:buffer": "buffer",
          "node:util": "util",
          "node:net": "net",
          "node:url": "url",
          perf_hooks: "perf_hooks",
        },
        inlineDynamicImports: true,
      },
    },
  },
})

Best solution IMO:

{
  build: {
    target: "node12"
  }
}

target: "node12" is already supported by esbuild according to the docs https://esbuild.github.io/api/#target

require('esbuild').buildSync({
  entryPoints: ['app.js'],
  target: [
    'es2020',
    'chrome58',
    'edge16',
    'firefox57',
    'ie11',
    'ios10',
    'node12',
    'opera45',
    'safari11',
  ],
  outfile: 'out.js',
})

However if I try target: "node12" I get:

'promisify' is not exported by __vite-browser-external

@abarke
Copy link

abarke commented May 31, 2022

Anyone seen this? https://esbuild.github.io/getting-started/#bundling-for-node

Could this option be used for targeting the node platform? Or is rollup responsible for this!?

Keep wondering what responsibilities esbuild and rollup have exactly in vite...
anyone know where the line is drawn? Is it documented somewhere?

@abarke
Copy link

abarke commented May 31, 2022

I could imagine that if target: "node12" is set that esbuild takes over the build instead of rollup in this case.

@svedova
Copy link

svedova commented Nov 18, 2022

I have a similar use case. I need to SSR my app and bundle dependencies as I'll deploy to a serverless environment. The SSR part uses core modules such as node:fs and node:path.

Unfortunately, providing noExternals: true tries to bundle everything, including built in modules. I tried doing something like:

import { builtinModules } from "module";

ssr: {
  noExternals: true,
  externals: builtinModules
}

but I believe externals is not prioritised over noExternals. All I need is to bundle ALL dependencies except built-in modules.

Is there any way to do this?

@svedova
Copy link

svedova commented Jan 13, 2023

I have a similar use case. I need to SSR my app and bundle dependencies as I'll deploy to a serverless environment. The SSR part uses core modules such as node:fs and node:path.

Unfortunately, providing noExternals: true tries to bundle everything, including built in modules. I tried doing something like:

import { builtinModules } from "module";

ssr: {
  noExternals: true,
  externals: builtinModules
}

but I believe externals is not prioritised over noExternals. All I need is to bundle ALL dependencies except built-in modules.

Is there any way to do this?

For anyone struggling with this, I'm importing package.json and specifying the dependencies as noExternal as a workaround.

If you need a list of built-in modules anyways, here's how to do it:

import { builtinModules as builtin } from "node:module";

You can see an example here: stormkit-io/monorepo-template-react@01704b5#diff-78f8b638be47e5d535453248b4cbfa52bb6d9a306abf44ce2f5b167facb32ad5R11

Though, bear in mind that reading package.json may not be a viable solution because it lists only direct dependencies. You can also get the full list of dependencies from node_modules instead.

@justwiebe
Copy link

justwiebe commented Feb 16, 2023

Is there a full example anywhere of how to build a library for Node? I need to create some wrappers around my Nuxt server and would like my code for that bundled together, but it runs into problems with things like FormData being undefined for axios. It all works fine when building with Webpack.

My working Webpack Config:

export default {
  mode: 'production',
  entry: path.join(__dirname, 'index.js'),
  output: {
    path: path.join(__dirname, '../../.output/'),
    filename: 'cloud-entry-webpack.cjs',
    library: {
      name: 'cloudEntry',
      type: 'umd'
    }
  },
  target: 'node16'
};

Broken Vite config:

const builtins = builtinModules.filter(m => !m.startsWith('_')); builtins.push(...builtins.map(m => `node:${m}`));

export default defineConfig({
  build: {
    target: 'node12',
    outDir: path.join(__dirname, '../../.output/'),
    lib: {
      entry: path.join(__dirname, 'index.js'),
      fileName: 'cloud-entry',
      name: 'cloudEntry',
      formats: ['umd']
    },
    rollupOptions: {
      external: [...builtins]
    }
  },
  optimizeDeps: {
    exclude: [...builtins]
  }
});

@feaswcy
Copy link

feaswcy commented Mar 13, 2023

https://www.npmjs.com/package/vite-plugin-node-polyfills seems to be the offical version, works fine for me.

@0x80
Copy link

0x80 commented Apr 28, 2023

Hi, I'm struggling trying to bundle for a Node target and this thread seems to be related. I hope someone can point out where I'm going wrong. Here is my config:

https://gist.github.com/0x80/586283af54ff2b8a436a01a4b62bcea6

I have tried the config with and without the globals declaration. I suspect that I shouldn't use the globals as these core modules are not available as a global, but in either case, when I execute the result like node dist/index.js I get the following error:

file:///something/something/dist/index.js:24680
const crypto$8 = crypto, isCryptoKey = (r) => r instanceof CryptoKey, digest = async (r, e) => {
                 ^
ReferenceError: crypto is not defined

What do I need to do to make Vite understand that "crypto" should be an imported core module?

@pthieu
Copy link

pthieu commented May 5, 2023

For anyone reading, I'm able to externalize node core libs with this config:

import { builtinModules } from 'module';
import path from 'path';
import { defineConfig } from 'vite';
import tsconfigPaths from 'vite-tsconfig-paths';

export default defineConfig({
  plugins: [tsconfigPaths()],
  build: {
    lib: {
      entry: path.resolve(__dirname, 'index.ts'),
      formats: ['es'],
      fileName: () => 'out.js',
    },
    rollupOptions: {
      external: [...builtinModules, ...builtinModules.map((m) => `node:${m}`)],
    },
  },
});

I'm building a boilerplate for a Typescript+Lambda monorepo and this would be placed in a function's folder. The idea is that common libs will be imported from a folder outside the function's folder, but be bundled during build time.

The assumption is that the Lambda runtime will have node core lib's available.

I haven't gotten to the CI/CD point yet, but I'm thinking issues might come up then, especially if your CI is trying to figure out if it should build a function and deploy it or not. If anyone's gone down this road, let me know, would love to get some pointers.

@roydukkey
Copy link

Wouldn't this simply be enough?

import { builtinModules } from 'module';

export default defineConfig({
  /* ... */
  build: {
    rollupOptions: {
      external: [...builtinModules, /^node:/]
    }
  }
}

@justin0mcateer
Copy link

Library mode still seems to be trying to create a browser bundle even when 'node' is specified as the target. I've run into problems with libraries that have browser specific sources (eg. via tha 'browser' field in package.json)

@Inlustra
Copy link

I have a similar use case. I need to SSR my app and bundle dependencies as I'll deploy to a serverless environment. The SSR part uses core modules such as node:fs and node:path.
Unfortunately, providing noExternals: true tries to bundle everything, including built in modules. I tried doing something like:

import { builtinModules } from "module";

ssr: {
  noExternals: true,
  externals: builtinModules
}

but I believe externals is not prioritised over noExternals. All I need is to bundle ALL dependencies except built-in modules.
Is there any way to do this?

For anyone struggling with this, I'm importing package.json and specifying the dependencies as noExternal as a workaround.

If you need a list of built-in modules anyways, here's how to do it:

import { builtinModules as builtin } from "node:module";

You can see an example here: stormkit-io/monorepo-template-react@01704b5#diff-78f8b638be47e5d535453248b4cbfa52bb6d9a306abf44ce2f5b167facb32ad5R11

Though, bear in mind that reading package.json may not be a viable solution because it lists only direct dependencies. You can also get the full list of dependencies from node_modules instead.

I was able to solve this using the external both, inside rollupConfig and inside ssr:


import { builtinModules } from "node:module";

import { defineConfig } from "vite";

const external =  [...builtinModules, ...builtinModules.map((m) => `node:${m}`)];

export default defineConfig({
  resolve: {
    preserveSymlinks: true,
  },
  plugins: [],
  build: {
    ssr: "./src/index.ts",
    outDir: "dist",
    rollupOptions: {
        output: {
            inlineDynamicImports: true,
        },
        preserveSymlinks: true,
        external
    },
  },
  ssr: {
    noExternal: process.env.NODE_ENV === "production" || undefined,
    external
  }
});

This will bundle absolutely everything while in production except the node built in modules.

@EvanBacon
Copy link

I have a similar use case. I need to SSR my app and bundle dependencies as I'll deploy to a serverless environment. The SSR part uses core modules such as node:fs and node:path.

Unfortunately, providing noExternals: true tries to bundle everything, including built in modules. I tried doing something like:
Is there any way to do this?

I found that using a glob array noExternal: [/.*/], instead of noExternal: true will force Vite to observe the external field, which you can fill with a list of Node.js externals, e.g. external: require("module").builtinModules.

@daniel-nagy
Copy link

To get this working during development and in production for a React app I had to use the following config.

import { builtinModules } from "node:module";

export default defineConfig({
  ...,
  ssr: {
    external: [...builtinModules, ...builtinModules.map((m) => `node:${m}`)],
    noExternal: process.env.NODE_ENV === "production" ? [/.*/] : undefined
  }
});

If i just had noExternal: [/.*/] I would get errors in react/jsx-runtime that require was not defined during development.

@patak-dev patak-dev added p2-to-be-discussed Enhancement under consideration (priority) and removed discussion labels Feb 12, 2024
@jaanli
Copy link

jaanli commented Feb 18, 2024

Also interested in this for using https://github.com/observablehq/framework for certain pages and vite / nuxt to enable SSR for these pages to be dynamically generated & routed.

@Eliot00
Copy link

Eliot00 commented Mar 15, 2024

I just want to build a nodejs native package, hope vite provide option target: "node"

zjavax added a commit to zjavax/cardano-wallet-connector-vue that referenced this issue Mar 28, 2024
vitejs/vite#7821
import { builtinModules } from 'module';

export default defineConfig({
  /* ... */
  build: {
    rollupOptions: {
      external: [...builtinModules, /^node:/]
    }
  }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
p2-to-be-discussed Enhancement under consideration (priority)
Projects
None yet
Development

No branches or pull requests