-
Notifications
You must be signed in to change notification settings - Fork 2
/
index.js
270 lines (248 loc) · 9.48 KB
/
index.js
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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
import fs from 'node:fs/promises'
import fsSync from 'node:fs'
import path from 'node:path'
import {
isDepIncluded,
isDepExcluded,
isDepNoExternaled,
isDepExternaled
} from './sync.cjs'
/** @type {import('pnpapi')} */
let pnp
if (process.versions.pnp) {
try {
const { createRequire } = (await import('module')).default
pnp = createRequire(import.meta.url)('pnpapi')
} catch {}
}
export { isDepIncluded, isDepExcluded, isDepNoExternaled, isDepExternaled }
/** @type {import('..').crawlFrameworkPkgs} */
export async function crawlFrameworkPkgs(options) {
const pkgJsonPath = await findClosestPkgJsonPath(options.root)
if (!pkgJsonPath) {
// @ts-expect-error don't throw in deno as package.json is not required
if (typeof Deno !== 'undefined') {
return {
optimizeDeps: { include: [], exclude: [] },
ssr: { noExternal: [], external: [] }
}
} else {
throw new Error(`Cannot find package.json from ${options.root}`)
}
}
const pkgJson = await readJson(pkgJsonPath).catch((e) => {
throw new Error(`Unable to read ${pkgJsonPath}`, { cause: e })
})
/** @type {string[]} */
let optimizeDepsInclude = []
/** @type {string[]} */
let optimizeDepsExclude = []
/** @type {string[]} */
let ssrNoExternal = []
/** @type {string[]} */
let ssrExternal = []
await crawl(pkgJsonPath, pkgJson)
// respect vite user config
if (options.viteUserConfig) {
// remove includes that are explicitly excluded in optimizeDeps
const _optimizeDepsExclude = options.viteUserConfig?.optimizeDeps?.exclude
if (_optimizeDepsExclude) {
optimizeDepsInclude = optimizeDepsInclude.filter(
(dep) => !isDepExcluded(dep, _optimizeDepsExclude)
)
}
// remove excludes that are explicitly included in optimizeDeps
const _optimizeDepsInclude = options.viteUserConfig?.optimizeDeps?.include
if (_optimizeDepsInclude) {
optimizeDepsExclude = optimizeDepsExclude.filter(
(dep) => !isDepIncluded(dep, _optimizeDepsInclude)
)
}
// remove noExternals that are explicitly externalized
const _ssrExternal = options.viteUserConfig?.ssr?.external
if (_ssrExternal) {
ssrNoExternal = ssrNoExternal.filter(
(dep) => !isDepExternaled(dep, _ssrExternal)
)
}
// remove externals that are explicitly noExternal
const _ssrNoExternal = options.viteUserConfig?.ssr?.noExternal
if (_ssrNoExternal) {
ssrExternal = ssrExternal.filter(
(dep) => !isDepNoExternaled(dep, _ssrNoExternal)
)
}
}
return {
optimizeDeps: {
include: optimizeDepsInclude,
exclude: optimizeDepsExclude
},
ssr: {
noExternal: ssrNoExternal,
external: ssrExternal
}
}
/**
* crawl the package.json dependencies for framework packages. rules:
* 1. a framework package should be `optimizeDeps.exclude` and `ssr.noExternal`.
* 2. the deps of the framework package should be `optimizeDeps.include` and `ssr.external`
* unless the dep is also a framework package, in which case do no1 & no2 recursively.
* 3. any non-framework packages that aren't imported by a framework package can be skipped entirely.
* 4. a semi-framework package is like a framework package, except it isn't `optimizeDeps.exclude`,
* but only applies `ssr.noExternal`.
* @param {string} pkgJsonPath
* @param {Record<string, any>} pkgJson
* @param {string[]} [parentDepNames]
*/
async function crawl(pkgJsonPath, pkgJson, parentDepNames = []) {
const isRoot = parentDepNames.length === 0
/** @type {string[]} */
let deps = [
...Object.keys(pkgJson.dependencies || {}),
...(isRoot ? Object.keys(pkgJson.devDependencies || {}) : [])
]
deps = deps.filter((dep) => {
// skip circular deps
if (parentDepNames.includes(dep)) {
return false
}
const isFrameworkPkg = options.isFrameworkPkgByName?.(dep)
const isSemiFrameworkPkg = options.isSemiFrameworkPkgByName?.(dep)
if (isFrameworkPkg) {
// framework packages should be excluded from optimization as esbuild can't handle them.
// otherwise it'll cause https://github.com/vitejs/vite/issues/3910
optimizeDepsExclude.push(dep)
// framework packages should be noExternal so that they go through vite's transformation
// pipeline, since nodejs can't support them.
ssrNoExternal.push(dep)
} else if (isSemiFrameworkPkg) {
// semi-framework packages should do the same except for optimization exclude as they
// aren't needed to work (they don't contain raw framework components)
ssrNoExternal.push(dep)
}
// only those that are explictly false can skip crawling since we don't need to do anything
// special for them
if (isFrameworkPkg === false || isSemiFrameworkPkg === false) {
return false
}
// if `true`, we need to crawl the nested deps to deep include and ssr externalize them in dev.
// if `undefined`, it's the same as "i don't know". we need to crawl and find the package.json
// to find out.
else {
return true
}
})
const promises = deps.map(async (dep) => {
const depPkgJsonPath = await findDepPkgJsonPath(dep, pkgJsonPath)
if (!depPkgJsonPath) return
const depPkgJson = await readJson(depPkgJsonPath).catch(() => {})
if (!depPkgJson) return
// fast path if this dep is already a framework dep based on the filter condition above
const cachedIsFrameworkPkg = ssrNoExternal.includes(dep)
if (cachedIsFrameworkPkg) {
return crawl(depPkgJsonPath, depPkgJson, parentDepNames.concat(dep))
}
// check if this dep is a framework dep, if so, track and crawl it
const isFrameworkPkg = options.isFrameworkPkgByJson?.(depPkgJson)
const isSemiFrameworkPkg = options.isSemiFrameworkPkgByJson?.(depPkgJson)
if (isFrameworkPkg || isSemiFrameworkPkg) {
// see explanation in filter condition above
if (isFrameworkPkg) {
optimizeDepsExclude.push(dep)
ssrNoExternal.push(dep)
} else if (isSemiFrameworkPkg) {
ssrNoExternal.push(dep)
}
return crawl(depPkgJsonPath, depPkgJson, parentDepNames.concat(dep))
}
// if we're crawling in a non-root state, the parent is 100% a framework package
// because of the above if block. in this case, if it's dep of a non-framework
// package, handle special cases for them.
if (!isRoot) {
// deep include it if it's a CJS package, so it becomes ESM and vite is happy.
if (await pkgNeedsOptimization(depPkgJson, depPkgJsonPath)) {
optimizeDepsInclude.push(parentDepNames.concat(dep).join(' > '))
}
// also externalize it in dev so it doesn't trip vite's SSR transformation.
// we do in dev only as build cannot access deep external packages in strict
// dependency installations, such as pnpm.
if (!options.isBuild && !ssrExternal.includes(dep)) {
ssrExternal.push(dep)
}
}
})
await Promise.all(promises)
}
}
/** @type {import('..').findDepPkgJsonPath} */
export async function findDepPkgJsonPath(dep, parent) {
if (pnp) {
const depRoot = pnp.resolveToUnqualified(dep, parent)
if (!depRoot) return undefined
return path.join(depRoot, 'package.json')
}
let root = await findClosestPkgJsonPath(parent)
if (!root) return undefined
root = path.dirname(root)
while (root) {
const pkg = path.join(root, 'node_modules', dep, 'package.json')
try {
await fs.access(pkg)
// use 'node:fs' version to match 'vite:resolve' and avoid realpath.native quirk
// https://github.com/sveltejs/vite-plugin-svelte/issues/525#issuecomment-1355551264
return fsSync.realpathSync(pkg)
} catch {}
const nextRoot = path.dirname(root)
if (nextRoot === root) break
root = nextRoot
}
return undefined
}
/** @type {import('..').findClosestPkgJsonPath} */
export async function findClosestPkgJsonPath(dir, predicate = undefined) {
if (dir.endsWith('package.json')) {
dir = path.dirname(dir)
}
while (dir) {
const pkg = path.join(dir, 'package.json')
try {
const stat = await fs.stat(pkg)
if (stat.isFile() && (!predicate || (await predicate(pkg)))) {
return pkg
}
} catch {}
const nextDir = path.dirname(dir)
if (nextDir === dir) break
dir = nextDir
}
return undefined
}
/** @type {import('..').pkgNeedsOptimization} */
export async function pkgNeedsOptimization(pkgJson, pkgJsonPath) {
// only optimize if is cjs, using the below as heuristic
// see https://github.com/sveltejs/vite-plugin-svelte/issues/162
if (pkgJson.module || pkgJson.exports) return false
// if have main, ensure entry is js so vite can prebundle it
// see https://github.com/sveltejs/vite-plugin-svelte/issues/233
if (pkgJson.main) {
const entryExt = path.extname(pkgJson.main)
return !entryExt || entryExt === '.js' || entryExt === '.cjs'
}
// check if has implicit index.js entrypoint to prebundle
// see https://github.com/sveltejs/vite-plugin-svelte/issues/281
// see https://github.com/solidjs/vite-plugin-solid/issues/70#issuecomment-1306488154
try {
await fs.access(path.join(path.dirname(pkgJsonPath), 'index.js'))
return true
} catch {
return false
}
}
/**
* @param {string} findDepPkgJsonPath
* @returns {Promise<Record<string, any>>}
*/
async function readJson(findDepPkgJsonPath) {
return JSON.parse(await fs.readFile(findDepPkgJsonPath, 'utf8'))
}