Skip to content

Commit 037a6c7

Browse files
authoredApr 6, 2023
perf: parallelize imports processing in import analysis plugin (#12754)
1 parent f736930 commit 037a6c7

File tree

1 file changed

+264
-235
lines changed

1 file changed

+264
-235
lines changed
 

‎packages/vite/src/node/plugins/importAnalysis.ts

+264-235
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,12 @@ const hasViteIgnoreRE = /\/\*\s*@vite-ignore\s*\*\//
8080
const cleanUpRawUrlRE = /\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm
8181
const urlIsStringRE = /^(?:'.*'|".*"|`.*`)$/
8282

83+
interface UrlPosition {
84+
url: string
85+
start: number
86+
end: number
87+
}
88+
8389
export function isExplicitImportRequired(url: string): boolean {
8490
return !isJSRequest(cleanUrl(url)) && !isCSSRequest(url)
8591
}
@@ -271,13 +277,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
271277
let s: MagicString | undefined
272278
const str = () => s || (s = new MagicString(source))
273279
const importedUrls = new Set<string>()
274-
const acceptedUrls = new Set<{
275-
url: string
276-
start: number
277-
end: number
278-
}>()
279280
let isPartiallySelfAccepting = false
280-
const acceptedExports = new Set<string>()
281281
const importedBindings = enablePartialAccept
282282
? new Map<string, Set<string>>()
283283
: null
@@ -409,268 +409,288 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
409409
return [url, resolved.id]
410410
}
411411

