feat: experimental client-side Node.js compatibility (#25028)

This commit is contained in:
Pooya Parsa 2024-01-18 17:09:27 +01:00 committed by GitHub
parent 807ead6f1a
commit dab2188d58
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 109 additions and 4 deletions

View File

@ -359,3 +359,18 @@ const { data } = await useAsyncData(async () => {
const { data } = await useAsyncData(route.params.slug, async () => { const { data } = await useAsyncData(route.params.slug, async () => {
return await $fetch(`/api/my-page/${route.params.slug}`) 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
```
::

View File

@ -50,6 +50,7 @@ export default defineBuildConfig({
'pug', 'pug',
'sass-loader', 'sass-loader',
'c12', 'c12',
'unenv',
// Implicit // Implicit
'@vue/compiler-core', '@vue/compiler-core',
'@vue/shared', '@vue/shared',

View File

@ -50,6 +50,7 @@
"ofetch": "1.3.3", "ofetch": "1.3.3",
"unbuild": "latest", "unbuild": "latest",
"unctx": "2.3.1", "unctx": "2.3.1",
"unenv": "^1.9.0",
"vite": "5.0.11", "vite": "5.0.11",
"vue": "3.4.14", "vue": "3.4.14",
"vue-bundle-renderer": "2.0.0", "vue-bundle-renderer": "2.0.0",

View File

@ -307,6 +307,21 @@ export default defineUntypedSchema({
}, },
/** @type {Pick<typeof import('ofetch')['FetchOptions'], 'timeout' | 'retry' | 'retryDelay' | 'retryStatusCodes'>} */ /** @type {Pick<typeof import('ofetch')['FetchOptions'], 'timeout' | 'retry' | 'retryDelay' | 'retryStatusCodes'>} */
useFetch: {} 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,
} }
}) })

View File

@ -60,6 +60,7 @@
"std-env": "^3.7.0", "std-env": "^3.7.0",
"strip-literal": "^2.0.0", "strip-literal": "^2.0.0",
"ufo": "^1.3.2", "ufo": "^1.3.2",
"unenv": "^1.8.0",
"unplugin": "^1.6.0", "unplugin": "^1.6.0",
"vite": "5.0.11", "vite": "5.0.11",
"vite-node": "^1.1.1", "vite-node": "^1.1.1",

View File

@ -8,6 +8,7 @@ import { logger } from '@nuxt/kit'
import { getPort } from 'get-port-please' import { getPort } from 'get-port-please'
import { joinURL, withoutLeadingSlash } from 'ufo' import { joinURL, withoutLeadingSlash } from 'ufo'
import { defu } from 'defu' import { defu } from 'defu'
import { env, nodeless } from 'unenv'
import { appendCorsHeaders, appendCorsPreflightHeaders, defineEventHandler } from 'h3' import { appendCorsHeaders, appendCorsPreflightHeaders, defineEventHandler } from 'h3'
import type { ViteConfig } from '@nuxt/schema' import type { ViteConfig } from '@nuxt/schema'
import { chunkErrorPlugin } from './plugins/chunk-error' import { chunkErrorPlugin } from './plugins/chunk-error'
@ -19,6 +20,13 @@ import { viteNodePlugin } from './vite-node'
import { createViteLogger } from './utils/logger' import { createViteLogger } from './utils/logger'
export async function buildClient (ctx: ViteBuildContext) { 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({ const clientConfig: ViteConfig = vite.mergeConfig(ctx.config, vite.mergeConfig({
configFile: false, configFile: false,
base: ctx.nuxt.options.dev base: ctx.nuxt.options.dev
@ -48,15 +56,18 @@ export async function buildClient (ctx: ViteBuildContext) {
'import.meta.browser': true, 'import.meta.browser': true,
'import.meta.nitro': false, 'import.meta.nitro': false,
'import.meta.prerender': false, 'import.meta.prerender': false,
'module.hot': false 'module.hot': false,
...nodeCompat.define
}, },
optimizeDeps: { optimizeDeps: {
entries: [ctx.entry] entries: [ctx.entry]
}, },
resolve: { resolve: {
alias: { alias: {
...nodeCompat.alias,
...ctx.config.resolve?.alias,
'#build/plugins': resolve(ctx.nuxt.options.buildDir, 'plugins/client'), '#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: [ dedupe: [
'vue' 'vue'

View File

@ -57,6 +57,7 @@
"std-env": "^3.7.0", "std-env": "^3.7.0",
"time-fix-plugin": "^2.0.7", "time-fix-plugin": "^2.0.7",
"ufo": "^1.3.2", "ufo": "^1.3.2",
"unenv": "^1.8.0",
"unplugin": "^1.6.0", "unplugin": "^1.6.0",
"url-loader": "^4.1.1", "url-loader": "^4.1.1",
"vue-bundle-renderer": "^2.0.0", "vue-bundle-renderer": "^2.0.0",

View File

@ -5,6 +5,7 @@ import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'
import { logger } from '@nuxt/kit' import { logger } from '@nuxt/kit'
import { joinURL } from 'ufo' import { joinURL } from 'ufo'
import ForkTSCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin' import ForkTSCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin'
import { env, nodeless } from 'unenv'
import type { WebpackConfigContext } from '../utils/config' import type { WebpackConfigContext } from '../utils/config'
import { applyPresets } from '../utils/config' import { applyPresets } from '../utils/config'
@ -20,7 +21,8 @@ export function client (ctx: WebpackConfigContext) {
clientOptimization, clientOptimization,
clientDevtool, clientDevtool,
clientPerformance, 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) { function clientHMR (ctx: WebpackConfigContext) {
if (!ctx.isDev) { if (!ctx.isDev) {
return return

View File

@ -508,6 +508,9 @@ importers:
unctx: unctx:
specifier: 2.3.1 specifier: 2.3.1
version: 2.3.1 version: 2.3.1
unenv:
specifier: ^1.9.0
version: 1.9.0
vite: vite:
specifier: 5.0.11 specifier: 5.0.11
version: 5.0.11(@types/node@20.11.5) version: 5.0.11(@types/node@20.11.5)
@ -616,6 +619,9 @@ importers:
ufo: ufo:
specifier: ^1.3.2 specifier: ^1.3.2
version: 1.3.2 version: 1.3.2
unenv:
specifier: ^1.8.0
version: 1.9.0
unplugin: unplugin:
specifier: ^1.6.0 specifier: ^1.6.0
version: 1.6.0 version: 1.6.0
@ -749,6 +755,9 @@ importers:
ufo: ufo:
specifier: ^1.3.2 specifier: ^1.3.2
version: 1.3.2 version: 1.3.2
unenv:
specifier: ^1.8.0
version: 1.9.0
unplugin: unplugin:
specifier: ^1.6.0 specifier: ^1.6.0
version: 1.6.0 version: 1.6.0

View File

@ -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) { function normaliseIslandResult (result: NuxtIslandResponse) {
return { return {
...result, ...result,

View File

@ -200,6 +200,7 @@ export default defineNuxtConfig({
respectNoSSRHeader: true, respectNoSSRHeader: true,
clientFallback: true, clientFallback: true,
restoreState: true, restoreState: true,
clientNodeCompat: true,
componentIslands: { componentIslands: {
selectiveClient: true selectiveClient: true
}, },

View File

@ -0,0 +1,20 @@
<script setup lang="ts">
import { Buffer } from 'node:buffer'
import process from 'node:process'
const base64 = atob(Buffer.from('Nuxt is Awesome!', 'utf8').toString('base64'))
const cwd = typeof process.cwd == 'function' && "[available]"
</script>
<template>
<div>
<ClientOnly>
<div>
{{ base64 }}
</div>
<div>
CWD: {{ cwd }}
</div>
</ClientOnly>
</div>
</template>