Skip to content

Commit

Permalink
refactor: performance, support node_modules, custom resolve importee
Browse files Browse the repository at this point in the history
  • Loading branch information
caoxiemeihao committed Apr 27, 2023
1 parent 8dd5cac commit 17b1eb0
Showing 1 changed file with 113 additions and 100 deletions.
213 changes: 113 additions & 100 deletions src/index.ts
Expand Up @@ -2,13 +2,14 @@ import path from 'node:path'
import type { AcornNode as AcornNode2 } from 'rollup'
export type AcornNode<T = any> = AcornNode2 & Record<string, T>
import type { Plugin, ResolvedConfig } from 'vite'
import {
type ImportSpecifier,
init as initParseImports,
parse as parseImports,
} from 'es-module-lexer'
import fastGlob from 'fast-glob'
import { DEFAULT_EXTENSIONS } from 'vite-plugin-utils/constant'
import {
MagicString,
walk,
relativeify,
} from 'vite-plugin-utils/function'
import { MagicString, relativeify } from 'vite-plugin-utils/function'

import {
hasDynamicImport,
Expand All @@ -32,17 +33,17 @@ export {
} from './utils'

export interface Options {
filter?: (id: string) => false | void
filter?: (id: string) => boolean | void
/**
* ```
* 1. `true` - Match all possibilities as much as possible, more like `webpack`
* see https://webpack.js.org/guides/dependency-management/#require-with-expression
*
* 2. `false` - It behaves more like `@rollup/plugin-dynamic-import-vars`
* see https://github.com/rollup/plugins/tree/master/packages/dynamic-import-vars#how-it-works
*
* default true
* ```
*
* @defaultValue true
*/
loose?: boolean
/**
Expand All @@ -51,10 +52,11 @@ export interface Options {
*/
onFiles?: (files: string[], id: string) => typeof files | void
/**
* It will add `@vite-ignore`
* `import(/*@vite-ignore* / 'import-path')`
* Custom importee
*
* e.g. - append `\/*@vite-ignore*\/` in front of importee to bypass to Vite
*/
viteIgnore?: (rawImportee: string, id: string) => true | void
onResolve?: (rawImportee: string, id: string) => typeof rawImportee | void
}

const PLUGIN_NAME = 'vite-plugin-dynamic-import'
Expand All @@ -69,99 +71,123 @@ export default function dynamicImport(options: Options = {}): Plugin {
configResolved(_config) {
config = _config
resolve = new Resolve(_config)
// https://github.com/vitejs/vite/blob/37ac91e5f680aea56ce5ca15ce1291adc3cbe05e/packages/vite/src/node/plugins/resolve.ts#L450
// https://github.com/vitejs/vite/blob/v4.3.0/packages/vite/src/node/config.ts#L498
if (config.resolve?.extensions) extensions = config.resolve.extensions
},
async transform(code, id) {
if (/node_modules\/(?!\.vite\/)/.test(id)) return
// TODO: better handling of `.vite` 🤔
if (/node_modules\/(?!\.vite\/)/.test(id) && !options.filter?.(id)) return
if (!extensions.includes(path.extname(id))) return
if (!hasDynamicImport(code)) return
if (options.filter?.(id) === false) return

const ast = this.parse(code)
// https://github.com/vitejs/vite/blob/v4.3.0/packages/vite/src/node/plugins/dynamicImportVars.ts#L179
await initParseImports

let imports: readonly ImportSpecifier[] = []
try {
imports = parseImports(code)[0]
} catch (e: any) {
// ignore as it might not be a JS file, the subsequent plugins will catch the error
return null
}

if (!imports.length) {
return null
}

const ms = new MagicString(code)
let dynamicImportIndex = 0
const runtimeFunctions: string[] = []

await walk(ast, {
for (let index = 0; index < imports.length; index++) {
const {
s: start,
e: end,
ss: expStart,
se: expEnd,
d: dynamicIndex,
} = imports[index]

if (dynamicIndex === -1) continue

const importExpression = code.slice(expStart, expEnd)
let rawImportee = code.slice(start, end)

// user custom importee
const userImportee = options.onResolve?.(rawImportee, id)
if (userImportee) {
rawImportee = userImportee
}

// skip @vite-ignore
// https://github.com/vitejs/vite/blob/v4.3.0/packages/vite/src/node/plugins/importAnalysis.ts#L663
if (viteIgnoreRE.test(importExpression)) continue

// @ts-ignore
async ImportExpression(node: AcornNode) {
const importStatement = code.slice(node.start, node.end)
const importeeRaw = code.slice(node.source.start, node.source.end)
const importExpressionAst: AcornNode = this.parse(importExpression).body[0]./* ImportExpression */expression

// skip @vite-ignore
if (viteIgnoreRE.test(importStatement)) return
if (importExpressionAst.source.type === 'Literal') {
const importee = rawImportee.slice(1, -1)
// normally importee
if (normallyImporteeRE.test(importee)) continue

// the user explicitly ignore this import
if (options.viteIgnore?.(importeeRaw, id)) {
ms.overwrite(node.source.start, node.source.start, '/*@vite-ignore*/') // append left
return
const rsld = await resolve.tryResolve(importee, id)
// alias or bare-module - 2.x
if (rsld && normallyImporteeRE.test(rsld.import.resolved)) {
ms.overwrite(expStart, expEnd, `import("${rsld.import.resolved}")`)
continue
}
}

if (node.source.type === 'Literal') {
const importee = importeeRaw.slice(1, -1)
// empty value
if (!importee) return
// normally importee
if (normallyImporteeRE.test(importee)) return

const rsld = await resolve.tryResolve(importee, id)
// alias or bare
if (rsld && normallyImporteeRE.test(rsld.import.resolved)) {
ms.overwrite(node.start, node.end, `import("${rsld.import.resolved}")`)
return
}
}
const globResult = await globFiles(
importExpressionAst,
importExpression,
id,
resolve,
extensions,
options.loose !== false,
)
if (!globResult) continue

const globResult = await globFiles(
node,
code,
id,
resolve,
extensions,
options.loose !== false,
)
if (!globResult) return

let { files, resolved, normally } = globResult
// skip itself
files = files!.filter(f => path.posix.join(path.dirname(id), f) !== id)
// execute the Options.onFiles
options.onFiles && (files = options.onFiles(files, id) || files)

if (normally) {
// normally importee (🚧-③ After `expressiontoglob()` processing)
ms.overwrite(node.start, node.end, `import('${normally}')`)
} else {
if (!files?.length) return
const mapAlias = resolved
? { [resolved.alias.relative]: resolved.alias.findString }
: undefined

const maps = mappingPath(files, mapAlias)
const runtimeName = `__variableDynamicImportRuntime${dynamicImportIndex++}__`
const runtimeFn = generateDynamicImportRuntime(maps, runtimeName)

// extension should be removed, because if the "index" file is in the directory, an error will occur
//
// e.g.
// ├─┬ views
// │ ├─┬ foo
// │ │ └── index.js
// │ └── bar.js
//
// the './views/*.js' should be matched ['./views/foo/index.js', './views/bar.js'], this may not be rigorous
ms.overwrite(node.start, node.end, `${runtimeName}(${importeeRaw})`)
runtimeFunctions.push(runtimeFn)
}
},
})
let { files, resolved, normally } = globResult
// skip itself
files = files!.filter(f => path.posix.join(path.dirname(id), f) !== id)
// execute the Options.onFiles
options.onFiles && (files = options.onFiles(files, id) || files)

if (normally) {
// normally importee (🚧-③ After `expressiontoglob()` processing)
ms.overwrite(expStart, expEnd, `import('${normally}')`)
} else {
if (!files?.length) continue
const mapAlias = resolved
? { [resolved.alias.relative]: resolved.alias.findString }
: undefined

const maps = mappingPath(files, mapAlias)
const runtimeName = `__variableDynamicImportRuntime${dynamicImportIndex++}__`
const runtimeFn = generateDynamicImportRuntime(maps, runtimeName)

// extension should be removed, because if the "index" file is in the directory, an error will occur
//
// e.g.
// ├─┬ views
// │ ├─┬ foo
// │ │ └── index.js
// │ └── bar.js
//
// the './views/*.js' should be matched ['./views/foo/index.js', './views/bar.js'], this may not be rigorous
ms.overwrite(expStart, expEnd, `${runtimeName}(${rawImportee})`)
runtimeFunctions.push(runtimeFn)
}
}

if (runtimeFunctions.length) {
ms.append([
'// ---- dynamic import runtime functions --S--',
'// [vite-plugin-dynamic-import] runtime -S-',
...runtimeFunctions,
'// ---- dynamic import runtime functions --E--',
'// [vite-plugin-dynamic-import] runtime -E-',
].join('\n'))
}

Expand All @@ -173,8 +199,8 @@ export default function dynamicImport(options: Options = {}): Plugin {

async function globFiles(
/** ImportExpression */
node: AcornNode,
code: string,
importExpressionAst: AcornNode,
importExpression: string,
importer: string,
resolve: Resolve,
extensions: string[],
Expand Down Expand Up @@ -202,8 +228,8 @@ async function globFiles(
let globRaw!: string

glob = await dynamicImportToGlob(
node.source,
code.slice(node.start, node.end),
importExpressionAst.source,
importExpression,
async (raw) => {
globRaw = raw
resolved = await resolve.tryResolve(raw, importer)
Expand Down Expand Up @@ -245,19 +271,6 @@ async function globFiles(
: g + `.{${extensions.map(e => e.replace(/^\./, '')).join(',')}}`
)

/*
loose && (glob = toLooseGlob(glob))
glob.includes(PAHT_FILL) && (glob = glob.replace(PAHT_FILL, ''))
glob.endsWith(EXT_FILL) && (glob = glob.replace(EXT_FILL, ''))
const fileGlob = path.extname(glob)
? glob
// If not ext is not specified, fill necessary extensions
// e.g.
// `./foo/*` -> `./foo/*.{js,ts,vue,...}`
: glob + `.{${extensions.map(e => e.replace(/^\./, '')).join(',')}}`
*/

files = fastGlob
.sync(fileGlobs, { cwd: /* 🚧-① */path.dirname(importer) })
.map(file => relativeify(file))
Expand Down

0 comments on commit 17b1eb0

Please sign in to comment.