Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: rework dynamic-import-vars #7756

Merged
merged 36 commits into from May 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
9bbf3dd
feat: re-implement dynamicImportVars
poyoho Apr 15, 2022
cc59a3b
fix: types
poyoho Apr 15, 2022
6d2ec2c
fix: ?raw
poyoho Apr 19, 2022
a58c98d
fix: dynamic import runtime helper
poyoho Apr 19, 2022
5cbc0a9
test: error
poyoho Apr 19, 2022
9228c48
fix: type error
poyoho Apr 19, 2022
09cc654
test: resolve path TODO
poyoho Apr 20, 2022
63b9bd4
chore: rebase
poyoho Apr 20, 2022
b505f42
feat: support dynamic import path alias
poyoho Apr 20, 2022
1bf029e
feat: virtual module for dynamicImportHelper
poyoho Apr 21, 2022
fc37bdc
fix: normal path
poyoho Apr 21, 2022
0d556c6
feat: test
poyoho Apr 21, 2022
fe061ee
chore: rebase
poyoho May 6, 2022
88a2e7f
fix: types
poyoho May 6, 2022
27a230c
chore: rebase
poyoho May 9, 2022
a0e999c
fix: bugs
poyoho May 9, 2022
9c42293
feat: hmr
poyoho May 9, 2022
4092cf1
feat: hmr
poyoho May 9, 2022
eab35a0
feat: snapshot
poyoho May 9, 2022
511e827
chore: merge
antfu May 9, 2022
d37615e
chore: update
antfu May 9, 2022
a2250ec
fix: simplify the implementation
antfu May 9, 2022
a998435
chore: update snap
antfu May 9, 2022
d4204ec
chore: try fix windows
antfu May 9, 2022
ef9041f
chore: fix windows
antfu May 9, 2022
79c1909
fix(dynamic-import-vars): simplify the implementation
poyoho May 9, 2022
7e4c42a
fix: window
poyoho May 9, 2022
07c2f8f
chore: rebase
poyoho May 9, 2022
5d11b74
fix: window
poyoho May 9, 2022
b987e85
chore: safe
poyoho May 9, 2022
e715d41
fix: edge case
poyoho May 10, 2022
e88ec20
fix: sourcemap generate logic
poyoho May 10, 2022
1be3693
chore: nice judge
poyoho May 10, 2022
de3582f
fix: parseUrl
poyoho May 10, 2022
c8f62f8
chore: useless
poyoho May 10, 2022
837624e
chore: format
poyoho May 10, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -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)
poyoho marked this conversation as resolved.
Show resolved Hide resolved
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