diff --git a/.changeset/silver-pears-wait.md b/.changeset/silver-pears-wait.md new file mode 100644 index 000000000000..53dc7afd663c --- /dev/null +++ b/.changeset/silver-pears-wait.md @@ -0,0 +1,5 @@ +--- +"wrangler": patch +--- + +improve the diffing representation for `wrangler deploy` (run under `--x-remote-diff-check`) diff --git a/packages/wrangler/package.json b/packages/wrangler/package.json index 4493138f6d9e..bc9b11589524 100644 --- a/packages/wrangler/package.json +++ b/packages/wrangler/package.json @@ -95,6 +95,7 @@ "@types/glob-to-regexp": "^0.4.1", "@types/is-ci": "^3.0.0", "@types/javascript-time-ago": "^2.0.3", + "@types/json-diff": "^1.0.3", "@types/mime": "^3.0.4", "@types/minimatch": "^5.1.2", "@types/node": "catalog:default", @@ -128,6 +129,7 @@ "is-ci": "^3.0.1", "itty-time": "^1.0.6", "javascript-time-ago": "^2.5.4", + "json-diff": "^1.0.6", "md5-file": "5.0.0", "mime": "^3.0.0", "minimatch": "^5.1.0", diff --git a/packages/wrangler/src/__tests__/deploy.test.ts b/packages/wrangler/src/__tests__/deploy.test.ts index b6acce165e2e..7374b17cf72d 100644 --- a/packages/wrangler/src/__tests__/deploy.test.ts +++ b/packages/wrangler/src/__tests__/deploy.test.ts @@ -14711,6 +14711,34 @@ export default{ }); }); + function normalizeLogWithConfigDiff(log: string): string { + // If the path is long the log could be wrapped so we need to remove the potential wrapping + let normalizedLog = log.replace(/"main":\s*"/, '"main": "'); + + if (process.platform === "win32") { + // On windows the snapshot paths incorrectly use double slashes, such as: + // `\"main\": \"C://Users//RUNNER~1//AppData//Local//Temp//wrangler-testse63LuJ//index.js\", + // so in the `main` field we replace all possible occurrences of `//` with just `\\` + // (so that the path normalization of `normalizeString` can appropriately work) + normalizedLog = normalizedLog.replace( + /"main": "(.*?)"/, + (_, mainPath: string) => `"main": "${mainPath.replaceAll("//", "\\")}"` + ); + } + + normalizedLog = normalizeString(normalizedLog); + + // Let's remove the various extra characters for colors to get a more clear output + normalizedLog = normalizedLog + .replaceAll("", "X") + .replaceAll(/X\[\d+(?:;\d+)?m/g, ""); + + // Let's also normalize Windows newlines + normalizedLog = normalizedLog.replaceAll("\r\n", "\n"); + + return normalizedLog; + } + describe("config remote differences", () => { it("should present a diff warning to the user when there are differences between the local config (json/jsonc) and the dash config", async () => { writeWorkerSource(); @@ -14760,16 +14788,15 @@ export default{ await runWrangler("deploy --x-remote-diff-check"); expect(normalizeLogWithConfigDiff(std.warn)).toMatchInlineSnapshot(` - "▲ [WARNING] The local configuration being used (generated from your local configuration file) differs from the remote configuration of your Worker set via the Cloudflare Dashboard: + "▲ [WARNING] The local configuration being used (generated from your local configuration file) differs from the remote configuration of your Worker set via the Cloudflare Dashboard: + + { + vars: { + - MY_VAR: \\"abc\\" + + MY_VAR: 123 + } + } - \\"workers_dev\\": true, - \\"preview_urls\\": false, - \\"vars\\": { - - \\"MY_VAR\\": \\"abc\\" - + \\"MY_VAR\\": 123 - }, - \\"define\\": {}, - \\"durable_objects\\": { Deploying the Worker will override the remote configuration with your local one. @@ -14828,43 +14855,21 @@ export default{ // to be able to show toml content/diffs, that combined with the fact that json(c) config files are the // recommended ones moving forward makes this small shortcoming of the config diffing acceptable expect(normalizeLogWithConfigDiff(std.warn)).toMatchInlineSnapshot(` - "▲ [WARNING] The local configuration being used (generated from your local configuration file) differs from the remote configuration of your Worker set via the Cloudflare Dashboard: + "▲ [WARNING] The local configuration being used (generated from your local configuration file) differs from the remote configuration of your Worker set via the Cloudflare Dashboard: + + { + vars: { + - MY_VAR: \\"abc\\" + + MY_VAR: \\"this is a toml file\\" + } + } - \\"workers_dev\\": true, - \\"preview_urls\\": false, - \\"vars\\": { - - \\"MY_VAR\\": \\"abc\\" - + \\"MY_VAR\\": \\"this is a toml file\\" - }, - \\"define\\": {}, - \\"durable_objects\\": { Deploying the Worker will override the remote configuration with your local one. " `); }); - - function normalizeLogWithConfigDiff(log: string): string { - // If the path is long the log could be wrapped so we need to remove the potential wrapping - let normalizedLog = log.replace(/"main":\s*"/, '"main": "'); - - if (process.platform === "win32") { - // On windows the snapshot paths incorrectly use double slashes, such as: - // `\"main\": \"C://Users//RUNNER~1//AppData//Local//Temp//wrangler-testse63LuJ//index.js\", - // so in the `main` field we replace all possible occurrences of `//` with just `\\` - // (so that the path normalization of `normalizeString` can appropriately work) - normalizedLog = normalizedLog.replace( - /"main": "(.*?)"/, - (_, mainPath: string) => - `"main": "${mainPath.replaceAll("//", "\\")}"` - ); - } - - normalizedLog = normalizeString(normalizedLog); - - return normalizedLog; - } }); describe("with strict mode enabled", () => { @@ -14904,17 +14909,16 @@ export default{ await runWrangler("deploy --x-remote-diff-check --strict"); - expect(std.warn).toMatchInlineSnapshot(` - "▲ [WARNING] The local configuration being used (generated from your local configuration file) differs from the remote configuration of your Worker set via the Cloudflare Dashboard: - - \\"bindings\\": [] - }, - \\"observability\\": { - - \\"enabled\\": true, - + \\"enabled\\": false, - \\"head_sampling_rate\\": 1, - \\"logs\\": { - \\"enabled\\": false, + expect(normalizeLogWithConfigDiff(std.warn)).toMatchInlineSnapshot(` + "▲ [WARNING] The local configuration being used (generated from your local configuration file) differs from the remote configuration of your Worker set via the Cloudflare Dashboard: + + { + observability: { + - enabled: true + + enabled: false + } + } + Deploying the Worker will override the remote configuration with your local one. diff --git a/packages/wrangler/src/__tests__/deploy/config-diffs.test.ts b/packages/wrangler/src/__tests__/deploy/config-diffs.test.ts index 9a97a009ddc1..d5760ece250e 100644 --- a/packages/wrangler/src/__tests__/deploy/config-diffs.test.ts +++ b/packages/wrangler/src/__tests__/deploy/config-diffs.test.ts @@ -1,6 +1,20 @@ import { getRemoteConfigDiff } from "../../deploy/config-diffs"; import type { Config, RawConfig } from "../../config"; +function normalizeDiff(log: string): string { + let normalizedLog = log; + + // Let's remove the various extra characters for colors to get a more clear + normalizedLog = normalizedLog + .replaceAll("", "X") + .replaceAll(/X\[\d+(?:;\d+)?m/g, ""); + + // Let's also normalize Windows newlines + normalizedLog = normalizedLog.replaceAll("\r\n", "\n"); + + return normalizedLog; +} + describe("getRemoteConfigsDiff", () => { it("should handle a very simple diffing scenario (no diffs, random order)", () => { const { diff, nonDestructive } = getRemoteConfigDiff( @@ -29,7 +43,7 @@ describe("getRemoteConfigsDiff", () => { } as unknown as Config ); - expect(diff.toString()).toEqual(""); + expect(diff).toBe(null); expect(nonDestructive).toBe(true); }); @@ -60,13 +74,13 @@ describe("getRemoteConfigsDiff", () => { } as unknown as Config ); - expect(diff.toString()).toMatchInlineSnapshot(` - " { - - \\"compatibility_date\\": \\"2025-07-08\\", - + \\"compatibility_date\\": \\"2025-07-09\\", - \\"main\\": \\"/tmp/src/index.js\\", - \\"compatibility_flags\\": [], - \\"name\\": \\"silent-firefly-dbe3\\"," + assert(diff); + expect(normalizeDiff(diff.toString())).toMatchInlineSnapshot(` + " { + - compatibility_date: \\"2025-07-08\\" + + compatibility_date: \\"2025-07-09\\" + } + " `); expect(nonDestructive).toBe(false); }); @@ -97,21 +111,65 @@ describe("getRemoteConfigsDiff", () => { ], } as unknown as Config ); - expect(diff.toString()).toMatchInlineSnapshot(` - " { - \\"binding\\": \\"MY_KV\\", - \\"id\\": \\"my-kv-123\\" - + }, - + { - + \\"binding\\": \\"MY_KV_2\\", - + \\"id\\": \\"my-kv-456\\" - } - ], - \\"workers_dev\\": true," + assert(diff); + expect(normalizeDiff(diff.toString())).toMatchInlineSnapshot(` + " { + kv_namespaces: [ + ... + + { + + binding: \\"MY_KV_2\\" + + id: \\"my-kv-456\\" + + } + ] + } + " `); expect(nonDestructive).toBe(true); }); + it("should handle a diffing scenario with only deletions", () => { + const { diff, nonDestructive } = getRemoteConfigDiff( + { + name: "silent-firefly-dbe3", + main: "/tmp/src/index.js", + workers_dev: true, + preview_urls: false, + compatibility_date: "2025-07-08", + compatibility_flags: undefined, + placement: undefined, + limits: undefined, + tail_consumers: undefined, + account_id: "account-id-123", + kv_namespaces: [{ binding: "MY_KV", id: "my-kv-123" }], + }, + { + name: "silent-firefly-dbe3", + main: "/tmp/src/index.js", + workers_dev: true, + preview_urls: false, + compatibility_date: "2025-07-08", + compatibility_flags: undefined, + placement: undefined, + limits: undefined, + tail_consumers: undefined, + account_id: "account-id-123", + } as unknown as Config + ); + assert(diff); + expect(normalizeDiff(diff.toString())).toMatchInlineSnapshot(` + " { + - kv_namespaces: [ + - { + - binding: \\"MY_KV\\" + - id: \\"my-kv-123\\" + - } + - ] + } + " + `); + expect(nonDestructive).toBe(false); + }); + it("should handle a diffing scenario with modifications and removals", () => { const { diff, nonDestructive } = getRemoteConfigDiff( { @@ -137,33 +195,23 @@ describe("getRemoteConfigsDiff", () => { account_id: "account-id-123", } as unknown as Config ); - expect(diff.toString()).toMatchInlineSnapshot(` - " { - \\"name\\": \\"silent-firefly-dbe3\\", - \\"main\\": \\"src/index.js\\", - - \\"compatibility_date\\": \\"2025-07-08\\", - + \\"compatibility_date\\": \\"2025-07-09\\", - \\"observability\\": { - - \\"enabled\\": true, - + \\"enabled\\": false, - \\"head_sampling_rate\\": 1, - \\"logs\\": { - \\"enabled\\": false, - - ... - - }, - \\"account_id\\": \\"account-id-123\\", - \\"workers_dev\\": true, - + \\"preview_urls\\": false - - \\"preview_urls\\": false, - - \\"kv_namespaces\\": [ - - { - - \\"binding\\": \\"MY_KV\\", - - \\"id\\": \\"my-kv-123\\" - - } - - ] - }" + assert(diff); + expect(normalizeDiff(diff.toString())).toMatchInlineSnapshot(` + " { + - kv_namespaces: [ + - { + - binding: \\"MY_KV\\" + - id: \\"my-kv-123\\" + - } + - ] + - compatibility_date: \\"2025-07-08\\" + + compatibility_date: \\"2025-07-09\\" + observability: { + - enabled: true + + enabled: false + } + } + " `); expect(nonDestructive).toBe(false); }); @@ -196,7 +244,7 @@ describe("getRemoteConfigsDiff", () => { undefined, { enabled: false } ); - expect(diff.toString()).toMatchInlineSnapshot(`""`); + expect(diff).toBe(null); }); it("should treat a remote undefined equal to a remote { enabled: false, logs: { enabled: false } }", () => { @@ -205,7 +253,7 @@ describe("getRemoteConfigsDiff", () => { undefined, { enabled: false, logs: { enabled: false } } ); - expect(diff.toString()).toMatchInlineSnapshot(`""`); + expect(diff).toBe(null); }); it("should correctly show the diff of boolean when the remote is undefined and the local is { enabled: true }", () => { @@ -214,14 +262,15 @@ describe("getRemoteConfigsDiff", () => { undefined, { enabled: true } ); - expect(diff.toString()).toMatchInlineSnapshot(` - " { - \\"observability\\": { - - \\"enabled\\": false, - + \\"enabled\\": true, - \\"head_sampling_rate\\": 1, - \\"logs\\": { - \\"enabled\\": false," + assert(diff); + expect(normalizeDiff(diff.toString())).toMatchInlineSnapshot(` + " { + observability: { + - enabled: false + + enabled: true + } + } + " `); }); @@ -231,15 +280,17 @@ describe("getRemoteConfigsDiff", () => { undefined, { logs: { enabled: true } } ); - expect(diff.toString()).toMatchInlineSnapshot(` - " { - \\"observability\\": { - \\"logs\\": { - - \\"enabled\\": false, - + \\"enabled\\": true, - \\"head_sampling_rate\\": 1, - \\"invocation_logs\\": true - }," + assert(diff); + expect(normalizeDiff(diff.toString())).toMatchInlineSnapshot(` + " { + observability: { + logs: { + - enabled: false + + enabled: true + } + } + } + " `); }); @@ -249,7 +300,7 @@ describe("getRemoteConfigsDiff", () => { { enabled: false, head_sampling_rate: 1, logs: { enabled: true } }, { enabled: false, logs: { enabled: true, invocation_logs: true } } ); - expect(diff.toString()).toMatchInlineSnapshot(`""`); + expect(diff).toBe(null); }); it("should correctly not show logs.invocation_logs being added remotely", () => { @@ -260,15 +311,17 @@ describe("getRemoteConfigsDiff", () => { }, { logs: { enabled: true, head_sampling_rate: 0.9 } } ); - expect(diff.toString()).toMatchInlineSnapshot(` - " \\"observability\\": { - \\"logs\\": { - \\"enabled\\": true, - - \\"head_sampling_rate\\": 1, - + \\"head_sampling_rate\\": 0.9, - \\"invocation_logs\\": true - }, - \\"enabled\\": false," + assert(diff); + expect(normalizeDiff(diff.toString())).toMatchInlineSnapshot(` + " { + observability: { + logs: { + - head_sampling_rate: 1 + + head_sampling_rate: 0.9 + } + } + } + " `); }); }); diff --git a/packages/wrangler/src/deploy/config-diffs.ts b/packages/wrangler/src/deploy/config-diffs.ts index ec33fe0ae4c6..dd45161cebec 100644 --- a/packages/wrangler/src/deploy/config-diffs.ts +++ b/packages/wrangler/src/deploy/config-diffs.ts @@ -1,14 +1,14 @@ -import { green, red } from "@cloudflare/cli/colors"; import { getSubdomainValues } from "../triggers/deploy"; -import { Diff } from "../utils/diff"; +import { diffJsonObjects, isNonDestructive } from "../utils/diff-json"; import type { Config, RawConfig } from "../config"; +import type { DiffJson, Json } from "../utils/diff-json"; /** * Object representing the difference of two configuration objects. */ type ConfigDiff = { /** The actual (raw) computed diff of the two objects */ - diff: Diff; + diff: Record | null; /** * Flag indicating whether the difference includes some destructive changes. * @@ -35,62 +35,17 @@ export function getRemoteConfigDiff( localResolvedConfig ); - const diff = new Diff( - JSON.stringify(normalizedRemoteConfig, null, 2), - JSON.stringify(normalizedLocalConfig, null, 2) + const diff = diffJsonObjects( + normalizedRemoteConfig as unknown as Record, + normalizedLocalConfig as unknown as Record ); return { diff, - nonDestructive: configDiffOnlyHasAdditionsIfAny(diff), + nonDestructive: isNonDestructive(diff), }; } -/** - * Given a diff object, representing the diff of config files, it computes whether such diff - * object only represents at most additions (so no removal nor modifications). - * - * For example the diff: - * ``` - * - "name": "my-worker" - * + "name": "my-worker", - * + "vars": { - * + MY_VAR: "my variable", - * + }, - * ``` - * only contains additions. - * - * While - * ``` - * "name": "my-worker", - * "vars": { - * - MY_VAR: "my variable", - * + MY_VAR: "my modified variable", - * }, - * - compatibility_date: "2025-07-08", - * ``` - * contains also a modification and a removal - * - * @param diff The diff object to analyze - * @returns true if there are only additions or no diffs at all, false otherwise - */ -function configDiffOnlyHasAdditionsIfAny(diff: Diff): boolean { - const diffLines = diff.toString().split("\n"); - const removalLines = diffLines.filter((line) => { - const withoutLeadingSpaces = line.replace(/^\s*/, ""); - return withoutLeadingSpaces.startsWith(red("-")); - }); - const diffLinesSet = new Set(diffLines); - return removalLines.every((line) => { - if (line.endsWith(",")) { - const removalButAsAdditionAndWithCommaRemoved = `${line.slice(0, -1).replace(red("-"), green("+"))}`; - return diffLinesSet.has(removalButAsAdditionAndWithCommaRemoved); - } - const removalButAsAdditionAndWithComma = `${line.replace(red("-"), green("+"))},`; - return diffLinesSet.has(removalButAsAdditionAndWithComma); - }); -} - /** * Normalized a local (resolved) config object so that it can be compared against * the remote config object. This mainly means resolving and setting defaults to diff --git a/packages/wrangler/src/utils/diff-json.ts b/packages/wrangler/src/utils/diff-json.ts new file mode 100644 index 000000000000..83c13149be26 --- /dev/null +++ b/packages/wrangler/src/utils/diff-json.ts @@ -0,0 +1,78 @@ +import jsonDiff from "json-diff"; + +export type Json = + | string + | number + | boolean + | null + | Json[] + | { [id: string]: Json }; + +export type DiffJson = Json | (Json & { __old: DiffJson; __new: DiffJson }); + +/** + * Given two objects A and B that are Json serializable this function computes the difference between them + * + * The difference object includes: + * - fields in object B but not in object A included as `` + * - fields in object A but not in object B included as `` + * - fields present in both objects but modified as `: { __old: , __new: }` + * + * Additionally the difference object contains a `toString` method that can be used to generate a string representation + * of the difference between the two objects (to be presented to users) + * + * @param jsonObjA The first target object + * @param jsonObjB The second target object + * @returns An object representing the diff between the two objects, or null if the objects are equal + */ +export function diffJsonObjects( + jsonObjA: Record, + jsonObjB: Record +): Record | null { + const result = jsonDiff.diff(jsonObjA, jsonObjB); + + if (result) { + result.toString = () => jsonDiff.diffString(jsonObjA, jsonObjB); + return result; + } else { + return null; + } +} + +/** + * Given a diff object (generated by `diffJsonObjects`) this function computes whether the + * difference is non-destructive, i.e. if the second object only contained additions to the + * first one and no removal nor modifications. + * + * @param diff The difference object to use (generated by `diffJsonObjects`) + * @returns `true` if the difference is non-destructive, `false` if it is + */ +export function isNonDestructive(diff: DiffJson): boolean { + if (diff === null || typeof diff !== "object") { + return true; + } + + if ( + Object.keys(diff).some( + (key) => key === "__old" || key.endsWith("__deleted") + ) + ) { + return false; + } + + if (Array.isArray(diff)) { + for (const field of diff) { + if (!isNonDestructive(field)) { + return false; + } + } + } else { + for (const field in diff) { + if (!isNonDestructive(diff[field])) { + return false; + } + } + } + + return true; +} diff --git a/packages/wrangler/src/utils/diff.ts b/packages/wrangler/src/utils/diff.ts index b2da5ffdfe37..597523d6eda6 100644 --- a/packages/wrangler/src/utils/diff.ts +++ b/packages/wrangler/src/utils/diff.ts @@ -50,6 +50,13 @@ type BestPath = { lastComponent?: Result; }; +/** + * @deprecated When trying to compute the difference between two json object use `diffJsonObjects` instead + * (as it includes a more polished print representation and it also includes information regarding + * the difference between the two objects) + * (For diffing other values, such as TOMLs this class should still be used, hopefully we'll be + * able to move away from TOML files at some point) + */ export class Diff { #results: Result[] = []; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f78e0423dd81..dd8982a56b56 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1184,7 +1184,7 @@ importers: version: 5.8.2 vitest: specifier: ~3.0.7 - version: 3.0.9(@types/debug@4.1.12)(@types/node@20.19.9)(@vitest/ui@3.2.3)(jiti@2.6.0)(lightningcss@1.29.2)(yaml@2.8.1) + version: 3.0.9(@types/debug@4.1.12)(@types/node@20.19.9)(@vitest/ui@3.2.3(vitest@3.2.3))(jiti@2.6.0)(lightningcss@1.29.2)(yaml@2.8.1) wrangler: specifier: workspace:* version: link:../../packages/wrangler @@ -3682,6 +3682,9 @@ importers: '@types/javascript-time-ago': specifier: ^2.0.3 version: 2.0.3 + '@types/json-diff': + specifier: ^1.0.3 + version: 1.0.3 '@types/mime': specifier: ^3.0.4 version: 3.0.4 @@ -3781,6 +3784,9 @@ importers: javascript-time-ago: specifier: ^2.5.4 version: 2.5.7 + json-diff: + specifier: ^1.0.6 + version: 1.0.6 md5-file: specifier: 5.0.0 version: 5.0.0 @@ -5661,6 +5667,9 @@ packages: resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@ewoudenberg/difflib@0.1.0': + resolution: {integrity: sha512-OU5P5mJyD3OoWYMWY+yIgwvgNS9cFAU10f+DDuvtogcWQOoJIsQ4Hy2McSfUfhKjq8L0FuWVb4Rt7kgA+XK86A==} + '@fastify/busboy@2.1.1': resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} engines: {node: '>=14'} @@ -7253,6 +7262,9 @@ packages: '@types/jest@29.5.14': resolution: {integrity: sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==} + '@types/json-diff@1.0.3': + resolution: {integrity: sha512-Qvxm8fpRMv/1zZR3sQWImeRK2mBYJji20xF51Fq9Gt//Ed18u0x6/FNLogLS1xhfUWTEmDyqveJqn95ltB6Kvw==} + '@types/json-schema@7.0.13': resolution: {integrity: sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ==} @@ -8369,6 +8381,10 @@ packages: colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + colors@1.4.0: + resolution: {integrity: sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==} + engines: {node: '>=0.1.90'} + combined-stream@1.0.8: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} @@ -8875,6 +8891,10 @@ packages: peerDependencies: react: '>=16.12.0' + dreamopt@0.8.0: + resolution: {integrity: sha512-vyJTp8+mC+G+5dfgsY+r3ckxlz+QMX40VjPQsZc5gxVAxLmi64TBoVkP54A/pRAXMXsbu2GMMBrZPxNv23waMg==} + engines: {node: '>=0.4.0'} + dts-resolver@2.1.2: resolution: {integrity: sha512-xeXHBQkn2ISSXxbJWD828PFjtyg+/UrMDo7W4Ffcs7+YWCquxU8YjV1KoxuiL+eJ5pg3ll+bC6flVv61L3LKZg==} engines: {node: '>=20.18.0'} @@ -9766,6 +9786,9 @@ packages: resolution: {integrity: sha512-kUGoI3p7u6B41z/dp33G6OaL7J4DRqRYwVmeIlwLClx7yaaAy7hoDExnuejTKtuDwfcatGmddHDEOjf6EyIxtQ==} engines: {node: '>=10.0.0'} + heap@0.2.7: + resolution: {integrity: sha512-2bsegYkkHO+h/9MGbn6KWcE45cHZgPANo5LXF7EvWdT0yT2EguSVO1nDgU5c8+ZOPwp2vMNa7YFsJhVcDR9Sdg==} + hono@4.7.1: resolution: {integrity: sha512-V3eWoPkBxoNgFCkSc5Y5rpLF6YoQQx1pkYO4qrF6YfOw8RZbujUNlJLZCxh0z9gZct70+je2Ih7Zrdpv21hP9w==} engines: {node: '>=16.9.0'} @@ -10254,6 +10277,10 @@ packages: engines: {node: '>=6'} hasBin: true + json-diff@1.0.6: + resolution: {integrity: sha512-tcFIPRdlc35YkYdGxcamJjllUhXWv4n2rK9oJ2RsAzV4FBkuV4ojKEDgcZ+kpKxDmJKv+PFK65+1tVVOnSeEqA==} + hasBin: true + json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -15341,6 +15368,10 @@ snapshots: '@eslint/js@8.57.1': {} + '@ewoudenberg/difflib@0.1.0': + dependencies: + heap: 0.2.7 + '@fastify/busboy@2.1.1': {} '@hono/zod-validator@0.4.2(hono@4.7.1)(zod@3.22.3)': @@ -17068,6 +17099,8 @@ snapshots: expect: 29.7.0 pretty-format: 29.7.0 + '@types/json-diff@1.0.3': {} + '@types/json-schema@7.0.13': {} '@types/json5@0.0.29': {} @@ -18475,6 +18508,8 @@ snapshots: colorette@2.0.20: {} + colors@1.4.0: {} + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 @@ -18937,6 +18972,10 @@ snapshots: react-is: 17.0.2 tslib: 2.8.1 + dreamopt@0.8.0: + dependencies: + wordwrap: 1.0.0 + dts-resolver@2.1.2: {} dunder-proto@1.0.0: @@ -20208,6 +20247,8 @@ snapshots: heap-js@2.5.0: {} + heap@0.2.7: {} + hono@4.7.1: {} hookable@5.5.3: {} @@ -20642,6 +20683,12 @@ snapshots: jsesc@3.0.2: {} + json-diff@1.0.6: + dependencies: + '@ewoudenberg/difflib': 0.1.0 + colors: 1.4.0 + dreamopt: 0.8.0 + json-schema-traverse@0.4.1: {} json-schema-traverse@1.0.0: {} @@ -23800,7 +23847,7 @@ snapshots: - supports-color - terser - vitest@3.0.9(@types/debug@4.1.12)(@types/node@20.19.9)(@vitest/ui@3.2.3)(jiti@2.6.0)(lightningcss@1.29.2)(yaml@2.8.1): + vitest@3.0.9(@types/debug@4.1.12)(@types/node@20.19.9)(@vitest/ui@3.2.3(vitest@3.2.3))(jiti@2.6.0)(lightningcss@1.29.2)(yaml@2.8.1): dependencies: '@vitest/expect': 3.0.9 '@vitest/mocker': 3.0.9(vite@5.4.14(@types/node@20.19.9)(lightningcss@1.29.2))