Created
March 5, 2025 10:47
-
-
Save notcome/18d31fe63313041b2880753b04816a3e to your computer and use it in GitHub Desktop.
A faster Deno vite plugin
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Plugin } from 'vite' | |
import fsp from 'node:fs/promises' | |
import { transform } from "esbuild"; | |
import { | |
isDenoSpecifier, | |
mediaTypeToLoader, | |
parseDenoSpecifier, | |
resolveDeno, | |
ResolvedInfo, | |
toDenoSpecifier, | |
} from './utils.ts' | |
import { cwd } from 'node:process' | |
const logLevel = 100 | |
const peerModulePrefix = '@pea2' | |
const viteTempPrefixes = ['/__manifest?', '/@id/__x00', '/shadcn/'] | |
async function log(message: string, level: number = 0) { | |
if (level < logLevel) { | |
return | |
} | |
await fsp.appendFile('/tmp/faster-deno.txt', message + '\n\n') | |
} | |
class Lock { | |
private locked: boolean = false | |
private waitQueue: Array<() => void> = [] | |
acquireLock(): Promise<void> { | |
// If lock is not currently held, acquire it immediately | |
if (!this.locked) { | |
this.locked = true | |
return Promise.resolve() | |
} | |
// Otherwise, wait for the lock to be released | |
return new Promise<void>((resolve) => { | |
this.waitQueue.push(resolve) | |
}) | |
} | |
releaseLock(): void { | |
if (!this.locked) { | |
throw new Error('Cannot release a lock that is not held') | |
} | |
// If there are waiters, resolve the next one in queue | |
if (this.waitQueue.length > 0) { | |
const nextResolver = this.waitQueue.shift()! | |
nextResolver() | |
} else { | |
// Otherwise, mark the lock as free | |
this.locked = false | |
} | |
} | |
} | |
class Resolver { | |
cwd: string | |
esmModules: Map<string, ResolvedInfo> | |
codeSpecifierToPath: Map<string, string> | |
peerModules: Map<string, ResolvedInfo> | |
lock: Lock | |
constructor(cwd: string) { | |
this.cwd = cwd | |
this.esmModules = new Map() | |
this.codeSpecifierToPath = new Map() | |
this.peerModules = new Map() | |
this.lock = new Lock() | |
} | |
async resolveId(id: string): Promise<ResolvedInfo | null> { | |
const isPeerModule = id.startsWith(peerModulePrefix) | |
if (isPeerModule) { | |
const peerModule = this.peerModules.get(id) | |
if (peerModule) { | |
return peerModule | |
} | |
} | |
await log( | |
`Execute "deno info" for\n ${id}`, 1 | |
) | |
const result = await resolveDeno(id, this.cwd) | |
if (!result) { | |
return null | |
} | |
const { specifier, tree } = result | |
let targetModule: ResolvedInfo | null = null | |
let cachedPathCount = 0 | |
for (const module of tree.modules) { | |
if (!('kind' in module && module.kind === 'esm')) { | |
// await log(`Skipping non-esm module\n ${module.specifier}`) | |
continue | |
} | |
const modulePath = module.local | |
if (!this.esmModules.has(modulePath)) { | |
this.esmModules.set(modulePath, module) | |
this.codeSpecifierToPath.set(module.specifier, modulePath) | |
cachedPathCount += 1 | |
} | |
if (specifier === module.specifier) { | |
targetModule = module | |
if (isPeerModule) { | |
this.peerModules.set(id, module) | |
} | |
} | |
} | |
await log(`Cached ${cachedPathCount} paths`) | |
return targetModule | |
} | |
async fetchESMModule(path: string, id: string): Promise<ResolvedInfo | null> { | |
const module = this.esmModules.get(path) | |
if (module) { | |
return module | |
} | |
return await this.resolveId(id) | |
} | |
async _resolveNestedImport( | |
id: string, | |
importerId: string, | |
importerPath: string, | |
): Promise<string | null> { | |
const module = await this.fetchESMModule(importerPath, importerId) | |
if (!module) { | |
await log('Cannot resolve due to missing importer module!') | |
return null | |
} | |
const dependency = module.dependencies.find((d) => d.specifier === id) | |
if (!dependency) { | |
await log( | |
`Cannot resolve since dependency is missing!:\n${ | |
JSON.stringify(module, null, 2) | |
}`, | |
) | |
return null | |
} | |
const specifier = dependency.code.specifier | |
let targetModule: ResolvedInfo | undefined = undefined | |
const targetPath = this.codeSpecifierToPath.get(specifier) | |
if (targetPath) { | |
targetModule = this.esmModules.get(targetPath) | |
} else { | |
const result = await this.resolveId(specifier) | |
if (!result) { | |
await log(`Cannot resolve ${specifier}`) | |
return null | |
} | |
targetModule = result | |
} | |
if (!targetModule) { | |
throw new Error('Internal inconsistency!') | |
} | |
return toDenoSpecifier( | |
targetModule.mediaType, | |
specifier, | |
targetModule.local, | |
) | |
} | |
async resolveNestedImport( | |
id: string, | |
importerId: string, | |
importerPath: string, | |
): Promise<string | null> { | |
await this.lock.acquireLock() | |
try { | |
await log( | |
`Requested to resolve\n ${id}\n ${importerId}\n ${importerPath}`, | |
) | |
const resolved = await this._resolveNestedImport( | |
id, | |
importerId, | |
importerPath, | |
) | |
await log( | |
`Resolved to ${resolved} for:\n ${id}\n ${importerId}\n ${importerPath}`, | |
) | |
return resolved | |
} finally { | |
this.lock.releaseLock() | |
} | |
} | |
async _resolveBase(id: string): Promise<string | null> { | |
const targetModule = await this.resolveId(id) | |
if (!targetModule) { | |
await log(`Cannot resolve ${id}`) | |
return null | |
} | |
return toDenoSpecifier( | |
targetModule.mediaType, | |
targetModule.specifier, | |
targetModule.local, | |
) | |
} | |
async resolveBase(id: string): Promise<string | null> { | |
await this.lock.acquireLock() | |
try { | |
return await this._resolveBase(id) | |
} finally { | |
this.lock.releaseLock() | |
} | |
} | |
} | |
function mainPlugin(): Plugin { | |
const root = cwd() | |
const resolver = new Resolver(root) | |
return { | |
async resolveId(id: string, importer: string) { | |
// Those are temporary virtual files created by vite. | |
// Fast-path here to improve refresh performance. | |
for (const prefix of viteTempPrefixes) { | |
if (id.startsWith(prefix)) { | |
return null | |
} | |
} | |
if (isDenoSpecifier(id)) { | |
return | |
} | |
if (!isDenoSpecifier(importer)) { | |
return await resolver.resolveBase(id) | |
} else { | |
// We only work with dependencies of Deno-specific files. | |
const { id: importerId, resolved: importerPath } = parseDenoSpecifier( | |
importer, | |
) | |
return await resolver.resolveNestedImport(id, importerId, importerPath) | |
} | |
}, | |
async load(id: string) { | |
if (!isDenoSpecifier(id)) { | |
return | |
} | |
const { loader, resolved } = parseDenoSpecifier(id); | |
const content = await fsp.readFile(resolved, "utf-8"); | |
if (loader === "JavaScript") | |
return content; | |
if (loader === "Json") { | |
return `export default ${content}`; | |
} | |
const result = await transform(content, { | |
format: "esm", | |
loader: mediaTypeToLoader(loader), | |
logLevel: "debug", | |
}); | |
// Issue: https://github.com/denoland/deno-vite-plugin/issues/38 | |
// Esbuild uses an empty string as empty value and vite expects | |
// `null` to be the empty value. This seems to be only the case in | |
// `dev` mode | |
const map = result.map === "" ? null : result.map; | |
return { | |
code: result.code, | |
map, | |
}; | |
}, | |
} | |
} | |
export default function fasterDeno(): Plugin[] { | |
return [mainPlugin()] | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { exec, execFile, ExecOptions } from 'node:child_process' | |
import process from 'node:process' | |
import { Loader } from 'esbuild' | |
export type DenoSpecifierName = string & { __brand: 'deno' } | |
export type DenoMediaType = | |
| 'TypeScript' | |
| 'TSX' | |
| 'JavaScript' | |
| 'JSX' | |
| 'Json' | |
export function isDenoSpecifier(str: string): str is DenoSpecifierName { | |
return str.startsWith('\0deno') | |
} | |
export function toDenoSpecifier( | |
loader: DenoMediaType, | |
id: string, | |
resolved: string, | |
): DenoSpecifierName { | |
return `\0deno::${loader}::${id}::${resolved}` as DenoSpecifierName | |
} | |
export function parseDenoSpecifier(spec: DenoSpecifierName): { | |
loader: DenoMediaType | |
id: string | |
resolved: string | |
} { | |
const [_, loader, id, resolved] = spec.split('::') as [ | |
string, | |
string, | |
DenoMediaType, | |
string, | |
] | |
return { loader: loader as DenoMediaType, id, resolved } | |
} | |
export interface ResolvedInfo { | |
kind: 'esm' | |
local: string | |
size: number | |
mediaType: DenoMediaType | |
specifier: string | |
dependencies: Array<{ | |
specifier: string | |
code: { | |
specifier: string | |
span: { start: unknown; end: unknown } | |
} | |
}> | |
} | |
interface NpmResolvedInfo { | |
kind: 'npm' | |
specifier: string | |
npmPackage: string | |
} | |
interface ExternalResolvedInfo { | |
kind: 'external' | |
specifier: string | |
} | |
interface ResolveError { | |
specifier: string | |
error: string | |
} | |
interface DenoInfoJsonV1 { | |
version: 1 | |
redirects: Record<string, string> | |
roots: string[] | |
modules: Array< | |
NpmResolvedInfo | ResolvedInfo | ExternalResolvedInfo | ResolveError | |
> | |
} | |
export interface DenoResolveResult { | |
id: string | |
kind: 'esm' | 'npm' | |
loader: DenoMediaType | null | |
dependencies: ResolvedInfo['dependencies'] | |
} | |
function isResolveError( | |
info: NpmResolvedInfo | ResolvedInfo | ExternalResolvedInfo | ResolveError, | |
): info is ResolveError { | |
return 'error' in info && typeof info.error === 'string' | |
} | |
let checkedDenoInstall = false | |
const DENO_BINARY = process.platform === 'win32' ? 'deno.exe' : 'deno' | |
export async function execAsync( | |
cmd: string, | |
options: ExecOptions, | |
): Promise<{ stderr: string; stdout: string }> { | |
return await new Promise((resolve, reject) => | |
exec(cmd, options, (error, stdout, stderr) => { | |
if (error) reject(error) | |
else resolve({ stdout, stderr }) | |
}) | |
) | |
} | |
export async function resolveDeno( | |
id: string, | |
cwd: string, | |
): Promise<{ specifier: string; tree: DenoInfoJsonV1 } | null> { | |
if (!checkedDenoInstall) { | |
try { | |
await execAsync(`${DENO_BINARY} --version`, { cwd }) | |
checkedDenoInstall = true | |
} catch { | |
throw new Error( | |
`Deno binary could not be found. Install Deno to resolve this error.`, | |
) | |
} | |
} | |
// There is no JS-API in Deno to get the final file path in Deno's | |
// cache directory. The `deno info` command reveals that information | |
// though, so we can use that. | |
const output = await new Promise<string | null>((resolve, reject) => { | |
execFile(DENO_BINARY, ['info', '--json', id], { cwd }, (error, stdout) => { | |
if (error) { | |
if (String(error).includes('Integrity check failed')) { | |
reject(error) | |
} else { | |
resolve(null) | |
} | |
} else resolve(stdout) | |
}) | |
}) | |
if (output === null) { | |
return null | |
} | |
const json = JSON.parse(output) as DenoInfoJsonV1 | |
const actualId = json.roots[0] | |
// Find the final resolved cache path. First, we need to check | |
// if the redirected specifier, which represents the final specifier. | |
// This is often used for `http://` imports where a server can do | |
// redirects. | |
const redirected = json.redirects[actualId] ?? actualId | |
// Find the module information based on the redirected speciffier | |
const mod = json.modules.find((info) => info.specifier === redirected) | |
if (mod === undefined) { | |
return null | |
} | |
// Specifier not found by deno | |
if (isResolveError(mod)) { | |
return null | |
} | |
return { | |
specifier: redirected, | |
tree: json, | |
} | |
} | |
export function mediaTypeToLoader(media: DenoMediaType): Loader { | |
switch (media) { | |
case "JSX": | |
return "jsx"; | |
case "JavaScript": | |
return "js"; | |
case "Json": | |
return "json"; | |
case "TSX": | |
return "tsx"; | |
case "TypeScript": | |
return "ts"; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment