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 () => {
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',
'sass-loader',
'c12',
'unenv',
// Implicit
'@vue/compiler-core',
'@vue/shared',

View File

@ -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",

View File

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

View File

@ -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'

View File

@ -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",

View File

@ -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

View File

@ -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

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) {
return {
...result,

View File

@ -200,6 +200,7 @@ export default defineNuxtConfig({
respectNoSSRHeader: true,
clientFallback: true,
restoreState: true,
clientNodeCompat: true,
componentIslands: {
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>