Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ssr): backport ssr.resolve.conditions and ssr.resolve.externalConditions (#14498) #14668

Merged
merged 1 commit into from Oct 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line can be thought as a breaking change (previously resolve.conditions were used, but now ssr.resolve.externalConditions will be used). But I guess it wouldn't cause any problems.

}

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')
}),
)
}