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'

0 commit comments

Comments
 (0)