UNPKG

vite-jsconfig-paths

Version:

Vite resolver for JavaScript compilerOptions.paths

220 lines (191 loc) 6.44 kB
import { dirname, isAbsolute, join, relative, resolve } from './path' import { normalizePath, Plugin } from 'vite' import { createMatchPathAsync } from 'tsconfig-paths' import { crawl } from 'recrawl-sync' import globRex from 'globrex' import { PluginOptions } from './types' import { loadConfig } from './config' import _debug from 'debug' const debug = _debug('vite-jsconfig-paths') type ViteResolve = (id: string, importer: string) => Promise<string | undefined> type Resolver = ( viteResolve: ViteResolve, id: string, importer: string ) => Promise<string | undefined> export default (opts: PluginOptions = {}): Plugin => { let resolvers: Resolver[] return { name: 'vite:tsconfig-paths', enforce: 'pre', configResolved(config) { const projects = findProjects(config.root, opts) const extensions = getFileExtensions(opts.extensions) debug('options:', { projects, extensions }) resolvers = projects .map((project) => createResolver(project, extensions)) .filter(Boolean) as Resolver[] }, async resolveId(id, importer) { if (importer && !relativeImportRE.test(id) && !isAbsolute(id)) { const viteResolve: ViteResolve = async (id, importer) => (await this.resolve(id, importer, { skipSelf: true }))?.id for (const resolve of resolvers) { const resolved = await resolve(viteResolve, id, importer) if (resolved) { return resolved } } } }, } function createResolver(root: string, extensions: string[]): Resolver | null { const configPath = root.endsWith('.json') ? root : null if (configPath) root = dirname(root) root += '/' const config = loadConfig(configPath || root) if (!config) { debug(`[!] config not found: "${configPath || root}"`) return null } const { baseUrl, paths } = config if (!baseUrl && !paths) { debug(`[!] missing baseUrl and paths: "${config.configPath}"`) return null } debug('config loaded:', config) const resolveWithBaseUrl: Resolver | undefined = baseUrl ? (viteResolve, id, importer) => viteResolve(join(baseUrl, id), importer) : undefined let resolveId: Resolver if (paths) { const matchPath = createMatchPathAsync(baseUrl ?? root, paths, mainFields) const resolveWithPaths: Resolver = (viteResolve, id, importer) => new Promise((done) => { matchPath(id, void 0, void 0, extensions, (error, path) => { if (path) { path = normalizePath(path) done(viteResolve(path, importer)) } else { error && debug(error.message) done(void 0) } }) }) if (resolveWithBaseUrl) { resolveId = (viteResolve, id, importer) => resolveWithPaths(viteResolve, id, importer).then((resolved) => { return resolved ?? resolveWithBaseUrl(viteResolve, id, importer) }) } else { resolveId = resolveWithPaths } } else { resolveId = resolveWithBaseUrl! } const isIncluded = getIncluder(config) const resolved = new Map<string, string>() return async (viteResolve, id, importer) => { // Skip virtual modules. if (id.includes('\0')) return importer = normalizePath(importer) const importerFile = importer.replace(/[#?].+$/, '') // Respect the include/exclude properties. if (!isIncluded(relative(root, importerFile))) return // Find and remove Vite's suffix (e.g. "?url") if present. // If the path is resolved, the suffix will be added back. const suffix = /\?.+$/.exec(id)?.[0] if (suffix) { id = id.slice(0, -suffix.length) } let path = resolved.get(id) if (!path) { path = await resolveId(viteResolve, id, importer) if (path) { resolved.set(id, path) debug(`resolved:`, { id, importer, resolvedId: path, configPath: config.configPath, }) } } return path && suffix ? path + suffix : path } } } const relativeImportRE = /^\.\.?(\/|$)/ const mainFields = ['module', 'jsnext', 'jsnext:main', 'browser', 'main'] const defaultInclude = ['**/*'] const defaultExclude = ['node_modules', 'bower_components', 'jspm_packages'] function compileGlob(glob: string) { if (!relativeImportRE.test(glob)) { glob = './' + glob } if (!glob.split('/').pop()!.includes('*')) { glob += '/**' } return globRex(glob, { extended: true, globstar: true, }).regex } /** * The returned function does not support absolute paths. * Be sure to call `path.relative` on your path first. */ function getIncluder({ include = defaultInclude, exclude = defaultExclude, outDir, }: { include?: string[] exclude?: string[] outDir?: string }) { if (outDir) { exclude = exclude.concat(outDir) } if (include.length || exclude.length) { const included = include.map(compileGlob) const excluded = exclude.map(compileGlob) debug(`compiled globs:`, { included, excluded }) return (path: string) => { path = path.replace(/\?.+$/, '') if (!relativeImportRE.test(path)) { path = './' + path } const test = (glob: RegExp) => glob.test(path) return included.some(test) && !excluded.some(test) } } return () => true } function findProjects(viteRoot: string, opts: PluginOptions) { const root = opts.root ? resolve(viteRoot, normalizePath(opts.root)) : viteRoot let { projects } = opts if (!projects) { debug(`crawling "${root}"`) projects = crawl(root, { only: ['jsconfig.json'], skip: ['node_modules', '.git'], }) } // Calculate the depth of each project path. const depthMap: { [path: string]: number } = {} projects = projects.map((projectPath) => { projectPath = resolve(root, normalizePath(projectPath)) depthMap[projectPath] = projectPath.split('/').length - (projectPath.endsWith('.json') ? 1 : 0) return projectPath }) // Ensure deeper projects take precedence. return projects.sort((a, b) => depthMap[b] - depthMap[a]) } function getFileExtensions(exts?: string[]) { const requiredExts = ['.ts', '.tsx', '.js', '.jsx', '.mjs'] return exts ? exts.concat(requiredExts) : requiredExts }