Skip to content

Commit

Permalink
feat: enable dependencies discovery and pre-bundling in ssr environme…
Browse files Browse the repository at this point in the history
…nts (#18358)

Co-authored-by: 翠 / green <[email protected]>
  • Loading branch information
dario-piotrowicz and sapphi-red authored Oct 23, 2024
1 parent 291830f commit 9b21f69
Show file tree
Hide file tree
Showing 12 changed files with 147 additions and 68 deletions.
1 change: 0 additions & 1 deletion packages/vite/src/node/external.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,6 @@ export function createIsConfiguredAsExternal(
config.command === 'build' ? undefined : importer,
resolveOptions,
undefined,
true,
// try to externalize, will return undefined or an object without
// a external flag if it isn't externalizable
true,
Expand Down
20 changes: 16 additions & 4 deletions packages/vite/src/node/optimizer/scan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,13 +253,25 @@ async function computeEntries(environment: ScanEnvironment) {
if (explicitEntryPatterns) {
entries = await globEntries(explicitEntryPatterns, environment)
} else if (buildInput) {
const resolvePath = (p: string) => path.resolve(environment.config.root, p)
const resolvePath = async (p: string) => {
const id = (
await environment.pluginContainer.resolveId(p, undefined, {
scan: true,
})
)?.id
if (id === undefined) {
throw new Error(
`failed to resolve rollupOptions.input value: ${JSON.stringify(p)}.`,
)
}
return id
}
if (typeof buildInput === 'string') {
entries = [resolvePath(buildInput)]
entries = [await resolvePath(buildInput)]
} else if (Array.isArray(buildInput)) {
entries = buildInput.map(resolvePath)
entries = await Promise.all(buildInput.map(resolvePath))
} else if (isObject(buildInput)) {
entries = Object.values(buildInput).map(resolvePath)
entries = await Promise.all(Object.values(buildInput).map(resolvePath))
} else {
throw new Error('invalid rollupOptions.input value.')
}
Expand Down
2 changes: 1 addition & 1 deletion packages/vite/src/node/plugins/importAnalysis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -512,7 +512,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
if (isExternalUrl(specifier) || isDataUrl(specifier)) {
return
}
// skip ssr external
// skip ssr externals and builtins
if (ssr && !matchAlias(specifier)) {
if (shouldExternalize(environment, specifier, importer)) {
return
Expand Down
16 changes: 1 addition & 15 deletions packages/vite/src/node/plugins/resolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import {
isBuiltin,
isDataUrl,
isExternalUrl,
isFilePathESM,
isInNodeModules,
isNonDriveRelativeAbsolutePath,
isObject,
Expand Down Expand Up @@ -444,7 +443,6 @@ export function resolvePlugin(
importer,
options,
depsOptimizer,
ssr,
external,
undefined,
depsOptimizerOptions,
Expand Down Expand Up @@ -746,7 +744,6 @@ export function tryNodeResolve(
importer: string | null | undefined,
options: InternalResolveOptionsWithOverrideConditions,
depsOptimizer?: DepsOptimizer,
ssr: boolean = false,
externalize?: boolean,
allowLinkedExternal: boolean = true,
depsOptimizerOptions?: DepOptimizationOptions,
Expand Down Expand Up @@ -880,11 +877,9 @@ export function tryNodeResolve(
: OPTIMIZABLE_ENTRY_RE.test(resolved)

let exclude = depsOptimizer?.options.exclude
let include = depsOptimizer?.options.include
if (options.ssrOptimizeCheck) {
// we don't have the depsOptimizer
exclude = depsOptimizerOptions?.exclude
include = depsOptimizerOptions?.include
}

const skipOptimization =
Expand All @@ -893,15 +888,7 @@ export function tryNodeResolve(
(importer && isInNodeModules(importer)) ||
exclude?.includes(pkgId) ||
exclude?.includes(id) ||
SPECIAL_QUERY_RE.test(resolved) ||
// During dev SSR, we don't have a way to reload the module graph if
// a non-optimized dep is found. So we need to skip optimization here.
// The only optimized deps are the ones explicitly listed in the config.
(!options.ssrOptimizeCheck && !isBuild && ssr) ||
// Only optimize non-external CJS deps during SSR by default
(ssr &&
isFilePathESM(resolved, options.packageCache) &&
!(include?.includes(pkgId) || include?.includes(id)))
SPECIAL_QUERY_RE.test(resolved)

if (options.ssrOptimizeCheck) {
return {
Expand Down Expand Up @@ -1222,7 +1209,6 @@ function tryResolveBrowserMapping(
undefined,
undefined,
undefined,
undefined,
depsOptimizerOptions,
)?.id
: tryFsResolve(path.join(pkg.dir, browserMappedPath), options))
Expand Down
8 changes: 1 addition & 7 deletions packages/vite/src/node/server/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,14 +137,8 @@ export class DevEnvironment extends BaseEnvironment {
} else if (isDepOptimizationDisabled(optimizeDeps)) {
this.depsOptimizer = undefined
} else {
// We only support auto-discovery for the client environment, for all other
// environments `noDiscovery` has no effect and a simpler explicit deps
// optimizer is used that only optimizes explicitly included dependencies
// so it doesn't need to reload the environment. Now that we have proper HMR
// and full reload for general environments, we can enable auto-discovery for
// them in the future
this.depsOptimizer = (
optimizeDeps.noDiscovery || options.consumer !== 'client'
optimizeDeps.noDiscovery
? createExplicitDepsOptimizer
: createDepsOptimizer
)(this)
Expand Down
42 changes: 16 additions & 26 deletions packages/vite/src/node/ssr/fetchModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,32 +44,22 @@ export async function fetchModule(
const { externalConditions, dedupe, preserveSymlinks } =
environment.config.resolve

const resolved = tryNodeResolve(
url,
importer,
{
mainFields: ['main'],
conditions: [],
externalConditions,
external: [],
noExternal: [],
overrideConditions: [
...externalConditions,
'production',
'development',
],
extensions: ['.js', '.cjs', '.json'],
dedupe,
preserveSymlinks,
isBuild: false,
isProduction,
root,
packageCache: environment.config.packageCache,
webCompatible: environment.config.webCompatible,
},
undefined,
true,
)
const resolved = tryNodeResolve(url, importer, {
mainFields: ['main'],
conditions: [],
externalConditions,
external: [],
noExternal: [],
overrideConditions: [...externalConditions, 'production', 'development'],
extensions: ['.js', '.cjs', '.json'],
dedupe,
preserveSymlinks,
isBuild: false,
isProduction,
root,
packageCache: environment.config.packageCache,
webCompatible: environment.config.webCompatible,
})
if (!resolved) {
const err: any = new Error(
`Cannot find module '${url}' imported from '${importer}'`,
Expand Down
1 change: 0 additions & 1 deletion packages/vite/src/node/ssr/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ export function resolveSSROptions(
...ssr,
optimizeDeps: {
...optimizeDeps,
noDiscovery: true, // always true for ssr
esbuildOptions: {
preserveSymlinks,
...optimizeDeps.esbuildOptions,
Expand Down
9 changes: 0 additions & 9 deletions playground/environment-react-ssr/__tests__/basic.spec.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import fs from 'node:fs'
import path from 'node:path'
import { describe, expect, onTestFinished, test } from 'vitest'
import type { DepOptimizationMetadata } from 'vite'
import {
isBuild,
page,
readFile,
serverLogs,
testDir,
untilUpdated,
} from '~utils'

test('basic', async () => {
await page.getByText('hydrated: true').isVisible()
await page.getByText('Count: 0').isVisible()
await page.getByRole('button', { name: '+' }).click()
await page.getByText('Count: 1').isVisible()
})

describe.runIf(!isBuild)('pre-bundling', () => {
test('client', async () => {
const meta = await readFile('node_modules/.vite/deps/_metadata.json')
const metaJson: DepOptimizationMetadata = JSON.parse(meta)

expect(metaJson.optimized['react']).toBeTruthy()
expect(metaJson.optimized['react-dom/client']).toBeTruthy()
expect(metaJson.optimized['react/jsx-dev-runtime']).toBeTruthy()

expect(metaJson.optimized['react-dom/server']).toBeFalsy()
})

test('ssr', async () => {
const meta = await readFile('node_modules/.vite/deps_ssr/_metadata.json')
const metaJson: DepOptimizationMetadata = JSON.parse(meta)

expect(metaJson.optimized['react']).toBeTruthy()
expect(metaJson.optimized['react-dom/server']).toBeTruthy()
expect(metaJson.optimized['react/jsx-dev-runtime']).toBeTruthy()

expect(metaJson.optimized['react-dom/client']).toBeFalsy()
})

test('deps reload', async () => {
const envs = ['client', 'server'] as const

const getMeta = (env: (typeof envs)[number]): DepOptimizationMetadata => {
const meta = readFile(
`node_modules/.vite/deps${env === 'client' ? '' : '_ssr'}/_metadata.json`,
)
return JSON.parse(meta)
}

expect(getMeta('client').optimized['react-fake-client']).toBeFalsy()
expect(getMeta('client').optimized['react-fake-server']).toBeFalsy()
expect(getMeta('server').optimized['react-fake-server']).toBeFalsy()
expect(getMeta('server').optimized['react-fake-client']).toBeFalsy()

envs.forEach((env) => {
const filePath = path.resolve(testDir, `src/entry-${env}.tsx`)
const originalContent = readFile(filePath)
fs.writeFileSync(
filePath,
`import 'react-fake-${env}'\n${originalContent}`,
'utf-8',
)
onTestFinished(() => {
fs.writeFileSync(filePath, originalContent, 'utf-8')
})
})

await untilUpdated(
() =>
serverLogs
.map(
(log) =>
log
// eslint-disable-next-line no-control-regex
.replace(/\x1B\[\d+m/g, '')
.match(/new dependencies optimized: (react-fake-.*)/)?.[1],
)
.filter(Boolean)
.join(', '),
'react-fake-server, react-fake-client',
)

expect(getMeta('client').optimized['react-fake-client']).toBeTruthy()
expect(getMeta('client').optimized['react-fake-server']).toBeFalsy()
expect(getMeta('server').optimized['react-fake-server']).toBeTruthy()
expect(getMeta('server').optimized['react-fake-client']).toBeFalsy()
})
})
2 changes: 2 additions & 0 deletions playground/environment-react-ssr/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
"@types/react": "^18.3.11",
"@types/react-dom": "^18.3.1",
"react": "^18.3.1",
"react-fake-client": "npm:react@^18.3.1",
"react-fake-server": "npm:react@^18.3.1",
"react-dom": "^18.3.1"
}
}
8 changes: 8 additions & 0 deletions playground/environment-react-ssr/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ export default defineConfig((env) => ({
},
},
],
resolve: {
noExternal: true,
},
environments: {
client: {
build: {
Expand All @@ -30,6 +33,11 @@ export default defineConfig((env) => ({
},
},
ssr: {
dev: {
optimizeDeps: {
noDiscovery: false,
},
},
build: {
outDir: 'dist/server',
// [feedback]
Expand Down
14 changes: 10 additions & 4 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 9b21f69

Please sign in to comment.