From 8e1d3d82f6d3ba9e48adb60a1b016c35f0aab627 Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 29 Jan 2021 19:13:35 -0500 Subject: [PATCH] fix(optimizer): separate dep entry proxy modules from actual modules --- .../src/node/optimizer/esbuildDepPlugin.ts | 109 +++++++++++------- packages/vite/src/node/optimizer/index.ts | 22 +++- 2 files changed, 84 insertions(+), 47 deletions(-) diff --git a/packages/vite/src/node/optimizer/esbuildDepPlugin.ts b/packages/vite/src/node/optimizer/esbuildDepPlugin.ts index 0cbe5d2748a388..67b8b421829a39 100644 --- a/packages/vite/src/node/optimizer/esbuildDepPlugin.ts +++ b/packages/vite/src/node/optimizer/esbuildDepPlugin.ts @@ -1,10 +1,10 @@ -import fs from 'fs' import path from 'path' import { Loader, Plugin } from 'esbuild' import { knownAssetTypes } from '../constants' import { ResolvedConfig } from '..' -import { bareImportRE, isRunningWithYarnPnp, flattenId } from '../utils' +import { isRunningWithYarnPnp, flattenId } from '../utils' import { browserExternalId } from '../plugins/resolve' +import { ExportsData } from '.' const externalTypes = [ 'css', @@ -23,6 +23,7 @@ const externalTypes = [ export function esbuildDepPlugin( qualified: Record, + exportsData: Record, config: ResolvedConfig ): Plugin { const _resolve = config.createResolver({ asSrc: false }) @@ -55,62 +56,88 @@ export function esbuildDepPlugin( } ) + function resolveEntry(id: string, isEntry: boolean) { + const flatId = flattenId(id) + if (flatId in qualified) { + return isEntry + ? { + path: flatId, + namespace: 'dep' + } + : { + path: path.resolve(qualified[flatId]) + } + } + } + build.onResolve( { filter: /^[\w@][^:]/ }, async ({ path: id, importer }) => { - // ensure esbuild uses our resolved entires of optimized deps in all - // cases - const flatId = flattenId(id) - if (flatId in qualified) { - // if is optimized entry, redirect to entry namespace - return { - path: flatId, - namespace: 'dep' - } - } else { - // check alias fist - const aliased = await _resolve(id, undefined, true) - if (aliased && bareImportRE.test(aliased)) { - const flatId = flattenId(aliased) - if (flatId in qualified) { - // #1780 - // id was aliased to a qualified entry, use the entry to - // avoid duplicated copies of the module - return { - path: flatId, - namespace: 'dep' - } - } - } + const isEntry = !importer + // ensure esbuild uses our resolved entires + let entry + // if this is an entry, return entry namespace resolve result + if ((entry = resolveEntry(id, isEntry))) return entry - // use vite resolver - const resolved = await resolve(id, importer) - if (resolved) { - if (resolved.startsWith(browserExternalId)) { - return { - path: id, - namespace: 'browser-external' - } - } + // check if this is aliased to an entry - also return entry namespace + const aliased = await _resolve(id, undefined, true) + if (aliased && (entry = resolveEntry(aliased, isEntry))) { + return entry + } + + // use vite resolver + const resolved = await resolve(id, importer) + if (resolved) { + if (resolved.startsWith(browserExternalId)) { return { - path: path.resolve(resolved) + path: id, + namespace: 'browser-external' } } + return { + path: path.resolve(resolved) + } } } ) - // for entry files, we'll read it ourselves to retain the entry's raw id - // instead of file path - // so that esbuild outputs desired output file structure. + // For entry files, we'll read it ourselves and construct a proxy module + // to retain the entry's raw id instead of file path so that esbuild + // outputs desired output file structure. + // It is necessary to do the re-exporting to separate the virtual proxy + // module from the actual module since the actual module may get + // referenced via relative imports - if we don't separate the proxy and + // the actual module, esbuild will create duplicated copies of the same + // module! + const root = path.resolve(config.root) build.onLoad({ filter: /.*/, namespace: 'dep' }, ({ path: id }) => { const entryFile = qualified[id] + + let relativePath = path.relative(root, entryFile) + if (!relativePath.startsWith('.')) { + relativePath = `./${relativePath}` + } + + let contents = '' + const [imports, exports] = exportsData[id] + if (!imports.length && !exports.length) { + // cjs + contents += `import d from "${relativePath}";export default d;` + } else { + if (exports.includes('default')) { + contents += `import d from "${relativePath}";export default d;` + } + if (exports.length > 1 || exports[0] !== 'default') { + contents += `\nexport * from "${relativePath}"` + } + } + let ext = path.extname(entryFile).slice(1) if (ext === 'mjs') ext = 'js' return { loader: ext as Loader, - contents: fs.readFileSync(entryFile, 'utf-8'), - resolveDir: path.dirname(entryFile) + contents, + resolveDir: root } }) diff --git a/packages/vite/src/node/optimizer/index.ts b/packages/vite/src/node/optimizer/index.ts index 0a2e793ecb1f27..4f9e883c0debf7 100644 --- a/packages/vite/src/node/optimizer/index.ts +++ b/packages/vite/src/node/optimizer/index.ts @@ -12,12 +12,14 @@ import { flattenId } from '../utils' import { esbuildDepPlugin } from './esbuildDepPlugin' -import { init, parse } from 'es-module-lexer' +import { ImportSpecifier, init, parse } from 'es-module-lexer' import { scanImports } from './scan' import { ensureService, stopService } from '../plugins/esbuild' const debug = createDebugger('vite:deps') +export type ExportsData = [ImportSpecifier[], string[]] + export interface DepOptimizationOptions { /** * By default, Vite will crawl your index.html to detect dependencies that @@ -194,8 +196,16 @@ export async function optimizeDeps( // 2. in the plugin, read the entry ourselves as virtual files to retain the // path. const flatIdDeps: Record = {} + const idToExports: Record = {} + const flatIdToExports: Record = {} + + await init for (const id in deps) { - flatIdDeps[flattenId(id)] = deps[id] + const flatId = flattenId(id) + flatIdDeps[flatId] = deps[id] + const exportsData = parse(fs.readFileSync(deps[id], 'utf-8')) + idToExports[id] = exportsData + flatIdToExports[flatId] = exportsData } const start = Date.now() @@ -214,18 +224,17 @@ export async function optimizeDeps( define: { 'process.env.NODE_ENV': '"development"' }, - plugins: [esbuildDepPlugin(flatIdDeps, config)] + plugins: [esbuildDepPlugin(flatIdDeps, flatIdToExports, config)] }) const meta = JSON.parse(fs.readFileSync(esbuildMetaPath, 'utf-8')) - await init for (const id in deps) { const entry = deps[id] data.optimized[id] = { file: normalizePath(path.resolve(cacheDir, flattenId(id) + '.js')), src: entry, - needsInterop: needsInterop(id, entry, meta.outputs) + needsInterop: needsInterop(id, entry, idToExports[id], meta.outputs) } } @@ -246,12 +255,13 @@ const KNOWN_INTEROP_IDS = new Set(['moment']) function needsInterop( id: string, entry: string, + exportsData: ExportsData, outputs: Record ): boolean { if (KNOWN_INTEROP_IDS.has(id)) { return true } - const [imports, exports] = parse(fs.readFileSync(entry, 'utf-8')) + const [imports, exports] = exportsData // entry has no ESM syntax - likely CJS or UMD if (!exports.length && !imports.length) { return true