mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-22 05:35:13 +00:00
feat: experimental client-side Node.js compatibility (#25028)
This commit is contained in:
parent
807ead6f1a
commit
dab2188d58
@ -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
|
||||||
|
```
|
||||||
|
::
|
||||||
|
@ -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',
|
||||||
|
@ -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",
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -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",
|
||||||
|
@ -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'
|
||||||
|
@ -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",
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
1
test/fixtures/basic/nuxt.config.ts
vendored
1
test/fixtures/basic/nuxt.config.ts
vendored
@ -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
|
||||||
},
|
},
|
||||||
|
20
test/fixtures/basic/pages/node-compat.vue
vendored
Normal file
20
test/fixtures/basic/pages/node-compat.vue
vendored
Normal 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>
|
Loading…
Reference in New Issue
Block a user