mirror of
https://github.com/certd/certd.git
synced 2026-06-25 03:57:30 +08:00
feat: 通过插件配置懒加载依赖,动态加载第三方依赖包,精简安装镜像大小
This commit is contained in:
@@ -16,8 +16,11 @@ import { tmpdir } from "node:os";
|
||||
import { DefaultUploadFileMimeType, uploadWhiteList } from "@midwayjs/upload";
|
||||
import path from "path";
|
||||
import { logger } from "@certd/basic";
|
||||
import { createRequire } from "module";
|
||||
|
||||
const env = process.env.NODE_ENV || "development";
|
||||
const require = createRequire(import.meta.url);
|
||||
const pkg = require("../../package.json");
|
||||
|
||||
const development = {
|
||||
midwayLogger: {
|
||||
@@ -103,6 +106,21 @@ const development = {
|
||||
certd: {
|
||||
fileRootDir: "./data/files",
|
||||
},
|
||||
runtimeDeps: {
|
||||
enabled: true,
|
||||
rootDir: "./data/.runtime-deps",
|
||||
autoInstall: true,
|
||||
pnpmCommand: "",
|
||||
installTimeoutMs: 120000,
|
||||
lazyDependencies: pkg.lazyDependencies || {},
|
||||
registry: {
|
||||
mode: "auto",
|
||||
fixedUrl: "",
|
||||
candidates: ["https://registry.npmmirror.com", "https://registry.npmjs.org"],
|
||||
probeTimeoutMs: 3000,
|
||||
cacheTtlMs: 6 * 60 * 60 * 1000,
|
||||
},
|
||||
},
|
||||
system: {
|
||||
resetAdminPasswd: false,
|
||||
},
|
||||
|
||||
@@ -8,6 +8,7 @@ import { TaskServiceBuilder } from "../../../modules/pipeline/service/getter/tas
|
||||
import { cloneDeep } from "lodash-es";
|
||||
import { ApiTags } from "@midwayjs/swagger";
|
||||
import { AuthService } from "../../../modules/sys/authority/service/auth-service.js";
|
||||
import { RuntimeDepsService } from "../../../modules/runtime-deps/runtime-deps-service.js";
|
||||
|
||||
@Provide()
|
||||
@Controller("/api/pi/handle")
|
||||
@@ -28,6 +29,9 @@ export class HandleController extends BaseController {
|
||||
@Inject()
|
||||
notificationService: NotificationService;
|
||||
|
||||
@Inject()
|
||||
runtimeDepsService: RuntimeDepsService;
|
||||
|
||||
@Post("/access", { description: Constants.per.authOnly, summary: "处理授权请求" })
|
||||
async accessRequest(@Body(ALL) body: AccessRequestHandleReq) {
|
||||
let { projectId, userId } = await this.getProjectUserIdRead();
|
||||
@@ -59,8 +63,16 @@ export class HandleController extends BaseController {
|
||||
inputAccess = this.accessService.decryptAccessEntity(param);
|
||||
}
|
||||
}
|
||||
const accessGetter = new AccessGetter(userId, projectId, this.accessService.getById.bind(this.accessService));
|
||||
const access = await newAccess(body.typeName, inputAccess, accessGetter);
|
||||
const getAccessById = this.accessService.getById.bind(this.accessService);
|
||||
const accessGetter = new AccessGetter(userId, projectId, getAccessById, this.runtimeDepsService);
|
||||
const accessContext = {
|
||||
http,
|
||||
logger,
|
||||
utils,
|
||||
accessService: accessGetter,
|
||||
define: undefined,
|
||||
} as any;
|
||||
const access = await newAccess(body.typeName, inputAccess, accessGetter, accessContext);
|
||||
|
||||
// mergeUtils.merge(access, body.input);
|
||||
const res = await access.onRequest(body);
|
||||
@@ -70,14 +82,17 @@ export class HandleController extends BaseController {
|
||||
|
||||
@Post("/notification", { description: Constants.per.authOnly, summary: "处理通知请求" })
|
||||
async notificationRequest(@Body(ALL) body: NotificationRequestHandleReq) {
|
||||
const { projectId, userId } = await this.getProjectUserIdRead();
|
||||
const input = body.input;
|
||||
const serviceGetter = this.taskServiceBuilder.create({ userId, projectId });
|
||||
|
||||
const notification = await newNotification(body.typeName, input, {
|
||||
http,
|
||||
logger,
|
||||
utils,
|
||||
emailService: this.emailService,
|
||||
});
|
||||
serviceGetter,
|
||||
} as any);
|
||||
|
||||
const res = await notification.onRequest(body);
|
||||
|
||||
@@ -138,8 +153,8 @@ export class HandleController extends BaseController {
|
||||
// signal: this.abort.signal,
|
||||
utils,
|
||||
serviceGetter: taskServiceGetter,
|
||||
};
|
||||
instance.setCtx(taskCtx);
|
||||
} as any;
|
||||
await instance.setCtx(taskCtx);
|
||||
mergeUtils.merge(plugin, body.input);
|
||||
await instance.onInstance();
|
||||
const res = await plugin.onRequest(body);
|
||||
|
||||
@@ -6,6 +6,7 @@ import { SmsServiceFactory } from "../sms/factory.js";
|
||||
import { CaptchaService } from "./captcha-service.js";
|
||||
import { EmailService } from "./email-service.js";
|
||||
import { CaptchaRequest } from "../../../plugins/plugin-captcha/api.js";
|
||||
import { RuntimeDepsService } from "../../runtime-deps/runtime-deps-service.js";
|
||||
|
||||
// {data: '<svg.../svg>', text: 'abcd'}
|
||||
/**
|
||||
@@ -24,6 +25,9 @@ export class CodeService {
|
||||
@Inject()
|
||||
captchaService: CaptchaService;
|
||||
|
||||
@Inject()
|
||||
runtimeDepsService: RuntimeDepsService;
|
||||
|
||||
async checkCaptcha(body: any, req: CaptchaRequest) {
|
||||
return await this.captchaService.doValidate({ form: body, req });
|
||||
}
|
||||
@@ -53,9 +57,10 @@ export class CodeService {
|
||||
const smsConfig = sysSettings.sms.config;
|
||||
const sender: ISmsService = await SmsServiceFactory.createSmsService(smsType);
|
||||
const accessGetter = new AccessSysGetter(this.accessService);
|
||||
sender.setCtx({
|
||||
await sender.setCtx({
|
||||
accessService: accessGetter,
|
||||
config: smsConfig,
|
||||
runtimeDepsService: this.runtimeDepsService,
|
||||
});
|
||||
const smsCode = randomNumber(verificationCodeLength);
|
||||
await sender.sendSmsCode({
|
||||
|
||||
@@ -44,7 +44,7 @@ export class AliyunSmsService implements ISmsService {
|
||||
|
||||
ctx: SmsPluginCtx<AliyunSmsConfig>;
|
||||
|
||||
setCtx(ctx: any) {
|
||||
async setCtx(ctx: any) {
|
||||
this.ctx = ctx;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { FormItemProps, IAccessService } from "@certd/pipeline";
|
||||
import type { RuntimeDepsService } from "../../runtime-deps/runtime-deps-service.js";
|
||||
|
||||
export interface ISmsService {
|
||||
sendSmsCode(opts: { mobile: string; code: string; phoneCode: string }): Promise<void>;
|
||||
setCtx(ctx: { accessService: IAccessService; config: { [key: string]: any } }): void;
|
||||
setCtx(ctx: { accessService: IAccessService; config: { [key: string]: any }; runtimeDepsService?: RuntimeDepsService }): Promise<void>;
|
||||
}
|
||||
|
||||
export type PluginInputs<T = any> = {
|
||||
@@ -12,4 +13,5 @@ export type PluginInputs<T = any> = {
|
||||
export type SmsPluginCtx<T = any> = {
|
||||
accessService: IAccessService;
|
||||
config: T;
|
||||
runtimeDepsService?: RuntimeDepsService;
|
||||
};
|
||||
|
||||
@@ -68,12 +68,20 @@ export class TencentSmsService implements ISmsService {
|
||||
|
||||
ctx: SmsPluginCtx<TencentSmsConfig>;
|
||||
|
||||
setCtx(ctx: any) {
|
||||
async setCtx(ctx: any) {
|
||||
this.ctx = ctx;
|
||||
if (this.ctx.runtimeDepsService) {
|
||||
await this.ctx.runtimeDepsService.ensureDependencies({
|
||||
"tencentcloud-sdk-nodejs": "^4.1.112",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async getClient() {
|
||||
const sdk = await import("tencentcloud-sdk-nodejs/tencentcloud/services/sms/v20210111/index.js");
|
||||
if (!this.ctx.runtimeDepsService) {
|
||||
throw new Error("动态依赖服务未初始化,无法加载腾讯云短信SDK");
|
||||
}
|
||||
const sdk = await this.ctx.runtimeDepsService.importRuntime("tencentcloud-sdk-nodejs/tencentcloud/services/sms/v20210111/index.js");
|
||||
const client = sdk.v20210111.Client;
|
||||
const access = await this.ctx.accessService.getById<TencentAccess>(this.ctx.config.accessId);
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ export class YfySmsService implements ISmsService {
|
||||
|
||||
ctx: SmsPluginCtx<YfySmsConfig>;
|
||||
|
||||
setCtx(ctx: any) {
|
||||
async setCtx(ctx: SmsPluginCtx<YfySmsConfig>) {
|
||||
this.ctx = ctx;
|
||||
}
|
||||
|
||||
|
||||
@@ -87,7 +87,7 @@ export class CommonDnsProvider implements IDnsProvider {
|
||||
return res;
|
||||
}
|
||||
|
||||
setCtx(ctx: DnsProviderContext): void {
|
||||
async setCtx(ctx: DnsProviderContext): Promise<void> {
|
||||
this.ctx = ctx;
|
||||
}
|
||||
}
|
||||
|
||||
+10
-1
@@ -13,6 +13,7 @@ import { CertInfoGetter } from "./cert-info-getter.js";
|
||||
import { CertInfoService } from "../../../monitor/index.js";
|
||||
import { ICertInfoGetter } from "@certd/plugin-lib";
|
||||
import { CnameProviderService } from "../../../cname/service/cname-provider-service.js";
|
||||
import { RuntimeDepsService } from "../../../runtime-deps/runtime-deps-service.js";
|
||||
|
||||
const serviceNames = ["ocrService"];
|
||||
export class TaskServiceGetter implements IServiceGetter {
|
||||
@@ -38,6 +39,8 @@ export class TaskServiceGetter implements IServiceGetter {
|
||||
return (await this.getDomainVerifierGetter()) as T;
|
||||
} else if (serviceName === "certInfoGetter") {
|
||||
return (await this.getCertInfoGetter()) as T;
|
||||
} else if (serviceName === "runtimeDepsService") {
|
||||
return (await this.getRuntimeDepsService()) as T;
|
||||
} else {
|
||||
if (!serviceNames.includes(serviceName)) {
|
||||
throw new Error(`${serviceName} not in whitelist`);
|
||||
@@ -63,7 +66,9 @@ export class TaskServiceGetter implements IServiceGetter {
|
||||
|
||||
async getAccessService(): Promise<AccessGetter> {
|
||||
const accessService: AccessService = await this.appCtx.getAsync("accessService");
|
||||
return new AccessGetter(this.userId, this.projectId, accessService.getById.bind(accessService));
|
||||
const runtimeDepsService = await this.getRuntimeDepsService();
|
||||
const getAccessById = accessService.getById.bind(accessService);
|
||||
return new AccessGetter(this.userId, this.projectId, getAccessById, runtimeDepsService);
|
||||
}
|
||||
|
||||
async getCnameProxyService(): Promise<CnameProxyService> {
|
||||
@@ -80,6 +85,10 @@ export class TaskServiceGetter implements IServiceGetter {
|
||||
const domainService: DomainService = await this.appCtx.getAsync("domainService");
|
||||
return new DomainVerifierGetter(this.userId, this.projectId, domainService);
|
||||
}
|
||||
|
||||
async getRuntimeDepsService(): Promise<RuntimeDepsService> {
|
||||
return await this.appCtx.getAsync("runtimeDepsService");
|
||||
}
|
||||
}
|
||||
@Provide()
|
||||
@Scope(ScopeEnum.Request, { allowDowngrade: true })
|
||||
|
||||
@@ -7,6 +7,7 @@ import { NotificationInstanceConfig, notificationRegistry, NotificationSendReq,
|
||||
import { http, utils } from "@certd/basic";
|
||||
import { EmailService } from "../../basic/service/email-service.js";
|
||||
import { isComm, isPlus } from "@certd/plus-core";
|
||||
import { TaskServiceBuilder } from "./getter/task-service-getter.js";
|
||||
|
||||
@Provide()
|
||||
@Scope(ScopeEnum.Request, { allowDowngrade: true })
|
||||
@@ -20,6 +21,9 @@ export class NotificationService extends BaseService<NotificationEntity> {
|
||||
@Inject()
|
||||
sysSettingsService: SysSettingsService;
|
||||
|
||||
@Inject()
|
||||
taskServiceBuilder: TaskServiceBuilder;
|
||||
|
||||
//@ts-ignore
|
||||
getRepository() {
|
||||
return this.repository;
|
||||
@@ -199,6 +203,7 @@ export class NotificationService extends BaseService<NotificationEntity> {
|
||||
logger: logger,
|
||||
utils: utils,
|
||||
emailService: this.emailService,
|
||||
serviceGetter: this.taskServiceBuilder.create({ userId, projectId }),
|
||||
},
|
||||
body: req.body,
|
||||
});
|
||||
|
||||
@@ -136,10 +136,10 @@ return class DemoTask extends AbstractTaskPlugin {
|
||||
export function getDefaultDnsPlugin() {
|
||||
const metadata = `
|
||||
accessType: aliyun # 授权类型名称
|
||||
#dependPlugins: # 依赖第三方库,安装插件时会安装依赖库,尽量使用certd已安装的库,比如http、lodash-es、utils
|
||||
#dependPackages: # 依赖第三方 npm 包,运行插件时会按需安装,尽量使用 certd 已安装的库,比如 http、lodash-es、utils
|
||||
# @alicloud/openapi-client: ^0.4.12
|
||||
#dependLibs: # 依赖的插件,应用商店安装时会先安装依赖插件
|
||||
# aliyun: *
|
||||
#dependPlugins: # 依赖的其他插件,使用 type:name 格式避免不同类型插件同名;运行插件时会同时确保被依赖插件的 dependPackages
|
||||
# access:aliyun: *
|
||||
|
||||
`;
|
||||
|
||||
|
||||
@@ -13,13 +13,21 @@ import yaml from "js-yaml";
|
||||
import { getDefaultAccessPlugin, getDefaultDeployPlugin, getDefaultDnsPlugin } from "./default-plugin.js";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { RuntimeDepsService } from "../../runtime-deps/runtime-deps-service.js";
|
||||
|
||||
export type PluginImportReq = {
|
||||
content: string;
|
||||
override?: boolean;
|
||||
};
|
||||
|
||||
async function importer(modulePath: string) {
|
||||
function isBareModuleSpecifier(modulePath: string) {
|
||||
if (modulePath.startsWith(".") || modulePath.startsWith("/") || modulePath.startsWith("file:") || modulePath.startsWith("node:")) {
|
||||
return false;
|
||||
}
|
||||
return !/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(modulePath);
|
||||
}
|
||||
|
||||
async function importLocalModule(modulePath: string) {
|
||||
if (!modulePath) {
|
||||
throw new Error("modules path 不能为空");
|
||||
}
|
||||
@@ -41,6 +49,9 @@ export class PluginService extends BaseService<PluginEntity> {
|
||||
@Inject()
|
||||
builtInPluginService: BuiltInPluginService;
|
||||
|
||||
@Inject()
|
||||
runtimeDepsService: RuntimeDepsService;
|
||||
|
||||
//@ts-ignore
|
||||
getRepository() {
|
||||
return this.repository;
|
||||
@@ -314,6 +325,16 @@ export class PluginService extends BaseService<PluginEntity> {
|
||||
}).outputText;
|
||||
}
|
||||
|
||||
async importer(modulePath: string) {
|
||||
if (!modulePath) {
|
||||
throw new Error("modules path 不能为空");
|
||||
}
|
||||
if (!isBareModuleSpecifier(modulePath)) {
|
||||
return await importLocalModule(modulePath);
|
||||
}
|
||||
return await this.runtimeDepsService.importRuntime(modulePath);
|
||||
}
|
||||
|
||||
private async getPluginClassFromFile(item: any) {
|
||||
const scriptFilePath = item.scriptFilePath;
|
||||
const res = await import(`../../..${scriptFilePath}`);
|
||||
@@ -345,6 +366,7 @@ export class PluginService extends BaseService<PluginEntity> {
|
||||
// const script = await this.compile(plugin.content);
|
||||
const script = plugin.content;
|
||||
const getPluginClass = new AsyncFunction("_ctx", script);
|
||||
const importer = this.importer.bind(this);
|
||||
return await getPluginClass({ logger: logger, import: importer });
|
||||
} catch (e) {
|
||||
logger.error("编译插件失败:", e);
|
||||
@@ -439,6 +461,24 @@ export class PluginService extends BaseService<PluginEntity> {
|
||||
});
|
||||
}
|
||||
|
||||
async getRuntimeDependencyPluginDefines() {
|
||||
const builtInList = await this.getEnabledBuiltInList();
|
||||
const customList = await this.list({
|
||||
buildQuery: bq => {
|
||||
bq.andWhere("type != :type", {
|
||||
type: "builtIn",
|
||||
});
|
||||
},
|
||||
});
|
||||
const list = [...builtInList];
|
||||
for (const plugin of customList) {
|
||||
const metadata = plugin.metadata ? yaml.load(plugin.metadata) : {};
|
||||
const extra = plugin.extra ? yaml.load(plugin.extra) : {};
|
||||
list.push({ ...plugin, ...metadata, ...extra });
|
||||
}
|
||||
return list.filter(item => item.dependPackages);
|
||||
}
|
||||
|
||||
async exportPlugin(id: number) {
|
||||
const info = await this.info(id);
|
||||
if (!info) {
|
||||
@@ -483,6 +523,7 @@ export class PluginService extends BaseService<PluginEntity> {
|
||||
};
|
||||
const extra = {
|
||||
dependPlugins: loaded.dependPlugins,
|
||||
dependPackages: loaded.dependPackages,
|
||||
default: loaded.default,
|
||||
showRunStrategy: loaded.showRunStrategy,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import assert from "assert";
|
||||
import { NpmRegistryResolver } from "./npm-registry-resolver.js";
|
||||
|
||||
describe("NpmRegistryResolver", () => {
|
||||
it("chooses the fastest successful registry in auto mode", async () => {
|
||||
const resolver = new NpmRegistryResolver();
|
||||
resolver.config = {
|
||||
mode: "auto",
|
||||
fixedUrl: "",
|
||||
candidates: ["https://slow.example.com", "https://fast.example.com"],
|
||||
probeTimeoutMs: 100,
|
||||
cacheTtlMs: 1000,
|
||||
};
|
||||
resolver.probe = async registryUrl => {
|
||||
return {
|
||||
registryUrl,
|
||||
ok: true,
|
||||
elapsedMs: registryUrl.includes("fast") ? 10 : 50,
|
||||
};
|
||||
};
|
||||
|
||||
const result = await resolver.resolve();
|
||||
|
||||
assert.equal(result, "https://fast.example.com");
|
||||
});
|
||||
|
||||
it("uses fixed registry without probing", async () => {
|
||||
const resolver = new NpmRegistryResolver();
|
||||
resolver.config = {
|
||||
mode: "fixed",
|
||||
fixedUrl: "https://registry.example.com",
|
||||
candidates: [],
|
||||
probeTimeoutMs: 100,
|
||||
cacheTtlMs: 1000,
|
||||
};
|
||||
|
||||
const result = await resolver.resolve();
|
||||
|
||||
assert.equal(result, "https://registry.example.com");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
import { Config, Provide, Scope, ScopeEnum } from "@midwayjs/core";
|
||||
|
||||
export type NpmRegistryResolverConfig = {
|
||||
mode: "auto" | "fixed" | "system";
|
||||
fixedUrl: string;
|
||||
candidates: string[];
|
||||
probeTimeoutMs: number;
|
||||
cacheTtlMs: number;
|
||||
};
|
||||
|
||||
export type RegistryProbeResult = {
|
||||
registryUrl: string;
|
||||
ok: boolean;
|
||||
elapsedMs: number;
|
||||
};
|
||||
|
||||
@Provide()
|
||||
@Scope(ScopeEnum.Request, { allowDowngrade: true })
|
||||
export class NpmRegistryResolver {
|
||||
@Config("runtimeDeps.registry")
|
||||
config!: NpmRegistryResolverConfig;
|
||||
|
||||
private cache?: { registryUrl: string; expiresAt: number };
|
||||
|
||||
async resolve(): Promise<string> {
|
||||
const config = this.config;
|
||||
if (config?.mode === "fixed" && config.fixedUrl) {
|
||||
return config.fixedUrl;
|
||||
}
|
||||
if (config?.mode === "system") {
|
||||
return "";
|
||||
}
|
||||
|
||||
const cached = this.cache;
|
||||
if (cached && cached.expiresAt > Date.now()) {
|
||||
return cached.registryUrl;
|
||||
}
|
||||
|
||||
const candidates = (config?.candidates || []).filter(Boolean);
|
||||
const probes = await Promise.allSettled(candidates.map(registryUrl => this.probe(registryUrl)));
|
||||
const okList = probes
|
||||
.map(item => (item.status === "fulfilled" ? item.value : null))
|
||||
.filter((item): item is RegistryProbeResult => !!item && item.ok);
|
||||
|
||||
if (okList.length > 0) {
|
||||
okList.sort((a, b) => a.elapsedMs - b.elapsedMs);
|
||||
const best = okList[0].registryUrl;
|
||||
this.cache = {
|
||||
registryUrl: best,
|
||||
expiresAt: Date.now() + (config?.cacheTtlMs || 0),
|
||||
};
|
||||
return best;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
async probe(registryUrl: string): Promise<RegistryProbeResult> {
|
||||
const timeoutMs = this.config?.probeTimeoutMs || 3000;
|
||||
const started = Date.now();
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
||||
try {
|
||||
const res = await fetch(`${registryUrl.replace(/\/$/, "")}/-/ping`, { signal: controller.signal });
|
||||
return {
|
||||
registryUrl,
|
||||
ok: res.ok,
|
||||
elapsedMs: Date.now() - started,
|
||||
};
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
} catch {
|
||||
return {
|
||||
registryUrl,
|
||||
ok: false,
|
||||
elapsedMs: Date.now() - started,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,490 @@
|
||||
import assert from "assert";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import os from "os";
|
||||
import { RuntimeDepsService, type RuntimeDependencyPluginDefine } from "./runtime-deps-service.js";
|
||||
import { accessRegistry, pluginRegistry } from "@certd/pipeline";
|
||||
import { addonRegistry } from "@certd/lib-server";
|
||||
|
||||
describe("RuntimeDepsService", () => {
|
||||
it("detects conflicting dependency ranges across plugins", () => {
|
||||
const service = new RuntimeDepsService();
|
||||
const merged = service.collectDependencies([
|
||||
{ name: "a", dependPackages: { foo: "^1.0.0" } },
|
||||
{ name: "b", dependPackages: { foo: "^1.2.0" } },
|
||||
]);
|
||||
|
||||
assert.deepEqual(merged.dependencies, { foo: "^1.0.0" });
|
||||
assert.equal(merged.conflicts.length, 0);
|
||||
});
|
||||
|
||||
it("reports incompatible dependency ranges", () => {
|
||||
const service = new RuntimeDepsService();
|
||||
const merged = service.collectDependencies([
|
||||
{ name: "a", dependPackages: { foo: "^1.0.0" } },
|
||||
{ name: "b", dependPackages: { foo: "^2.0.0" } },
|
||||
]);
|
||||
|
||||
assert.equal(merged.conflicts.length, 1);
|
||||
assert.equal(merged.conflicts[0].packageName, "foo");
|
||||
});
|
||||
|
||||
it("builds a runtime package manifest in the target directory", async () => {
|
||||
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-"));
|
||||
const service = new RuntimeDepsService();
|
||||
service.runtimeDepsRootDir = rootDir;
|
||||
service.registryResolver = {
|
||||
async resolve() {
|
||||
return "https://registry.npmmirror.com";
|
||||
},
|
||||
} as any;
|
||||
service.commandRunner = {
|
||||
async run(command: string, args: string[]) {
|
||||
assert.equal(command, "pnpm");
|
||||
if (args.includes("--version")) {
|
||||
return { stdout: "9.1.0\n", stderr: "", code: 0 };
|
||||
}
|
||||
assert.equal(args[0], "install");
|
||||
assert.ok(args.includes("--ignore-workspace"));
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
},
|
||||
} as any;
|
||||
|
||||
const plugins: RuntimeDependencyPluginDefine[] = [{ name: "a", dependPackages: { foo: "^1.0.0" } }];
|
||||
const result = await service.ensureInstalled(plugins);
|
||||
|
||||
assert.equal(result.registryUrl, "https://registry.npmmirror.com");
|
||||
assert.ok(fs.existsSync(path.join(rootDir, "package.json")));
|
||||
});
|
||||
|
||||
it("installs direct dependency maps without plugin metadata", async () => {
|
||||
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-direct-"));
|
||||
const service = new RuntimeDepsService();
|
||||
service.runtimeDepsRootDir = rootDir;
|
||||
service.registryResolver = {
|
||||
async resolve() {
|
||||
return "";
|
||||
},
|
||||
} as any;
|
||||
service.commandRunner = {
|
||||
async run(command: string, args: string[]) {
|
||||
assert.equal(command, "pnpm");
|
||||
if (args.includes("--version")) {
|
||||
return { stdout: "9.1.0\n", stderr: "", code: 0 };
|
||||
}
|
||||
fs.mkdirSync(path.join(rootDir, "node_modules"), { recursive: true });
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
},
|
||||
} as any;
|
||||
|
||||
await service.ensureDependencies({ directPkg: "^1.0.0" });
|
||||
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
|
||||
assert.deepEqual(manifest.dependencies, { directPkg: "^1.0.0" });
|
||||
});
|
||||
|
||||
it("imports from runtime node_modules without installing", async () => {
|
||||
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-import-"));
|
||||
const packageDir = path.join(rootDir, "node_modules", "runtime-only");
|
||||
fs.mkdirSync(packageDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(rootDir, "package.json"), JSON.stringify({ name: "runtime-root", type: "module" }), "utf8");
|
||||
fs.writeFileSync(path.join(packageDir, "package.json"), JSON.stringify({ name: "runtime-only", type: "module", main: "index.js" }), "utf8");
|
||||
fs.writeFileSync(path.join(packageDir, "index.js"), "export const value = 42;\n", "utf8");
|
||||
|
||||
const service = new RuntimeDepsService();
|
||||
service.runtimeDepsRootDir = rootDir;
|
||||
service.commandRunner = {
|
||||
async run() {
|
||||
throw new Error("install should not run");
|
||||
},
|
||||
} as any;
|
||||
|
||||
const mod = await service.importRuntime("runtime-only");
|
||||
|
||||
assert.equal(mod.value, 42);
|
||||
});
|
||||
|
||||
it("installs configured lazy dependency when import target is missing", async () => {
|
||||
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-lazy-"));
|
||||
const service = new RuntimeDepsService();
|
||||
service.runtimeDepsRootDir = rootDir;
|
||||
service.lazyDependencies = {
|
||||
"lazy-pkg": "^1.2.3",
|
||||
};
|
||||
service.registryResolver = {
|
||||
async resolve() {
|
||||
return "";
|
||||
},
|
||||
} as any;
|
||||
service.commandRunner = {
|
||||
async run(command: string, args: string[]) {
|
||||
assert.equal(command, "pnpm");
|
||||
if (args.includes("--version")) {
|
||||
return { stdout: "9.1.0\n", stderr: "", code: 0 };
|
||||
}
|
||||
const packageDir = path.join(rootDir, "node_modules", "lazy-pkg", "sub");
|
||||
fs.mkdirSync(packageDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(rootDir, "node_modules", "lazy-pkg", "package.json"), JSON.stringify({ name: "lazy-pkg", type: "module" }), "utf8");
|
||||
fs.writeFileSync(path.join(packageDir, "entry.js"), "export const value = 7;\n", "utf8");
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
},
|
||||
} as any;
|
||||
|
||||
const mod = await service.importRuntime("lazy-pkg/sub/entry.js");
|
||||
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
|
||||
assert.deepEqual(manifest.dependencies, { "lazy-pkg": "^1.2.3" });
|
||||
assert.equal(mod.value, 7);
|
||||
});
|
||||
|
||||
it("resolves scoped package names for lazy imports", async () => {
|
||||
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-scoped-"));
|
||||
const service = new RuntimeDepsService();
|
||||
service.runtimeDepsRootDir = rootDir;
|
||||
service.lazyDependencies = {
|
||||
"@scope/lazy": "^2.0.0",
|
||||
};
|
||||
service.registryResolver = {
|
||||
async resolve() {
|
||||
return "";
|
||||
},
|
||||
} as any;
|
||||
service.commandRunner = {
|
||||
async run(command: string, args: string[]) {
|
||||
assert.equal(command, "pnpm");
|
||||
if (args.includes("--version")) {
|
||||
return { stdout: "9.1.0\n", stderr: "", code: 0 };
|
||||
}
|
||||
const packageDir = path.join(rootDir, "node_modules", "@scope", "lazy", "dist");
|
||||
fs.mkdirSync(packageDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(rootDir, "node_modules", "@scope", "lazy", "package.json"), JSON.stringify({ name: "@scope/lazy", type: "module" }), "utf8");
|
||||
fs.writeFileSync(path.join(packageDir, "index.js"), "export const scoped = true;\n", "utf8");
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
},
|
||||
} as any;
|
||||
|
||||
const mod = await service.importRuntime("@scope/lazy/dist/index.js");
|
||||
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
|
||||
assert.deepEqual(manifest.dependencies, { "@scope/lazy": "^2.0.0" });
|
||||
assert.equal(mod.scoped, true);
|
||||
});
|
||||
|
||||
it("reports missing lazy dependency configuration", async () => {
|
||||
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-lazy-missing-"));
|
||||
const service = new RuntimeDepsService();
|
||||
service.runtimeDepsRootDir = rootDir;
|
||||
service.lazyDependencies = {};
|
||||
|
||||
await assert.rejects(() => service.importRuntime("missing-pkg/sub.js"), /未配置懒加载版本: missing-pkg/);
|
||||
});
|
||||
|
||||
it("falls back to project node_modules when lazy dependency is not configured", async () => {
|
||||
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-project-fallback-"));
|
||||
const service = new RuntimeDepsService();
|
||||
service.runtimeDepsRootDir = rootDir;
|
||||
service.lazyDependencies = {};
|
||||
|
||||
const mod = await service.importRuntime("dayjs");
|
||||
|
||||
assert.equal(typeof mod.default, "function");
|
||||
});
|
||||
|
||||
it("falls back to project node_modules when lazy install fails", async () => {
|
||||
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-project-fallback-install-"));
|
||||
const service = new RuntimeDepsService();
|
||||
service.runtimeDepsRootDir = rootDir;
|
||||
service.lazyDependencies = {
|
||||
dayjs: "^1.11.7",
|
||||
};
|
||||
service.registryResolver = {
|
||||
async resolve() {
|
||||
return "";
|
||||
},
|
||||
} as any;
|
||||
service.commandRunner = {
|
||||
async run(command: string, args: string[]) {
|
||||
assert.equal(command, "pnpm");
|
||||
if (args.includes("--version")) {
|
||||
return { stdout: "9.1.0\n", stderr: "", code: 0 };
|
||||
}
|
||||
return { stdout: "", stderr: "install failed in test", code: 1 };
|
||||
},
|
||||
} as any;
|
||||
|
||||
const mod = await service.importRuntime("dayjs");
|
||||
|
||||
assert.equal(typeof mod.default, "function");
|
||||
});
|
||||
|
||||
it("keeps previously installed dependencies when installing a later plugin", async () => {
|
||||
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-merge-"));
|
||||
const service = new RuntimeDepsService();
|
||||
service.runtimeDepsRootDir = rootDir;
|
||||
service.registryResolver = {
|
||||
async resolve() {
|
||||
return "";
|
||||
},
|
||||
} as any;
|
||||
service.commandRunner = {
|
||||
async run(command: string, args: string[]) {
|
||||
assert.equal(command, "pnpm");
|
||||
if (args.includes("--version")) {
|
||||
return { stdout: "9.1.0\n", stderr: "", code: 0 };
|
||||
}
|
||||
fs.mkdirSync(path.join(rootDir, "node_modules"), { recursive: true });
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
},
|
||||
} as any;
|
||||
|
||||
await service.ensureInstalled([{ name: "a", pluginType: "deploy", dependPackages: { foo: "^1.0.0" } }]);
|
||||
await service.ensureInstalled([{ name: "b", pluginType: "deploy", dependPackages: { bar: "^2.0.0" } }]);
|
||||
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
|
||||
assert.deepEqual(manifest.dependencies, {
|
||||
foo: "^1.0.0",
|
||||
bar: "^2.0.0",
|
||||
});
|
||||
});
|
||||
|
||||
it("includes npm dependencies from dependent plugins", () => {
|
||||
const service = new RuntimeDepsService();
|
||||
accessRegistry.register("runtimeDepsAccess", {
|
||||
define: { name: "runtimeDepsAccess", title: "access", dependPackages: { accessOnly: "^1.0.0" } } as any,
|
||||
target: async () => ({} as any),
|
||||
});
|
||||
try {
|
||||
const resolved = service.resolvePluginDependencies({
|
||||
name: "deploy",
|
||||
pluginType: "deploy",
|
||||
dependPlugins: { "access:runtimeDepsAccess": "*" },
|
||||
dependPackages: { deployOnly: "^1.0.0" },
|
||||
});
|
||||
const merged = service.collectDependencies(resolved);
|
||||
|
||||
assert.deepEqual(merged.dependencies, {
|
||||
deployOnly: "^1.0.0",
|
||||
accessOnly: "^1.0.0",
|
||||
});
|
||||
} finally {
|
||||
accessRegistry.unRegister("runtimeDepsAccess");
|
||||
}
|
||||
});
|
||||
|
||||
it("installs dependencies by registered plugin key", async () => {
|
||||
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-key-"));
|
||||
const service = new RuntimeDepsService();
|
||||
service.runtimeDepsRootDir = rootDir;
|
||||
service.registryResolver = {
|
||||
async resolve() {
|
||||
return "";
|
||||
},
|
||||
} as any;
|
||||
service.commandRunner = {
|
||||
async run(command: string, args: string[]) {
|
||||
assert.equal(command, "pnpm");
|
||||
if (args.includes("--version")) {
|
||||
return { stdout: "9.1.0\n", stderr: "", code: 0 };
|
||||
}
|
||||
fs.mkdirSync(path.join(rootDir, "node_modules"), { recursive: true });
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
},
|
||||
} as any;
|
||||
pluginRegistry.register("runtimeDepsKey", {
|
||||
define: { name: "runtimeDepsKey", title: "key", dependPackages: { keyed: "^1.0.0" } } as any,
|
||||
target: async () => ({} as any),
|
||||
});
|
||||
try {
|
||||
await service.ensureRuntimeDependencies("plugin:runtimeDepsKey");
|
||||
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
|
||||
assert.deepEqual(manifest.dependencies, { keyed: "^1.0.0" });
|
||||
} finally {
|
||||
pluginRegistry.unRegister("runtimeDepsKey");
|
||||
}
|
||||
});
|
||||
|
||||
it("installs dependencies from multiple plugin keys including addon subtype keys", async () => {
|
||||
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-keys-"));
|
||||
const service = new RuntimeDepsService();
|
||||
service.runtimeDepsRootDir = rootDir;
|
||||
service.registryResolver = {
|
||||
async resolve() {
|
||||
return "";
|
||||
},
|
||||
} as any;
|
||||
service.commandRunner = {
|
||||
async run(command: string, args: string[]) {
|
||||
assert.equal(command, "pnpm");
|
||||
if (args.includes("--version")) {
|
||||
return { stdout: "9.1.0\n", stderr: "", code: 0 };
|
||||
}
|
||||
fs.mkdirSync(path.join(rootDir, "node_modules"), { recursive: true });
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
},
|
||||
} as any;
|
||||
accessRegistry.register("runtimeDepsArrayAccess", {
|
||||
define: { name: "runtimeDepsArrayAccess", title: "access", dependPackages: { accessPkg: "^1.0.0" } } as any,
|
||||
target: async () => ({} as any),
|
||||
});
|
||||
addonRegistry.register("captcha:runtimeDepsArrayAddon", {
|
||||
define: { addonType: "captcha", name: "runtimeDepsArrayAddon", title: "addon", dependPackages: { addonPkg: "^2.0.0" } } as any,
|
||||
target: async () => ({} as any),
|
||||
});
|
||||
try {
|
||||
await service.ensureRuntimeDependencies(["access:runtimeDepsArrayAccess", "addon:captcha:runtimeDepsArrayAddon"]);
|
||||
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
|
||||
assert.deepEqual(manifest.dependencies, {
|
||||
accessPkg: "^1.0.0",
|
||||
addonPkg: "^2.0.0",
|
||||
});
|
||||
} finally {
|
||||
accessRegistry.unRegister("runtimeDepsArrayAccess");
|
||||
addonRegistry.unRegister("captcha:runtimeDepsArrayAddon");
|
||||
}
|
||||
});
|
||||
|
||||
it("reports missing dependent plugins", () => {
|
||||
const service = new RuntimeDepsService();
|
||||
|
||||
assert.throws(() => service.resolvePluginDependencies({ name: "deploy", pluginType: "deploy", dependPlugins: { "access:access": "*" } }), /插件依赖缺失/);
|
||||
});
|
||||
|
||||
it("reports incompatible dependent plugin versions", () => {
|
||||
const service = new RuntimeDepsService();
|
||||
accessRegistry.register("runtimeDepsVersionedAccess", {
|
||||
define: { name: "runtimeDepsVersionedAccess", title: "access", version: "1.4.0", dependPackages: { accessOnly: "^1.0.0" } } as any,
|
||||
target: async () => ({} as any),
|
||||
});
|
||||
try {
|
||||
assert.throws(
|
||||
() =>
|
||||
service.resolvePluginDependencies({
|
||||
name: "deploy",
|
||||
pluginType: "deploy",
|
||||
dependPlugins: { "access:runtimeDepsVersionedAccess": "^2.0.0" },
|
||||
}),
|
||||
/插件依赖版本冲突/
|
||||
);
|
||||
} finally {
|
||||
accessRegistry.unRegister("runtimeDepsVersionedAccess");
|
||||
}
|
||||
});
|
||||
|
||||
it("reports bare dependent plugin names as invalid format", () => {
|
||||
const service = new RuntimeDepsService();
|
||||
|
||||
assert.throws(
|
||||
() => service.resolvePluginDependencies({ name: "deploy", pluginType: "deploy", dependPlugins: { runtimeDepsBareName: "*" } }),
|
||||
/插件依赖格式错误/
|
||||
);
|
||||
});
|
||||
|
||||
it("records runtime install environment state", async () => {
|
||||
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-state-"));
|
||||
const service = new RuntimeDepsService();
|
||||
service.runtimeDepsRootDir = rootDir;
|
||||
service.registryResolver = {
|
||||
async resolve() {
|
||||
return "";
|
||||
},
|
||||
} as any;
|
||||
service.commandRunner = {
|
||||
async run(command: string, args: string[]) {
|
||||
assert.equal(command, "pnpm");
|
||||
if (args.includes("--version")) {
|
||||
return { stdout: "9.1.0\n", stderr: "", code: 0 };
|
||||
}
|
||||
assert.equal(args[0], "install");
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
},
|
||||
} as any;
|
||||
|
||||
await service.ensureInstalled([{ name: "a", dependPackages: { foo: "^1.0.0" } }]);
|
||||
|
||||
const state = JSON.parse(fs.readFileSync(path.join(rootDir, "install-state.json"), "utf8"));
|
||||
assert.equal(state.nodeVersion, process.version);
|
||||
assert.equal(state.pnpmVersion, "9.1.0");
|
||||
assert.equal(state.lastError, undefined);
|
||||
});
|
||||
|
||||
it("serializes installs with a file lock", async () => {
|
||||
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-lock-"));
|
||||
const serviceA = new RuntimeDepsService();
|
||||
const serviceB = new RuntimeDepsService();
|
||||
for (const service of [serviceA, serviceB]) {
|
||||
service.runtimeDepsRootDir = rootDir;
|
||||
service.registryResolver = {
|
||||
async resolve() {
|
||||
return "";
|
||||
},
|
||||
} as any;
|
||||
}
|
||||
let installCount = 0;
|
||||
const commandRunner = {
|
||||
async run(command: string, args: string[]) {
|
||||
assert.equal(command, "pnpm");
|
||||
if (args.includes("--version")) {
|
||||
return { stdout: "9.1.0\n", stderr: "", code: 0 };
|
||||
}
|
||||
assert.equal(args[0], "install");
|
||||
installCount++;
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
fs.mkdirSync(path.join(rootDir, "node_modules"), { recursive: true });
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
},
|
||||
};
|
||||
serviceA.commandRunner = commandRunner as any;
|
||||
serviceB.commandRunner = commandRunner as any;
|
||||
|
||||
await Promise.all([
|
||||
serviceA.ensureInstalled([{ name: "a", dependPackages: { foo: "^1.0.0" } }]),
|
||||
serviceB.ensureInstalled([{ name: "a", dependPackages: { foo: "^1.0.0" } }]),
|
||||
]);
|
||||
|
||||
assert.equal(installCount, 1);
|
||||
});
|
||||
|
||||
it("does not pass node debugger options to pnpm child process", async () => {
|
||||
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-deps-env-"));
|
||||
const oldNodeOptions = process.env.NODE_OPTIONS;
|
||||
const oldInspectorOptions = process.env.VSCODE_INSPECTOR_OPTIONS;
|
||||
process.env.NODE_OPTIONS = "--inspect=127.0.0.1:9229 --max-old-space-size=4096";
|
||||
process.env.VSCODE_INSPECTOR_OPTIONS = '{"inspectorIpc":"test"}';
|
||||
try {
|
||||
const service = new RuntimeDepsService();
|
||||
service.runtimeDepsRootDir = rootDir;
|
||||
service.registryResolver = {
|
||||
async resolve() {
|
||||
return "";
|
||||
},
|
||||
} as any;
|
||||
service.commandRunner = {
|
||||
async run(command: string, args: string[], options: { env?: NodeJS.ProcessEnv }) {
|
||||
assert.equal(options.env?.NODE_OPTIONS, "--max-old-space-size=4096");
|
||||
assert.equal(options.env?.VSCODE_INSPECTOR_OPTIONS, undefined);
|
||||
assert.equal(options.env?.CI, "true");
|
||||
assert.equal(options.env?.pnpm_config_confirm_modules_purge, "false");
|
||||
if (args.includes("--version")) {
|
||||
return { stdout: "9.1.0\n", stderr: "", code: 0 };
|
||||
}
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
},
|
||||
} as any;
|
||||
|
||||
await service.ensureInstalled([{ name: "a", dependPackages: { foo: "^1.0.0" } }]);
|
||||
} finally {
|
||||
if (oldNodeOptions == null) {
|
||||
delete process.env.NODE_OPTIONS;
|
||||
} else {
|
||||
process.env.NODE_OPTIONS = oldNodeOptions;
|
||||
}
|
||||
if (oldInspectorOptions == null) {
|
||||
delete process.env.VSCODE_INSPECTOR_OPTIONS;
|
||||
} else {
|
||||
process.env.VSCODE_INSPECTOR_OPTIONS = oldInspectorOptions;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,618 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { spawn } from "child_process";
|
||||
import crypto from "crypto";
|
||||
import { Config, Inject, Provide, Scope, ScopeEnum } from "@midwayjs/core";
|
||||
import { createRequire } from "module";
|
||||
import { pathToFileURL } from "url";
|
||||
import { NpmRegistryResolver } from "./npm-registry-resolver.js";
|
||||
import { Registry, accessRegistry, notificationRegistry, pluginRegistry } from "@certd/pipeline";
|
||||
import { dnsProviderRegistry } from "@certd/plugin-lib";
|
||||
import { addonRegistry } from "@certd/lib-server";
|
||||
|
||||
export type RuntimeDependencyPluginDefine = {
|
||||
name: string;
|
||||
key?: string;
|
||||
title?: string;
|
||||
version?: string;
|
||||
pluginType?: string;
|
||||
addonType?: string;
|
||||
dependPlugins?: Record<string, string>;
|
||||
dependPackages?: Record<string, string>;
|
||||
};
|
||||
|
||||
type RegisteredDefineLike = RuntimeDependencyPluginDefine & {
|
||||
key?: string;
|
||||
pluginType?: string;
|
||||
addonType?: string;
|
||||
dependPlugins?: Record<string, string>;
|
||||
dependPackages?: Record<string, string>;
|
||||
};
|
||||
|
||||
function normalizeRange(range: string) {
|
||||
return range.trim().replace(/^\^/, "").replace(/^~?/, "");
|
||||
}
|
||||
|
||||
function areRangesCompatible(a: string, b: string) {
|
||||
if (!a || !b) {
|
||||
return true;
|
||||
}
|
||||
if (a === "*" || b === "*") {
|
||||
return true;
|
||||
}
|
||||
const left = normalizeRange(a).split(".");
|
||||
const right = normalizeRange(b).split(".");
|
||||
return left[0] === right[0];
|
||||
}
|
||||
|
||||
type DependencyConflict = {
|
||||
packageName: string;
|
||||
ranges: Array<{ pluginName: string; range: string }>;
|
||||
};
|
||||
|
||||
type CollectDependenciesResult = {
|
||||
dependencies: Record<string, string>;
|
||||
conflicts: DependencyConflict[];
|
||||
};
|
||||
|
||||
type InstallResult = {
|
||||
registryUrl: string;
|
||||
packageJsonPath: string;
|
||||
};
|
||||
|
||||
type RuntimeImportResolveResult = {
|
||||
resolved: string;
|
||||
packageName: string;
|
||||
};
|
||||
|
||||
type CommandRunnerResult = {
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
code: number;
|
||||
};
|
||||
|
||||
type CommandRunner = {
|
||||
run(command: string, args: string[], options: { cwd: string; timeoutMs: number; env?: NodeJS.ProcessEnv }): Promise<CommandRunnerResult>;
|
||||
};
|
||||
|
||||
const PROCESS_LOCKS = new Map<string, Promise<unknown>>();
|
||||
|
||||
class DefaultCommandRunner implements CommandRunner {
|
||||
async run(command: string, args: string[], options: { cwd: string; timeoutMs: number; env?: NodeJS.ProcessEnv }): Promise<CommandRunnerResult> {
|
||||
return await new Promise<CommandRunnerResult>(resolve => {
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
let settled = false;
|
||||
const child = spawn(command, args, {
|
||||
cwd: options.cwd,
|
||||
env: options.env,
|
||||
windowsHide: true,
|
||||
shell: process.platform === "win32",
|
||||
});
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
child.kill("SIGTERM");
|
||||
resolve({ stdout, stderr: stderr || `command timeout after ${options.timeoutMs}ms`, code: 1 });
|
||||
}, options.timeoutMs);
|
||||
|
||||
child.stdout?.on("data", chunk => {
|
||||
stdout += chunk.toString();
|
||||
});
|
||||
child.stderr?.on("data", chunk => {
|
||||
stderr += chunk.toString();
|
||||
});
|
||||
child.on("error", error => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
clearTimeout(timer);
|
||||
resolve({ stdout, stderr: error.message, code: 1 });
|
||||
});
|
||||
child.on("close", code => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
clearTimeout(timer);
|
||||
resolve({ stdout, stderr, code: code || 0 });
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Provide()
|
||||
@Scope(ScopeEnum.Request, { allowDowngrade: true })
|
||||
export class RuntimeDepsService {
|
||||
@Config("runtimeDeps.rootDir")
|
||||
runtimeDepsRootDir = "./data/.runtime-deps";
|
||||
|
||||
@Config("runtimeDeps.autoInstall")
|
||||
autoInstall = true;
|
||||
|
||||
@Config("runtimeDeps.enabled")
|
||||
enabled = true;
|
||||
|
||||
@Config("runtimeDeps.installTimeoutMs")
|
||||
installTimeoutMs = 120000;
|
||||
|
||||
@Config("runtimeDeps.pnpmCommand")
|
||||
pnpmCommand = "";
|
||||
|
||||
@Config("runtimeDeps.lazyDependencies")
|
||||
lazyDependencies: Record<string, string> = {};
|
||||
|
||||
@Inject()
|
||||
registryResolver!: NpmRegistryResolver;
|
||||
|
||||
commandRunner: CommandRunner = new DefaultCommandRunner();
|
||||
|
||||
private installPromises = new Map<string, Promise<InstallResult>>();
|
||||
|
||||
collectDependencies(plugins: RuntimeDependencyPluginDefine[]): CollectDependenciesResult {
|
||||
const merged: Record<string, string> = {};
|
||||
const seen: Record<string, Array<{ pluginName: string; range: string }>> = {};
|
||||
|
||||
for (const plugin of plugins) {
|
||||
const deps = plugin.dependPackages || {};
|
||||
for (const [packageName, range] of Object.entries(deps)) {
|
||||
seen[packageName] ||= [];
|
||||
seen[packageName].push({ pluginName: plugin.name, range });
|
||||
}
|
||||
}
|
||||
|
||||
const conflicts: DependencyConflict[] = [];
|
||||
for (const [packageName, ranges] of Object.entries(seen)) {
|
||||
const first = ranges[0]?.range;
|
||||
if (!first) {
|
||||
continue;
|
||||
}
|
||||
const conflict = ranges.some(item => !areRangesCompatible(first, item.range));
|
||||
if (conflict) {
|
||||
conflicts.push({ packageName, ranges });
|
||||
continue;
|
||||
}
|
||||
merged[packageName] = first;
|
||||
}
|
||||
|
||||
return { dependencies: merged, conflicts };
|
||||
}
|
||||
|
||||
async ensureInstalled(plugins: RuntimeDependencyPluginDefine[]): Promise<InstallResult> {
|
||||
const { dependencies, conflicts } = this.resolveDependenciesFromPlugins(plugins);
|
||||
if (conflicts.length > 0) {
|
||||
const conflict = conflicts[0];
|
||||
throw new Error(
|
||||
`动态依赖版本冲突: ${conflict.packageName} => ${conflict.ranges.map(item => `${item.pluginName}:${item.range}`).join(", ")}`
|
||||
);
|
||||
}
|
||||
return await this.ensureDependencies(dependencies);
|
||||
}
|
||||
|
||||
async ensureDependencies(dependencies: Record<string, string>): Promise<InstallResult> {
|
||||
if (!this.enabled) {
|
||||
return {
|
||||
registryUrl: "",
|
||||
packageJsonPath: path.join(this.getRuntimeDepsRootDir(), "package.json"),
|
||||
};
|
||||
}
|
||||
if (!this.autoInstall) {
|
||||
return {
|
||||
registryUrl: "",
|
||||
packageJsonPath: path.join(this.getRuntimeDepsRootDir(), "package.json"),
|
||||
};
|
||||
}
|
||||
const dependenciesHash = this.createDependenciesHash(dependencies);
|
||||
let installPromise = this.installPromises.get(dependenciesHash);
|
||||
if (!installPromise) {
|
||||
installPromise = this.doEnsureInstalled(dependencies).catch(error => {
|
||||
this.installPromises.delete(dependenciesHash);
|
||||
throw error;
|
||||
});
|
||||
this.installPromises.set(dependenciesHash, installPromise);
|
||||
}
|
||||
return await installPromise;
|
||||
}
|
||||
|
||||
resolveDependenciesFromPlugins(plugins: RuntimeDependencyPluginDefine[]): CollectDependenciesResult {
|
||||
const expandedPlugins = plugins.flatMap(plugin => this.resolvePluginDependencies(plugin));
|
||||
return this.collectDependencies(expandedPlugins);
|
||||
}
|
||||
|
||||
async ensureRuntimeDependencies(pluginKeys: string | string[]): Promise<InstallResult> {
|
||||
const keys = Array.isArray(pluginKeys) ? pluginKeys : [pluginKeys];
|
||||
const pluginDefines = keys.map(pluginKey => this.getDefineByPluginKey(pluginKey));
|
||||
if (pluginDefines.every(pluginDefine => !pluginDefine.dependPackages && !pluginDefine.dependPlugins)) {
|
||||
return {
|
||||
registryUrl: "",
|
||||
packageJsonPath: path.join(this.getRuntimeDepsRootDir(), "package.json"),
|
||||
};
|
||||
}
|
||||
const expandedPluginDefines = pluginDefines.flatMap(pluginDefine => this.resolvePluginDependencies(pluginDefine));
|
||||
return await this.ensureInstalled(expandedPluginDefines);
|
||||
}
|
||||
|
||||
private async doEnsureInstalled(dependencies: Record<string, string>): Promise<InstallResult> {
|
||||
return await this.withInstallLock(async () => {
|
||||
const rootDir = this.getRuntimeDepsRootDir();
|
||||
const packageJsonPath = path.join(rootDir, "package.json");
|
||||
const lockPath = path.join(rootDir, "pnpm-lock.yaml");
|
||||
dependencies = this.mergeInstalledDependencies(this.readManifestDependencies(packageJsonPath), dependencies);
|
||||
const dependenciesHash = this.createDependenciesHash(dependencies);
|
||||
const statePath = path.join(rootDir, "install-state.json");
|
||||
const currentState = this.readInstallState(statePath);
|
||||
if (currentState?.dependenciesHash === dependenciesHash && fs.existsSync(path.join(rootDir, "node_modules"))) {
|
||||
return { registryUrl: currentState.registryUrl || "", packageJsonPath };
|
||||
}
|
||||
const manifest = {
|
||||
name: "certd-runtime-deps",
|
||||
private: true,
|
||||
type: "module",
|
||||
dependencies,
|
||||
};
|
||||
fs.writeFileSync(packageJsonPath, JSON.stringify(manifest, null, 2), "utf8");
|
||||
|
||||
const registryUrl = await this.registryResolver.resolve();
|
||||
const env = this.buildChildEnv(registryUrl);
|
||||
const command = this.getPnpmCommand();
|
||||
const pnpmVersion = await this.getPnpmVersion(command, env);
|
||||
const args = ["install", "--prod", "--ignore-scripts", "--ignore-workspace", "--reporter=append-only"];
|
||||
if (registryUrl) {
|
||||
args.push(`--registry=${registryUrl}`);
|
||||
}
|
||||
const result = await this.commandRunner.run(command, args, {
|
||||
cwd: rootDir,
|
||||
timeoutMs: this.installTimeoutMs,
|
||||
env,
|
||||
});
|
||||
if (result.code !== 0) {
|
||||
const message = result.stderr || result.stdout || "unknown error";
|
||||
this.writeInstallState(statePath, {
|
||||
...currentState,
|
||||
installedAt: currentState?.installedAt,
|
||||
failedAt: new Date().toISOString(),
|
||||
registryUrl,
|
||||
dependenciesHash,
|
||||
nodeVersion: process.version,
|
||||
pnpmVersion,
|
||||
lockFileExists: fs.existsSync(lockPath),
|
||||
lastError: message,
|
||||
});
|
||||
throw new Error(`动态依赖安装失败: ${message}`);
|
||||
}
|
||||
this.writeInstallState(statePath, {
|
||||
installedAt: new Date().toISOString(),
|
||||
registryUrl,
|
||||
dependenciesHash,
|
||||
nodeVersion: process.version,
|
||||
pnpmVersion,
|
||||
lockFileExists: fs.existsSync(lockPath),
|
||||
});
|
||||
return { registryUrl, packageJsonPath };
|
||||
});
|
||||
}
|
||||
|
||||
async importRuntime(specifier: string) {
|
||||
if (this.isNativeImportSpecifier(specifier)) {
|
||||
return await import(specifier);
|
||||
}
|
||||
|
||||
const resolved = await this.resolveImportSpecifier(specifier);
|
||||
return await import(pathToFileURL(resolved).href);
|
||||
}
|
||||
|
||||
private async resolveImportSpecifier(specifier: string) {
|
||||
try {
|
||||
return this.resolveRuntimeSpecifier(specifier).resolved;
|
||||
} catch (runtimeError: any) {
|
||||
if (!this.isModuleNotFoundError(runtimeError)) {
|
||||
throw runtimeError;
|
||||
}
|
||||
return await this.resolveMissingRuntimeSpecifier(specifier, runtimeError);
|
||||
}
|
||||
}
|
||||
|
||||
private async resolveMissingRuntimeSpecifier(specifier: string, runtimeError: any) {
|
||||
const packageName = this.parsePackageName(specifier);
|
||||
const lazyRange = this.lazyDependencies?.[packageName];
|
||||
if (!lazyRange) {
|
||||
try {
|
||||
return this.resolveProjectSpecifier(specifier, runtimeError).resolved;
|
||||
} catch {
|
||||
throw new Error(`动态依赖未安装且未配置懒加载版本: ${packageName}`);
|
||||
}
|
||||
}
|
||||
try {
|
||||
await this.ensureLazyDependency(packageName);
|
||||
return this.resolveRuntimeSpecifier(specifier).resolved;
|
||||
} catch (lazyError: any) {
|
||||
return this.resolveProjectSpecifier(specifier, lazyError).resolved;
|
||||
}
|
||||
}
|
||||
|
||||
private isNativeImportSpecifier(specifier: string) {
|
||||
return specifier.startsWith(".") || specifier.startsWith("/") || specifier.startsWith("file:") || specifier.startsWith("node:");
|
||||
}
|
||||
|
||||
private resolveRuntimeSpecifier(specifier: string): RuntimeImportResolveResult {
|
||||
const packageName = this.parsePackageName(specifier);
|
||||
const packageJsonPath = path.join(this.getRuntimeDepsRootDir(), "package.json");
|
||||
const require = createRequire(packageJsonPath);
|
||||
const resolved = require.resolve(specifier);
|
||||
return { packageName, resolved };
|
||||
}
|
||||
|
||||
private resolveProjectSpecifier(specifier: string, cause?: any): RuntimeImportResolveResult {
|
||||
try {
|
||||
const packageName = this.parsePackageName(specifier);
|
||||
const packageJsonPath = path.resolve("package.json");
|
||||
const require = createRequire(packageJsonPath);
|
||||
const resolved = require.resolve(specifier);
|
||||
return { packageName, resolved };
|
||||
} catch (projectError: any) {
|
||||
if (cause) {
|
||||
projectError.cause = cause;
|
||||
}
|
||||
throw projectError;
|
||||
}
|
||||
}
|
||||
|
||||
private parsePackageName(specifier: string) {
|
||||
if (!specifier || specifier.trim() !== specifier) {
|
||||
throw new Error(`动态依赖导入路径无效: ${specifier}`);
|
||||
}
|
||||
const parts = specifier.split("/");
|
||||
if (specifier.startsWith("@")) {
|
||||
if (parts.length < 2 || !parts[0] || !parts[1]) {
|
||||
throw new Error(`动态依赖导入路径无效: ${specifier}`);
|
||||
}
|
||||
return `${parts[0]}/${parts[1]}`;
|
||||
}
|
||||
if (!parts[0]) {
|
||||
throw new Error(`动态依赖导入路径无效: ${specifier}`);
|
||||
}
|
||||
return parts[0];
|
||||
}
|
||||
|
||||
private async ensureLazyDependency(packageName: string) {
|
||||
const range = this.lazyDependencies?.[packageName];
|
||||
if (!range) {
|
||||
throw new Error(`动态依赖未安装且未配置懒加载版本: ${packageName}`);
|
||||
}
|
||||
const dependencies = {
|
||||
[packageName]: range,
|
||||
};
|
||||
await this.ensureDependencies(dependencies);
|
||||
}
|
||||
|
||||
private isModuleNotFoundError(error: any) {
|
||||
return error?.code === "MODULE_NOT_FOUND" || error?.code === "ERR_MODULE_NOT_FOUND";
|
||||
}
|
||||
|
||||
resolvePluginDependencies(current: RuntimeDependencyPluginDefine): RuntimeDependencyPluginDefine[] {
|
||||
const resolved: RuntimeDependencyPluginDefine[] = [];
|
||||
const visited = new Set<string>();
|
||||
|
||||
const visit = (item: RuntimeDependencyPluginDefine) => {
|
||||
const key = this.buildPluginDependencyKey(item);
|
||||
if (visited.has(key)) {
|
||||
return;
|
||||
}
|
||||
visited.add(key);
|
||||
resolved.push(item);
|
||||
for (const [dependencyName, expectedRange] of Object.entries(item.dependPlugins || {})) {
|
||||
const dependency = this.getDefineByPluginKey(dependencyName, item);
|
||||
if (!isPluginVersionCompatible(dependency, expectedRange)) {
|
||||
throw new Error(`插件依赖版本冲突: ${item.name} 依赖 ${dependencyName}@${expectedRange},当前版本为 ${dependency.version || "未声明"}`);
|
||||
}
|
||||
visit(dependency);
|
||||
}
|
||||
};
|
||||
|
||||
visit(current);
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private buildPluginDependencyKey(plugin: RuntimeDependencyPluginDefine) {
|
||||
if (plugin.pluginType === "addon" && plugin.addonType) {
|
||||
return `addon:${plugin.addonType}:${plugin.name}`;
|
||||
}
|
||||
const pluginType = plugin.pluginType === "deploy" ? "plugin" : plugin.pluginType || "unknown";
|
||||
return `${pluginType}:${plugin.name}`;
|
||||
}
|
||||
|
||||
private getDefineByPluginKey(pluginKey: string, owner?: RuntimeDependencyPluginDefine): RuntimeDependencyPluginDefine {
|
||||
const parts = pluginKey.split(":");
|
||||
const [pluginType, subtype, name] = parts;
|
||||
if (parts.length < 2 || (pluginType === "addon" && parts.length !== 3) || (pluginType !== "addon" && parts.length !== 2)) {
|
||||
const ownerName = owner?.name || pluginKey;
|
||||
throw new Error(`插件依赖格式错误: ${ownerName} 依赖 ${pluginKey},请使用 plugin:name、access:name、notification:name、dnsProvider:name 或 addon:subtype:name 格式`);
|
||||
}
|
||||
const registryMap: Record<string, { registry: Registry<any>; key: string; pluginType: string; addonType?: string }> = {
|
||||
plugin: { registry: pluginRegistry, key: subtype, pluginType: "plugin" },
|
||||
access: { registry: accessRegistry, key: subtype, pluginType: "access" },
|
||||
notification: { registry: notificationRegistry, key: subtype, pluginType: "notification" },
|
||||
dnsProvider: { registry: dnsProviderRegistry, key: subtype, pluginType: "dnsProvider" },
|
||||
addon: { registry: addonRegistry, key: `${subtype}:${name}`, pluginType: "addon", addonType: subtype },
|
||||
};
|
||||
const target = registryMap[pluginType];
|
||||
if (!target) {
|
||||
const ownerName = owner?.name || pluginKey;
|
||||
throw new Error(`插件依赖格式错误: ${ownerName} 依赖 ${pluginKey},未知插件类型 ${pluginType}`);
|
||||
}
|
||||
const define = target.registry.getDefine(target.key) as RegisteredDefineLike;
|
||||
if (!define) {
|
||||
throw new Error(`插件依赖缺失: ${owner?.name || pluginKey} 依赖 ${pluginKey},但该插件未注册或已禁用`);
|
||||
}
|
||||
return { ...define, key: pluginKey, pluginType: target.pluginType, addonType: target.addonType };
|
||||
}
|
||||
|
||||
private async withInstallLock<T>(run: () => Promise<T>): Promise<T> {
|
||||
const rootDir = this.getRuntimeDepsRootDir();
|
||||
fs.mkdirSync(rootDir, { recursive: true });
|
||||
const lockFile = path.join(rootDir, ".install.lock");
|
||||
const previous = PROCESS_LOCKS.get(lockFile);
|
||||
if (previous) {
|
||||
await previous.catch(() => undefined);
|
||||
}
|
||||
let releaseProcessLock!: () => void;
|
||||
const current = new Promise<void>(resolve => {
|
||||
releaseProcessLock = resolve;
|
||||
});
|
||||
PROCESS_LOCKS.set(lockFile, current);
|
||||
let fd: number | undefined;
|
||||
try {
|
||||
fd = await this.acquireFileLock(lockFile);
|
||||
return await run();
|
||||
} finally {
|
||||
if (fd != null) {
|
||||
fs.closeSync(fd);
|
||||
fs.rmSync(lockFile, { force: true });
|
||||
}
|
||||
releaseProcessLock();
|
||||
if (PROCESS_LOCKS.get(lockFile) === current) {
|
||||
PROCESS_LOCKS.delete(lockFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async acquireFileLock(lockFile: string) {
|
||||
const deadline = Date.now() + this.installTimeoutMs;
|
||||
while (true) {
|
||||
try {
|
||||
const fd = fs.openSync(lockFile, "wx");
|
||||
fs.writeFileSync(fd, JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() }), "utf8");
|
||||
return fd;
|
||||
} catch (error: any) {
|
||||
if (error?.code !== "EEXIST") {
|
||||
throw error;
|
||||
}
|
||||
if (Date.now() > deadline) {
|
||||
throw new Error(`动态依赖安装锁等待超时: ${lockFile}`);
|
||||
}
|
||||
await this.waitForExternalLock(lockFile, deadline);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async waitForExternalLock(lockFile: string, deadline: number) {
|
||||
while (fs.existsSync(lockFile)) {
|
||||
if (Date.now() > deadline) {
|
||||
throw new Error(`动态依赖安装锁等待超时: ${lockFile}`);
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
}
|
||||
}
|
||||
|
||||
private readInstallState(statePath: string): any {
|
||||
if (!fs.existsSync(statePath)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(statePath, "utf8"));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private writeInstallState(statePath: string, state: any) {
|
||||
fs.writeFileSync(statePath, JSON.stringify(state, null, 2), "utf8");
|
||||
}
|
||||
|
||||
private readManifestDependencies(packageJsonPath: string): Record<string, string> {
|
||||
if (!fs.existsSync(packageJsonPath)) {
|
||||
return {};
|
||||
}
|
||||
try {
|
||||
const manifest = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
||||
return manifest.dependencies || {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
private mergeInstalledDependencies(installed: Record<string, string>, requested: Record<string, string>) {
|
||||
const dependencies = { ...installed };
|
||||
for (const [packageName, range] of Object.entries(requested)) {
|
||||
const installedRange = dependencies[packageName];
|
||||
if (installedRange && !areRangesCompatible(installedRange, range)) {
|
||||
throw new Error(`动态依赖版本冲突: ${packageName} => installed:${installedRange}, requested:${range}`);
|
||||
}
|
||||
dependencies[packageName] = installedRange || range;
|
||||
}
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
private async getPnpmVersion(command: string, env: NodeJS.ProcessEnv) {
|
||||
const rootDir = this.getRuntimeDepsRootDir();
|
||||
const result = await this.commandRunner.run(command, ["--version"], {
|
||||
cwd: rootDir,
|
||||
timeoutMs: Math.min(this.installTimeoutMs, 10000),
|
||||
env,
|
||||
});
|
||||
if (result.code !== 0) {
|
||||
return "";
|
||||
}
|
||||
return (result.stdout || result.stderr || "").trim();
|
||||
}
|
||||
|
||||
private getPnpmCommand() {
|
||||
if (this.pnpmCommand) {
|
||||
return this.pnpmCommand;
|
||||
}
|
||||
return "pnpm";
|
||||
}
|
||||
|
||||
private buildChildEnv(registryUrl: string) {
|
||||
const env = { ...process.env };
|
||||
for (const key of ["NODE_OPTIONS", "VSCODE_INSPECTOR_OPTIONS", "NODE_INSPECTOR_PORT", "NODE_DEBUG"]) {
|
||||
if (!env[key]) {
|
||||
continue;
|
||||
}
|
||||
if (key === "NODE_OPTIONS") {
|
||||
env[key] = this.stripDebugNodeOptions(env[key] as string);
|
||||
} else {
|
||||
delete env[key];
|
||||
}
|
||||
}
|
||||
if (registryUrl) {
|
||||
env.npm_config_registry = registryUrl;
|
||||
env.pnpm_config_registry = registryUrl;
|
||||
}
|
||||
env.CI = env.CI || "true";
|
||||
env.npm_config_confirm_modules_purge = "false";
|
||||
env.pnpm_config_confirm_modules_purge = "false";
|
||||
return env;
|
||||
}
|
||||
|
||||
private stripDebugNodeOptions(value: string) {
|
||||
return value
|
||||
.split(/\s+/)
|
||||
.filter(Boolean)
|
||||
.filter(item => !/^--inspect(-brk|-port)?(=|$)/.test(item))
|
||||
.filter(item => !/^--debug(=|$)/.test(item))
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
private getRuntimeDepsRootDir() {
|
||||
return path.resolve(this.runtimeDepsRootDir);
|
||||
}
|
||||
|
||||
private createDependenciesHash(dependencies: Record<string, string>) {
|
||||
return crypto.createHash("sha256").update(JSON.stringify(dependencies)).digest("hex");
|
||||
}
|
||||
}
|
||||
|
||||
function isPluginVersionCompatible(plugin: RuntimeDependencyPluginDefine, expectedRange: string) {
|
||||
if (!expectedRange || expectedRange === "*") {
|
||||
return true;
|
||||
}
|
||||
if (!plugin.version) {
|
||||
return false;
|
||||
}
|
||||
return areRangesCompatible(expectedRange, plugin.version);
|
||||
}
|
||||
@@ -8,6 +8,9 @@ import { TencentAccess } from "../../plugin-lib/tencent/access.js";
|
||||
title: "腾讯云验证码",
|
||||
desc: "",
|
||||
showTest: false,
|
||||
dependPackages: {
|
||||
"tencentcloud-sdk-nodejs": "^4.1.112",
|
||||
},
|
||||
})
|
||||
export class TencentCaptcha extends BaseAddon implements ICaptchaAddon {
|
||||
@AddonInput({
|
||||
@@ -50,7 +53,7 @@ export class TencentCaptcha extends BaseAddon implements ICaptchaAddon {
|
||||
|
||||
const access = await this.getAccess<TencentAccess>(this.accessId);
|
||||
|
||||
const sdk = await import("tencentcloud-sdk-nodejs/tencentcloud/services/captcha/v20190722/index.js");
|
||||
const sdk = await this.importRuntime("tencentcloud-sdk-nodejs/tencentcloud/services/captcha/v20190722/index.js");
|
||||
|
||||
const CaptchaClient = sdk.v20190722.Client;
|
||||
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import { IsAccess, AccessInput, BaseAccess } from "@certd/pipeline";
|
||||
|
||||
@IsAccess({
|
||||
const tencentAccessDefine: any = {
|
||||
name: "tencent",
|
||||
title: "腾讯云",
|
||||
icon: "svg:icon-tencentcloud",
|
||||
order: 0,
|
||||
})
|
||||
dependPackages: {
|
||||
"tencentcloud-sdk-nodejs": "^4.1.112",
|
||||
},
|
||||
};
|
||||
|
||||
@IsAccess(tencentAccessDefine)
|
||||
export class TencentAccess extends BaseAccess {
|
||||
@AccessInput({
|
||||
title: "secretId",
|
||||
@@ -104,7 +109,7 @@ export class TencentAccess extends BaseAccess {
|
||||
}
|
||||
|
||||
async getStsClient() {
|
||||
const sdk = await import("tencentcloud-sdk-nodejs/tencentcloud/services/sts/v20180813/index.js");
|
||||
const sdk = await (this as any).importRuntime("tencentcloud-sdk-nodejs/tencentcloud/services/sts/v20180813/index.js");
|
||||
const StsClient = sdk.v20180813.Client;
|
||||
|
||||
const clientConfig = {
|
||||
|
||||
@@ -15,7 +15,7 @@ export class TencentSslClient {
|
||||
this.region = opts.region;
|
||||
}
|
||||
async getSslClient(): Promise<any> {
|
||||
const sdk = await import("tencentcloud-sdk-nodejs/tencentcloud/services/ssl/v20191205/index.js");
|
||||
const sdk = await this.access.importRuntime("tencentcloud-sdk-nodejs/tencentcloud/services/ssl/v20191205/index.js");
|
||||
const SslClient = sdk.v20191205.Client;
|
||||
|
||||
const clientConfig = {
|
||||
|
||||
+8
-3
@@ -2,13 +2,18 @@ import { AbstractDnsProvider, CreateRecordOptions, DnsResolveRecord, DomainRecor
|
||||
import { TencentAccess } from "../../plugin-lib/tencent/index.js";
|
||||
import { Pager, PageRes, PageSearch } from "@certd/pipeline";
|
||||
|
||||
@IsDnsProvider({
|
||||
const tencentDnsProviderDefine: any = {
|
||||
name: "tencent",
|
||||
title: "腾讯云",
|
||||
desc: "腾讯云域名DNS解析提供者",
|
||||
accessType: "tencent",
|
||||
icon: "svg:icon-tencentcloud",
|
||||
})
|
||||
dependPlugins: {
|
||||
"access:tencent": "*",
|
||||
},
|
||||
};
|
||||
|
||||
@IsDnsProvider(tencentDnsProviderDefine)
|
||||
export class TencentDnsProvider extends AbstractDnsProvider {
|
||||
access!: TencentAccess;
|
||||
|
||||
@@ -27,7 +32,7 @@ export class TencentDnsProvider extends AbstractDnsProvider {
|
||||
},
|
||||
},
|
||||
};
|
||||
const dnspodSdk = await import("tencentcloud-sdk-nodejs/tencentcloud/services/dnspod/v20210323/index.js");
|
||||
const dnspodSdk = await (this as any).importRuntime("tencentcloud-sdk-nodejs/tencentcloud/services/dnspod/v20210323/index.js");
|
||||
const DnspodClient = dnspodSdk.v20210323.Client;
|
||||
// 实例化要请求产品的client对象,clientProfile是可选的
|
||||
this.client = new DnspodClient(clientConfig);
|
||||
|
||||
+4
-1
@@ -7,6 +7,9 @@ import { TencentAccess } from "../../plugin-lib/tencent/access.js";
|
||||
desc: "腾讯云EO DNS解析提供者",
|
||||
accessType: "tencent",
|
||||
icon: "svg:icon-tencentcloud",
|
||||
dependPlugins: {
|
||||
"access:tencent": "*",
|
||||
},
|
||||
})
|
||||
export class TencentEoDnsProvider extends AbstractDnsProvider {
|
||||
access!: TencentAccess;
|
||||
@@ -24,7 +27,7 @@ export class TencentEoDnsProvider extends AbstractDnsProvider {
|
||||
},
|
||||
},
|
||||
};
|
||||
const teosdk = await import("tencentcloud-sdk-nodejs/tencentcloud/services/teo/v20220901/index.js");
|
||||
const teosdk = await this.importRuntime("tencentcloud-sdk-nodejs/tencentcloud/services/teo/v20220901/index.js");
|
||||
const TeoClient = teosdk.v20220901.Client;
|
||||
// 实例化要请求产品的client对象,clientProfile是可选的
|
||||
this.client = new TeoClient(clientConfig);
|
||||
|
||||
+3
@@ -10,6 +10,9 @@ import { TencentAccess, TencentSslClient } from "../../../plugin-lib/tencent/ind
|
||||
icon: "svg:icon-tencentcloud",
|
||||
group: pluginGroups.tencent.key,
|
||||
desc: "仅删除未使用的证书",
|
||||
dependPlugins: {
|
||||
"access:tencent": "*",
|
||||
},
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.AlwaysRun,
|
||||
|
||||
@@ -9,6 +9,9 @@ import { TencentSslClient } from "../../../plugin-lib/tencent/index.js";
|
||||
icon: "svg:icon-tencentcloud",
|
||||
group: pluginGroups.tencent.key,
|
||||
desc: "支持负载均衡、CDN、DDoS、直播、点播、Web应用防火墙、API网关、TEO、容器服务、对象存储、轻应用服务器、云原生微服务、云开发",
|
||||
dependPlugins: {
|
||||
"access:tencent": "*",
|
||||
},
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
@@ -108,7 +111,7 @@ export class DeployCertToTencentAll extends AbstractTaskPlugin {
|
||||
async execute(): Promise<void> {
|
||||
const access = await this.getAccess(this.accessId);
|
||||
|
||||
const sdk = await import("tencentcloud-sdk-nodejs/tencentcloud/services/ssl/v20191205/index.js");
|
||||
const sdk = await (this as any).importRuntime("tencentcloud-sdk-nodejs/tencentcloud/services/ssl/v20191205/index.js");
|
||||
const Client = sdk.v20191205.Client;
|
||||
const client = new Client({
|
||||
credential: {
|
||||
|
||||
+4
-2
@@ -9,6 +9,9 @@ import { CertApplyPluginNames } from "@certd/plugin-cert";
|
||||
icon: "svg:icon-tencentcloud",
|
||||
group: pluginGroups.tencent.key,
|
||||
desc: "推荐使用,支持CDN域名以及COS加速域名",
|
||||
dependPlugins: {
|
||||
"access:tencent": "*",
|
||||
},
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
@@ -29,7 +32,6 @@ export class TencentDeployCertToCDNv2 extends AbstractTaskPlugin {
|
||||
|
||||
@TaskInput(createCertDomainGetterInputDefine({ props: { required: false } }))
|
||||
certDomains!: string[];
|
||||
|
||||
@TaskInput({
|
||||
title: "Access提供者",
|
||||
helper: "access 授权",
|
||||
@@ -89,7 +91,7 @@ export class TencentDeployCertToCDNv2 extends AbstractTaskPlugin {
|
||||
|
||||
async getCdnClient() {
|
||||
const accessProvider = await this.getAccess<TencentAccess>(this.accessId);
|
||||
const sdk = await import("tencentcloud-sdk-nodejs/tencentcloud/services/cdn/v20180606/index.js");
|
||||
const sdk = await (this as any).importRuntime("tencentcloud-sdk-nodejs/tencentcloud/services/cdn/v20180606/index.js");
|
||||
const CdnClient = sdk.v20180606.Client;
|
||||
|
||||
const clientConfig = {
|
||||
|
||||
@@ -8,6 +8,9 @@ import { CertApplyPluginNames } from "@certd/plugin-cert";
|
||||
icon: "svg:icon-tencentcloud",
|
||||
group: pluginGroups.tencent.key,
|
||||
desc: "已废弃,请使用v2版",
|
||||
dependPlugins: {
|
||||
"access:tencent": "*",
|
||||
},
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
@@ -63,7 +66,7 @@ export class DeployToCdnPlugin extends AbstractTaskPlugin {
|
||||
Client: any;
|
||||
|
||||
async onInstance() {
|
||||
const sdk = await import("tencentcloud-sdk-nodejs/tencentcloud/services/cdn/v20180606/index.js");
|
||||
const sdk = await (this as any).importRuntime("tencentcloud-sdk-nodejs/tencentcloud/services/cdn/v20180606/index.js");
|
||||
this.Client = sdk.v20180606.Client;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,9 @@ import { CertApplyPluginNames, CertInfo } from "@certd/plugin-cert";
|
||||
icon: "svg:icon-tencentcloud",
|
||||
group: pluginGroups.tencent.key,
|
||||
desc: "暂时只支持单向认证证书,暂时只支持通用负载均衡",
|
||||
dependPlugins: {
|
||||
"access:tencent": "*",
|
||||
},
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
@@ -106,7 +109,7 @@ export class DeployCertToTencentCLB extends AbstractTaskPlugin {
|
||||
}
|
||||
|
||||
async getClient() {
|
||||
const sdk = await import("tencentcloud-sdk-nodejs/tencentcloud/services/clb/v20180317/index.js");
|
||||
const sdk = await (this as any).importRuntime("tencentcloud-sdk-nodejs/tencentcloud/services/clb/v20180317/index.js");
|
||||
const ClbClient = sdk.v20180317.Client;
|
||||
|
||||
const accessProvider = (await this.getAccess(this.accessId)) as TencentAccess;
|
||||
|
||||
@@ -3,19 +3,28 @@ import { CertInfo } from "@certd/plugin-cert";
|
||||
import { createRemoteSelectInputDefine } from "@certd/plugin-lib";
|
||||
import { TencentSslClient } from "../../../plugin-lib/tencent/index.js";
|
||||
import { CertApplyPluginNames } from "@certd/plugin-cert";
|
||||
@IsTaskPlugin({
|
||||
|
||||
const deployCertToTencentCosDefine: any = {
|
||||
name: "DeployCertToTencentCosPlugin",
|
||||
title: "腾讯云-部署证书到COS",
|
||||
needPlus: false,
|
||||
icon: "svg:icon-tencentcloud",
|
||||
group: pluginGroups.tencent.key,
|
||||
desc: "部署到腾讯云COS源站域名证书,注意是源站域名,加速域名请使用腾讯云CDN v2插件【注意:很不稳定,需要重试很多次偶尔才能成功一次】",
|
||||
dependPlugins: {
|
||||
"access:tencent": "*",
|
||||
},
|
||||
dependPackages: {
|
||||
"cos-nodejs-sdk-v5": "^2.14.6",
|
||||
},
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
},
|
||||
},
|
||||
})
|
||||
};
|
||||
|
||||
@IsTaskPlugin(deployCertToTencentCosDefine)
|
||||
export class DeployCertToTencentCosPlugin extends AbstractTaskPlugin {
|
||||
/**
|
||||
* AccessProvider的id
|
||||
@@ -133,7 +142,7 @@ export class DeployCertToTencentCosPlugin extends AbstractTaskPlugin {
|
||||
async onGetDomainList(data: any) {
|
||||
const access = await this.getAccess(this.accessId);
|
||||
|
||||
const cosv5 = await import("cos-nodejs-sdk-v5");
|
||||
const cosv5 = await (this as any).importRuntime("cos-nodejs-sdk-v5");
|
||||
const cos = new cosv5.default({
|
||||
SecretId: access.secretId,
|
||||
SecretKey: access.secretKey,
|
||||
|
||||
@@ -11,6 +11,9 @@ import { TencentSslClient } from "../../../plugin-lib/tencent/index.js";
|
||||
icon: "svg:icon-tencentcloud",
|
||||
desc: "腾讯云边缘安全加速平台EdgeOne(EO)",
|
||||
group: pluginGroups.tencent.key,
|
||||
dependPlugins: {
|
||||
"access:tencent": "*",
|
||||
},
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
@@ -85,7 +88,7 @@ export class DeployCertToTencentEO extends AbstractTaskPlugin {
|
||||
Client: any;
|
||||
|
||||
async onInstance() {
|
||||
const sdk = await import("tencentcloud-sdk-nodejs/tencentcloud/services/teo/v20220901/index.js");
|
||||
const sdk = await (this as any).importRuntime("tencentcloud-sdk-nodejs/tencentcloud/services/teo/v20220901/index.js");
|
||||
this.Client = sdk.v20220901.Client;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,9 @@ import { TencentSslClient } from "../../../plugin-lib/tencent/index.js";
|
||||
desc: "https://console.cloud.tencent.com/live/",
|
||||
group: pluginGroups.tencent.key,
|
||||
needPlus: false,
|
||||
dependPlugins: {
|
||||
"access:tencent": "*",
|
||||
},
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
@@ -92,7 +95,7 @@ export class TencentDeployCertToLive extends AbstractTaskPlugin {
|
||||
|
||||
async getLiveClient() {
|
||||
const accessProvider = await this.getAccess<TencentAccess>(this.accessId);
|
||||
const sdk = await import("tencentcloud-sdk-nodejs/tencentcloud/services/live/v20180801/index.js");
|
||||
const sdk = await (this as any).importRuntime("tencentcloud-sdk-nodejs/tencentcloud/services/live/v20180801/index.js");
|
||||
const CssClient = sdk.v20180801.Client;
|
||||
|
||||
const clientConfig = {
|
||||
|
||||
+4
-1
@@ -11,6 +11,9 @@ import yaml from "js-yaml";
|
||||
icon: "svg:icon-tencentcloud",
|
||||
group: pluginGroups.tencent.key,
|
||||
desc: "修改TKE集群密钥配置,支持Opaque和TLS证书类型。注意:\n1. serverless集群请使用K8S部署插件;\n2. Opaque类型需要【上传到腾讯云】作为前置任务;\n3. ApiServer需要开通公网访问(或者certd可访问),实际上底层仍然是通过KubeClient进行部署",
|
||||
dependPlugins: {
|
||||
"access:tencent": "*",
|
||||
},
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
@@ -203,7 +206,7 @@ export class DeployCertToTencentTKEIngressPlugin extends AbstractTaskPlugin {
|
||||
}
|
||||
|
||||
async getTkeClient(accessProvider: any, region = "ap-guangzhou") {
|
||||
const sdk = await import("tencentcloud-sdk-nodejs/tencentcloud/services/tke/v20180525/index.js");
|
||||
const sdk = await this.importRuntime("tencentcloud-sdk-nodejs/tencentcloud/services/tke/v20180525/index.js");
|
||||
const TkeClient = sdk.v20180525.Client;
|
||||
const clientConfig = {
|
||||
credential: {
|
||||
|
||||
@@ -14,6 +14,9 @@ import { omit } from "lodash-es";
|
||||
group: pluginGroups.tencent.key,
|
||||
needPlus: false,
|
||||
deprecated: "腾讯更新证书(Id不变)接口已失效,本插件已下架,请使用其他接口",
|
||||
dependPlugins: {
|
||||
"access:tencent": "*",
|
||||
},
|
||||
default: {
|
||||
//默认值配置照抄即可
|
||||
strategy: {
|
||||
|
||||
@@ -8,6 +8,9 @@ import { TencentAccess } from "../../../plugin-lib/tencent/access.js";
|
||||
icon: "svg:icon-tencentcloud",
|
||||
group: pluginGroups.tencent.key,
|
||||
desc: "腾讯云实例开关机",
|
||||
dependPlugins: {
|
||||
"access:tencent": "*",
|
||||
},
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.AlwaysRun,
|
||||
@@ -137,7 +140,7 @@ export class TencentActionInstancesPlugin extends AbstractTaskPlugin {
|
||||
|
||||
async getCvmClient() {
|
||||
const accessProvider = await this.getAccess<TencentAccess>(this.accessId);
|
||||
const sdk = await import("tencentcloud-sdk-nodejs/tencentcloud/services/cvm/v20170312/index.js");
|
||||
const sdk = await this.importRuntime("tencentcloud-sdk-nodejs/tencentcloud/services/cvm/v20170312/index.js");
|
||||
const CvmClient = sdk.v20170312.Client;
|
||||
|
||||
if (!this.region) {
|
||||
|
||||
+8
-3
@@ -3,18 +3,23 @@ import { CertApplyPluginNames, CertReader } from "@certd/plugin-cert";
|
||||
import { TencentAccess } from "../../../plugin-lib/tencent/access.js";
|
||||
import { TencentSslClient } from "../../../plugin-lib/tencent/index.js";
|
||||
|
||||
@IsTaskPlugin({
|
||||
const uploadCertToTencentDefine: any = {
|
||||
name: "UploadCertToTencent",
|
||||
title: "腾讯云-上传证书到腾讯云",
|
||||
icon: "svg:icon-tencentcloud",
|
||||
desc: "上传成功后输出:tencentCertId",
|
||||
group: pluginGroups.tencent.key,
|
||||
dependPlugins: {
|
||||
"access:tencent": "*",
|
||||
},
|
||||
default: {
|
||||
strategy: {
|
||||
runStrategy: RunStrategy.SkipWhenSucceed,
|
||||
},
|
||||
},
|
||||
})
|
||||
};
|
||||
|
||||
@IsTaskPlugin(uploadCertToTencentDefine)
|
||||
export class UploadCertToTencent extends AbstractTaskPlugin {
|
||||
// @TaskInput({ title: '证书名称' })
|
||||
// name!: string;
|
||||
@@ -48,7 +53,7 @@ export class UploadCertToTencent extends AbstractTaskPlugin {
|
||||
|
||||
Client: any;
|
||||
async onInstance() {
|
||||
const sdk = await import("tencentcloud-sdk-nodejs/tencentcloud/services/ssl/v20191205/index.js");
|
||||
const sdk = await (this as any).importRuntime("tencentcloud-sdk-nodejs/tencentcloud/services/ssl/v20191205/index.js");
|
||||
this.Client = sdk.v20191205.Client;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user