Skip to content

Commit 520139c

Browse files
patak-devMarc MacLeod
and
Marc MacLeod
authored
feat(ssr): backport ssr.resolve.conditions and ssr.resolve.externalConditions (#14498) (#14668)
Co-authored-by: Marc MacLeod <[email protected]>
1 parent ad7466c commit 520139c

38 files changed

+416
-2
lines changed

docs/config/ssr-options.md

+16
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,19 @@ Build target for the SSR server.
2929
- **Default:** `esm`
3030

3131
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.
32+
33+
## ssr.resolve.conditions
34+
35+
- **Type:** `string[]`
36+
- **Related:** [Resolve Conditions](./shared-options.md#resolve-conditions)
37+
38+
Defaults to the the root [`resolve.conditions`](./shared-options.md#resolve-conditions).
39+
40+
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.
41+
42+
## ssr.resolve.externalConditions
43+
44+
- **Type:** `string[]`
45+
- **Default:** `[]`
46+
47+
Conditions that are used during ssr import (including `ssrLoadModule`) of externalized dependencies.

docs/guide/ssr.md

+4
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

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

+20
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@ export type SsrDepOptimizationOptions = DepOptimizationConfig
88
export interface SSROptions {
99
noExternal?: string | RegExp | (string | RegExp)[] | true
1010
external?: string[]
11+
1112
/**
1213
* Define the target for the ssr build. The browser field in package.json
1314
* is ignored for node but used if webworker is the target
1415
* @default 'node'
1516
*/
1617
target?: SSRTarget
18+
1719
/**
1820
* Define the format for the ssr build. Since Vite v3 the SSR build generates ESM by default.
1921
* `'cjs'` can be selected to generate a CJS build, but it isn't recommended. This option is
@@ -33,6 +35,24 @@ export interface SSROptions {
3335
* @experimental
3436
*/
3537
optimizeDeps?: SsrDepOptimizationOptions
38+
39+
resolve?: {
40+
/**
41+
* Conditions that are used in the plugin pipeline. The default value is the root config's `resolve.conditions`.
42+
*
43+
* Use this to override the default ssr conditions for the ssr build.
44+
*
45+
* @default rootConfig.resolve.conditions
46+
*/
47+
conditions?: string[]
48+
49+
/**
50+
* Conditions that are used during ssr import (including `ssrLoadModule`) of externalized dependencies.
51+
*
52+
* @default []
53+
*/
54+
externalConditions?: string[]
55+
}
3656
}
3757

3858
export interface ResolvedSSROptions extends SSROptions {

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

+3
Original file line numberDiff line numberDiff line change
@@ -119,11 +119,14 @@ export function createIsConfiguredAsSsrExternal(
119119
typeof noExternal !== 'boolean' &&
120120
createFilter(undefined, noExternal, { resolve: false })
121121

122+
const targetConditions = config.ssr.resolve?.externalConditions || []
123+
122124
const resolveOptions: InternalResolveOptions = {
123125
...config.resolve,
124126
root,
125127
isProduction: false,
126128
isBuild: true,
129+
conditions: targetConditions,
127130
}
128131

129132
const isExternalizable = (

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

+7-1
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(
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+
}
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+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default 'browser.js'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default 'default.js'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default 'edge.js'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default 'node.js'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default 'node.unbundled.js'
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

+29
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>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default 'browser.js'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default 'default.js'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default 'edge.js'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default 'node.js'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export default 'node.unbundled.js'
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+
}
+18
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

+70
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)