Skip to content

Commit

Permalink
feat: rework dynamic-import-vars (#7756)
Browse files Browse the repository at this point in the history
Co-authored-by: Anthony Fu <anthonyfu117@hotmail.com>
Co-authored-by: Bjorn Lu <bjornlu.dev@gmail.com>
  • Loading branch information
3 people committed May 10, 2022
1 parent 85cab70 commit 80d113b
Show file tree
Hide file tree
Showing 18 changed files with 372 additions and 36 deletions.
@@ -0,0 +1,13 @@
// Vitest Snapshot v1

exports[`parse positives > ? in url 1`] = `"__variableDynamicImportRuntimeHelper((import.meta.glob(\\"./mo?ds/*.js\\", {\\"as\\":\\"raw\\",\\"import\\":\\"*\\"})), \`./mo?ds/\${base ?? foo}.js\`)"`;
exports[`parse positives > ? in variables 1`] = `"__variableDynamicImportRuntimeHelper((import.meta.glob(\\"./mods/*.js\\", {\\"as\\":\\"raw\\",\\"import\\":\\"*\\"})), \`./mods/\${base ?? foo}.js\`)"`;
exports[`parse positives > alias path 1`] = `"__variableDynamicImportRuntimeHelper((import.meta.glob(\\"./mods/*.js\\")), \`./mods/\${base}.js\`)"`;
exports[`parse positives > basic 1`] = `"__variableDynamicImportRuntimeHelper((import.meta.glob(\\"./mods/*.js\\")), \`./mods/\${base}.js\`)"`;
exports[`parse positives > with query raw 1`] = `"__variableDynamicImportRuntimeHelper((import.meta.glob(\\"./mods/*.js\\", {\\"as\\":\\"raw\\",\\"import\\":\\"*\\"})), \`./mods/\${base}.js\`)"`;
exports[`parse positives > with query url 1`] = `"__variableDynamicImportRuntimeHelper((import.meta.glob(\\"./mods/*.js\\")), \`./mods/\${base}.js\`)"`;
@@ -0,0 +1,3 @@
export function hello() {
return 'hello'
}
@@ -0,0 +1,3 @@
export function hi() {
return 'hi'
}
@@ -0,0 +1,38 @@
import { describe, expect, it } from 'vitest'
import { transformDynamicImport } from '../../../plugins/dynamicImportVars'
import { resolve } from 'path'

async function run(input: string) {
const { glob, rawPattern } = await transformDynamicImport(
input,
resolve(__dirname, 'index.js'),
(id) => id.replace('@', resolve(__dirname, './mods/'))
)
return `__variableDynamicImportRuntimeHelper(${glob}, \`${rawPattern}\`)`
}

describe('parse positives', () => {
it('basic', async () => {
expect(await run('`./mods/${base}.js`')).toMatchSnapshot()
})

it('alias path', async () => {
expect(await run('`@/${base}.js`')).toMatchSnapshot()
})

it('with query raw', async () => {
expect(await run('`./mods/${base}.js?raw`')).toMatchSnapshot()
})

it('with query url', async () => {
expect(await run('`./mods/${base}.js?url`')).toMatchSnapshot()
})

it('? in variables', async () => {
expect(await run('`./mods/${base ?? foo}.js?raw`')).toMatchSnapshot()
})

it('? in url', async () => {
expect(await run('`./mo?ds/${base ?? foo}.js?raw`')).toMatchSnapshot()
})
})
2 changes: 0 additions & 2 deletions packages/vite/src/node/build.ts
Expand Up @@ -26,7 +26,6 @@ import { copyDir, emptyDir, lookupFile, normalizePath } from './utils'
import { manifestPlugin } from './plugins/manifest'
import commonjsPlugin from '@rollup/plugin-commonjs'
import type { RollupCommonJSOptions } from 'types/commonjs'
import dynamicImportVars from '@rollup/plugin-dynamic-import-vars'
import type { RollupDynamicImportVarsOptions } from 'types/dynamicImportVars'
import type { Logger } from './logger'
import type { TransformOptions } from 'esbuild'
Expand Down Expand Up @@ -285,7 +284,6 @@ export function resolveBuildPlugins(config: ResolvedConfig): {
watchPackageDataPlugin(config),
commonjsPlugin(options.commonjsOptions),
dataURIPlugin(),
dynamicImportVars(options.dynamicImportVarsOptions),
assetImportMetaUrlPlugin(config),
...(options.rollupOptions.plugins
? (options.rollupOptions.plugins.filter(Boolean) as Plugin[])
Expand Down
217 changes: 217 additions & 0 deletions packages/vite/src/node/plugins/dynamicImportVars.ts
@@ -0,0 +1,217 @@
import { posix } from 'path'
import MagicString from 'magic-string'
import { init, parse as parseImports } from 'es-module-lexer'
import type { ImportSpecifier } from 'es-module-lexer'
import type { Plugin } from '../plugin'
import type { ResolvedConfig } from '../config'
import { normalizePath, parseRequest, requestQuerySplitRE } from '../utils'
import { parse as parseJS } from 'acorn'
import { createFilter } from '@rollup/pluginutils'
import { dynamicImportToGlob } from '@rollup/plugin-dynamic-import-vars'

export const dynamicImportHelperId = '/@vite/dynamic-import-helper'

interface DynamicImportRequest {
as?: 'raw'
}

interface DynamicImportPattern {
globParams: DynamicImportRequest | null
userPattern: string
rawPattern: string
}

const dynamicImportHelper = (glob: Record<string, any>, path: string) => {
const v = glob[path]
if (v) {
return typeof v === 'function' ? v() : Promise.resolve(v)
}
return new Promise((_, reject) => {
;(typeof queueMicrotask === 'function' ? queueMicrotask : setTimeout)(
reject.bind(null, new Error('Unknown variable dynamic import: ' + path))
)
})
}

function parseDynamicImportPattern(
strings: string
): DynamicImportPattern | null {
const filename = strings.slice(1, -1)
const rawQuery = parseRequest(filename)
let globParams: DynamicImportRequest | null = null
const ast = (
parseJS(strings, {
ecmaVersion: 'latest',
sourceType: 'module'
}) as any
).body[0].expression

const userPatternQuery = dynamicImportToGlob(ast, filename)
if (!userPatternQuery) {
return null
}

const [userPattern] = userPatternQuery.split(requestQuerySplitRE, 2)
const [rawPattern] = filename.split(requestQuerySplitRE, 2)

if (rawQuery?.raw !== undefined) {
globParams = { as: 'raw' }
}

return {
globParams,
userPattern,
rawPattern
}
}

export async function transformDynamicImport(
importSource: string,
importer: string,
resolve: (
url: string,
importer?: string
) => Promise<string | undefined> | string | undefined
): Promise<{
glob: string
pattern: string
rawPattern: string
} | null> {
if (importSource[1] !== '.' && importSource[1] !== '/') {
const resolvedFileName = await resolve(importSource.slice(1, -1), importer)
if (!resolvedFileName) {
return null
}
const relativeFileName = posix.relative(
posix.dirname(normalizePath(importer)),
normalizePath(resolvedFileName)
)
importSource = normalizePath(
'`' + (relativeFileName[0] === '.' ? '' : './') + relativeFileName + '`'
)
}

const dynamicImportPattern = parseDynamicImportPattern(importSource)
if (!dynamicImportPattern) {
return null
}
const { globParams, rawPattern, userPattern } = dynamicImportPattern
const params = globParams
? `, ${JSON.stringify({ ...globParams, import: '*' })}`
: ''
const exp = `(import.meta.glob(${JSON.stringify(userPattern)}${params}))`

return {
rawPattern,
pattern: userPattern,
glob: exp
}
}

export function dynamicImportVarsPlugin(config: ResolvedConfig): Plugin {
const resolve = config.createResolver({
preferRelative: true,
tryIndex: false,
extensions: []
})
const { include, exclude, warnOnError } =
config.build.dynamicImportVarsOptions
const filter = createFilter(include, exclude)
const isBuild = config.command === 'build'
return {
name: 'vite:dynamic-import-vars',

resolveId(id) {
if (id === dynamicImportHelperId) {
return id
}
},

load(id) {
if (id === dynamicImportHelperId) {
return 'export default ' + dynamicImportHelper.toString()
}
},

async transform(source, importer) {
if (!filter(importer)) {
return
}

await init

let imports: readonly ImportSpecifier[] = []
try {
imports = parseImports(source)[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
}

let s: MagicString | undefined
let needDynamicImportHelper = false

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

if (dynamicIndex === -1 || source[start] !== '`') {
continue
}

s ||= new MagicString(source)
let result
try {
result = await transformDynamicImport(
source.slice(start, end),
importer,
resolve
)
} catch (error) {
if (warnOnError) {
this.warn(error)
} else {
this.error(error)
}
}

if (!result) {
continue
}

const { rawPattern, glob } = result

needDynamicImportHelper = true
s.overwrite(
expStart,
expEnd,
`__variableDynamicImportRuntimeHelper(${glob}, \`${rawPattern}\`)`
)
}

if (s) {
if (needDynamicImportHelper) {
s.prepend(
`import __variableDynamicImportRuntimeHelper from "${dynamicImportHelperId}";`
)
}
return {
code: s.toString(),
map:
!isBuild || config.build.sourcemap
? s.generateMap({ hires: true })
: null
}
}
}
}
}
23 changes: 1 addition & 22 deletions packages/vite/src/node/plugins/importAnalysis.ts
Expand Up @@ -490,7 +490,7 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
const url = rawUrl
.replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, '')
.trim()
if (!hasViteIgnore && !isSupportedDynamicImport(url)) {
if (!hasViteIgnore) {
this.warn(
`\n` +
colors.cyan(importerModule.file) +
Expand Down Expand Up @@ -651,27 +651,6 @@ export function importAnalysisPlugin(config: ResolvedConfig): Plugin {
}
}

/**
* https://github.com/rollup/plugins/tree/master/packages/dynamic-import-vars#limitations
* This is probably less accurate but is much cheaper than a full AST parse.
*/
function isSupportedDynamicImport(url: string) {
url = url.trim().slice(1, -1)
// must be relative
if (!url.startsWith('./') && !url.startsWith('../')) {
return false
}
// must have extension
if (!path.extname(url)) {
return false
}
// must be more specific if importing from same dir
if (url.startsWith('./${') && url.indexOf('/') === url.lastIndexOf('/')) {
return false
}
return true
}

type ImportNameSpecifier = { importedName: string; localName: string }

/**
Expand Down
22 changes: 16 additions & 6 deletions packages/vite/src/node/plugins/importMetaGlob.ts
Expand Up @@ -11,7 +11,7 @@ import type { ViteDevServer } from '../server'
import type { ModuleNode } from '../server/moduleGraph'
import type { ResolvedConfig } from '../config'
import { isCSSRequest } from './css'
import type { GeneralImportGlobOptions } from '../../../types/importGlob'
import type { GeneralImportGlobOptions } from 'types/importGlob'
import { normalizePath, slash } from '../utils'

export interface ParsedImportGlob {
Expand Down Expand Up @@ -168,12 +168,13 @@ export async function parseImportGlob(
for (const property of arg2.properties) {
if (
property.type === 'SpreadElement' ||
property.key.type !== 'Identifier'
(property.key.type !== 'Identifier' &&
property.key.type !== 'Literal')
)
throw err('Could only use literals')

const name = property.key.name as keyof GeneralImportGlobOptions

const name = ((property.key as any).name ||
(property.key as any).value) as keyof GeneralImportGlobOptions
if (name === 'query') {
if (property.value.type === 'ObjectExpression') {
const data: Record<string, string> = {}
Expand Down Expand Up @@ -260,13 +261,22 @@ const importPrefix = '__vite_glob_'

const { basename, dirname, relative, join } = posix

export interface TransformGlobImportResult {
s: MagicString
matches: ParsedImportGlob[]
files: Set<string>
}

/**
* @param optimizeExport for dynamicImportVar plugin don't need to optimize export.
*/
export async function transformGlobImport(
code: string,
id: string,
root: string,
resolveId: IdResolver,
restoreQueryExtension = false
) {
): Promise<TransformGlobImportResult | null> {
id = slash(id)
root = slash(root)
const isVirtual = isVirtualModule(id)
Expand All @@ -288,7 +298,7 @@ export async function transformGlobImport(
}
})

if (!matches.length) return
if (!matches.length) return null

const s = new MagicString(code)

Expand Down

0 comments on commit 80d113b

Please sign in to comment.