-
-
Notifications
You must be signed in to change notification settings - Fork 6.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: rework
dynamic-import-vars
(#7756)
Co-authored-by: Anthony Fu <anthonyfu117@hotmail.com> Co-authored-by: Bjorn Lu <bjornlu.dev@gmail.com>
- Loading branch information
1 parent
85cab70
commit 80d113b
Showing
18 changed files
with
372 additions
and
36 deletions.
There are no files selected for viewing
13 changes: 13 additions & 0 deletions
13
packages/vite/src/node/__tests__/plugins/dynamicImportVar/__snapshots__/parse.test.ts.snap
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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\`)"`; |
3 changes: 3 additions & 0 deletions
3
packages/vite/src/node/__tests__/plugins/dynamicImportVar/mods/hello.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export function hello() { | ||
return 'hello' | ||
} |
3 changes: 3 additions & 0 deletions
3
packages/vite/src/node/__tests__/plugins/dynamicImportVar/mods/hi.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export function hi() { | ||
return 'hi' | ||
} |
38 changes: 38 additions & 0 deletions
38
packages/vite/src/node/__tests__/plugins/dynamicImportVar/parse.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.