mkcert.mjs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663
  1. // plugin/index.ts
  2. import { createLogger } from "vite";
  3. // plugin/lib/constant.ts
  4. import os from "node:os";
  5. import path from "node:path";
  6. var PKG_NAME = "vite-plugin-mkcert";
  7. var PLUGIN_NAME = PKG_NAME.replace(/-/g, ":");
  8. var PLUGIN_DATA_DIR = path.join(os.homedir(), `.${PKG_NAME}`);
  9. // plugin/lib/util.ts
  10. import child_process from "node:child_process";
  11. import crypto from "node:crypto";
  12. import fs from "node:fs";
  13. import os2 from "node:os";
  14. import path2 from "node:path";
  15. import util from "node:util";
  16. var exists = async (filePath) => {
  17. try {
  18. await fs.promises.access(filePath);
  19. return true;
  20. } catch (_error) {
  21. return false;
  22. }
  23. };
  24. var mkdir = async (dirname) => {
  25. const isExist = await exists(dirname);
  26. if (!isExist) {
  27. await fs.promises.mkdir(dirname, { recursive: true });
  28. }
  29. };
  30. var ensureDirExist = async (filePath, strip = false) => {
  31. const dirname = strip ? path2.dirname(filePath) : filePath;
  32. await mkdir(dirname);
  33. };
  34. var readFile = async (filePath) => {
  35. const isExist = await exists(filePath);
  36. return isExist ? (await fs.promises.readFile(filePath)).toString() : void 0;
  37. };
  38. var writeFile = async (filePath, data) => {
  39. await ensureDirExist(filePath, true);
  40. await fs.promises.writeFile(filePath, data);
  41. await fs.promises.chmod(filePath, 511);
  42. };
  43. var readDir = async (source) => {
  44. return fs.promises.readdir(source);
  45. };
  46. var copyDir = async (source, dest) => {
  47. try {
  48. await fs.promises.cp(source, dest, {
  49. recursive: true
  50. });
  51. } catch (error) {
  52. console.log(`${PLUGIN_NAME}:`, error);
  53. }
  54. };
  55. var exec = async (cmd, options) => {
  56. return util.promisify(child_process.exec)(cmd, options);
  57. };
  58. var isIPV4 = (family) => {
  59. return family === "IPv4" || family === 4;
  60. };
  61. var getLocalV4Ips = () => {
  62. const interfaceDict = os2.networkInterfaces();
  63. const addresses = [];
  64. for (const key in interfaceDict) {
  65. const interfaces = interfaceDict[key];
  66. if (interfaces) {
  67. for (const item of interfaces) {
  68. if (isIPV4(item.family)) {
  69. addresses.push(item.address);
  70. }
  71. }
  72. }
  73. }
  74. return addresses;
  75. };
  76. var getDefaultHosts = () => {
  77. return ["localhost", ...getLocalV4Ips()];
  78. };
  79. var getHash = async (filePath) => {
  80. const content = await readFile(filePath);
  81. if (content) {
  82. const hash = crypto.createHash("sha256");
  83. hash.update(content);
  84. return hash.digest("hex");
  85. }
  86. return void 0;
  87. };
  88. var isObj = (obj) => Object.prototype.toString.call(obj) === "[object Object]";
  89. var mergeObj = (target, source) => {
  90. if (!(isObj(target) && isObj(source))) {
  91. return target;
  92. }
  93. for (const key in source) {
  94. if (Object.prototype.hasOwnProperty.call(source, key)) {
  95. const targetValue = target[key];
  96. const sourceValue = source[key];
  97. if (isObj(targetValue) && isObj(sourceValue)) {
  98. mergeObj(targetValue, sourceValue);
  99. } else {
  100. target[key] = sourceValue;
  101. }
  102. }
  103. }
  104. };
  105. var deepMerge = (target, ...source) => {
  106. return source.reduce((a, b) => mergeObj(a, b), target);
  107. };
  108. var prettyLog = (obj) => {
  109. return JSON.stringify(obj, null, 2);
  110. };
  111. var escapeStr = (path5) => {
  112. return `"${path5}"`;
  113. };
  114. // plugin/mkcert/index.ts
  115. import path4 from "node:path";
  116. import process2 from "node:process";
  117. import pc from "picocolors";
  118. // plugin/lib/logger.ts
  119. import Debug from "debug";
  120. var debug = Debug(PLUGIN_NAME);
  121. // plugin/mkcert/config.ts
  122. import path3 from "node:path";
  123. var CONFIG_FILE_NAME = "config.json";
  124. var Config = class {
  125. /**
  126. * The mkcert version
  127. */
  128. version;
  129. record;
  130. configFilePath;
  131. constructor({ savePath }) {
  132. this.configFilePath = path3.resolve(savePath, CONFIG_FILE_NAME);
  133. }
  134. async init() {
  135. const str = await readFile(this.configFilePath);
  136. const options = str ? JSON.parse(str) : void 0;
  137. if (options) {
  138. this.version = options.version;
  139. this.record = options.record;
  140. }
  141. }
  142. async serialize() {
  143. await writeFile(this.configFilePath, prettyLog(this));
  144. }
  145. // deep merge
  146. async merge(obj) {
  147. const currentStr = prettyLog(this);
  148. deepMerge(this, obj);
  149. const nextStr = prettyLog(this);
  150. debug(
  151. `Receive parameter
  152. ${prettyLog(
  153. obj
  154. )}
  155. Update config from
  156. ${currentStr}
  157. to
  158. ${nextStr}`
  159. );
  160. await this.serialize();
  161. }
  162. getRecord() {
  163. return this.record;
  164. }
  165. getVersion() {
  166. return this.version;
  167. }
  168. };
  169. var config_default = Config;
  170. // plugin/lib/request.ts
  171. import axios from "axios";
  172. var request = axios.create();
  173. request.interceptors.response.use(
  174. (res) => {
  175. return res;
  176. },
  177. (error) => {
  178. debug("Request error: %o", error);
  179. return Promise.reject(error);
  180. }
  181. );
  182. var request_default = request;
  183. // plugin/mkcert/downloader.ts
  184. var Downloader = class _Downloader {
  185. static create() {
  186. return new _Downloader();
  187. }
  188. constructor() {
  189. }
  190. async download(downloadUrl, savedPath) {
  191. debug("Downloading the mkcert executable from %s", downloadUrl);
  192. const { data } = await request_default.get(downloadUrl, {
  193. responseType: "arraybuffer"
  194. });
  195. await writeFile(savedPath, data);
  196. debug("The mkcert has been saved to %s", savedPath);
  197. }
  198. };
  199. var downloader_default = Downloader;
  200. // plugin/mkcert/record.ts
  201. var Record = class {
  202. config;
  203. constructor(options) {
  204. this.config = options.config;
  205. }
  206. getHosts() {
  207. return this.config.getRecord()?.hosts;
  208. }
  209. getHash() {
  210. return this.config.getRecord()?.hash;
  211. }
  212. contains(hosts) {
  213. const oldHosts = this.getHosts();
  214. if (!oldHosts) {
  215. return false;
  216. }
  217. for (const host of hosts) {
  218. if (!oldHosts.includes(host)) {
  219. return false;
  220. }
  221. }
  222. return true;
  223. }
  224. // whether the files has been tampered with
  225. equal(hash) {
  226. const oldHash = this.getHash();
  227. if (!oldHash) {
  228. return false;
  229. }
  230. return oldHash.key === hash.key && oldHash.cert === hash.cert;
  231. }
  232. async update(record) {
  233. await this.config.merge({ record });
  234. }
  235. };
  236. var record_default = Record;
  237. // plugin/mkcert/source.ts
  238. var BaseSource = class {
  239. getPlatformIdentifier() {
  240. const arch = process.arch === "x64" ? "amd64" : process.arch;
  241. return process.platform === "win32" ? `windows-${arch}.exe` : `${process.platform}-${arch}`;
  242. }
  243. };
  244. var GithubSource = class _GithubSource extends BaseSource {
  245. static create() {
  246. return new _GithubSource();
  247. }
  248. constructor() {
  249. super();
  250. }
  251. async getSourceInfo() {
  252. const { data } = await request_default({
  253. method: "GET",
  254. url: "https://api.github.com/repos/FiloSottile/mkcert/releases/latest"
  255. });
  256. const platformIdentifier = this.getPlatformIdentifier();
  257. const version = data.tag_name;
  258. const downloadUrl = data.assets.find(
  259. (item) => item.name.includes(platformIdentifier)
  260. )?.browser_download_url;
  261. if (!(version && downloadUrl)) {
  262. return void 0;
  263. }
  264. return {
  265. downloadUrl,
  266. version
  267. };
  268. }
  269. };
  270. var CodingSource = class _CodingSource extends BaseSource {
  271. static CODING_API = "https://e.coding.net/open-api";
  272. static CODING_AUTHORIZATION = "token 000f7831ec425079439b0f55f55c729c9280d66e";
  273. static CODING_PROJECT_ID = 8524617;
  274. static REPOSITORY = "mkcert";
  275. static create() {
  276. return new _CodingSource();
  277. }
  278. constructor() {
  279. super();
  280. }
  281. async request(data) {
  282. return request_default({
  283. data,
  284. method: "POST",
  285. url: _CodingSource.CODING_API,
  286. headers: {
  287. Authorization: _CodingSource.CODING_AUTHORIZATION
  288. }
  289. });
  290. }
  291. /**
  292. * Get filename of Coding.net artifacts
  293. *
  294. * @see https://liuweigl.coding.net/p/github/artifacts/885241/generic/packages
  295. *
  296. * @returns name
  297. */
  298. getPackageName() {
  299. return `mkcert-${this.getPlatformIdentifier()}`;
  300. }
  301. async getSourceInfo() {
  302. const { data: VersionData } = await this.request({
  303. Action: "DescribeArtifactVersionList",
  304. ProjectId: _CodingSource.CODING_PROJECT_ID,
  305. Repository: _CodingSource.REPOSITORY,
  306. Package: this.getPackageName(),
  307. PageSize: 1
  308. });
  309. const version = VersionData.Response.Data?.InstanceSet[0]?.Version;
  310. if (!version) {
  311. return void 0;
  312. }
  313. const { data: FileData } = await this.request({
  314. Action: "DescribeArtifactFileDownloadUrl",
  315. ProjectId: _CodingSource.CODING_PROJECT_ID,
  316. Repository: _CodingSource.REPOSITORY,
  317. Package: this.getPackageName(),
  318. PackageVersion: version
  319. });
  320. const downloadUrl = FileData.Response.Url;
  321. if (!downloadUrl) {
  322. return void 0;
  323. }
  324. return {
  325. downloadUrl,
  326. version
  327. };
  328. }
  329. };
  330. // plugin/mkcert/version.ts
  331. var parseVersion = (version) => {
  332. const str = version.trim().replace(/v/i, "");
  333. return str.split(".");
  334. };
  335. var VersionManger = class {
  336. config;
  337. constructor(props) {
  338. this.config = props.config;
  339. }
  340. async update(version) {
  341. try {
  342. await this.config.merge({ version });
  343. } catch (err) {
  344. debug("Failed to record mkcert version info: %o", err);
  345. }
  346. }
  347. compare(version) {
  348. const currentVersion = this.config.getVersion();
  349. if (!currentVersion) {
  350. return {
  351. currentVersion,
  352. nextVersion: version,
  353. breakingChange: false,
  354. shouldUpdate: true
  355. };
  356. }
  357. let breakingChange = false;
  358. let shouldUpdate = false;
  359. const newVersion = parseVersion(version);
  360. const oldVersion = parseVersion(currentVersion);
  361. for (let i = 0; i < newVersion.length; i++) {
  362. if (newVersion[i] > oldVersion[i]) {
  363. shouldUpdate = true;
  364. breakingChange = i === 0;
  365. break;
  366. }
  367. }
  368. return {
  369. breakingChange,
  370. shouldUpdate,
  371. currentVersion,
  372. nextVersion: version
  373. };
  374. }
  375. };
  376. var version_default = VersionManger;
  377. // plugin/mkcert/index.ts
  378. var Mkcert = class _Mkcert {
  379. force;
  380. autoUpgrade;
  381. sourceType;
  382. savePath;
  383. logger;
  384. source;
  385. localMkcert;
  386. savedMkcert;
  387. keyFilePath;
  388. certFilePath;
  389. config;
  390. static create(options) {
  391. return new _Mkcert(options);
  392. }
  393. constructor(options) {
  394. const {
  395. force,
  396. autoUpgrade,
  397. source,
  398. mkcertPath,
  399. savePath = PLUGIN_DATA_DIR,
  400. keyFileName = "dev.pem",
  401. certFileName = "cert.pem",
  402. logger
  403. } = options;
  404. this.force = force;
  405. this.logger = logger;
  406. this.autoUpgrade = autoUpgrade;
  407. this.localMkcert = mkcertPath;
  408. this.savePath = path4.resolve(savePath);
  409. this.keyFilePath = path4.resolve(savePath, keyFileName);
  410. this.certFilePath = path4.resolve(savePath, certFileName);
  411. this.sourceType = source || "github";
  412. if (this.sourceType === "github") {
  413. this.source = GithubSource.create();
  414. } else if (this.sourceType === "coding") {
  415. this.source = CodingSource.create();
  416. } else {
  417. this.source = this.sourceType;
  418. }
  419. this.savedMkcert = path4.resolve(
  420. savePath,
  421. process2.platform === "win32" ? "mkcert.exe" : "mkcert"
  422. );
  423. this.config = new config_default({ savePath: this.savePath });
  424. }
  425. async getMkcertBinary() {
  426. let binary;
  427. if (this.localMkcert) {
  428. if (await exists(this.localMkcert)) {
  429. binary = this.localMkcert;
  430. } else {
  431. this.logger.error(
  432. pc.red(
  433. `${this.localMkcert} does not exist, please check the mkcertPath parameter`
  434. )
  435. );
  436. }
  437. } else if (await exists(this.savedMkcert)) {
  438. binary = this.savedMkcert;
  439. }
  440. return binary;
  441. }
  442. async checkCAExists() {
  443. const files = await readDir(this.savePath);
  444. return files.some((file) => file.includes("rootCA"));
  445. }
  446. async retainExistedCA() {
  447. if (await this.checkCAExists()) {
  448. return;
  449. }
  450. const mkcertBinary = await this.getMkcertBinary();
  451. const commandStatement = `${escapeStr(mkcertBinary)} -CAROOT`;
  452. debug(`Exec ${commandStatement}`);
  453. const commandResult = await exec(commandStatement);
  454. const caDirPath = path4.resolve(
  455. commandResult.stdout.toString().replace(/\n/g, "")
  456. );
  457. if (caDirPath === this.savePath) {
  458. return;
  459. }
  460. const caDirExists = await exists(caDirPath);
  461. if (!caDirExists) {
  462. return;
  463. }
  464. await copyDir(caDirPath, this.savePath);
  465. }
  466. async getCertificate() {
  467. const key = await readFile(this.keyFilePath);
  468. const cert = await readFile(this.certFilePath);
  469. return {
  470. key,
  471. cert
  472. };
  473. }
  474. async createCertificate(hosts) {
  475. const names = hosts.join(" ");
  476. const mkcertBinary = await this.getMkcertBinary();
  477. if (!mkcertBinary) {
  478. debug(
  479. `Mkcert does not exist, unable to generate certificate for ${names}`
  480. );
  481. }
  482. await ensureDirExist(this.savePath);
  483. await this.retainExistedCA();
  484. const cmd = `${escapeStr(mkcertBinary)} -install -key-file ${escapeStr(
  485. this.keyFilePath
  486. )} -cert-file ${escapeStr(this.certFilePath)} ${names}`;
  487. await exec(cmd, {
  488. env: {
  489. ...process2.env,
  490. CAROOT: this.savePath,
  491. JAVA_HOME: void 0
  492. }
  493. });
  494. this.logger.info(
  495. `The list of generated files:
  496. ${this.keyFilePath}
  497. ${this.certFilePath}`
  498. );
  499. }
  500. getLatestHash = async () => {
  501. return {
  502. key: await getHash(this.keyFilePath),
  503. cert: await getHash(this.certFilePath)
  504. };
  505. };
  506. async regenerate(record, hosts) {
  507. await this.createCertificate(hosts);
  508. const hash = await this.getLatestHash();
  509. record.update({ hosts, hash });
  510. }
  511. async init() {
  512. await ensureDirExist(this.savePath);
  513. await this.config.init();
  514. const mkcertBinary = await this.getMkcertBinary();
  515. if (!mkcertBinary) {
  516. await this.initMkcert();
  517. } else if (this.autoUpgrade) {
  518. await this.upgradeMkcert();
  519. }
  520. }
  521. async getSourceInfo() {
  522. const sourceInfo = await this.source.getSourceInfo();
  523. if (!sourceInfo) {
  524. const message = typeof this.sourceType === "string" ? `Unsupported platform. Unable to find a binary file for ${process2.platform} platform with ${process2.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';
  525. throw new Error(message);
  526. }
  527. return sourceInfo;
  528. }
  529. async initMkcert() {
  530. const sourceInfo = await this.getSourceInfo();
  531. debug("The mkcert does not exist, download it now");
  532. await this.downloadMkcert(sourceInfo.downloadUrl, this.savedMkcert);
  533. }
  534. async upgradeMkcert() {
  535. const versionManger = new version_default({ config: this.config });
  536. const sourceInfo = await this.getSourceInfo();
  537. if (!sourceInfo) {
  538. this.logger.error(
  539. "Can not obtain download information of mkcert, update skipped"
  540. );
  541. return;
  542. }
  543. const versionInfo = versionManger.compare(sourceInfo.version);
  544. if (!versionInfo.shouldUpdate) {
  545. debug("Mkcert is kept latest version, update skipped");
  546. return;
  547. }
  548. if (versionInfo.breakingChange) {
  549. debug(
  550. "The current version of mkcert is %s, and the latest version is %s, there may be some breaking changes, update skipped",
  551. versionInfo.currentVersion,
  552. versionInfo.nextVersion
  553. );
  554. return;
  555. }
  556. debug(
  557. "The current version of mkcert is %s, and the latest version is %s, mkcert will be updated",
  558. versionInfo.currentVersion,
  559. versionInfo.nextVersion
  560. );
  561. await this.downloadMkcert(sourceInfo.downloadUrl, this.savedMkcert);
  562. versionManger.update(versionInfo.nextVersion);
  563. }
  564. async downloadMkcert(sourceUrl, distPath) {
  565. const downloader = downloader_default.create();
  566. await downloader.download(sourceUrl, distPath);
  567. }
  568. async renew(hosts) {
  569. const record = new record_default({ config: this.config });
  570. if (this.force) {
  571. debug("Certificate is forced to regenerate");
  572. await this.regenerate(record, hosts);
  573. }
  574. if (!record.contains(hosts)) {
  575. debug(
  576. `The hosts changed from [${record.getHosts()}] to [${hosts}], start regenerate certificate`
  577. );
  578. await this.regenerate(record, hosts);
  579. return;
  580. }
  581. const hash = await this.getLatestHash();
  582. if (!record.equal(hash)) {
  583. debug(
  584. `The hash changed from ${prettyLog(record.getHash())} to ${prettyLog(
  585. hash
  586. )}, start regenerate certificate`
  587. );
  588. await this.regenerate(record, hosts);
  589. return;
  590. }
  591. debug("Neither hosts nor hash has changed, skip regenerate certificate");
  592. }
  593. /**
  594. * Get certificates
  595. *
  596. * @param hosts host collection
  597. * @returns cretificates
  598. */
  599. async install(hosts) {
  600. if (hosts.length) {
  601. await this.renew(hosts);
  602. }
  603. return await this.getCertificate();
  604. }
  605. };
  606. var mkcert_default = Mkcert;
  607. // plugin/index.ts
  608. var plugin = (options = {}) => {
  609. return {
  610. name: PLUGIN_NAME,
  611. apply: "serve",
  612. config: async ({ server = {}, logLevel }) => {
  613. if (typeof server.https === "boolean" && server.https === false) {
  614. return;
  615. }
  616. const { hosts = [], ...mkcertOptions } = options;
  617. const logger = createLogger(logLevel, {
  618. prefix: PLUGIN_NAME
  619. });
  620. const mkcert = mkcert_default.create({
  621. logger,
  622. ...mkcertOptions
  623. });
  624. await mkcert.init();
  625. const allHosts = [...getDefaultHosts(), ...hosts];
  626. if (typeof server.host === "string") {
  627. allHosts.push(server.host);
  628. }
  629. const uniqueHosts = Array.from(new Set(allHosts)).filter(Boolean);
  630. const certificate = await mkcert.install(uniqueHosts);
  631. const httpsConfig = {
  632. key: certificate.key && Buffer.from(certificate.key),
  633. cert: certificate.cert && Buffer.from(certificate.cert)
  634. };
  635. return {
  636. server: {
  637. https: httpsConfig
  638. },
  639. preview: {
  640. https: httpsConfig
  641. }
  642. };
  643. }
  644. };
  645. };
  646. var index_default = plugin;
  647. export {
  648. BaseSource,
  649. index_default as default
  650. };
  651. //# sourceMappingURL=mkcert.mjs.map