diff --git a/packages/plugin-vue/src/index.ts b/packages/plugin-vue/src/index.ts index 240d49a064537a..31dc332e13c961 100644 --- a/packages/plugin-vue/src/index.ts +++ b/packages/plugin-vue/src/index.ts @@ -77,8 +77,7 @@ export default function vuePlugin(rawOptions: Options = {}): Plugin { define: { __VUE_OPTIONS_API__: true, __VUE_PROD_DEVTOOLS__: false - }, - ssrExternal: ['vue', '@vue/server-renderer'] + } } }, diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index 39c7354ca52445..85ec88f9619968 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -28,6 +28,7 @@ import { TransformOptions } from 'esbuild' import { CleanCSS } from 'types/clean-css' import { dataURIPlugin } from './plugins/dataUri' import { buildImportAnalysisPlugin } from './plugins/importAnaysisBuild' +import { resolveSSRExternal } from './ssrExternal' export interface BuildOptions { /** @@ -290,10 +291,9 @@ async function doBuild( // inject ssrExternal if present const userExternal = options.rollupOptions?.external - const external = - options.ssr && config.ssrExternal - ? resolveExternal(config.ssrExternal, userExternal) - : userExternal + const external = options.ssr + ? resolveExternal(resolveSSRExternal(config.root), userExternal) + : userExternal const rollup = require('rollup') as typeof Rollup diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index a7d77b8bdd90dd..6eed677aa9638f 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -107,11 +107,6 @@ export interface UserConfig { * Default: true */ clearScreen?: boolean - /** - * Externalize deps for SSR. These deps must provide a CommonJS build that - * can be `required()` and has the same module signature as its ESM build. - */ - ssrExternal?: string[] } export interface InlineConfig extends UserConfig { diff --git a/packages/vite/src/node/plugins/importAnalysis.ts b/packages/vite/src/node/plugins/importAnalysis.ts index 54bb34464a0f68..8bc424c2d54f10 100644 --- a/packages/vite/src/node/plugins/importAnalysis.ts +++ b/packages/vite/src/node/plugins/importAnalysis.ts @@ -294,7 +294,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { continue } // skip ssr external - if (ssr && config.ssrExternal?.includes(url)) { + if (ssr && server._ssrExternals?.includes(url)) { continue } // skip client @@ -459,7 +459,10 @@ function isSupportedDynamicImport(url: string) { function isOptimizedCjs( id: string, - { optimizeDepsMetadata, config: { optimizeCacheDir } }: ViteDevServer + { + _optimizeDepsMetadata: optimizeDepsMetadata, + config: { optimizeCacheDir } + }: ViteDevServer ): boolean { if (optimizeDepsMetadata && optimizeCacheDir) { const relative = path.relative(optimizeCacheDir, cleanUrl(id)) diff --git a/packages/vite/src/node/plugins/resolve.ts b/packages/vite/src/node/plugins/resolve.ts index b649066eded9d2..05df6a70270255 100644 --- a/packages/vite/src/node/plugins/resolve.ts +++ b/packages/vite/src/node/plugins/resolve.ts @@ -275,8 +275,8 @@ export function tryNodeResolve( if ( deepMatch && server && - server.optimizeDepsMetadata && - pkg.data.name in server.optimizeDepsMetadata.map && + server._optimizeDepsMetadata && + pkg.data.name in server._optimizeDepsMetadata.map && !isCSSRequest(id) && !server.config.assetsInclude(id) ) { @@ -316,7 +316,7 @@ export function tryNodeResolve( // files actually inside node_modules so that locally linked packages // in monorepos are not cached this way. if (resolved.includes('node_modules')) { - const versionHash = server?.optimizeDepsMetadata?.hash + const versionHash = server?._optimizeDepsMetadata?.hash if (versionHash) { resolved = injectQuery(resolved, `v=${versionHash}`) } @@ -330,7 +330,7 @@ export function tryOptimizedResolve( server: ViteDevServer ): string | undefined { const cacheDir = server.config.optimizeCacheDir - const depData = server.optimizeDepsMetadata + const depData = server._optimizeDepsMetadata if (cacheDir && depData) { const [id, q] = rawId.split(`?`, 2) const query = q ? `?${q}` : `` diff --git a/packages/vite/src/node/server/index.ts b/packages/vite/src/node/server/index.ts index 17689893ebdba1..52929b7c663a2a 100644 --- a/packages/vite/src/node/server/index.ts +++ b/packages/vite/src/node/server/index.ts @@ -42,6 +42,7 @@ import { import { TransformOptions as EsbuildTransformOptions } from 'esbuild' import { DepOptimizationMetadata, optimizeDeps } from '../optimizer' import { ssrLoadModule } from './ssrModuleLoader' +import { resolveSSRExternal } from '../ssrExternal' export interface ServerOptions { host?: string @@ -197,7 +198,12 @@ export interface ViteDevServer { /** * @internal */ - optimizeDepsMetadata: DepOptimizationMetadata | null + _optimizeDepsMetadata: DepOptimizationMetadata | null + /** + * Deps that are extenralized + * @internal + */ + _ssrExternals: string[] | null } export async function createServer( @@ -239,12 +245,14 @@ export async function createServer( pluginContainer: container, ws, moduleGraph, - optimizeDepsMetadata: null, transformWithEsbuild, transformRequest(url, options) { return transformRequest(url, server, options) }, ssrLoadModule(url) { + if (!server._ssrExternals) { + server._ssrExternals = resolveSSRExternal(config.root) + } return ssrLoadModule(url, server) }, listen(port?: number) { @@ -257,7 +265,9 @@ export async function createServer( container.close(), closeHttpServer() ]) - } + }, + _optimizeDepsMetadata: null, + _ssrExternals: null } process.once('SIGTERM', async () => { @@ -373,7 +383,7 @@ export async function createServer( // after optimization, read updated optimization metadata const dataPath = path.resolve(config.optimizeCacheDir, 'metadata.json') if (fs.existsSync(dataPath)) { - server.optimizeDepsMetadata = JSON.parse( + server._optimizeDepsMetadata = JSON.parse( fs.readFileSync(dataPath, 'utf-8') ) } diff --git a/packages/vite/src/node/ssrExternal.ts b/packages/vite/src/node/ssrExternal.ts new file mode 100644 index 00000000000000..423dcbd5b17903 --- /dev/null +++ b/packages/vite/src/node/ssrExternal.ts @@ -0,0 +1,56 @@ +import fs from 'fs' +import path from 'path' +import { tryNodeResolve } from './plugins/resolve' +import { lookupFile, resolveFrom } from './utils' + +/** + * Heuristics for determining whether a dependency should be externalized for + * server-side rendering. + */ +export function resolveSSRExternal(root: string): string[] { + const pkgContent = lookupFile(root, ['package.json']) + if (!pkgContent) { + return [] + } + const pkg = JSON.parse(pkgContent) + const ssrExternals = Object.keys(pkg.devDependencies || {}) + const deps = Object.keys(pkg.dependencies || {}) + for (const dep of deps) { + const entry = tryNodeResolve(dep, root, false)?.id + let requireEntry + try { + requireEntry = require.resolve(dep, { paths: [root] }) + } catch (e) { + continue + } + if (!entry) { + // no esm entry but has require entry (is this even possible?) + ssrExternals.push(dep) + continue + } + // node resolve and esm resolve resolves to the same file + if (path.extname(entry) !== '.js') { + // entry is not js, cannot externalize + continue + } + if (!entry.includes('node_modules')) { + // entry is not a node dep, possibly linked - don't externalize + // instead, trace its dependencies. + const depRoot = path.dirname(resolveFrom(`${dep}/package.json`, root)) + ssrExternals.push(...resolveSSRExternal(depRoot)) + continue + } + if (entry !== requireEntry) { + // has separate esm/require entry, assume require entry is cjs + ssrExternals.push(dep) + } else { + // node resolve and esm resolve resolves to the same file. + // check if the entry is cjs + const content = fs.readFileSync(entry, 'utf-8') + if (/\bmodule\.exports\b|\bexports[.\[]|\brequire\s*\(/.test(content)) { + ssrExternals.push(dep) + } + } + } + return [...new Set(ssrExternals)] +}