vite
Version:
Native-ESM powered web dev build tool
145 lines (135 loc) • 3.9 kB
text/typescript
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]
}
}