UNPKG

vite-plugin-mkcert

Version:

Provide certificates for vite's https dev service

399 lines (325 loc) 9.47 kB
import path from 'node:path' import process from 'node:process' import { execFile, findCommandFromPath } from '../utils/command' import { PLUGIN_DATA_DIR } from '../utils/constant' import { copyDir, ensureDirExist, exists, getHash, readDir, readFile } from '../utils/fs' import { debug_log, error_log, prettyLog, warn_log } from '../utils/logger' import Config from './config' import Downloader from './downloader' import Record from './record' import { type BaseSource, CodingSource, GithubSource } from './source' import VersionManger from './version' export type SourceType = 'github' | 'coding' | BaseSource export type MkcertBaseOptions = { /** * Whether to force generate */ force?: boolean /** * Automatically upgrade mkcert * * @default false */ autoUpgrade?: boolean /** * Specify mkcert download source * * @default github */ source?: SourceType /** * If your network is restricted, you can specify a local binary file instead of downloading, it should be an absolute path * * @default none */ mkcertPath?: string /** * Proxy used when downloading mkcert binary. * * @example http://127.0.0.1:7890 */ proxy?: string /** * Whether to print download progress logs when fetching mkcert binary. * * @default true */ downloadProgress?: boolean /** * The location to save the files, such as key and cert files */ savePath?: string /** * The name of private key file generated by mkcert */ keyFileName?: string /** * The name of cert file generated by mkcert */ certFileName?: string } export type MkcertOptions = MkcertBaseOptions class Mkcert { private force?: boolean private autoUpgrade?: boolean private sourceType: SourceType private savePath: string private source: BaseSource private localMkcert?: string private proxy?: string private downloadProgress: boolean private savedMkcert: string private keyFilePath: string private certFilePath: string private config: Config public static create(options: MkcertOptions) { return new Mkcert(options) } private constructor(options: MkcertOptions) { const { force, autoUpgrade, source, mkcertPath, proxy, downloadProgress = true, savePath = PLUGIN_DATA_DIR, keyFileName = 'dev.pem', certFileName = 'cert.pem' } = options this.force = force this.autoUpgrade = autoUpgrade this.localMkcert = mkcertPath this.proxy = proxy?.trim() || undefined this.downloadProgress = downloadProgress this.savePath = path.resolve(savePath) this.keyFilePath = path.resolve(this.savePath, keyFileName) this.certFilePath = path.resolve(this.savePath, certFileName) this.sourceType = source || 'github' if (this.sourceType === 'github') { this.source = GithubSource.create() } else if (this.sourceType === 'coding') { this.source = CodingSource.create() } else { this.source = this.sourceType } this.savedMkcert = path.resolve( this.savePath, process.platform === 'win32' ? 'mkcert.exe' : 'mkcert' ) this.config = new Config({ savePath: this.savePath }) } private async getMkcertBinary() { if (this.localMkcert) { if (await exists(this.localMkcert)) { return this.localMkcert } else { warn_log( `${this.localMkcert} does not exist, please check the mkcertPath parameter` ) } } if (await exists(this.savedMkcert)) { return this.savedMkcert } return findCommandFromPath('mkcert') } private async checkCAExists() { const files = await readDir(this.savePath) return files.some(file => file.includes('rootCA')) } private async retainExistedCA() { if (await this.checkCAExists()) { return } const mkcertBinary = await this.getMkcertBinary() if (!mkcertBinary) { return } const commandArgs = ['-CAROOT'] debug_log(`Exec ${mkcertBinary} ${commandArgs.join(' ')}`) const commandResult = await execFile(mkcertBinary, commandArgs) const caDirPath = path.resolve(commandResult.stdout.toString().trim()) if (caDirPath === this.savePath) { return } const caDirExists = await exists(caDirPath) if (!caDirExists) { return } await copyDir(caDirPath, this.savePath) } private async getCertificate() { const key = await readFile(this.keyFilePath) const cert = await readFile(this.certFilePath) return { key, cert } } private async createCertificate(hosts: string[]) { const mkcertBinary = await this.getMkcertBinary() if (!mkcertBinary) { error_log( `Mkcert does not exist, unable to generate certificate for ${hosts.join(', ')}` ) throw new Error('Mkcert binary is not found') } await ensureDirExist(this.savePath) await this.retainExistedCA() const commandArgs = [ '-install', '-key-file', this.keyFilePath, '-cert-file', this.certFilePath, ...hosts ] await execFile(mkcertBinary, commandArgs, { env: { ...process.env, CAROOT: this.savePath, JAVA_HOME: undefined } }) debug_log( `The list of generated files:\n${this.keyFilePath}\n${this.certFilePath}` ) } private getLatestHash = async () => { return { key: await getHash(this.keyFilePath), cert: await getHash(this.certFilePath) } } private async regenerate(record: Record, hosts: string[]) { await this.createCertificate(hosts) const hash = await this.getLatestHash() record.update({ hosts, hash }) } public async init() { await ensureDirExist(this.savePath) await this.config.init() const mkcertBinary = await this.getMkcertBinary() if (!mkcertBinary) { await this.initMkcert() } else if (this.autoUpgrade) { await this.upgradeMkcert() } } private async getSourceInfo() { const sourceInfo = await this.source.getSourceInfo() if (!sourceInfo) { const message = typeof this.sourceType === 'string' ? `Unsupported platform. Unable to find a binary file for ${process.platform } platform with ${process.arch} arch on ${this.sourceType === 'github' ? 'https://github.com/FiloSottile/mkcert/releases' : 'https://liuweigl.coding.net/p/github/artifacts?hash=8d4dd8949af543159c1b5ac71ff1ff72' }` : 'Please check your custom "source", it seems to return invalid result' throw new Error(message) } return sourceInfo } private async initMkcert() { const sourceInfo = await this.getSourceInfo() warn_log('The mkcert does not exist, download it now') await this.downloadMkcert( sourceInfo.downloadUrl, this.savedMkcert, this.proxy ) } private async upgradeMkcert() { const versionManger = new VersionManger({ config: this.config }) const sourceInfo = await this.getSourceInfo() if (!sourceInfo) { warn_log('Can not obtain download information of mkcert, update skipped') return } const versionInfo = versionManger.compare(sourceInfo.version) if (!versionInfo.shouldUpdate) { debug_log('Mkcert is kept latest version, update skipped') return } if (versionInfo.breakingChange) { debug_log( 'The current version of mkcert is %s, and the latest version is %s, there may be some breaking changes, update skipped', versionInfo.currentVersion, versionInfo.nextVersion ) return } debug_log( 'The current version of mkcert is %s, and the latest version is %s, mkcert will be updated', versionInfo.currentVersion, versionInfo.nextVersion ) await this.downloadMkcert( sourceInfo.downloadUrl, this.savedMkcert, this.proxy ) versionManger.update(versionInfo.nextVersion) } private async downloadMkcert( sourceUrl: string, distPath: string, proxy?: string ) { const downloader = Downloader.create() await downloader.download({ downloadUrl: sourceUrl, savedPath: distPath, proxy, logProgress: this.downloadProgress }) } public async renew(hosts: string[]) { const record = new Record({ config: this.config }) if (this.force) { debug_log('Certificate is forced to regenerate') await this.regenerate(record, hosts) return } if (!record.contains(hosts)) { debug_log( `The hosts changed from [${record.getHosts()}] to [${hosts}], start regenerate certificate` ) await this.regenerate(record, hosts) return } const hash = await this.getLatestHash() if (!record.equal(hash)) { debug_log( `The hash changed from ${prettyLog(record.getHash())} to ${prettyLog( hash )}, start regenerate certificate` ) await this.regenerate(record, hosts) return } debug_log('Neither hosts nor hash has changed, skip regenerate certificate') } /** * Get certificates * * @param hosts host collection * @returns cretificates */ public async install(hosts: string[]) { if (hosts.length) { await this.renew(hosts) } return await this.getCertificate() } } export default Mkcert