From dab2188d58e2dcb03a900ac6626a21589df21356 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Thu, 18 Jan 2024 17:09:27 +0100 Subject: [PATCH] feat: experimental client-side Node.js compatibility (#25028) --- .../1.experimental-features.md | 15 +++++++++++++ packages/schema/build.config.ts | 1 + packages/schema/package.json | 1 + packages/schema/src/config/experimental.ts | 17 +++++++++++++- packages/vite/package.json | 1 + packages/vite/src/client.ts | 15 +++++++++++-- packages/webpack/package.json | 1 + packages/webpack/src/configs/client.ts | 22 ++++++++++++++++++- pnpm-lock.yaml | 9 ++++++++ test/basic.test.ts | 10 +++++++++ test/fixtures/basic/nuxt.config.ts | 1 + test/fixtures/basic/pages/node-compat.vue | 20 +++++++++++++++++ 12 files changed, 109 insertions(+), 4 deletions(-) create mode 100644 test/fixtures/basic/pages/node-compat.vue diff --git a/docs/2.guide/3.going-further/1.experimental-features.md b/docs/2.guide/3.going-further/1.experimental-features.md index 23cc9c52be..de1717faaa 100644 --- a/docs/2.guide/3.going-further/1.experimental-features.md +++ b/docs/2.guide/3.going-further/1.experimental-features.md @@ -359,3 +359,18 @@ const { data } = await useAsyncData(async () => { const { data } = await useAsyncData(route.params.slug, async () => { return await $fetch(`/api/my-page/${route.params.slug}`) }) +``` + +## clientNodeCompat + +With this feature, Nuxt will automatically polyfill Node.js imports in the client build using [`unenv`](https://github.com/unjs/unenv). + +::alert{type=info} +To make globals like `Buffer` work in the browser, you need to manually inject them. + +```ts +import { Buffer } from 'node:buffer' + +globalThis.Buffer = globalThis.Buffer || Buffer +``` +:: diff --git a/packages/schema/build.config.ts b/packages/schema/build.config.ts index 179fe33a72..c41cb37406 100644 --- a/packages/schema/build.config.ts +++ b/packages/schema/build.config.ts @@ -50,6 +50,7 @@ export default defineBuildConfig({ 'pug', 'sass-loader', 'c12', + 'unenv', // Implicit '@vue/compiler-core', '@vue/shared', diff --git a/packages/schema/package.json b/packages/schema/package.json index cb9507218f..0e291d99c8 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -50,6 +50,7 @@ "ofetch": "1.3.3", "unbuild": "latest", "unctx": "2.3.1", + "unenv": "^1.9.0", "vite": "5.0.11", "vue": "3.4.14", "vue-bundle-renderer": "2.0.0", diff --git a/packages/schema/src/config/experimental.ts b/packages/schema/src/config/experimental.ts index 22a86f2423..2ec9cd0a1e 100644 --- a/packages/schema/src/config/experimental.ts +++ b/packages/schema/src/config/experimental.ts @@ -307,6 +307,21 @@ export default defineUntypedSchema({ }, /** @type {Pick} */ useFetch: {} - } + }, + + /** + * Automatically polyfill Node.js imports in the client build using `unenv`. + * @see https://github.com/unjs/unenv + * + * **Note:** To make globals like `Buffer` work in the browser, you need to manually inject them. + * + * ```ts + * import { Buffer } from 'node:buffer' + * + * globalThis.Buffer = globalThis.Buffer || Buffer + * ``` + * @type {boolean} + */ + clientNodeCompat: false, } }) diff --git a/packages/vite/package.json b/packages/vite/package.json index 1deffe255c..cbfdc08d10 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -60,6 +60,7 @@ "std-env": "^3.7.0", "strip-literal": "^2.0.0", "ufo": "^1.3.2", + "unenv": "^1.8.0", "unplugin": "^1.6.0", "vite": "5.0.11", "vite-node": "^1.1.1", diff --git a/packages/vite/src/client.ts b/packages/vite/src/client.ts index f8c2982c71..2722b4deed 100644 --- a/packages/vite/src/client.ts +++ b/packages/vite/src/client.ts @@ -8,6 +8,7 @@ import { logger } from '@nuxt/kit' import { getPort } from 'get-port-please' import { joinURL, withoutLeadingSlash } from 'ufo' import { defu } from 'defu' +import { env, nodeless } from 'unenv' import { appendCorsHeaders, appendCorsPreflightHeaders, defineEventHandler } from 'h3' import type { ViteConfig } from '@nuxt/schema' import { chunkErrorPlugin } from './plugins/chunk-error' @@ -19,6 +20,13 @@ import { viteNodePlugin } from './vite-node' import { createViteLogger } from './utils/logger' export async function buildClient (ctx: ViteBuildContext) { + const nodeCompat = ctx.nuxt.options.experimental.clientNodeCompat ? { + alias: env(nodeless).alias, + define: { + global: 'globalThis', + } + } : { alias: {}, define: {} } + const clientConfig: ViteConfig = vite.mergeConfig(ctx.config, vite.mergeConfig({ configFile: false, base: ctx.nuxt.options.dev @@ -48,15 +56,18 @@ export async function buildClient (ctx: ViteBuildContext) { 'import.meta.browser': true, 'import.meta.nitro': false, 'import.meta.prerender': false, - 'module.hot': false + 'module.hot': false, + ...nodeCompat.define }, optimizeDeps: { entries: [ctx.entry] }, resolve: { alias: { + ...nodeCompat.alias, + ...ctx.config.resolve?.alias, '#build/plugins': resolve(ctx.nuxt.options.buildDir, 'plugins/client'), - '#internal/nitro': resolve(ctx.nuxt.options.buildDir, 'nitro.client.mjs') + '#internal/nitro': resolve(ctx.nuxt.options.buildDir, 'nitro.client.mjs'), }, dedupe: [ 'vue' diff --git a/packages/webpack/package.json b/packages/webpack/package.json index 2ad5cda878..57640c7f91 100644 --- a/packages/webpack/package.json +++ b/packages/webpack/package.json @@ -57,6 +57,7 @@ "std-env": "^3.7.0", "time-fix-plugin": "^2.0.7", "ufo": "^1.3.2", + "unenv": "^1.8.0", "unplugin": "^1.6.0", "url-loader": "^4.1.1", "vue-bundle-renderer": "^2.0.0", diff --git a/packages/webpack/src/configs/client.ts b/packages/webpack/src/configs/client.ts index b4a36828eb..7380882d14 100644 --- a/packages/webpack/src/configs/client.ts +++ b/packages/webpack/src/configs/client.ts @@ -5,6 +5,7 @@ import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer' import { logger } from '@nuxt/kit' import { joinURL } from 'ufo' import ForkTSCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin' +import { env, nodeless } from 'unenv' import type { WebpackConfigContext } from '../utils/config' import { applyPresets } from '../utils/config' @@ -20,7 +21,8 @@ export function client (ctx: WebpackConfigContext) { clientOptimization, clientDevtool, clientPerformance, - clientHMR + clientHMR, + clientNodeCompat, ]) } @@ -48,6 +50,24 @@ function clientPerformance (ctx: WebpackConfigContext) { } } +function clientNodeCompat(ctx: WebpackConfigContext) { + if (!ctx.nuxt.options.experimental.clientNodeCompat) { + return + } + ctx.config.plugins!.push(new webpack.DefinePlugin({ global: 'globalThis', })) + + ctx.config.resolve = ctx.config.resolve || {} + ctx.config.resolve.fallback = { + ...env(nodeless).alias, + ...ctx.config.resolve.fallback, + } + + // https://github.com/webpack/webpack/issues/13290#issuecomment-1188760779 + ctx.config.plugins!.unshift(new webpack.NormalModuleReplacementPlugin(/node:/, (resource) => { + resource.request = resource.request.replace(/^node:/, ''); + })) +} + function clientHMR (ctx: WebpackConfigContext) { if (!ctx.isDev) { return diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dbcde1f9fa..55cfd4fc4a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -508,6 +508,9 @@ importers: unctx: specifier: 2.3.1 version: 2.3.1 + unenv: + specifier: ^1.9.0 + version: 1.9.0 vite: specifier: 5.0.11 version: 5.0.11(@types/node@20.11.5) @@ -616,6 +619,9 @@ importers: ufo: specifier: ^1.3.2 version: 1.3.2 + unenv: + specifier: ^1.8.0 + version: 1.9.0 unplugin: specifier: ^1.6.0 version: 1.6.0 @@ -749,6 +755,9 @@ importers: ufo: specifier: ^1.3.2 version: 1.3.2 + unenv: + specifier: ^1.8.0 + version: 1.9.0 unplugin: specifier: ^1.6.0 version: 1.6.0 diff --git a/test/basic.test.ts b/test/basic.test.ts index 51d284ba4f..ab67992c8c 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -2285,6 +2285,16 @@ describe('keepalive', () => { }) }) +describe('Node.js compatibility for client-side', () => { + it('should work', async () => { + const { page } = await renderPage('/node-compat') + const html = await page.innerHTML('body') + expect(html).toContain('Nuxt is Awesome!') + expect(html).toContain('CWD: [available]') + await page.close() + }) +}) + function normaliseIslandResult (result: NuxtIslandResponse) { return { ...result, diff --git a/test/fixtures/basic/nuxt.config.ts b/test/fixtures/basic/nuxt.config.ts index b9d986aa94..3c6253e4c4 100644 --- a/test/fixtures/basic/nuxt.config.ts +++ b/test/fixtures/basic/nuxt.config.ts @@ -200,6 +200,7 @@ export default defineNuxtConfig({ respectNoSSRHeader: true, clientFallback: true, restoreState: true, + clientNodeCompat: true, componentIslands: { selectiveClient: true }, diff --git a/test/fixtures/basic/pages/node-compat.vue b/test/fixtures/basic/pages/node-compat.vue new file mode 100644 index 0000000000..d6b5dfc28f --- /dev/null +++ b/test/fixtures/basic/pages/node-compat.vue @@ -0,0 +1,20 @@ + + +