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: enable dependencies discovery and pre-bundling in ssr environments #18358

Merged
merged 4 commits into from
Oct 23, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 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 Expand Up @@ -133,6 +132,14 @@ export function createIsConfiguredAsExternal(
if (noExternalFilter && !noExternalFilter(pkgName)) {
return false
}
if (
!environment.config.dev.optimizeDeps.noDiscovery &&
!environment.config.dev.optimizeDeps.exclude?.includes(id)
) {
// If there is server side pre-bundling and the module is not
// in the `exclude` config then it is not external
return false
}
// If external is true, all will be externalized by default, regardless if
// it's a linked package
return isExternalizable(id, importer, external === true)
Expand Down
25 changes: 21 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,30 @@ 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 isRelative = ['./', '../', '/'].some((prefix) =>
p.startsWith(prefix),
)

if (!isRelative) {
// We want to make sure that the path looks like a relative one since a bare import
// path doesn't really make sense as an entry and it would also create issues during
// module resolution
p = `/${p}`
}

const id = (await environment.pluginContainer.resolveId(p))?.id
if (id === undefined) {
throw new Error('failed to resolve rollupOptions.input value.')
}
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"
}
}
5 changes: 5 additions & 0 deletions playground/environment-react-ssr/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,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.