Skip to content

Commit

Permalink
feat(ssr): backport ssr.resolve.conditions and ssr.resolve.externalCo…
Browse files Browse the repository at this point in the history
…nditions (#14498) (#14668)

Co-authored-by: Marc MacLeod <marbemac@gmail.com>
  • Loading branch information
patak-dev and Marc MacLeod committed Oct 18, 2023
1 parent ad7466c commit 520139c
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
Expand Up @@ -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.
4 changes: 4 additions & 0 deletions docs/guide/ssr.md
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
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
Expand Up @@ -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
Expand All @@ -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 {
Expand Down
3 changes: 3 additions & 0 deletions packages/vite/src/node/ssr/ssrExternal.ts
Expand Up @@ -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 = (
Expand Down
8 changes: 7 additions & 1 deletion packages/vite/src/node/ssr/ssrModuleLoader.ts
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
@@ -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
@@ -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
@@ -0,0 +1 @@
export default 'browser.js'
1 change: 1 addition & 0 deletions playground/ssr-conditions/external/default.js
@@ -0,0 +1 @@
export default 'default.js'
1 change: 1 addition & 0 deletions playground/ssr-conditions/external/edge.js
@@ -0,0 +1 @@
export default 'edge.js'
1 change: 1 addition & 0 deletions playground/ssr-conditions/external/node.js
@@ -0,0 +1 @@
export default 'node.js'
1 change: 1 addition & 0 deletions playground/ssr-conditions/external/node.unbundled.js
@@ -0,0 +1 @@
export default 'node.unbundled.js'
21 changes: 21 additions & 0 deletions playground/ssr-conditions/external/package.json
@@ -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
@@ -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
@@ -0,0 +1 @@
export default 'browser.js'
1 change: 1 addition & 0 deletions playground/ssr-conditions/no-external/default.js
@@ -0,0 +1 @@
export default 'default.js'
1 change: 1 addition & 0 deletions playground/ssr-conditions/no-external/edge.js
@@ -0,0 +1 @@
export default 'edge.js'
1 change: 1 addition & 0 deletions playground/ssr-conditions/no-external/node.js
@@ -0,0 +1 @@
export default 'node.js'
1 change: 1 addition & 0 deletions playground/ssr-conditions/no-external/node.unbundled.js
@@ -0,0 +1 @@
export default 'node.unbundled.js'
21 changes: 21 additions & 0 deletions playground/ssr-conditions/no-external/package.json
@@ -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
@@ -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
@@ -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')
}),
)
}

0 comments on commit 520139c

Please sign in to comment.