UNPKG

vite

Version:

Native-ESM powered web dev build tool

508 lines (453 loc) 13.9 kB
import debug from 'debug' import chalk from 'chalk' import fs from 'fs' import os from 'os' import path from 'path' import { pathToFileURL, URL } from 'url' import { FS_PREFIX, DEFAULT_EXTENSIONS, VALID_ID_PREFIX } from './constants' import resolve from 'resolve' import builtins from 'builtin-modules' import { FSWatcher } from 'chokidar' import remapping from '@ampproject/remapping' import { DecodedSourceMap, RawSourceMap } from '@ampproject/remapping/dist/types/types' export function slash(p: string): string { return p.replace(/\\/g, '/') } // Strip valid id prefix. This is prepended to resolved Ids that are // not valid browser import specifiers by the importAnalysis plugin. export function unwrapId(id: string): string { return id.startsWith(VALID_ID_PREFIX) ? id.slice(VALID_ID_PREFIX.length) : id } export const flattenId = (id: string): string => id.replace(/[\/\.]/g, '_') export function isBuiltin(id: string): boolean { return builtins.includes(id) } export const bareImportRE = /^[\w@](?!.*:\/\/)/ export const deepImportRE = /^([^@][^/]*)\/|^(@[^/]+\/[^/]+)\// export let isRunningWithYarnPnp: boolean try { isRunningWithYarnPnp = Boolean(require('pnpapi')) } catch {} const ssrExtensions = ['.js', '.json', '.node'] export function resolveFrom(id: string, basedir: string, ssr = false): string { return resolve.sync(id, { basedir, extensions: ssr ? ssrExtensions : DEFAULT_EXTENSIONS, // necessary to work with pnpm preserveSymlinks: isRunningWithYarnPnp || false }) } // set in bin/vite.js const filter = process.env.VITE_DEBUG_FILTER const DEBUG = process.env.DEBUG interface DebuggerOptions { onlyWhenFocused?: boolean | string } export function createDebugger( ns: string, options: DebuggerOptions = {} ): debug.Debugger['log'] { const log = debug(ns) const { onlyWhenFocused } = options const focus = typeof onlyWhenFocused === 'string' ? onlyWhenFocused : ns return (msg: string, ...args: any[]) => { if (filter && !msg.includes(filter)) { return } if (onlyWhenFocused && !DEBUG?.includes(focus)) { return } log(msg, ...args) } } export const isWindows = os.platform() === 'win32' const VOLUME_RE = /^[A-Z]:/i export function normalizePath(id: string): string { return path.posix.normalize(isWindows ? slash(id) : id) } export function fsPathFromId(id: string): string { const fsPath = normalizePath(id.slice(FS_PREFIX.length)) return fsPath.startsWith('/') || fsPath.match(VOLUME_RE) ? fsPath : `/${fsPath}` } export function ensureVolumeInPath(file: string): string { return isWindows ? path.resolve(file) : file } export const queryRE = /\?.*$/ export const hashRE = /#.*$/ export const cleanUrl = (url: string): string => url.replace(hashRE, '').replace(queryRE, '') export const externalRE = /^(https?:)?\/\// export const isExternalUrl = (url: string): boolean => externalRE.test(url) export const dataUrlRE = /^\s*data:/i export const isDataUrl = (url: string): boolean => dataUrlRE.test(url) const knownJsSrcRE = /\.((j|t)sx?|mjs|vue|marko|svelte)($|\?)/ export const isJSRequest = (url: string): boolean => { url = cleanUrl(url) if (knownJsSrcRE.test(url)) { return true } if (!path.extname(url) && !url.endsWith('/')) { return true } return false } const importQueryRE = /(\?|&)import=?(?:&|$)/ const trailingSeparatorRE = /[\?&]$/ export const isImportRequest = (url: string): boolean => importQueryRE.test(url) export function removeImportQuery(url: string): string { return url.replace(importQueryRE, '$1').replace(trailingSeparatorRE, '') } export function injectQuery(url: string, queryToInject: string): string { // encode percents for consistent behavior with pathToFileURL // see #2614 for details let resolvedUrl = new URL(url.replace(/%/g, '%25'), 'relative:///') if (resolvedUrl.protocol !== 'relative:') { resolvedUrl = pathToFileURL(url) } let { protocol, pathname, search, hash } = resolvedUrl if (protocol === 'file:') { pathname = pathname.slice(1) } pathname = decodeURIComponent(pathname) return `${pathname}?${queryToInject}${search ? `&` + search.slice(1) : ''}${ hash || '' }` } const timestampRE = /\bt=\d{13}&?\b/ export function removeTimestampQuery(url: string): string { return url.replace(timestampRE, '').replace(trailingSeparatorRE, '') } export async function asyncReplace( input: string, re: RegExp, replacer: (match: RegExpExecArray) => string | Promise<string> ): Promise<string> { let match: RegExpExecArray | null let remaining = input let rewritten = '' while ((match = re.exec(remaining))) { rewritten += remaining.slice(0, match.index) rewritten += await replacer(match) remaining = remaining.slice(match.index + match[0].length) } rewritten += remaining return rewritten } export function timeFrom(start: number, subtract = 0): string { const time: number | string = Date.now() - start - subtract const timeString = (time + `ms`).padEnd(5, ' ') if (time < 10) { return chalk.green(timeString) } else if (time < 50) { return chalk.yellow(timeString) } else { return chalk.red(timeString) } } /** * pretty url for logging. */ export function prettifyUrl(url: string, root: string): string { url = removeTimestampQuery(url) const isAbsoluteFile = url.startsWith(root) if (isAbsoluteFile || url.startsWith(FS_PREFIX)) { let file = path.relative(root, isAbsoluteFile ? url : fsPathFromId(url)) const seg = file.split('/') const npmIndex = seg.indexOf(`node_modules`) const isSourceMap = file.endsWith('.map') if (npmIndex > 0) { file = seg[npmIndex + 1] if (file.startsWith('@')) { file = `${file}/${seg[npmIndex + 2]}` } file = `npm: ${chalk.dim(file)}${isSourceMap ? ` (source map)` : ``}` } return chalk.dim(file) } else { return chalk.dim(url) } } export function isObject(value: unknown): value is Record<string, any> { return Object.prototype.toString.call(value) === '[object Object]' } export function isDefined<T>(value: T | undefined | null): value is T { return value !== undefined && value !== null } export function lookupFile( dir: string, formats: string[], pathOnly = false ): string | undefined { for (const format of formats) { const fullPath = path.join(dir, format) if (fs.existsSync(fullPath) && fs.statSync(fullPath).isFile()) { return pathOnly ? fullPath : fs.readFileSync(fullPath, 'utf-8') } } const parentDir = path.dirname(dir) if (parentDir !== dir) { return lookupFile(parentDir, formats, pathOnly) } } const splitRE = /\r?\n/ const range: number = 2 export function pad(source: string, n = 2): string { const lines = source.split(splitRE) return lines.map((l) => ` `.repeat(n) + l).join(`\n`) } export function posToNumber( source: string, pos: number | { line: number; column: number } ): number { if (typeof pos === 'number') return pos const lines = source.split(splitRE) const { line, column } = pos let start = 0 for (let i = 0; i < line - 1; i++) { start += lines[i].length + 1 } return start + column } export function numberToPos( source: string, offset: number | { line: number; column: number } ): { line: number; column: number } { if (typeof offset !== 'number') return offset if (offset > source.length) { throw new Error('offset is longer than source length!') } const lines = source.split(splitRE) let counted = 0 let line = 0 let column = 0 for (; line < lines.length; line++) { const lineLength = lines[line].length + 1 if (counted + lineLength >= offset) { column = offset - counted + 1 break } counted += lineLength } return { line: line + 1, column } } export function generateCodeFrame( source: string, start: number | { line: number; column: number } = 0, end?: number ): string { start = posToNumber(source, start) end = end || start const lines = source.split(splitRE) let count = 0 const res: string[] = [] for (let i = 0; i < lines.length; i++) { count += lines[i].length + 1 if (count >= start) { for (let j = i - range; j <= i + range || end > count; j++) { if (j < 0 || j >= lines.length) continue const line = j + 1 res.push( `${line}${' '.repeat(Math.max(3 - String(line).length, 0))}| ${ lines[j] }` ) const lineLength = lines[j].length if (j === i) { // push underline const pad = start - (count - lineLength) + 1 const length = Math.max( 1, end > count ? lineLength - pad : end - start ) res.push(` | ` + ' '.repeat(pad) + '^'.repeat(length)) } else if (j > i) { if (end > count) { const length = Math.max(Math.min(end - count, lineLength), 1) res.push(` | ` + '^'.repeat(length)) } count += lineLength + 1 } } break } } return res.join('\n') } export function writeFile( filename: string, content: string | Uint8Array ): void { const dir = path.dirname(filename) if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }) } fs.writeFileSync(filename, content) } /** * Delete every file and subdirectory. **The given directory must exist.** * Pass an optional `skip` array to preserve files in the root directory. */ export function emptyDir(dir: string, skip?: string[]): void { for (const file of fs.readdirSync(dir)) { if (skip?.includes(file)) { continue } const abs = path.resolve(dir, file) // baseline is Node 12 so can't use rmSync :( if (fs.lstatSync(abs).isDirectory()) { emptyDir(abs) fs.rmdirSync(abs) } else { fs.unlinkSync(abs) } } } export function copyDir(srcDir: string, destDir: string): void { fs.mkdirSync(destDir, { recursive: true }) for (const file of fs.readdirSync(srcDir)) { const srcFile = path.resolve(srcDir, file) const destFile = path.resolve(destDir, file) const stat = fs.statSync(srcFile) if (stat.isDirectory()) { copyDir(srcFile, destFile) } else { fs.copyFileSync(srcFile, destFile) } } } export function ensureLeadingSlash(path: string): string { return !path.startsWith('/') ? '/' + path : path } export function ensureWatchedFile( watcher: FSWatcher, file: string | null, root: string ): void { if ( file && // only need to watch if out of root !file.startsWith(root + '/') && // some rollup plugins use null bytes for private resolved Ids !file.includes('\0') && fs.existsSync(file) ) { // resolve file to normalized system path watcher.add(path.resolve(file)) } } interface ImageCandidate { url: string descriptor: string } const escapedSpaceCharacters = /( |\\t|\\n|\\f|\\r)+/g export async function processSrcSet( srcs: string, replacer: (arg: ImageCandidate) => Promise<string> ): Promise<string> { const imageCandidates: ImageCandidate[] = srcs .split(',') .map((s) => { const [url, descriptor] = s .replace(escapedSpaceCharacters, ' ') .trim() .split(' ', 2) return { url, descriptor } }) .filter(({ url }) => !!url) const ret = await Promise.all( imageCandidates.map(async ({ url, descriptor }) => { return { url: await replacer({ url, descriptor }), descriptor } }) ) const url = ret.reduce((prev, { url, descriptor }, index) => { descriptor = descriptor || '' return (prev += url + ` ${descriptor}${index === ret.length - 1 ? '' : ', '}`) }, '') return url } // based on https://github.com/sveltejs/svelte/blob/abf11bb02b2afbd3e4cac509a0f70e318c306364/src/compiler/utils/mapped_code.ts#L221 const nullSourceMap: RawSourceMap = { names: [], sources: [], mappings: '', version: 3 } export function combineSourcemaps( filename: string, sourcemapList: Array<DecodedSourceMap | RawSourceMap> ): RawSourceMap { if ( sourcemapList.length === 0 || sourcemapList.every((m) => m.sources.length === 0) ) { return { ...nullSourceMap } } let map let mapIndex = 1 const useArrayInterface = sourcemapList.slice(0, -1).find((m) => m.sources.length !== 1) === undefined if (useArrayInterface) { map = remapping(sourcemapList, () => null, true) } else { map = remapping( sourcemapList[0], function loader(sourcefile) { if (sourcefile === filename && sourcemapList[mapIndex]) { return sourcemapList[mapIndex++] } else { return { ...nullSourceMap } } }, true ) } if (!map.file) { delete map.file } return map as RawSourceMap } export function unique<T>(arr: T[]): T[] { return Array.from(new Set(arr)) } export interface Hostname { // undefined sets the default behaviour of server.listen host: string | undefined // resolve to localhost when possible name: string } export function resolveHostname( optionsHost: string | boolean | undefined ): Hostname { let host: string | undefined if ( optionsHost === undefined || optionsHost === false || optionsHost === 'localhost' ) { // Use a secure default host = '127.0.0.1' } else if (optionsHost === true) { // If passed --host in the CLI without arguments host = undefined // undefined typically means 0.0.0.0 or :: (listen on all IPs) } else { host = optionsHost } // Set host name to localhost when possible, unless the user explicitly asked for '127.0.0.1' const name = (optionsHost !== '127.0.0.1' && host === '127.0.0.1') || host === '0.0.0.0' || host === '::' || host === undefined ? 'localhost' : host return { host, name } }