vite
Version:
Native-ESM powered web dev build tool
508 lines (453 loc) • 13.9 kB
text/typescript
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 }
}