Skip to content

Commit

Permalink
fix(optimizer): separate dep entry proxy modules from actual modules
Browse files Browse the repository at this point in the history
  • Loading branch information
yyx990803 committed Jan 30, 2021
1 parent 3c22f84 commit 8e1d3d8
Show file tree
Hide file tree
Showing 2 changed files with 84 additions and 47 deletions.
109 changes: 68 additions & 41 deletions packages/vite/src/node/optimizer/esbuildDepPlugin.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -23,6 +23,7 @@ const externalTypes = [

export function esbuildDepPlugin(
qualified: Record<string, string>,
exportsData: Record<string, ExportsData>,
config: ResolvedConfig
): Plugin {
const _resolve = config.createResolver({ asSrc: false })
Expand Down Expand Up @@ -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
}
})

Expand Down
22 changes: 16 additions & 6 deletions packages/vite/src/node/optimizer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<string, string> = {}
const idToExports: Record<string, ExportsData> = {}
const flatIdToExports: Record<string, ExportsData> = {}

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()
Expand All @@ -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)
}
}

Expand All @@ -246,12 +255,13 @@ const KNOWN_INTEROP_IDS = new Set(['moment'])
function needsInterop(
id: string,
entry: string,
exportsData: ExportsData,
outputs: Record<string, any>
): 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
Expand Down

0 comments on commit 8e1d3d8

Please sign in to comment.