From 07ad00fc11c50a50466db28ad27b089f5e1ee3d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BF=A0=20/=20green?= Date: Tue, 19 Nov 2024 19:03:23 +0900 Subject: [PATCH] feat: use native Vite resolver (#65) --- packages/vite/src/node/idResolver.ts | 1 + packages/vite/src/node/plugin.ts | 12 +- packages/vite/src/node/plugins/index.ts | 45 +-- packages/vite/src/node/plugins/resolve.ts | 318 +++++++++++++++++-- packages/vite/src/node/server/environment.ts | 10 +- playground/resolve/__tests__/resolve.spec.ts | 6 +- playground/resolve/index.html | 15 +- 7 files changed, 345 insertions(+), 62 deletions(-) diff --git a/packages/vite/src/node/idResolver.ts b/packages/vite/src/node/idResolver.ts index b2b7e567b145ce..98472094c588dc 100644 --- a/packages/vite/src/node/idResolver.ts +++ b/packages/vite/src/node/idResolver.ts @@ -61,6 +61,7 @@ export function createIdResolver( [ // @ts-expect-error the aliasPlugin uses rollup types aliasPlugin({ entries: environment.config.resolve.alias }), + // TODO: use oxcResolvePlugin here as well resolvePlugin({ root: config.root, isProduction: config.isProduction, diff --git a/packages/vite/src/node/plugin.ts b/packages/vite/src/node/plugin.ts index b7634178ee7dc6..d1a4a2464a7590 100644 --- a/packages/vite/src/node/plugin.ts +++ b/packages/vite/src/node/plugin.ts @@ -349,9 +349,19 @@ export type PluginOption = Thenable export async function resolveEnvironmentPlugins( environment: PartialEnvironment, +): Promise { + return resolveEnvironmentPluginsRaw( + environment.getTopLevelConfig().plugins, + environment, + ) +} + +export async function resolveEnvironmentPluginsRaw( + plugins: readonly Plugin[], + environment: PartialEnvironment, ): Promise { const environmentPlugins: Plugin[] = [] - for (const plugin of environment.getTopLevelConfig().plugins) { + for (const plugin of plugins) { if (plugin.applyToEnvironment) { const applied = await plugin.applyToEnvironment(environment) if (!applied) { diff --git a/packages/vite/src/node/plugins/index.ts b/packages/vite/src/node/plugins/index.ts index 968dd94b586eab..097d790e4aed04 100644 --- a/packages/vite/src/node/plugins/index.ts +++ b/packages/vite/src/node/plugins/index.ts @@ -20,7 +20,7 @@ import { } from '../plugin' import { watchPackageDataPlugin } from '../packages' import { jsonPlugin } from './json' -import { filteredResolvePlugin, resolvePlugin } from './resolve' +import { oxcResolvePlugin, resolvePlugin } from './resolve' import { optimizedDepsPlugin } from './optimizedDeps' import { importAnalysisPlugin } from './importAnalysis' import { cssAnalysisPlugin, cssPlugin, cssPostPlugin } from './css' @@ -96,25 +96,30 @@ export async function resolvePlugins( ) : modulePreloadPolyfillPlugin(config) : null, - enableNativePlugin - ? filteredResolvePlugin({ - root: config.root, - isProduction: config.isProduction, - isBuild, - packageCache: config.packageCache, - asSrc: true, - optimizeDeps: true, - externalize: true, - }) - : resolvePlugin({ - root: config.root, - isProduction: config.isProduction, - isBuild, - packageCache: config.packageCache, - asSrc: true, - optimizeDeps: true, - externalize: true, - }), + ...(enableNativePlugin + ? oxcResolvePlugin( + { + root: config.root, + isProduction: config.isProduction, + isBuild, + packageCache: config.packageCache, + asSrc: true, + optimizeDeps: true, + externalize: true, + }, + isWorker ? config : undefined, + ) + : [ + resolvePlugin({ + root: config.root, + isProduction: config.isProduction, + isBuild, + packageCache: config.packageCache, + asSrc: true, + optimizeDeps: true, + externalize: true, + }), + ]), htmlInlineProxyPlugin(config), cssPlugin(config), config.oxc !== false diff --git a/packages/vite/src/node/plugins/resolve.ts b/packages/vite/src/node/plugins/resolve.ts index cbeed8e2f3c99d..9dd90fb5f82888 100644 --- a/packages/vite/src/node/plugins/resolve.ts +++ b/packages/vite/src/node/plugins/resolve.ts @@ -5,7 +5,8 @@ import colors from 'picocolors' import type { PartialResolvedId, RolldownPlugin } from 'rolldown' import { exports, imports } from 'resolve.exports' import { hasESMSyntax } from 'mlly' -import type { Plugin } from '../plugin' +import { viteResolvePlugin } from 'rolldown/experimental' +import { type Plugin, perEnvironmentPlugin } from '../plugin' import { CLIENT_ENTRY, DEP_VERSION_RE, @@ -35,7 +36,7 @@ import { } from '../utils' import { optimizedDepInfoFromFile, optimizedDepInfoFromId } from '../optimizer' import type { DepsOptimizer } from '../optimizer' -import type { SSROptions } from '..' +import type { Environment, ResolvedConfig, SSROptions } from '..' import type { PackageCache, PackageData } from '../packages' import { canExternalizeFile, shouldExternalize } from '../external' import { @@ -51,6 +52,7 @@ import { splitFileAndPostfix, withTrailingSlash, } from '../../shared/utils' +import type { ResolvedEnvironmentOptions } from '../config' const normalizedClientEntry = normalizePath(CLIENT_ENTRY) const normalizedEnvEntry = normalizePath(ENV_ENTRY) @@ -178,37 +180,275 @@ export interface ResolvePluginOptionsWithOverrides extends ResolveOptions, ResolvePluginOptions {} -export function filteredResolvePlugin( +const perEnvironmentOrWorkerPlugin = ( + name: string, + configIfWorker: ResolvedConfig | undefined, + f: (env: { + name: string + config: ResolvedConfig & ResolvedEnvironmentOptions + }) => Plugin, +): Plugin => { + if (configIfWorker) { + return f({ + name: 'client', + config: { ...configIfWorker, consumer: 'client' }, + }) + } + return perEnvironmentPlugin(name, f) +} + +export function oxcResolvePlugin( + resolveOptions: ResolvePluginOptionsWithOverrides, + configIfWorker: ResolvedConfig | undefined, +): (RolldownPlugin | Plugin)[] { + return [ + optimizerResolvePlugin(resolveOptions), + importGlobSubpathImportsResolvePlugin(resolveOptions), + perEnvironmentOrWorkerPlugin( + 'vite:resolve-builtin', + configIfWorker, + (env) => { + const environment = env as Environment + // The resolve plugin is used for createIdResolver and the depsOptimizer should be + // disabled in that case, so deps optimization is opt-in when creating the plugin. + const depsOptimizer = + resolveOptions.optimizeDeps && environment?.mode === 'dev' + ? environment.depsOptimizer + : undefined + + const options: InternalResolveOptions = { + ...environment.config.resolve, + ...resolveOptions, // plugin options + resolve options overrides + } + const noExternal = + Array.isArray(options.noExternal) || options.noExternal === true + ? options.noExternal + : [options.noExternal] + if ( + Array.isArray(noExternal) && + noExternal.some((e) => typeof e !== 'string') + ) { + throw new Error('RegExp is not supported for noExternal for now') + } + const filteredNoExternal = noExternal as true | string[] + + return viteResolvePlugin({ + resolveOptions: { + isBuild: options.isBuild, + isProduction: options.isProduction, + asSrc: options.asSrc ?? false, + preferRelative: options.preferRelative ?? false, + root: options.root, + scan: options.scan ?? false, + + mainFields: options.mainFields, + conditions: options.conditions, + externalConditions: options.externalConditions, + extensions: options.extensions, + tryIndex: options.tryIndex ?? true, + tryPrefix: options.tryPrefix, + preserveSymlinks: options.preserveSymlinks, + }, + environmentConsumer: environment.config.consumer, + environmentName: environment.name, + external: options.external, + noExternal: filteredNoExternal, + finalizeBareSpecifier: !depsOptimizer + ? undefined + : (resolvedId, rawId, importer) => { + // if we reach here, it's a valid dep import that hasn't been optimized. + const isJsType = isOptimizable( + resolvedId, + depsOptimizer.options, + ) + const exclude = depsOptimizer?.options.exclude + + // check for deep import, e.g. "my-lib/foo" + const deepMatch = deepImportRE.exec(rawId) + // package name doesn't include postfixes + // trim them to support importing package with queries (e.g. `import css from 'normalize.css?inline'`) + const pkgId = deepMatch + ? deepMatch[1] || deepMatch[2] + : cleanUrl(rawId) + + const skipOptimization = + depsOptimizer.options.noDiscovery || + !isJsType || + (importer && isInNodeModules(importer)) || + exclude?.includes(pkgId) || + exclude?.includes(rawId) || + SPECIAL_QUERY_RE.test(resolvedId) + + let newId = resolvedId + if (skipOptimization) { + // excluded from optimization + // Inject a version query to npm deps so that the browser + // can cache it without re-validation, but only do so for known js types. + // otherwise we may introduce duplicated modules for externalized files + // from pre-bundled deps. + const versionHash = depsOptimizer!.metadata.browserHash + if (versionHash && isJsType) { + newId = injectQuery(newId, `v=${versionHash}`) + } + } else { + // this is a missing import, queue optimize-deps re-run and + // get a resolved its optimized info + const optimizedInfo = depsOptimizer!.registerMissingImport( + rawId, + newId, + ) + newId = depsOptimizer!.getOptimizedDepId(optimizedInfo) + } + return newId + }, + finalizeOtherSpecifiers(resolvedId, rawId) { + return ensureVersionQuery(resolvedId, rawId, options, depsOptimizer) + }, + }) as unknown as Plugin + }, + ), + ] +} + +function optimizerResolvePlugin( resolveOptions: ResolvePluginOptionsWithOverrides, ): RolldownPlugin { - const originalPlugin = resolvePlugin(resolveOptions) + const { root, asSrc } = resolveOptions return { - name: 'vite:resolve', - options(option) { - option.resolve ??= {} - option.resolve.extensions = this.environment.config.resolve.extensions - option.resolve.extensionAlias = { - '.js': ['.ts', '.tsx', '.js'], - '.jsx': ['.ts', '.tsx', '.jsx'], - '.mjs': ['.mts', '.mjs'], - '.cjs': ['.cts', '.cjs'], - } + name: 'vite:resolve-dev', + ...({ + apply: 'serve', + } satisfies Plugin), + resolveId: { + filter: { + id: { + exclude: [/^\0/, /^virtual:/, /^\/virtual:/, /^__vite-/], + }, + }, + async handler(id, importer, resolveOpts) { + if ( + id[0] === '\0' || + id.startsWith('virtual:') || + // When injected directly in html/client code + id.startsWith('/virtual:') || + id.startsWith('__vite-') + ) { + return + } + + // The resolve plugin is used for createIdResolver and the depsOptimizer should be + // disabled in that case, so deps optimization is opt-in when creating the plugin. + const depsOptimizer = + resolveOptions.optimizeDeps && this.environment.mode === 'dev' + ? this.environment.depsOptimizer + : undefined + if (!depsOptimizer) { + return + } + + const options: InternalResolveOptions = { + isRequire: resolveOpts.kind === 'require-call', + ...this.environment.config.resolve, + ...resolveOptions, + // @ts-expect-error scan exists + scan: resolveOpts.scan ?? resolveOptions.scan, + } + options.preferRelative ||= importer?.endsWith('.html') + + // resolve pre-bundled deps requests, these could be resolved by + // tryFileResolve or /fs/ resolution but these files may not yet + // exists if we are in the middle of a deps re-processing + if (asSrc && depsOptimizer.isOptimizedDepUrl(id)) { + const optimizedPath = id.startsWith(FS_PREFIX) + ? fsPathFromId(id) + : normalizePath(path.resolve(root, id.slice(1))) + return optimizedPath + } + + if (!isDataUrl(id) && !isExternalUrl(id)) { + if ( + id[0] === '.' || + (options.preferRelative && startsWithWordCharRE.test(id)) + ) { + const basedir = importer ? path.dirname(importer) : root + const fsPath = path.resolve(basedir, id) + // handle browser field mapping for relative imports + + const normalizedFsPath = normalizePath(fsPath) + + if (depsOptimizer.isOptimizedDepFile(normalizedFsPath)) { + // Optimized files could not yet exist in disk, resolve to the full path + // Inject the current browserHash version if the path doesn't have one + if (!DEP_VERSION_RE.test(normalizedFsPath)) { + const browserHash = optimizedDepInfoFromFile( + depsOptimizer.metadata, + normalizedFsPath, + )?.browserHash + if (browserHash) { + return injectQuery(normalizedFsPath, `v=${browserHash}`) + } + } + return normalizedFsPath + } + } + + // bare package imports, perform node resolve + if (bareImportRE.test(id)) { + let res: string | PartialResolvedId | undefined + if ( + asSrc && + !options.scan && + (res = await tryOptimizedResolve( + depsOptimizer, + id, + importer, + options.preserveSymlinks, + options.packageCache, + )) + ) { + return res + } + } + } + }, }, + } +} + +function importGlobSubpathImportsResolvePlugin( + resolveOptions: ResolvePluginOptionsWithOverrides, +): RolldownPlugin { + const { root } = resolveOptions + + return { + name: 'vite:resolve-import-glob-subpath-imports', resolveId: { filter: { id: { - exclude: [ - // relative paths without query - /^\.\.?[/\\][^?]+$/, - /^(?:\0|\/?virtual:)/, - ], + include: [/^#/], }, }, - // @ts-expect-error the options is incompatible - handler: originalPlugin.resolveId!, + handler(id, importer, resolveOpts) { + const options: InternalResolveOptions = { + isRequire: resolveOpts.kind === 'require-call', + ...this.environment.config.resolve, + ...resolveOptions, + // @ts-expect-error scan exists + scan: resolveOpts.scan ?? resolveOptions.scan, + } + options.preferRelative ||= importer?.endsWith('.html') + + if (id.startsWith(subpathImportsPrefix)) { + if (resolveOpts.custom?.['vite:import-glob']?.isSubImportsPattern) { + const resolvedImports = resolveSubpathImports(id, importer, options) + if (resolvedImports) { + return normalizePath(path.join(root, resolvedImports)) + } + } + } + }, }, - load: originalPlugin.load, } } @@ -1046,25 +1286,39 @@ function packageEntryFailure(id: string, details?: string) { throw err } -function resolveExportsOrImports( - pkg: PackageData['data'], - key: string, - options: InternalResolveOptions, - type: 'imports' | 'exports', +function getConditions( + conditions: string[], + isProduction: boolean, + isRequire: boolean | undefined, ) { - const conditions = options.conditions.map((condition) => { + const resolvedConditions = conditions.map((condition) => { if (condition === DEV_PROD_CONDITION) { - return options.isProduction ? 'production' : 'development' + return isProduction ? 'production' : 'development' } return condition }) - if (options.isRequire) { - conditions.push('require') + if (isRequire) { + resolvedConditions.push('require') } else { - conditions.push('import') + resolvedConditions.push('import') } + return resolvedConditions +} + +function resolveExportsOrImports( + pkg: PackageData['data'], + key: string, + options: InternalResolveOptions, + type: 'imports' | 'exports', +) { + const conditions = getConditions( + options.conditions, + options.isProduction, + options.isRequire, + ) + const fn = type === 'imports' ? imports : exports const result = fn(pkg, key, { conditions, unsafe: true }) return result ? result[0] : undefined diff --git a/packages/vite/src/node/server/environment.ts b/packages/vite/src/node/server/environment.ts index f42d305af65bd1..353fe1f310cf11 100644 --- a/packages/vite/src/node/server/environment.ts +++ b/packages/vite/src/node/server/environment.ts @@ -1,6 +1,10 @@ import type { FetchFunctionOptions, FetchResult } from 'vite/module-runner' import type { FSWatcher } from 'dep-types/chokidar' import colors from 'picocolors' +import { + isCallableCompatibleBuiltinPlugin, + makeBuiltinPluginCallable, +} from 'rolldown/experimental' import { BaseEnvironment, getDefaultResolvedEnvironmentOptions, @@ -167,7 +171,11 @@ export class DevEnvironment extends BaseEnvironment { return } this._initiated = true - this._plugins = await resolveEnvironmentPlugins(this) + this._plugins = (await resolveEnvironmentPlugins(this)).map((plugin) => + isCallableCompatibleBuiltinPlugin(plugin) + ? makeBuiltinPluginCallable(plugin) + : plugin, + ) this._pluginContainer = await createEnvironmentPluginContainer( this, this._plugins, diff --git a/playground/resolve/__tests__/resolve.spec.ts b/playground/resolve/__tests__/resolve.spec.ts index e12ddecedb5c0d..f87725ef35a5f3 100644 --- a/playground/resolve/__tests__/resolve.spec.ts +++ b/playground/resolve/__tests__/resolve.spec.ts @@ -135,11 +135,13 @@ test('Resolve browser field even if module field exists', async () => { expect(await page.textContent('.browser-module1')).toMatch('[success]') }) -test('Resolve module field if browser field is likely UMD or CJS', async () => { +// should not fallback +test.skip('Resolve module field if browser field is likely UMD or CJS', async () => { expect(await page.textContent('.browser-module2')).toMatch('[success]') }) -test('Resolve module field if browser field is likely IIFE', async () => { +// should not fallback +test.skip('Resolve module field if browser field is likely IIFE', async () => { expect(await page.textContent('.browser-module3')).toMatch('[success]') }) diff --git a/playground/resolve/index.html b/playground/resolve/index.html index d42d1a39904fe1..79f188c2c2eead 100644 --- a/playground/resolve/index.html +++ b/playground/resolve/index.html @@ -313,7 +313,8 @@

resolve non normalized absolute path

import c from '@vitejs/test-resolve-browser-field/ext' import d from '@vitejs/test-resolve-browser-field/ext.js' import e from '@vitejs/test-resolve-browser-field/ext-index/index.js' - import f from '@vitejs/test-resolve-browser-field/ext-index' + // webpack does not support, so should be fine + // import f from '@vitejs/test-resolve-browser-field/ext-index' import g from '@vitejs/test-resolve-browser-field/no-ext-index/index.js' // no substitution import h from '@vitejs/test-resolve-browser-field/no-ext?query' import i from '@vitejs/test-resolve-browser-field/bare-import' @@ -327,7 +328,7 @@

resolve non normalized absolute path

rf, } from '@vitejs/test-resolve-browser-field/relative' - const success = [main, a, c, d, e, f, h, i, ra, rc, rd, re] + const success = [main, a, c, d, e, h, i, ra, rc, rd, re] const noSuccess = [b, g, rb, rf] if ( @@ -340,11 +341,13 @@

resolve non normalized absolute path

import browserModule1 from '@vitejs/test-resolve-browser-module-field1' text('.browser-module1', browserModule1) - import browserModule2 from '@vitejs/test-resolve-browser-module-field2' - text('.browser-module2', browserModule2) + // should not fallback + // import browserModule2 from '@vitejs/test-resolve-browser-module-field2' + // text('.browser-module2', browserModule2) - import browserModule3 from '@vitejs/test-resolve-browser-module-field3' - text('.browser-module3', browserModule3) + // should not fallback + // import browserModule3 from '@vitejs/test-resolve-browser-module-field3' + // text('.browser-module3', browserModule3) import { msg as requireButWithModuleFieldMsg } from '@vitejs/test-require-pkg-with-module-field' text('.require-pkg-with-module-field', requireButWithModuleFieldMsg)