vite-plugin-mkcert
Version:
Provide certificates for vite's https dev service
399 lines (325 loc) • 9.47 kB
text/typescript
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