412-
for (let index = 0; index < imports.length; index++) {
413-
const {
414-
s: start,
415-
e: end,
416-
ss: expStart,
417-
se: expEnd,
418-
d: dynamicIndex,
419-
// #2083 User may use escape path,
420-
// so use imports[index].n to get the unescaped string
421-
n: specifier,
422-
a: assertIndex,
423-
} = imports[index]
424-
425-
const rawUrl = source.slice(start, end)
426-
427-
// check import.meta usage
428-
if (rawUrl === 'import.meta') {
429-
const prop = source.slice(end, end + 4)
430-
if (prop === '.hot') {
431-
hasHMR = true
432-
const endHot = end + 4 + (source[end + 4] === '?' ? 1 : 0)
433-
if (source.slice(endHot, endHot + 7) === '.accept') {
434-
// further analyze accepted modules
435-
if (source.slice(endHot, endHot + 14) === '.acceptExports') {
436-
lexAcceptedHmrExports(
437-
source,
438-
source.indexOf('(', endHot + 14) + 1,
439-
acceptedExports,
440-
)
441-
isPartiallySelfAccepting = true
442-
} else if (
443-
lexAcceptedHmrDeps(
444-
source,
445-
source.indexOf('(', endHot + 7) + 1,
446-
acceptedUrls,
447-
)
448-
) {
449-
isSelfAccepting = true
412+
const orderedAcceptedUrls = new Array<Set<UrlPosition> | undefined>(
413+
imports.length,
414+
)
415+
const orderedAcceptedExports = new Array<Set<string> | undefined>(
416+
imports.length,
417+
)
418+
419+
await Promise.all(
420+
imports.map(async (importSpecifier, index) => {
421+
const {
422+
s: start,
423+
e: end,
424+
ss: expStart,
425+
se: expEnd,
426+
d: dynamicIndex,
427+
// #2083 User may use escape path,
428+
// so use imports[index].n to get the unescaped string
429+
n: specifier,
430+
a: assertIndex,
431+
} = importSpecifier
432+
433+
const rawUrl = source.slice(start, end)
434+
435+
// check import.meta usage
436+
if (rawUrl === 'import.meta') {
437+
const prop = source.slice(end, end + 4)
438+
if (prop === '.hot') {
439+
hasHMR = true
440+
const endHot = end + 4 + (source[end + 4] === '?' ? 1 : 0)
441+
if (source.slice(endHot, endHot + 7) === '.accept') {
442+
// further analyze accepted modules
443+
if (source.slice(endHot, endHot + 14) === '.acceptExports') {
444+
const importAcceptedExports = (orderedAcceptedExports[index] =
445+
new Set<string>())
446+
lexAcceptedHmrExports(
447+
source,
448+
source.indexOf('(', endHot + 14) + 1,
449+
importAcceptedExports,
450+
)
451+
isPartiallySelfAccepting = true
452+
} else {
453+
const importAcceptedUrls = (orderedAcceptedUrls[index] =
454+
new Set<UrlPosition>())
455+
if (
456+
lexAcceptedHmrDeps(
457+
source,
458+
source.indexOf('(', endHot + 7) + 1,
459+
importAcceptedUrls,
460+
)
461+
) {
462+
isSelfAccepting = true
463+
}
464+
}
450465
}
466+
} else if (prop === '.env') {
467+
hasEnv = true
451468
}
452-
} else if (prop === '.env') {
453-
hasEnv = true
469+
return
454470
}
455-
continue
456-
}
457471

458-
const isDynamicImport = dynamicIndex > -1
472+
const isDynamicImport = dynamicIndex > -1
459473

460-
// strip import assertions as we can process them ourselves
461-
if (!isDynamicImport && assertIndex > -1) {
462-
str().remove(end + 1, expEnd)
463-
}
464-
465-
// static import or valid string in dynamic import
466-
// If resolvable, let's resolve it
467-
if (specifier) {
468-
// skip external / data uri
469-
if (isExternalUrl(specifier) || isDataUrl(specifier)) {
470-
continue
474+
// strip import assertions as we can process them ourselves
475+
if (!isDynamicImport && assertIndex > -1) {
476+
str().remove(end + 1, expEnd)
471477
}
472-
// skip ssr external
473-
if (ssr) {
474-
if (config.legacy?.buildSsrCjsExternalHeuristics) {
475-
if (cjsShouldExternalizeForSSR(specifier, server._ssrExternals)) {
476-
continue
478+
479+
// static import or valid string in dynamic import
480+
// If resolvable, let's resolve it
481+
if (specifier) {
482+
// skip external / data uri
483+
if (isExternalUrl(specifier) || isDataUrl(specifier)) {
484+
return
485+
}
486+
// skip ssr external
487+
if (ssr) {
488+
if (config.legacy?.buildSsrCjsExternalHeuristics) {
489+
if (
490+
cjsShouldExternalizeForSSR(specifier, server._ssrExternals)
491+
) {
492+
return
493+
}
494+
} else if (shouldExternalizeForSSR(specifier, config)) {
495+
return
496+
}
497+
if (isBuiltin(specifier)) {
498+
return
477499
}
478-
} else if (shouldExternalizeForSSR(specifier, config)) {
479-
continue
480500
}
481-
if (isBuiltin(specifier)) {
482-
continue
501+
// skip client
502+
if (specifier === clientPublicPath) {
503+
return
483504
}
484-
}
485-
// skip client
486-
if (specifier === clientPublicPath) {
487-
continue
488-
}
489505

490-
// warn imports to non-asset /public files
491-
if (
492-
specifier[0] === '/' &&
493-
!config.assetsInclude(cleanUrl(specifier)) &&
494-
!specifier.endsWith('.json') &&
495-
checkPublicFile(specifier, config)
496-
) {
497-
throw new Error(
498-
`Cannot import non-asset file ${specifier} which is inside /public.` +
499-
`JS/CSS files inside /public are copied as-is on build and ` +
500-
`can only be referenced via <script src> or <link href> in html.`,
501-
)
502-
}
503-
504-
// normalize
505-
const [url, resolvedId] = await normalizeUrl(specifier, start)
506-
507-
if (
508-
!isDynamicImport &&
509-
specifier &&
510-
!specifier.includes('?') && // ignore custom queries
511-
isCSSRequest(resolvedId) &&
512-
!isModuleCSSRequest(resolvedId)
513-
) {
514-
const sourceExp = source.slice(expStart, start)
506+
// warn imports to non-asset /public files
515507
if (
516-
sourceExp.includes('from') && // check default and named imports
517-
!sourceExp.includes('__vite_glob_') // glob handles deprecation message itself
508+
specifier[0] === '/' &&
509+
!config.assetsInclude(cleanUrl(specifier)) &&
510+
!specifier.endsWith('.json') &&
511+
checkPublicFile(specifier, config)
518512
) {
519-
const newImport =
520-
sourceExp + specifier + `?inline` + source.slice(end, expEnd)
521-
this.warn(
522-
`\n` +
523-
colors.cyan(importerModule.file) +
524-
`\n` +
525-
colors.reset(generateCodeFrame(source, start)) +
526-
`\n` +
527-
colors.yellow(
528-
`Default and named imports from CSS files are deprecated. ` +
529-
`Use the ?inline query instead. ` +
530-
`For example: ${newImport}`,
531-
),
513+
throw new Error(
514+
`Cannot import non-asset file ${specifier} which is inside /public.` +
515+
`JS/CSS files inside /public are copied as-is on build and ` +
516+
`can only be referenced via <script src> or <link href> in html.`,
532517
)
533518
}
534-
}
535519

536-
// record as safe modules
537-
server?.moduleGraph.safeModulesPath.add(fsPathFromUrl(url))
520+
// normalize
521+
const [url, resolvedId] = await normalizeUrl(specifier, start)
538522

539-
if (url !== specifier) {
540-
let rewriteDone = false
541523
if (
542-
depsOptimizer?.isOptimizedDepFile(resolvedId) &&
543-
!resolvedId.match(optimizedDepChunkRE)
524+
!isDynamicImport &&
525+
specifier &&
526+
!specifier.includes('?') && // ignore custom queries
527+
isCSSRequest(resolvedId) &&
528+
!isModuleCSSRequest(resolvedId)
544529
) {
545-
// for optimized cjs deps, support named imports by rewriting named imports to const assignments.
546-
// internal optimized chunks don't need es interop and are excluded
547-
548-
// The browserHash in resolvedId could be stale in which case there will be a full
549-
// page reload. We could return a 404 in that case but it is safe to return the request
550-
const file = cleanUrl(resolvedId) // Remove ?v={hash}
551-
552-
const needsInterop = await optimizedDepNeedsInterop(
553-
depsOptimizer.metadata,
554-
file,
555-
config,
556-
ssr,
557-
)
558-
559-
if (needsInterop === undefined) {
560-
// Non-entry dynamic imports from dependencies will reach here as there isn't
561-
// optimize info for them, but they don't need es interop. If the request isn't
562-
// a dynamic import, then it is an internal Vite error
563-
if (!file.match(optimizedDepDynamicRE)) {
564-
config.logger.error(
565-
colors.red(
566-
`Vite Error, ${url} optimized info should be defined`,
530+
const sourceExp = source.slice(expStart, start)
531+
if (
532+
sourceExp.includes('from') && // check default and named imports
533+
!sourceExp.includes('__vite_glob_') // glob handles deprecation message itself
534+
) {
535+
const newImport =
536+
sourceExp + specifier + `?inline` + source.slice(end, expEnd)
537+
this.warn(
538+
`\n` +
539+
colors.cyan(importerModule.file) +
540+
`\n` +
541+
colors.reset(generateCodeFrame(source, start)) +
542+
`\n` +
543+
colors.yellow(
544+
`Default and named imports from CSS files are deprecated. ` +
545+
`Use the ?inline query instead. ` +
546+
`For example: ${newImport}`,
567547
),
568-
)
569-
}
570-
} else if (needsInterop) {
571-
debug?.(`${url} needs interop`)
572-
interopNamedImports(str(), imports[index], url, index)
573-
rewriteDone = true
548+
)
574549
}
575550
}
576-
// If source code imports builtin modules via named imports, the stub proxy export
577-
// would fail as it's `export default` only. Apply interop for builtin modules to
578-
// correctly throw the error message.
579-
else if (
580-
url.includes(browserExternalId) &&
581-
source.slice(expStart, start).includes('{')
582-
) {
583-
interopNamedImports(str(), imports[index], url, index)
584-
rewriteDone = true
585-
}
586-
if (!rewriteDone) {
587-
const rewrittenUrl = JSON.stringify(url)
588-
const s = isDynamicImport ? start : start - 1
589-
const e = isDynamicImport ? end : end + 1
590-
str().overwrite(s, e, rewrittenUrl, {
591-
contentOnly: true,
592-
})
593-
}
594-
}
595551

596-
// record for HMR import chain analysis
597-
// make sure to unwrap and normalize away base
598-
const hmrUrl = unwrapId(stripBase(url, base))
599-
const isLocalImport = !isExternalUrl(hmrUrl) && !isDataUrl(hmrUrl)
600-
if (isLocalImport) {
601-
importedUrls.add(hmrUrl)
602-
}
552+
// record as safe modules
553+
server?.moduleGraph.safeModulesPath.add(fsPathFromUrl(url))
603554

604-
if (enablePartialAccept && importedBindings) {
605-
extractImportedBindings(
606-
resolvedId,
607-
source,
608-
imports[index],
609-
importedBindings,
610-
)
611-
}
555+
if (url !== specifier) {
556+
let rewriteDone = false
557+
if (
558+
depsOptimizer?.isOptimizedDepFile(resolvedId) &&
559+
!resolvedId.match(optimizedDepChunkRE)
560+
) {
561+
// for optimized cjs deps, support named imports by rewriting named imports to const assignments.
562+
// internal optimized chunks don't need es interop and are excluded
563+
564+
// The browserHash in resolvedId could be stale in which case there will be a full
565+
// page reload. We could return a 404 in that case but it is safe to return the request
566+
const file = cleanUrl(resolvedId) // Remove ?v={hash}
567+
568+
const needsInterop = await optimizedDepNeedsInterop(
569+
depsOptimizer.metadata,
570+
file,
571+
config,
572+
ssr,
573+
)
612574

613-
if (
614-
!isDynamicImport &&
615-
isLocalImport &&
616-
config.server.preTransformRequests
617-
) {
618-
// pre-transform known direct imports
619-
// These requests will also be registered in transformRequest to be awaited
620-
// by the deps optimizer
621-
const url = removeImportQuery(hmrUrl)
622-
server.transformRequest(url, { ssr }).catch((e) => {
623-
if (e?.code === ERR_OUTDATED_OPTIMIZED_DEP) {
624-
// This are expected errors
625-
return
575+
if (needsInterop === undefined) {
576+
// Non-entry dynamic imports from dependencies will reach here as there isn't
577+
// optimize info for them, but they don't need es interop. If the request isn't
578+
// a dynamic import, then it is an internal Vite error
579+
if (!file.match(optimizedDepDynamicRE)) {
580+
config.logger.error(
581+
colors.red(
582+
`Vite Error, ${url} optimized info should be defined`,
583+
),
584+
)
585+
}
586+
} else if (needsInterop) {
587+
debug?.(`${url} needs interop`)
588+
interopNamedImports(str(), importSpecifier, url, index)
589+
rewriteDone = true
590+
}
626591
}
627-
// Unexpected error, log the issue but avoid an unhandled exception
628-
config.logger.error(e.message)
629-
})
630-
}
631-
} else if (!importer.startsWith(clientDir)) {
632-
if (!isInNodeModules(importer)) {
633-
// check @vite-ignore which suppresses dynamic import warning
634-
const hasViteIgnore = hasViteIgnoreRE.test(
635-
// complete expression inside parens
636-
source.slice(dynamicIndex + 1, end),
637-
)
638-
if (!hasViteIgnore) {
639-
this.warn(
640-
`\n` +
641-
colors.cyan(importerModule.file) +
642-
`\n` +
643-
colors.reset(generateCodeFrame(source, start)) +
644-
colors.yellow(
645-
`\nThe above dynamic import cannot be analyzed by Vite.\n` +
646-
`See ${colors.blue(
647-
`https://github.com/rollup/plugins/tree/master/packages/dynamic-import-vars#limitations`,
648-
)} ` +
649-
`for supported dynamic import formats. ` +
650-
`If this is intended to be left as-is, you can use the ` +
651-
`/* @vite-ignore */ comment inside the import() call to suppress this warning.\n`,
652-
),
592+
// If source code imports builtin modules via named imports, the stub proxy export
593+
// would fail as it's `export default` only. Apply interop for builtin modules to
594+
// correctly throw the error message.
595+
else if (
596+
url.includes(browserExternalId) &&
597+
source.slice(expStart, start).includes('{')
598+
) {
599+
interopNamedImports(str(), importSpecifier, url, index)
600+
rewriteDone = true
601+
}
602+
if (!rewriteDone) {
603+
const rewrittenUrl = JSON.stringify(url)
604+
const s = isDynamicImport ? start : start - 1
605+
const e = isDynamicImport ? end : end + 1
606+
str().overwrite(s, e, rewrittenUrl, {
607+
contentOnly: true,
608+
})
609+
}
610+
}
611+
612+
// record for HMR import chain analysis
613+
// make sure to unwrap and normalize away base
614+
const hmrUrl = unwrapId(stripBase(url, base))
615+
const isLocalImport = !isExternalUrl(hmrUrl) && !isDataUrl(hmrUrl)
616+
if (isLocalImport) {
617+
importedUrls.add(hmrUrl)
618+
}
619+
620+
if (enablePartialAccept && importedBindings) {
621+
extractImportedBindings(
622+
resolvedId,
623+
source,
624+
importSpecifier,
625+
importedBindings,
653626
)
654627
}
655-
}
656628

657-
if (!ssr) {
658-
const url = rawUrl.replace(cleanUpRawUrlRE, '').trim()
659629
if (
660-
!urlIsStringRE.test(url) ||
661-
isExplicitImportRequired(url.slice(1, -1))
630+
!isDynamicImport &&
631+
isLocalImport &&
632+
config.server.preTransformRequests
662633
) {
663-
needQueryInjectHelper = true
664-
str().overwrite(
665-
start,
666-
end,
667-
`__vite__injectQuery(${url}, 'import')`,
668-
{ contentOnly: true },
634+
// pre-transform known direct imports
635+
// These requests will also be registered in transformRequest to be awaited
636+
// by the deps optimizer
637+
const url = removeImportQuery(hmrUrl)
638+
server.transformRequest(url, { ssr }).catch((e) => {
639+
if (e?.code === ERR_OUTDATED_OPTIMIZED_DEP) {
640+
// This are expected errors
641+
return
642+
}
643+
// Unexpected error, log the issue but avoid an unhandled exception
644+
config.logger.error(e.message)
645+
})
646+
}
647+
} else if (!importer.startsWith(clientDir)) {
648+
if (!isInNodeModules(importer)) {
649+
// check @vite-ignore which suppresses dynamic import warning
650+
const hasViteIgnore = hasViteIgnoreRE.test(
651+
// complete expression inside parens
652+
source.slice(dynamicIndex + 1, end),
669653
)
654+
if (!hasViteIgnore) {
655+
this.warn(
656+
`\n` +
657+
colors.cyan(importerModule.file) +
658+
`\n` +
659+
colors.reset(generateCodeFrame(source, start)) +
660+
colors.yellow(
661+
`\nThe above dynamic import cannot be analyzed by Vite.\n` +
662+
`See ${colors.blue(
663+
`https://github.com/rollup/plugins/tree/master/packages/dynamic-import-vars#limitations`,
664+
)} ` +
665+
`for supported dynamic import formats. ` +
666+
`If this is intended to be left as-is, you can use the ` +
667+
`/* @vite-ignore */ comment inside the import() call to suppress this warning.\n`,
668+
),
669+
)
670+
}
671+
}
672+
673+
if (!ssr) {
674+
const url = rawUrl.replace(cleanUpRawUrlRE, '').trim()
675+
if (
676+
!urlIsStringRE.test(url) ||
677+
isExplicitImportRequired(url.slice(1, -1))
678+
) {
679+
needQueryInjectHelper = true
680+
str().overwrite(
681+
start,
682+
end,
683+
`__vite__injectQuery(${url}, 'import')`,
684+
{ contentOnly: true },
685+
)
686+
}
670687
}
671688
}
672-
}
673-
}
689+
}),
690+
)
691+
692+
const acceptedUrls = mergeAcceptedUrls(orderedAcceptedUrls)
693+
const acceptedExports = mergeAcceptedUrls(orderedAcceptedExports)
674694

675695
if (hasEnv) {
676696
// inject import.meta.env
@@ -777,6 +797,15 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
777797
}
778798
}
779799

800+
function mergeAcceptedUrls<T>(orderedUrls: Array<Set<T> | undefined>) {
801+
const acceptedUrls = new Set<T>()
802+
for (const urls of orderedUrls) {
803+
if (!urls) continue
804+
for (const url of urls) acceptedUrls.add(url)
805+
}
806+
return acceptedUrls
807+
}
808+
780809
export function interopNamedImports(
781810
str: MagicString,
782811
importSpecifier: ImportSpecifier,

0 commit comments

Comments
 (0)
Please sign in to comment.