/
ssrExternal.ts
141 lines (131 loc) · 3.69 KB
/
ssrExternal.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
import fs from 'fs'
import path from 'path'
import { tryNodeResolve, InternalResolveOptions } from '../plugins/resolve'
import { isDefined, lookupFile, resolveFrom, unique } from '../utils'
import { ResolvedConfig } from '..'
/**
* 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) {
externals = externals.filter((id) => !config.ssr!.noExternal!.includes(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]
}
}