UNPKG

vite

Version:

Native-ESM powered web dev build tool

145 lines (135 loc) 3.9 kB
import fs from 'fs' import path from 'path' import { tryNodeResolve, InternalResolveOptions } from '../plugins/resolve' import { isDefined, lookupFile, resolveFrom, unique } from '../utils' import { ResolvedConfig } from '..' import { createFilter } from '@rollup/pluginutils' /** * Heuristics for determining whether a dependency should be externalized for * server-side rendering. * * TODO right now externals are imported using require(), we probably need to * rework this when more libraries ship native ESM distributions for Node. */ export function resolveSSRExternal( config: ResolvedConfig, knownImports: string[], ssrExternals: Set<string> = new Set(), seen: Set<string> = new Set() ): string[] { const { root } = config const pkgContent = lookupFile(root, ['package.json']) if (!pkgContent) { return [] } const pkg = JSON.parse(pkgContent) const devDeps = Object.keys(pkg.devDependencies || {}) const importedDeps = knownImports.map(getNpmPackageName).filter(isDefined) const deps = unique([...importedDeps, ...Object.keys(pkg.dependencies || {})]) for (const id of devDeps) { ssrExternals.add(id) seen.add(id) } const resolveOptions: InternalResolveOptions = { root, isProduction: false, isBuild: true } const depsToTrace = new Set<string>() for (const id of deps) { if (seen.has(id)) { continue } seen.add(id) let entry let requireEntry try { entry = tryNodeResolve( id, undefined, resolveOptions, true, undefined, true )?.id requireEntry = require.resolve(id, { paths: [root] }) } catch (e) { // resolve failed, assume include continue } if (!entry) { // no esm entry but has require entry (is this even possible?) ssrExternals.add(id) continue } if (!entry.includes('node_modules')) { // entry is not a node dep, possibly linked - don't externalize // instead, trace its dependencies. depsToTrace.add(id) continue } if (entry !== requireEntry) { // has separate esm/require entry, assume require entry is cjs ssrExternals.add(id) } else { // node resolve and esm resolve resolves to the same file. if (!/\.m?js$/.test(entry)) { // entry is not js, cannot externalize continue } // check if the entry is cjs const content = fs.readFileSync(entry, 'utf-8') if (/\bmodule\.exports\b|\bexports[.\[]|\brequire\s*\(/.test(content)) { ssrExternals.add(id) } } } for (const id of depsToTrace) { const depRoot = path.dirname(resolveFrom(`${id}/package.json`, root)) resolveSSRExternal( { ...config, root: depRoot }, knownImports, ssrExternals, seen ) } if (config.ssr?.external) { config.ssr.external.forEach((id) => ssrExternals.add(id)) } let externals = [...ssrExternals] if (config.ssr?.noExternal) { const filter = createFilter(undefined, config.ssr.noExternal, { resolve: false }) externals = externals.filter((id) => filter(id)) } return externals.filter((id) => id !== 'vite') } export function shouldExternalizeForSSR( id: string, externals: string[] ): boolean { const should = externals.some((e) => { if (id === e) { return true } // deep imports, check ext before externalizing - only externalize // extension-less imports and explicit .js imports if (id.startsWith(e + '/') && (!path.extname(id) || id.endsWith('.js'))) { return true } }) return should } function getNpmPackageName(importPath: string): string | null { const parts = importPath.split('/') if (parts[0].startsWith('@')) { if (!parts[1]) return null return `${parts[0]}/${parts[1]}` } else { return parts[0] } }