-
-
Notifications
You must be signed in to change notification settings - Fork 6.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(optimizer): support glob includes (#12414)
- Loading branch information
Showing
22 changed files
with
284 additions
and
77 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,173 @@ | ||
import path from 'node:path' | ||
import glob from 'fast-glob' | ||
import micromatch from 'micromatch' | ||
import type { ResolvedConfig } from '../config' | ||
import { escapeRegex, getNpmPackageName, slash } from '../utils' | ||
import { resolvePackageData } from '../packages' | ||
|
||
export function createOptimizeDepsIncludeResolver( | ||
config: ResolvedConfig, | ||
ssr: boolean, | ||
): (id: string) => Promise<string | undefined> { | ||
const resolve = config.createResolver({ | ||
asSrc: false, | ||
scan: true, | ||
ssrOptimizeCheck: ssr, | ||
ssrConfig: config.ssr, | ||
packageCache: new Map(), | ||
}) | ||
return async (id: string) => { | ||
const lastArrowIndex = id.lastIndexOf('>') | ||
if (lastArrowIndex === -1) { | ||
return await resolve(id, undefined, undefined, ssr) | ||
} | ||
// split nested selected id by last '>', for example: | ||
// 'foo > bar > baz' => 'foo > bar' & 'baz' | ||
const nestedRoot = id.substring(0, lastArrowIndex).trim() | ||
const nestedPath = id.substring(lastArrowIndex + 1).trim() | ||
const basedir = nestedResolveBasedir( | ||
nestedRoot, | ||
config.root, | ||
config.resolve.preserveSymlinks, | ||
) | ||
return await resolve( | ||
nestedPath, | ||
path.resolve(basedir, 'package.json'), | ||
undefined, | ||
ssr, | ||
) | ||
} | ||
} | ||
|
||
/** | ||
* Expand the glob syntax in `optimizeDeps.include` to proper import paths | ||
*/ | ||
export function expandGlobIds(id: string, config: ResolvedConfig): string[] { | ||
const pkgName = getNpmPackageName(id) | ||
if (!pkgName) return [] | ||
|
||
const pkgData = resolvePackageData( | ||
pkgName, | ||
config.root, | ||
config.resolve.preserveSymlinks, | ||
config.packageCache, | ||
) | ||
if (!pkgData) return [] | ||
|
||
const pattern = '.' + id.slice(pkgName.length) | ||
const exports = pkgData.data.exports | ||
|
||
// if package has exports field, get all possible export paths and apply | ||
// glob on them with micromatch | ||
if (exports) { | ||
if (typeof exports === 'string' || Array.isArray(exports)) { | ||
return [pkgName] | ||
} | ||
|
||
const possibleExportPaths: string[] = [] | ||
for (const key in exports) { | ||
if (key.startsWith('.')) { | ||
if (key.includes('*')) { | ||
// "./glob/*": { | ||
// "browser": "./dist/glob/*-browser/*.js", <-- get this one | ||
// "default": "./dist/glob/*/*.js" | ||
// } | ||
// NOTE: theoretically the "default" condition could map to a different | ||
// set of files, but that complicates the resolve logic, so we assume | ||
// all conditions map to the same set of files, and get the first one. | ||
const exportsValue = getFirstExportStringValue(exports[key]) | ||
if (!exportsValue) continue | ||
|
||
// "./dist/glob/*-browser/*.js" => "./dist/glob/**/*-browser/**/*.js" | ||
// NOTE: in some cases, this could expand to consecutive /**/*/**/* etc | ||
// but it's fine since fast-glob handles it the same. | ||
const exportValuePattern = exportsValue.replace(/\*/g, '**/*') | ||
// "./dist/glob/*-browser/*.js" => /dist\/glob\/(.*)-browser\/(.*)\.js/ | ||
const exportsValueGlobRe = new RegExp( | ||
exportsValue.split('*').map(escapeRegex).join('(.*)'), | ||
) | ||
|
||
possibleExportPaths.push( | ||
...glob | ||
.sync(exportValuePattern, { | ||
cwd: pkgData.dir, | ||
ignore: ['node_modules'], | ||
}) | ||
.map((filePath) => { | ||
// "./glob/*": "./dist/glob/*-browser/*.js" | ||
// `filePath`: "./dist/glob/foo-browser/foo.js" | ||
// we need to revert the file path back to the export key by | ||
// matching value regex and replacing the capture groups to the key | ||
const matched = slash(filePath).match(exportsValueGlobRe) | ||
// `matched`: [..., 'foo', 'foo'] | ||
if (matched) { | ||
let allGlobSame = matched.length === 2 | ||
// exports key can only have one *, so for >=2 matched groups, | ||
// make sure they have the same value | ||
if (!allGlobSame) { | ||
// assume true, if one group is different, set false and break | ||
allGlobSame = true | ||
for (let i = 2; i < matched.length; i++) { | ||
if (matched[i] !== matched[i - 1]) { | ||
allGlobSame = false | ||
break | ||
} | ||
} | ||
} | ||
if (allGlobSame) { | ||
return key.replace('*', matched[1]).slice(2) | ||
} | ||
} | ||
return '' | ||
}) | ||
.filter(Boolean), | ||
) | ||
} else { | ||
possibleExportPaths.push(key.slice(2)) | ||
} | ||
} | ||
} | ||
|
||
const matched = micromatch(possibleExportPaths, pattern).map((match) => | ||
path.posix.join(pkgName, match), | ||
) | ||
matched.unshift(pkgName) | ||
return matched | ||
} else { | ||
// for packages without exports, we can do a simple glob | ||
const matched = glob | ||
.sync(pattern, { cwd: pkgData.dir, ignore: ['node_modules'] }) | ||
.map((match) => path.posix.join(pkgName, slash(match))) | ||
matched.unshift(pkgName) | ||
return matched | ||
} | ||
} | ||
|
||
function getFirstExportStringValue( | ||
obj: string | string[] | Record<string, any>, | ||
): string | undefined { | ||
if (typeof obj === 'string') { | ||
return obj | ||
} else if (Array.isArray(obj)) { | ||
return obj[0] | ||
} else { | ||
for (const key in obj) { | ||
return getFirstExportStringValue(obj[key]) | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Continuously resolve the basedir of packages separated by '>' | ||
*/ | ||
function nestedResolveBasedir( | ||
id: string, | ||
basedir: string, | ||
preserveSymlinks = false, | ||
) { | ||
const pkgs = id.split('>').map((pkg) => pkg.trim()) | ||
for (const pkg of pkgs) { | ||
basedir = resolvePackageData(pkg, basedir, preserveSymlinks)?.dir || basedir | ||
} | ||
return basedir | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
11 changes: 11 additions & 0 deletions
11
playground/optimize-deps/dep-optimize-exports-with-glob/package.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
{ | ||
"name": "@vitejs/test-dep-optimize-exports-with-glob", | ||
"private": true, | ||
"version": "1.0.0", | ||
"type": "module", | ||
"exports": { | ||
".": "./index.js", | ||
"./named": "./named.js", | ||
"./glob-dir/*": "./glob/*.js" | ||
} | ||
} |
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
{ | ||
"name": "@vitejs/test-dep-optimize-with-glob", | ||
"private": true, | ||
"version": "1.0.0", | ||
"type": "module" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.