Skip to content

Commit

Permalink
feat: support Pre-Bundling
Browse files Browse the repository at this point in the history
  • Loading branch information
caoxiemeihao committed Apr 28, 2023
1 parent 966eca5 commit c4ef2f6
Showing 1 changed file with 173 additions and 123 deletions.
296 changes: 173 additions & 123 deletions src/index.ts
@@ -1,12 +1,12 @@
import fs from 'node:fs'
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 { parse as parseAst } from 'acorn'
import fastGlob from 'fast-glob'
import { DEFAULT_EXTENSIONS } from 'vite-plugin-utils/constant'
import { MagicString, relativeify } from 'vite-plugin-utils/function'
Expand Down Expand Up @@ -71,133 +71,184 @@ export default function dynamicImport(options: Options = {}): Plugin {
resolve = new Resolve(_config)
// https://github.com/vitejs/vite/blob/v4.3.0/packages/vite/src/node/config.ts#L498
if (config.resolve?.extensions) extensions = config.resolve.extensions

// esbuild plugin for Vite's Pre-Bundling
_config.optimizeDeps.esbuildOptions ??= {}
_config.optimizeDeps.esbuildOptions.plugins ??= []
_config.optimizeDeps.esbuildOptions.plugins.push({
name: 'vite-plugin-dynamic-import:pre-bundle',
setup(build) {
build.onLoad({ filter: /.*/ }, async ({ path: id }) => {
let code: string
try {
code = fs.readFileSync(id, 'utf8')
} catch (error) {
return
}

const contents = await transformDynamicImport({
options,
code,
id,
resolve,
extensions,
})

if (contents != null) {
return { contents }
}
})
},
})
},
async transform(code, id) {
if (!hasDynamicImport(code)) return

const userCondition = options.filter?.(id)
if (userCondition === false) return
// exclude `node_modules` by default
// here can only get the files in `node_modules/.vite` and `node_modules/vite/dist/client`
if (userCondition !== true && id.includes('node_modules')) return

// 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
}
return transformDynamicImport({
options,
code,
id,
resolve,
extensions,
})
},
}
}

if (!imports.length) {
return null
}
async function transformDynamicImport({
options,
code,
id,
resolve,
extensions,
}: {
options: Options,
code: string,
id: string,
resolve: Resolve,
extensions: string[],
}) {
if (!hasDynamicImport(code)) return

const userCondition = options.filter?.(id)
if (userCondition === false) return
// exclude `node_modules` by default
// here can only get the files in `node_modules/.vite` and `node_modules/vite/dist/client`
if (userCondition !== true && id.includes('node_modules')) return

// 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
}

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

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
const importExpressionAst: AcornNode = this.parse(importExpression).body[0]./* ImportExpression */expression

// maybe `import.meta`
if (importExpressionAst.type !== 'ImportExpression') continue

if (importExpressionAst.source.type === 'Literal') {
const importee = rawImportee.slice(1, -1)
// normally importee
if (normallyImporteeRE.test(importee)) continue

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
}
}

const globResult = await globFiles(
importExpressionAst,
importExpression,
id,
resolve,
extensions,
options.loose !== false,
)
if (!globResult) continue

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 (!imports.length) {
return null
}

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

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

if (runtimeFunctions.length) {
ms.append([
'// [vite-plugin-dynamic-import] runtime -S-',
...runtimeFunctions,
'// [vite-plugin-dynamic-import] runtime -E-',
].join('\n'))
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

const ast = parseAst(importExpression, { ecmaVersion: 2020 }) as AcornNode
const importExpressionAst = ast.body[0]./* ImportExpression */expression as AcornNode

// maybe `import.meta`
if (importExpressionAst.type !== 'ImportExpression') continue

if (importExpressionAst.source.type === 'Literal') {
const importee = rawImportee.slice(1, -1)
// normally importee
if (normallyImporteeRE.test(importee)) continue

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
}
}

const str = ms.toString()
return str === code ? null : str
},
const globResult = await globFiles(
importExpressionAst,
importExpression,
id,
resolve,
extensions,
options.loose !== false,
)
if (!globResult) continue

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([
'// [vite-plugin-dynamic-import] runtime -S-',
...runtimeFunctions,
'// [vite-plugin-dynamic-import] runtime -E-',
].join('\n'))
}

const str = ms.toString()
return str !== code ? str : null
}

async function globFiles(
Expand Down Expand Up @@ -258,8 +309,7 @@ async function globFiles(
return
}

// @ts-ignore
const globs = [].concat(loose ? toLooseGlob(glob) : glob)
const globs = [].concat(loose ? toLooseGlob(glob) as any : glob)
.map((g: any) => {
g.includes(PAHT_FILL) && (g = g.replace(PAHT_FILL, ''))
g.endsWith(EXT_FILL) && (g = g.replace(EXT_FILL, ''))
Expand Down

0 comments on commit c4ef2f6

Please sign in to comment.