mirror of
https://github.com/certd/certd.git
synced 2026-04-24 04:17:25 +08:00
refactor: plugins
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"extends": [
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:prettier/recommended",
|
||||
"prettier"
|
||||
],
|
||||
"env": {
|
||||
"mocha": true
|
||||
},
|
||||
"rules": {
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
"@typescript-eslint/ban-ts-ignore": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
// "no-unused-expressions": "off",
|
||||
"max-len": [0, 160, 2, { "ignoreUrls": true }]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
test/user.secret.ts
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"extension": ["ts"],
|
||||
"spec": "test/**/*.test.ts",
|
||||
"require": "ts-node/register"
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"printWidth": 160
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
# Vue 3 + TypeScript + Vite
|
||||
|
||||
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar)
|
||||
|
||||
## Type Support For `.vue` Imports in TS
|
||||
|
||||
Since TypeScript cannot handle type information for `.vue` imports, they are shimmed to be a generic Vue component type by default. In most cases this is fine if you don't really care about component prop types outside of templates. However, if you wish to get actual prop types in `.vue` imports (for example to get props validation when using manual `h(...)` calls), you can enable Volar's Take Over mode by following these steps:
|
||||
|
||||
1. Run `Extensions: Show Built-in Extensions` from VS Code's command palette, look for `TypeScript and JavaScript Language Features`, then right click and select `Disable (Workspace)`. By default, Take Over mode will enable itself if the default TypeScript extension is disabled.
|
||||
2. Reload the VS Code window by running `Developer: Reload Window` from the command palette.
|
||||
|
||||
You can learn more about Take Over mode [here](https://github.com/johnsoncodehk/volar/discussions/471).
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./src";
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "@certd/plugin-cert",
|
||||
"private": true,
|
||||
"version": "0.3.0",
|
||||
"main": "./src/index.ts",
|
||||
"module": "./dist/plugin-aliyun.mjs",
|
||||
"types": "./dist/es/plugin-aliyun.d.ts",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@certd/acme-client": "^0.3.0",
|
||||
"node-forge": "^0.10.0",
|
||||
"@certd/pipeline": "^0.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"dayjs": "^1.11.6",
|
||||
"lodash": "^4.17.21",
|
||||
"@types/lodash": "^4.14.186",
|
||||
"vue-tsc": "^0.38.9",
|
||||
"@alicloud/cs20151215": "^3.0.3",
|
||||
"@alicloud/openapi-client": "^0.4.0",
|
||||
"@alicloud/pop-core": "^1.7.10",
|
||||
"@midwayjs/core": "^3.0.0",
|
||||
"@midwayjs/decorator": "^3.0.0",
|
||||
"@types/chai": "^4.3.3",
|
||||
"@types/mocha": "^10.0.0",
|
||||
"@types/node-forge": "^1.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.38.1",
|
||||
"@typescript-eslint/parser": "^5.38.1",
|
||||
"chai": "^4.3.6",
|
||||
"eslint": "^8.24.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"log4js": "^6.3.0",
|
||||
"mocha": "^10.1.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.8.4",
|
||||
"vite": "^3.1.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./plugin";
|
||||
@@ -0,0 +1,196 @@
|
||||
// @ts-ignore
|
||||
import * as acme from "@certd/acme-client";
|
||||
import _ from "lodash";
|
||||
import { AbstractDnsProvider, IDnsProvider } from "@certd/pipeline";
|
||||
import { Challenge } from "@certd/acme-client/types/rfc8555";
|
||||
import { Logger } from "log4js";
|
||||
import { IContext } from "@certd/pipeline/src/core/context";
|
||||
|
||||
export class AcmeService {
|
||||
userContext: IContext;
|
||||
logger: Logger;
|
||||
constructor(options: { userContext: IContext; logger: Logger }) {
|
||||
this.userContext = options.userContext;
|
||||
this.logger = options.logger;
|
||||
acme.setLogger((text: string) => {
|
||||
this.logger.info(text);
|
||||
});
|
||||
}
|
||||
|
||||
async getAccountConfig(email: string) {
|
||||
return (await this.userContext.get(this.buildAccountKey(email))) || {};
|
||||
}
|
||||
|
||||
buildAccountKey(email: string) {
|
||||
return "acme.config." + email;
|
||||
}
|
||||
|
||||
async saveAccountConfig(email: string, conf: any) {
|
||||
await this.userContext.set(this.buildAccountKey(email), conf);
|
||||
}
|
||||
|
||||
async getAcmeClient(email: string, isTest = false): Promise<acme.Client> {
|
||||
const conf = await this.getAccountConfig(email);
|
||||
if (conf.key == null) {
|
||||
conf.key = await this.createNewKey();
|
||||
await this.saveAccountConfig(email, conf);
|
||||
}
|
||||
const client = new acme.Client({
|
||||
directoryUrl: isTest ? acme.directory.letsencrypt.staging : acme.directory.letsencrypt.production,
|
||||
accountKey: conf.key,
|
||||
accountUrl: conf.accountUrl,
|
||||
backoffAttempts: 20,
|
||||
backoffMin: 5000,
|
||||
backoffMax: 10000,
|
||||
});
|
||||
|
||||
if (conf.accountUrl == null) {
|
||||
const accountPayload = {
|
||||
termsOfServiceAgreed: true,
|
||||
contact: [`mailto:${email}`],
|
||||
};
|
||||
await client.createAccount(accountPayload);
|
||||
conf.accountUrl = client.getAccountUrl();
|
||||
await this.saveAccountConfig(email, conf);
|
||||
}
|
||||
return client;
|
||||
}
|
||||
|
||||
async createNewKey() {
|
||||
const key = await acme.forge.createPrivateKey();
|
||||
return key.toString();
|
||||
}
|
||||
|
||||
async challengeCreateFn(authz: any, challenge: any, keyAuthorization: string, dnsProvider: IDnsProvider) {
|
||||
this.logger.info("Triggered challengeCreateFn()");
|
||||
|
||||
/* http-01 */
|
||||
if (challenge.type === "http-01") {
|
||||
const filePath = `/var/www/html/.well-known/acme-challenge/${challenge.token}`;
|
||||
const fileContents = keyAuthorization;
|
||||
|
||||
this.logger.info(`Creating challenge response for ${authz.identifier.value} at path: ${filePath}`);
|
||||
|
||||
/* Replace this */
|
||||
this.logger.info(`Would write "${fileContents}" to path "${filePath}"`);
|
||||
// await fs.writeFileAsync(filePath, fileContents);
|
||||
} else if (challenge.type === "dns-01") {
|
||||
/* dns-01 */
|
||||
const dnsRecord = `_acme-challenge.${authz.identifier.value}`;
|
||||
const recordValue = keyAuthorization;
|
||||
|
||||
this.logger.info(`Creating TXT record for ${authz.identifier.value}: ${dnsRecord}`);
|
||||
|
||||
/* Replace this */
|
||||
this.logger.info(`Would create TXT record "${dnsRecord}" with value "${recordValue}"`);
|
||||
|
||||
return await dnsProvider.createRecord({
|
||||
fullRecord: dnsRecord,
|
||||
type: "TXT",
|
||||
value: recordValue,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function used to remove an ACME challenge response
|
||||
*
|
||||
* @param {object} authz Authorization object
|
||||
* @param {object} challenge Selected challenge
|
||||
* @param {string} keyAuthorization Authorization key
|
||||
* @param recordItem challengeCreateFn create record item
|
||||
* @param dnsProvider dnsProvider
|
||||
* @returns {Promise}
|
||||
*/
|
||||
|
||||
async challengeRemoveFn(authz: any, challenge: any, keyAuthorization: string, recordItem: any, dnsProvider: IDnsProvider) {
|
||||
this.logger.info("Triggered challengeRemoveFn()");
|
||||
|
||||
/* http-01 */
|
||||
if (challenge.type === "http-01") {
|
||||
const filePath = `/var/www/html/.well-known/acme-challenge/${challenge.token}`;
|
||||
|
||||
this.logger.info(`Removing challenge response for ${authz.identifier.value} at path: ${filePath}`);
|
||||
|
||||
/* Replace this */
|
||||
this.logger.info(`Would remove file on path "${filePath}"`);
|
||||
// await fs.unlinkAsync(filePath);
|
||||
} else if (challenge.type === "dns-01") {
|
||||
const dnsRecord = `_acme-challenge.${authz.identifier.value}`;
|
||||
const recordValue = keyAuthorization;
|
||||
|
||||
this.logger.info(`Removing TXT record for ${authz.identifier.value}: ${dnsRecord}`);
|
||||
|
||||
/* Replace this */
|
||||
this.logger.info(`Would remove TXT record "${dnsRecord}" with value "${recordValue}"`);
|
||||
await dnsProvider.removeRecord({
|
||||
fullRecord: dnsRecord,
|
||||
type: "TXT",
|
||||
value: keyAuthorization,
|
||||
record: recordItem,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async order(options: { email: string; domains: string | string[]; dnsProvider: AbstractDnsProvider; csrInfo: any; isTest?: boolean }) {
|
||||
const { email, isTest, domains, csrInfo, dnsProvider } = options;
|
||||
const client: acme.Client = await this.getAcmeClient(email, isTest);
|
||||
|
||||
/* Create CSR */
|
||||
const { commonName, altNames } = this.buildCommonNameByDomains(domains);
|
||||
|
||||
const [key, csr] = await acme.forge.createCsr({
|
||||
commonName,
|
||||
...csrInfo,
|
||||
altNames,
|
||||
});
|
||||
if (dnsProvider == null) {
|
||||
throw new Error("dnsProvider 不能为空");
|
||||
}
|
||||
/* 自动申请证书 */
|
||||
const crt = await client.auto({
|
||||
csr,
|
||||
email: email,
|
||||
termsOfServiceAgreed: true,
|
||||
challengePriority: ["dns-01"],
|
||||
challengeCreateFn: async (authz: acme.Authorization, challenge: Challenge, keyAuthorization: string): Promise<any> => {
|
||||
return await this.challengeCreateFn(authz, challenge, keyAuthorization, dnsProvider);
|
||||
},
|
||||
challengeRemoveFn: async (authz: acme.Authorization, challenge: Challenge, keyAuthorization: string, recordItem: any): Promise<any> => {
|
||||
return await this.challengeRemoveFn(authz, challenge, keyAuthorization, recordItem, dnsProvider);
|
||||
},
|
||||
});
|
||||
|
||||
const cert = {
|
||||
crt: crt.toString(),
|
||||
key: key.toString(),
|
||||
csr: csr.toString(),
|
||||
};
|
||||
/* Done */
|
||||
this.logger.debug(`CSR:\n${cert.csr}`);
|
||||
this.logger.debug(`Certificate:\n${cert.crt}`);
|
||||
this.logger.info("证书申请成功");
|
||||
return cert;
|
||||
}
|
||||
|
||||
buildCommonNameByDomains(domains: string | string[]): {
|
||||
commonName: string;
|
||||
altNames: string[] | undefined;
|
||||
} {
|
||||
if (typeof domains === "string") {
|
||||
domains = domains.split(",");
|
||||
}
|
||||
if (domains.length === 0) {
|
||||
throw new Error("domain can not be empty");
|
||||
}
|
||||
const commonName = domains[0];
|
||||
let altNames: undefined | string[] = undefined;
|
||||
if (domains.length > 1) {
|
||||
altNames = _.slice(domains, 1);
|
||||
}
|
||||
return {
|
||||
commonName,
|
||||
altNames,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,237 @@
|
||||
import { AbstractDnsProvider, AbstractPlugin, dnsProviderRegistry, IsTask, RunStrategy, TaskInput, TaskOutput, TaskPlugin } from "@certd/pipeline";
|
||||
import forge from "node-forge";
|
||||
import dayjs from "dayjs";
|
||||
import { AcmeService } from "./acme";
|
||||
import _ from "lodash";
|
||||
|
||||
export type CertInfo = {
|
||||
crt: string;
|
||||
key: string;
|
||||
csr: string;
|
||||
};
|
||||
@IsTask(() => {
|
||||
return {
|
||||
name: "CertApply",
|
||||
title: "证书申请",
|
||||
desc: "免费通配符域名证书申请,支持多个域名打到同一个证书上",
|
||||
input: {
|
||||
domains: {
|
||||
title: "域名",
|
||||
component: {
|
||||
name: "a-select",
|
||||
vModel: "value",
|
||||
mode: "tags",
|
||||
open: false,
|
||||
},
|
||||
required: true,
|
||||
col: {
|
||||
span: 24,
|
||||
},
|
||||
helper:
|
||||
"支持通配符域名,例如: *.foo.com 、 *.test.handsfree.work\n" +
|
||||
"支持多个域名、多个子域名、多个通配符域名打到一个证书上(域名必须是在同一个DNS提供商解析)\n" +
|
||||
"多级子域名要分成多个域名输入(*.foo.com的证书不能用于xxx.yyy.foo.com)\n" +
|
||||
"输入一个回车之后,再输入下一个",
|
||||
},
|
||||
email: {
|
||||
title: "邮箱",
|
||||
component: {
|
||||
name: "a-input",
|
||||
vModel: "value",
|
||||
},
|
||||
required: true,
|
||||
helper: "请输入邮箱",
|
||||
},
|
||||
dnsProviderType: {
|
||||
title: "DNS提供商",
|
||||
component: {
|
||||
name: "pi-dns-provider-selector",
|
||||
},
|
||||
required: true,
|
||||
helper: "请选择dns解析提供商",
|
||||
},
|
||||
dnsProviderAccess: {
|
||||
title: "DNS解析授权",
|
||||
component: {
|
||||
name: "pi-access-selector",
|
||||
},
|
||||
required: true,
|
||||
helper: "请选择dns解析提供商授权",
|
||||
},
|
||||
renewDays: {
|
||||
title: "更新天数",
|
||||
component: {
|
||||
name: "a-input-number",
|
||||
vModel: "value",
|
||||
},
|
||||
required: true,
|
||||
helper: "到期前多少天后更新证书",
|
||||
},
|
||||
forceUpdate: {
|
||||
title: "强制更新",
|
||||
component: {
|
||||
name: "a-switch",
|
||||
vModel: "checked",
|
||||
},
|
||||
helper: "是否强制重新申请证书",
|
||||
},
|
||||
},
|
||||
default: {
|
||||
input: {
|
||||
renewDays: 20,
|
||||
forceUpdate: false,
|
||||
},
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.AlwaysRun,
|
||||
},
|
||||
},
|
||||
output: {
|
||||
cert: {
|
||||
key: "cert",
|
||||
type: "CertInfo",
|
||||
title: "域名证书",
|
||||
},
|
||||
},
|
||||
};
|
||||
})
|
||||
export class CertApplyPlugin extends AbstractPlugin implements TaskPlugin {
|
||||
// @ts-ignore
|
||||
acme: AcmeService;
|
||||
protected async onInit() {
|
||||
this.acme = new AcmeService({ userContext: this.userContext, logger: this.logger });
|
||||
}
|
||||
|
||||
async execute(input: TaskInput): Promise<TaskOutput> {
|
||||
const oldCert = await this.condition(input);
|
||||
if (oldCert != null) {
|
||||
return {
|
||||
cert: oldCert,
|
||||
};
|
||||
}
|
||||
const cert = await this.doCertApply(input);
|
||||
return { cert };
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否更新证书
|
||||
* @param input
|
||||
*/
|
||||
async condition(input: TaskInput) {
|
||||
if (input.forceUpdate) {
|
||||
return null;
|
||||
}
|
||||
let oldCert;
|
||||
try {
|
||||
oldCert = await this.readCurrentCert();
|
||||
} catch (e) {
|
||||
this.logger.warn("读取cert失败:", e);
|
||||
}
|
||||
if (oldCert == null) {
|
||||
this.logger.info("还未申请过,准备申请新证书");
|
||||
return null;
|
||||
}
|
||||
|
||||
const ret = this.isWillExpire(oldCert.expires, input.renewDays);
|
||||
if (!ret.isWillExpire) {
|
||||
this.logger.info(`证书还未过期:过期时间${dayjs(oldCert.expires).format("YYYY-MM-DD HH:mm:ss")},剩余${ret.leftDays}天`);
|
||||
return oldCert;
|
||||
}
|
||||
this.logger.info("即将过期,开始更新证书");
|
||||
return null;
|
||||
}
|
||||
|
||||
async doCertApply(input: TaskInput) {
|
||||
const email = input["email"];
|
||||
const domains = input["domains"];
|
||||
const dnsProviderType = input["dnsProviderType"];
|
||||
const dnsProviderAccessId = input["dnsProviderAccess"];
|
||||
const csrInfo = _.merge(
|
||||
{
|
||||
country: "CN",
|
||||
state: "GuangDong",
|
||||
locality: "ShengZhen",
|
||||
organization: "CertD Org.",
|
||||
organizationUnit: "IT Department",
|
||||
emailAddress: email,
|
||||
},
|
||||
input["csrInfo"]
|
||||
);
|
||||
this.logger.info("开始申请证书,", email, domains);
|
||||
|
||||
const dnsProviderClass = dnsProviderRegistry.get(dnsProviderType);
|
||||
const access = await this.accessService.getById(dnsProviderAccessId);
|
||||
// @ts-ignore
|
||||
const dnsProvider: AbstractDnsProvider = new dnsProviderClass();
|
||||
dnsProvider.doInit({ access, logger: this.logger, http: this.http });
|
||||
|
||||
const cert = await this.acme.order({
|
||||
email,
|
||||
domains,
|
||||
dnsProvider,
|
||||
csrInfo,
|
||||
isTest: false,
|
||||
});
|
||||
|
||||
await this.writeCert(cert);
|
||||
const ret = await this.readCurrentCert();
|
||||
|
||||
return {
|
||||
...ret,
|
||||
isNew: true,
|
||||
};
|
||||
}
|
||||
|
||||
formatCert(pem: string) {
|
||||
pem = pem.replace(/\r/g, "");
|
||||
pem = pem.replace(/\n\n/g, "\n");
|
||||
pem = pem.replace(/\n$/g, "");
|
||||
return pem;
|
||||
}
|
||||
|
||||
async writeCert(cert: { crt: string; key: string; csr: string }) {
|
||||
const newCert = {
|
||||
crt: this.formatCert(cert.crt),
|
||||
key: this.formatCert(cert.key),
|
||||
csr: this.formatCert(cert.csr),
|
||||
};
|
||||
await this.pipelineContext.set("cert", newCert);
|
||||
}
|
||||
|
||||
async readCurrentCert() {
|
||||
const cert: CertInfo = await this.pipelineContext.get("cert");
|
||||
if (cert == null) {
|
||||
return undefined;
|
||||
}
|
||||
const { detail, expires } = this.getCrtDetail(cert.crt);
|
||||
return {
|
||||
...cert,
|
||||
detail,
|
||||
expires: expires.getTime(),
|
||||
};
|
||||
}
|
||||
|
||||
getCrtDetail(crt: string) {
|
||||
const pki = forge.pki;
|
||||
const detail = pki.certificateFromPem(crt.toString());
|
||||
const expires = detail.validity.notAfter;
|
||||
return { detail, expires };
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否过期,默认提前20天
|
||||
* @param expires
|
||||
* @param maxDays
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isWillExpire(expires: number, maxDays = 20) {
|
||||
if (expires == null) {
|
||||
throw new Error("过期时间不能为空");
|
||||
}
|
||||
// 检查有效期
|
||||
const leftDays = dayjs(expires).diff(dayjs(), "day");
|
||||
return {
|
||||
isWillExpire: leftDays < maxDays,
|
||||
leftDays,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./cert-plugin";
|
||||
@@ -0,0 +1,17 @@
|
||||
import { FileStorage } from "@certd/pipeline/src/core/storage";
|
||||
import { ContextFactory } from "@certd/pipeline/src/core/context";
|
||||
import { AccessServiceTest } from "@certd/pipeline/test/pipeline/access-service-test";
|
||||
import { logger } from "@certd/pipeline";
|
||||
import { request } from "@certd/pipeline/src/utils/util.request";
|
||||
|
||||
const contextFactory = new ContextFactory(new FileStorage());
|
||||
|
||||
const userContext = contextFactory.getContext("user", "test");
|
||||
const pipelineContext = contextFactory.getContext("pipeline", "test");
|
||||
export const pluginInitProps = {
|
||||
accessService: new AccessServiceTest(),
|
||||
pipelineContext: pipelineContext,
|
||||
userContext: userContext,
|
||||
logger: logger,
|
||||
http: request,
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
import { expect } from "chai";
|
||||
import "mocha";
|
||||
import { pluginInitProps } from "../init.test";
|
||||
import { CertApplyPlugin } from "../../../src";
|
||||
describe("CertApply", function () {
|
||||
it("#execute", async function () {
|
||||
this.timeout(120000);
|
||||
const plugin = new CertApplyPlugin();
|
||||
// @ts-ignore
|
||||
delete plugin.define;
|
||||
await plugin.doInit(pluginInitProps);
|
||||
const output = await plugin.execute({
|
||||
domains: ["*.docmirror.cn", "docmirror.cn"],
|
||||
email: "xiaojunnuo@qq.com",
|
||||
dnsProviderType: "aliyun",
|
||||
accessId: "111",
|
||||
forceUpdate: true,
|
||||
});
|
||||
const cert = output.cert;
|
||||
expect(plugin.getDefine().name).eq("CertApply");
|
||||
expect(cert.crt != null).eq(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "Node",
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"sourceMap": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"skipLibCheck": true,
|
||||
"experimentalDecorators": true
|
||||
},
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue","test/**/*.ts"],
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { defineConfig } from "vite";
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [],
|
||||
build: {
|
||||
lib: {
|
||||
entry: "src/index.ts",
|
||||
name: "pipeline",
|
||||
},
|
||||
rollupOptions: {
|
||||
external: ["vue", "lodash-es", "dayjs", "@fast-crud/fast-crud"],
|
||||
output: {
|
||||
// Provide global variables to use in the UMD build
|
||||
// for externalized deps
|
||||
globals: {
|
||||
vue: "Vue",
|
||||
"lodash-es": "_",
|
||||
dayjs: "dayjs",
|
||||
"@fast-crud/fast-crud": "FastCrud",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user