mirror of
https://github.com/certd/certd.git
synced 2026-07-05 19:37:34 +08:00
chore: 完善第三方依赖动态加载
This commit is contained in:
@@ -38,9 +38,7 @@ export class NpmRegistryResolver {
|
||||
|
||||
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);
|
||||
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);
|
||||
|
||||
@@ -46,6 +46,7 @@ describe("RuntimeDepsService", () => {
|
||||
}
|
||||
assert.equal(args[0], "install");
|
||||
assert.ok(args.includes("--ignore-workspace"));
|
||||
assert.ok(args.includes("--no-frozen-lockfile"));
|
||||
return { stdout: "", stderr: "", code: 0 };
|
||||
},
|
||||
} as any;
|
||||
@@ -295,7 +296,7 @@ describe("RuntimeDepsService", () => {
|
||||
target: async () => ({} as any),
|
||||
});
|
||||
try {
|
||||
await service.ensureRuntimeDependencies("plugin:runtimeDepsKey");
|
||||
await service.ensureRuntimeDependencies({ pluginKeys: "plugin:runtimeDepsKey" });
|
||||
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
|
||||
assert.deepEqual(manifest.dependencies, { keyed: "^1.0.0" });
|
||||
@@ -332,7 +333,7 @@ describe("RuntimeDepsService", () => {
|
||||
target: async () => ({} as any),
|
||||
});
|
||||
try {
|
||||
await service.ensureRuntimeDependencies(["access:runtimeDepsArrayAccess", "addon:captcha:runtimeDepsArrayAddon"]);
|
||||
await service.ensureRuntimeDependencies({ pluginKeys: ["access:runtimeDepsArrayAccess", "addon:captcha:runtimeDepsArrayAddon"] });
|
||||
|
||||
const manifest = JSON.parse(fs.readFileSync(path.join(rootDir, "package.json"), "utf8"));
|
||||
assert.deepEqual(manifest.dependencies, {
|
||||
@@ -375,10 +376,7 @@ describe("RuntimeDepsService", () => {
|
||||
it("reports bare dependent plugin names as invalid format", () => {
|
||||
const service = new RuntimeDepsService();
|
||||
|
||||
assert.throws(
|
||||
() => service.resolvePluginDependencies({ name: "deploy", pluginType: "deploy", dependPlugins: { runtimeDepsBareName: "*" } }),
|
||||
/插件依赖格式错误/
|
||||
);
|
||||
assert.throws(() => service.resolvePluginDependencies({ name: "deploy", pluginType: "deploy", dependPlugins: { runtimeDepsBareName: "*" } }), /插件依赖格式错误/);
|
||||
});
|
||||
|
||||
it("records runtime install environment state", async () => {
|
||||
@@ -438,10 +436,7 @@ describe("RuntimeDepsService", () => {
|
||||
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" } }]),
|
||||
]);
|
||||
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);
|
||||
});
|
||||
@@ -487,4 +482,27 @@ describe("RuntimeDepsService", () => {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("clears runtime dependency directory", async () => {
|
||||
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-clear-"));
|
||||
const runtimeRootDir = path.join(rootDir, ".runtime-deps");
|
||||
fs.mkdirSync(path.join(runtimeRootDir, "node_modules", "foo"), { recursive: true });
|
||||
fs.writeFileSync(path.join(runtimeRootDir, "package.json"), "{}", "utf8");
|
||||
const service = new RuntimeDepsService();
|
||||
service.runtimeDepsRootDir = runtimeRootDir;
|
||||
service.installTimeoutMs = 1000;
|
||||
|
||||
await service.clearRuntimeDeps();
|
||||
|
||||
assert.equal(fs.existsSync(runtimeRootDir), true);
|
||||
assert.equal(fs.readdirSync(runtimeRootDir).length, 0);
|
||||
});
|
||||
|
||||
it("rejects clearing unexpected runtime dependency path", async () => {
|
||||
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "certd-runtime-clear-invalid-"));
|
||||
const service = new RuntimeDepsService();
|
||||
service.runtimeDepsRootDir = rootDir;
|
||||
|
||||
await assert.rejects(() => service.clearRuntimeDeps(), /动态依赖目录配置异常/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ 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";
|
||||
import { logger, ILogger } from "@certd/basic";
|
||||
|
||||
export type RuntimeDependencyPluginDefine = {
|
||||
name: string;
|
||||
@@ -72,12 +73,14 @@ type CommandRunnerResult = {
|
||||
};
|
||||
|
||||
type CommandRunner = {
|
||||
// @ts-ignore
|
||||
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 {
|
||||
// @ts-ignore
|
||||
async run(command: string, args: string[], options: { cwd: string; timeoutMs: number; env?: NodeJS.ProcessEnv }): Promise<CommandRunnerResult> {
|
||||
return await new Promise<CommandRunnerResult>(resolve => {
|
||||
let stdout = "";
|
||||
@@ -87,6 +90,7 @@ class DefaultCommandRunner implements CommandRunner {
|
||||
cwd: options.cwd,
|
||||
env: options.env,
|
||||
windowsHide: true,
|
||||
// @ts-ignore
|
||||
shell: process.platform === "win32",
|
||||
});
|
||||
|
||||
@@ -182,18 +186,18 @@ export class RuntimeDepsService {
|
||||
return { dependencies: merged, conflicts };
|
||||
}
|
||||
|
||||
async ensureInstalled(plugins: RuntimeDependencyPluginDefine[]): Promise<InstallResult> {
|
||||
async ensureInstalled(options: { plugins: RuntimeDependencyPluginDefine[]; logger?: ILogger }): Promise<InstallResult> {
|
||||
const { plugins, logger: log } = options;
|
||||
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(", ")}`
|
||||
);
|
||||
throw new Error(`动态依赖版本冲突: ${conflict.packageName} => ${conflict.ranges.map(item => `${item.pluginName}:${item.range}`).join(", ")}`);
|
||||
}
|
||||
return await this.ensureDependencies(dependencies);
|
||||
return await this.ensureDependencies({ dependencies, logger: log });
|
||||
}
|
||||
|
||||
async ensureDependencies(dependencies: Record<string, string>): Promise<InstallResult> {
|
||||
async ensureDependencies(options: { dependencies: Record<string, string>; logger?: ILogger }): Promise<InstallResult> {
|
||||
const { dependencies, logger: log } = options;
|
||||
if (!this.enabled) {
|
||||
return {
|
||||
registryUrl: "",
|
||||
@@ -209,7 +213,7 @@ export class RuntimeDepsService {
|
||||
const dependenciesHash = this.createDependenciesHash(dependencies);
|
||||
let installPromise = this.installPromises.get(dependenciesHash);
|
||||
if (!installPromise) {
|
||||
installPromise = this.doEnsureInstalled(dependencies).catch(error => {
|
||||
installPromise = this.doEnsureInstalled({ dependencies, logger: log }).catch(error => {
|
||||
this.installPromises.delete(dependenciesHash);
|
||||
throw error;
|
||||
});
|
||||
@@ -223,7 +227,8 @@ export class RuntimeDepsService {
|
||||
return this.collectDependencies(expandedPlugins);
|
||||
}
|
||||
|
||||
async ensureRuntimeDependencies(pluginKeys: string | string[]): Promise<InstallResult> {
|
||||
async ensureRuntimeDependencies(options: { pluginKeys: string | string[]; logger?: ILogger }): Promise<InstallResult> {
|
||||
const { pluginKeys, logger: log } = options;
|
||||
const keys = Array.isArray(pluginKeys) ? pluginKeys : [pluginKeys];
|
||||
const pluginDefines = keys.map(pluginKey => this.getDefineByPluginKey(pluginKey));
|
||||
if (pluginDefines.every(pluginDefine => !pluginDefine.dependPackages && !pluginDefine.dependPlugins)) {
|
||||
@@ -233,19 +238,23 @@ export class RuntimeDepsService {
|
||||
};
|
||||
}
|
||||
const expandedPluginDefines = pluginDefines.flatMap(pluginDefine => this.resolvePluginDependencies(pluginDefine));
|
||||
return await this.ensureInstalled(expandedPluginDefines);
|
||||
return await this.ensureInstalled({ plugins: expandedPluginDefines, logger: log });
|
||||
}
|
||||
|
||||
private async doEnsureInstalled(dependencies: Record<string, string>): Promise<InstallResult> {
|
||||
private async doEnsureInstalled(options: { dependencies: Record<string, string>; logger?: ILogger }): Promise<InstallResult> {
|
||||
let { dependencies } = options;
|
||||
const log = options.logger || logger;
|
||||
return await this.withInstallLock(async () => {
|
||||
const rootDir = this.getRuntimeDepsRootDir();
|
||||
const packageJsonPath = path.join(rootDir, "package.json");
|
||||
const lockPath = path.join(rootDir, "pnpm-lock.yaml");
|
||||
log.info(`第三方依赖安装: ${JSON.stringify(dependencies)}`);
|
||||
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"))) {
|
||||
log.info("第三方依赖已安装");
|
||||
return { registryUrl: currentState.registryUrl || "", packageJsonPath };
|
||||
}
|
||||
const manifest = {
|
||||
@@ -260,10 +269,12 @@ export class RuntimeDepsService {
|
||||
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"];
|
||||
const args = ["install", "--prod", "--ignore-scripts", "--ignore-workspace", "--no-frozen-lockfile", "--reporter=append-only"];
|
||||
if (registryUrl) {
|
||||
args.push(`--registry=${registryUrl}`);
|
||||
}
|
||||
|
||||
log.info(`开始安装第三方依赖: ${Object.keys(dependencies).join(", ")}`);
|
||||
const result = await this.commandRunner.run(command, args, {
|
||||
cwd: rootDir,
|
||||
timeoutMs: this.installTimeoutMs,
|
||||
@@ -277,6 +288,7 @@ export class RuntimeDepsService {
|
||||
failedAt: new Date().toISOString(),
|
||||
registryUrl,
|
||||
dependenciesHash,
|
||||
// @ts-ignore
|
||||
nodeVersion: process.version,
|
||||
pnpmVersion,
|
||||
lockFileExists: fs.existsSync(lockPath),
|
||||
@@ -288,35 +300,37 @@ export class RuntimeDepsService {
|
||||
installedAt: new Date().toISOString(),
|
||||
registryUrl,
|
||||
dependenciesHash,
|
||||
// @ts-ignore
|
||||
nodeVersion: process.version,
|
||||
pnpmVersion,
|
||||
lockFileExists: fs.existsSync(lockPath),
|
||||
});
|
||||
log.info("第三方依赖安装完成");
|
||||
return { registryUrl, packageJsonPath };
|
||||
});
|
||||
}
|
||||
|
||||
async importRuntime(specifier: string) {
|
||||
async importRuntime(specifier: string,logger?:ILogger) {
|
||||
if (this.isNativeImportSpecifier(specifier)) {
|
||||
return await import(specifier);
|
||||
}
|
||||
|
||||
const resolved = await this.resolveImportSpecifier(specifier);
|
||||
const resolved = await this.resolveImportSpecifier(specifier,logger);
|
||||
return await import(pathToFileURL(resolved).href);
|
||||
}
|
||||
|
||||
private async resolveImportSpecifier(specifier: string) {
|
||||
private async resolveImportSpecifier(specifier: string,logger?:ILogger) {
|
||||
try {
|
||||
return this.resolveRuntimeSpecifier(specifier).resolved;
|
||||
} catch (runtimeError: any) {
|
||||
if (!this.isModuleNotFoundError(runtimeError)) {
|
||||
throw runtimeError;
|
||||
}
|
||||
return await this.resolveMissingRuntimeSpecifier(specifier, runtimeError);
|
||||
return await this.resolveMissingRuntimeSpecifier(specifier, runtimeError,logger);
|
||||
}
|
||||
}
|
||||
|
||||
private async resolveMissingRuntimeSpecifier(specifier: string, runtimeError: any) {
|
||||
private async resolveMissingRuntimeSpecifier(specifier: string, runtimeError: any,logger?:ILogger) {
|
||||
const packageName = this.parsePackageName(specifier);
|
||||
const lazyRange = this.lazyDependencies?.[packageName];
|
||||
if (!lazyRange) {
|
||||
@@ -327,7 +341,7 @@ export class RuntimeDepsService {
|
||||
}
|
||||
}
|
||||
try {
|
||||
await this.ensureLazyDependency(packageName);
|
||||
await this.ensureLazyDependency(packageName,logger);
|
||||
return this.resolveRuntimeSpecifier(specifier).resolved;
|
||||
} catch (lazyError: any) {
|
||||
return this.resolveProjectSpecifier(specifier, lazyError).resolved;
|
||||
@@ -378,7 +392,7 @@ export class RuntimeDepsService {
|
||||
return parts[0];
|
||||
}
|
||||
|
||||
private async ensureLazyDependency(packageName: string) {
|
||||
private async ensureLazyDependency(packageName: string,logger?:ILogger) {
|
||||
const range = this.lazyDependencies?.[packageName];
|
||||
if (!range) {
|
||||
throw new Error(`动态依赖未安装且未配置懒加载版本: ${packageName}`);
|
||||
@@ -386,7 +400,7 @@ export class RuntimeDepsService {
|
||||
const dependencies = {
|
||||
[packageName]: range,
|
||||
};
|
||||
await this.ensureDependencies(dependencies);
|
||||
await this.ensureDependencies({ dependencies,logger });
|
||||
}
|
||||
|
||||
private isModuleNotFoundError(error: any) {
|
||||
@@ -485,6 +499,7 @@ export class RuntimeDepsService {
|
||||
while (true) {
|
||||
try {
|
||||
const fd = fs.openSync(lockFile, "wx");
|
||||
// @ts-ignore
|
||||
fs.writeFileSync(fd, JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() }), "utf8");
|
||||
return fd;
|
||||
} catch (error: any) {
|
||||
@@ -508,6 +523,28 @@ export class RuntimeDepsService {
|
||||
}
|
||||
}
|
||||
|
||||
async clearRuntimeDeps() {
|
||||
const rootDir = this.getRuntimeDepsRootDir();
|
||||
const normalizedRootDir = path.normalize(rootDir);
|
||||
if (!normalizedRootDir.endsWith(path.normalize(".runtime-deps"))) {
|
||||
throw new Error(`动态依赖目录配置异常,拒绝清理: ${rootDir}`);
|
||||
}
|
||||
await this.withInstallLock(async () => {
|
||||
if (fs.existsSync(rootDir)) {
|
||||
const entries = fs.readdirSync(rootDir);
|
||||
for (const entry of entries) {
|
||||
if (entry === ".install.lock") {
|
||||
continue;
|
||||
}
|
||||
const entryPath = path.join(rootDir, entry);
|
||||
fs.rmSync(entryPath, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
this.installPromises.clear();
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
private readInstallState(statePath: string): any {
|
||||
if (!fs.existsSync(statePath)) {
|
||||
return null;
|
||||
@@ -547,6 +584,7 @@ export class RuntimeDepsService {
|
||||
return dependencies;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
private async getPnpmVersion(command: string, env: NodeJS.ProcessEnv) {
|
||||
const rootDir = this.getRuntimeDepsRootDir();
|
||||
const result = await this.commandRunner.run(command, ["--version"], {
|
||||
@@ -568,6 +606,7 @@ export class RuntimeDepsService {
|
||||
}
|
||||
|
||||
private buildChildEnv(registryUrl: string) {
|
||||
// @ts-ignore
|
||||
const env = { ...process.env };
|
||||
for (const key of ["NODE_OPTIONS", "VSCODE_INSPECTOR_OPTIONS", "NODE_INSPECTOR_PORT", "NODE_DEBUG"]) {
|
||||
if (!env[key]) {
|
||||
|
||||
Reference in New Issue
Block a user