Skip to content

Commit

Permalink
feat(ssr): support for ssr.resolve.conditions and ssr.resolve.externa…
Browse files Browse the repository at this point in the history
…lConditions options (#14498)
  • Loading branch information
Marc MacLeod committed Oct 5, 2023
1 parent 22bd67d commit d0afc39
Show file tree
Hide file tree
Showing 38 changed files with 416 additions and 2 deletions.
16 changes: 16 additions & 0 deletions docs/config/ssr-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,19 @@ Prevent listed dependencies from being externalized for SSR. If `true`, no depen
- **Default:** `node`

Build target for the SSR server.

## 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.
4 changes: 4 additions & 0 deletions docs/guide/ssr.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
7 changes: 7 additions & 0 deletions packages/vite/src/node/plugins/resolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
20 changes: 20 additions & 0 deletions packages/vite/src/node/ssr/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,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

/**
* Control over which dependencies are optimized during SSR and esbuild options
* During build:
Expand All @@ -22,6 +24,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 {
Expand Down
3 changes: 3 additions & 0 deletions packages/vite/src/node/ssr/ssrExternal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,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 = (
Expand Down
8 changes: 7 additions & 1 deletion packages/vite/src/node/ssr/ssrModuleLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -281,6 +285,8 @@ async function nodeImport(
? { ...resolveOptions, tryEsmOnly: true }
: resolveOptions,
false,
undefined,
true,
)
if (!resolved) {
const err: any = new Error(
Expand Down
35 changes: 35 additions & 0 deletions playground/ssr-conditions/__tests__/serve.ts
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
27 changes: 27 additions & 0 deletions playground/ssr-conditions/__tests__/ssr-conditions.spec.ts
Original file line number Diff line number Diff line change
@@ -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',
)
})
1 change: 1 addition & 0 deletions playground/ssr-conditions/external/browser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default 'browser.js'
1 change: 1 addition & 0 deletions playground/ssr-conditions/external/default.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default 'default.js'
1 change: 1 addition & 0 deletions playground/ssr-conditions/external/edge.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default 'edge.js'
1 change: 1 addition & 0 deletions playground/ssr-conditions/external/node.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default 'node.js'
1 change: 1 addition & 0 deletions playground/ssr-conditions/external/node.unbundled.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default 'node.unbundled.js'
21 changes: 21 additions & 0 deletions playground/ssr-conditions/external/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
29 changes: 29 additions & 0 deletions playground/ssr-conditions/index.html
Original file line number Diff line number Diff line change
@@ -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>
1 change: 1 addition & 0 deletions playground/ssr-conditions/no-external/browser.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default 'browser.js'
1 change: 1 addition & 0 deletions playground/ssr-conditions/no-external/default.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default 'default.js'
1 change: 1 addition & 0 deletions playground/ssr-conditions/no-external/edge.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default 'edge.js'
1 change: 1 addition & 0 deletions playground/ssr-conditions/no-external/node.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default 'node.js'
1 change: 1 addition & 0 deletions playground/ssr-conditions/no-external/node.unbundled.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default 'node.unbundled.js'
21 changes: 21 additions & 0 deletions playground/ssr-conditions/no-external/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
18 changes: 18 additions & 0 deletions playground/ssr-conditions/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
70 changes: 70 additions & 0 deletions playground/ssr-conditions/server.js
Original file line number Diff line number Diff line change
@@ -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')
}),
)
}
Loading

0 comments on commit d0afc39

Please sign in to comment.