diff --git a/packages/vite/src/node/external.ts b/packages/vite/src/node/external.ts index 1fef5113a2612f..e55eabb7f4ce7f 100644 --- a/packages/vite/src/node/external.ts +++ b/packages/vite/src/node/external.ts @@ -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, diff --git a/packages/vite/src/node/optimizer/scan.ts b/packages/vite/src/node/optimizer/scan.ts index 4eb6dd883b891a..c8a3020d2b34cd 100644 --- a/packages/vite/src/node/optimizer/scan.ts +++ b/packages/vite/src/node/optimizer/scan.ts @@ -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.') } diff --git a/packages/vite/src/node/plugins/importAnalysis.ts b/packages/vite/src/node/plugins/importAnalysis.ts index bf74811cd3de73..a7a77968ea5550 100644 --- a/packages/vite/src/node/plugins/importAnalysis.ts +++ b/packages/vite/src/node/plugins/importAnalysis.ts @@ -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 diff --git a/packages/vite/src/node/plugins/resolve.ts b/packages/vite/src/node/plugins/resolve.ts index a51b7c20a0c047..f43bda309cbd37 100644 --- a/packages/vite/src/node/plugins/resolve.ts +++ b/packages/vite/src/node/plugins/resolve.ts @@ -23,7 +23,6 @@ import { isBuiltin, isDataUrl, isExternalUrl, - isFilePathESM, isInNodeModules, isNonDriveRelativeAbsolutePath, isObject, @@ -444,7 +443,6 @@ export function resolvePlugin( importer, options, depsOptimizer, - ssr, external, undefined, depsOptimizerOptions, @@ -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, @@ -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 = @@ -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 { @@ -1222,7 +1209,6 @@ function tryResolveBrowserMapping( undefined, undefined, undefined, - undefined, depsOptimizerOptions, )?.id : tryFsResolve(path.join(pkg.dir, browserMappedPath), options)) diff --git a/packages/vite/src/node/server/environment.ts b/packages/vite/src/node/server/environment.ts index 8b7da8bb6f47e7..722cf963c1fc34 100644 --- a/packages/vite/src/node/server/environment.ts +++ b/packages/vite/src/node/server/environment.ts @@ -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) diff --git a/packages/vite/src/node/ssr/fetchModule.ts b/packages/vite/src/node/ssr/fetchModule.ts index 70b866ed1834a4..b517a0febea3c2 100644 --- a/packages/vite/src/node/ssr/fetchModule.ts +++ b/packages/vite/src/node/ssr/fetchModule.ts @@ -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}'`, diff --git a/packages/vite/src/node/ssr/index.ts b/packages/vite/src/node/ssr/index.ts index 975aa26aaa3976..dc597ea41a3b04 100644 --- a/packages/vite/src/node/ssr/index.ts +++ b/packages/vite/src/node/ssr/index.ts @@ -62,7 +62,6 @@ export function resolveSSROptions( ...ssr, optimizeDeps: { ...optimizeDeps, - noDiscovery: true, // always true for ssr esbuildOptions: { preserveSymlinks, ...optimizeDeps.esbuildOptions, diff --git a/playground/environment-react-ssr/__tests__/basic.spec.ts b/playground/environment-react-ssr/__tests__/basic.spec.ts deleted file mode 100644 index 4b98b37a2394f7..00000000000000 --- a/playground/environment-react-ssr/__tests__/basic.spec.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { test } from 'vitest' -import { page } 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() -}) diff --git a/playground/environment-react-ssr/__tests__/environment-react-ssr.spec.ts b/playground/environment-react-ssr/__tests__/environment-react-ssr.spec.ts new file mode 100644 index 00000000000000..bc9c716ffb097b --- /dev/null +++ b/playground/environment-react-ssr/__tests__/environment-react-ssr.spec.ts @@ -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() + }) +}) diff --git a/playground/environment-react-ssr/package.json b/playground/environment-react-ssr/package.json index 44910cc40687dc..14473dd31d72c0 100644 --- a/playground/environment-react-ssr/package.json +++ b/playground/environment-react-ssr/package.json @@ -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" } } diff --git a/playground/environment-react-ssr/vite.config.ts b/playground/environment-react-ssr/vite.config.ts index 05d667b5087755..0d14c778f0829e 100644 --- a/playground/environment-react-ssr/vite.config.ts +++ b/playground/environment-react-ssr/vite.config.ts @@ -21,6 +21,9 @@ export default defineConfig((env) => ({ }, }, ], + resolve: { + noExternal: true, + }, environments: { client: { build: { @@ -30,6 +33,11 @@ export default defineConfig((env) => ({ }, }, ssr: { + dev: { + optimizeDeps: { + noDiscovery: false, + }, + }, build: { outDir: 'dist/server', // [feedback] diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a0f1a57f33959f..31e590d93219a5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -698,6 +698,12 @@ importers: react-dom: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) + react-fake-client: + specifier: npm:react@^18.3.1 + version: react@18.3.1 + react-fake-server: + specifier: npm:react@^18.3.1 + version: react@18.3.1 playground/extensions: dependencies: @@ -7123,8 +7129,8 @@ packages: zimmerframe@1.0.0: resolution: {integrity: sha512-H6qQ6LtjP+kDQwDgol18fPi4OCo7F+73ZBYt2U9c1D3V74bIMKxXvyrN0x+1I7/RYh5YsausflQxQR/qwDLHPQ==} - zod@3.22.4: - resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==} + zod@3.23.8: + resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -11301,7 +11307,7 @@ snapshots: workerd: 1.20241011.1 ws: 8.18.0 youch: 3.2.3 - zod: 3.22.4 + zod: 3.23.8 transitivePeerDependencies: - bufferutil - supports-color @@ -12908,6 +12914,6 @@ snapshots: zimmerframe@1.0.0: {} - zod@3.22.4: {} + zod@3.23.8: {} zwitch@2.0.4: {}