From 28bd520095137ac9040d01c4b9cfc2b3fb1669bb Mon Sep 17 00:00:00 2001 From: kevinwang Date: Mon, 10 Feb 2025 08:34:35 -0500 Subject: [PATCH 01/25] feat: Added itemType parameter, it's like type but for items of arrays (sets default transformation and isElementEqual fns) --- package.json | 2 +- src/plan/plan.ts | 12 ++-- src/plugin/plugin.test.ts | 2 +- src/plugin/plugin.ts | 2 +- src/resource/parsed-resource-settings.test.ts | 8 +-- src/resource/parsed-resource-settings.ts | 11 ++-- src/resource/resource-controller.ts | 35 +++++++++-- src/resource/resource-settings.test.ts | 59 ++++++++++++++++++- src/resource/resource-settings.ts | 56 ++++++++++++++---- .../stateful-parameter-controller.ts | 5 +- 10 files changed, 152 insertions(+), 40 deletions(-) diff --git a/package.json b/package.json index 86230df..d20205e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codify-plugin-lib", - "version": "1.0.131", + "version": "1.0.132", "description": "Library plugin library", "main": "dist/index.js", "typings": "dist/index.d.ts", diff --git a/src/plan/plan.ts b/src/plan/plan.ts index 323ccaa..4213a07 100644 --- a/src/plan/plan.ts +++ b/src/plan/plan.ts @@ -384,15 +384,15 @@ export class Plan { const defaultFilterMethod = ((desired: any[], current: any[]) => { const result = []; - for (let counter = desiredCopy.length - 1; counter >= 0; counter--) { - const idx = currentCopy.findIndex((e2) => matcher(desiredCopy[counter], e2)) + for (let counter = desired.length - 1; counter >= 0; counter--) { + const idx = currentCopy.findIndex((e2) => matcher(desired[counter], e2)) if (idx === -1) { continue; } - desiredCopy.splice(counter, 1) - const [element] = currentCopy.splice(idx, 1) + desired.splice(counter, 1) + const [element] = current.splice(idx, 1) result.push(element) } @@ -413,9 +413,7 @@ export class Plan { return this.changeSet.operation !== ResourceOperation.NOOP; } - /** - * Convert the plan to a JSON response object - */ + /** Convert the plan to a JSON response object */ toResponse(): PlanResponseData { return { planId: this.id, diff --git a/src/plugin/plugin.test.ts b/src/plugin/plugin.test.ts index 046839a..476a173 100644 --- a/src/plugin/plugin.test.ts +++ b/src/plugin/plugin.test.ts @@ -194,7 +194,7 @@ describe('Plugin tests', () => { return { id: 'typeId', schema, - import: { + importAndDestroy: { requiredParameters: [] } } diff --git a/src/plugin/plugin.ts b/src/plugin/plugin.ts index 1ab7302..1481538 100644 --- a/src/plugin/plugin.ts +++ b/src/plugin/plugin.ts @@ -67,7 +67,7 @@ export class Plugin { const schema = resource.settings.schema as JSONSchemaType | undefined; const requiredPropertyNames = ( - resource.settings.import?.requiredParameters + resource.settings.importAndDestroy?.requiredParameters ?? schema?.required ?? null ) as null | string[]; diff --git a/src/resource/parsed-resource-settings.test.ts b/src/resource/parsed-resource-settings.test.ts index 3bc87bc..f2671ab 100644 --- a/src/resource/parsed-resource-settings.test.ts +++ b/src/resource/parsed-resource-settings.test.ts @@ -81,7 +81,7 @@ describe('Resource options parser tests', () => { const option: ResourceSettings = { id: 'typeId', schema, - import: { + importAndDestroy: { requiredParameters: ['import-error'] } } @@ -104,7 +104,7 @@ describe('Resource options parser tests', () => { const option: ResourceSettings = { id: 'typeId', schema, - import: { + importAndDestroy: { refreshKeys: ['import-error'] } } @@ -127,7 +127,7 @@ describe('Resource options parser tests', () => { const option: ResourceSettings = { id: 'typeId', schema, - import: { + importAndDestroy: { refreshKeys: ['remote'], } } @@ -150,7 +150,7 @@ describe('Resource options parser tests', () => { const option: ResourceSettings = { id: 'typeId', schema, - import: { + importAndDestroy: { defaultRefreshValues: { repository: 'abc' } diff --git a/src/resource/parsed-resource-settings.ts b/src/resource/parsed-resource-settings.ts index f9f4a63..d440c9b 100644 --- a/src/resource/parsed-resource-settings.ts +++ b/src/resource/parsed-resource-settings.ts @@ -6,8 +6,8 @@ import { ArrayParameterSetting, DefaultParameterSetting, ParameterSetting, + resolveElementEqualsFn, resolveEqualsFn, - resolveFnFromEqualsFnOrString, resolveParameterTransformFn, ResourceSettings, StatefulParameterSetting @@ -95,8 +95,7 @@ export class ParsedResourceSettings implements Re if (v.type === 'array') { const parsed = { ...v, - isElementEqual: resolveFnFromEqualsFnOrString((v as ArrayParameterSetting).isElementEqual) - ?? ((a: unknown, b: unknown) => a === b), + isElementEqual: resolveElementEqualsFn(v as ArrayParameterSetting) } return [k, parsed as ParsedArrayParameterSetting]; @@ -186,7 +185,7 @@ export class ParsedResourceSettings implements Re } const schema = this.settings.schema as JSONSchemaType; - if (!this.settings.import && (schema?.oneOf + if (!this.settings.importAndDestroy && (schema?.oneOf && Array.isArray(schema.oneOf) && schema.oneOf.some((s) => s.required) ) @@ -214,8 +213,8 @@ export class ParsedResourceSettings implements Re ) } - if (this.settings.import) { - const { requiredParameters, refreshKeys, defaultRefreshValues } = this.settings.import; + if (this.settings.importAndDestroy) { + const { requiredParameters, refreshKeys, defaultRefreshValues } = this.settings.importAndDestroy; const requiredParametersNotInSchema = requiredParameters ?.filter( diff --git a/src/resource/resource-controller.ts b/src/resource/resource-controller.ts index c9a636c..d0fe8ad 100644 --- a/src/resource/resource-controller.ts +++ b/src/resource/resource-controller.ts @@ -158,6 +158,33 @@ export class ResourceController { }) } + async planDestroy( + core: ResourceConfig, + parameters: Partial + ): Promise> { + this.addDefaultValues(parameters); + await this.applyTransformParameters(parameters); + + // Use refresh parameters if specified, otherwise try to refresh as many parameters as possible here + const parametersToRefresh = this.settings.importAndDestroy?.refreshKeys + ? { + ...Object.fromEntries( + this.settings.importAndDestroy?.refreshKeys.map((k) => [k, null]) + ), + ...this.settings.importAndDestroy?.defaultRefreshValues, + ...parameters, + } + : { + ...Object.fromEntries( + this.getAllParameterKeys().map((k) => [k, null]) + ), + ...this.settings.importAndDestroy?.defaultRefreshValues, + ...parameters, + }; + + return this.plan(core, null, parametersToRefresh, true); + } + async apply(plan: Plan): Promise { if (plan.getResourceType() !== this.typeId) { throw new Error(`Internal error: Plan set to wrong resource during apply. Expected ${this.typeId} but got: ${plan.getResourceType()}`); @@ -191,19 +218,19 @@ export class ResourceController { await this.applyTransformParameters(parameters); // Use refresh parameters if specified, otherwise try to refresh as many parameters as possible here - const parametersToRefresh = this.settings.import?.refreshKeys + const parametersToRefresh = this.settings.importAndDestroy?.refreshKeys ? { ...Object.fromEntries( - this.settings.import?.refreshKeys.map((k) => [k, null]) + this.settings.importAndDestroy?.refreshKeys.map((k) => [k, null]) ), - ...this.settings.import?.defaultRefreshValues, + ...this.settings.importAndDestroy?.defaultRefreshValues, ...parameters, } : { ...Object.fromEntries( this.getAllParameterKeys().map((k) => [k, null]) ), - ...this.settings.import?.defaultRefreshValues, + ...this.settings.importAndDestroy?.defaultRefreshValues, ...parameters, }; diff --git a/src/resource/resource-settings.test.ts b/src/resource/resource-settings.test.ts index b5fa66d..edfd3f6 100644 --- a/src/resource/resource-settings.test.ts +++ b/src/resource/resource-settings.test.ts @@ -637,7 +637,7 @@ describe('Resource parameter tests', () => { getSettings(): ResourceSettings { return { id: 'resourceType', - import: { + importAndDestroy: { requiredParameters: [ 'propA', 'propB', @@ -723,7 +723,7 @@ describe('Resource parameter tests', () => { getSettings(): ResourceSettings { return { id: 'resourceType', - import: { + importAndDestroy: { requiredParameters: ['propA'], refreshKeys: ['propB', 'propA'], defaultRefreshValues: { @@ -969,4 +969,59 @@ describe('Resource parameter tests', () => { ); }) + + it('Supports equality check for itemType', async () => { + const resource = new class extends TestResource { + getSettings(): ResourceSettings { + return { + id: 'resourceType', + parameterSettings: { + propA: { type: 'array', itemType: 'version' } + } + } + } + + async refresh(parameters: Partial): Promise | null> { + return { + propA: ['10.0.0'] + } + } + }; + + const controller = new ResourceController(resource); + + const result = await controller.plan({ type: 'resourceType' }, { propA: ['10.0'] }, null, false); + expect(result.changeSet).toMatchObject({ + operation: ResourceOperation.NOOP, + }) + }) + + it('Supports transformations for itemType', async () => { + const home = os.homedir() + const testPath = path.join(home, 'test/folder'); + + const resource = new class extends TestResource { + getSettings(): ResourceSettings { + return { + id: 'resourceType', + parameterSettings: { + propA: { type: 'array', itemType: 'directory' } + } + } + } + + async refresh(parameters: Partial): Promise | null> { + return { + propA: [testPath] + } + } + }; + + const controller = new ResourceController(resource); + + const result = await controller.plan({ type: 'resourceType' }, { propA: ['~/test/folder'] }, null, false); + expect(result.changeSet).toMatchObject({ + operation: ResourceOperation.NOOP, + }) + }) }) diff --git a/src/resource/resource-settings.ts b/src/resource/resource-settings.ts index a033d16..33f6049 100644 --- a/src/resource/resource-settings.ts +++ b/src/resource/resource-settings.ts @@ -67,9 +67,9 @@ export interface ResourceSettings { inputTransformation?: (desired: Partial) => Promise | unknown; /** - * Customize the import behavior of the resource. By default, codify import will call `refresh()` with - * every parameter set to null and return the result of the refresh as the imported config. It looks for required parameters - * in the schema and will prompt the user for these values before performing the import. + * Customize the import and destory behavior of the resource. By default, codify import and codify destroy will call + * `refresh()` with every parameter set to null and return the result of the refresh as the imported config. It looks for required parameters + * in the schema and will prompt the user for these values before performing the import or destroy. * * Example:
* Resource `alias` with parameters @@ -85,7 +85,7 @@ export interface ResourceSettings { * { type: 'alias', alias: 'user-input', value: 'git push' } * ``` */ - import?: { + importAndDestroy?: { /** * Customize the required parameters needed to import this resource. By default, the `requiredParameters` are taken @@ -96,7 +96,7 @@ export interface ResourceSettings { * the required parameters change the behaviour of the refresh (for example for the `alias` resource, the `alias` parmaeter * chooses which alias the resource is managing). * - * See {@link import} for more information on how importing works. + * See {@link importAndDestroy} for more information on how importing works. */ requiredParameters?: Array>; @@ -107,14 +107,14 @@ export interface ResourceSettings { * By default all parameters (except for {@link requiredParameters }) are passed in with the value `null`. The passed * in value can be customized using {@link defaultRefreshValues} * - * See {@link import} for more information on how importing works. + * See {@link importAndDestroy} for more information on how importing works. */ refreshKeys?: Array>; /** * Customize the value that is passed into refresh when importing. This must only contain keys found in {@link refreshKeys}. * - * See {@link import} for more information on how importing works. + * See {@link importAndDestroy} for more information on how importing works. */ defaultRefreshValues?: Partial } @@ -233,6 +233,12 @@ export interface ArrayParameterSetting extends DefaultParameterSetting { * Defaults to true. */ filterInStatelessMode?: ((desired: any[], current: any[]) => any[]) | boolean, + + /** + * The type of the array item. See {@link ParameterSettingType} for the available options. This value + * is mainly used to determine the equality method when performing diffing. + */ + itemType?: ParameterSettingType, } /** @@ -273,10 +279,7 @@ export function resolveEqualsFn(parameter: ParameterSetting): (desired: unknown, const isEqual = resolveFnFromEqualsFnOrString(parameter.isEqual); if (parameter.type === 'array') { - const arrayParameter = parameter as ArrayParameterSetting; - const isElementEqual = resolveFnFromEqualsFnOrString(arrayParameter.isElementEqual); - - return isEqual ?? areArraysEqual.bind(areArraysEqual, isElementEqual) + return isEqual ?? areArraysEqual.bind(areArraysEqual, resolveElementEqualsFn(parameter as ArrayParameterSetting)) } if (parameter.type === 'stateful') { @@ -286,6 +289,21 @@ export function resolveEqualsFn(parameter: ParameterSetting): (desired: unknown, return isEqual ?? ParameterEqualsDefaults[parameter.type as ParameterSettingType] ?? (((a, b) => a === b)); } +export function resolveElementEqualsFn(parameter: ArrayParameterSetting): (desired: unknown, current: unknown) => boolean { + if (parameter.isElementEqual) { + const elementEq = resolveFnFromEqualsFnOrString(parameter.isElementEqual); + if (elementEq) { + return elementEq; + } + } + + if (parameter.itemType && ParameterEqualsDefaults[parameter.itemType]) { + return ParameterEqualsDefaults[parameter.itemType]! + } + + return (a, b) => a === b; +} + // This resolves the fn if it is a string. // A string can be specified to use a default equals method export function resolveFnFromEqualsFnOrString( @@ -312,10 +330,26 @@ const ParameterTransformationDefaults: Partial arr.map((i) => (parameter as ArrayParameterSetting).itemType ? ParameterTransformationDefaults[]) } export function resolveParameterTransformFn( parameter: ParameterSetting ): ((input: any, parameter: ParameterSetting) => Promise | any) | undefined { + + if (parameter.type === 'array' + && (parameter as ArrayParameterSetting).itemType + && ParameterTransformationDefaults[(parameter as ArrayParameterSetting).itemType!] + && !parameter.inputTransformation + ) { + const itemType = (parameter as ArrayParameterSetting).itemType!; + const itemTransformation = ParameterTransformationDefaults[itemType]!; + + return (input: unknown[], parameter) => { + return input.map((i) => itemTransformation(i, parameter)) + } + } + return parameter.inputTransformation ?? ParameterTransformationDefaults[parameter.type as ParameterSettingType] ?? undefined; } diff --git a/src/stateful-parameter/stateful-parameter-controller.ts b/src/stateful-parameter/stateful-parameter-controller.ts index 17a7543..171d2a2 100644 --- a/src/stateful-parameter/stateful-parameter-controller.ts +++ b/src/stateful-parameter/stateful-parameter-controller.ts @@ -5,8 +5,8 @@ import { ParsedArrayParameterSetting, ParsedParameterSetting, } from '../resourc import { ArrayParameterSetting, ParameterSetting, + resolveElementEqualsFn, resolveEqualsFn, - resolveFnFromEqualsFnOrString } from '../resource/resource-settings.js'; import { ArrayStatefulParameter, StatefulParameter } from './stateful-parameter.js'; @@ -31,8 +31,7 @@ export class StatefulParameterController a === b) + isElementEqual: resolveElementEqualsFn(this.settings as ArrayParameterSetting) } as ParsedParameterSetting : { ...this.settings, isEqual: resolveEqualsFn(this.settings), From 9bf8e80f6b3ed0f88620285a030b45d24b7152fd Mon Sep 17 00:00:00 2001 From: kevinwang Date: Mon, 10 Feb 2025 09:33:11 -0500 Subject: [PATCH 02/25] feat: Changed parameter inputTransformations to have to: and from: so they can be mapped back --- src/resource/parsed-resource-settings.ts | 2 +- src/resource/resource-settings.test.ts | 42 +++++++++++++------ src/resource/resource-settings.ts | 52 ++++++++++++++++-------- src/utils/utils.ts | 4 ++ 4 files changed, 70 insertions(+), 30 deletions(-) diff --git a/src/resource/parsed-resource-settings.ts b/src/resource/parsed-resource-settings.ts index d440c9b..b91adbf 100644 --- a/src/resource/parsed-resource-settings.ts +++ b/src/resource/parsed-resource-settings.ts @@ -141,7 +141,7 @@ export class ParsedResourceSettings implements Re return Object.fromEntries( Object.entries(this.settings.parameterSettings) .filter(([_, v]) => resolveParameterTransformFn(v!) !== undefined) - .map(([k, v]) => [k, resolveParameterTransformFn(v!)] as const) + .map(([k, v]) => [k, resolveParameterTransformFn(v!)!.to] as const) ) as Record unknown>; }); } diff --git a/src/resource/resource-settings.test.ts b/src/resource/resource-settings.test.ts index edfd3f6..6b8ea9c 100644 --- a/src/resource/resource-settings.test.ts +++ b/src/resource/resource-settings.test.ts @@ -852,16 +852,25 @@ describe('Resource parameter tests', () => { parameterSettings: { propD: { type: 'array', - inputTransformation: (hosts: Record[]) => hosts.map((h) => Object.fromEntries( + inputTransformation: { + to: (hosts: Record[]) => hosts.map((h) => Object.fromEntries( + Object.entries(h) + .map(([k, v]) => [ + k, + typeof v === 'boolean' + ? (v ? 'yes' : 'no') // The file takes 'yes' or 'no' instead of booleans + : v, + ]) + ) + ), + from: (hosts: Record[]) => hosts.map((h) => Object.fromEntries( Object.entries(h) .map(([k, v]) => [ k, - typeof v === 'boolean' - ? (v ? 'yes' : 'no') // The file takes 'yes' or 'no' instead of booleans - : v, + v === 'yes', ]) - ) - ) + )) + } } } } @@ -909,16 +918,25 @@ describe('Resource parameter tests', () => { getSettings(): any { return { type: 'array', - inputTransformation: (hosts: Record[]) => hosts.map((h) => Object.fromEntries( + inputTransformation: { + to: (hosts: Record[]) => hosts.map((h) => Object.fromEntries( + Object.entries(h) + .map(([k, v]) => [ + k, + typeof v === 'boolean' + ? (v ? 'yes' : 'no') // The file takes 'yes' or 'no' instead of booleans + : v, + ]) + ) + ), + from: (hosts: Record[]) => hosts.map((h) => Object.fromEntries( Object.entries(h) .map(([k, v]) => [ k, - typeof v === 'boolean' - ? (v ? 'yes' : 'no') // The file takes 'yes' or 'no' instead of booleans - : v, + v === 'yes', ]) - ) - ) + )) + } } } diff --git a/src/resource/resource-settings.ts b/src/resource/resource-settings.ts index 33f6049..61feee0 100644 --- a/src/resource/resource-settings.ts +++ b/src/resource/resource-settings.ts @@ -3,7 +3,12 @@ import isObjectsEqual from 'lodash.isequal' import path from 'node:path'; import { ArrayStatefulParameter, StatefulParameter } from '../stateful-parameter/stateful-parameter.js'; -import { areArraysEqual, untildify } from '../utils/utils.js'; +import { areArraysEqual, tildify, untildify } from '../utils/utils.js'; + +export interface InputTransformation { + to: (input: any) => Promise | any; + from: (current: any) => Promise | any; +} /** * The configuration and settings for a resource. @@ -166,12 +171,13 @@ export interface DefaultParameterSetting { default?: unknown; /** - * A transformation of the input value for this parameter. This transformation is only applied to the desired parameter - * value supplied by the user. + * A transformation of the input value for this parameter. Two transformations need to be provided: to (from desired to + * the internal type), and from (from the internal type back to desired). All transformations need to be bi-directional + * to support imports properly * * @param input The original parameter value from the desired config. */ - inputTransformation?: (input: any) => Promise | any; + inputTransformation?: InputTransformation; /** * Customize the equality comparison for a parameter. This is used in the diffing algorithm for generating the plan. @@ -321,22 +327,29 @@ export function resolveFnFromEqualsFnOrString( return fnOrString as ((a: unknown, b: unknown) => boolean) | undefined; } -const ParameterTransformationDefaults: Partial Promise | any>> = { - 'directory': (a: unknown) => path.resolve(untildify(String(a))), - 'stateful': (a: unknown, b: ParameterSetting) => { - const sp = b as StatefulParameterSetting; - return (sp.definition?.getSettings()?.inputTransformation) - ? (sp.definition.getSettings().inputTransformation!(a)) - : a; +const ParameterTransformationDefaults: Partial> = { + 'directory': { + to: (a: unknown) => path.resolve(untildify(String(a))), + from: (a: unknown) => tildify(String(a)), }, - 'string': String, - // TODO: Add a array parameter itemType parameter - // 'array': (arr: unknown[]) => arr.map((i) => (parameter as ArrayParameterSetting).itemType ? ParameterTransformationDefaults[]) + 'string': { + to: String, + from: String, + } } export function resolveParameterTransformFn( parameter: ParameterSetting -): ((input: any, parameter: ParameterSetting) => Promise | any) | undefined { +): InputTransformation | undefined { + + if (parameter.type === 'stateful' && !parameter.inputTransformation) { + const sp = (parameter as StatefulParameterSetting).definition.getSettings(); + if (sp.inputTransformation) { + return (parameter as StatefulParameterSetting).definition?.getSettings()?.inputTransformation + } + + return sp.type ? ParameterTransformationDefaults[sp.type] : undefined; + } if (parameter.type === 'array' && (parameter as ArrayParameterSetting).itemType @@ -346,8 +359,13 @@ export function resolveParameterTransformFn( const itemType = (parameter as ArrayParameterSetting).itemType!; const itemTransformation = ParameterTransformationDefaults[itemType]!; - return (input: unknown[], parameter) => { - return input.map((i) => itemTransformation(i, parameter)) + return { + to(input: unknown[]) { + return input.map((i) => itemTransformation.to(i)) + }, + from(input: unknown[]) { + return input.map((i) => itemTransformation.from(i)) + } } } diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 2485dc8..4b0314a 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -33,6 +33,10 @@ export function untildify(pathWithTilde: string) { return homeDirectory ? pathWithTilde.replace(/^~(?=$|\/|\\)/, homeDirectory) : pathWithTilde; } +export function tildify(pathWithTilde: string) { + return homeDirectory ? pathWithTilde.replace(homeDirectory, '~') : pathWithTilde; +} + export function areArraysEqual( isElementEqual: ((desired: unknown, current: unknown) => boolean) | undefined, desired: unknown, From 93c041b98b3f3598dc085dc127ad0a0de4a72f51 Mon Sep 17 00:00:00 2001 From: kevinwang Date: Sat, 15 Feb 2025 13:27:01 -0500 Subject: [PATCH 03/25] feat: Added required parameters as a default matcher for when multiple resources are returned. Updated vitest --- package-lock.json | 1228 +++++++++------------- package.json | 4 +- src/plan/plan.test.ts | 114 ++ src/plan/plan.ts | 35 +- src/plugin/plugin.test.ts | 3 +- src/resource/parsed-resource-settings.ts | 8 +- src/resource/resource-settings.ts | 18 +- 7 files changed, 641 insertions(+), 769 deletions(-) diff --git a/package-lock.json b/package-lock.json index df2a910..9244da3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "codify-plugin-lib", - "version": "1.0.131", + "version": "1.0.133", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "codify-plugin-lib", - "version": "1.0.131", + "version": "1.0.133", "license": "ISC", "dependencies": { "@homebridge/node-pty-prebuilt-multiarch": "^0.12.0-beta.5", @@ -38,7 +38,7 @@ "ts-node": "^10.9.1", "tsc-watch": "^6.0.4", "typescript": "^5", - "vitest": "^1.4.0", + "vitest": "^3.0.5", "vitest-mock-extended": "^1.3.1" }, "engines": { @@ -166,9 +166,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", + "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", "cpu": [ "ppc64" ], @@ -178,13 +178,13 @@ "aix" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", + "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", "cpu": [ "arm" ], @@ -194,13 +194,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", + "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", "cpu": [ "arm64" ], @@ -210,13 +210,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", + "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", "cpu": [ "x64" ], @@ -226,13 +226,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", + "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", "cpu": [ "arm64" ], @@ -242,13 +242,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", + "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", "cpu": [ "x64" ], @@ -258,13 +258,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", + "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", "cpu": [ "arm64" ], @@ -274,13 +274,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", + "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", "cpu": [ "x64" ], @@ -290,13 +290,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", + "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", "cpu": [ "arm" ], @@ -306,13 +306,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", + "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", "cpu": [ "arm64" ], @@ -322,13 +322,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", + "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", "cpu": [ "ia32" ], @@ -338,13 +338,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", + "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", "cpu": [ "loong64" ], @@ -354,13 +354,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", + "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", "cpu": [ "mips64el" ], @@ -370,13 +370,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", + "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", "cpu": [ "ppc64" ], @@ -386,13 +386,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", + "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", "cpu": [ "riscv64" ], @@ -402,13 +402,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", + "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", "cpu": [ "s390x" ], @@ -418,13 +418,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", + "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", "cpu": [ "x64" ], @@ -434,13 +434,29 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", + "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", + "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", "cpu": [ "x64" ], @@ -450,13 +466,29 @@ "netbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", + "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", + "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", "cpu": [ "x64" ], @@ -466,13 +498,13 @@ "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", + "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", "cpu": [ "x64" ], @@ -482,13 +514,13 @@ "sunos" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", + "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", "cpu": [ "arm64" ], @@ -498,13 +530,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", + "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", "cpu": [ "ia32" ], @@ -514,13 +546,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", + "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", "cpu": [ "x64" ], @@ -530,7 +562,7 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@eslint-community/eslint-utils": { @@ -719,18 +751,6 @@ "deprecated": "Use @eslint/object-schema instead", "dev": true }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -741,9 +761,9 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", "dev": true }, "node_modules/@jridgewell/trace-mapping": { @@ -952,9 +972,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz", - "integrity": "sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ==", + "version": "4.34.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.7.tgz", + "integrity": "sha512-l6CtzHYo8D2TQ3J7qJNpp3Q1Iye56ssIAtqbM2H8axxCEEwvN7o8Ze9PuIapbxFL3OHrJU2JBX6FIIVnP/rYyw==", "cpu": [ "arm" ], @@ -965,9 +985,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.0.tgz", - "integrity": "sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA==", + "version": "4.34.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.7.tgz", + "integrity": "sha512-KvyJpFUueUnSp53zhAa293QBYqwm94TgYTIfXyOTtidhm5V0LbLCJQRGkQClYiX3FXDQGSvPxOTD/6rPStMMDg==", "cpu": [ "arm64" ], @@ -978,9 +998,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.0.tgz", - "integrity": "sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w==", + "version": "4.34.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.7.tgz", + "integrity": "sha512-jq87CjmgL9YIKvs8ybtIC98s/M3HdbqXhllcy9EdLV0yMg1DpxES2gr65nNy7ObNo/vZ/MrOTxt0bE5LinL6mA==", "cpu": [ "arm64" ], @@ -991,9 +1011,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.0.tgz", - "integrity": "sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA==", + "version": "4.34.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.7.tgz", + "integrity": "sha512-rSI/m8OxBjsdnMMg0WEetu/w+LhLAcCDEiL66lmMX4R3oaml3eXz3Dxfvrxs1FbzPbJMaItQiksyMfv1hoIxnA==", "cpu": [ "x64" ], @@ -1003,10 +1023,36 @@ "darwin" ] }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.34.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.7.tgz", + "integrity": "sha512-oIoJRy3ZrdsXpFuWDtzsOOa/E/RbRWXVokpVrNnkS7npz8GEG++E1gYbzhYxhxHbO2om1T26BZjVmdIoyN2WtA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.34.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.7.tgz", + "integrity": "sha512-X++QSLm4NZfZ3VXGVwyHdRf58IBbCu9ammgJxuWZYLX0du6kZvdNqPwrjvDfwmi6wFdvfZ/s6K7ia0E5kI7m8Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.0.tgz", - "integrity": "sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA==", + "version": "4.34.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.7.tgz", + "integrity": "sha512-Z0TzhrsNqukTz3ISzrvyshQpFnFRfLunYiXxlCRvcrb3nvC5rVKI+ZXPFG/Aa4jhQa1gHgH3A0exHaRRN4VmdQ==", "cpu": [ "arm" ], @@ -1017,9 +1063,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.0.tgz", - "integrity": "sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A==", + "version": "4.34.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.7.tgz", + "integrity": "sha512-nkznpyXekFAbvFBKBy4nNppSgneB1wwG1yx/hujN3wRnhnkrYVugMTCBXED4+Ni6thoWfQuHNYbFjgGH0MBXtw==", "cpu": [ "arm" ], @@ -1030,9 +1076,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.0.tgz", - "integrity": "sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw==", + "version": "4.34.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.7.tgz", + "integrity": "sha512-KCjlUkcKs6PjOcxolqrXglBDcfCuUCTVlX5BgzgoJHw+1rWH1MCkETLkLe5iLLS9dP5gKC7mp3y6x8c1oGBUtA==", "cpu": [ "arm64" ], @@ -1043,9 +1089,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.0.tgz", - "integrity": "sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ==", + "version": "4.34.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.7.tgz", + "integrity": "sha512-uFLJFz6+utmpbR313TTx+NpPuAXbPz4BhTQzgaP0tozlLnGnQ6rCo6tLwaSa6b7l6gRErjLicXQ1iPiXzYotjw==", "cpu": [ "arm64" ], @@ -1055,10 +1101,23 @@ "linux" ] }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.34.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.7.tgz", + "integrity": "sha512-ws8pc68UcJJqCpneDFepnwlsMUFoWvPbWXT/XUrJ7rWUL9vLoIN3GAasgG+nCvq8xrE3pIrd+qLX/jotcLy0Qw==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.0.tgz", - "integrity": "sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA==", + "version": "4.34.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.7.tgz", + "integrity": "sha512-vrDk9JDa/BFkxcS2PbWpr0C/LiiSLxFbNOBgfbW6P8TBe9PPHx9Wqbvx2xgNi1TOAyQHQJ7RZFqBiEohm79r0w==", "cpu": [ "ppc64" ], @@ -1069,9 +1128,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.0.tgz", - "integrity": "sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg==", + "version": "4.34.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.7.tgz", + "integrity": "sha512-rB+ejFyjtmSo+g/a4eovDD1lHWHVqizN8P0Hm0RElkINpS0XOdpaXloqM4FBkF9ZWEzg6bezymbpLmeMldfLTw==", "cpu": [ "riscv64" ], @@ -1082,9 +1141,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.0.tgz", - "integrity": "sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg==", + "version": "4.34.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.7.tgz", + "integrity": "sha512-nNXNjo4As6dNqRn7OrsnHzwTgtypfRA3u3AKr0B3sOOo+HkedIbn8ZtFnB+4XyKJojIfqDKmbIzO1QydQ8c+Pw==", "cpu": [ "s390x" ], @@ -1095,9 +1154,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz", - "integrity": "sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==", + "version": "4.34.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.7.tgz", + "integrity": "sha512-9kPVf9ahnpOMSGlCxXGv980wXD0zRR3wyk8+33/MXQIpQEOpaNe7dEHm5LMfyRZRNt9lMEQuH0jUKj15MkM7QA==", "cpu": [ "x64" ], @@ -1108,9 +1167,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.0.tgz", - "integrity": "sha512-LKaqQL9osY/ir2geuLVvRRs+utWUNilzdE90TpyoX0eNqPzWjRm14oMEE+YLve4k/NAqCdPkGYDaDF5Sw+xBfg==", + "version": "4.34.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.7.tgz", + "integrity": "sha512-7wJPXRWTTPtTFDFezA8sle/1sdgxDjuMoRXEKtx97ViRxGGkVQYovem+Q8Pr/2HxiHp74SSRG+o6R0Yq0shPwQ==", "cpu": [ "x64" ], @@ -1121,9 +1180,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.0.tgz", - "integrity": "sha512-7J6TkZQFGo9qBKH0pk2cEVSRhJbL6MtfWxth7Y5YmZs57Pi+4x6c2dStAUvaQkHQLnEQv1jzBUW43GvZW8OFqA==", + "version": "4.34.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.7.tgz", + "integrity": "sha512-MN7aaBC7mAjsiMEZcsJvwNsQVNZShgES/9SzWp1HC9Yjqb5OpexYnRjF7RmE4itbeesHMYYQiAtUAQaSKs2Rfw==", "cpu": [ "arm64" ], @@ -1134,9 +1193,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.0.tgz", - "integrity": "sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg==", + "version": "4.34.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.7.tgz", + "integrity": "sha512-aeawEKYswsFu1LhDM9RIgToobquzdtSc4jSVqHV8uApz4FVvhFl/mKh92wc8WpFc6aYCothV/03UjY6y7yLgbg==", "cpu": [ "ia32" ], @@ -1147,9 +1206,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.0.tgz", - "integrity": "sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g==", + "version": "4.34.7", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.7.tgz", + "integrity": "sha512-4ZedScpxxIrVO7otcZ8kCX1mZArtH2Wfj3uFCxRJ9NO80gg1XV0U/b2f/MKaGwj2X3QopHfoWiDQ917FRpwY3w==", "cpu": [ "x64" ], @@ -1159,12 +1218,6 @@ "win32" ] }, - "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true - }, "node_modules/@sinonjs/commons": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", @@ -1249,9 +1302,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "dev": true }, "node_modules/@types/json-schema": { @@ -1544,179 +1597,111 @@ "dev": true }, "node_modules/@vitest/expect": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.0.tgz", - "integrity": "sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.5.tgz", + "integrity": "sha512-nNIOqupgZ4v5jWuQx2DSlHLEs7Q4Oh/7AYwNyE+k0UQzG7tSmjPXShUikn1mpNGzYEN2jJbTvLejwShMitovBA==", "dev": true, "dependencies": { - "@vitest/spy": "1.6.0", - "@vitest/utils": "1.6.0", - "chai": "^4.3.10" + "@vitest/spy": "3.0.5", + "@vitest/utils": "3.0.5", + "chai": "^5.1.2", + "tinyrainbow": "^2.0.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/expect/node_modules/assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/@vitest/expect/node_modules/chai": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", - "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==", + "node_modules/@vitest/mocker": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.5.tgz", + "integrity": "sha512-CLPNBFBIE7x6aEGbIjaQAX03ZZlBMaWwAjBdMkIf/cAn6xzLTiM3zYqO/WAbieEjsAZir6tO71mzeHZoodThvw==", "dev": true, "dependencies": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.3", - "deep-eql": "^4.1.3", - "get-func-name": "^2.0.2", - "loupe": "^2.3.6", - "pathval": "^1.1.1", - "type-detect": "^4.0.8" + "@vitest/spy": "3.0.5", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@vitest/expect/node_modules/check-error": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", - "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", - "dev": true, - "dependencies": { - "get-func-name": "^2.0.2" + "funding": { + "url": "https://opencollective.com/vitest" }, - "engines": { - "node": "*" - } - }, - "node_modules/@vitest/expect/node_modules/deep-eql": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", - "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", - "dev": true, - "dependencies": { - "type-detect": "^4.0.0" + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0" }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@vitest/expect/node_modules/loupe": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", - "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", - "dev": true, - "dependencies": { - "get-func-name": "^2.0.1" - } - }, - "node_modules/@vitest/expect/node_modules/pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", - "dev": true, - "engines": { - "node": "*" + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } } }, - "node_modules/@vitest/runner": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.0.tgz", - "integrity": "sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==", + "node_modules/@vitest/pretty-format": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.5.tgz", + "integrity": "sha512-CjUtdmpOcm4RVtB+up8r2vVDLR16Mgm/bYdkGFe3Yj/scRfCpbSi2W/BDSDcFK7ohw8UXvjMbOp9H4fByd/cOA==", "dev": true, "dependencies": { - "@vitest/utils": "1.6.0", - "p-limit": "^5.0.0", - "pathe": "^1.1.1" + "tinyrainbow": "^2.0.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/runner/node_modules/p-limit": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", - "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", + "node_modules/@vitest/runner": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.5.tgz", + "integrity": "sha512-BAiZFityFexZQi2yN4OX3OkJC6scwRo8EhRB0Z5HIGGgd2q+Nq29LgHU/+ovCtd0fOfXj5ZI6pwdlUmC5bpi8A==", "dev": true, "dependencies": { - "yocto-queue": "^1.0.0" - }, - "engines": { - "node": ">=18" + "@vitest/utils": "3.0.5", + "pathe": "^2.0.2" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@vitest/runner/node_modules/yocto-queue": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", - "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", - "dev": true, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/snapshot": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.0.tgz", - "integrity": "sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.5.tgz", + "integrity": "sha512-GJPZYcd7v8QNUJ7vRvLDmRwl+a1fGg4T/54lZXe+UOGy47F9yUfE18hRCtXL5aHN/AONu29NGzIXSVFh9K0feA==", "dev": true, "dependencies": { - "magic-string": "^0.30.5", - "pathe": "^1.1.1", - "pretty-format": "^29.7.0" + "@vitest/pretty-format": "3.0.5", + "magic-string": "^0.30.17", + "pathe": "^2.0.2" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/spy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.0.tgz", - "integrity": "sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.5.tgz", + "integrity": "sha512-5fOzHj0WbUNqPK6blI/8VzZdkBlQLnT25knX0r4dbZI9qoZDf3qAdjoMmDcLG5A83W6oUUFJgUd0EYBc2P5xqg==", "dev": true, "dependencies": { - "tinyspy": "^2.2.0" + "tinyspy": "^3.0.2" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.0.tgz", - "integrity": "sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.5.tgz", + "integrity": "sha512-N9AX0NUoUtVwKwy21JtwzaqR5L5R5A99GAbrHfCCXK1lp593i/3AZAXhSP43wRQuxYsflrdzEfXZFo1reR1Nkg==", "dev": true, "dependencies": { - "diff-sequences": "^29.6.3", - "estree-walker": "^3.0.3", - "loupe": "^2.3.7", - "pretty-format": "^29.7.0" + "@vitest/pretty-format": "3.0.5", + "loupe": "^3.1.2", + "tinyrainbow": "^2.0.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/utils/node_modules/loupe": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", - "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", - "dev": true, - "dependencies": { - "get-func-name": "^2.0.1" - } - }, "node_modules/acorn": { "version": "8.11.3", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", @@ -1968,7 +1953,6 @@ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, - "peer": true, "engines": { "node": ">=12" } @@ -2154,11 +2138,10 @@ } }, "node_modules/chai": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz", - "integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", + "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", "dev": true, - "peer": true, "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", @@ -2227,7 +2210,6 @@ "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", "dev": true, - "peer": true, "engines": { "node": ">= 16" } @@ -2355,12 +2337,6 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, - "node_modules/confbox": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.7.tgz", - "integrity": "sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==", - "dev": true - }, "node_modules/confusing-browser-globals": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz", @@ -2460,12 +2436,12 @@ } }, "node_modules/debug": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", - "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "dev": true, "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -2495,7 +2471,6 @@ "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", "dev": true, - "peer": true, "engines": { "node": ">=6" } @@ -2565,15 +2540,6 @@ "node": ">=0.3.1" } }, - "node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -2736,6 +2702,12 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", + "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", + "dev": true + }, "node_modules/es-object-atoms": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", @@ -2789,41 +2761,43 @@ } }, "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", + "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", "dev": true, "hasInstallScript": true, "bin": { "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12" + "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" + "@esbuild/aix-ppc64": "0.24.2", + "@esbuild/android-arm": "0.24.2", + "@esbuild/android-arm64": "0.24.2", + "@esbuild/android-x64": "0.24.2", + "@esbuild/darwin-arm64": "0.24.2", + "@esbuild/darwin-x64": "0.24.2", + "@esbuild/freebsd-arm64": "0.24.2", + "@esbuild/freebsd-x64": "0.24.2", + "@esbuild/linux-arm": "0.24.2", + "@esbuild/linux-arm64": "0.24.2", + "@esbuild/linux-ia32": "0.24.2", + "@esbuild/linux-loong64": "0.24.2", + "@esbuild/linux-mips64el": "0.24.2", + "@esbuild/linux-ppc64": "0.24.2", + "@esbuild/linux-riscv64": "0.24.2", + "@esbuild/linux-s390x": "0.24.2", + "@esbuild/linux-x64": "0.24.2", + "@esbuild/netbsd-arm64": "0.24.2", + "@esbuild/netbsd-x64": "0.24.2", + "@esbuild/openbsd-arm64": "0.24.2", + "@esbuild/openbsd-x64": "0.24.2", + "@esbuild/sunos-x64": "0.24.2", + "@esbuild/win32-arm64": "0.24.2", + "@esbuild/win32-ia32": "0.24.2", + "@esbuild/win32-x64": "0.24.2" } }, "node_modules/escape-string-regexp": { @@ -3534,29 +3508,6 @@ "through": "~2.3.1" } }, - "node_modules/execa": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", - "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", - "dev": true, - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^8.0.1", - "human-signals": "^5.0.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^4.1.0", - "strip-final-newline": "^3.0.0" - }, - "engines": { - "node": ">=16.17" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -3565,6 +3516,15 @@ "node": ">=6" } }, + "node_modules/expect-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.1.0.tgz", + "integrity": "sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==", + "dev": true, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fancy-test": { "version": "3.0.16", "resolved": "https://registry.npmjs.org/fancy-test/-/fancy-test-3.0.16.tgz", @@ -3873,18 +3833,6 @@ "node": ">=8.0.0" } }, - "node_modules/get-stream": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", - "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", - "dev": true, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/get-symbol-description": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", @@ -4148,15 +4096,6 @@ "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", "dev": true }, - "node_modules/human-signals": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", - "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", - "dev": true, - "engines": { - "node": ">=16.17.0" - } - }, "node_modules/hyperlinker": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/hyperlinker/-/hyperlinker-1.0.0.tgz", @@ -4512,18 +4451,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-string": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", @@ -4753,22 +4680,6 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true }, - "node_modules/local-pkg": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz", - "integrity": "sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==", - "dev": true, - "dependencies": { - "mlly": "^1.4.2", - "pkg-types": "^1.0.3" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -4808,22 +4719,18 @@ "dev": true }, "node_modules/loupe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz", - "integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==", - "dev": true, - "peer": true, - "dependencies": { - "get-func-name": "^2.0.1" - } + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", + "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==", + "dev": true }, "node_modules/magic-string": { - "version": "0.30.10", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz", - "integrity": "sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==", + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", "dev": true, "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" + "@jridgewell/sourcemap-codec": "^1.5.0" } }, "node_modules/make-error": { @@ -4838,12 +4745,6 @@ "integrity": "sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==", "dev": true }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -4866,18 +4767,6 @@ "node": ">=8.6" } }, - "node_modules/mimic-fn": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/mimic-response": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", @@ -4926,18 +4815,6 @@ "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" }, - "node_modules/mlly": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.1.tgz", - "integrity": "sha512-rrVRZRELyQzrIUAVMHxP97kv+G786pHmOKzuFII8zDYahFBS7qnHh2AlYSl1GAHhaMPCz6/oHjVMcfFYgFYHgA==", - "dev": true, - "dependencies": { - "acorn": "^8.11.3", - "pathe": "^1.1.2", - "pkg-types": "^1.1.1", - "ufo": "^1.5.3" - } - }, "node_modules/mock-stdin": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/mock-stdin/-/mock-stdin-1.0.0.tgz", @@ -4945,9 +4822,9 @@ "dev": true }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, "node_modules/nanoid": { @@ -5063,33 +4940,6 @@ "semver": "bin/semver" } }, - "node_modules/npm-run-path": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", - "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", - "dev": true, - "dependencies": { - "path-key": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm-run-path/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/object-inspect": { "version": "1.13.1", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", @@ -5192,21 +5042,6 @@ "wrappy": "1" } }, - "node_modules/onetime": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", - "dev": true, - "dependencies": { - "mimic-fn": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -5352,9 +5187,9 @@ } }, "node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "dev": true }, "node_modules/pathval": { @@ -5362,7 +5197,6 @@ "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", "dev": true, - "peer": true, "engines": { "node": ">= 14.16" } @@ -5377,9 +5211,9 @@ } }, "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true }, "node_modules/picomatch": { @@ -5394,17 +5228,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pkg-types": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.1.1.tgz", - "integrity": "sha512-ko14TjmDuQJ14zsotODv7dBlwxKhUKQEhuhmbqo1uCi9BB0Z2alo/wAXg6q1dTR5TyuqYyWhjtfe/Tsh+X28jQ==", - "dev": true, - "dependencies": { - "confbox": "^0.1.7", - "mlly": "^1.7.0", - "pathe": "^1.1.2" - } - }, "node_modules/pluralize": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", @@ -5424,9 +5247,9 @@ } }, "node_modules/postcss": { - "version": "8.4.38", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", - "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.2.tgz", + "integrity": "sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA==", "dev": true, "funding": [ { @@ -5443,9 +5266,9 @@ } ], "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.2.0" + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" @@ -5503,32 +5326,6 @@ "node": ">= 0.8.0" } }, - "node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/propagate": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", @@ -5618,12 +5415,6 @@ "node": ">=0.10.0" } }, - "node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true - }, "node_modules/read-pkg": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", @@ -5890,12 +5681,12 @@ } }, "node_modules/rollup": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.0.tgz", - "integrity": "sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg==", + "version": "4.34.7", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.7.tgz", + "integrity": "sha512-8qhyN0oZ4x0H6wmBgfKxJtxM7qS98YJ0k0kNh5ECVtuchIJ7z9IVVvzpmtQyT10PXKMtBxYr1wQ5Apg8RS8kXQ==", "dev": true, "dependencies": { - "@types/estree": "1.0.5" + "@types/estree": "1.0.6" }, "bin": { "rollup": "dist/bin/rollup" @@ -5905,22 +5696,25 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.18.0", - "@rollup/rollup-android-arm64": "4.18.0", - "@rollup/rollup-darwin-arm64": "4.18.0", - "@rollup/rollup-darwin-x64": "4.18.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.18.0", - "@rollup/rollup-linux-arm-musleabihf": "4.18.0", - "@rollup/rollup-linux-arm64-gnu": "4.18.0", - "@rollup/rollup-linux-arm64-musl": "4.18.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.18.0", - "@rollup/rollup-linux-riscv64-gnu": "4.18.0", - "@rollup/rollup-linux-s390x-gnu": "4.18.0", - "@rollup/rollup-linux-x64-gnu": "4.18.0", - "@rollup/rollup-linux-x64-musl": "4.18.0", - "@rollup/rollup-win32-arm64-msvc": "4.18.0", - "@rollup/rollup-win32-ia32-msvc": "4.18.0", - "@rollup/rollup-win32-x64-msvc": "4.18.0", + "@rollup/rollup-android-arm-eabi": "4.34.7", + "@rollup/rollup-android-arm64": "4.34.7", + "@rollup/rollup-darwin-arm64": "4.34.7", + "@rollup/rollup-darwin-x64": "4.34.7", + "@rollup/rollup-freebsd-arm64": "4.34.7", + "@rollup/rollup-freebsd-x64": "4.34.7", + "@rollup/rollup-linux-arm-gnueabihf": "4.34.7", + "@rollup/rollup-linux-arm-musleabihf": "4.34.7", + "@rollup/rollup-linux-arm64-gnu": "4.34.7", + "@rollup/rollup-linux-arm64-musl": "4.34.7", + "@rollup/rollup-linux-loongarch64-gnu": "4.34.7", + "@rollup/rollup-linux-powerpc64le-gnu": "4.34.7", + "@rollup/rollup-linux-riscv64-gnu": "4.34.7", + "@rollup/rollup-linux-s390x-gnu": "4.34.7", + "@rollup/rollup-linux-x64-gnu": "4.34.7", + "@rollup/rollup-linux-x64-musl": "4.34.7", + "@rollup/rollup-win32-arm64-msvc": "4.34.7", + "@rollup/rollup-win32-ia32-msvc": "4.34.7", + "@rollup/rollup-win32-x64-msvc": "4.34.7", "fsevents": "~2.3.2" } }, @@ -6122,18 +5916,6 @@ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", "dev": true }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/simple-concat": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", @@ -6249,9 +6031,9 @@ } }, "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, "engines": { "node": ">=0.10.0" @@ -6314,9 +6096,9 @@ "dev": true }, "node_modules/std-env": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz", - "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.8.0.tgz", + "integrity": "sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==", "dev": true }, "node_modules/stdout-stderr": { @@ -6486,18 +6268,6 @@ "node": ">=4" } }, - "node_modules/strip-final-newline": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/strip-indent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", @@ -6522,24 +6292,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/strip-literal": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.0.tgz", - "integrity": "sha512-Op+UycaUt/8FbN/Z2TWPBLge3jWrP3xj10f3fnYxf052bKuS3EKs1ZQcVGjnEMdsNVAM+plXRdmjrZ/KgG3Skw==", - "dev": true, - "dependencies": { - "js-tokens": "^9.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/strip-literal/node_modules/js-tokens": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.0.tgz", - "integrity": "sha512-WriZw1luRMlmV3LGJaR6QOJjWwgLUTf89OwT2lUOyjX2dJGBwgmIkbcz+7WFZjrZM635JOIR517++e/67CP9dQ==", - "dev": true - }, "node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -6640,24 +6392,39 @@ "dev": true }, "node_modules/tinybench": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.8.0.tgz", - "integrity": "sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", "dev": true }, "node_modules/tinypool": { - "version": "0.8.4", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz", - "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.2.tgz", + "integrity": "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==", + "dev": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", "dev": true, "engines": { "node": ">=14.0.0" } }, "node_modules/tinyspy": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", - "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", "dev": true, "engines": { "node": ">=14.0.0" @@ -6916,12 +6683,6 @@ "node": ">=14.17" } }, - "node_modules/ufo": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.3.tgz", - "integrity": "sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==", - "dev": true - }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", @@ -6985,20 +6746,20 @@ } }, "node_modules/vite": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.0.tgz", - "integrity": "sha512-hA6vAVK977NyW1Qw+fLvqSo7xDPej7von7C3DwwqPRmnnnK36XEBC/J3j1V5lP8fbt7y0TgTKJbpNGSwM+Bdeg==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.1.0.tgz", + "integrity": "sha512-RjjMipCKVoR4hVfPY6GQTgveinjNuyLw+qruksLDvA5ktI1150VmcMBKmQaEWJhg/j6Uaf6dNCNA0AfdzUb/hQ==", "dev": true, "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.38", - "rollup": "^4.13.0" + "esbuild": "^0.24.2", + "postcss": "^8.5.1", + "rollup": "^4.30.1" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -7007,18 +6768,25 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", + "sass-embedded": "*", "stylus": "*", "sugarss": "*", - "terser": "^5.4.0" + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" }, "peerDependenciesMeta": { "@types/node": { "optional": true }, + "jiti": { + "optional": true + }, "less": { "optional": true }, @@ -7028,6 +6796,9 @@ "sass": { "optional": true }, + "sass-embedded": { + "optional": true + }, "stylus": { "optional": true }, @@ -7036,72 +6807,79 @@ }, "terser": { "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true } } }, "node_modules/vite-node": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.0.tgz", - "integrity": "sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.5.tgz", + "integrity": "sha512-02JEJl7SbtwSDJdYS537nU6l+ktdvcREfLksk/NDAqtdKWGqHl+joXzEubHROmS3E6pip+Xgu2tFezMu75jH7A==", "dev": true, "dependencies": { "cac": "^6.7.14", - "debug": "^4.3.4", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "vite": "^5.0.0" + "debug": "^4.4.0", + "es-module-lexer": "^1.6.0", + "pathe": "^2.0.2", + "vite": "^5.0.0 || ^6.0.0" }, "bin": { "vite-node": "vite-node.mjs" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/vitest": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.0.tgz", - "integrity": "sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==", - "dev": true, - "dependencies": { - "@vitest/expect": "1.6.0", - "@vitest/runner": "1.6.0", - "@vitest/snapshot": "1.6.0", - "@vitest/spy": "1.6.0", - "@vitest/utils": "1.6.0", - "acorn-walk": "^8.3.2", - "chai": "^4.3.10", - "debug": "^4.3.4", - "execa": "^8.0.1", - "local-pkg": "^0.5.0", - "magic-string": "^0.30.5", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "std-env": "^3.5.0", - "strip-literal": "^2.0.0", - "tinybench": "^2.5.1", - "tinypool": "^0.8.3", - "vite": "^5.0.0", - "vite-node": "1.6.0", - "why-is-node-running": "^2.2.2" + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.5.tgz", + "integrity": "sha512-4dof+HvqONw9bvsYxtkfUp2uHsTN9bV2CZIi1pWgoFpL1Lld8LA1ka9q/ONSsoScAKG7NVGf2stJTI7XRkXb2Q==", + "dev": true, + "dependencies": { + "@vitest/expect": "3.0.5", + "@vitest/mocker": "3.0.5", + "@vitest/pretty-format": "^3.0.5", + "@vitest/runner": "3.0.5", + "@vitest/snapshot": "3.0.5", + "@vitest/spy": "3.0.5", + "@vitest/utils": "3.0.5", + "chai": "^5.1.2", + "debug": "^4.4.0", + "expect-type": "^1.1.0", + "magic-string": "^0.30.17", + "pathe": "^2.0.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinypool": "^1.0.2", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0", + "vite-node": "3.0.5", + "why-is-node-running": "^2.3.0" }, "bin": { "vitest": "vitest.mjs" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "@edge-runtime/vm": "*", - "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "1.6.0", - "@vitest/ui": "1.6.0", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.0.5", + "@vitest/ui": "3.0.5", "happy-dom": "*", "jsdom": "*" }, @@ -7109,6 +6887,9 @@ "@edge-runtime/vm": { "optional": true }, + "@types/debug": { + "optional": true + }, "@types/node": { "optional": true }, @@ -7139,75 +6920,6 @@ "vitest": ">=0.31.1" } }, - "node_modules/vitest/node_modules/assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/vitest/node_modules/chai": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", - "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==", - "dev": true, - "dependencies": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.3", - "deep-eql": "^4.1.3", - "get-func-name": "^2.0.2", - "loupe": "^2.3.6", - "pathval": "^1.1.1", - "type-detect": "^4.0.8" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/vitest/node_modules/check-error": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", - "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", - "dev": true, - "dependencies": { - "get-func-name": "^2.0.2" - }, - "engines": { - "node": "*" - } - }, - "node_modules/vitest/node_modules/deep-eql": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", - "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", - "dev": true, - "dependencies": { - "type-detect": "^4.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/vitest/node_modules/loupe": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", - "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", - "dev": true, - "dependencies": { - "get-func-name": "^2.0.1" - } - }, - "node_modules/vitest/node_modules/pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", - "dev": true, - "engines": { - "node": "*" - } - }, "node_modules/which": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", @@ -7258,9 +6970,9 @@ } }, "node_modules/why-is-node-running": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.2.2.tgz", - "integrity": "sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", "dev": true, "dependencies": { "siginfo": "^2.0.0", diff --git a/package.json b/package.json index d20205e..5e3523b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codify-plugin-lib", - "version": "1.0.132", + "version": "1.0.133", "description": "Library plugin library", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -34,7 +34,7 @@ "@types/uuid": "^10.0.0", "@types/lodash.isequal": "^4.5.8", "chai-as-promised": "^7.1.1", - "vitest": "^1.4.0", + "vitest": "^3.0.5", "vitest-mock-extended": "^1.3.1", "sinon": "^17.0.1", "eslint": "^8.51.0", diff --git a/src/plan/plan.test.ts b/src/plan/plan.test.ts index 257b205..108b4aa 100644 --- a/src/plan/plan.test.ts +++ b/src/plan/plan.test.ts @@ -230,6 +230,120 @@ describe('Plan entity tests', () => { ]) }) }) + + it('Can use the requiredParameters to match the correct resources together', async () => { + const resource1 = new class extends TestResource { + getSettings(): ResourceSettings { + return { + id: 'type', + parameterSettings: { + propA: { type: 'string' }, + propB: { type: 'string', canModify: true }, + }, + allowMultiple: { + requiredParameters: ['propA'] + } + } + } + + async refresh(): Promise | null> { + return [{ + propA: 'same', + propB: 'old', + }, { + propA: 'different', + propB: 'different', + }] + } + } + + const controller = new ResourceController(resource1); + const plan = await controller.plan( + { type: 'type' }, + { propA: 'same', propB: 'new' }, + null, + false + ) + + expect(plan.changeSet).toMatchObject({ + operation: ResourceOperation.MODIFY, + parameterChanges: expect.arrayContaining([ + expect.objectContaining({ + name: 'propA', + previousValue: 'same', + newValue: 'same', + operation: 'noop' + }), + expect.objectContaining({ + name: 'propB', + previousValue: 'old', + newValue: 'new', + operation: 'modify' + }) + ]) + }) + }) + + it('Can use the schema to determine required parameters for multiple allowed', async () => { + const resource1 = new class extends TestResource { + getSettings(): ResourceSettings { + return { + id: 'type', + parameterSettings: { + propA: { type: 'string' }, + propB: { type: 'string', canModify: true }, + }, + allowMultiple: true, + schema: { + '$schema': 'http://json-schema.org/draft-07/schema', + '$id': 'https://www.codifycli.com/type.json', + 'type': 'object', + 'properties': { + propA: { type: 'string' }, + propB: { type: 'string' } + }, + required: ['propA'] + } + } + } + + async refresh(): Promise | null> { + return [{ + propA: 'same', + propB: 'old', + }, { + propA: 'different', + propB: 'different', + }] + } + } + + const controller = new ResourceController(resource1); + const plan = await controller.plan( + { type: 'type' }, + { propA: 'same', propB: 'new' }, + null, + false + ) + + expect(plan.changeSet).toMatchObject({ + operation: ResourceOperation.MODIFY, + parameterChanges: expect.arrayContaining([ + expect.objectContaining({ + name: 'propA', + previousValue: 'same', + newValue: 'same', + operation: 'noop' + }), + expect.objectContaining({ + name: 'propB', + previousValue: 'old', + newValue: 'new', + operation: 'modify' + }) + ]) + }) + }) }) function createTestResource() { diff --git a/src/plan/plan.ts b/src/plan/plan.ts index 4213a07..2d01591 100644 --- a/src/plan/plan.ts +++ b/src/plan/plan.ts @@ -260,13 +260,44 @@ export class Plan { return null; } + const matcher = typeof settings.allowMultiple === 'boolean' || !settings.allowMultiple.matcher + ? ((desired: Partial, currentArr: Array>) => { + const requiredParameters = typeof settings.allowMultiple === 'object' + ? settings.allowMultiple?.requiredParameters ?? (settings.schema?.required as string[]) ?? [] + : (settings.schema?.required as string[]) ?? [] + + const matched = currentArr.filter((c) => requiredParameters.every((key) => { + const currentParameter = c[key]; + const desiredParameter = desired[key]; + + if (!currentParameter) { + console.warn(`Unable to find required parameter for current ${currentParameter}`) + return false; + } + + if (!desiredParameter) { + console.warn(`Unable to find required parameter for current ${currentParameter}`) + return false; + } + + return currentParameter === desiredParameter; + })) + + if (matched.length > 1) { + console.warn(`Required parameters did not uniquely identify a resource: ${currentArray}. Defaulting to the first one`); + } + + return matched[0]; + }) + : settings.allowMultiple.matcher + if (isStateful) { return state - ? settings.allowMultiple.matcher(state, currentArray) + ? matcher(state, currentArray) ?? null : null } - return settings.allowMultiple.matcher(desired!, currentArray); + return matcher(desired!, currentArray) ?? null; } /** diff --git a/src/plugin/plugin.test.ts b/src/plugin/plugin.test.ts index 476a173..b60fb56 100644 --- a/src/plugin/plugin.test.ts +++ b/src/plugin/plugin.test.ts @@ -6,7 +6,6 @@ import { Plan } from '../plan/plan.js'; import { spy } from 'sinon'; import { ResourceSettings } from '../resource/resource-settings.js'; import { TestConfig } from '../utils/test-utils.test.js'; -import { ApplyValidationError } from '../common/errors.js'; import { getPty } from '../pty/index.js'; interface TestConfig extends StringIndexedObject { @@ -229,7 +228,7 @@ describe('Plugin tests', () => { await expect(() => testPlugin.apply({ plan })) .rejects - .toThrowError(new ApplyValidationError(Plan.fromResponse(plan))); + .toThrowError(); expect(resource.modify.calledOnce).to.be.true; }) diff --git a/src/resource/parsed-resource-settings.ts b/src/resource/parsed-resource-settings.ts index b91adbf..000f78b 100644 --- a/src/resource/parsed-resource-settings.ts +++ b/src/resource/parsed-resource-settings.ts @@ -35,8 +35,12 @@ export type ParsedParameterSetting = export class ParsedResourceSettings implements ResourceSettings { private cache = new Map(); id!: string; - schema?: unknown; - allowMultiple?: { matcher: (desired: Partial, current: Partial[]) => Partial; } | undefined; + schema?: JSONSchemaType; + allowMultiple?: { + matcher?: (desired: Partial, current: Partial[]) => Partial; + requiredParameters?: string[] + } | boolean; + removeStatefulParametersBeforeDestroy?: boolean | undefined; dependencies?: string[] | undefined; inputTransformation?: ((desired: Partial) => unknown) | undefined; diff --git a/src/resource/resource-settings.ts b/src/resource/resource-settings.ts index 61feee0..ba8c298 100644 --- a/src/resource/resource-settings.ts +++ b/src/resource/resource-settings.ts @@ -1,3 +1,4 @@ +import { JSONSchemaType } from 'ajv'; import { StringIndexedObject } from 'codify-schemas'; import isObjectsEqual from 'lodash.isequal' import path from 'node:path'; @@ -23,7 +24,7 @@ export interface ResourceSettings { /** * Schema to validate user configs with. Must be in the format JSON Schema draft07 */ - schema?: unknown; + schema?: JSONSchemaType; /** * Allow multiple of the same resource to unique. Set truthy if @@ -32,6 +33,17 @@ export interface ResourceSettings { */ allowMultiple?: { + /** + * A set of parameters that uniquely identifies a resource. The value of these parameters is used to determine which + * resource is which when multiple can exist at the same time. Defaults to the required parameters inside the json + * schema. + * + * For example: + * If paramA is required, then if resource1.paramA === resource2.paramA then are the same resource. + * If resource1.paramA !== resource1.paramA, then they are different. + */ + requiredParameters?: string[] + /** * If multiple copies are allowed then a matcher must be defined to match the desired * config with one of the resources currently existing on the system. Return null if there is no match. @@ -41,8 +53,8 @@ export interface ResourceSettings { * * @return The matched resource. */ - matcher: (desired: Partial, current: Partial[],) => Partial - } + matcher?: (desired: Partial, current: Partial[],) => Partial + } | boolean /** * If true, {@link StatefulParameter} remove() will be called before resource destruction. This is useful From 72e69fcead287e41cb0afc787523c84dd745690c Mon Sep 17 00:00:00 2001 From: kevinwang Date: Sat, 15 Feb 2025 13:45:39 -0500 Subject: [PATCH 04/25] fix: updated typing for schema --- src/resource/parsed-resource-settings.ts | 2 +- src/resource/resource-settings.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/resource/parsed-resource-settings.ts b/src/resource/parsed-resource-settings.ts index 000f78b..5736b95 100644 --- a/src/resource/parsed-resource-settings.ts +++ b/src/resource/parsed-resource-settings.ts @@ -35,7 +35,7 @@ export type ParsedParameterSetting = export class ParsedResourceSettings implements ResourceSettings { private cache = new Map(); id!: string; - schema?: JSONSchemaType; + schema?: Partial>; allowMultiple?: { matcher?: (desired: Partial, current: Partial[]) => Partial; requiredParameters?: string[] diff --git a/src/resource/resource-settings.ts b/src/resource/resource-settings.ts index ba8c298..e851ec1 100644 --- a/src/resource/resource-settings.ts +++ b/src/resource/resource-settings.ts @@ -24,7 +24,7 @@ export interface ResourceSettings { /** * Schema to validate user configs with. Must be in the format JSON Schema draft07 */ - schema?: JSONSchemaType; + schema?: Partial>; /** * Allow multiple of the same resource to unique. Set truthy if From 97de6a0dd90db4281dd3b908513d44ebd5b89b03 Mon Sep 17 00:00:00 2001 From: kevinwang Date: Sat, 15 Feb 2025 15:40:40 -0500 Subject: [PATCH 05/25] feat: Used the reverse transformation to alter the import output to match a user's config + added tests --- package.json | 2 +- src/resource/parsed-resource-settings.ts | 7 +- src/resource/resource-controller.test.ts | 84 ++++++++++++++++++++++++ src/resource/resource-controller.ts | 11 +++- 4 files changed, 97 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 5e3523b..62a95c8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codify-plugin-lib", - "version": "1.0.133", + "version": "1.0.135", "description": "Library plugin library", "main": "dist/index.js", "typings": "dist/index.d.ts", diff --git a/src/resource/parsed-resource-settings.ts b/src/resource/parsed-resource-settings.ts index 5736b95..bd5f68e 100644 --- a/src/resource/parsed-resource-settings.ts +++ b/src/resource/parsed-resource-settings.ts @@ -5,6 +5,7 @@ import { StatefulParameterController } from '../stateful-parameter/stateful-para import { ArrayParameterSetting, DefaultParameterSetting, + InputTransformation, ParameterSetting, resolveElementEqualsFn, resolveEqualsFn, @@ -136,7 +137,7 @@ export class ParsedResourceSettings implements Re }); } - get inputTransformations(): Partial unknown>> { + get inputTransformations(): Partial> { return this.getFromCacheOrCreate('inputTransformations', () => { if (!this.settings.parameterSettings) { return {}; @@ -145,8 +146,8 @@ export class ParsedResourceSettings implements Re return Object.fromEntries( Object.entries(this.settings.parameterSettings) .filter(([_, v]) => resolveParameterTransformFn(v!) !== undefined) - .map(([k, v]) => [k, resolveParameterTransformFn(v!)!.to] as const) - ) as Record unknown>; + .map(([k, v]) => [k, resolveParameterTransformFn(v!)] as const) + ) as Record; }); } diff --git a/src/resource/resource-controller.test.ts b/src/resource/resource-controller.test.ts index 188f092..2ca87ab 100644 --- a/src/resource/resource-controller.test.ts +++ b/src/resource/resource-controller.test.ts @@ -535,4 +535,88 @@ describe('Resource tests', () => { expect(parameter1.modify.calledOnce).to.be.true; expect(parameter2.addItem.calledOnce).to.be.true; }); + + it('Applies reverse input transformations for imports', async () => { + const resource = new class extends TestResource { + getSettings(): ResourceSettings { + return { + id: 'resourceType', + parameterSettings: { + propD: { + type: 'array', + inputTransformation: { + to: (hosts: Record[]) => hosts.map((h) => Object.fromEntries( + Object.entries(h) + .map(([k, v]) => [ + k, + typeof v === 'boolean' + ? (v ? 'yes' : 'no') // The file takes 'yes' or 'no' instead of booleans + : v, + ]) + ) + ), + from: (hosts: Record[]) => hosts.map((h) => Object.fromEntries( + Object.entries(h) + .map(([k, v]) => [ + k, + v === 'yes' || v === 'no' + ? (v === 'yes') + : v, + ]) + )) + } + } + } + } + } + + async refresh(parameters: Partial): Promise | null> { + return { + propD: [ + { + Host: 'new.com', + AddKeysToAgent: true, + IdentityFile: 'id_ed25519' + }, + { + Host: 'github.com', + AddKeysToAgent: true, + UseKeychain: true, + }, + { + Match: 'User bob,joe,phil', + PasswordAuthentication: true, + } + ], + } + } + } + + const controller = new ResourceController(resource); + const plan = await controller.import({ type: 'resourceType' }, {}); + + expect(plan![0]).toMatchObject({ + 'core': { + 'type': 'resourceType' + }, + 'parameters': { + 'propD': [ + { + 'Host': 'new.com', + 'AddKeysToAgent': true, + 'IdentityFile': 'id_ed25519' + }, + { + 'Host': 'github.com', + 'AddKeysToAgent': true, + 'UseKeychain': true + }, + { + 'Match': 'User bob,joe,phil', + 'PasswordAuthentication': true + } + ] + } + }) + }) }); diff --git a/src/resource/resource-controller.ts b/src/resource/resource-controller.ts index d0fe8ad..fbd6311 100644 --- a/src/resource/resource-controller.ts +++ b/src/resource/resource-controller.ts @@ -254,7 +254,10 @@ export class ResourceController { } const statefulCurrentParameters = await this.refreshStatefulParameters(allStatefulParameters, parametersToRefresh); - return [{ core, parameters: { ...currentParametersArray[0], ...statefulCurrentParameters } }]; + const resultParameters = { ...currentParametersArray[0], ...statefulCurrentParameters }; + + await this.applyTransformParameters(resultParameters, true); + return [{ core, parameters: resultParameters }]; } private async applyCreate(plan: Plan): Promise { @@ -333,7 +336,7 @@ ${JSON.stringify(refresh, null, 2)} } } - private async applyTransformParameters(config: Partial | null): Promise { + private async applyTransformParameters(config: Partial | null, reverse = false): Promise { if (!config) { return; } @@ -343,7 +346,9 @@ ${JSON.stringify(refresh, null, 2)} continue; } - (config as Record)[key] = await inputTransformation(config[key], this.settings.parameterSettings![key]!); + (config as Record)[key] = reverse + ? await inputTransformation.from(config[key]) + : await inputTransformation.to(config[key]); } if (this.settings.inputTransformation) { From 90826bc71f6ed22c5de5627084ee0c1146e969f9 Mon Sep 17 00:00:00 2001 From: kevinwang Date: Sat, 15 Feb 2025 17:24:06 -0500 Subject: [PATCH 06/25] feat: Added reverse transformation for object level transforms --- package.json | 2 +- src/resource/parsed-resource-settings.ts | 4 +- src/resource/resource-controller.test.ts | 100 ++++++++++++++++++++++- src/resource/resource-controller.ts | 7 +- src/resource/resource-settings.test.ts | 32 +++++--- src/resource/resource-settings.ts | 14 ++-- 6 files changed, 133 insertions(+), 26 deletions(-) diff --git a/package.json b/package.json index 62a95c8..1e7e0e2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codify-plugin-lib", - "version": "1.0.135", + "version": "1.0.136", "description": "Library plugin library", "main": "dist/index.js", "typings": "dist/index.d.ts", diff --git a/src/resource/parsed-resource-settings.ts b/src/resource/parsed-resource-settings.ts index bd5f68e..cb22588 100644 --- a/src/resource/parsed-resource-settings.ts +++ b/src/resource/parsed-resource-settings.ts @@ -44,7 +44,7 @@ export class ParsedResourceSettings implements Re removeStatefulParametersBeforeDestroy?: boolean | undefined; dependencies?: string[] | undefined; - inputTransformation?: ((desired: Partial) => unknown) | undefined; + transformation?: InputTransformation; private settings: ResourceSettings; constructor(settings: ResourceSettings) { @@ -54,7 +54,7 @@ export class ParsedResourceSettings implements Re this.allowMultiple = settings.allowMultiple; this.removeStatefulParametersBeforeDestroy = settings.removeStatefulParametersBeforeDestroy; this.dependencies = settings.dependencies; - this.inputTransformation = settings.inputTransformation; + this.transformation = settings.transformation; this.validateSettings(); } diff --git a/src/resource/resource-controller.test.ts b/src/resource/resource-controller.test.ts index 2ca87ab..35016be 100644 --- a/src/resource/resource-controller.test.ts +++ b/src/resource/resource-controller.test.ts @@ -7,7 +7,7 @@ import { CreatePlan, DestroyPlan, ModifyPlan } from '../plan/plan-types.js'; import { ParameterChange } from '../plan/change-set.js'; import { ResourceController } from './resource-controller.js'; import { TestConfig, testPlan, TestResource, TestStatefulParameter } from '../utils/test-utils.test.js'; -import { untildify } from '../utils/utils.js'; +import { tildify, untildify } from '../utils/utils.js'; import { ArrayStatefulParameter, StatefulParameter } from '../stateful-parameter/stateful-parameter.js'; import { Plan } from '../plan/plan.js'; @@ -20,9 +20,11 @@ describe('Resource tests', () => { id: 'type', dependencies: ['homebrew', 'python'], parameterSettings: { - propA: { canModify: true, inputTransformation: (input) => untildify(input) }, + propA: { + canModify: true, + transformation: { to: (input) => untildify(input), from: (input) => tildify(input) } + }, }, - inputTransformation: (config) => ({ propA: config.propA, propC: config.propB }), } } @@ -544,7 +546,7 @@ describe('Resource tests', () => { parameterSettings: { propD: { type: 'array', - inputTransformation: { + transformation: { to: (hosts: Record[]) => hosts.map((h) => Object.fromEntries( Object.entries(h) .map(([k, v]) => [ @@ -619,4 +621,94 @@ describe('Resource tests', () => { } }) }) + + it('Applies reverse input transformations for imports (object level)', async () => { + const resource = new class extends TestResource { + getSettings(): ResourceSettings { + return { + id: 'resourceType', + parameterSettings: { + propD: { + type: 'array', + } + }, + transformation: { + to: (input: any) => ({ + ...input, + propD: input.propD?.map((h) => Object.fromEntries( + Object.entries(h) + .map(([k, v]) => [ + k, + typeof v === 'boolean' + ? (v ? 'yes' : 'no') // The file takes 'yes' or 'no' instead of booleans + : v, + ]) + ) + ) + }), + from: (output: any) => ({ + ...output, + propD: output.propD?.map((h) => Object.fromEntries( + Object.entries(h) + .map(([k, v]) => [ + k, + v === 'yes' || v === 'no' + ? (v === 'yes') + : v, + ]) + )) + }) + } + } + } + + async refresh(parameters: Partial): Promise | null> { + return { + propD: [ + { + Host: 'new.com', + AddKeysToAgent: true, + IdentityFile: 'id_ed25519' + }, + { + Host: 'github.com', + AddKeysToAgent: true, + UseKeychain: true, + }, + { + Match: 'User bob,joe,phil', + PasswordAuthentication: true, + } + ], + } + } + } + + const controller = new ResourceController(resource); + const plan = await controller.import({ type: 'resourceType' }, {}); + + expect(plan![0]).toMatchObject({ + 'core': { + 'type': 'resourceType' + }, + 'parameters': { + 'propD': [ + { + 'Host': 'new.com', + 'AddKeysToAgent': true, + 'IdentityFile': 'id_ed25519' + }, + { + 'Host': 'github.com', + 'AddKeysToAgent': true, + 'UseKeychain': true + }, + { + 'Match': 'User bob,joe,phil', + 'PasswordAuthentication': true + } + ] + } + }) + }) }); diff --git a/src/resource/resource-controller.ts b/src/resource/resource-controller.ts index fbd6311..2b8e4c4 100644 --- a/src/resource/resource-controller.ts +++ b/src/resource/resource-controller.ts @@ -351,8 +351,11 @@ ${JSON.stringify(refresh, null, 2)} : await inputTransformation.to(config[key]); } - if (this.settings.inputTransformation) { - const transformed = await this.settings.inputTransformation({ ...config }) + if (this.settings.transformation) { + const transformed = reverse + ? await this.settings.transformation.from({ ...config }) + : await this.settings.transformation.to({ ...config }) + Object.keys(config).forEach((k) => delete config[k]) Object.assign(config, transformed); } diff --git a/src/resource/resource-settings.test.ts b/src/resource/resource-settings.test.ts index 6b8ea9c..e9fe9c6 100644 --- a/src/resource/resource-settings.test.ts +++ b/src/resource/resource-settings.test.ts @@ -569,10 +569,16 @@ describe('Resource parameter tests', () => { getSettings(): ResourceSettings { return { id: 'resourceType', - inputTransformation: (desired) => ({ - propA: 'propA', - propB: 10, - }) + transformation: { + to: (desired) => ({ + propA: 'propA', + propB: 10, + }), + from: (current) => ({ + propA: 'propA', + propB: 10, + }) + } } } @@ -604,10 +610,16 @@ describe('Resource parameter tests', () => { getSettings(): ResourceSettings { return { id: 'resourceType', - inputTransformation: (desired) => ({ - propA: 'propA', - propB: 10, - }) + transformation: { + to: (desired) => ({ + propA: 'propA', + propB: 10, + }), + from: (desired) => ({ + propA: 'propA', + propB: 10, + }) + } } } @@ -852,7 +864,7 @@ describe('Resource parameter tests', () => { parameterSettings: { propD: { type: 'array', - inputTransformation: { + transformation: { to: (hosts: Record[]) => hosts.map((h) => Object.fromEntries( Object.entries(h) .map(([k, v]) => [ @@ -918,7 +930,7 @@ describe('Resource parameter tests', () => { getSettings(): any { return { type: 'array', - inputTransformation: { + transformation: { to: (hosts: Record[]) => hosts.map((h) => Object.fromEntries( Object.entries(h) .map(([k, v]) => [ diff --git a/src/resource/resource-settings.ts b/src/resource/resource-settings.ts index e851ec1..dd816a2 100644 --- a/src/resource/resource-settings.ts +++ b/src/resource/resource-settings.ts @@ -81,7 +81,7 @@ export interface ResourceSettings { * * @param desired */ - inputTransformation?: (desired: Partial) => Promise | unknown; + transformation?: InputTransformation; /** * Customize the import and destory behavior of the resource. By default, codify import and codify destroy will call @@ -189,7 +189,7 @@ export interface DefaultParameterSetting { * * @param input The original parameter value from the desired config. */ - inputTransformation?: InputTransformation; + transformation?: InputTransformation; /** * Customize the equality comparison for a parameter. This is used in the diffing algorithm for generating the plan. @@ -354,10 +354,10 @@ export function resolveParameterTransformFn( parameter: ParameterSetting ): InputTransformation | undefined { - if (parameter.type === 'stateful' && !parameter.inputTransformation) { + if (parameter.type === 'stateful' && !parameter.transformation) { const sp = (parameter as StatefulParameterSetting).definition.getSettings(); - if (sp.inputTransformation) { - return (parameter as StatefulParameterSetting).definition?.getSettings()?.inputTransformation + if (sp.transformation) { + return (parameter as StatefulParameterSetting).definition?.getSettings()?.transformation } return sp.type ? ParameterTransformationDefaults[sp.type] : undefined; @@ -366,7 +366,7 @@ export function resolveParameterTransformFn( if (parameter.type === 'array' && (parameter as ArrayParameterSetting).itemType && ParameterTransformationDefaults[(parameter as ArrayParameterSetting).itemType!] - && !parameter.inputTransformation + && !parameter.transformation ) { const itemType = (parameter as ArrayParameterSetting).itemType!; const itemTransformation = ParameterTransformationDefaults[itemType]!; @@ -381,5 +381,5 @@ export function resolveParameterTransformFn( } } - return parameter.inputTransformation ?? ParameterTransformationDefaults[parameter.type as ParameterSettingType] ?? undefined; + return parameter.transformation ?? ParameterTransformationDefaults[parameter.type as ParameterSettingType] ?? undefined; } From 580520fd47a59ad21d15b3a319de684448bf3264 Mon Sep 17 00:00:00 2001 From: kevinwang Date: Sat, 15 Feb 2025 18:00:07 -0500 Subject: [PATCH 07/25] feat: Revised how setting parameters are set in the resource config --- package.json | 2 +- src/resource/resource-settings.test.ts | 2 +- src/resource/resource-settings.ts | 16 +++++++++++++++- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 1e7e0e2..306d2ce 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codify-plugin-lib", - "version": "1.0.136", + "version": "1.0.137", "description": "Library plugin library", "main": "dist/index.js", "typings": "dist/index.d.ts", diff --git a/src/resource/resource-settings.test.ts b/src/resource/resource-settings.test.ts index e9fe9c6..26167c1 100644 --- a/src/resource/resource-settings.test.ts +++ b/src/resource/resource-settings.test.ts @@ -696,7 +696,7 @@ describe('Resource parameter tests', () => { return { id: 'resourceType', parameterSettings: { - propA: { type: 'setting' }, + propA: { type: 'string', setting: true }, propB: { type: 'number' } } } diff --git a/src/resource/resource-settings.ts b/src/resource/resource-settings.ts index dd816a2..e11de6b 100644 --- a/src/resource/resource-settings.ts +++ b/src/resource/resource-settings.ts @@ -212,6 +212,16 @@ export interface DefaultParameterSetting { * 2. AWS profile secret keys that can be updated without the re-installation of AWS CLI */ canModify?: boolean + + /** + * This option allows the plan to skip this parameter entirely as it is used for setting purposes only. The value + * of this parameter is used to configure the resource or other parameters. + * + * Examples: + * 1. homebrew.onlyPlanUserInstalled option will tell homebrew to filter by --installed-on-request. But the value, + * of the parameter itself (true or false) does not have an impact on the plan + */ + setting?: boolean } /** @@ -289,11 +299,15 @@ const ParameterEqualsDefaults: Partial Number(a) === Number(b), 'string': (a: unknown, b: unknown) => String(a) === String(b), 'version': (desired: unknown, current: unknown) => String(current).includes(String(desired)), - 'setting': () => true, 'object': isObjectsEqual, } export function resolveEqualsFn(parameter: ParameterSetting): (desired: unknown, current: unknown) => boolean { + // Setting parameters do not impact the plan + if (parameter.setting) { + return () => true; + } + const isEqual = resolveFnFromEqualsFnOrString(parameter.isEqual); if (parameter.type === 'array') { From c933cd1b5a59368a5170a7b4a88683f39be018d7 Mon Sep 17 00:00:00 2001 From: kevinwang Date: Sun, 16 Feb 2025 09:32:46 -0500 Subject: [PATCH 08/25] feat: Updated to import and destroy and added allow multiple to getResourceInfo --- package-lock.json | 12 ++++++------ package.json | 4 ++-- src/plugin/plugin.ts | 9 ++++++++- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9244da3..46d0bab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,19 @@ { "name": "codify-plugin-lib", - "version": "1.0.133", + "version": "1.0.137", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "codify-plugin-lib", - "version": "1.0.133", + "version": "1.0.137", "license": "ISC", "dependencies": { "@homebridge/node-pty-prebuilt-multiarch": "^0.12.0-beta.5", "@npmcli/promise-spawn": "^7.0.1", "ajv": "^8.12.0", "ajv-formats": "^2.1.1", - "codify-schemas": "1.0.63", + "codify-schemas": "1.0.64", "lodash.isequal": "^4.5.0", "nanoid": "^5.0.9", "strip-ansi": "^7.1.0", @@ -2283,9 +2283,9 @@ } }, "node_modules/codify-schemas": { - "version": "1.0.63", - "resolved": "https://registry.npmjs.org/codify-schemas/-/codify-schemas-1.0.63.tgz", - "integrity": "sha512-0khOFJOK7UPibAw8Dfsf9XDcK1ad5c+YbSBMGdpTv6L4lwkYiuEgJhgoxM3oL7fzywe96Woj1KdLVecwZDyaZQ==", + "version": "1.0.64", + "resolved": "https://registry.npmjs.org/codify-schemas/-/codify-schemas-1.0.64.tgz", + "integrity": "sha512-fIkiuFRNQNGaFwbRWETc9nwekqclHkaD0YlvEa/luVKaqjlaiIAjeP6w9K6iFmQyv5mSLmTbwhVJpEccotpbTg==", "dependencies": { "ajv": "^8.12.0" } diff --git a/package.json b/package.json index 306d2ce..4d66014 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codify-plugin-lib", - "version": "1.0.137", + "version": "1.0.138", "description": "Library plugin library", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -16,7 +16,7 @@ "dependencies": { "ajv": "^8.12.0", "ajv-formats": "^2.1.1", - "codify-schemas": "1.0.63", + "codify-schemas": "1.0.64", "@npmcli/promise-spawn": "^7.0.1", "@homebridge/node-pty-prebuilt-multiarch": "^0.12.0-beta.5", "uuid": "^10.0.0", diff --git a/src/plugin/plugin.ts b/src/plugin/plugin.ts index 1481538..25a3215 100644 --- a/src/plugin/plugin.ts +++ b/src/plugin/plugin.ts @@ -72,14 +72,21 @@ export class Plugin { ?? null ) as null | string[]; + const allowMultiple = resource.settings.allowMultiple !== undefined + ? (typeof resource.settings.allowMultiple === 'boolean' + ? { requiredParameters: schema?.required ?? [] } + : { requiredParameters: resource.settings.allowMultiple.requiredParameters ?? schema?.required ?? [] } + ) : undefined + return { plugin: this.name, type: data.type, dependencies: resource.dependencies, schema: schema as Record | undefined, - import: { + importAndDestroy: { requiredParameters: requiredPropertyNames, }, + allowMultiple } } From c7549e3d141aaacb7af39c4af43a68b8d6f7fd4b Mon Sep 17 00:00:00 2001 From: kevinwang Date: Tue, 18 Feb 2025 22:19:06 -0500 Subject: [PATCH 09/25] feat: Updated requiredParameters to identifyingParameters --- package-lock.json | 12 ++++----- package.json | 4 +-- src/plan/plan.test.ts | 2 +- src/plan/plan.ts | 2 +- src/plugin/plugin.test.ts | 42 +++++++++++++++++++++++++++++ src/plugin/plugin.ts | 8 ++++-- src/resource/resource-controller.ts | 4 +++ src/resource/resource-settings.ts | 11 ++++++-- 8 files changed, 71 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index 46d0bab..52e4a88 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,19 @@ { "name": "codify-plugin-lib", - "version": "1.0.137", + "version": "1.0.141", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "codify-plugin-lib", - "version": "1.0.137", + "version": "1.0.141", "license": "ISC", "dependencies": { "@homebridge/node-pty-prebuilt-multiarch": "^0.12.0-beta.5", "@npmcli/promise-spawn": "^7.0.1", "ajv": "^8.12.0", "ajv-formats": "^2.1.1", - "codify-schemas": "1.0.64", + "codify-schemas": "1.0.70", "lodash.isequal": "^4.5.0", "nanoid": "^5.0.9", "strip-ansi": "^7.1.0", @@ -2283,9 +2283,9 @@ } }, "node_modules/codify-schemas": { - "version": "1.0.64", - "resolved": "https://registry.npmjs.org/codify-schemas/-/codify-schemas-1.0.64.tgz", - "integrity": "sha512-fIkiuFRNQNGaFwbRWETc9nwekqclHkaD0YlvEa/luVKaqjlaiIAjeP6w9K6iFmQyv5mSLmTbwhVJpEccotpbTg==", + "version": "1.0.70", + "resolved": "https://registry.npmjs.org/codify-schemas/-/codify-schemas-1.0.70.tgz", + "integrity": "sha512-LPcWt9NS5lhycTDxspFNDXDlDMPF5NNWL51DzhFC7q/fLLjwHM3cOjBtPYhkKnwGaQb+CMQcupVnEUOlMFt4DA==", "dependencies": { "ajv": "^8.12.0" } diff --git a/package.json b/package.json index 4d66014..6d82a0b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codify-plugin-lib", - "version": "1.0.138", + "version": "1.0.141", "description": "Library plugin library", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -16,7 +16,7 @@ "dependencies": { "ajv": "^8.12.0", "ajv-formats": "^2.1.1", - "codify-schemas": "1.0.64", + "codify-schemas": "1.0.70", "@npmcli/promise-spawn": "^7.0.1", "@homebridge/node-pty-prebuilt-multiarch": "^0.12.0-beta.5", "uuid": "^10.0.0", diff --git a/src/plan/plan.test.ts b/src/plan/plan.test.ts index 108b4aa..b879390 100644 --- a/src/plan/plan.test.ts +++ b/src/plan/plan.test.ts @@ -241,7 +241,7 @@ describe('Plan entity tests', () => { propB: { type: 'string', canModify: true }, }, allowMultiple: { - requiredParameters: ['propA'] + identifyingParameters: ['propA'] } } } diff --git a/src/plan/plan.ts b/src/plan/plan.ts index 2d01591..b15b52d 100644 --- a/src/plan/plan.ts +++ b/src/plan/plan.ts @@ -263,7 +263,7 @@ export class Plan { const matcher = typeof settings.allowMultiple === 'boolean' || !settings.allowMultiple.matcher ? ((desired: Partial, currentArr: Array>) => { const requiredParameters = typeof settings.allowMultiple === 'object' - ? settings.allowMultiple?.requiredParameters ?? (settings.schema?.required as string[]) ?? [] + ? settings.allowMultiple?.identifyingParameters ?? (settings.schema?.required as string[]) ?? [] : (settings.schema?.required as string[]) ?? [] const matched = currentArr.filter((c) => requiredParameters.every((key) => { diff --git a/src/plugin/plugin.test.ts b/src/plugin/plugin.test.ts index b60fb56..1171a50 100644 --- a/src/plugin/plugin.test.ts +++ b/src/plugin/plugin.test.ts @@ -324,4 +324,46 @@ describe('Plugin tests', () => { console.log(result); }) + + it('Returns allowMultiple for getResourceInfo', async () => { + const resource = spy(new class extends TestResource { + getSettings(): ResourceSettings { + return { + ...super.getSettings(), + allowMultiple: { + identifyingParameters: ['path', 'paths'] + } + } + } + }) + + const testPlugin = Plugin.create('testPlugin', [resource as any]); + + const resourceInfo = await testPlugin.getResourceInfo({ + type: 'testResource', + }) + + expect(resourceInfo.allowMultiple?.requiredParameters).toMatchObject([ + 'path', 'paths' + ]) + }) + + it('Returns an empty array by default for allowMultiple for getResourceInfo', async () => { + const resource = spy(new class extends TestResource { + getSettings(): ResourceSettings { + return { + ...super.getSettings(), + allowMultiple: true + } + } + }) + + const testPlugin = Plugin.create('testPlugin', [resource as any]); + + const resourceInfo = await testPlugin.getResourceInfo({ + type: 'testResource', + }) + + expect(resourceInfo.allowMultiple?.requiredParameters).toMatchObject([]) + }) }); diff --git a/src/plugin/plugin.ts b/src/plugin/plugin.ts index 25a3215..bf2938c 100644 --- a/src/plugin/plugin.ts +++ b/src/plugin/plugin.ts @@ -74,8 +74,8 @@ export class Plugin { const allowMultiple = resource.settings.allowMultiple !== undefined ? (typeof resource.settings.allowMultiple === 'boolean' - ? { requiredParameters: schema?.required ?? [] } - : { requiredParameters: resource.settings.allowMultiple.requiredParameters ?? schema?.required ?? [] } + ? { identifyingParameters: schema?.required ?? [] } + : { identifyingParameters: resource.settings.allowMultiple.identifyingParameters ?? schema?.required ?? [] } ) : undefined return { @@ -84,6 +84,10 @@ export class Plugin { dependencies: resource.dependencies, schema: schema as Record | undefined, importAndDestroy: { + preventImport: resource.settings.importAndDestroy?.preventImport, + requiredParameters: requiredPropertyNames, + }, + import: { requiredParameters: requiredPropertyNames, }, allowMultiple diff --git a/src/resource/resource-controller.ts b/src/resource/resource-controller.ts index 2b8e4c4..4b8a25d 100644 --- a/src/resource/resource-controller.ts +++ b/src/resource/resource-controller.ts @@ -214,6 +214,10 @@ export class ResourceController { core: ResourceConfig, parameters: Partial ): Promise | null> { + if (this.settings.importAndDestroy?.preventImport) { + throw new Error(`Type: ${this.typeId} cannot be imported`); + } + this.addDefaultValues(parameters); await this.applyTransformParameters(parameters); diff --git a/src/resource/resource-settings.ts b/src/resource/resource-settings.ts index e11de6b..bb6bfad 100644 --- a/src/resource/resource-settings.ts +++ b/src/resource/resource-settings.ts @@ -42,7 +42,7 @@ export interface ResourceSettings { * If paramA is required, then if resource1.paramA === resource2.paramA then are the same resource. * If resource1.paramA !== resource1.paramA, then they are different. */ - requiredParameters?: string[] + identifyingParameters?: string[] /** * If multiple copies are allowed then a matcher must be defined to match the desired @@ -103,6 +103,13 @@ export interface ResourceSettings { * ``` */ importAndDestroy?: { + /** + * Can this resources be imported? If set to false then the codifyCLI will skip over/not consider this + * resource valid for imports. Defaults to true. + * + * Resources that can't be imported in the core library for example are: action resources + */ + preventImport?: boolean; /** * Customize the required parameters needed to import this resource. By default, the `requiredParameters` are taken @@ -133,7 +140,7 @@ export interface ResourceSettings { * * See {@link importAndDestroy} for more information on how importing works. */ - defaultRefreshValues?: Partial + defaultRefreshValues?: Partial; } } From 0af79804c70fdc05bbbc8c506e55cf36161752f6 Mon Sep 17 00:00:00 2001 From: kevinwang Date: Tue, 18 Feb 2025 22:28:12 -0500 Subject: [PATCH 10/25] feat: Added feature to remove un-necessary default values from imports --- package.json | 2 +- src/resource/resource-controller.test.ts | 35 ++++++++++++++++++++++++ src/resource/resource-controller.ts | 15 ++++++++++ src/resource/resource-settings.ts | 4 +++ 4 files changed, 55 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 6d82a0b..f826120 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codify-plugin-lib", - "version": "1.0.141", + "version": "1.0.142", "description": "Library plugin library", "main": "dist/index.js", "typings": "dist/index.d.ts", diff --git a/src/resource/resource-controller.test.ts b/src/resource/resource-controller.test.ts index 35016be..9f4a8e9 100644 --- a/src/resource/resource-controller.test.ts +++ b/src/resource/resource-controller.test.ts @@ -711,4 +711,39 @@ describe('Resource tests', () => { } }) }) + + it('Applies removes default values if they remain default for imports', async () => { + const resource = new class extends TestResource { + getSettings(): ResourceSettings { + return { + id: 'resourceType', + parameterSettings: { + propA: { type: 'string', default: 'defaultValue' }, + propB: { type: 'boolean', default: true } + }, + } + } + + async refresh(parameters: Partial): Promise | null> { + return { + propA: 'defaultValue', + propB: false, + propC: 'newPropC' + } + } + } + + const controller = new ResourceController(resource); + const plan = await controller.import({ type: 'resourceType' }, {}); + + expect(plan![0]).toMatchObject({ + 'core': { + 'type': 'resourceType' + }, + 'parameters': { + propB: false, + propC: 'newPropC' + } + }) + }) }); diff --git a/src/resource/resource-controller.ts b/src/resource/resource-controller.ts index 4b8a25d..c894866 100644 --- a/src/resource/resource-controller.ts +++ b/src/resource/resource-controller.ts @@ -261,6 +261,8 @@ export class ResourceController { const resultParameters = { ...currentParametersArray[0], ...statefulCurrentParameters }; await this.applyTransformParameters(resultParameters, true); + this.removeDefaultValues(resultParameters, parameters) + return [{ core, parameters: resultParameters }]; } @@ -377,6 +379,19 @@ ${JSON.stringify(refresh, null, 2)} } } + private removeDefaultValues(newConfig: Partial | null, originalConfig: Partial): void { + if (!newConfig) { + return; + } + + for (const [key, defaultValue] of Object.entries(this.parsedSettings.defaultValues)) { + if (defaultValue !== undefined && (newConfig[key] === defaultValue || originalConfig[key] === undefined || originalConfig[key] === null)) { + delete newConfig[key]; + } + } + + } + private async refreshNonStatefulParameters(resourceParameters: Partial): Promise> | null> { const result = await this.resource.refresh(resourceParameters); diff --git a/src/resource/resource-settings.ts b/src/resource/resource-settings.ts index bb6bfad..1b29adc 100644 --- a/src/resource/resource-settings.ts +++ b/src/resource/resource-settings.ts @@ -368,6 +368,10 @@ const ParameterTransformationDefaults: Partial Date: Wed, 19 Feb 2025 10:10:21 -0500 Subject: [PATCH 11/25] feat: Added matcher request to match multiple requests together --- package-lock.json | 12 ++--- package.json | 4 +- src/messages/handlers.ts | 9 +++- src/plan/plan.ts | 39 ++++---------- src/plugin/plugin.test.ts | 40 ++++++++++++--- src/plugin/plugin.ts | 39 +++++++++++--- src/resource/parsed-resource-settings.ts | 7 ++- src/resource/resource-settings.test.ts | 65 ++++++++++++++++++++++++ src/resource/resource-settings.ts | 43 +++++++++++++++- 9 files changed, 203 insertions(+), 55 deletions(-) diff --git a/package-lock.json b/package-lock.json index 52e4a88..9f3aa43 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,19 +1,19 @@ { "name": "codify-plugin-lib", - "version": "1.0.141", + "version": "1.0.144", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "codify-plugin-lib", - "version": "1.0.141", + "version": "1.0.144", "license": "ISC", "dependencies": { "@homebridge/node-pty-prebuilt-multiarch": "^0.12.0-beta.5", "@npmcli/promise-spawn": "^7.0.1", "ajv": "^8.12.0", "ajv-formats": "^2.1.1", - "codify-schemas": "1.0.70", + "codify-schemas": "1.0.73", "lodash.isequal": "^4.5.0", "nanoid": "^5.0.9", "strip-ansi": "^7.1.0", @@ -2283,9 +2283,9 @@ } }, "node_modules/codify-schemas": { - "version": "1.0.70", - "resolved": "https://registry.npmjs.org/codify-schemas/-/codify-schemas-1.0.70.tgz", - "integrity": "sha512-LPcWt9NS5lhycTDxspFNDXDlDMPF5NNWL51DzhFC7q/fLLjwHM3cOjBtPYhkKnwGaQb+CMQcupVnEUOlMFt4DA==", + "version": "1.0.73", + "resolved": "https://registry.npmjs.org/codify-schemas/-/codify-schemas-1.0.73.tgz", + "integrity": "sha512-rokit3Ayz/B32NBZm4fsCb3/0Ju0lqhWu/K/soDV5ls3BqbuHd8Cg35D3FGgTxNqoGWLfLt5odHOuvMqS4Q/KA==", "dependencies": { "ajv": "^8.12.0" } diff --git a/package.json b/package.json index f826120..afddf85 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codify-plugin-lib", - "version": "1.0.142", + "version": "1.0.145", "description": "Library plugin library", "main": "dist/index.js", "typings": "dist/index.d.ts", @@ -16,7 +16,7 @@ "dependencies": { "ajv": "^8.12.0", "ajv-formats": "^2.1.1", - "codify-schemas": "1.0.70", + "codify-schemas": "1.0.73", "@npmcli/promise-spawn": "^7.0.1", "@homebridge/node-pty-prebuilt-multiarch": "^0.12.0-beta.5", "uuid": "^10.0.0", diff --git a/src/messages/handlers.ts b/src/messages/handlers.ts index 1fa8298..f728327 100644 --- a/src/messages/handlers.ts +++ b/src/messages/handlers.ts @@ -13,6 +13,8 @@ import { IpcMessageSchema, IpcMessageV2, IpcMessageV2Schema, + MatchRequestDataSchema, + MatchResponseDataSchema, MessageStatus, PlanRequestDataSchema, PlanResponseDataSchema, @@ -40,6 +42,11 @@ const SupportedRequests: Record plugin.match(data), + requestValidator: MatchRequestDataSchema, + responseValidator: MatchResponseDataSchema + }, 'import': { handler: async (plugin: Plugin, data: any) => plugin.import(data), requestValidator: ImportRequestDataSchema, @@ -106,7 +113,7 @@ export class MessageHandler { const responseValidator = this.responseValidators.get(message.cmd); if (responseValidator && !responseValidator(result)) { - throw new Error(`Plugin: ${this.plugin}. Malformed response data: ${JSON.stringify(responseValidator.errors, null, 2)}`) + throw new Error(`Plugin: ${this.plugin.name}. Malformed response data: ${JSON.stringify(responseValidator.errors, null, 2)}. Received ${JSON.stringify(result, null, 2)}`); } process.send!({ diff --git a/src/plan/plan.ts b/src/plan/plan.ts index b15b52d..3b70ae4 100644 --- a/src/plan/plan.ts +++ b/src/plan/plan.ts @@ -241,7 +241,7 @@ export class Plan { desired: Partial | null, currentArray: Partial[] | null, state: Partial | null, - settings: ResourceSettings, + settings: ParsedResourceSettings, isStateful: boolean, }): Partial | null { const { @@ -260,36 +260,15 @@ export class Plan { return null; } - const matcher = typeof settings.allowMultiple === 'boolean' || !settings.allowMultiple.matcher - ? ((desired: Partial, currentArr: Array>) => { - const requiredParameters = typeof settings.allowMultiple === 'object' - ? settings.allowMultiple?.identifyingParameters ?? (settings.schema?.required as string[]) ?? [] - : (settings.schema?.required as string[]) ?? [] - - const matched = currentArr.filter((c) => requiredParameters.every((key) => { - const currentParameter = c[key]; - const desiredParameter = desired[key]; - - if (!currentParameter) { - console.warn(`Unable to find required parameter for current ${currentParameter}`) - return false; - } - - if (!desiredParameter) { - console.warn(`Unable to find required parameter for current ${currentParameter}`) - return false; - } - - return currentParameter === desiredParameter; - })) - - if (matched.length > 1) { - console.warn(`Required parameters did not uniquely identify a resource: ${currentArray}. Defaulting to the first one`); - } + const { matcher: parameterMatcher, id } = settings; + const matcher = (desired: Partial, currentArray: Partial[]): Partial | undefined => { + const matched = currentArray.filter((c) => parameterMatcher(desired, c)) + if (matched.length > 0) { + console.log(`Resource: ${id} did not uniquely match resources when allow multiple is set to true`) + } - return matched[0]; - }) - : settings.allowMultiple.matcher + return matched[0]; + } if (isStateful) { return state diff --git a/src/plugin/plugin.test.ts b/src/plugin/plugin.test.ts index 1171a50..e60f67d 100644 --- a/src/plugin/plugin.test.ts +++ b/src/plugin/plugin.test.ts @@ -343,27 +343,51 @@ describe('Plugin tests', () => { type: 'testResource', }) - expect(resourceInfo.allowMultiple?.requiredParameters).toMatchObject([ - 'path', 'paths' - ]) + expect(resourceInfo.allowMultiple).to.be.true; }) - it('Returns an empty array by default for allowMultiple for getResourceInfo', async () => { + it('Can match resources together', async () => { const resource = spy(new class extends TestResource { getSettings(): ResourceSettings { return { ...super.getSettings(), - allowMultiple: true + parameterSettings: { + path: { type: 'directory' }, + paths: { type: 'array', itemType: 'directory' } + }, + allowMultiple: { + identifyingParameters: ['path', 'paths'] + } } } }) const testPlugin = Plugin.create('testPlugin', [resource as any]); - const resourceInfo = await testPlugin.getResourceInfo({ - type: 'testResource', + const { match } = await testPlugin.match({ + resource: { + core: { type: 'testResource' }, + parameters: { path: '/my/path', propA: 'abc' }, + }, + array: [ + { + core: { type: 'testResource' }, + parameters: { path: '/my/other/path', propA: 'abc' }, + }, + { + core: { type: 'testResource' }, + parameters: { paths: ['/my/path'], propA: 'def' }, + }, + { + core: { type: 'testResource' }, + parameters: { path: '/my/path', propA: 'hig' }, + }, + ] + }) + expect(match).toMatchObject({ + core: { type: 'testResource' }, + parameters: { path: '/my/path', propA: 'hig' }, }) - expect(resourceInfo.allowMultiple?.requiredParameters).toMatchObject([]) }) }); diff --git a/src/plugin/plugin.ts b/src/plugin/plugin.ts index bf2938c..f365ff0 100644 --- a/src/plugin/plugin.ts +++ b/src/plugin/plugin.ts @@ -6,6 +6,8 @@ import { ImportRequestData, ImportResponseData, InitializeResponseData, + MatchRequestData, + MatchResponseData, PlanRequestData, PlanResponseData, ResourceConfig, @@ -69,14 +71,11 @@ export class Plugin { const requiredPropertyNames = ( resource.settings.importAndDestroy?.requiredParameters ?? schema?.required - ?? null - ) as null | string[]; + ?? undefined + ) as any; const allowMultiple = resource.settings.allowMultiple !== undefined - ? (typeof resource.settings.allowMultiple === 'boolean' - ? { identifyingParameters: schema?.required ?? [] } - : { identifyingParameters: resource.settings.allowMultiple.identifyingParameters ?? schema?.required ?? [] } - ) : undefined + && resource.settings.allowMultiple !== false; return { plugin: this.name, @@ -94,6 +93,34 @@ export class Plugin { } } + async match(data: MatchRequestData): Promise { + const { resource: resourceConfig, array } = data; + + const resource = this.resourceControllers.get(resourceConfig.core.type); + if (!resource) { + throw new Error(`Resource of type ${resourceConfig.core.type} could not be found for match`); + } + + const parameterMatcher = resource?.parsedSettings.matcher; + const match = array.find((r) => { + if (resourceConfig.core.type !== r.core.type) { + return false; + } + + // If the user specifies the same name for the resource and it's not auto-generated (a number) then it's the same resource + if (resourceConfig.core.name === r.core.name + && resourceConfig.core.name + && Number.isInteger(Number.parseInt(resourceConfig.core.name, 10)) + ) { + return true; + } + + return parameterMatcher(resourceConfig.parameters, r.parameters); + }); + + return { match } + } + async import(data: ImportRequestData): Promise { const { core, parameters } = data; diff --git a/src/resource/parsed-resource-settings.ts b/src/resource/parsed-resource-settings.ts index cb22588..e1c25e4 100644 --- a/src/resource/parsed-resource-settings.ts +++ b/src/resource/parsed-resource-settings.ts @@ -9,6 +9,7 @@ import { ParameterSetting, resolveElementEqualsFn, resolveEqualsFn, + resolveMatcher, resolveParameterTransformFn, ResourceSettings, StatefulParameterSetting @@ -38,7 +39,7 @@ export class ParsedResourceSettings implements Re id!: string; schema?: Partial>; allowMultiple?: { - matcher?: (desired: Partial, current: Partial[]) => Partial; + matcher?: (desired: Partial, current: Partial) => boolean; requiredParameters?: string[] } | boolean; @@ -172,6 +173,10 @@ export class ParsedResourceSettings implements Re }); } + get matcher(): (desired: Partial, current: Partial) => boolean { + return resolveMatcher(this); + } + private validateSettings(): void { // validate parameter settings if (this.settings.parameterSettings) { diff --git a/src/resource/resource-settings.test.ts b/src/resource/resource-settings.test.ts index 26167c1..59f8b2e 100644 --- a/src/resource/resource-settings.test.ts +++ b/src/resource/resource-settings.test.ts @@ -1054,4 +1054,69 @@ describe('Resource parameter tests', () => { operation: ResourceOperation.NOOP, }) }) + + it('Supports matching using the identfying parameters', async () => { + const home = os.homedir() + const testPath = path.join(home, 'test/folder'); + + const resource = new class extends TestResource { + getSettings(): ResourceSettings { + return { + id: 'resourceType', + parameterSettings: { + propA: { type: 'array', itemType: 'directory' } + }, + allowMultiple: { + identifyingParameters: ['propA'] + } + } + } + }; + + const controller = new ResourceController(resource); + expect(controller.parsedSettings.matcher({ + propA: [testPath], + propB: 'random1', + }, { + propA: [testPath], + propB: 'random2', + })).to.be.true; + + expect(controller.parsedSettings.matcher({ + propA: [testPath], + propB: 'random1', + }, { + propA: [testPath, testPath], + propB: 'random2', + })).to.be.false; + }) + + it('Supports matching using custom matcher', async () => { + const home = os.homedir() + const testPath = path.join(home, 'test/folder'); + + const resource = new class extends TestResource { + getSettings(): ResourceSettings { + return { + id: 'resourceType', + parameterSettings: { + propA: { type: 'array', itemType: 'directory' } + }, + allowMultiple: { + identifyingParameters: ['propA'], + matcher: () => false, + } + } + } + }; + + const controller = new ResourceController(resource); + expect(controller.parsedSettings.matcher({ + propA: [testPath], + propB: 'random1', + }, { + propA: [testPath], + propB: 'random2', + })).to.be.false; + }) }) diff --git a/src/resource/resource-settings.ts b/src/resource/resource-settings.ts index 1b29adc..05cf9ab 100644 --- a/src/resource/resource-settings.ts +++ b/src/resource/resource-settings.ts @@ -53,7 +53,7 @@ export interface ResourceSettings { * * @return The matched resource. */ - matcher?: (desired: Partial, current: Partial[],) => Partial + matcher?: (desired: Partial, current: Partial) => boolean } | boolean /** @@ -408,3 +408,44 @@ export function resolveParameterTransformFn( return parameter.transformation ?? ParameterTransformationDefaults[parameter.type as ParameterSettingType] ?? undefined; } + +export function resolveMatcher( + settings: ResourceSettings +): (desired: Partial, current: Partial) => boolean { + + return typeof settings.allowMultiple === 'boolean' || !settings.allowMultiple?.matcher + ? ((desired: Partial, current: Partial) => { + if (!desired || !current) { + return false; + } + + const requiredParameters = typeof settings.allowMultiple === 'object' + ? settings.allowMultiple?.identifyingParameters ?? (settings.schema?.required as string[]) ?? [] + : (settings.schema?.required as string[]) ?? [] + + return requiredParameters.every((key) => { + const currentParameter = current[key]; + const desiredParameter = desired[key]; + + // If both desired and current don't have a certain parameter then we assume they are the same + if (!currentParameter && !desiredParameter) { + return true; + } + + if (!currentParameter) { + console.warn(`Unable to find required parameter for current ${currentParameter}`) + return false; + } + + if (!desiredParameter) { + console.warn(`Unable to find required parameter for current ${currentParameter}`) + return false; + } + + const parameterSetting = settings.parameterSettings?.[key]; + const isEq = parameterSetting ? resolveEqualsFn(parameterSetting) : null + return isEq?.(desiredParameter, currentParameter) ?? currentParameter === desiredParameter; + }) + }) + : settings.allowMultiple.matcher +} From 8a7fabe464ed130e7727a482b3de988fb2a9ea36 Mon Sep 17 00:00:00 2001 From: kevinwang Date: Fri, 21 Feb 2025 07:53:10 -0500 Subject: [PATCH 12/25] feat: Bug fixes and improvements --- package.json | 2 +- src/plan/plan.ts | 50 -------------------- src/plugin/plugin.test.ts | 60 +++++++++++++++++++++++- src/plugin/plugin.ts | 4 ++ src/resource/parsed-resource-settings.ts | 1 - src/resource/resource-controller.ts | 39 +++++++++++++++ src/resource/resource-settings.ts | 5 +- 7 files changed, 107 insertions(+), 54 deletions(-) diff --git a/package.json b/package.json index afddf85..7fbd679 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codify-plugin-lib", - "version": "1.0.145", + "version": "1.0.147", "description": "Library plugin library", "main": "dist/index.js", "typings": "dist/index.d.ts", diff --git a/src/plan/plan.ts b/src/plan/plan.ts index 3b70ae4..1ec041f 100644 --- a/src/plan/plan.ts +++ b/src/plan/plan.ts @@ -229,56 +229,6 @@ export class Plan { return this.coreParameters.type } - /** - * When multiples of the same resource are allowed, this matching function will match a given config with one of the - * existing configs on the system. For example if there are multiple versions of Android Studios installed, we can use - * the application name and location to match it to our desired configs name and location. - * - * @param params - * @private - */ - private static matchCurrentParameters(params: { - desired: Partial | null, - currentArray: Partial[] | null, - state: Partial | null, - settings: ParsedResourceSettings, - isStateful: boolean, - }): Partial | null { - const { - desired, - currentArray, - state, - settings, - isStateful - } = params; - - if (!settings.allowMultiple) { - return currentArray?.[0] ?? null; - } - - if (!currentArray) { - return null; - } - - const { matcher: parameterMatcher, id } = settings; - const matcher = (desired: Partial, currentArray: Partial[]): Partial | undefined => { - const matched = currentArray.filter((c) => parameterMatcher(desired, c)) - if (matched.length > 0) { - console.log(`Resource: ${id} did not uniquely match resources when allow multiple is set to true`) - } - - return matched[0]; - } - - if (isStateful) { - return state - ? matcher(state, currentArray) ?? null - : null - } - - return matcher(desired!, currentArray) ?? null; - } - /** * Only keep relevant params for the plan. We don't want to change settings that were not already * defined. diff --git a/src/plugin/plugin.test.ts b/src/plugin/plugin.test.ts index e60f67d..a934335 100644 --- a/src/plugin/plugin.test.ts +++ b/src/plugin/plugin.test.ts @@ -5,7 +5,7 @@ import { Resource } from '../resource/resource.js'; import { Plan } from '../plan/plan.js'; import { spy } from 'sinon'; import { ResourceSettings } from '../resource/resource-settings.js'; -import { TestConfig } from '../utils/test-utils.test.js'; +import { TestConfig, TestStatefulParameter } from '../utils/test-utils.test.js'; import { getPty } from '../pty/index.js'; interface TestConfig extends StringIndexedObject { @@ -389,5 +389,63 @@ describe('Plugin tests', () => { parameters: { path: '/my/path', propA: 'hig' }, }) + const match2 = await testPlugin.match({ + resource: { + core: { type: 'testResource' }, + parameters: { path: '/my/path', propA: 'abc' }, + }, + array: [] + }) + + expect(match2).toMatchObject({ + match: undefined, + }) + }) + + it('Can match resources together 2', { timeout: 3000000 }, async () => { + const resource = spy(new class extends TestResource { + getSettings(): ResourceSettings { + return { + id: 'ssh-config', + parameterSettings: { + hosts: { type: 'stateful', definition: new TestStatefulParameter() } + }, + importAndDestroy: { + refreshKeys: ['hosts'], + defaultRefreshValues: { hosts: [] }, + requiredParameters: [] + }, + dependencies: ['ssh-key'], + allowMultiple: { + matcher: (a, b) => a.hosts === b.hosts + } + } + } + }) + + const testPlugin = Plugin.create('testPlugin', [resource as any]); + + const { match } = await testPlugin.match({ + resource: { + core: { type: 'ssh-config' }, + parameters: { hosts: 'a' }, + }, + array: [ + { + core: { type: 'ssh-config' }, + parameters: { hosts: 'b' }, + }, + { + core: { type: 'ssh-config' }, + parameters: { hosts: 'a' }, + }, + { + core: { type: 'ssh-config' }, + parameters: { hosts: 'c' }, + }, + ] + }) + + console.log(match) }) }); diff --git a/src/plugin/plugin.ts b/src/plugin/plugin.ts index f365ff0..bab0c62 100644 --- a/src/plugin/plugin.ts +++ b/src/plugin/plugin.ts @@ -101,6 +101,10 @@ export class Plugin { throw new Error(`Resource of type ${resourceConfig.core.type} could not be found for match`); } + if (!resource.settings.allowMultiple) { + return { match: array.find((r) => r.core.type === resourceConfig.core.type) } + } + const parameterMatcher = resource?.parsedSettings.matcher; const match = array.find((r) => { if (resourceConfig.core.type !== r.core.type) { diff --git a/src/resource/parsed-resource-settings.ts b/src/resource/parsed-resource-settings.ts index e1c25e4..a29ece6 100644 --- a/src/resource/parsed-resource-settings.ts +++ b/src/resource/parsed-resource-settings.ts @@ -92,7 +92,6 @@ export class ParsedResourceSettings implements Re ...v, controller: spController, nestedSettings: spController?.parsedSettings, - definition: undefined, }; return [k, parsed as ParsedStatefulParameterSetting]; diff --git a/src/resource/resource-controller.ts b/src/resource/resource-controller.ts index c894866..c0634af 100644 --- a/src/resource/resource-controller.ts +++ b/src/resource/resource-controller.ts @@ -252,6 +252,11 @@ export class ResourceController { || this.settings.allowMultiple // Stateful parameters are not supported currently if allowMultiple is true || currentParametersArray.filter(Boolean).length === 0 ) { + for (const result of currentParametersArray ?? []) { + await this.applyTransformParameters(result, true); + this.removeDefaultValues(result, parameters) + } + return currentParametersArray ?.map((r) => ({ core, parameters: r })) ?? null; @@ -456,5 +461,39 @@ ${JSON.stringify(refresh, null, 2)} ? Object.keys((this.settings.schema as any)?.properties) : Object.keys(this.parsedSettings.parameterSettings); } + + /** + * When multiples of the same resource are allowed, this matching function will match a given config with one of the + * existing configs on the system. For example if there are multiple versions of Android Studios installed, we can use + * the application name and location to match it to our desired configs name and location. + * + * @param params + * @private + */ + private matchParameters( + desired: Partial | null, + currentArray: Partial[] | null + ): Partial | null { + if (!this.parsedSettings.allowMultiple) { + return currentArray?.[0] ?? null; + } + + if (!currentArray) { + return null; + } + + const { matcher: parameterMatcher, id } = this.parsedSettings; + const matcher = (desired: Partial, currentArray: Partial[]): Partial | undefined => { + const matched = currentArray.filter((c) => parameterMatcher(desired, c)) + if (matched.length > 0) { + console.log(`Resource: ${id} did not uniquely match resources when allow multiple is set to true`) + } + + return matched[0]; + } + + return matcher(desired!, currentArray) ?? null; + } + } diff --git a/src/resource/resource-settings.ts b/src/resource/resource-settings.ts index 05cf9ab..f6f308e 100644 --- a/src/resource/resource-settings.ts +++ b/src/resource/resource-settings.ts @@ -412,13 +412,16 @@ export function resolveParameterTransformFn( export function resolveMatcher( settings: ResourceSettings ): (desired: Partial, current: Partial) => boolean { - return typeof settings.allowMultiple === 'boolean' || !settings.allowMultiple?.matcher ? ((desired: Partial, current: Partial) => { if (!desired || !current) { return false; } + if (!settings.allowMultiple) { + throw new Error(`Matching only works when allow multiple is enabled. Type: ${settings.id}`) + } + const requiredParameters = typeof settings.allowMultiple === 'object' ? settings.allowMultiple?.identifyingParameters ?? (settings.schema?.required as string[]) ?? [] : (settings.schema?.required as string[]) ?? [] From 59fdf3ae68bf4828884aea386aee4c6a8e4ba201 Mon Sep 17 00:00:00 2001 From: kevinwang Date: Fri, 21 Feb 2025 08:29:52 -0500 Subject: [PATCH 13/25] feat: Allow resources with stateful parameters to allow multple as well --- src/plan/plan.ts | 50 ++++++++++++++ src/resource/parsed-resource-settings.ts | 7 +- src/resource/resource-controller.ts | 87 +++++++----------------- 3 files changed, 78 insertions(+), 66 deletions(-) diff --git a/src/plan/plan.ts b/src/plan/plan.ts index 1ec041f..3b70ae4 100644 --- a/src/plan/plan.ts +++ b/src/plan/plan.ts @@ -229,6 +229,56 @@ export class Plan { return this.coreParameters.type } + /** + * When multiples of the same resource are allowed, this matching function will match a given config with one of the + * existing configs on the system. For example if there are multiple versions of Android Studios installed, we can use + * the application name and location to match it to our desired configs name and location. + * + * @param params + * @private + */ + private static matchCurrentParameters(params: { + desired: Partial | null, + currentArray: Partial[] | null, + state: Partial | null, + settings: ParsedResourceSettings, + isStateful: boolean, + }): Partial | null { + const { + desired, + currentArray, + state, + settings, + isStateful + } = params; + + if (!settings.allowMultiple) { + return currentArray?.[0] ?? null; + } + + if (!currentArray) { + return null; + } + + const { matcher: parameterMatcher, id } = settings; + const matcher = (desired: Partial, currentArray: Partial[]): Partial | undefined => { + const matched = currentArray.filter((c) => parameterMatcher(desired, c)) + if (matched.length > 0) { + console.log(`Resource: ${id} did not uniquely match resources when allow multiple is set to true`) + } + + return matched[0]; + } + + if (isStateful) { + return state + ? matcher(state, currentArray) ?? null + : null + } + + return matcher(desired!, currentArray) ?? null; + } + /** * Only keep relevant params for the plan. We don't want to change settings that were not already * defined. diff --git a/src/resource/parsed-resource-settings.ts b/src/resource/parsed-resource-settings.ts index a29ece6..2495e22 100644 --- a/src/resource/parsed-resource-settings.ts +++ b/src/resource/parsed-resource-settings.ts @@ -188,9 +188,10 @@ export class ParsedResourceSettings implements Re } } - if (this.allowMultiple - && Object.values(this.parameterSettings).some((v) => v.type === 'stateful')) { - throw new Error(`Resource: ${this.id}. Stateful parameters are not allowed if multiples of a resource exist`) + if (Object.entries(this.parameterSettings).some(([k, v]) => + v.type === 'stateful' + && typeof this.settings.allowMultiple === 'object' && this.settings.allowMultiple?.identifyingParameters?.includes(k))) { + throw new Error(`Resource: ${this.id}. Stateful parameters are not allowed to be identifying parameters for allowMultiple.`) } const schema = this.settings.schema as JSONSchemaType; diff --git a/src/resource/resource-controller.ts b/src/resource/resource-controller.ts index c0634af..6a8151b 100644 --- a/src/resource/resource-controller.ts +++ b/src/resource/resource-controller.ts @@ -119,7 +119,6 @@ export class ResourceController { // Parse data from the user supplied config const parsedConfig = new ConfigParser(desired, state, this.parsedSettings.statefulParameters) const { - allParameters, allNonStatefulParameters, allStatefulParameters, } = parsedConfig; @@ -130,7 +129,6 @@ export class ResourceController { // Short circuit here. If the resource is non-existent, there's no point checking stateful parameters if (currentArray === null || currentArray === undefined - || this.settings.allowMultiple // Stateful parameters are not supported currently if allowMultiple is true || currentArray.length === 0 || currentArray.filter(Boolean).length === 0 ) { @@ -144,13 +142,13 @@ export class ResourceController { }); } - // Refresh stateful parameters. These parameters have state external to the resource. allowMultiple - // does not work together with stateful parameters - const statefulCurrentParameters = await this.refreshStatefulParameters(allStatefulParameters, allParameters); + // Refresh stateful parameters. These parameters have state external to the resource. Each variation of the + // current parameters (each array element) is passed into the stateful parameter refresh. + const statefulCurrentParameters = await this.refreshStatefulParameters(allStatefulParameters, currentArray); return Plan.calculate({ desired, - currentArray: [{ ...currentArray[0], ...statefulCurrentParameters }] as Partial[], + currentArray: currentArray.map((c, idx) => ({ ...c, ...statefulCurrentParameters[idx] })), state, core, settings: this.parsedSettings, @@ -249,26 +247,21 @@ export class ResourceController { if (currentParametersArray === null || currentParametersArray === undefined - || this.settings.allowMultiple // Stateful parameters are not supported currently if allowMultiple is true || currentParametersArray.filter(Boolean).length === 0 ) { - for (const result of currentParametersArray ?? []) { - await this.applyTransformParameters(result, true); - this.removeDefaultValues(result, parameters) - } - - return currentParametersArray - ?.map((r) => ({ core, parameters: r })) - ?? null; + return []; } - const statefulCurrentParameters = await this.refreshStatefulParameters(allStatefulParameters, parametersToRefresh); - const resultParameters = { ...currentParametersArray[0], ...statefulCurrentParameters }; + const statefulCurrentParameters = await this.refreshStatefulParameters(allStatefulParameters, currentParametersArray); + const resultParametersArray = currentParametersArray + ?.map((r, idx) => ({ ...r, ...statefulCurrentParameters[idx] })) - await this.applyTransformParameters(resultParameters, true); - this.removeDefaultValues(resultParameters, parameters) + for (const result of resultParametersArray) { + await this.applyTransformParameters(result, true); + this.removeDefaultValues(result, parameters); + } - return [{ core, parameters: resultParameters }]; + return resultParametersArray?.map((r) => ({ core, parameters: r })) } private async applyCreate(plan: Plan): Promise { @@ -410,21 +403,23 @@ ${JSON.stringify(refresh, null, 2)} // Refresh stateful parameters // This refreshes parameters that are stateful (they can be added, deleted separately from the resource) - private async refreshStatefulParameters(statefulParametersConfig: Partial, allParameters: Partial): Promise> { - const result: Partial = {} + private async refreshStatefulParameters(statefulParametersConfig: Partial, allParameters: Array>): Promise>> { + const result: Array> = Array.from({ length: allParameters.length }, () => ({})) const sortedEntries = Object.entries(statefulParametersConfig) .sort( ([key1], [key2]) => this.parsedSettings.statefulParameterOrder.get(key1)! - this.parsedSettings.statefulParameterOrder.get(key2)! ) - await Promise.all(sortedEntries.map(async ([key, desiredValue]) => { - const statefulParameter = this.parsedSettings.statefulParameters.get(key); - if (!statefulParameter) { - throw new Error(`Stateful parameter ${key} was not found`); - } + for (const [idx, refreshedParams] of allParameters.entries()) { + await Promise.all(sortedEntries.map(async ([key, desiredValue]) => { + const statefulParameter = this.parsedSettings.statefulParameters.get(key); + if (!statefulParameter) { + throw new Error(`Stateful parameter ${key} was not found`); + } - (result as Record)[key] = await statefulParameter.refresh(desiredValue ?? null, allParameters) - })) + (result[idx][key] as T[keyof T] | null) = await statefulParameter.refresh(desiredValue ?? null, refreshedParams) + })) + } return result; } @@ -461,39 +456,5 @@ ${JSON.stringify(refresh, null, 2)} ? Object.keys((this.settings.schema as any)?.properties) : Object.keys(this.parsedSettings.parameterSettings); } - - /** - * When multiples of the same resource are allowed, this matching function will match a given config with one of the - * existing configs on the system. For example if there are multiple versions of Android Studios installed, we can use - * the application name and location to match it to our desired configs name and location. - * - * @param params - * @private - */ - private matchParameters( - desired: Partial | null, - currentArray: Partial[] | null - ): Partial | null { - if (!this.parsedSettings.allowMultiple) { - return currentArray?.[0] ?? null; - } - - if (!currentArray) { - return null; - } - - const { matcher: parameterMatcher, id } = this.parsedSettings; - const matcher = (desired: Partial, currentArray: Partial[]): Partial | undefined => { - const matched = currentArray.filter((c) => parameterMatcher(desired, c)) - if (matched.length > 0) { - console.log(`Resource: ${id} did not uniquely match resources when allow multiple is set to true`) - } - - return matched[0]; - } - - return matcher(desired!, currentArray) ?? null; - } - } From a247e4faef84474f97fb8481cb551688a1744f72 Mon Sep 17 00:00:00 2001 From: kevinwang Date: Fri, 21 Feb 2025 08:45:12 -0500 Subject: [PATCH 14/25] feat: Updated validation checks to ensure that only one of an element exists if allowMultiple: false --- src/plugin/plugin.test.ts | 43 +++++++++++++++++++++++++++++++++++++++ src/plugin/plugin.ts | 28 ++++++++++++++++++++++++- 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/src/plugin/plugin.test.ts b/src/plugin/plugin.test.ts index a934335..53b3bb5 100644 --- a/src/plugin/plugin.test.ts +++ b/src/plugin/plugin.test.ts @@ -448,4 +448,47 @@ describe('Plugin tests', () => { console.log(match) }) + + it('Validates that a config correctly allows multiple of a config when allowMultiple is true', async () => { + const resource = spy(new class extends TestResource { + getSettings(): ResourceSettings { + return { + id: 'ssh-config', + allowMultiple: true + } + } + }) + + const testPlugin = Plugin.create('testPlugin', [resource as any]); + + const validate1 = await testPlugin.validate({ + configs: [{ core: { type: 'ssh-config' }, parameters: { propA: 'a' } }, { + core: { type: 'ssh-config' }, + parameters: { propB: 'b' } + }] + }) + + expect(validate1.resourceValidations.every((r) => r.isValid)).to.be.true; + }) + + it('Validates that a config correctly dis-allows multiple of a config when allowMultiple is false', async () => { + const resource = spy(new class extends TestResource { + getSettings(): ResourceSettings { + return { + id: 'ssh-config', + } + } + }) + + const testPlugin = Plugin.create('testPlugin', [resource as any]); + + const validate1 = await testPlugin.validate({ + configs: [{ core: { type: 'ssh-config' }, parameters: { propA: 'a' } }, { + core: { type: 'ssh-config' }, + parameters: { propB: 'b' } + }] + }) + + expect(validate1.resourceValidations.every((r) => r.isValid)).to.be.false; + }) }); diff --git a/src/plugin/plugin.ts b/src/plugin/plugin.ts index bab0c62..316c244 100644 --- a/src/plugin/plugin.ts +++ b/src/plugin/plugin.ts @@ -145,7 +145,7 @@ export class Plugin { } async validate(data: ValidateRequestData): Promise { - const validationResults = []; + const validationResults: ValidateResponseData['resourceValidations'] = []; for (const config of data.configs) { const { core, parameters } = config; @@ -160,6 +160,32 @@ export class Plugin { validationResults.push(validation); } + // Validate that if allow multiple is false, then only 1 of each resource exists + const countMap = data.configs.reduce((map, resource) => { + if (!map.has(resource.core.type)) { + map.set(resource.core.type, 0); + } + + const count = map.get(resource.core.type)!; + map.set(resource.core.type, count + 1) + + return map; + }, new Map()) + + const invalidMultipleConfigs = [...countMap.entries()].filter(([k, v]) => { + const controller = this.resourceControllers.get(k)!; + return !controller.parsedSettings.allowMultiple && v > 1; + }); + + if (invalidMultipleConfigs.length > 0) { + validationResults.push(...invalidMultipleConfigs.map(([k, v]) => ({ + resourceType: k, + schemaValidationErrors: [], + customValidationErrorMessage: `Multiple of resource type: ${k} found in configs. Only allowed 1.`, + isValid: false, + }))); + } + await this.crossValidateResources(data.configs); return { resourceValidations: validationResults From ba3fd920c98631c79ce309c0b8a46980e1f6812c Mon Sep 17 00:00:00 2001 From: kevinwang Date: Fri, 21 Feb 2025 09:00:15 -0500 Subject: [PATCH 15/25] fix: Fixed bug where certain parameters are not passed to stateful parameters --- package.json | 2 +- src/resource/resource-controller.ts | 18 ++++++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 7fbd679..a309ed3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codify-plugin-lib", - "version": "1.0.147", + "version": "1.0.149", "description": "Library plugin library", "main": "dist/index.js", "typings": "dist/index.d.ts", diff --git a/src/resource/resource-controller.ts b/src/resource/resource-controller.ts index 6a8151b..9339a2f 100644 --- a/src/resource/resource-controller.ts +++ b/src/resource/resource-controller.ts @@ -119,6 +119,7 @@ export class ResourceController { // Parse data from the user supplied config const parsedConfig = new ConfigParser(desired, state, this.parsedSettings.statefulParameters) const { + allParameters, allNonStatefulParameters, allStatefulParameters, } = parsedConfig; @@ -144,7 +145,7 @@ export class ResourceController { // Refresh stateful parameters. These parameters have state external to the resource. Each variation of the // current parameters (each array element) is passed into the stateful parameter refresh. - const statefulCurrentParameters = await this.refreshStatefulParameters(allStatefulParameters, currentArray); + const statefulCurrentParameters = await this.refreshStatefulParameters(allStatefulParameters, currentArray, allParameters); return Plan.calculate({ desired, @@ -239,6 +240,7 @@ export class ResourceController { // Parse data from the user supplied config const parsedConfig = new ConfigParser(parametersToRefresh, null, this.parsedSettings.statefulParameters) const { + allParameters, allNonStatefulParameters, allStatefulParameters, } = parsedConfig; @@ -252,7 +254,7 @@ export class ResourceController { return []; } - const statefulCurrentParameters = await this.refreshStatefulParameters(allStatefulParameters, currentParametersArray); + const statefulCurrentParameters = await this.refreshStatefulParameters(allStatefulParameters, currentParametersArray, allParameters); const resultParametersArray = currentParametersArray ?.map((r, idx) => ({ ...r, ...statefulCurrentParameters[idx] })) @@ -403,21 +405,25 @@ ${JSON.stringify(refresh, null, 2)} // Refresh stateful parameters // This refreshes parameters that are stateful (they can be added, deleted separately from the resource) - private async refreshStatefulParameters(statefulParametersConfig: Partial, allParameters: Array>): Promise>> { - const result: Array> = Array.from({ length: allParameters.length }, () => ({})) + private async refreshStatefulParameters( + statefulParametersConfig: Partial, + currentArray: Array>, + allParameters: Partial + ): Promise>> { + const result: Array> = Array.from({ length: currentArray.length }, () => ({})) const sortedEntries = Object.entries(statefulParametersConfig) .sort( ([key1], [key2]) => this.parsedSettings.statefulParameterOrder.get(key1)! - this.parsedSettings.statefulParameterOrder.get(key2)! ) - for (const [idx, refreshedParams] of allParameters.entries()) { + for (const [idx, refreshedParams] of currentArray.entries()) { await Promise.all(sortedEntries.map(async ([key, desiredValue]) => { const statefulParameter = this.parsedSettings.statefulParameters.get(key); if (!statefulParameter) { throw new Error(`Stateful parameter ${key} was not found`); } - (result[idx][key] as T[keyof T] | null) = await statefulParameter.refresh(desiredValue ?? null, refreshedParams) + (result[idx][key] as T[keyof T] | null) = await statefulParameter.refresh(desiredValue ?? null, { ...allParameters, ...refreshedParams }) })) } From 63fff9e1931ec6a90b835b6b2edd2e2ef4cc5ea2 Mon Sep 17 00:00:00 2001 From: kevinwang Date: Fri, 21 Feb 2025 10:16:14 -0500 Subject: [PATCH 16/25] fix: Changed error message type for multiple resources found --- package.json | 2 +- src/plugin/plugin.test.ts | 15 +++++++-------- src/plugin/plugin.ts | 11 +++++------ 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index a309ed3..a8fea93 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codify-plugin-lib", - "version": "1.0.149", + "version": "1.0.150", "description": "Library plugin library", "main": "dist/index.js", "typings": "dist/index.d.ts", diff --git a/src/plugin/plugin.test.ts b/src/plugin/plugin.test.ts index 53b3bb5..11e34b7 100644 --- a/src/plugin/plugin.test.ts +++ b/src/plugin/plugin.test.ts @@ -482,13 +482,12 @@ describe('Plugin tests', () => { const testPlugin = Plugin.create('testPlugin', [resource as any]); - const validate1 = await testPlugin.validate({ - configs: [{ core: { type: 'ssh-config' }, parameters: { propA: 'a' } }, { - core: { type: 'ssh-config' }, - parameters: { propB: 'b' } - }] - }) - - expect(validate1.resourceValidations.every((r) => r.isValid)).to.be.false; + await expect(() => testPlugin.validate({ + configs: [{ core: { type: 'ssh-config' }, parameters: { propA: 'a' } }, { + core: { type: 'ssh-config' }, + parameters: { propB: 'b' } + }] + }) + ).rejects.toThrowError(); }) }); diff --git a/src/plugin/plugin.ts b/src/plugin/plugin.ts index 316c244..3e5b331 100644 --- a/src/plugin/plugin.ts +++ b/src/plugin/plugin.ts @@ -178,12 +178,11 @@ export class Plugin { }); if (invalidMultipleConfigs.length > 0) { - validationResults.push(...invalidMultipleConfigs.map(([k, v]) => ({ - resourceType: k, - schemaValidationErrors: [], - customValidationErrorMessage: `Multiple of resource type: ${k} found in configs. Only allowed 1.`, - isValid: false, - }))); + throw new Error( + `Multiples of the following configs were found but only 1 is allowed. + +[${invalidMultipleConfigs.map(([k]) => k).join(', ')}] +`) } await this.crossValidateResources(data.configs); From 970c80f7920b709fbe396225c703e526a32163a2 Mon Sep 17 00:00:00 2001 From: kevinwang Date: Fri, 21 Feb 2025 14:24:35 -0500 Subject: [PATCH 17/25] fix: Added transformations for matcher. --- package.json | 2 +- src/plugin/plugin.ts | 28 +++---------------- src/resource/resource-controller.ts | 42 +++++++++++++++++++++++++++++ src/resource/resource-settings.ts | 2 +- 4 files changed, 47 insertions(+), 27 deletions(-) diff --git a/package.json b/package.json index a8fea93..4c75d22 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codify-plugin-lib", - "version": "1.0.150", + "version": "1.0.152", "description": "Library plugin library", "main": "dist/index.js", "typings": "dist/index.d.ts", diff --git a/src/plugin/plugin.ts b/src/plugin/plugin.ts index 3e5b331..12ad448 100644 --- a/src/plugin/plugin.ts +++ b/src/plugin/plugin.ts @@ -70,6 +70,7 @@ export class Plugin { const schema = resource.settings.schema as JSONSchemaType | undefined; const requiredPropertyNames = ( resource.settings.importAndDestroy?.requiredParameters + ?? (typeof resource.settings.allowMultiple === 'object' ? resource.settings.allowMultiple.identifyingParameters : null) ?? schema?.required ?? undefined ) as any; @@ -101,27 +102,7 @@ export class Plugin { throw new Error(`Resource of type ${resourceConfig.core.type} could not be found for match`); } - if (!resource.settings.allowMultiple) { - return { match: array.find((r) => r.core.type === resourceConfig.core.type) } - } - - const parameterMatcher = resource?.parsedSettings.matcher; - const match = array.find((r) => { - if (resourceConfig.core.type !== r.core.type) { - return false; - } - - // If the user specifies the same name for the resource and it's not auto-generated (a number) then it's the same resource - if (resourceConfig.core.name === r.core.name - && resourceConfig.core.name - && Number.isInteger(Number.parseInt(resourceConfig.core.name, 10)) - ) { - return true; - } - - return parameterMatcher(resourceConfig.parameters, r.parameters); - }); - + const match = await resource.match(resourceConfig, array); return { match } } @@ -179,10 +160,7 @@ export class Plugin { if (invalidMultipleConfigs.length > 0) { throw new Error( - `Multiples of the following configs were found but only 1 is allowed. - -[${invalidMultipleConfigs.map(([k]) => k).join(', ')}] -`) + `Multiples of the following configs were found but only 1 is allowed. [${invalidMultipleConfigs.map(([k, v]) => `${v}x ${k}`).join(', ')}] found.`) } await this.crossValidateResources(data.configs); diff --git a/src/resource/resource-controller.ts b/src/resource/resource-controller.ts index 9339a2f..fa33c77 100644 --- a/src/resource/resource-controller.ts +++ b/src/resource/resource-controller.ts @@ -102,6 +102,48 @@ export class ResourceController { } } + async match(resource: ResourceJson, array: Array): Promise { + if (resource.core.type !== this.typeId) { + throw new Error(`Unknown type passed into match method: ${resource.core.type} for ${this.typeId}`); + } + + if (!this.parsedSettings.allowMultiple) { + return array.find((r) => r.core.type === resource.core.type) + } + + + const { name, type } = resource.core; + const parameterMatcher = this.parsedSettings.matcher; + + for (const resourceToMatch of array) { + if (type !== resourceToMatch.core.type) { + return undefined; + } + + // If the user specifies the same name for the resource and it's not auto-generated (a number) then it's the same resource + if (name === resourceToMatch.core.name + && name + && Number.isInteger(Number.parseInt(name, 10)) + ) { + return resourceToMatch; + } + + const originalParams = structuredClone(resource.parameters) as Partial; + const paramsToMatch = structuredClone(resourceToMatch.parameters) as Partial; + + this.addDefaultValues(originalParams); + await this.applyTransformParameters(originalParams); + + this.addDefaultValues(paramsToMatch); + await this.applyTransformParameters(paramsToMatch); + + const match = parameterMatcher(originalParams, paramsToMatch); + if (match) { + return resourceToMatch; + } + } + } + async plan( core: ResourceConfig, desired: Partial | null, diff --git a/src/resource/resource-settings.ts b/src/resource/resource-settings.ts index f6f308e..8f627d0 100644 --- a/src/resource/resource-settings.ts +++ b/src/resource/resource-settings.ts @@ -113,7 +113,7 @@ export interface ResourceSettings { /** * Customize the required parameters needed to import this resource. By default, the `requiredParameters` are taken - * from the JSON schema. The `requiredParameters` parameter must be declared if a complex required is declared in + * from the identifyingParameters for allowMultiple. The `requiredParameters` parameter must be declared if a complex required is declared in * the schema (contains `oneOf`, `anyOf`, `allOf`, `if`, `then`, `else`). *
* The user will be prompted for the required parameters before the import starts. This is done because for most resources From 4e20a5e4b7e0ccaad08232969065ddaf819d327f Mon Sep 17 00:00:00 2001 From: kevinwang Date: Sat, 22 Feb 2025 12:49:21 -0500 Subject: [PATCH 18/25] feat: Added variable resolution for paths. Changed match length to be greater than 1 --- package.json | 2 +- src/plan/plan.ts | 2 +- src/resource/resource-settings.ts | 14 ++++++++++---- src/utils/utils.test.ts | 24 +++++++++++++++++++++++- src/utils/utils.ts | 23 +++++++++++++++++++++++ 5 files changed, 58 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 4c75d22..f8a9394 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codify-plugin-lib", - "version": "1.0.152", + "version": "1.0.154", "description": "Library plugin library", "main": "dist/index.js", "typings": "dist/index.d.ts", diff --git a/src/plan/plan.ts b/src/plan/plan.ts index 3b70ae4..7683a11 100644 --- a/src/plan/plan.ts +++ b/src/plan/plan.ts @@ -263,7 +263,7 @@ export class Plan { const { matcher: parameterMatcher, id } = settings; const matcher = (desired: Partial, currentArray: Partial[]): Partial | undefined => { const matched = currentArray.filter((c) => parameterMatcher(desired, c)) - if (matched.length > 0) { + if (matched.length > 1) { console.log(`Resource: ${id} did not uniquely match resources when allow multiple is set to true`) } diff --git a/src/resource/resource-settings.ts b/src/resource/resource-settings.ts index 8f627d0..ce1dab5 100644 --- a/src/resource/resource-settings.ts +++ b/src/resource/resource-settings.ts @@ -4,7 +4,7 @@ import isObjectsEqual from 'lodash.isequal' import path from 'node:path'; import { ArrayStatefulParameter, StatefulParameter } from '../stateful-parameter/stateful-parameter.js'; -import { areArraysEqual, tildify, untildify } from '../utils/utils.js'; +import { addVariablesToPath, areArraysEqual, resolvePathWithVariables, tildify, untildify } from '../utils/utils.js'; export interface InputTransformation { to: (input: any) => Promise | any; @@ -302,7 +302,13 @@ export interface StatefulParameterSetting extends DefaultParameterSetting { const ParameterEqualsDefaults: Partial boolean>> = { 'boolean': (a: unknown, b: unknown) => Boolean(a) === Boolean(b), - 'directory': (a: unknown, b: unknown) => path.resolve(untildify(String(a))) === path.resolve(untildify(String(b))), + 'directory': (a: unknown, b: unknown) => { + const notCaseSensitive = process.platform === 'darwin'; + const transformedA = path.resolve(resolvePathWithVariables(untildify(notCaseSensitive ? String(a).toLowerCase() : String(a)))) + const transformedB = path.resolve(resolvePathWithVariables(untildify(notCaseSensitive ? String(b).toLowerCase() : String(b)))) + + return transformedA === transformedB; + }, 'number': (a: unknown, b: unknown) => Number(a) === Number(b), 'string': (a: unknown, b: unknown) => String(a) === String(b), 'version': (desired: unknown, current: unknown) => String(current).includes(String(desired)), @@ -362,8 +368,8 @@ export function resolveFnFromEqualsFnOrString( const ParameterTransformationDefaults: Partial> = { 'directory': { - to: (a: unknown) => path.resolve(untildify(String(a))), - from: (a: unknown) => tildify(String(a)), + to: (a: unknown) => path.resolve(resolvePathWithVariables((untildify(String(a))))), + from: (a: unknown) => addVariablesToPath(tildify(String(a))), }, 'string': { to: String, diff --git a/src/utils/utils.test.ts b/src/utils/utils.test.ts index a420852..8b6fcbc 100644 --- a/src/utils/utils.test.ts +++ b/src/utils/utils.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { splitUserConfig } from './utils.js'; +import { addVariablesToPath, resolvePathWithVariables, splitUserConfig } from './utils.js'; +import os from 'node:os'; describe('Utils tests', () => { it('Can split a config correctly', () => { @@ -26,4 +27,25 @@ describe('Utils tests', () => { propD: 'propD', }) }) + + it('Can remove variables from a path', () => { + const testPath1 = '$HOME/my/path'; + const result1 = resolvePathWithVariables(testPath1); + + const home = os.homedir(); + expect(result1).to.eq(home + '/my/path'); + + + const testPath2 = '/var$HOME/my/path'; + const result2 = resolvePathWithVariables(testPath2); + expect(result2).to.eq('/var' + home + '/my/path'); + }) + + it('Can add variables to a path', () => { + const testPath1 = os.homedir() + '/my/path'; + const result1 = addVariablesToPath(testPath1); + + const home = os.homedir(); + expect(result1).to.eq('$HOME/my/path'); + }) }) diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 4b0314a..60c98d8 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -1,5 +1,6 @@ import { ResourceConfig, StringIndexedObject } from 'codify-schemas'; import os from 'node:os'; +import path from 'node:path'; export function isDebug(): boolean { return process.env.DEBUG != null && process.env.DEBUG.includes('codify'); // TODO: replace with debug library @@ -37,6 +38,28 @@ export function tildify(pathWithTilde: string) { return homeDirectory ? pathWithTilde.replace(homeDirectory, '~') : pathWithTilde; } +export function resolvePathWithVariables(pathWithVariables: string) { + // @ts-expect-error Ignore this for now + return pathWithVariables.replace(/\$([A-Z_]+[A-Z0-9_]*)|\${([A-Z0-9_]*)}/ig, (_, a, b) => process.env[a || b]) +} + +export function addVariablesToPath(pathWithoutVariables: string) { + let result = pathWithoutVariables; + for (const [key, value] of Object.entries(process.env)) { + if (!value || !path.isAbsolute(value) || value === '/') { + continue; + } + + result = result.replaceAll(value, `$${key}`) + } + + return result; +} + +export function unhome(pathWithHome: string): string { + return pathWithHome.includes('$HOME') ? pathWithHome.replaceAll('$HOME', os.homedir()) : pathWithHome; +} + export function areArraysEqual( isElementEqual: ((desired: unknown, current: unknown) => boolean) | undefined, desired: unknown, From 62b60a9b22368b71fc430cea69b48dbf6f6ad46c Mon Sep 17 00:00:00 2001 From: kevinwang Date: Sat, 22 Feb 2025 14:07:03 -0500 Subject: [PATCH 19/25] feat: Added refresh context --- src/resource/resource-controller.test.ts | 47 ++++++++++++++++++++++++ src/resource/resource-controller.ts | 21 ++++++++--- src/resource/resource.ts | 10 ++++- 3 files changed, 72 insertions(+), 6 deletions(-) diff --git a/src/resource/resource-controller.test.ts b/src/resource/resource-controller.test.ts index 9f4a8e9..2a294be 100644 --- a/src/resource/resource-controller.test.ts +++ b/src/resource/resource-controller.test.ts @@ -746,4 +746,51 @@ describe('Resource tests', () => { } }) }) + + it('Can plan with settings', async () => { + const resource = new class extends TestResource { + getSettings(): ResourceSettings { + return { + id: 'path', + parameterSettings: { + path: { type: 'string', isEqual: 'directory' }, + paths: { canModify: true, type: 'array', isElementEqual: 'directory' }, + prepend: { default: false, setting: true }, + declarationsOnly: { default: false, setting: true }, + }, + importAndDestroy: { + refreshKeys: ['paths', 'declarationsOnly'], + defaultRefreshValues: { + paths: [], + declarationsOnly: true, + } + }, + allowMultiple: { + matcher: (desired, current) => { + // console.log('Matcher'); + // console.log(desired); + // console.log(current); + + if (desired.path) { + return desired.path === current.path; + } + + const currentPaths = new Set(current.paths) + return desired.paths?.some((p) => currentPaths.has(p)); + } + } + } + } + + async refresh(parameters: Partial): Promise | null> { + return { path: '$HOME/.bun/bin', prepend: false, declarationsOnly: false } + } + } + + const controller = new ResourceController(resource); + const plan = await controller.plan({ type: 'path' }, { path: '$HOME/.bun/bin' }, null, false); + + expect(plan.requiresChanges()).to.be.false; + console.log(JSON.stringify(plan, null, 2)); + }) }); diff --git a/src/resource/resource-controller.ts b/src/resource/resource-controller.ts index fa33c77..16ac19e 100644 --- a/src/resource/resource-controller.ts +++ b/src/resource/resource-controller.ts @@ -13,7 +13,7 @@ import { Plan } from '../plan/plan.js'; import { CreatePlan, DestroyPlan, ModifyPlan } from '../plan/plan-types.js'; import { ConfigParser } from './config-parser.js'; import { ParsedResourceSettings } from './parsed-resource-settings.js'; -import { Resource } from './resource.js'; +import { RefreshContext, Resource } from './resource.js'; import { ResourceSettings } from './resource-settings.js'; export class ResourceController { @@ -151,6 +151,11 @@ export class ResourceController { isStateful = false, ): Promise> { this.validatePlanInputs(core, desired, state, isStateful); + const context: RefreshContext = { + commandType: 'plan', + isStateful, + originalDesiredConfig: structuredClone(desired), + }; this.addDefaultValues(desired); await this.applyTransformParameters(desired); @@ -167,7 +172,7 @@ export class ResourceController { } = parsedConfig; // Refresh resource parameters. This refreshes the parameters that configure the resource itself - const currentArray = await this.refreshNonStatefulParameters(allNonStatefulParameters); + const currentArray = await this.refreshNonStatefulParameters(allNonStatefulParameters, context); // Short circuit here. If the resource is non-existent, there's no point checking stateful parameters if (currentArray === null @@ -259,6 +264,12 @@ export class ResourceController { throw new Error(`Type: ${this.typeId} cannot be imported`); } + const context: RefreshContext = { + commandType: 'import', + isStateful: true, + originalDesiredConfig: structuredClone(parameters), + }; + this.addDefaultValues(parameters); await this.applyTransformParameters(parameters); @@ -287,7 +298,7 @@ export class ResourceController { allStatefulParameters, } = parsedConfig; - const currentParametersArray = await this.refreshNonStatefulParameters(allNonStatefulParameters); + const currentParametersArray = await this.refreshNonStatefulParameters(allNonStatefulParameters, context); if (currentParametersArray === null || currentParametersArray === undefined @@ -434,8 +445,8 @@ ${JSON.stringify(refresh, null, 2)} } - private async refreshNonStatefulParameters(resourceParameters: Partial): Promise> | null> { - const result = await this.resource.refresh(resourceParameters); + private async refreshNonStatefulParameters(resourceParameters: Partial, context: RefreshContext): Promise> | null> { + const result = await this.resource.refresh(resourceParameters, context); const currentParametersArray = Array.isArray(result) || result === null ? result diff --git a/src/resource/resource.ts b/src/resource/resource.ts index 1c25537..780191a 100644 --- a/src/resource/resource.ts +++ b/src/resource/resource.ts @@ -4,6 +4,12 @@ import { ParameterChange } from '../plan/change-set.js'; import { CreatePlan, DestroyPlan, ModifyPlan } from '../plan/plan-types.js'; import { ResourceSettings } from './resource-settings.js'; +export interface RefreshContext { + isStateful: boolean; + commandType: 'destroy' | 'import' | 'plan'; + originalDesiredConfig: Partial | null; +} + /** * A resource represents an object on the system (application, CLI tool, or setting) * that has state and can be created and destroyed. Examples of resources include CLI tools @@ -73,10 +79,12 @@ export abstract class Resource { * of the desired config. In stateful mode, this will be parameters of the state config + the desired * config of any new parameters. * + * @param context Context surrounding the request + * * @return A config or an array of configs representing the status of the resource on the * system currently */ - abstract refresh(parameters: Partial): Promise> | Partial | null>; + abstract refresh(parameters: Partial, context: RefreshContext): Promise> | Partial | null>; /** * Create the resource (install) based on the parameters passed in. Only the desired parameters will From 2607ad139c9a3c648ff60801d72cebc90d47705f Mon Sep 17 00:00:00 2001 From: kevinwang Date: Sat, 22 Feb 2025 14:23:22 -0500 Subject: [PATCH 20/25] fix: Fixed defaultRefreshValue not being used if the parameter has a default value --- package-lock.json | 4 +- package.json | 2 +- src/resource/resource-controller.test.ts | 47 +++++++++++++++++++++--- src/resource/resource-controller.ts | 8 ++++ 4 files changed, 53 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9f3aa43..afe21f0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "codify-plugin-lib", - "version": "1.0.144", + "version": "1.0.155", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "codify-plugin-lib", - "version": "1.0.144", + "version": "1.0.155", "license": "ISC", "dependencies": { "@homebridge/node-pty-prebuilt-multiarch": "^0.12.0-beta.5", diff --git a/package.json b/package.json index f8a9394..6b835a4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codify-plugin-lib", - "version": "1.0.154", + "version": "1.0.156", "description": "Library plugin library", "main": "dist/index.js", "typings": "dist/index.d.ts", diff --git a/src/resource/resource-controller.test.ts b/src/resource/resource-controller.test.ts index 2a294be..4f620f8 100644 --- a/src/resource/resource-controller.test.ts +++ b/src/resource/resource-controller.test.ts @@ -767,10 +767,6 @@ describe('Resource tests', () => { }, allowMultiple: { matcher: (desired, current) => { - // console.log('Matcher'); - // console.log(desired); - // console.log(current); - if (desired.path) { return desired.path === current.path; } @@ -791,6 +787,47 @@ describe('Resource tests', () => { const plan = await controller.plan({ type: 'path' }, { path: '$HOME/.bun/bin' }, null, false); expect(plan.requiresChanges()).to.be.false; - console.log(JSON.stringify(plan, null, 2)); + }) + + it('Can import with the correct default parameters', async () => { + const resource = new class extends TestResource { + getSettings(): ResourceSettings { + return { + id: 'path', + parameterSettings: { + path: { type: 'string', isEqual: 'directory' }, + paths: { canModify: true, type: 'array', isElementEqual: 'directory' }, + prepend: { default: false, setting: true }, + declarationsOnly: { default: false, setting: true }, + }, + importAndDestroy: { + refreshKeys: ['paths', 'declarationsOnly'], + defaultRefreshValues: { + paths: [], + declarationsOnly: true, + } + }, + allowMultiple: { + matcher: (desired, current) => { + if (desired.path) { + return desired.path === current.path; + } + + const currentPaths = new Set(current.paths) + return desired.paths?.some((p) => currentPaths.has(p)); + } + } + } + } + + async refresh(parameters: Partial): Promise | null> { + expect(parameters.declarationsOnly).to.be.true; + + return null; + } + } + + const controller = new ResourceController(resource); + const plan = await controller.import({ type: 'path' }, {}); }) }); diff --git a/src/resource/resource-controller.ts b/src/resource/resource-controller.ts index 16ac19e..6df0105 100644 --- a/src/resource/resource-controller.ts +++ b/src/resource/resource-controller.ts @@ -281,6 +281,14 @@ export class ResourceController { ), ...this.settings.importAndDestroy?.defaultRefreshValues, ...parameters, + ...(Object.fromEntries( // If a default value was used, but it was also declared in the defaultRefreshValues, prefer the defaultRefreshValue instead + Object.entries(parameters).filter(([k, v]) => + this.parsedSettings.defaultValues[k] !== undefined + && v === this.parsedSettings.defaultValues[k] + && context.originalDesiredConfig?.[k] === undefined + && this.settings.importAndDestroy?.defaultRefreshValues?.[k] !== undefined + ).map(([k]) => [k, this.settings.importAndDestroy!.defaultRefreshValues![k]]) + )) } : { ...Object.fromEntries( From ef29ea2c48289b11dbf64b48e5ce6c0189f4fef5 Mon Sep 17 00:00:00 2001 From: kevinwang Date: Sat, 22 Feb 2025 22:18:40 -0500 Subject: [PATCH 21/25] feat: Improved transformations by providing the original back in from, that way the user has the option to return the original if they are the same. Added a custom refreshMapper to apply logic for generating the refresh keys --- package.json | 2 +- src/resource/resource-controller.ts | 62 ++++++++++++++++------------- src/resource/resource-settings.ts | 24 +++++++++-- src/utils/utils.ts | 2 +- 4 files changed, 57 insertions(+), 33 deletions(-) diff --git a/package.json b/package.json index 6b835a4..707b652 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codify-plugin-lib", - "version": "1.0.156", + "version": "1.0.161", "description": "Library plugin library", "main": "dist/index.js", "typings": "dist/index.d.ts", diff --git a/src/resource/resource-controller.ts b/src/resource/resource-controller.ts index 6df0105..cf20195 100644 --- a/src/resource/resource-controller.ts +++ b/src/resource/resource-controller.ts @@ -274,29 +274,7 @@ export class ResourceController { await this.applyTransformParameters(parameters); // Use refresh parameters if specified, otherwise try to refresh as many parameters as possible here - const parametersToRefresh = this.settings.importAndDestroy?.refreshKeys - ? { - ...Object.fromEntries( - this.settings.importAndDestroy?.refreshKeys.map((k) => [k, null]) - ), - ...this.settings.importAndDestroy?.defaultRefreshValues, - ...parameters, - ...(Object.fromEntries( // If a default value was used, but it was also declared in the defaultRefreshValues, prefer the defaultRefreshValue instead - Object.entries(parameters).filter(([k, v]) => - this.parsedSettings.defaultValues[k] !== undefined - && v === this.parsedSettings.defaultValues[k] - && context.originalDesiredConfig?.[k] === undefined - && this.settings.importAndDestroy?.defaultRefreshValues?.[k] !== undefined - ).map(([k]) => [k, this.settings.importAndDestroy!.defaultRefreshValues![k]]) - )) - } - : { - ...Object.fromEntries( - this.getAllParameterKeys().map((k) => [k, null]) - ), - ...this.settings.importAndDestroy?.defaultRefreshValues, - ...parameters, - }; + const parametersToRefresh = this.getParametersToRefreshForImport(parameters, context); // Parse data from the user supplied config const parsedConfig = new ConfigParser(parametersToRefresh, null, this.parsedSettings.statefulParameters) @@ -320,7 +298,7 @@ export class ResourceController { ?.map((r, idx) => ({ ...r, ...statefulCurrentParameters[idx] })) for (const result of resultParametersArray) { - await this.applyTransformParameters(result, true); + await this.applyTransformParameters(result, { original: parameters }); this.removeDefaultValues(result, parameters); } @@ -403,7 +381,7 @@ ${JSON.stringify(refresh, null, 2)} } } - private async applyTransformParameters(config: Partial | null, reverse = false): Promise { + private async applyTransformParameters(config: Partial | null, reverse?: { original: Partial }): Promise { if (!config) { return; } @@ -414,13 +392,13 @@ ${JSON.stringify(refresh, null, 2)} } (config as Record)[key] = reverse - ? await inputTransformation.from(config[key]) + ? await inputTransformation.from(config[key], reverse.original?.[key]) : await inputTransformation.to(config[key]); } if (this.settings.transformation) { const transformed = reverse - ? await this.settings.transformation.from({ ...config }) + ? await this.settings.transformation.from({ ...config }, reverse.original) : await this.settings.transformation.to({ ...config }) Object.keys(config).forEach((k) => delete config[k]) @@ -523,5 +501,35 @@ ${JSON.stringify(refresh, null, 2)} ? Object.keys((this.settings.schema as any)?.properties) : Object.keys(this.parsedSettings.parameterSettings); } + + private getParametersToRefreshForImport(parameters: Partial, context: RefreshContext): Partial { + if (this.settings.importAndDestroy?.refreshMapper) { + return this.settings.importAndDestroy?.refreshMapper(parameters, context); + } + + return this.settings.importAndDestroy?.refreshKeys + ? { + ...Object.fromEntries( + this.settings.importAndDestroy?.refreshKeys.map((k) => [k, null]) + ), + ...this.settings.importAndDestroy?.defaultRefreshValues, + ...parameters, + ...(Object.fromEntries( // If a default value was used, but it was also declared in the defaultRefreshValues, prefer the defaultRefreshValue instead + Object.entries(parameters).filter(([k, v]) => + this.parsedSettings.defaultValues[k] !== undefined + && v === this.parsedSettings.defaultValues[k] + && context.originalDesiredConfig?.[k] === undefined + && this.settings.importAndDestroy?.defaultRefreshValues?.[k] !== undefined + ).map(([k]) => [k, this.settings.importAndDestroy!.defaultRefreshValues![k]]) + )) + } + : { + ...Object.fromEntries( + this.getAllParameterKeys().map((k) => [k, null]) + ), + ...this.settings.importAndDestroy?.defaultRefreshValues, + ...parameters, + }; + } } diff --git a/src/resource/resource-settings.ts b/src/resource/resource-settings.ts index ce1dab5..e388792 100644 --- a/src/resource/resource-settings.ts +++ b/src/resource/resource-settings.ts @@ -5,10 +5,11 @@ import path from 'node:path'; import { ArrayStatefulParameter, StatefulParameter } from '../stateful-parameter/stateful-parameter.js'; import { addVariablesToPath, areArraysEqual, resolvePathWithVariables, tildify, untildify } from '../utils/utils.js'; +import { RefreshContext } from './resource.js'; export interface InputTransformation { to: (input: any) => Promise | any; - from: (current: any) => Promise | any; + from: (current: any, original: any) => Promise | any; } /** @@ -141,6 +142,15 @@ export interface ResourceSettings { * See {@link importAndDestroy} for more information on how importing works. */ defaultRefreshValues?: Partial; + + /** + * A custom function that maps the input to what gets passed to refresh for imports. If this is set, then refreshKeys and + * defaultRefreshValues are ignored. + * + * @param input + * @param context + */ + refreshMapper?: (input: Partial, context: RefreshContext) => Partial } } @@ -369,7 +379,13 @@ export function resolveFnFromEqualsFnOrString( const ParameterTransformationDefaults: Partial> = { 'directory': { to: (a: unknown) => path.resolve(resolvePathWithVariables((untildify(String(a))))), - from: (a: unknown) => addVariablesToPath(tildify(String(a))), + from: (a: unknown, original) => { + if (ParameterEqualsDefaults.directory!(a, original)) { + return original; + } + + return tildify(addVariablesToPath(String(a))) + }, }, 'string': { to: String, @@ -406,8 +422,8 @@ export function resolveParameterTransformFn( to(input: unknown[]) { return input.map((i) => itemTransformation.to(i)) }, - from(input: unknown[]) { - return input.map((i) => itemTransformation.from(i)) + from(input: unknown[], original) { + return input.map((i) => itemTransformation.from(i, original)) } } } diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 60c98d8..aaa5868 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -46,7 +46,7 @@ export function resolvePathWithVariables(pathWithVariables: string) { export function addVariablesToPath(pathWithoutVariables: string) { let result = pathWithoutVariables; for (const [key, value] of Object.entries(process.env)) { - if (!value || !path.isAbsolute(value) || value === '/') { + if (!value || !path.isAbsolute(value) || value === '/' || key === 'HOME' || key === 'PATH' || key === 'SHELL' || key === 'PWD') { continue; } From eab6d428325c65c02d60e3dfc99cbc54f50af921 Mon Sep 17 00:00:00 2001 From: kevinwang Date: Sat, 22 Feb 2025 23:56:58 -0500 Subject: [PATCH 22/25] fix: Pass in the original desired parameter (before transformations) for original. Changed directory to return original element if it matches --- package.json | 2 +- src/resource/resource-controller.ts | 6 ++++-- src/resource/resource-settings.ts | 8 +++++++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 707b652..bca75a9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codify-plugin-lib", - "version": "1.0.161", + "version": "1.0.163", "description": "Library plugin library", "main": "dist/index.js", "typings": "dist/index.d.ts", diff --git a/src/resource/resource-controller.ts b/src/resource/resource-controller.ts index cf20195..548f4a2 100644 --- a/src/resource/resource-controller.ts +++ b/src/resource/resource-controller.ts @@ -298,7 +298,7 @@ export class ResourceController { ?.map((r, idx) => ({ ...r, ...statefulCurrentParameters[idx] })) for (const result of resultParametersArray) { - await this.applyTransformParameters(result, { original: parameters }); + await this.applyTransformParameters(result, { original: context.originalDesiredConfig }); this.removeDefaultValues(result, parameters); } @@ -381,7 +381,9 @@ ${JSON.stringify(refresh, null, 2)} } } - private async applyTransformParameters(config: Partial | null, reverse?: { original: Partial }): Promise { + private async applyTransformParameters(config: Partial | null, reverse?: { + original: Partial | null + }): Promise { if (!config) { return; } diff --git a/src/resource/resource-settings.ts b/src/resource/resource-settings.ts index e388792..0a10e31 100644 --- a/src/resource/resource-settings.ts +++ b/src/resource/resource-settings.ts @@ -423,7 +423,13 @@ export function resolveParameterTransformFn( return input.map((i) => itemTransformation.to(i)) }, from(input: unknown[], original) { - return input.map((i) => itemTransformation.from(i, original)) + return input.map((i, idx) => { + const originalElement = Array.isArray(original) + ? original.find((o) => resolveEqualsFn(parameter)(o, i)) ?? original[idx] + : original; + + return itemTransformation.from(i, originalElement); + }) } } } From dacf48d9738d8419e0d3b779d29998a68c23a5d4 Mon Sep 17 00:00:00 2001 From: kevinwang Date: Sun, 23 Feb 2025 00:09:13 -0500 Subject: [PATCH 23/25] fix: Bug fixes for directory transformations --- src/resource/resource-settings.test.ts | 26 ++++++++++++++++++++++++++ src/resource/resource-settings.ts | 17 +++++++++++++++-- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/resource/resource-settings.test.ts b/src/resource/resource-settings.test.ts index 59f8b2e..c53ca7d 100644 --- a/src/resource/resource-settings.test.ts +++ b/src/resource/resource-settings.test.ts @@ -1119,4 +1119,30 @@ describe('Resource parameter tests', () => { propB: 'random2', })).to.be.false; }) + + it('Can match directories 1', async () => { + const resource = new class extends TestResource { + getSettings(): ResourceSettings { + return { + id: 'resourceType', + parameterSettings: { + propA: { type: 'directory' } + }, + } + } + }; + + const controller = new ResourceController(resource); + const transformations = controller.parsedSettings.inputTransformations.propA; + + const to = transformations!.to('$HOME/abc/def') + expect(to).to.eq(os.homedir() + '/abc/def') + + const from = transformations!.from(os.homedir() + '/abc/def') + expect(from).to.eq('~/abc/def') + + const from2 = transformations!.from(os.homedir() + '/abc/def', '$HOME/abc/def') + expect(from2).to.eq('$HOME/abc/def') + + }) }) diff --git a/src/resource/resource-settings.ts b/src/resource/resource-settings.ts index 0a10e31..68f6a3b 100644 --- a/src/resource/resource-settings.ts +++ b/src/resource/resource-settings.ts @@ -313,9 +313,22 @@ export interface StatefulParameterSetting extends DefaultParameterSetting { const ParameterEqualsDefaults: Partial boolean>> = { 'boolean': (a: unknown, b: unknown) => Boolean(a) === Boolean(b), 'directory': (a: unknown, b: unknown) => { + let transformedA = resolvePathWithVariables(untildify(String(a))) + let transformedB = resolvePathWithVariables(untildify(String(b))) + + if (transformedA.startsWith('.')) { // Only relative paths start with '.' + transformedA = path.resolve(transformedA) + } + + if (transformedB.startsWith('.')) { // Only relative paths start with '.' + transformedB = path.resolve(transformedB) + } + const notCaseSensitive = process.platform === 'darwin'; - const transformedA = path.resolve(resolvePathWithVariables(untildify(notCaseSensitive ? String(a).toLowerCase() : String(a)))) - const transformedB = path.resolve(resolvePathWithVariables(untildify(notCaseSensitive ? String(b).toLowerCase() : String(b)))) + if (notCaseSensitive) { + transformedA = transformedA.toLowerCase(); + transformedB = transformedB.toLowerCase(); + } return transformedA === transformedB; }, From 03b3fa438e5848239e359dffb789802d764912ec Mon Sep 17 00:00:00 2001 From: kevinwang Date: Sun, 23 Feb 2025 09:09:54 -0500 Subject: [PATCH 24/25] fix: Fix array transformation using resolve equals fn instead it should be using elementEqualsFn --- package.json | 2 +- src/resource/resource-controller.test.ts | 104 ++++++++++++++++++++++- src/resource/resource-settings.ts | 2 +- src/utils/utils.test.ts | 4 +- 4 files changed, 107 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index bca75a9..f0cb4db 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codify-plugin-lib", - "version": "1.0.163", + "version": "1.0.165", "description": "Library plugin library", "main": "dist/index.js", "typings": "dist/index.d.ts", diff --git a/src/resource/resource-controller.test.ts b/src/resource/resource-controller.test.ts index 4f620f8..b5c0eb7 100644 --- a/src/resource/resource-controller.test.ts +++ b/src/resource/resource-controller.test.ts @@ -10,6 +10,7 @@ import { TestConfig, testPlan, TestResource, TestStatefulParameter } from '../ut import { tildify, untildify } from '../utils/utils.js'; import { ArrayStatefulParameter, StatefulParameter } from '../stateful-parameter/stateful-parameter.js'; import { Plan } from '../plan/plan.js'; +import os from 'node:os'; describe('Resource tests', () => { @@ -828,6 +829,107 @@ describe('Resource tests', () => { } const controller = new ResourceController(resource); - const plan = await controller.import({ type: 'path' }, {}); + await controller.import({ type: 'path' }, {}); + ; + }) + + it('Can import and return all of the imported parameters', async () => { + const resource = new class extends TestResource { + getSettings(): ResourceSettings { + return { + id: 'path', + parameterSettings: { + path: { type: 'directory' }, + paths: { canModify: true, type: 'array', itemType: 'directory' }, + prepend: { default: false, setting: true }, + declarationsOnly: { default: false, setting: true }, + }, + importAndDestroy: { + refreshMapper: (input, context) => { + if (Object.keys(input).length === 0) { + return { paths: [], declarationsOnly: true }; + } + + return input; + } + }, + allowMultiple: { + matcher: (desired, current) => { + if (desired.path) { + return desired.path === current.path; + } + + const currentPaths = new Set(current.paths) + return desired.paths?.some((p) => currentPaths.has(p)); + } + } + } + } + + async refresh(parameters: Partial): Promise | null> { + return { + paths: [ + `${os.homedir()}/.pyenv/bin`, + `${os.homedir()}/.bun/bin`, + `${os.homedir()}/.deno/bin`, + `${os.homedir()}/.jenv/bin`, + `${os.homedir()}/a/random/path`, + `${os.homedir()}/.nvm/.bin/2`, + `${os.homedir()}/.nvm/.bin/3` + ] + } + } + } + + const oldProcessEnv = structuredClone(process.env); + + process.env['PYENV_ROOT'] = `${os.homedir()}/.pyenv` + process.env['BUN_INSTALL'] = `${os.homedir()}/.bun` + process.env['DENO_INSTALL'] = `${os.homedir()}/.deno` + process.env['JENV'] = `${os.homedir()}/.jenv` + process.env['NVM_DIR'] = `${os.homedir()}/.nvm` + + const controller = new ResourceController(resource); + const importResult1 = await controller.import({ type: 'path' }, {}); + expect(importResult1).toMatchObject([ + { + 'core': { + 'type': 'path' + }, + 'parameters': { + 'paths': [ + '$PYENV_ROOT/bin', + '$BUN_INSTALL/bin', + '$DENO_INSTALL/bin', + '$JENV/bin', + '~/a/random/path', + '$NVM_DIR/.bin/2', + '$NVM_DIR/.bin/3' + ] + } + } + ]) + + const importResult2 = await controller.import({ type: 'path' }, { paths: ['$PYENV_ROOT/bin', '$BUN_INSTALL/bin'] }); + expect(importResult2).toMatchObject([ + { + 'core': { + 'type': 'path' + }, + 'parameters': { + 'paths': [ + '$PYENV_ROOT/bin', + '$BUN_INSTALL/bin', + '$DENO_INSTALL/bin', + '$JENV/bin', + '~/a/random/path', + '$NVM_DIR/.bin/2', + '$NVM_DIR/.bin/3' + ] + } + } + ]) + + process.env = oldProcessEnv; }) }); diff --git a/src/resource/resource-settings.ts b/src/resource/resource-settings.ts index 68f6a3b..26dc568 100644 --- a/src/resource/resource-settings.ts +++ b/src/resource/resource-settings.ts @@ -438,7 +438,7 @@ export function resolveParameterTransformFn( from(input: unknown[], original) { return input.map((i, idx) => { const originalElement = Array.isArray(original) - ? original.find((o) => resolveEqualsFn(parameter)(o, i)) ?? original[idx] + ? original.find((o) => resolveElementEqualsFn(parameter as ArrayParameterSetting)(o, i)) ?? original[idx] : original; return itemTransformation.from(i, originalElement); diff --git a/src/utils/utils.test.ts b/src/utils/utils.test.ts index 8b6fcbc..9d209f5 100644 --- a/src/utils/utils.test.ts +++ b/src/utils/utils.test.ts @@ -41,11 +41,11 @@ describe('Utils tests', () => { expect(result2).to.eq('/var' + home + '/my/path'); }) - it('Can add variables to a path', () => { + it('Can add variables to a path (ignores home)', () => { const testPath1 = os.homedir() + '/my/path'; const result1 = addVariablesToPath(testPath1); const home = os.homedir(); - expect(result1).to.eq('$HOME/my/path'); + expect(result1).to.eq(home + '/my/path'); }) }) From 56d63427f3a81490eeb0af08c284a27c2eb87176 Mon Sep 17 00:00:00 2001 From: kevinwang Date: Tue, 4 Mar 2025 18:21:26 -0500 Subject: [PATCH 25/25] fix: Fix bash history being written --- package.json | 2 +- src/pty/background-pty.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index f0cb4db..d99d7cb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codify-plugin-lib", - "version": "1.0.165", + "version": "1.0.166", "description": "Library plugin library", "main": "dist/index.js", "typings": "dist/index.d.ts", diff --git a/src/pty/background-pty.ts b/src/pty/background-pty.ts index faa1f3d..380b4a0 100644 --- a/src/pty/background-pty.ts +++ b/src/pty/background-pty.ts @@ -123,6 +123,7 @@ export class BackgroundPty implements IPty { let outputBuffer = ''; return new Promise(resolve => { + this.basePty.write('set +o history;\n'); this.basePty.write('unset PS1;\n'); this.basePty.write('unset PS0;\n') this.basePty.write('echo setup complete\\"\n')