diff --git a/.rat-excludes b/.rat-excludes index 097ccbe..1e4f92e 100644 --- a/.rat-excludes +++ b/.rat-excludes @@ -6,4 +6,5 @@ pnpm-lock.yaml pnpm-workspace.yaml repo/graph.dot repo/repo.iml +**/__snapshots__/** diff --git a/packages/serverless-workflow-diagram-editor/package.json b/packages/serverless-workflow-diagram-editor/package.json index 20484ec..7d99d90 100644 --- a/packages/serverless-workflow-diagram-editor/package.json +++ b/packages/serverless-workflow-diagram-editor/package.json @@ -38,7 +38,9 @@ "build:storybook": "pnpm clean:storybook && storybook build --output-dir ./dist-storybook" }, "dependencies": { - "@xyflow/react": "catalog:" + "@serverlessworkflow/sdk": "catalog:", + "@xyflow/react": "catalog:", + "js-yaml": "catalog:" }, "devDependencies": { "@chromatic-com/storybook": "catalog:", @@ -50,6 +52,7 @@ "@testing-library/jest-dom": "catalog:", "@testing-library/react": "catalog:", "@testing-library/user-event": "catalog:", + "@types/js-yaml": "catalog:", "@types/node": "catalog:", "@types/react": "catalog:", "@types/react-dom": "catalog:", diff --git a/packages/serverless-workflow-diagram-editor/src/core/README.md b/packages/serverless-workflow-diagram-editor/src/core/README.md index 4d30595..3194a6a 100644 --- a/packages/serverless-workflow-diagram-editor/src/core/README.md +++ b/packages/serverless-workflow-diagram-editor/src/core/README.md @@ -1,12 +1,12 @@ # core -Core package agnostic from the rendering library and its types. \ No newline at end of file + +Core package agnostic from the rendering library and its types. + +## Modules + +### workflowSdk.ts + +Abstraction layer over the `@serverlessworkflow/sdk`. This is the only place in the diagram editor that imports from the SDK directly keeping the rest of the editor decoupled from SDK implementation details. diff --git a/packages/serverless-workflow-diagram-editor/tests/core/sample.test.ts b/packages/serverless-workflow-diagram-editor/src/core/index.ts similarity index 81% rename from packages/serverless-workflow-diagram-editor/tests/core/sample.test.ts rename to packages/serverless-workflow-diagram-editor/src/core/index.ts index c56b6d3..16c12a3 100644 --- a/packages/serverless-workflow-diagram-editor/tests/core/sample.test.ts +++ b/packages/serverless-workflow-diagram-editor/src/core/index.ts @@ -14,10 +14,4 @@ * limitations under the License. */ -import { expect, describe, it } from "vitest"; - -describe("sampleTest", () => { - it("Testing...", () => { - expect(true).toBeTruthy(); - }); -}); +export * from "./workflowSdk"; diff --git a/packages/serverless-workflow-diagram-editor/src/core/workflowSdk.ts b/packages/serverless-workflow-diagram-editor/src/core/workflowSdk.ts new file mode 100644 index 0000000..3f0e25d --- /dev/null +++ b/packages/serverless-workflow-diagram-editor/src/core/workflowSdk.ts @@ -0,0 +1,55 @@ +/* + * Copyright 2021-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import yaml from "js-yaml"; +import { Classes, Specification, validate } from "@serverlessworkflow/sdk"; + +export type WorkflowParseResult = { + model: Specification.Workflow | null; + errors: Error[]; +}; + +export function validateWorkflow(model: Specification.Workflow): Error[] { + try { + validate("Workflow", model); + return []; + } catch (err) { + // TODO: Parse individual validation errors from the SDK into separate Error objects when we are ready to render them. + return [err instanceof Error ? err : new Error(String(err))]; + } +} + +export function parseWorkflow(text: string): WorkflowParseResult { + let raw: Partial; + + try { + raw = yaml.load(text, { schema: yaml.DEFAULT_SCHEMA }) as Partial; + } catch (err) { + return { + model: null, + errors: [err instanceof Error ? err : new Error(String(err))], + }; + } + + if (raw == null || typeof raw !== "object") { + return { model: null, errors: [new Error("Not a valid workflow object")] }; + } + + const model = new Classes.Workflow(raw) as Specification.Workflow; + const errors = validateWorkflow(model); + + return { model, errors }; +} diff --git a/packages/serverless-workflow-diagram-editor/tests/core/__snapshots__/workflowSdk.integration.test.ts.snap b/packages/serverless-workflow-diagram-editor/tests/core/__snapshots__/workflowSdk.integration.test.ts.snap new file mode 100644 index 0000000..6ce73a6 --- /dev/null +++ b/packages/serverless-workflow-diagram-editor/tests/core/__snapshots__/workflowSdk.integration.test.ts.snap @@ -0,0 +1,91 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`parseWorkflow > parses valid 'JSON' and returns model with no errors 1`] = ` +{ + "errors": [], + "model": t { + "do": t [ + t { + "step1": t { + "set": t { + "variable": "my first workflow", + }, + }, + }, + ], + "document": t { + "dsl": "1.0.0", + "name": "valid-workflow-json", + "namespace": "default", + "version": "1.0.0", + }, + }, +} +`; + +exports[`parseWorkflow > parses valid 'YAML' and returns model with no errors 1`] = ` +{ + "errors": [], + "model": t { + "do": t [ + t { + "step1": t { + "set": t { + "variable": "my first workflow", + }, + }, + }, + ], + "document": t { + "dsl": "1.0.0", + "name": "valid-workflow-yaml", + "namespace": "default", + "version": "1.0.0", + }, + }, +} +`; + +exports[`parseWorkflow > returns model and errors for invalid but parseable 'JSON' 1`] = ` +{ + "errors": [ + [Error: 'Workflow' is invalid - The DSL version of the workflow 'undefined' doesn't match the supported version of the SDK '1.0.0'.], + ], + "model": t { + "do": t [ + t { + "step1": t { + "set": t { + "variable": "my first invalid json workflow", + }, + }, + }, + ], + }, +} +`; + +exports[`parseWorkflow > returns model and errors for invalid but parseable 'YAML' 1`] = ` +{ + "errors": [ + [Error: 'Workflow' is invalid - The DSL version of the workflow 'undefined' doesn't match the supported version of the SDK '1.0.0'.], + ], + "model": t { + "do": t [ + t { + "step1": t { + "set": t { + "variable": "my first invalid yaml workflow", + }, + }, + }, + ], + }, +} +`; + +exports[`validateWorkflow > returns errors for an invalid workflow 1`] = ` +[ + [Error: 'Workflow' is invalid - The DSL version of the workflow 'undefined' doesn't match the supported version of the SDK '1.0.0'.], +] +`; diff --git a/packages/serverless-workflow-diagram-editor/tests/core/workflowSdk.integration.test.ts b/packages/serverless-workflow-diagram-editor/tests/core/workflowSdk.integration.test.ts new file mode 100644 index 0000000..24fe2ad --- /dev/null +++ b/packages/serverless-workflow-diagram-editor/tests/core/workflowSdk.integration.test.ts @@ -0,0 +1,83 @@ +/* + * Copyright 2021-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { describe, it, expect } from "vitest"; +import { parseWorkflow, validateWorkflow } from "../../src/core"; +import { + BASIC_VALID_WORKFLOW_YAML, + BASIC_VALID_WORKFLOW_JSON, + BASIC_INVALID_WORKFLOW_YAML, + BASIC_INVALID_WORKFLOW_JSON, +} from "../fixtures/workflows"; +import { Classes, Specification } from "@serverlessworkflow/sdk"; + +describe("parseWorkflow", () => { + it.each([ + { format: "YAML", input: BASIC_VALID_WORKFLOW_YAML, expectedName: "valid-workflow-yaml" }, + { format: "JSON", input: BASIC_VALID_WORKFLOW_JSON, expectedName: "valid-workflow-json" }, + ])("parses valid $format and returns model with no errors", ({ input, expectedName }) => { + const result = parseWorkflow(input); + expect(result.errors).toHaveLength(0); + expect(result.model?.document?.name).toBe(expectedName); + expect(result).toMatchSnapshot(); + }); + + it.each([ + { format: "YAML", input: BASIC_INVALID_WORKFLOW_YAML }, + { format: "JSON", input: BASIC_INVALID_WORKFLOW_JSON }, + ])("returns model and errors for invalid but parseable $format", ({ input }) => { + const result = parseWorkflow(input); + expect(result.model).not.toBeNull(); + expect(result.errors).toHaveLength(1); + expect(result).toMatchSnapshot(); + }); + + it.each([ + { description: "empty string", input: "" }, + { description: "non-object YAML", input: "just a string" }, + { description: "numeric YAML", input: "42" }, + ])("returns null model with error for $description", ({ input }) => { + const result = parseWorkflow(input); + expect(result.model).toBeNull(); + expect(result.errors[0].message).toBe("Not a valid workflow object"); + }); + + it("returns null model with errors for unparseable text", () => { + const result = parseWorkflow("}{not yaml or json}{"); + expect(result.model).toBeNull(); + expect(result.errors).toHaveLength(1); + }); +}); + +describe("validateWorkflow", () => { + it("returns empty array for a valid workflow", () => { + const valid = new Classes.Workflow({ + document: { dsl: "1.0.0", name: "valid-workflow", version: "1.0.0", namespace: "default" }, + do: [{ step1: { set: { variable: "value" } } }], + }) as Specification.Workflow; + const errors = validateWorkflow(valid); + expect(errors).toHaveLength(0); + }); + + it("returns errors for an invalid workflow", () => { + const invalid = new Classes.Workflow({ + do: [{ step1: { set: { variable: "value" } } }], + }) as Specification.Workflow; + const errors = validateWorkflow(invalid); + expect(errors).toHaveLength(1); + expect(errors).toMatchSnapshot(); + }); +}); diff --git a/packages/serverless-workflow-diagram-editor/tests/fixtures/workflows.ts b/packages/serverless-workflow-diagram-editor/tests/fixtures/workflows.ts new file mode 100644 index 0000000..c478625 --- /dev/null +++ b/packages/serverless-workflow-diagram-editor/tests/fixtures/workflows.ts @@ -0,0 +1,71 @@ +/* + * Copyright 2021-Present The Serverless Workflow Specification Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * Workflow test fixtures for parsing, validation, and error handling tests. + * Includes valid and invalid workflow definitions in YAML and JSON formats. + */ + +export const BASIC_VALID_WORKFLOW_YAML = ` + document: + dsl: 1.0.0 + name: valid-workflow-yaml + version: 1.0.0 + namespace: default + do: + - step1: + set: + variable: 'my first workflow' + `; + +export const BASIC_VALID_WORKFLOW_JSON = JSON.stringify({ + document: { + dsl: "1.0.0", + name: "valid-workflow-json", + version: "1.0.0", + namespace: "default", + }, + do: [ + { + step1: { + set: { + variable: "my first workflow", + }, + }, + }, + ], +}); + +// Missing required 'document' field +export const BASIC_INVALID_WORKFLOW_YAML = ` + do: + - step1: + set: + variable: 'my first invalid yaml workflow' + `; + +// Missing required 'document' field +export const BASIC_INVALID_WORKFLOW_JSON = JSON.stringify({ + do: [ + { + step1: { + set: { + variable: "my first invalid json workflow", + }, + }, + }, + ], +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7e49358..2d77801 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,6 +9,9 @@ catalogs: '@chromatic-com/storybook': specifier: ^5.0.1 version: 5.1.1 + '@serverlessworkflow/sdk': + specifier: ^1.0.1 + version: 1.0.1 '@storybook/addon-a11y': specifier: ^10.2.19 version: 10.3.3 @@ -33,6 +36,9 @@ catalogs: '@testing-library/user-event': specifier: ^14.6.1 version: 14.6.1 + '@types/js-yaml': + specifier: ^4.0.9 + version: 4.0.9 '@types/node': specifier: ^25.3.3 version: 25.5.0 @@ -54,6 +60,9 @@ catalogs: husky: specifier: ^9.1.7 version: 9.1.7 + js-yaml: + specifier: ^4.1.1 + version: 4.1.1 jsdom: specifier: ^25.0.0 version: 25.0.1 @@ -116,9 +125,15 @@ importers: packages/serverless-workflow-diagram-editor: dependencies: + '@serverlessworkflow/sdk': + specifier: 'catalog:' + version: 1.0.1 '@xyflow/react': specifier: 'catalog:' version: 12.10.1(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + js-yaml: + specifier: 'catalog:' + version: 4.1.1 devDependencies: '@chromatic-com/storybook': specifier: 'catalog:' @@ -147,6 +162,9 @@ importers: '@testing-library/user-event': specifier: 'catalog:' version: 14.6.1(@testing-library/dom@10.4.1) + '@types/js-yaml': + specifier: 'catalog:' + version: 4.0.9 '@types/node': specifier: 'catalog:' version: 25.5.0 @@ -1050,6 +1068,10 @@ packages: cpu: [x64] os: [win32] + '@serverlessworkflow/sdk@1.0.1': + resolution: {integrity: sha512-ds/FsRbFI/l1W89wWOZxzuiIAeuZLm3U5wtrDrpMrfHJBFex8hiYDDg0Db03V+CGEQZR6eki1KdnmvSX9JeBRg==} + engines: {node: '>=20.0', npm: '>=10.0.0'} + '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -1214,6 +1236,9 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/js-yaml@4.0.9': + resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} + '@types/mdx@2.0.13': resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} @@ -1304,6 +1329,17 @@ packages: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@8.18.0: + resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + ansi-escapes@7.3.0: resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==} engines: {node: '>=18'} @@ -1324,6 +1360,9 @@ packages: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + aria-query@5.3.0: resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} @@ -1602,6 +1641,12 @@ packages: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -1750,6 +1795,10 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + jsdom@25.0.1: resolution: {integrity: sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==} engines: {node: '>=18'} @@ -1764,6 +1813,9 @@ packages: engines: {node: '>=6'} hasBin: true + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} @@ -1955,6 +2007,10 @@ packages: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} engines: {node: '>=8'} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + resolve@1.22.11: resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} engines: {node: '>= 0.4'} @@ -2926,6 +2982,12 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.60.0': optional: true + '@serverlessworkflow/sdk@1.0.1': + dependencies: + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) + js-yaml: 4.1.1 + '@standard-schema/spec@1.1.0': {} '@storybook/addon-a11y@10.3.3(storybook@10.3.3(@testing-library/dom@10.4.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))': @@ -3121,6 +3183,8 @@ snapshots: '@types/estree@1.0.8': {} + '@types/js-yaml@4.0.9': {} + '@types/mdx@2.0.13': {} '@types/node@25.5.0': @@ -3252,6 +3316,17 @@ snapshots: agent-base@7.1.4: {} + ajv-formats@3.0.1(ajv@8.18.0): + optionalDependencies: + ajv: 8.18.0 + + ajv@8.18.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + ansi-escapes@7.3.0: dependencies: environment: 1.1.0 @@ -3264,6 +3339,8 @@ snapshots: ansi-styles@6.2.3: {} + argparse@2.0.1: {} + aria-query@5.3.0: dependencies: dequal: 2.0.3 @@ -3534,6 +3611,10 @@ snapshots: expect-type@1.3.0: {} + fast-deep-equal@3.1.3: {} + + fast-uri@3.1.0: {} + fdir@6.5.0(picomatch@4.0.4): optionalDependencies: picomatch: 4.0.4 @@ -3669,6 +3750,10 @@ snapshots: js-tokens@4.0.0: {} + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + jsdom@25.0.1: dependencies: cssstyle: 4.6.0 @@ -3699,6 +3784,8 @@ snapshots: jsesc@3.1.0: {} + json-schema-traverse@1.0.0: {} + json5@2.2.3: {} jsonfile@6.2.0: @@ -3924,6 +4011,8 @@ snapshots: indent-string: 4.0.0 strip-indent: 3.0.0 + require-from-string@2.0.2: {} + resolve@1.22.11: dependencies: is-core-module: 2.16.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index d2b98e9..0427005 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,6 +2,8 @@ packages: - packages/* catalog: "@chromatic-com/storybook": ^5.0.1 + "js-yaml": ^4.1.1 + "@serverlessworkflow/sdk": ^1.0.1 "@storybook/addon-a11y": ^10.2.19 "@storybook/addon-docs": ^10.2.19 "@storybook/addon-vitest": ^10.2.19 @@ -10,6 +12,7 @@ catalog: "@testing-library/jest-dom": ^6.9.1 "@testing-library/react": ^16.3.2 "@testing-library/user-event": ^14.6.1 + "@types/js-yaml": ^4.0.9 "@types/node": ^25.3.3 "@types/react": ^19.2.14 "@types/react-dom": ^19.2.3