webpack-mkcert
Version:
Provide certificates for webpack https dev service
325 lines (262 loc) • 7.65 kB
text/typescript
import fs from 'fs'
import process from 'process'
import pc from 'picocolors'
// import { Logger } from 'vite'
import { debug } from '../lib/logger'
import {
ensureDirExist,
escape,
exec,
exists,
getHash,
prettyLog,
resolvePath
} from '../lib/util'
import Config from './config'
import Downloader from './downloader'
import Record from './record'
import { BaseSource, GithubSource, CodingSource } from './source'
import VersionManger from './version'
export type SourceType = 'github' | 'coding' | BaseSource
export type Logger = typeof console
export type MkcertOptions = {
/**
* 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
*
* @description it should be absolute path
* @default none
*/
mkcertPath?: string
}
export type MkcertProps = MkcertOptions & {
logger: Logger
}
const KEY_FILE_PATH = resolvePath('certs/dev.key')
const CERT_FILE_PATH = resolvePath('certs/dev.pem')
class Mkcert {
private force?: boolean
private autoUpgrade?: boolean
private mkcertLocalPath?: string
private source: BaseSource
private logger: Logger
private mkcertSavedPath: string
private sourceType: SourceType
private config: Config
public static create(options: MkcertProps) {
return new Mkcert(options)
}
private constructor(options: MkcertProps) {
const { force, autoUpgrade, source, mkcertPath, logger } = options
this.force = force
this.logger = logger
this.autoUpgrade = autoUpgrade
this.mkcertLocalPath = mkcertPath
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.mkcertSavedPath = resolvePath(
process.platform === 'win32' ? 'mkcert.exe' : 'mkcert'
)
this.config = new Config()
}
private async getMkcertBinnary() {
return (await this.checkMkcert())
? this.mkcertLocalPath || this.mkcertSavedPath
: undefined
}
/**
* Check if mkcert exists
*/
private async checkMkcert() {
let exist: boolean
if (this.mkcertLocalPath) {
exist = await exists(this.mkcertLocalPath)
if (!exists) {
this.logger.error(
pc.red(
`${this.mkcertLocalPath} does not exist, please check the mkcertPath paramter`
)
)
}
} else {
exist = await exists(this.mkcertSavedPath)
}
return exist
}
private async getCertificate() {
const key = await fs.promises.readFile(KEY_FILE_PATH)
const cert = await fs.promises.readFile(CERT_FILE_PATH)
return {
key,
cert
}
}
private async createCertificate(hosts: string[]) {
const names = hosts.join(' ')
const mkcertBinnary = await this.getMkcertBinnary()
if (!mkcertBinnary) {
debug(
`Mkcert does not exist, unable to generate certificate for ${names}`
)
}
await ensureDirExist(KEY_FILE_PATH)
await ensureDirExist(CERT_FILE_PATH)
const cmd = `${escape(mkcertBinnary)} -install -key-file ${escape(
KEY_FILE_PATH
)} -cert-file ${escape(CERT_FILE_PATH)} ${names}`
await exec(cmd, {
env: {
...process.env,
JAVA_HOME: undefined
}
})
this.logger.info(
`The certificate is saved in:\n${KEY_FILE_PATH}\n${CERT_FILE_PATH}`
)
}
private getLatestHash = async () => {
return {
key: await getHash(KEY_FILE_PATH),
cert: await getHash(CERT_FILE_PATH)
}
}
private async regenerate(record: Record, hosts: string[]) {
await this.createCertificate(hosts)
const hash = await this.getLatestHash()
record.update({ hosts, hash })
}
public async init() {
await this.config.init()
const exist = await this.checkMkcert()
if (!exist) {
await this.initMkcert()
} else if (this.autoUpgrade) {
await this.upgradeMkcert()
}
}
private async getSourceInfo() {
const sourceInfo = await this.source.getSourceInfo()
if (!sourceInfo) {
if (typeof this.sourceType === 'string') {
this.logger.error(
'Failed to request mkcert information, please check your network'
)
if (this.sourceType === 'github') {
this.logger.info(
'If you are a user in china, maybe you should set "source" paramter to "coding"'
)
}
} else {
this.logger.info(
'Please check your custom "source", it seems to return invalid result'
)
}
return undefined
}
return sourceInfo
}
private async initMkcert() {
const sourceInfo = await this.getSourceInfo()
debug('The mkcert does not exist, download it now')
if (!sourceInfo) {
this.logger.error(
'Can not obtain download information of mkcert, init skipped'
)
return
}
await this.downloadMkcert(sourceInfo.downloadUrl, this.mkcertSavedPath)
}
private async upgradeMkcert() {
const versionManger = new VersionManger({ config: this.config })
const sourceInfo = await this.getSourceInfo()
if (!sourceInfo) {
this.logger.error(
'Can not obtain download information of mkcert, update skipped'
)
return
}
const versionInfo = versionManger.compare(sourceInfo.version)
if (!versionInfo.shouldUpdate) {
debug('Mkcert is kept latest version, update skipped')
return
}
if (versionInfo.breakingChange) {
debug(
'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(
'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.mkcertSavedPath)
versionManger.update(versionInfo.nextVersion)
}
private async downloadMkcert(sourceUrl: string, distPath: string) {
const downloader = Downloader.create()
await downloader.download(sourceUrl, distPath)
}
public async renew(hosts: string[]) {
const record = new Record({ config: this.config })
if (this.force) {
debug(`Certificate is forced to regenerate`)
await this.regenerate(record, hosts)
}
if (!record.contains(hosts)) {
debug(
`The hosts changed from [${record.getHosts()}] to [${hosts}], start regenerate certificate`
)
await this.regenerate(record, hosts)
return
}
const hash = await this.getLatestHash()
if (record.tamper(hash)) {
debug(
`The hash changed from ${prettyLog(record.getHash())} to ${prettyLog(
hash
)}, start regenerate certificate`
)
await this.regenerate(record, hosts)
return
}
debug('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