Skip to content

Commit d0afc39

Browse files
author
Marc MacLeod
authored
feat(ssr): support for ssr.resolve.conditions and ssr.resolve.externalConditions options (#14498)
1 parent 22bd67d commit d0afc39

38 files changed

+416
-2
lines changed

docs/config/ssr-options.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,19 @@ Prevent listed dependencies from being externalized for SSR. If `true`, no depen
2020
- **Default:** `node`
2121

2222
Build target for the SSR server.
23+
24+
## ssr.resolve.conditions
25+
26+
- **Type:** `string[]`
27+
- **Related:** [Resolve Conditions](./shared-options.md#resolve-conditions)
28+
29+
Defaults to the the root [`resolve.conditions`](./shared-options.md#resolve-conditions).
30+
31+
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.
32+
33+
## ssr.resolve.externalConditions
34+
35+
- **Type:** `string[]`
36+
- **Default:** `[]`
37+
38+
Conditions that are used during ssr import (including `ssrLoadModule`) of externalized dependencies.

docs/guide/ssr.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,10 @@ In some cases like `webworker` runtimes, you might want to bundle your SSR build
259259
- Treat all dependencies as `noExternal`
260260
- Throw an error if any Node.js built-ins are imported
261261
262+
## SSR Resolve Conditions
263+
264+
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.
265+
262266
## Vite CLI
263267
264268
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).

packages/vite/src/node/plugins/resolve.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,10 +173,17 @@ export function resolvePlugin(resolveOptions: InternalResolveOptions): Plugin {
173173
const isRequire: boolean =
174174
resolveOpts?.custom?.['node-resolve']?.isRequire ?? false
175175

176+
// end user can configure different conditions for ssr and client.
177+
// falls back to client conditions if no ssr conditions supplied
178+
const ssrConditions =
179+
resolveOptions.ssrConfig?.resolve?.conditions ||
180+
resolveOptions.conditions
181+
176182
const options: InternalResolveOptions = {
177183
isRequire,
178184
...resolveOptions,
179185
scan: resolveOpts?.scan ?? resolveOptions.scan,
186+
conditions: ssr ? ssrConditions : resolveOptions.conditions,
180187
}
181188

182189
const resolvedImports = resolveSubpathImports(

packages/vite/src/node/ssr/index.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ export type SsrDepOptimizationOptions = DepOptimizationConfig
77
export interface SSROptions {
88
noExternal?: string | RegExp | (string | RegExp)[] | true
99
external?: string[]
10+
1011
/**
1112
* Define the target for the ssr build. The browser field in package.json
1213
* is ignored for node but used if webworker is the target
1314
* @default 'node'
1415
*/
1516
target?: SSRTarget
17+
1618
/**
1719
* Control over which dependencies are optimized during SSR and esbuild options
1820
* During build:
@@ -22,6 +24,24 @@ export interface SSROptions {
2224
* @experimental
2325
*/
2426
optimizeDeps?: SsrDepOptimizationOptions
27+
28+
resolve?: {
29+
/**
30+
* Conditions that are used in the plugin pipeline. The default value is the root config's `resolve.conditions`.
31+
*
32+
* Use this to override the default ssr conditions for the ssr build.
33+
*
34+
* @default rootConfig.resolve.conditions
35+
*/
36+
conditions?: string[]
37+
38+
/**
39+
* Conditions that are used during ssr import (including `ssrLoadModule`) of externalized dependencies.
40+
*
41+
* @default []
42+
*/
43+
externalConditions?: string[]
44+
}
2545
}
2646

2747
export interface ResolvedSSROptions extends SSROptions {

packages/vite/src/node/ssr/ssrExternal.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,14 @@ export function createIsConfiguredAsSsrExternal(
4040
typeof noExternal !== 'boolean' &&
4141
createFilter(undefined, noExternal, { resolve: false })
4242

43+
const targetConditions = config.ssr.resolve?.externalConditions || []
44+
4345
const resolveOptions: InternalResolveOptions = {
4446
...config.resolve,
4547
root,
4648
isProduction: false,
4749
isBuild: true,
50+
conditions: targetConditions,
4851
}
4952

5053
const isExternalizable = (

packages/vite/src/node/ssr/ssrModuleLoader.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,19 +123,23 @@ async function instantiateModule(
123123
isProduction,
124124
resolve: { dedupe, preserveSymlinks },
125125
root,
126+
ssr,
126127
} = server.config
127128

129+
const overrideConditions = ssr.resolve?.externalConditions || []
130+
128131
const resolveOptions: InternalResolveOptionsWithOverrideConditions = {
129132
mainFields: ['main'],
130133
browserField: true,
131134
conditions: [],
132-
overrideConditions: ['production', 'development'],
135+
overrideConditions: [...overrideConditions, 'production', 'development'],
133136
extensions: ['.js', '.cjs', '.json'],
134137
dedupe,
135138
preserveSymlinks,
136139
isBuild: false,
137140
isProduction,
138141
root,
142+
ssrConfig: ssr,
139143
}
140144

141145
// Since dynamic imports can happen in parallel, we need to
@@ -281,6 +285,8 @@ async function nodeImport(
281285
? { ...resolveOptions, tryEsmOnly: true }
282286
: resolveOptions,
283287
false,
288+
undefined,
289+
true,
284290
)
285291
if (!resolved) {
286292
const err: any = new Error(
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// this is automatically detected by playground/vitestSetup.ts and will replace
2+
// the default e2e test serve behavior
3+
4+
import path from 'node:path'
5+
import kill from 'kill-port'
6+
import { hmrPorts, ports, rootDir } from '~utils'
7+
8+
export const port = ports['ssr-conditions']
9+
10+
export async function serve(): Promise<{ close(): Promise<void> }> {
11+
await kill(port)
12+
13+
const { createServer } = await import(path.resolve(rootDir, 'server.js'))
14+
const { app, vite } = await createServer(rootDir, hmrPorts['ssr-conditions'])
15+
16+
return new Promise((resolve, reject) => {
17+
try {
18+
const server = app.listen(port, () => {
19+
resolve({
20+
// for test teardown
21+
async close() {
22+
await new Promise((resolve) => {
23+
server.close(resolve)
24+
})
25+
if (vite) {
26+
await vite.close()
27+
}
28+
},
29+
})
30+
})
31+
} catch (e) {
32+
reject(e)
33+
}
34+
})
35+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { expect, test } from 'vitest'
2+
import { port } from './serve'
3+
import { page } from '~utils'
4+
5+
const url = `http://localhost:${port}`
6+
7+
test('ssr.resolve.conditions affect non-externalized imports during ssr', async () => {
8+
await page.goto(url)
9+
expect(await page.textContent('.no-external-react-server')).toMatch(
10+
'node.unbundled.js',
11+
)
12+
})
13+
14+
test('ssr.resolve.externalConditions affect externalized imports during ssr', async () => {
15+
await page.goto(url)
16+
expect(await page.textContent('.external-react-server')).toMatch('edge.js')
17+
})
18+
19+
test('ssr.resolve settings do not affect non-ssr imports', async () => {
20+
await page.goto(url)
21+
expect(await page.textContent('.browser-no-external-react-server')).toMatch(
22+
'default.js',
23+
)
24+
expect(await page.textContent('.browser-external-react-server')).toMatch(
25+
'default.js',
26+
)
27+
})
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default 'browser.js'
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default 'default.js'
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default 'edge.js'
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default 'node.js'
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default 'node.unbundled.js'
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"name": "@vitejs/test-ssr-conditions-external",
3+
"private": true,
4+
"version": "0.0.0",
5+
"type": "module",
6+
"exports": {
7+
"./server": {
8+
"react-server": {
9+
"workerd": "./edge.js",
10+
"deno": "./browser.js",
11+
"node": {
12+
"webpack": "./node.js",
13+
"default": "./node.unbundled.js"
14+
},
15+
"edge-light": "./edge.js",
16+
"browser": "./browser.js"
17+
},
18+
"default": "./default.js"
19+
}
20+
}
21+
}

playground/ssr-conditions/index.html

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>SSR Resolve Conditions</title>
7+
</head>
8+
<body>
9+
<h1>SSR Resolve Conditions</h1>
10+
<div id="app"><!--app-html--></div>
11+
12+
<script type="module">
13+
import('@vitejs/test-ssr-conditions-no-external/server').then(
14+
({ default: message }) => {
15+
document.querySelector(
16+
'.browser-no-external-react-server',
17+
).textContent = message
18+
},
19+
)
20+
21+
import('@vitejs/test-ssr-conditions-external/server').then(
22+
({ default: message }) => {
23+
document.querySelector('.browser-external-react-server').textContent =
24+
message
25+
},
26+
)
27+
</script>
28+
</body>
29+
</html>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default 'browser.js'
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default 'default.js'
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default 'edge.js'
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default 'node.js'
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default 'node.unbundled.js'
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"name": "@vitejs/test-ssr-conditions-no-external",
3+
"private": true,
4+
"version": "0.0.0",
5+
"type": "module",
6+
"exports": {
7+
"./server": {
8+
"react-server": {
9+
"workerd": "./edge.js",
10+
"deno": "./browser.js",
11+
"node": {
12+
"webpack": "./node.js",
13+
"default": "./node.unbundled.js"
14+
},
15+
"edge-light": "./edge.js",
16+
"browser": "./browser.js"
17+
},
18+
"default": "./default.js"
19+
}
20+
}
21+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"name": "@vitejs/test-ssr-conditions",
3+
"private": true,
4+
"version": "0.0.0",
5+
"type": "module",
6+
"scripts": {
7+
"dev": "node server",
8+
"serve": "NODE_ENV=production node server",
9+
"debug": "node --inspect-brk server"
10+
},
11+
"dependencies": {
12+
"@vitejs/test-ssr-conditions-external": "file:./external",
13+
"@vitejs/test-ssr-conditions-no-external": "file:./no-external"
14+
},
15+
"devDependencies": {
16+
"express": "^4.18.2"
17+
}
18+
}

playground/ssr-conditions/server.js

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import fs from 'node:fs'
2+
import path from 'node:path'
3+
import { fileURLToPath } from 'node:url'
4+
import express from 'express'
5+
6+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
7+
8+
const isTest = process.env.VITEST
9+
10+
export async function createServer(root = process.cwd(), hmrPort) {
11+
const resolve = (p) => path.resolve(__dirname, p)
12+
13+
const app = express()
14+
15+
/**
16+
* @type {import('vite').ViteDevServer}
17+
*/
18+
const vite = await (
19+
await import('vite')
20+
).createServer({
21+
root,
22+
logLevel: isTest ? 'error' : 'info',
23+
server: {
24+
middlewareMode: true,
25+
watch: {
26+
// During tests we edit the files too fast and sometimes chokidar
27+
// misses change events, so enforce polling for consistency
28+
usePolling: true,
29+
interval: 100,
30+
},
31+
hmr: {
32+
port: hmrPort,
33+
},
34+
},
35+
appType: 'custom',
36+
})
37+
38+
app.use(vite.middlewares)
39+
40+
app.use('*', async (req, res) => {
41+
try {
42+
const url = req.originalUrl
43+
44+
let template
45+
template = fs.readFileSync(resolve('index.html'), 'utf-8')
46+
template = await vite.transformIndexHtml(url, template)
47+
const render = (await vite.ssrLoadModule('/src/app.js')).render
48+
49+
const appHtml = await render(url, __dirname)
50+
51+
const html = template.replace(`<!--app-html-->`, appHtml)
52+
53+
res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
54+
} catch (e) {
55+
vite && vite.ssrFixStacktrace(e)
56+
console.log(e.stack)
57+
res.status(500).end(e.stack)
58+
}
59+
})
60+
61+
return { app, vite }
62+
}
63+
64+
if (!isTest) {
65+
createServer().then(({ app }) =>
66+
app.listen(5173, () => {
67+
console.log('http://localhost:5173')
68+
}),
69+
)
70+
}

0 commit comments

Comments
 (0)