-
-
Notifications
You must be signed in to change notification settings - Fork 6.1k
/
importAnalysisBuild.ts
660 lines (594 loc) · 23.3 KB
/
importAnalysisBuild.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
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
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
import path from 'node:path'
import MagicString from 'magic-string'
import type { ImportSpecifier } from 'es-module-lexer'
import { init, parse as parseImports } from 'es-module-lexer'
import type { OutputChunk, SourceMap } from 'rollup'
import colors from 'picocolors'
import type { RawSourceMap } from '@ampproject/remapping'
import convertSourceMap from 'convert-source-map'
import {
bareImportRE,
cleanUrl,
combineSourcemaps,
isDataUrl,
isExternalUrl,
isInNodeModules,
moduleListContains,
} from '../utils'
import type { Plugin } from '../plugin'
import { getDepOptimizationConfig } from '../config'
import type { ResolvedConfig } from '../config'
import { toOutputFilePathInJS } from '../build'
import { genSourceMapUrl } from '../server/sourcemap'
import { getDepsOptimizer, optimizedDepNeedsInterop } from '../optimizer'
import { SPECIAL_QUERY_RE } from '../constants'
import { isCSSRequest, removedPureCssFilesCache } from './css'
import { interopNamedImports } from './importAnalysis'
/**
* A flag for injected helpers. This flag will be set to `false` if the output
* target is not native es - so that injected helper logic can be conditionally
* dropped.
*/
export const isModernFlag = `__VITE_IS_MODERN__`
export const preloadMethod = `__vitePreload`
export const preloadMarker = `__VITE_PRELOAD__`
export const preloadBaseMarker = `__VITE_PRELOAD_BASE__`
export const preloadHelperId = '\0vite/preload-helper'
const preloadMarkerWithQuote = new RegExp(`['"]${preloadMarker}['"]`)
const dynamicImportPrefixRE = /import\s*\(/
// TODO: abstract
const optimizedDepChunkRE = /\/chunk-[A-Z\d]{8}\.js/
const optimizedDepDynamicRE = /-[A-Z\d]{8}\.js/
function toRelativePath(filename: string, importer: string) {
const relPath = path.relative(path.dirname(importer), filename)
return relPath[0] === '.' ? relPath : `./${relPath}`
}
function indexOfMatchInSlice(
str: string,
reg: RegExp,
pos: number = 0,
): number {
if (pos !== 0) {
str = str.slice(pos)
}
const matcher = str.match(reg)
return matcher?.index !== undefined ? matcher.index + pos : -1
}
/**
* Helper for preloading CSS and direct imports of async chunks in parallel to
* the async chunk itself.
*/
function detectScriptRel() {
const relList = document.createElement('link').relList
return relList && relList.supports && relList.supports('modulepreload')
? 'modulepreload'
: 'preload'
}
declare const scriptRel: string
declare const seen: Record<string, boolean>
function preload(
baseModule: () => Promise<{}>,
deps?: string[],
importerUrl?: string,
) {
// @ts-expect-error __VITE_IS_MODERN__ will be replaced with boolean later
if (!__VITE_IS_MODERN__ || !deps || deps.length === 0) {
return baseModule()
}
const links = document.getElementsByTagName('link')
return Promise.all(
deps.map((dep) => {
// @ts-expect-error assetsURL is declared before preload.toString()
dep = assetsURL(dep, importerUrl)
if (dep in seen) return
seen[dep] = true
const isCss = dep.endsWith('.css')
const cssSelector = isCss ? '[rel="stylesheet"]' : ''
const isBaseRelative = !!importerUrl
// check if the file is already preloaded by SSR markup
if (isBaseRelative) {
// When isBaseRelative is true then we have `importerUrl` and `dep` is
// already converted to an absolute URL by the `assetsURL` function
for (let i = links.length - 1; i >= 0; i--) {
const link = links[i]
// The `links[i].href` is an absolute URL thanks to browser doing the work
// for us. See https://html.spec.whatwg.org/multipage/common-dom-interfaces.html#reflecting-content-attributes-in-idl-attributes:idl-domstring-5
if (link.href === dep && (!isCss || link.rel === 'stylesheet')) {
return
}
}
} else if (document.querySelector(`link[href="${dep}"]${cssSelector}`)) {
return
}
const link = document.createElement('link')
link.rel = isCss ? 'stylesheet' : scriptRel
if (!isCss) {
link.as = 'script'
link.crossOrigin = ''
}
link.href = dep
document.head.appendChild(link)
if (isCss) {
return new Promise((res, rej) => {
link.addEventListener('load', res)
link.addEventListener('error', () =>
rej(new Error(`Unable to preload CSS for ${dep}`)),
)
})
}
}),
).then(() => baseModule())
}
/**
* Build only. During serve this is performed as part of ./importAnalysis.
*/
export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin {
const ssr = !!config.build.ssr
const isWorker = config.isWorker
const insertPreload = !(
ssr ||
!!config.build.lib ||
isWorker ||
config.build.modulePreload === false
)
const resolveModulePreloadDependencies =
config.build.modulePreload && config.build.modulePreload.resolveDependencies
const renderBuiltUrl = config.experimental.renderBuiltUrl
const customModulePreloadPaths = !!(
resolveModulePreloadDependencies || renderBuiltUrl
)
const isRelativeBase = config.base === './' || config.base === ''
const optimizeModulePreloadRelativePaths =
isRelativeBase && !customModulePreloadPaths
const { modulePreload } = config.build
const scriptRel =
modulePreload && modulePreload.polyfill
? `'modulepreload'`
: `(${detectScriptRel.toString()})()`
// There are three different cases for the preload list format in __vitePreload
//
// __vitePreload(() => import(asyncChunk), [ ...deps... ])
//
// This is maintained to keep backwards compatibility as some users developed plugins
// using regex over this list to workaround the fact that module preload wasn't
// configurable.
const assetsURL = customModulePreloadPaths
? // If `experimental.renderBuiltUrl` or `build.modulePreload.resolveDependencies` are used
// the dependencies are already resolved. To avoid the need for `new URL(dep, import.meta.url)`
// a helper `__vitePreloadRelativeDep` is used to resolve from relative paths which can be minimized.
`function(dep, importerUrl) { return dep.startsWith('.') ? new URL(dep, importerUrl).href : dep }`
: optimizeModulePreloadRelativePaths
? // If there isn't custom resolvers affecting the deps list, deps in the list are relative
// to the current chunk and are resolved to absolute URL by the __vitePreload helper itself.
// The importerUrl is passed as third parameter to __vitePreload in this case
`function(dep, importerUrl) { return new URL(dep, importerUrl).href }`
: // If the base isn't relative, then the deps are relative to the projects `outDir` and the base
// is appended inside __vitePreload too.
`function(dep) { return ${JSON.stringify(config.base)}+dep }`
const preloadCode = `const scriptRel = ${scriptRel};const assetsURL = ${assetsURL};const seen = {};export const ${preloadMethod} = ${preload.toString()}`
return {
name: 'vite:build-import-analysis',
resolveId(id) {
if (id === preloadHelperId) {
return id
}
},
load(id) {
if (id === preloadHelperId) {
return preloadCode
}
},
async transform(source, importer) {
if (isInNodeModules(importer) && !dynamicImportPrefixRE.test(source)) {
return
}
await init
let imports: readonly ImportSpecifier[] = []
try {
imports = parseImports(source)[0]
} catch (e: any) {
this.error(e, e.idx)
}
if (!imports.length) {
return null
}
const { root } = config
const depsOptimizer = getDepsOptimizer(config, ssr)
const normalizeUrl = async (
url: string,
pos: number,
): Promise<[string, string]> => {
let importerFile = importer
const optimizeDeps = getDepOptimizationConfig(config, ssr)
if (moduleListContains(optimizeDeps?.exclude, url)) {
if (depsOptimizer) {
await depsOptimizer.scanProcessing
// if the dependency encountered in the optimized file was excluded from the optimization
// the dependency needs to be resolved starting from the original source location of the optimized file
// because starting from node_modules/.vite will not find the dependency if it was not hoisted
// (that is, if it is under node_modules directory in the package source of the optimized file)
for (const optimizedModule of depsOptimizer.metadata.depInfoList) {
if (!optimizedModule.src) continue // Ignore chunks
if (optimizedModule.file === importer) {
importerFile = optimizedModule.src
}
}
}
}
const resolved = await this.resolve(url, importerFile)
if (!resolved) {
// in ssr, we should let node handle the missing modules
if (ssr) {
return [url, url]
}
return this.error(
`Failed to resolve import "${url}" from "${path.relative(
process.cwd(),
importerFile,
)}". Does the file exist?`,
pos,
)
}
// normalize all imports into resolved URLs
// e.g. `import 'foo'` -> `import '/@fs/.../node_modules/foo/index.js'`
if (resolved.id.startsWith(root + '/')) {
// in root: infer short absolute path from root
url = resolved.id.slice(root.length)
} else {
url = resolved.id
}
if (isExternalUrl(url)) {
return [url, url]
}
return [url, resolved.id]
}
let s: MagicString | undefined
const str = () => s || (s = new MagicString(source))
let needPreloadHelper = false
for (let index = 0; index < imports.length; index++) {
const {
s: start,
e: end,
ss: expStart,
se: expEnd,
n: specifier,
d: dynamicIndex,
a: assertIndex,
} = imports[index]
const isDynamicImport = dynamicIndex > -1
// strip import assertions as we can process them ourselves
if (!isDynamicImport && assertIndex > -1) {
str().remove(end + 1, expEnd)
}
if (isDynamicImport && insertPreload) {
needPreloadHelper = true
str().prependLeft(expStart, `${preloadMethod}(() => `)
str().appendRight(
expEnd,
`,${isModernFlag}?"${preloadMarker}":void 0${
optimizeModulePreloadRelativePaths || customModulePreloadPaths
? ',import.meta.url'
: ''
})`,
)
}
// static import or valid string in dynamic import
// If resolvable, let's resolve it
if (depsOptimizer && specifier) {
// skip external / data uri
if (isExternalUrl(specifier) || isDataUrl(specifier)) {
continue
}
// normalize
const [url, resolvedId] = await normalizeUrl(specifier, start)
if (url !== specifier) {
if (
depsOptimizer.isOptimizedDepFile(resolvedId) &&
!resolvedId.match(optimizedDepChunkRE)
) {
const file = cleanUrl(resolvedId) // Remove ?v={hash}
const needsInterop = await optimizedDepNeedsInterop(
depsOptimizer.metadata,
file,
config,
ssr,
)
let rewriteDone = false
if (needsInterop === undefined) {
// Non-entry dynamic imports from dependencies will reach here as there isn't
// optimize info for them, but they don't need es interop. If the request isn't
// a dynamic import, then it is an internal Vite error
if (!file.match(optimizedDepDynamicRE)) {
config.logger.error(
colors.red(
`Vite Error, ${url} optimized info should be defined`,
),
)
}
} else if (needsInterop) {
// config.logger.info(`${url} needs interop`)
interopNamedImports(
str(),
imports[index],
url,
index,
importer,
config,
)
rewriteDone = true
}
if (!rewriteDone) {
const rewrittenUrl = JSON.stringify(file)
const s = isDynamicImport ? start : start - 1
const e = isDynamicImport ? end : end + 1
str().update(s, e, rewrittenUrl)
}
}
}
}
// Differentiate CSS imports that use the default export from those that
// do not by injecting a ?used query - this allows us to avoid including
// the CSS string when unnecessary (esbuild has trouble tree-shaking
// them)
if (
specifier &&
isCSSRequest(specifier) &&
// always inject ?used query when it is a dynamic import
// because there is no way to check whether the default export is used
(source.slice(expStart, start).includes('from') || isDynamicImport) &&
// already has ?used query (by import.meta.glob)
!specifier.match(/\?used(&|$)/) &&
// don't append ?used when SPECIAL_QUERY_RE exists
!specifier.match(SPECIAL_QUERY_RE) &&
// edge case for package names ending with .css (e.g normalize.css)
!(bareImportRE.test(specifier) && !specifier.includes('/'))
) {
const url = specifier.replace(/\?|$/, (m) => `?used${m ? '&' : ''}`)
str().update(start, end, isDynamicImport ? `'${url}'` : url)
}
}
if (
needPreloadHelper &&
insertPreload &&
!source.includes(`const ${preloadMethod} =`)
) {
str().prepend(`import { ${preloadMethod} } from "${preloadHelperId}";`)
}
if (s) {
return {
code: s.toString(),
map: config.build.sourcemap ? s.generateMap({ hires: true }) : null,
}
}
},
renderChunk(code, _, { format }) {
// make sure we only perform the preload logic in modern builds.
if (code.indexOf(isModernFlag) > -1) {
const re = new RegExp(isModernFlag, 'g')
const isModern = String(format === 'es')
if (config.build.sourcemap) {
const s = new MagicString(code)
let match: RegExpExecArray | null
while ((match = re.exec(code))) {
s.update(match.index, match.index + isModernFlag.length, isModern)
}
return {
code: s.toString(),
map: s.generateMap({ hires: true }),
}
} else {
return code.replace(re, isModern)
}
}
return null
},
generateBundle({ format }, bundle) {
if (
format !== 'es' ||
ssr ||
isWorker ||
config.build.modulePreload === false
) {
return
}
for (const file in bundle) {
const chunk = bundle[file]
// can't use chunk.dynamicImports.length here since some modules e.g.
// dynamic import to constant json may get inlined.
if (chunk.type === 'chunk' && chunk.code.indexOf(preloadMarker) > -1) {
const code = chunk.code
let imports!: ImportSpecifier[]
try {
imports = parseImports(code)[0].filter((i) => i.d > -1)
} catch (e: any) {
this.error(e, e.idx)
}
const s = new MagicString(code)
const rewroteMarkerStartPos = new Set() // position of the leading double quote
if (imports.length) {
for (let index = 0; index < imports.length; index++) {
// To handle escape sequences in specifier strings, the .n field will be provided where possible.
const {
n: name,
s: start,
e: end,
ss: expStart,
se: expEnd,
} = imports[index]
// check the chunk being imported
let url = name
if (!url) {
const rawUrl = code.slice(start, end)
if (rawUrl[0] === `"` && rawUrl[rawUrl.length - 1] === `"`)
url = rawUrl.slice(1, -1)
}
const deps: Set<string> = new Set()
let hasRemovedPureCssChunk = false
let normalizedFile: string | undefined = undefined
if (url) {
normalizedFile = path.posix.join(
path.posix.dirname(chunk.fileName),
url,
)
const ownerFilename = chunk.fileName
// literal import - trace direct imports and add to deps
const analyzed: Set<string> = new Set<string>()
const addDeps = (filename: string) => {
if (filename === ownerFilename) return
if (analyzed.has(filename)) return
analyzed.add(filename)
const chunk = bundle[filename] as OutputChunk | undefined
if (chunk) {
deps.add(chunk.fileName)
chunk.imports.forEach(addDeps)
// Ensure that the css imported by current chunk is loaded after the dependencies.
// So the style of current chunk won't be overwritten unexpectedly.
chunk.viteMetadata!.importedCss.forEach((file) => {
deps.add(file)
})
} else {
const removedPureCssFiles =
removedPureCssFilesCache.get(config)!
const chunk = removedPureCssFiles.get(filename)
if (chunk) {
if (chunk.viteMetadata!.importedCss.size) {
chunk.viteMetadata!.importedCss.forEach((file) => {
deps.add(file)
})
hasRemovedPureCssChunk = true
}
s.update(expStart, expEnd, 'Promise.resolve({})')
}
}
}
addDeps(normalizedFile)
}
let markerStartPos = indexOfMatchInSlice(
code,
preloadMarkerWithQuote,
end,
)
// fix issue #3051
if (markerStartPos === -1 && imports.length === 1) {
markerStartPos = indexOfMatchInSlice(
code,
preloadMarkerWithQuote,
)
}
if (markerStartPos > 0) {
// the dep list includes the main chunk, so only need to reload when there are actual other deps.
const depsArray =
deps.size > 1 ||
// main chunk is removed
(hasRemovedPureCssChunk && deps.size > 0)
? [...deps]
: []
let renderedDeps: string[]
if (normalizedFile && customModulePreloadPaths) {
const { modulePreload } = config.build
const resolveDependencies =
modulePreload && modulePreload.resolveDependencies
let resolvedDeps: string[]
if (resolveDependencies) {
// We can't let the user remove css deps as these aren't really preloads, they are just using
// the same mechanism as module preloads for this chunk
const cssDeps: string[] = []
const otherDeps: string[] = []
for (const dep of depsArray) {
;(dep.endsWith('.css') ? cssDeps : otherDeps).push(dep)
}
resolvedDeps = [
...resolveDependencies(normalizedFile, otherDeps, {
hostId: file,
hostType: 'js',
}),
...cssDeps,
]
} else {
resolvedDeps = depsArray
}
renderedDeps = resolvedDeps.map((dep: string) => {
const replacement = toOutputFilePathInJS(
dep,
'asset',
chunk.fileName,
'js',
config,
toRelativePath,
)
const replacementString =
typeof replacement === 'string'
? JSON.stringify(replacement)
: replacement.runtime
return replacementString
})
} else {
renderedDeps = depsArray.map((d) =>
// Don't include the assets dir if the default asset file names
// are used, the path will be reconstructed by the import preload helper
JSON.stringify(
optimizeModulePreloadRelativePaths
? toRelativePath(d, file)
: d,
),
)
}
s.update(
markerStartPos,
markerStartPos + preloadMarker.length + 2,
`[${renderedDeps.join(',')}]`,
)
rewroteMarkerStartPos.add(markerStartPos)
}
}
}
// there may still be markers due to inlined dynamic imports, remove
// all the markers regardless
let markerStartPos = indexOfMatchInSlice(code, preloadMarkerWithQuote)
while (markerStartPos >= 0) {
if (!rewroteMarkerStartPos.has(markerStartPos)) {
s.update(
markerStartPos,
markerStartPos + preloadMarker.length + 2,
'void 0',
)
}
markerStartPos = indexOfMatchInSlice(
code,
preloadMarkerWithQuote,
markerStartPos + preloadMarker.length + 2,
)
}
if (s.hasChanged()) {
chunk.code = s.toString()
if (config.build.sourcemap && chunk.map) {
const nextMap = s.generateMap({
source: chunk.fileName,
hires: true,
})
const map = combineSourcemaps(
chunk.fileName,
[nextMap as RawSourceMap, chunk.map as RawSourceMap],
false,
) as SourceMap
map.toUrl = () => genSourceMapUrl(map)
chunk.map = map
if (config.build.sourcemap === 'inline') {
chunk.code = chunk.code.replace(
convertSourceMap.mapFileCommentRegex,
'',
)
chunk.code += `\n//# sourceMappingURL=${genSourceMapUrl(map)}`
} else if (config.build.sourcemap) {
const mapAsset = bundle[chunk.fileName + '.map']
if (mapAsset && mapAsset.type === 'asset') {
mapAsset.source = map.toString()
}
}
}
}
}
}
},
}
}