From e6f7fbad75c1b406b9a95060c5d2a42d3a8994a8 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sun, 10 Jan 2021 14:16:26 -0500 Subject: [PATCH] feat: async chunk loading optimizations - load async chunk CSS and nested imports in parallel to the main chunk - ensure CSS loaded before evaluating main chunk --- packages/playground/css/__tests__/css.spec.ts | 6 +- packages/playground/css/async.js | 4 +- .../dynamic-import/nested/shared.js | 1 + .../playground/dynamic-import/views/bar.js | 3 + .../playground/dynamic-import/views/baz.js | 3 + .../playground/dynamic-import/views/foo.js | 3 + packages/vite/src/node/build.ts | 4 +- packages/vite/src/node/plugins/css.ts | 24 +- .../src/node/plugins/dynamicImportPolyfill.ts | 48 +-- packages/vite/src/node/plugins/html.ts | 1 + .../vite/src/node/plugins/importAnalysis.ts | 2 +- .../src/node/plugins/importAnaysisBuild.ts | 375 ++++++++++++++++++ packages/vite/src/node/plugins/importGlob.ts | 193 --------- 13 files changed, 414 insertions(+), 253 deletions(-) create mode 100644 packages/playground/dynamic-import/nested/shared.js create mode 100644 packages/vite/src/node/plugins/importAnaysisBuild.ts delete mode 100644 packages/vite/src/node/plugins/importGlob.ts diff --git a/packages/playground/css/__tests__/css.spec.ts b/packages/playground/css/__tests__/css.spec.ts index de8c70302a7a52..69256e5d3f52b7 100644 --- a/packages/playground/css/__tests__/css.spec.ts +++ b/packages/playground/css/__tests__/css.spec.ts @@ -104,10 +104,10 @@ test('async chunk', async () => { expect(await getColor(el)).toBe('teal') if (isBuild) { - // assert that the css is inlined in the async chunk instead of in the + // assert that the css is extracted into its own file instead of in the // main css file - expect(findAssetFile(/\.css$/)).not.toMatch('teal') - expect(findAssetFile(/async\.\w+\.js$/)).toMatch('.async{color:teal}') + expect(findAssetFile(/index\.\w+\.css$/)).not.toMatch('teal') + expect(findAssetFile(/async\.\w+\.css$/)).toMatch('.async{color:teal}') } else { // test hmr editFile('async.css', (code) => code.replace('color: teal', 'color: blue')) diff --git a/packages/playground/css/async.js b/packages/playground/css/async.js index 7cfcad126b4de6..9706f0741e9f50 100644 --- a/packages/playground/css/async.js +++ b/packages/playground/css/async.js @@ -2,5 +2,7 @@ import './async.css' const div = document.createElement('div') div.className = 'async' -div.textContent = 'async chunk (this should be teal)' document.body.appendChild(div) +div.textContent = `async chunk (this should be teal) ${ + getComputedStyle(div).color +}` diff --git a/packages/playground/dynamic-import/nested/shared.js b/packages/playground/dynamic-import/nested/shared.js new file mode 100644 index 00000000000000..4384f77f2a3613 --- /dev/null +++ b/packages/playground/dynamic-import/nested/shared.js @@ -0,0 +1 @@ +export const n = 1 diff --git a/packages/playground/dynamic-import/views/bar.js b/packages/playground/dynamic-import/views/bar.js index 94401802f2dd50..12b73fd6da9e8c 100644 --- a/packages/playground/dynamic-import/views/bar.js +++ b/packages/playground/dynamic-import/views/bar.js @@ -1 +1,4 @@ +import { n } from '../nested/shared' +console.log('bar' + n) + export const msg = 'Bar view' diff --git a/packages/playground/dynamic-import/views/baz.js b/packages/playground/dynamic-import/views/baz.js index 058ba96c5ed595..0bcfd54d53e564 100644 --- a/packages/playground/dynamic-import/views/baz.js +++ b/packages/playground/dynamic-import/views/baz.js @@ -1 +1,4 @@ +import { n } from '../nested/shared' +console.log('baz' + n) + export const msg = 'Baz view' diff --git a/packages/playground/dynamic-import/views/foo.js b/packages/playground/dynamic-import/views/foo.js index f87abff5b03072..1205c3f4148934 100644 --- a/packages/playground/dynamic-import/views/foo.js +++ b/packages/playground/dynamic-import/views/foo.js @@ -1 +1,4 @@ +import { n } from '../nested/shared' +console.log('foo' + n) + export const msg = 'Foo view' diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index d8ef6812574f4f..218af27c73fffb 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -25,7 +25,7 @@ import { Logger } from './logger' import { TransformOptions } from 'esbuild' import { CleanCSS } from 'types/clean-css' import { dataURIPlugin } from './plugins/dataUri' -import { importGlobPlugin } from './plugins/importGlob' +import { buildImportAnalysisPlugin } from './plugins/importAnaysisBuild' export interface BuildOptions { /** @@ -204,7 +204,6 @@ export function resolveBuildPlugins( extensions: ['.js', '.cjs'] }), dataURIPlugin(), - importGlobPlugin(config), buildDefinePlugin(config), dynamicImportVars({ warnOnError: true, @@ -213,6 +212,7 @@ export function resolveBuildPlugins( ...(options.rollupOptions.plugins || []) ], post: [ + buildImportAnalysisPlugin(config), buildEsbuildPlugin(config), ...(options.minify && options.minify !== 'esbuild' ? [terserPlugin(options.terserOptions)] diff --git a/packages/vite/src/node/plugins/css.ts b/packages/vite/src/node/plugins/css.ts index 7aec26be6afa0e..53bb9ba1298a24 100644 --- a/packages/vite/src/node/plugins/css.ts +++ b/packages/vite/src/node/plugins/css.ts @@ -210,23 +210,13 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { if (config.build.minify) { chunkCSS = await minifyCSS(chunkCSS, config) } - // for non-entry chunks, collect its css and inline it as JS strings. - if (!chunk.isEntry) { - const placeholder = `__VITE_CSS__` - code = - `let ${placeholder} = document.createElement('style');` + - `${placeholder}.innerHTML = ${JSON.stringify(chunkCSS)};` + - `document.head.appendChild(${placeholder});` + - code - } else { - // for entry chunks, emit corresponding css file - const fileHandle = this.emitFile({ - name: chunk.name + '.css', - type: 'asset', - source: chunkCSS - }) - chunkToEmittedCssFileMap.set(chunk, fileHandle) - } + // emit corresponding css file + const fileHandle = this.emitFile({ + name: chunk.name + '.css', + type: 'asset', + source: chunkCSS + }) + chunkToEmittedCssFileMap.set(chunk, fileHandle) return { code, map: null diff --git a/packages/vite/src/node/plugins/dynamicImportPolyfill.ts b/packages/vite/src/node/plugins/dynamicImportPolyfill.ts index e02563610d6451..bd12442f87ef67 100644 --- a/packages/vite/src/node/plugins/dynamicImportPolyfill.ts +++ b/packages/vite/src/node/plugins/dynamicImportPolyfill.ts @@ -1,16 +1,17 @@ -import MagicString from 'magic-string' import { ResolvedConfig } from '..' import { Plugin } from '../plugin' +import { isModernFlag } from './importAnaysisBuild' export const polyfillId = 'vite/dynamic-import-polyfill' -const polyfillPlaceholder = `__DYNAMIC_IMPORT_POLYFILL__()` export function dynamicImportPolyfillPlugin(config: ResolvedConfig): Plugin { const skip = config.command === 'serve' || config.build.ssr let polyfillLoaded = false const polyfillString = `${__dynamic_import_polyfill__.toString()};` + - `__dynamic_import_polyfill__(${JSON.stringify(config.build.base)});` + `${isModernFlag}&&${__dynamic_import_polyfill__.name}(${JSON.stringify( + config.build.base + )});` return { name: 'vite:dynamic-import-polyfill', @@ -27,7 +28,7 @@ export function dynamicImportPolyfillPlugin(config: ResolvedConfig): Plugin { polyfillLoaded = true // return a placeholder here and defer the injection to renderChunk // so that we can selectively skip the injection based on output format - return polyfillPlaceholder + return polyfillString } }, @@ -43,38 +44,9 @@ export function dynamicImportPolyfillPlugin(config: ResolvedConfig): Plugin { `your custom entry.` ) } - return { - left: '__import__(', - right: ')' - } - }, - - renderChunk(code, chunk, { format }) { - if (skip) { - return null - } - if (!chunk.facadeModuleId) { - return null - } - const i = code.indexOf(polyfillPlaceholder) - if (i < 0) { - return null - } - // if format is not ES, simply empty it - const replacement = format === 'es' ? polyfillString : '' - if (config.build.sourcemap) { - const s = new MagicString(code) - s.overwrite(i, i + polyfillPlaceholder.length, replacement) - return { - code: s.toString(), - map: s.generateMap({ hires: true }) - } - } else { - return { - code: code.replace(polyfillPlaceholder, replacement), - map: null - } - } + // we do not actually return anything here because rewriting here would + // make it impossible to use es-module-lexer on the rendered chunks, which + // we need for import graph optimization in ./importAnalysisBuild. } } } @@ -115,6 +87,10 @@ function __dynamic_import_polyfill__( modulePath = '.', importFunctionName = '__import__' ) { + // @ts-ignore injected by ./importAnalysisBuild + if (!__VITE_IS_MODERN__) { + return + } try { self[importFunctionName] = new Function('u', `return import(u)`) } catch (error) { diff --git a/packages/vite/src/node/plugins/html.ts b/packages/vite/src/node/plugins/html.ts index 2ca940b5b09387..c8e2154b1b82c0 100644 --- a/packages/vite/src/node/plugins/html.ts +++ b/packages/vite/src/node/plugins/html.ts @@ -282,6 +282,7 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { tag: 'script', attrs: { type: 'module', + crossorigin: true, src: toPublicPath(chunk.fileName, config) } }, diff --git a/packages/vite/src/node/plugins/importAnalysis.ts b/packages/vite/src/node/plugins/importAnalysis.ts index b2233c0e40b5f8..490adf130c3868 100644 --- a/packages/vite/src/node/plugins/importAnalysis.ts +++ b/packages/vite/src/node/plugins/importAnalysis.ts @@ -35,7 +35,7 @@ import { checkPublicFile } from './asset' import { parse as parseJS } from 'acorn' import type { Node } from 'estree' import { makeLegalIdentifier } from '@rollup/pluginutils' -import { transformImportGlob } from './importGlob' +import { transformImportGlob } from './importAnaysisBuild' const isDebug = !!process.env.DEBUG const debugRewrite = createDebugger('vite:rewrite') diff --git a/packages/vite/src/node/plugins/importAnaysisBuild.ts b/packages/vite/src/node/plugins/importAnaysisBuild.ts new file mode 100644 index 00000000000000..28a57819ba64a1 --- /dev/null +++ b/packages/vite/src/node/plugins/importAnaysisBuild.ts @@ -0,0 +1,375 @@ +import path from 'path' +import glob from 'fast-glob' +import { ResolvedConfig } from '../config' +import { Plugin } from '../plugin' +import { cleanUrl } from '../utils' +import MagicString from 'magic-string' +import { ImportSpecifier, init, parse as parseImports } from 'es-module-lexer' +import { RollupError, OutputChunk } from 'rollup' +import { chunkToEmittedCssFileMap } from './css' + +/** + * A flag for injected helpers. This flag will be set to `false` if the output + * target is not native es - so that injected helper logic can be conditinally + * dropped. + */ +export const isModernFlag = `__VITE_IS_MODERN__` + +const preloadHelperId = 'vite/preload-helper' +const preloadMethod = __vitePreload.name +const preloadModuleCode = `const seen = new Set();export ${__vitePreload.toString()}` +const preloadMarker = `__VITE_PRELOAD__` +const preloadMarkerRE = new RegExp(`,?"${preloadMarker}"`, 'g') + +/** + * Helper for preloading CSS and direct imports of async chunks in parallell to + * the async chunk itself. + */ +function __vitePreload(baseModule: () => Promise<{}>, deps?: string[]) { + // @ts-ignore + if (!__VITE_IS_MODERN__ || !deps) { + return baseModule() + } + return Promise.all( + deps.map((dep) => { + // @ts-ignore + if (seen.has(dep)) return + // @ts-ignore + seen.add(dep) + // @ts-ignore + const link = document.createElement('link') + const isCss = /\.css$/.test(dep) + link.rel = isCss + ? 'stylesheet' + : link.relList && + link.relList.supports && + link.relList.supports('modulepreload') + ? 'modulepreload' + : 'preload' + if (!isCss) { + link.as = 'script' + link.crossOrigin = '' + } + link.href = dep + // @ts-ignore + document.head.appendChild(link) + if (isCss) { + return new Promise((res) => { + link.addEventListener('load', res) + }) + } + }) + ).then(() => baseModule()) +} + +/** + * Build only. During serve this is performed as part of ./importAnalysis. + */ +export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { + return { + name: 'vite:import-analysis', + + resolveId(id) { + if (id === preloadHelperId) { + return id + } + }, + + load(id) { + if (id === preloadHelperId) { + return preloadModuleCode + } + }, + + async transform(source, importer) { + if (importer.includes('node_modules')) { + return + } + + await init + + let imports: ImportSpecifier[] = [] + try { + imports = parseImports(source)[0] + } catch (e) { + this.error(e, e.idx) + } + + if (!imports.length) { + return null + } + + let s: MagicString | undefined + const str = () => s || (s = new MagicString(source)) + let hasInjectedHelper = false + + for (let index = 0; index < imports.length; index++) { + const { s: start, e: end, ss: expStart, d: dynamicIndex } = imports[ + index + ] + + const isGlob = + source.slice(start, end) === 'import.meta' && + source.slice(end, end + 5) === '.glob' + + if (isGlob || dynamicIndex > -1) { + // inject parallelPreload helper. + if (!hasInjectedHelper) { + hasInjectedHelper = true + str().prepend( + `import { ${preloadMethod} } from "${preloadHelperId}";` + ) + } + } + + // import.meta.glob + if (isGlob) { + const { imports, exp, endIndex } = await transformImportGlob( + source, + start, + importer, + index + ) + str().prepend(imports) + str().overwrite(expStart, endIndex, exp) + continue + } + + if (dynamicIndex > -1) { + const dynamicEnd = source.indexOf(`)`, end) + 1 + const original = source.slice(dynamicIndex, dynamicEnd) + str().overwrite( + dynamicIndex, + dynamicEnd, + `${preloadMethod}(()=>${original}${ + isModernFlag ? `,"${preloadMarker}"` : `` + })` + ) + } + } + + if (s) { + return { + code: s.toString(), + map: config.build.sourcemap ? s.generateMap({ hires: true }) : null + } + } + }, + + renderChunk(code, _, { format }) { + // make sure we only perform the preload logic in modern builds. + if (code.indexOf(isModernFlag) > -1) { + return code.replace(/__VITE_IS_MODERN__/g, String(format === 'es')) + } + return null + }, + + generateBundle({ format }, bundle) { + if (format !== 'es') { + return + } + + const isPolyfillEnabled = config.build.polyfillDynamicImport + for (const file in bundle) { + const chunk = bundle[file] + // can't use chunk.dynamicImports.length here since some modules e.g. + // dynamic import to constant json may get inlined. + if (chunk.type === 'chunk' && chunk.code.indexOf(preloadMarker) > -1) { + const code = chunk.code + let imports: ImportSpecifier[] + try { + imports = parseImports(code)[0] + } catch (e) { + this.error(e, e.idx) + } + + if (imports.length) { + const s = new MagicString(code) + for (let index = 0; index < imports.length; index++) { + const { s: start, e: end, d: dynamicIndex } = imports[index] + if (dynamicIndex > -1) { + // if dynmamic import polyfill is used, rewrite the import to + // use the polyfilled function. + if (isPolyfillEnabled) { + s.overwrite(dynamicIndex, dynamicIndex + 6, `__import__`) + } + // check the chunk being imported + const url = code.slice(start, end) + if (url[0] !== `"` || url[url.length - 1] !== `"`) { + // non literal chunk import, skip + continue + } + + const deps: Set = new Set() + + // trace direct imports and add to deps + const addDeps = (filename: string) => { + const chunk = bundle[filename] as OutputChunk | undefined + if (chunk) { + deps.add(config.build.base + chunk.fileName) + const cssId = chunkToEmittedCssFileMap.get(chunk) + if (cssId) { + deps.add(config.build.base + this.getFileName(cssId)) + } + chunk.imports.forEach(addDeps) + } + } + const normalizedFile = path.posix.join( + path.posix.dirname(chunk.fileName), + url.slice(1, -1) + ) + addDeps(normalizedFile) + + const markPos = code.indexOf(preloadMarker, end) + s.overwrite( + markPos - 1, + markPos + preloadMarker.length + 1, + deps.size + ? `[${[...deps].map((d) => JSON.stringify(d)).join(',')}]` + : `` + ) + } + } + chunk.code = s.toString() + // TODO source map + } else { + // inlined dynamic import, remove the marker + chunk.code = code.replace(preloadMarkerRE, 'void 0') + } + } + } + } + } +} + +export async function transformImportGlob( + source: string, + pos: number, + importer: string, + importIndex: number, + normalizeUrl?: (url: string, pos: number) => Promise<[string, string]> +): Promise<{ imports: string; exp: string; endIndex: number }> { + const err = (msg: string) => { + const e = new Error(`Invalid glob import syntax: ${msg}`) + ;(e as any).pos = pos + return e + } + + importer = cleanUrl(importer) + const importerBasename = path.basename(importer) + + let [pattern, endIndex] = lexGlobPattern(source, pos) + if (!pattern.startsWith('.')) { + throw err(`pattern must start with "."`) + } + let base = path.dirname(importer) + let parentDepth = 0 + while (pattern.startsWith('../')) { + pattern = pattern.slice(3) + base = path.resolve(base, '../') + parentDepth++ + } + if (pattern.startsWith('./')) { + pattern = pattern.slice(2) + } + + const files = glob.sync(pattern, { cwd: base }) + let imports = `` + let entries = `` + for (let i = 0; i < files.length; i++) { + // skip importer itself + if (files[i] === importerBasename) continue + const file = parentDepth + ? `${'../'.repeat(parentDepth)}${files[i]}` + : `./${files[i]}` + let importee = file + if (normalizeUrl) { + ;[importee] = await normalizeUrl(file, pos) + } + const identifier = `__glob_${importIndex}_${i}` + const isEager = source.slice(pos, pos + 21) === 'import.meta.globEager' + if (isEager) { + imports += `import * as ${identifier} from ${JSON.stringify(importee)};` + entries += ` ${JSON.stringify(file)}: ${identifier},` + } else { + let imp = `import(${JSON.stringify(importee)})` + if (!normalizeUrl) { + imp = + `(${isModernFlag}` + + `? ${preloadMethod}(()=>${imp},"${preloadMarker}")` + + `: ${imp})` + } + entries += ` ${JSON.stringify(file)}: () => ${imp},` + } + } + + return { + imports, + exp: `{${entries}}`, + endIndex + } +} + +const enum LexerState { + inCall, + inSingleQuoteString, + inDoubleQuoteString, + inTemplateString +} + +function lexGlobPattern(code: string, pos: number): [string, number] { + let state = LexerState.inCall + let pattern = '' + + let i = code.indexOf(`(`, pos) + 1 + outer: for (; i < code.length; i++) { + const char = code.charAt(i) + switch (state) { + case LexerState.inCall: + if (char === `'`) { + state = LexerState.inSingleQuoteString + } else if (char === `"`) { + state = LexerState.inDoubleQuoteString + } else if (char === '`') { + state = LexerState.inTemplateString + } else if (/\s/.test(char)) { + continue + } else { + error(i) + } + break + case LexerState.inSingleQuoteString: + if (char === `'`) { + break outer + } else { + pattern += char + } + break + case LexerState.inDoubleQuoteString: + if (char === `"`) { + break outer + } else { + pattern += char + } + break + case LexerState.inTemplateString: + if (char === '`') { + break outer + } else { + pattern += char + } + break + default: + throw new Error('unknown import.meta.glob lexer state') + } + } + return [pattern, code.indexOf(`)`, i) + 1] +} + +function error(pos: number) { + const err = new Error( + `import.meta.glob() can only accept string literals.` + ) as RollupError + err.pos = pos + throw err +} diff --git a/packages/vite/src/node/plugins/importGlob.ts b/packages/vite/src/node/plugins/importGlob.ts deleted file mode 100644 index 1d07f56872f437..00000000000000 --- a/packages/vite/src/node/plugins/importGlob.ts +++ /dev/null @@ -1,193 +0,0 @@ -import path from 'path' -import glob from 'fast-glob' -import { ResolvedConfig } from '../config' -import { Plugin } from '../plugin' -import { cleanUrl } from '../utils' -import MagicString from 'magic-string' -import { ImportSpecifier, init, parse as parseImports } from 'es-module-lexer' -import { RollupError } from 'rollup' - -/** - * Build only. During serve this is performed as part of ./importAnalysis. - */ -export function importGlobPlugin(config: ResolvedConfig): Plugin { - return { - name: 'vite:import-glob', - - async transform(source, importer) { - if ( - // skip deps - importer.includes('node_modules') || - // fast check for presence of glob keyword - source.indexOf('import.meta.glob') < 0 - ) { - return - } - - await init - - let imports: ImportSpecifier[] = [] - try { - imports = parseImports(source)[0] - } catch (e) { - this.error(e, e.idx) - } - - if (!imports.length) { - return null - } - - let s: MagicString | undefined - const str = () => s || (s = new MagicString(source)) - - for (let index = 0; index < imports.length; index++) { - const { s: start, e: end, ss: expStart } = imports[index] - const url = source.slice(start, end) - if (url === 'import.meta' && source.slice(end, end + 5) === '.glob') { - const { imports, exp, endIndex } = await transformImportGlob( - source, - start, - importer, - index - ) - str().prepend(imports) - str().overwrite(expStart, endIndex, exp) - } - } - - if (s) { - return { - code: s.toString(), - map: config.build.sourcemap ? s.generateMap({ hires: true }) : null - } - } - } - } -} - -export async function transformImportGlob( - source: string, - pos: number, - importer: string, - importIndex: number, - normalizeUrl?: (url: string, pos: number) => Promise<[string, string]> -): Promise<{ imports: string; exp: string; endIndex: number }> { - const err = (msg: string) => { - const e = new Error(`Invalid glob import syntax: ${msg}`) - ;(e as any).pos = pos - return e - } - - importer = cleanUrl(importer) - const importerBasename = path.basename(importer) - - let [pattern, endIndex] = lexGlobPattern(source, pos) - if (!pattern.startsWith('.')) { - throw err(`pattern must start with "."`) - } - let base = path.dirname(importer) - let parentDepth = 0 - while (pattern.startsWith('../')) { - pattern = pattern.slice(3) - base = path.resolve(base, '../') - parentDepth++ - } - if (pattern.startsWith('./')) { - pattern = pattern.slice(2) - } - - const files = glob.sync(pattern, { cwd: base }) - let imports = `` - let entries = `` - for (let i = 0; i < files.length; i++) { - // skip importer itself - if (files[i] === importerBasename) continue - const file = parentDepth - ? `${'../'.repeat(parentDepth)}${files[i]}` - : `./${files[i]}` - let importee = file - if (normalizeUrl) { - ;[importee] = await normalizeUrl(file, pos) - } - const identifier = `__glob_${importIndex}_${i}` - const isEager = source.slice(pos, pos + 21) === 'import.meta.globEager' - if (isEager) { - imports += `import * as ${identifier} from ${JSON.stringify(importee)};` - entries += ` ${JSON.stringify(file)}: ${identifier},` - } else { - entries += ` ${JSON.stringify(file)}: () => import(${JSON.stringify( - importee - )}),` - } - } - - return { - imports, - exp: `{${entries}}`, - endIndex - } -} - -const enum LexerState { - inCall, - inSingleQuoteString, - inDoubleQuoteString, - inTemplateString -} - -function lexGlobPattern(code: string, pos: number): [string, number] { - let state = LexerState.inCall - let pattern = '' - - let i = code.indexOf(`(`, pos) + 1 - outer: for (; i < code.length; i++) { - const char = code.charAt(i) - switch (state) { - case LexerState.inCall: - if (char === `'`) { - state = LexerState.inSingleQuoteString - } else if (char === `"`) { - state = LexerState.inDoubleQuoteString - } else if (char === '`') { - state = LexerState.inTemplateString - } else if (/\s/.test(char)) { - continue - } else { - error(i) - } - break - case LexerState.inSingleQuoteString: - if (char === `'`) { - break outer - } else { - pattern += char - } - break - case LexerState.inDoubleQuoteString: - if (char === `"`) { - break outer - } else { - pattern += char - } - break - case LexerState.inTemplateString: - if (char === '`') { - break outer - } else { - pattern += char - } - break - default: - throw new Error('unknown import.meta.glob lexer state') - } - } - return [pattern, code.indexOf(`)`, i) + 1] -} - -function error(pos: number) { - const err = new Error( - `import.meta.glob() can only accept string literals.` - ) as RollupError - err.pos = pos - throw err -}