feat(ssr): backport ssr.resolve.conditions and ssr.resolve.externalConditions (#14498) (#14668)

Co-authored-by: Marc MacLeod <marbemac@gmail.com>
This commit is contained in:
patak 2023-10-18 10:31:54 +02:00 committed by GitHub
parent ad7466c45d
commit 520139cdff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 416 additions and 2 deletions

View File

@ -29,3 +29,19 @@ Build target for the SSR server.
- **Default:** `esm`
Build format for the SSR server. Since Vite v3 the SSR build generates ESM by default. `'cjs'` can be selected to generate a CJS build, but it isn't recommended. The option is left marked as experimental to give users more time to update to ESM. CJS builds require complex externalization heuristics that aren't present in the ESM format.
## ssr.resolve.conditions
- **Type:** `string[]`
- **Related:** [Resolve Conditions](./shared-options.md#resolve-conditions)
Defaults to the the root [`resolve.conditions`](./shared-options.md#resolve-conditions).
These conditions are used in the plugin pipeline, and only affect non-externalized dependencies during the SSR build. Use `ssr.resolve.externalConditions` to affect externalized imports.
## ssr.resolve.externalConditions
- **Type:** `string[]`
- **Default:** `[]`
Conditions that are used during ssr import (including `ssrLoadModule`) of externalized dependencies.

View File

@ -259,6 +259,10 @@ In some cases like `webworker` runtimes, you might want to bundle your SSR build
- Treat all dependencies as `noExternal`
- Throw an error if any Node.js built-ins are imported
## SSR Resolve Conditions
By default package entry resolution will use the conditions set in [`resolve.conditions`](../config/shared-options.md#resolve-conditions) for the SSR build. You can use [`ssr.resolve.conditions`](../config/ssr-options.md#ssr-resolve-conditions) and [`ssr.resolve.externalConditions`](../config/ssr-options.md#ssr-resolve-externalconditions) to customize this behavior.
## Vite CLI
The CLI commands `$ vite dev` and `$ vite preview` can also be used for SSR apps. You can add your SSR middlewares to the development server with [`configureServer`](/guide/api-plugin#configureserver) and to the preview server with [`configurePreviewServer`](/guide/api-plugin#configurepreviewserver).

View File

@ -173,10 +173,17 @@ export function resolvePlugin(resolveOptions: InternalResolveOptions): Plugin {
const isRequire: boolean =
resolveOpts?.custom?.['node-resolve']?.isRequire ?? false
// end user can configure different conditions for ssr and client.
// falls back to client conditions if no ssr conditions supplied
const ssrConditions =
resolveOptions.ssrConfig?.resolve?.conditions ||
resolveOptions.conditions
const options: InternalResolveOptions = {
isRequire,
...resolveOptions,
scan: resolveOpts?.scan ?? resolveOptions.scan,
conditions: ssr ? ssrConditions : resolveOptions.conditions,
}
const resolvedImports = resolveSubpathImports(

View File

@ -8,12 +8,14 @@ export type SsrDepOptimizationOptions = DepOptimizationConfig
export interface SSROptions {
noExternal?: string | RegExp | (string | RegExp)[] | true
external?: string[]
/**
* Define the target for the ssr build. The browser field in package.json
* is ignored for node but used if webworker is the target
* @default 'node'
*/
target?: SSRTarget
/**
* Define the format for the ssr build. Since Vite v3 the SSR build generates ESM by default.
* `'cjs'` can be selected to generate a CJS build, but it isn't recommended. This option is
@ -33,6 +35,24 @@ export interface SSROptions {
* @experimental
*/
optimizeDeps?: SsrDepOptimizationOptions
resolve?: {
/**
* Conditions that are used in the plugin pipeline. The default value is the root config's `resolve.conditions`.
*
* Use this to override the default ssr conditions for the ssr build.
*
* @default rootConfig.resolve.conditions
*/
conditions?: string[]
/**
* Conditions that are used during ssr import (including `ssrLoadModule`) of externalized dependencies.
*
* @default []
*/
externalConditions?: string[]
}
}
export interface ResolvedSSROptions extends SSROptions {

View File

@ -119,11 +119,14 @@ export function createIsConfiguredAsSsrExternal(
typeof noExternal !== 'boolean' &&
createFilter(undefined, noExternal, { resolve: false })
const targetConditions = config.ssr.resolve?.externalConditions || []
const resolveOptions: InternalResolveOptions = {
...config.resolve,
root,
isProduction: false,
isBuild: true,
conditions: targetConditions,
}
const isExternalizable = (

View File

@ -123,19 +123,23 @@ async function instantiateModule(
isProduction,
resolve: { dedupe, preserveSymlinks },
root,
ssr,
} = server.config
const overrideConditions = ssr.resolve?.externalConditions || []
const resolveOptions: InternalResolveOptionsWithOverrideConditions = {
mainFields: ['main'],
browserField: true,
conditions: [],
overrideConditions: ['production', 'development'],
overrideConditions: [...overrideConditions, 'production', 'development'],
extensions: ['.js', '.cjs', '.json'],
dedupe,
preserveSymlinks,
isBuild: false,
isProduction,
root,
ssrConfig: ssr,
}
// Since dynamic imports can happen in parallel, we need to
@ -281,6 +285,8 @@ async function nodeImport(
? { ...resolveOptions, tryEsmOnly: true }
: resolveOptions,
false,
undefined,
true,
)
if (!resolved) {
const err: any = new Error(

View File

@ -0,0 +1,35 @@
// this is automatically detected by playground/vitestSetup.ts and will replace
// the default e2e test serve behavior
import path from 'node:path'
import kill from 'kill-port'
import { hmrPorts, ports, rootDir } from '~utils'
export const port = ports['ssr-conditions']
export async function serve(): Promise<{ close(): Promise<void> }> {
await kill(port)
const { createServer } = await import(path.resolve(rootDir, 'server.js'))
const { app, vite } = await createServer(rootDir, hmrPorts['ssr-conditions'])
return new Promise((resolve, reject) => {
try {
const server = app.listen(port, () => {
resolve({
// for test teardown
async close() {
await new Promise((resolve) => {
server.close(resolve)
})
if (vite) {
await vite.close()
}
},
})
})
} catch (e) {
reject(e)
}
})
}

View File

@ -0,0 +1,27 @@
import { expect, test } from 'vitest'
import { port } from './serve'
import { page } from '~utils'
const url = `http://localhost:${port}`
test('ssr.resolve.conditions affect non-externalized imports during ssr', async () => {
await page.goto(url)
expect(await page.textContent('.no-external-react-server')).toMatch(
'node.unbundled.js',
)
})
test('ssr.resolve.externalConditions affect externalized imports during ssr', async () => {
await page.goto(url)
expect(await page.textContent('.external-react-server')).toMatch('edge.js')
})
test('ssr.resolve settings do not affect non-ssr imports', async () => {
await page.goto(url)
expect(await page.textContent('.browser-no-external-react-server')).toMatch(
'default.js',
)
expect(await page.textContent('.browser-external-react-server')).toMatch(
'default.js',
)
})

View File

@ -0,0 +1 @@
export default 'browser.js'

View File

@ -0,0 +1 @@
export default 'default.js'

View File

@ -0,0 +1 @@
export default 'edge.js'

View File

@ -0,0 +1 @@
export default 'node.js'

View File

@ -0,0 +1 @@
export default 'node.unbundled.js'

View File

@ -0,0 +1,21 @@
{
"name": "@vitejs/test-ssr-conditions-external",
"private": true,
"version": "0.0.0",
"type": "module",
"exports": {
"./server": {
"react-server": {
"workerd": "./edge.js",
"deno": "./browser.js",
"node": {
"webpack": "./node.js",
"default": "./node.unbundled.js"
},
"edge-light": "./edge.js",
"browser": "./browser.js"
},
"default": "./default.js"
}
}
}

View File

@ -0,0 +1,29 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SSR Resolve Conditions</title>
</head>
<body>
<h1>SSR Resolve Conditions</h1>
<div id="app"><!--app-html--></div>
<script type="module">
import('@vitejs/test-ssr-conditions-no-external/server').then(
({ default: message }) => {
document.querySelector(
'.browser-no-external-react-server',
).textContent = message
},
)
import('@vitejs/test-ssr-conditions-external/server').then(
({ default: message }) => {
document.querySelector('.browser-external-react-server').textContent =
message
},
)
</script>
</body>
</html>

View File

@ -0,0 +1 @@
export default 'browser.js'

View File

@ -0,0 +1 @@
export default 'default.js'

View File

@ -0,0 +1 @@
export default 'edge.js'

View File

@ -0,0 +1 @@
export default 'node.js'

View File

@ -0,0 +1 @@
export default 'node.unbundled.js'

View File

@ -0,0 +1,21 @@
{
"name": "@vitejs/test-ssr-conditions-no-external",
"private": true,
"version": "0.0.0",
"type": "module",
"exports": {
"./server": {
"react-server": {
"workerd": "./edge.js",
"deno": "./browser.js",
"node": {
"webpack": "./node.js",
"default": "./node.unbundled.js"
},
"edge-light": "./edge.js",
"browser": "./browser.js"
},
"default": "./default.js"
}
}
}

View File

@ -0,0 +1,18 @@
{
"name": "@vitejs/test-ssr-conditions",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "node server",
"serve": "NODE_ENV=production node server",
"debug": "node --inspect-brk server"
},
"dependencies": {
"@vitejs/test-ssr-conditions-external": "file:./external",
"@vitejs/test-ssr-conditions-no-external": "file:./no-external"
},
"devDependencies": {
"express": "^4.18.2"
}
}

View File

@ -0,0 +1,70 @@
import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import express from 'express'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const isTest = process.env.VITEST
export async function createServer(root = process.cwd(), hmrPort) {
const resolve = (p) => path.resolve(__dirname, p)
const app = express()
/**
* @type {import('vite').ViteDevServer}
*/
const vite = await (
await import('vite')
).createServer({
root,
logLevel: isTest ? 'error' : 'info',
server: {
middlewareMode: true,
watch: {
// During tests we edit the files too fast and sometimes chokidar
// misses change events, so enforce polling for consistency
usePolling: true,
interval: 100,
},
hmr: {
port: hmrPort,
},
},
appType: 'custom',
})
app.use(vite.middlewares)
app.use('*', async (req, res) => {
try {
const url = req.originalUrl
let template
template = fs.readFileSync(resolve('index.html'), 'utf-8')
template = await vite.transformIndexHtml(url, template)
const render = (await vite.ssrLoadModule('/src/app.js')).render
const appHtml = await render(url, __dirname)
const html = template.replace(`<!--app-html-->`, appHtml)
res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
} catch (e) {
vite && vite.ssrFixStacktrace(e)
console.log(e.stack)
res.status(500).end(e.stack)
}
})
return { app, vite }
}
if (!isTest) {
createServer().then(({ app }) =>
app.listen(5173, () => {
console.log('http://localhost:5173')
}),
)
}

View File

@ -0,0 +1,16 @@
import noExternalReactServerMessage from '@vitejs/test-ssr-conditions-no-external/server'
import externalReactServerMessage from '@vitejs/test-ssr-conditions-external/server'
export async function render(url) {
let html = ''
html += `\n<p class="no-external-react-server">${noExternalReactServerMessage}</p>`
html += `\n<p class="browser-no-external-react-server"></p>`
html += `\n<p class="external-react-server">${externalReactServerMessage}</p>`
html += `\n<p class="browser-external-react-server"></p>`
return html + '\n'
}

View File

@ -0,0 +1,12 @@
import { defineConfig } from 'vite'
export default defineConfig({
ssr: {
external: ['@vitejs/test-ssr-conditions-external'],
noExternal: ['@vitejs/test-ssr-conditions-no-external'],
resolve: {
conditions: ['react-server'],
externalConditions: ['workerd', 'react-server'],
},
},
})

View File

@ -11,6 +11,18 @@ test('/', async () => {
expect(await page.textContent('.external')).toMatch('object')
})
test('supports resolve.conditions', async () => {
await page.goto(url)
expect(await page.textContent('.worker-exports')).toMatch('[success] worker')
})
test('respects browser export', async () => {
await page.goto(url)
expect(await page.textContent('.browser-exports')).toMatch(
'[success] browser',
)
})
test.runIf(isBuild)('inlineDynamicImports', () => {
const dynamicJsContent = findAssetFile(/dynamic-\w+\.js/, 'worker')
expect(dynamicJsContent).toBe('')

View File

@ -0,0 +1 @@
export default '[success] browser'

View File

@ -0,0 +1 @@
export default '[fail] should not load me'

View File

@ -0,0 +1,12 @@
{
"name": "@vitejs/test-browser-exports",
"private": true,
"version": "0.0.0",
"exports": {
".": {
"browser": "./browser.js",
"node": "./node.js",
"default": "./node.js"
}
}
}

View File

@ -8,7 +8,9 @@
"build:worker": "vite build --ssr src/entry-worker.jsx --outDir dist/worker"
},
"dependencies": {
"react": "^18.2.0"
"react": "^18.2.0",
"@vitejs/test-browser-exports": "file:./browser-exports",
"@vitejs/test-worker-exports": "file:./worker-exports"
},
"devDependencies": {
"miniflare": "^1.4.1",

View File

@ -1,4 +1,6 @@
import { msg as linkedMsg } from '@vitejs/test-resolve-linked'
import browserExportsMessage from '@vitejs/test-browser-exports'
import workerExportsMessage from '@vitejs/test-worker-exports'
import React from 'react'
let loaded = false
@ -14,6 +16,8 @@ addEventListener('fetch', function (event) {
<p class="linked">${linkedMsg}</p>
<p class="external">${typeof React}</p>
<p>dynamic: ${loaded}</p>
<p class="browser-exports">${browserExportsMessage}</p>
<p class="worker-exports">${workerExportsMessage}</p>
`,
{
headers: {

View File

@ -6,6 +6,7 @@ export default defineConfig({
},
resolve: {
dedupe: ['react'],
conditions: ['worker'],
},
ssr: {
target: 'webworker',

View File

@ -0,0 +1,2 @@
// conditions are set to worker, and worker is higher up in the exports object in package.json, so should be preferred
export default '[fail] should not load me'

View File

@ -0,0 +1 @@
export default '[fail] should not load me'

View File

@ -0,0 +1,13 @@
{
"name": "@vitejs/test-worker-exports",
"private": true,
"version": "0.0.0",
"exports": {
".": {
"worker": "./worker.js",
"browser": "./browser.js",
"node": "./node.js",
"default": "./node.js"
}
}
}

View File

@ -0,0 +1 @@
export default '[success] worker'

View File

@ -32,6 +32,7 @@ export const ports = {
'ssr-webworker': 9605,
'proxy-hmr': 9606, // not imported but used in `proxy-hmr/vite.config.js`
'proxy-hmr/other-app': 9607, // not imported but used in `proxy-hmr/other-app/vite.config.js`
'ssr-conditions': 9608,
'css/postcss-caching': 5005,
'css/postcss-plugins-different-dir': 5006,
'css/dynamic-import': 5007,
@ -45,6 +46,7 @@ export const hmrPorts = {
'ssr-noexternal': 24684,
'ssr-pug': 24685,
'css/lightningcss-proxy': 24686,
'ssr-conditions': 24688,
}
const hexToNameMap: Record<string, string> = {}

View File

@ -1185,6 +1185,23 @@ importers:
specifier: ^4.18.2
version: 4.18.2
playground/ssr-conditions:
dependencies:
'@vitejs/test-ssr-conditions-external':
specifier: file:./external
version: file:playground/ssr-conditions/external
'@vitejs/test-ssr-conditions-no-external':
specifier: file:./no-external
version: file:playground/ssr-conditions/no-external
devDependencies:
express:
specifier: ^4.18.2
version: 4.18.2
playground/ssr-conditions/external: {}
playground/ssr-conditions/no-external: {}
playground/ssr-deps:
dependencies:
'@vitejs/test-css-lib':
@ -1386,6 +1403,12 @@ importers:
playground/ssr-webworker:
dependencies:
'@vitejs/test-browser-exports':
specifier: file:./browser-exports
version: file:playground/ssr-webworker/browser-exports
'@vitejs/test-worker-exports':
specifier: file:./worker-exports
version: file:playground/ssr-webworker/worker-exports
react:
specifier: ^18.2.0
version: 18.2.0
@ -1397,6 +1420,10 @@ importers:
specifier: ^1.4.1
version: 1.4.1
playground/ssr-webworker/browser-exports: {}
playground/ssr-webworker/worker-exports: {}
playground/tailwind:
dependencies:
autoprefixer:
@ -11032,6 +11059,16 @@ packages:
dep-a: file:playground/preload/dep-a
dev: true
file:playground/ssr-conditions/external:
resolution: {directory: playground/ssr-conditions/external, type: directory}
name: '@vitejs/test-ssr-conditions-external'
dev: false
file:playground/ssr-conditions/no-external:
resolution: {directory: playground/ssr-conditions/no-external, type: directory}
name: '@vitejs/test-ssr-conditions-no-external'
dev: false
file:playground/ssr-deps/css-lib:
resolution: {directory: playground/ssr-deps/css-lib, type: directory}
name: '@vitejs/test-css-lib'
@ -11166,6 +11203,16 @@ packages:
name: '@vitejs/test-resolve-pkg-exports'
dev: false
file:playground/ssr-webworker/browser-exports:
resolution: {directory: playground/ssr-webworker/browser-exports, type: directory}
name: '@vitejs/test-browser-exports'
dev: false
file:playground/ssr-webworker/worker-exports:
resolution: {directory: playground/ssr-webworker/worker-exports, type: directory}
name: '@vitejs/test-worker-exports'
dev: false
file:playground/worker/dep-to-optimize:
resolution: {directory: playground/worker/dep-to-optimize, type: directory}
name: '@vitejs/test-dep-to-optimize'