UNPKG

vite

Version:

Native-ESM powered web dev build tool

602 lines (550 loc) 19.4 kB
import fs from 'fs' import path from 'path' import { Plugin } from '../plugin' import { ResolvedConfig } from '../config' import chalk from 'chalk' import MagicString from 'magic-string' import { init, parse as parseImports, ImportSpecifier } from 'es-module-lexer' import { isCSSRequest, isDirectCSSRequest } from './css' import { isBuiltin, cleanUrl, createDebugger, generateCodeFrame, injectQuery, isDataUrl, isExternalUrl, isJSRequest, prettifyUrl, timeFrom, normalizePath } from '../utils' import { debugHmr, handlePrunedModules, lexAcceptedHmrDeps } from '../server/hmr' import { FS_PREFIX, CLIENT_DIR, CLIENT_PUBLIC_PATH, DEP_VERSION_RE, VALID_ID_PREFIX, NULL_BYTE_PLACEHOLDER } from '../constants' import { ViteDevServer } from '..' import { checkPublicFile } from './asset' import { parse as parseJS } from 'acorn' import type { Node } from 'estree' import { transformImportGlob } from '../importGlob' import { makeLegalIdentifier } from '@rollup/pluginutils' import { shouldExternalizeForSSR } from '../ssr/ssrExternal' const isDebug = !!process.env.DEBUG const debugRewrite = createDebugger('vite:rewrite') const clientDir = normalizePath(CLIENT_DIR) const skipRE = /\.(map|json)$/ const canSkip = (id: string) => skipRE.test(id) || isDirectCSSRequest(id) function isExplicitImportRequired(url: string) { return !isJSRequest(cleanUrl(url)) && !isCSSRequest(url) } function markExplicitImport(url: string) { if (isExplicitImportRequired(url)) { return injectQuery(url, 'import') } return url } /** * Server-only plugin that lexes, resolves, rewrites and analyzes url imports. * * - Imports are resolved to ensure they exist on disk * * - Lexes HMR accept calls and updates import relationships in the module graph * * - Bare module imports are resolved (by @rollup-plugin/node-resolve) to * absolute file paths, e.g. * * ```js * import 'foo' * ``` * is rewritten to * ```js * import '/@fs//project/node_modules/foo/dist/foo.js' * ``` * * - CSS imports are appended with `.js` since both the js module and the actual * css (referenced via <link>) may go through the transform pipeline: * * ```js * import './style.css' * ``` * is rewritten to * ```js * import './style.css.js' * ``` */ export function importAnalysisPlugin(config: ResolvedConfig): Plugin { const { root, base } = config const clientPublicPath = path.posix.join(base, CLIENT_PUBLIC_PATH) let server: ViteDevServer return { name: 'vite:import-analysis', configureServer(_server) { server = _server }, async transform(source, importer, ssr) { const prettyImporter = prettifyUrl(importer, root) if (canSkip(importer)) { isDebug && debugRewrite(chalk.dim(`[skipped] ${prettyImporter}`)) return null } const rewriteStart = Date.now() await init let imports: readonly ImportSpecifier[] = [] // strip UTF-8 BOM if (source.charCodeAt(0) === 0xfeff) { source = source.slice(1) } try { imports = parseImports(source)[0] } catch (e) { const isVue = importer.endsWith('.vue') const maybeJSX = !isVue && isJSRequest(importer) const msg = isVue ? `Install @vitejs/plugin-vue to handle .vue files.` : maybeJSX ? `If you are using JSX, make sure to name the file with the .jsx or .tsx extension.` : `You may need to install appropriate plugins to handle the ${path.extname( importer )} file format.` this.error( `Failed to parse source for import analysis because the content ` + `contains invalid JS syntax. ` + msg, e.idx ) } if (!imports.length) { isDebug && debugRewrite( `${timeFrom(rewriteStart)} ${chalk.dim( `[no imports] ${prettyImporter}` )}` ) return source } let hasHMR = false let isSelfAccepting = false let hasEnv = false let needQueryInjectHelper = false let s: MagicString | undefined const str = () => s || (s = new MagicString(source)) // vite-only server context const { moduleGraph } = server // since we are already in the transform phase of the importer, it must // have been loaded so its entry is guaranteed in the module graph. const importerModule = moduleGraph.getModuleById(importer)! const importedUrls = new Set<string>() const acceptedUrls = new Set<{ url: string start: number end: number }>() const toAbsoluteUrl = (url: string) => path.posix.resolve(path.posix.dirname(importerModule.url), url) const normalizeUrl = async ( url: string, pos: number ): Promise<[string, string]> => { if (base !== '/' && url.startsWith(base)) { url = url.replace(base, '/') } const resolved = await this.resolve(url, importer) if (!resolved) { this.error( `Failed to resolve import "${url}" from "${path.relative( process.cwd(), importer )}". Does the file exist?`, pos ) } const isRelative = url.startsWith('.') // normalize all imports into resolved URLs // e.g. `import 'foo'` -> `import '/@fs/.../node_modules/foo/index.js` if (resolved.id.startsWith(root + '/')) { // in root: infer short absolute path from root url = resolved.id.slice(root.length) } else if (fs.existsSync(cleanUrl(resolved.id))) { // exists but out of root: rewrite to absolute /@fs/ paths url = path.posix.join(FS_PREFIX + resolved.id) } else { url = resolved.id } if (isExternalUrl(url)) { return [url, url] } // if the resolved id is not a valid browser import specifier, // prefix it to make it valid. We will strip this before feeding it // back into the transform pipeline if (!url.startsWith('.') && !url.startsWith('/')) { url = VALID_ID_PREFIX + resolved.id.replace('\0', NULL_BYTE_PLACEHOLDER) } // make the URL browser-valid if not SSR if (!ssr) { // mark non-js/css imports with `?import` url = markExplicitImport(url) // for relative js/css imports, inherit importer's version query // do not do this for unknown type imports, otherwise the appended // query can break 3rd party plugin's extension checks. if (isRelative && !/[\?&]import=?\b/.test(url)) { const versionMatch = importer.match(DEP_VERSION_RE) if (versionMatch) { url = injectQuery(url, versionMatch[1]) } } // check if the dep has been hmr updated. If yes, we need to attach // its last updated timestamp to force the browser to fetch the most // up-to-date version of this module. try { const depModule = await moduleGraph.ensureEntryFromUrl(url) if (depModule.lastHMRTimestamp > 0) { url = injectQuery(url, `t=${depModule.lastHMRTimestamp}`) } } catch (e) { // it's possible that the dep fails to resolve (non-existent import) // attach location to the missing import e.pos = pos throw e } // prepend base (dev base is guaranteed to have ending slash) url = base + url.replace(/^\//, '') } return [url, resolved.id] } for (let index = 0; index < imports.length; index++) { const { s: start, e: end, ss: expStart, se: expEnd, d: dynamicIndex, // #2083 User may use escape path, // so use imports[index].n to get the unescaped string // @ts-ignore n: specifier } = imports[index] const rawUrl = source.slice(start, end) // check import.meta usage if (rawUrl === 'import.meta') { const prop = source.slice(end, end + 4) if (prop === '.hot') { hasHMR = true if (source.slice(end + 4, end + 11) === '.accept') { // further analyze accepted modules if ( lexAcceptedHmrDeps( source, source.indexOf('(', end + 11) + 1, acceptedUrls ) ) { isSelfAccepting = true } } } else if (prop === '.env') { hasEnv = true } else if (prop === '.glo' && source[end + 4] === 'b') { // transform import.meta.glob() // e.g. `import.meta.glob('glob:./dir/*.js')` const { imports, importsString, exp, endIndex, base, pattern } = await transformImportGlob( source, start, importer, index, root, normalizeUrl ) str().prepend(importsString) str().overwrite(expStart, endIndex, exp) imports.forEach((url) => importedUrls.add(url.replace(base, '/'))) if (!(importerModule.file! in server._globImporters)) { server._globImporters[importerModule.file!] = { module: importerModule, importGlobs: [] } } server._globImporters[importerModule.file!].importGlobs.push({ base, pattern }) } continue } const isDynamicImport = dynamicIndex >= 0 // static import or valid string in dynamic import // If resolvable, let's resolve it if (specifier) { // skip external / data uri if (isExternalUrl(specifier) || isDataUrl(specifier)) { continue } // skip ssr external if (ssr) { if ( server._ssrExternals && shouldExternalizeForSSR(specifier, server._ssrExternals) ) { continue } if (isBuiltin(specifier)) { continue } } // skip client if (specifier === clientPublicPath) { continue } // warn imports to non-asset /public files if ( specifier.startsWith('/') && !config.assetsInclude(cleanUrl(specifier)) && !specifier.endsWith('.json') && checkPublicFile(specifier, config) ) { throw new Error( `Cannot import non-asset file ${specifier} which is inside /public.` + `JS/CSS files inside /public are copied as-is on build and ` + `can only be referenced via <script src> or <link href> in html.` ) } // normalize const [normalizedUrl, resolvedId] = await normalizeUrl( specifier, start ) let url = normalizedUrl // record as safe modules server?.moduleGraph.safeModulesPath.add( cleanUrl(url).slice(4 /* '/@fs'.length */) ) // rewrite if (url !== specifier) { // for optimized cjs deps, support named imports by rewriting named // imports to const assignments. if (resolvedId.endsWith(`&es-interop`)) { url = url.slice(0, -11) if (isDynamicImport) { // rewrite `import('package')` to expose the default directly str().overwrite( dynamicIndex, end + 1, `import('${url}').then(m => ({ ...m.default, default: m.default }))` ) } else { const exp = source.slice(expStart, expEnd) const rewritten = transformCjsImport(exp, url, rawUrl, index) if (rewritten) { str().overwrite(expStart, expEnd, rewritten) } else { // #1439 export * from '...' str().overwrite(start, end, url) } } } else { str().overwrite(start, end, isDynamicImport ? `'${url}'` : url) } } // record for HMR import chain analysis // make sure to normalize away base importedUrls.add(url.replace(base, '/')) } else if (!importer.startsWith(clientDir) && !ssr) { // check @vite-ignore which suppresses dynamic import warning const hasViteIgnore = /\/\*\s*@vite-ignore\s*\*\//.test(rawUrl) const url = rawUrl .replace(/\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$/gm, '') .trim() if (!hasViteIgnore && !isSupportedDynamicImport(url)) { this.warn( `\n` + chalk.cyan(importerModule.file) + `\n` + generateCodeFrame(source, start) + `\nThe above dynamic import cannot be analyzed by vite.\n` + `See ${chalk.blue( `https://github.com/rollup/plugins/tree/master/packages/dynamic-import-vars#limitations` )} ` + `for supported dynamic import formats. ` + `If this is intended to be left as-is, you can use the ` + `/* @vite-ignore */ comment inside the import() call to suppress this warning.\n` ) } if ( !/^('.*'|".*"|`.*`)$/.test(url) || isExplicitImportRequired(url.slice(1, -1)) ) { needQueryInjectHelper = true str().overwrite(start, end, `__vite__injectQuery(${url}, 'import')`) } } } if (hasEnv) { // inject import.meta.env let env = `import.meta.env = ${JSON.stringify({ ...config.env, SSR: !!ssr })};` // account for user env defines for (const key in config.define) { if (key.startsWith(`import.meta.env.`)) { const val = config.define[key] env += `${key} = ${ typeof val === 'string' ? val : JSON.stringify(val) };` } } str().prepend(env) } if (hasHMR && !ssr) { debugHmr( `${ isSelfAccepting ? `[self-accepts]` : acceptedUrls.size ? `[accepts-deps]` : `[detected api usage]` } ${prettyImporter}` ) // inject hot context str().prepend( `import { createHotContext as __vite__createHotContext } from "${clientPublicPath}";` + `import.meta.hot = __vite__createHotContext(${JSON.stringify( importerModule.url )});` ) } if (needQueryInjectHelper) { str().prepend( `import { injectQuery as __vite__injectQuery } from "${clientPublicPath}";` ) } // normalize and rewrite accepted urls const normalizedAcceptedUrls = new Set<string>() for (const { url, start, end } of acceptedUrls) { const [normalized] = await moduleGraph.resolveUrl( toAbsoluteUrl(markExplicitImport(url)) ) normalizedAcceptedUrls.add(normalized) str().overwrite(start, end, JSON.stringify(normalized)) } // update the module graph for HMR analysis. // node CSS imports does its own graph update in the css plugin so we // only handle js graph updates here. if (!isCSSRequest(importer)) { const prunedImports = await moduleGraph.updateModuleInfo( importerModule, importedUrls, normalizedAcceptedUrls, isSelfAccepting ) if (hasHMR && prunedImports) { handlePrunedModules(prunedImports, server) } } if (s) { return s.toString() } else { return source } } } } /** * 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 } /** * Detect import statements to a known optimized CJS dependency and provide * ES named imports interop. We do this by rewriting named imports to a variable * assignment to the corresponding property on the `module.exports` of the cjs * module. Note this doesn't support dynamic re-assignments from within the cjs * module. * * Note that es-module-lexer treats `export * from '...'` as an import as well, * so, we may encounter ExportAllDeclaration here, in which case `undefined` * will be returned. * * Credits \@csr632 via #837 */ function transformCjsImport( importExp: string, url: string, rawUrl: string, importIndex: number ): string | undefined { const node = ( parseJS(importExp, { ecmaVersion: 2020, sourceType: 'module' }) as any ).body[0] as Node if (node.type === 'ImportDeclaration') { if (!node.specifiers.length) { return `import "${url}"` } const importNames: ImportNameSpecifier[] = [] for (const spec of node.specifiers) { if ( spec.type === 'ImportSpecifier' && spec.imported.type === 'Identifier' ) { const importedName = spec.imported.name const localName = spec.local.name importNames.push({ importedName, localName }) } else if (spec.type === 'ImportDefaultSpecifier') { importNames.push({ importedName: 'default', localName: spec.local.name }) } else if (spec.type === 'ImportNamespaceSpecifier') { importNames.push({ importedName: '*', localName: spec.local.name }) } } // If there is multiple import for same id in one file, // importIndex will prevent the cjsModuleName to be duplicate const cjsModuleName = makeLegalIdentifier( `__vite__cjsImport${importIndex}_${rawUrl}` ) const lines: string[] = [`import ${cjsModuleName} from "${url}"`] importNames.forEach(({ importedName, localName }) => { if (importedName === '*') { lines.push(`const ${localName} = ${cjsModuleName}`) } else if (importedName === 'default') { lines.push( `const ${localName} = ${cjsModuleName}.__esModule ? ${cjsModuleName}.default : ${cjsModuleName}` ) } else { lines.push(`const ${localName} = ${cjsModuleName}["${importedName}"]`) } }) return lines.join('; ') } }