546 lines
22 KiB
TypeScript
546 lines
22 KiB
TypeScript
import path from 'node:path';
|
|
import type {
|
|
CodingPluginDefinition,
|
|
CodingPluginToolDefinition,
|
|
} from '../../shared/coding-plugins';
|
|
import {
|
|
CORE_CODING_SKILL_IDS,
|
|
type CodingSkillId,
|
|
} from '../../shared/coding-skills';
|
|
import {
|
|
accountBindingKey,
|
|
type AccountBinding,
|
|
type MarketplaceLibrarySnapshot,
|
|
} from './account-plugin-cache';
|
|
import type { InstalledRelease } from './package-store';
|
|
import type {
|
|
PluginBillingPolicy,
|
|
PluginCatalog,
|
|
PluginCatalogOperation,
|
|
PluginPolicyClientState,
|
|
} from '../services/plugin-policy-client';
|
|
|
|
/** The serialisable Skill resource consumed by every next Pi worker. */
|
|
export interface SkillEntry {
|
|
readonly id: CodingSkillId;
|
|
readonly entryPath: string;
|
|
/** Main-verified package root for an installed Marketplace Skill. */
|
|
readonly packageRoot?: string;
|
|
}
|
|
|
|
/** A policy row copied from the last verified server catalog. */
|
|
export interface RuntimePolicy {
|
|
readonly pluginId: string;
|
|
readonly contractVersion: number;
|
|
readonly capabilityId: string;
|
|
readonly operation: string;
|
|
readonly billing: PluginBillingPolicy;
|
|
}
|
|
|
|
export type PluginUnavailableReasonCode =
|
|
| 'account_required'
|
|
| 'library_required'
|
|
| 'library_unavailable'
|
|
| 'release_not_installed'
|
|
| 'release_invalid'
|
|
| 'client_incompatible'
|
|
| 'project_disabled'
|
|
| 'skill_unassigned'
|
|
| 'runtime_suspended'
|
|
| 'policy_unavailable'
|
|
| 'policy_unsupported'
|
|
| 'billing_unavailable';
|
|
|
|
export interface PluginUnavailableReason {
|
|
readonly pluginId: string;
|
|
readonly code: PluginUnavailableReasonCode;
|
|
readonly message: string;
|
|
}
|
|
|
|
/**
|
|
* Main-owned, frozen worker contract. No renderer or worker state is an
|
|
* authority: all consumers receive this exact projection for one worker.
|
|
*/
|
|
export interface EffectivePluginSnapshot {
|
|
readonly accountSessionId: string;
|
|
readonly projectId: string;
|
|
readonly pluginReleaseIds: readonly string[];
|
|
readonly effectiveSkillIds: readonly CodingSkillId[];
|
|
readonly skillEntries: readonly SkillEntry[];
|
|
readonly toolDefinitions: readonly CodingPluginToolDefinition[];
|
|
readonly runtimePolicies: readonly RuntimePolicy[];
|
|
readonly unavailableReasons: readonly PluginUnavailableReason[];
|
|
}
|
|
|
|
export interface EffectivePluginResolverInput {
|
|
readonly projectId: string;
|
|
readonly projectPath: string;
|
|
readonly assignedSkillIds: readonly string[];
|
|
readonly role: 'parent' | 'child';
|
|
/** Test and composition seam for an already fetched Account Library. */
|
|
readonly library?: MarketplaceLibrarySnapshot | null;
|
|
}
|
|
|
|
export interface EffectivePluginResolverOptions {
|
|
readonly definitions?: readonly CodingPluginDefinition[];
|
|
readonly getDefinitions?: () => readonly CodingPluginDefinition[] | Promise<readonly CodingPluginDefinition[]>;
|
|
readonly installedDefinitions?: readonly CodingPluginDefinition[];
|
|
readonly getInstalled?: (pluginId: string) => Promise<InstalledRelease | null> | InstalledRelease | null;
|
|
readonly packageStore?: {
|
|
readInstalledIndex(): Promise<readonly { pluginId: string }[]>;
|
|
getInstalled(pluginId: string): Promise<InstalledRelease | null>;
|
|
};
|
|
readonly getLibrary?: (binding: AccountBinding) => Promise<MarketplaceLibrarySnapshot | null> | MarketplaceLibrarySnapshot | null;
|
|
readonly marketplace?: {
|
|
readLibrary(): Promise<MarketplaceLibrarySnapshot>;
|
|
};
|
|
readonly accountCache?: {
|
|
getLibrary(binding: AccountBinding): MarketplaceLibrarySnapshot | null;
|
|
};
|
|
readonly getAccountBinding?: () => AccountBinding | null;
|
|
readonly getAccountSessionId?: () => string;
|
|
readonly getEnabledPluginIds?: (projectPath: string) => Promise<readonly string[]>;
|
|
readonly projectPlugins?: {
|
|
getEnabledPluginIds(projectPath: string): Promise<readonly string[]>;
|
|
};
|
|
readonly policyClient?: {
|
|
getState(): PluginPolicyClientState;
|
|
refresh(): Promise<unknown>;
|
|
};
|
|
}
|
|
|
|
export interface EffectivePluginSkillSource {
|
|
readonly id: string;
|
|
readonly pluginId: string;
|
|
readonly packageRoot: string;
|
|
readonly directory: string;
|
|
readonly entryPath: string;
|
|
}
|
|
|
|
interface DefinitionRecord {
|
|
readonly definition: CodingPluginDefinition;
|
|
readonly installed: boolean;
|
|
readonly unavailableReason?: PluginUnavailableReasonCode;
|
|
}
|
|
|
|
const EMPTY_POLICY_STATE: PluginPolicyClientState = {
|
|
status: 'unavailable',
|
|
catalog: null,
|
|
revision: 0,
|
|
lastVerifiedAt: null,
|
|
};
|
|
|
|
function clone<T>(value: T): T {
|
|
return structuredClone(value);
|
|
}
|
|
|
|
function freezeArray<T>(value: readonly T[]): readonly T[] {
|
|
return Object.freeze([...value]);
|
|
}
|
|
|
|
function deepFreeze<T>(value: T): T {
|
|
if (value && typeof value === 'object' && !Object.isFrozen(value)) {
|
|
for (const child of Object.values(value as Record<string, unknown>)) deepFreeze(child);
|
|
Object.freeze(value);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function freezeSnapshot(value: EffectivePluginSnapshot): EffectivePluginSnapshot {
|
|
const effectiveSkillIds = [...new Set(value.effectiveSkillIds)];
|
|
const skillEntries = [...new Map(value.skillEntries.map((entry) => [entry.id, entry])).values()];
|
|
return Object.freeze({
|
|
accountSessionId: value.accountSessionId,
|
|
projectId: value.projectId,
|
|
pluginReleaseIds: freezeArray(value.pluginReleaseIds),
|
|
effectiveSkillIds: freezeArray(effectiveSkillIds),
|
|
skillEntries: freezeArray(skillEntries.map((entry) => Object.freeze({ ...entry }))),
|
|
toolDefinitions: freezeArray(value.toolDefinitions.map((tool) => deepFreeze(clone(tool)))),
|
|
runtimePolicies: freezeArray(value.runtimePolicies.map((policy) => deepFreeze({
|
|
...policy,
|
|
billing: clone(policy.billing),
|
|
}))),
|
|
unavailableReasons: freezeArray(value.unavailableReasons.map((reason) => Object.freeze({ ...reason }))),
|
|
});
|
|
}
|
|
|
|
function accountSessionId(
|
|
options: EffectivePluginResolverOptions,
|
|
binding: AccountBinding | null,
|
|
): string {
|
|
const provided = options.getAccountSessionId?.();
|
|
if (provided && provided.trim()) return provided;
|
|
return binding ? accountBindingKey(binding) : 'anonymous';
|
|
}
|
|
|
|
function normalizeIds(value: readonly string[]): string[] {
|
|
return [...new Set(value.map((id) => id.trim()).filter(Boolean))];
|
|
}
|
|
|
|
function policyOperation(
|
|
catalog: PluginCatalog | null,
|
|
definition: CodingPluginDefinition,
|
|
capabilityId: string,
|
|
operation: string,
|
|
): PluginCatalogOperation | null {
|
|
const plugin = catalog?.plugins.find(({ plugin_id }) => plugin_id === definition.id);
|
|
if (!plugin || plugin.status !== 'active'
|
|
|| !plugin.supported_contract_versions.includes(definition.contractVersion)) return null;
|
|
return plugin.capabilities
|
|
.find(({ capability_id }) => capability_id === capabilityId)
|
|
?.operations.find((candidate) => candidate.operation === operation) ?? null;
|
|
}
|
|
|
|
function policyIsBillingAvailable(policy: PluginCatalogOperation): boolean {
|
|
return policy.billing.mode !== 'platform_metered'
|
|
|| !('status' in policy.billing) || policy.billing.status !== 'billing_unavailable';
|
|
}
|
|
|
|
function unavailable(
|
|
pluginId: string,
|
|
code: PluginUnavailableReasonCode,
|
|
message: string,
|
|
): PluginUnavailableReason {
|
|
return { pluginId, code, message };
|
|
}
|
|
|
|
function validateBinding(value: AccountBinding | null): AccountBinding | null {
|
|
if (!value || typeof value.accountKey !== 'string' || value.accountKey.length === 0
|
|
|| value.accountKey.length > 512 || !Number.isSafeInteger(value.epoch) || value.epoch < 0) {
|
|
return null;
|
|
}
|
|
return { accountKey: value.accountKey, epoch: value.epoch };
|
|
}
|
|
|
|
/**
|
|
* A raw Skill ID is the public assignment key. Marketplace packages cannot
|
|
* shadow a core Skill or an already accepted package owner; the project
|
|
* assignment is retained, but the later package contributes no resources.
|
|
*/
|
|
function marketplaceSkillConflicts(records: readonly DefinitionRecord[]): ReadonlySet<string> {
|
|
const blocked = new Set<string>();
|
|
const owned = new Set<string>(CORE_CODING_SKILL_IDS);
|
|
for (const { definition } of records) {
|
|
if (definition.provenance.source === 'marketplace') continue;
|
|
for (const { id } of definition.skills) owned.add(id);
|
|
}
|
|
for (const { definition } of records) {
|
|
if (definition.provenance.source !== 'marketplace') continue;
|
|
const conflicts = definition.skills.some(({ id }) => owned.has(id));
|
|
if (conflicts) {
|
|
blocked.add(definition.id);
|
|
continue;
|
|
}
|
|
for (const { id } of definition.skills) owned.add(id);
|
|
}
|
|
return blocked;
|
|
}
|
|
|
|
/**
|
|
* Resolve a worker snapshot from separated Marketplace, Package Store,
|
|
* project, assignment, and policy state. This module deliberately performs
|
|
* no package execution and never trusts paths supplied by a package.
|
|
*/
|
|
export class EffectivePluginResolver {
|
|
private readonly cachedLibraries = new Map<string, MarketplaceLibrarySnapshot>();
|
|
|
|
constructor(private readonly options: EffectivePluginResolverOptions) {}
|
|
|
|
async resolve(input: EffectivePluginResolverInput): Promise<EffectivePluginSnapshot> {
|
|
const definitions = await this.definitionRecords();
|
|
const blockedMarketplacePlugins = marketplaceSkillConflicts(definitions);
|
|
const assigned = normalizeIds(input.assignedSkillIds);
|
|
|
|
const coreIds = new Set<string>(CORE_CODING_SKILL_IDS);
|
|
const effectiveSkillIds: CodingSkillId[] = assigned.filter((id) => coreIds.has(id));
|
|
const skillEntries: SkillEntry[] = effectiveSkillIds.map((id) => ({
|
|
id,
|
|
entryPath: `${id}/SKILL.md`,
|
|
}));
|
|
const pluginReleaseIds: string[] = [];
|
|
const toolDefinitions: CodingPluginToolDefinition[] = [];
|
|
const runtimePolicies: RuntimePolicy[] = [];
|
|
const unavailableReasons: PluginUnavailableReason[] = [];
|
|
|
|
// Child workers intentionally receive only core resources. Still validate
|
|
// assignment IDs above so malformed project configuration remains visible.
|
|
if (input.role === 'child') {
|
|
return freezeSnapshot({
|
|
accountSessionId: accountSessionId(this.options, validateBinding(this.options.getAccountBinding?.() ?? null)),
|
|
projectId: input.projectId,
|
|
pluginReleaseIds,
|
|
effectiveSkillIds,
|
|
skillEntries,
|
|
toolDefinitions,
|
|
runtimePolicies,
|
|
unavailableReasons,
|
|
});
|
|
}
|
|
|
|
const binding = validateBinding(this.options.getAccountBinding?.() ?? null);
|
|
const userDefinitions = definitions.filter(({ definition }) => definition.acquisitionMode === 'user_acquired');
|
|
const enabled = new Set(await this.enabledPluginIds(input.projectPath));
|
|
const requiresLibrary = userDefinitions.some(({ definition, installed }) => (
|
|
installed && enabled.has(definition.id)
|
|
&& definition.skills.some(({ id }) => assigned.includes(id))
|
|
));
|
|
const library = input.library !== undefined
|
|
? input.library
|
|
: await this.libraryFor(binding, requiresLibrary);
|
|
const libraryById = new Map((library?.items ?? []).map((entry) => [entry.pluginId, entry]));
|
|
let policyState: PluginPolicyClientState = this.options.policyClient?.getState() ?? EMPTY_POLICY_STATE;
|
|
|
|
const serverDefinitions = definitions.filter(({ definition }) => definition.requiresBackend);
|
|
const needsPolicyRefresh = policyState.status !== 'current'
|
|
&& serverDefinitions.some(({ definition, installed }) => (
|
|
installed && enabled.has(definition.id) && definition.skills.some(({ id }) => assigned.includes(id))
|
|
));
|
|
if (needsPolicyRefresh && this.options.policyClient) {
|
|
await this.options.policyClient.refresh();
|
|
policyState = this.options.policyClient.getState();
|
|
}
|
|
|
|
for (const { definition, installed, unavailableReason } of definitions) {
|
|
if (blockedMarketplacePlugins.has(definition.id)) continue;
|
|
const selectedSkills = definition.skills.filter(({ id }) => assigned.includes(id));
|
|
if (selectedSkills.length === 0) {
|
|
unavailableReasons.push(unavailable(definition.id, 'skill_unassigned', 'Plugin Skill is not assigned'));
|
|
continue;
|
|
}
|
|
|
|
if (!installed) {
|
|
unavailableReasons.push(unavailable(definition.id, 'release_not_installed', 'Plugin Release is not installed'));
|
|
continue;
|
|
}
|
|
if (unavailableReason) {
|
|
unavailableReasons.push(unavailable(
|
|
definition.id,
|
|
unavailableReason,
|
|
unavailableReason === 'client_incompatible'
|
|
? 'Plugin Release is incompatible with this MakeLore client'
|
|
: 'Plugin Release is unavailable',
|
|
));
|
|
continue;
|
|
}
|
|
if (!enabled.has(definition.id)) {
|
|
unavailableReasons.push(unavailable(definition.id, 'project_disabled', 'Plugin is not enabled for this project'));
|
|
continue;
|
|
}
|
|
if (definition.acquisitionMode === 'user_acquired') {
|
|
if (!binding) {
|
|
unavailableReasons.push(unavailable(definition.id, 'account_required', 'Marketplace account is required'));
|
|
continue;
|
|
}
|
|
if (!library || library.stale) {
|
|
unavailableReasons.push(unavailable(definition.id, 'library_unavailable', 'Marketplace Library is unavailable'));
|
|
continue;
|
|
}
|
|
const entry = libraryById.get(definition.id);
|
|
if (!entry || entry.acquisitionMode !== 'user_acquired' || entry.removedAt !== null) {
|
|
unavailableReasons.push(unavailable(definition.id, 'library_required', 'Plugin is not acquired in this Account Library'));
|
|
continue;
|
|
}
|
|
// Retired remains usable for an existing Library user. Suspension is
|
|
// a runtime gate and is intentionally independent from catalogStatus.
|
|
if (entry.runtimeStatus === 'suspended') {
|
|
unavailableReasons.push(unavailable(definition.id, 'runtime_suspended', 'Plugin runtime is suspended'));
|
|
continue;
|
|
}
|
|
if (!definition.releaseId) {
|
|
unavailableReasons.push(unavailable(definition.id, 'release_invalid', 'Installed Plugin Release is invalid'));
|
|
continue;
|
|
}
|
|
}
|
|
if (definition.requiresBackend) {
|
|
const pluginPolicy = policyState.catalog?.plugins.find(({ plugin_id }) => plugin_id === definition.id);
|
|
if (policyState.status !== 'current' || !policyState.catalog) {
|
|
unavailableReasons.push(unavailable(definition.id, 'policy_unavailable', 'Plugin runtime policy is unavailable'));
|
|
continue;
|
|
}
|
|
if (!pluginPolicy || !pluginPolicy.supported_contract_versions.includes(definition.contractVersion)) {
|
|
unavailableReasons.push(unavailable(definition.id, 'policy_unsupported', 'Plugin contract is not supported by runtime policy'));
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (definition.releaseId) pluginReleaseIds.push(definition.releaseId);
|
|
for (const skill of selectedSkills) {
|
|
effectiveSkillIds.push(skill.id);
|
|
skillEntries.push({
|
|
id: skill.id,
|
|
entryPath: skill.entryPath,
|
|
...(definition.provenance.source === 'marketplace'
|
|
? { packageRoot: definition.provenance.packageRoot }
|
|
: {}),
|
|
});
|
|
}
|
|
if (!definition.requiresBackend) continue;
|
|
for (const operation of definition.operations) {
|
|
const policy = policyOperation(
|
|
policyState.catalog,
|
|
definition,
|
|
operation.capabilityId,
|
|
operation.operation,
|
|
);
|
|
if (!policy) continue;
|
|
runtimePolicies.push({
|
|
pluginId: definition.id,
|
|
contractVersion: definition.contractVersion,
|
|
capabilityId: operation.capabilityId,
|
|
operation: operation.operation,
|
|
billing: clone(policy.billing),
|
|
});
|
|
const tool = operation.toolName
|
|
? definition.tools.find(({ name }) => name === operation.toolName)
|
|
: undefined;
|
|
if (!tool || !selectedSkills.some(({ grants }) => grants.includes(tool.capabilityId))) continue;
|
|
if (!policyIsBillingAvailable(policy)) {
|
|
unavailableReasons.push(unavailable(definition.id, 'billing_unavailable', 'Plugin billing is unavailable'));
|
|
continue;
|
|
}
|
|
toolDefinitions.push(clone(tool));
|
|
}
|
|
}
|
|
|
|
return freezeSnapshot({
|
|
accountSessionId: accountSessionId(this.options, binding),
|
|
projectId: input.projectId,
|
|
pluginReleaseIds: [...new Set(pluginReleaseIds)],
|
|
effectiveSkillIds: [...new Set(effectiveSkillIds)],
|
|
skillEntries,
|
|
toolDefinitions: [...new Map(toolDefinitions.map((tool) => [tool.name, tool])).values()],
|
|
runtimePolicies: [...new Map(runtimePolicies.map((policy) => [
|
|
`${policy.pluginId}\u0000${policy.capabilityId}\u0000${policy.operation}`,
|
|
policy,
|
|
])).values()],
|
|
unavailableReasons,
|
|
});
|
|
}
|
|
|
|
/** Alias kept explicit for callers that name the contract rather than the class. */
|
|
resolveEffectivePluginSnapshot(input: EffectivePluginResolverInput): Promise<EffectivePluginSnapshot> {
|
|
return this.resolve(input);
|
|
}
|
|
|
|
async getSkillSources(): Promise<readonly EffectivePluginSkillSource[]> {
|
|
const definitions = await this.definitionRecords();
|
|
const blockedMarketplacePlugins = marketplaceSkillConflicts(definitions);
|
|
// Bundled plugin Skills already have a trusted resource source supplied by
|
|
// composition. Returning their manifest-relative `packageRoot` here would
|
|
// replace that source with a cwd-relative path in the product projection;
|
|
// this seam is exclusively for immutable user-installed package roots.
|
|
return Object.freeze(definitions.flatMap(({ definition, installed, unavailableReason }) => (
|
|
!installed || unavailableReason || blockedMarketplacePlugins.has(definition.id)
|
|
|| definition.acquisitionMode !== 'user_acquired'
|
|
? []
|
|
: definition.skills.map((skill) => ({
|
|
id: skill.id,
|
|
pluginId: definition.id,
|
|
packageRoot: definition.provenance.packageRoot,
|
|
directory: path.join(definition.provenance.packageRoot, path.dirname(skill.entryPath)),
|
|
entryPath: path.basename(skill.entryPath),
|
|
}))
|
|
)));
|
|
}
|
|
|
|
getPolicyState(): PluginPolicyClientState {
|
|
return this.options.policyClient?.getState() ?? EMPTY_POLICY_STATE;
|
|
}
|
|
|
|
private async definitionRecords(): Promise<DefinitionRecord[]> {
|
|
const base = [
|
|
...(this.options.definitions ?? []),
|
|
...(this.options.getDefinitions ? await this.options.getDefinitions() : []),
|
|
];
|
|
const records = new Map<string, DefinitionRecord>();
|
|
for (const definition of base) {
|
|
if (!records.has(definition.id)) records.set(definition.id, { definition, installed: true });
|
|
}
|
|
for (const definition of this.options.installedDefinitions ?? []) {
|
|
records.set(definition.id, { definition, installed: true });
|
|
}
|
|
const installedIds = new Set<string>();
|
|
if (this.options.packageStore) {
|
|
const index = await this.options.packageStore.readInstalledIndex();
|
|
for (const record of index) installedIds.add(record.pluginId);
|
|
}
|
|
for (const definition of [...records.values()].map(({ definition }) => definition)) {
|
|
if (definition.acquisitionMode !== 'user_acquired') continue;
|
|
let installed = (this.options.installedDefinitions ?? []).some(({ id }) => id === definition.id);
|
|
let unavailableReason: PluginUnavailableReasonCode | undefined;
|
|
if (installedIds.has(definition.id)) {
|
|
const installedRelease = await this.options.packageStore?.getInstalled(definition.id);
|
|
installed = Boolean(installedRelease);
|
|
if (installedRelease?.unavailableReason === 'plugin_incompatible_client') {
|
|
unavailableReason = 'client_incompatible';
|
|
}
|
|
}
|
|
if (this.options.getInstalled) {
|
|
const installedRelease = await this.options.getInstalled(definition.id);
|
|
installed = Boolean(installedRelease);
|
|
if (installedRelease?.unavailableReason === 'plugin_incompatible_client') {
|
|
unavailableReason = 'client_incompatible';
|
|
}
|
|
}
|
|
if (!this.options.packageStore && !this.options.getInstalled
|
|
&& !(this.options.installedDefinitions ?? []).some(({ id }) => id === definition.id)) {
|
|
installed = false;
|
|
}
|
|
records.set(definition.id, { definition, installed, unavailableReason });
|
|
}
|
|
if (this.options.packageStore) {
|
|
for (const pluginId of installedIds) {
|
|
if (records.has(pluginId)) continue;
|
|
const installed = await this.options.packageStore.getInstalled(pluginId);
|
|
if (installed) records.set(pluginId, {
|
|
definition: installed.definition,
|
|
installed: true,
|
|
...(installed.unavailableReason === 'plugin_incompatible_client'
|
|
? { unavailableReason: 'client_incompatible' as const }
|
|
: {}),
|
|
});
|
|
}
|
|
}
|
|
return [...records.values()];
|
|
}
|
|
|
|
private async libraryFor(
|
|
binding: AccountBinding | null,
|
|
required: boolean,
|
|
): Promise<MarketplaceLibrarySnapshot | null> {
|
|
if (!required || !binding) return null;
|
|
const key = accountBindingKey(binding);
|
|
const cached = this.options.accountCache?.getLibrary(binding) ?? this.cachedLibraries.get(key);
|
|
if (cached && !cached.stale) this.cachedLibraries.set(key, clone(cached));
|
|
try {
|
|
const library = this.options.getLibrary
|
|
? await this.options.getLibrary(binding)
|
|
: this.options.marketplace
|
|
? await this.options.marketplace.readLibrary()
|
|
: cached ?? null;
|
|
if (library) this.cachedLibraries.set(key, clone(library));
|
|
return library ?? cached ?? null;
|
|
} catch {
|
|
return cached ?? null;
|
|
}
|
|
}
|
|
|
|
private async enabledPluginIds(projectPath: string): Promise<readonly string[]> {
|
|
if (this.options.getEnabledPluginIds) return await this.options.getEnabledPluginIds(projectPath);
|
|
if (this.options.projectPlugins) return await this.options.projectPlugins.getEnabledPluginIds(projectPath);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export function createEffectivePluginResolver(
|
|
options: EffectivePluginResolverOptions,
|
|
): EffectivePluginResolver {
|
|
return new EffectivePluginResolver(options);
|
|
}
|
|
|
|
export const createEffectiveResolver = createEffectivePluginResolver;
|
|
export const resolveEffectivePluginSnapshot = async (
|
|
options: EffectivePluginResolverOptions,
|
|
input: EffectivePluginResolverInput,
|
|
): Promise<EffectivePluginSnapshot> => await createEffectivePluginResolver(options).resolve(input);
|