diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index 7c87c0fae..3f95db21c 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -45,6 +45,7 @@ "@types/node": "^22.13.0", "@types/react": "^19.1.0", "@types/react-dom": "^19.1.0", + "happy-dom": "^20.11.2", "postcss": "^8.5.8", "tailwindcss": "^4.2.1", "typescript": "^5.9.3", diff --git a/apps/dashboard/src/app/(dashboard)/deployments/components/DeploymentsContent.tsx b/apps/dashboard/src/app/(dashboard)/deployments/components/DeploymentsContent.tsx index bc839c58f..6f1f2b19f 100644 --- a/apps/dashboard/src/app/(dashboard)/deployments/components/DeploymentsContent.tsx +++ b/apps/dashboard/src/app/(dashboard)/deployments/components/DeploymentsContent.tsx @@ -2,6 +2,7 @@ import React, { useState, useEffect, useMemo, useCallback } from "react"; import Link from "next/link"; +import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { Rocket, Activity, CheckCircle2, XCircle, Loader2, Zap, ArrowRight } from "lucide-react"; import { deployApi, projectsApi } from "@/lib/api"; import { useI18n, interpolate } from "@/components/i18n-provider"; @@ -16,6 +17,40 @@ import { mapRowToDeployment, } from "../utils"; +type StatusFilter = + | "all" + | "success" + | "failed" + | "building" + | "pending" + | "canceled"; + +const STATUS_FILTERS: readonly StatusFilter[] = [ + "all", + "success", + "failed", + "building", + "pending", + "canceled", +]; + +/** Query keys the standalone /deployments view keeps its filters in. */ +const P_STATUS = "status"; +const P_PROJECT = "project"; +const P_QUERY = "q"; + +/** Only accept a status the filter actually has, so a hand-edited URL can't wedge + * the list on a value `filterDeployments` will never match. */ +function readStatus(raw: string | null): StatusFilter { + return STATUS_FILTERS.includes(raw as StatusFilter) ? (raw as StatusFilter) : "all"; +} + +/** Keep the URL clean: a filter at its default is absent, not `?status=all`. */ +function setOrDelete(params: URLSearchParams, key: string, value: string, dflt: string) { + if (value === dflt) params.delete(key); + else params.set(key, value); +} + interface DeploymentsContentProps { /** When set, scope to this project and hide the project selector */ projectId?: string; @@ -35,15 +70,54 @@ export const DeploymentsContent: React.FC = ({ }) => { const { t } = useI18n(); const isProject = !!projectId; + const router = useRouter(); + const pathname = usePathname(); + const searchParams = useSearchParams(); + + /** + * Filters are mirrored into the URL on the standalone /deployments view, so + * opening a deployment and pressing Back returns to the list you were looking at. + * They used to be component state only: Back remounts this component, every + * filter reset to "all", and a filtered-down list silently became the full one. + * + * Scoped to the standalone view on purpose. Embedded in a project (`isProject`) + * the list is already one project's, the project selector is hidden, and that + * page owns its own URL — it rewrites it to `/projects/:id/:tab` after reading + * its params, which would strip anything written here and fight the sync below. + * + * Read once per mount: a Back navigation IS a fresh mount, which is exactly when + * the URL should seed the state. + */ + const urlFilters = !isProject; const [deployments, setDeployments] = useState([]); const [projects, setProjects] = useState([]); const [isLoading, setIsLoading] = useState(true); - const [filter, setFilter] = useState< - "all" | "success" | "failed" | "building" | "pending" | "canceled" - >("all"); - const [searchQuery, setSearchQuery] = useState(""); - const [selectedProjectId, setSelectedProjectId] = useState("all"); + const [filter, setFilter] = useState(() => + urlFilters ? readStatus(searchParams.get(P_STATUS)) : "all", + ); + const [searchQuery, setSearchQuery] = useState(() => + urlFilters ? (searchParams.get(P_QUERY) ?? "") : "", + ); + const [selectedProjectId, setSelectedProjectId] = useState(() => + urlFilters ? (searchParams.get(P_PROJECT) ?? "all") : "all", + ); + + // Write the current filters back to the URL. `replace`, not `push`, so filtering + // never builds up history entries the Back button has to chew through — and + // `scroll: false` so re-filtering doesn't jump the list to the top. Bails when the + // query string is already correct, which is the mount case: no redundant + // navigation, and no loop with the effect's own dependency on searchParams. + useEffect(() => { + if (!urlFilters) return; + const next = new URLSearchParams(Array.from(searchParams.entries())); + setOrDelete(next, P_STATUS, filter, "all"); + setOrDelete(next, P_QUERY, searchQuery, ""); + setOrDelete(next, P_PROJECT, selectedProjectId, "all"); + const qs = next.toString(); + if (qs === searchParams.toString()) return; + router.replace(qs ? `${pathname}?${qs}` : pathname, { scroll: false }); + }, [urlFilters, filter, searchQuery, selectedProjectId, searchParams, pathname, router]); const fetchDeployments = useCallback(async () => { setIsLoading(true); diff --git a/apps/dashboard/src/app/(dashboard)/deployments/components/filters-url-state.render.test.tsx b/apps/dashboard/src/app/(dashboard)/deployments/components/filters-url-state.render.test.tsx new file mode 100644 index 000000000..57949d934 --- /dev/null +++ b/apps/dashboard/src/app/(dashboard)/deployments/components/filters-url-state.render.test.tsx @@ -0,0 +1,195 @@ +// @vitest-environment happy-dom +/** + * Deployment filters must survive a Back navigation. + * + * Reported repro: open /deployments, filter by one project, click a deployment, hit + * Back — the list came back showing ALL projects. The filters were component state + * only, and Back remounts this component, so every one of them reset to its default + * and a deliberately narrowed list silently became the full one. + * + * The fix mirrors them into the query string, so this asserts both halves: changing + * a filter writes the URL, and mounting with that URL restores the filter (which is + * what a Back navigation actually does — it remounts at the previous URL). + */ +import { describe, expect, it, vi, beforeEach, afterEach } from "vitest"; +import { act } from "react"; +import { createRoot, type Root } from "react-dom/client"; +import { I18nProvider } from "@/components/i18n-provider"; + +(globalThis as unknown as { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true; + +/** Every URL the component asked the router to put in the address bar. */ +let replaced: string[] = []; +let searchParams = new URLSearchParams(); + +vi.mock("next/navigation", () => ({ + useRouter: () => ({ + replace: (url: string) => replaced.push(url), + push: (url: string) => replaced.push(url), + back: () => {}, + refresh: () => {}, + }), + usePathname: () => "/deployments", + useSearchParams: () => searchParams, +})); + +// The row's overflow menu is stubbed for ONE reason: it imports `@/utils/icons`, +// which is JSX inside a .js file that the test transform cannot parse, so pulling it +// in takes down any test that renders a deployment card (the monitoring suite +// documents the same constraint). Nothing here asserts on the menu; the card itself +// stays real, because the card is what proves a filter was applied. +vi.mock("./DeploymentMenu", () => ({ DeploymentMenu: () => null })); + +const DEPLOYMENTS = [ + { + id: "d1", + projectId: "p1", + projectName: "alpha", + status: "success", + createdAt: "2026-08-11T10:00:00Z", + }, + { + id: "d2", + projectId: "p2", + projectName: "beta", + status: "failed", + createdAt: "2026-08-11T11:00:00Z", + }, +]; + +function stubFetch() { + return vi.fn(async (input: unknown) => { + const url = String(typeof input === "string" ? input : (input as Request)?.url ?? input); + const json = (body: unknown) => + new Response(JSON.stringify(body), { + status: 200, + headers: { "content-type": "application/json" }, + }); + if (url.includes("deployments")) return json({ data: DEPLOYMENTS }); + return json({ data: [] }); + }); +} + +let container: HTMLDivElement; +let root: Root | undefined; +const errors: unknown[] = []; + +beforeEach(() => { + errors.length = 0; + replaced = []; + searchParams = new URLSearchParams(); + vi.stubGlobal("fetch", stubFetch()); + container = document.createElement("div"); + document.body.appendChild(container); +}); + +afterEach(() => { + if (root) act(() => root!.unmount()); + root = undefined; + container.remove(); + vi.unstubAllGlobals(); +}); + +async function mountDeployments() { + const { DeploymentsContent } = await import("./DeploymentsContent"); + await act(async () => { + root = createRoot(container, { + onUncaughtError: (e) => errors.push(e), + onCaughtError: (e) => errors.push(e), + }); + root.render( + + + , + ); + }); + for (let i = 0; i < 3; i++) { + await act(async () => { + await new Promise((r) => setTimeout(r, 0)); + }); + } +} + +/** Click the status-filter chip with this exact label. */ +async function clickChip(label: RegExp) { + const chip = Array.from(container.querySelectorAll("button")).find((b) => + label.test((b.textContent ?? "").trim()), + ); + expect(chip, `a filter chip matching ${label} should render`).toBeTruthy(); + await act(async () => { + chip!.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); +} + +describe("deployments filters ↔ URL", () => { + it("starts clean: no filter params for an unfiltered list", async () => { + await mountDeployments(); + expect(errors).toEqual([]); + // A default view must not rewrite the URL at all — otherwise every visit + // pushes ?status=all and the 'is anything filtered' check drifts. + expect(replaced).toEqual([]); + expect(container.textContent).toContain("alpha"); + expect(container.textContent).toContain("beta"); + }); + + it("writes the status filter to the URL when it changes", async () => { + await mountDeployments(); + await clickChip(/^failed$/i); + + expect(errors).toEqual([]); + expect(replaced.at(-1)).toBe("/deployments?status=failed"); + }); + + /** The actual regression: this is the state a Back navigation remounts into. */ + it("restores the project filter from the URL on mount", async () => { + searchParams = new URLSearchParams({ project: "p2" }); + await mountDeployments(); + + expect(errors).toEqual([]); + // Only the filtered project's deployment survives... + expect(container.textContent).toContain("beta"); + expect(container.textContent).not.toContain("alpha"); + // ...and restoring must not itself rewrite the URL. + expect(replaced).toEqual([]); + }); + + it("restores the status filter from the URL on mount", async () => { + searchParams = new URLSearchParams({ status: "failed" }); + await mountDeployments(); + + expect(errors).toEqual([]); + expect(container.textContent).toContain("beta"); + expect(container.textContent).not.toContain("alpha"); + expect(replaced).toEqual([]); + }); + + it("ignores a status the filter doesn't have instead of emptying the list", async () => { + searchParams = new URLSearchParams({ status: "not-a-status" }); + await mountDeployments(); + + expect(errors).toEqual([]); + // Falls back to "all" — a hand-edited or stale URL must not wedge the view on a + // value nothing will ever match. + expect(container.textContent).toContain("alpha"); + expect(container.textContent).toContain("beta"); + }); + + it("drops a filter param when it goes back to its default", async () => { + searchParams = new URLSearchParams({ status: "failed" }); + await mountDeployments(); + await clickChip(/^all$/i); + + expect(errors).toEqual([]); + // Back to the default → the param is removed, not left as ?status=all. + expect(replaced.at(-1)).toBe("/deployments"); + }); + + it("keeps unrelated query params intact", async () => { + searchParams = new URLSearchParams({ ref: "email" }); + await mountDeployments(); + await clickChip(/^failed$/i); + + expect(errors).toEqual([]); + expect(replaced.at(-1)).toBe("/deployments?ref=email&status=failed"); + }); +}); diff --git a/bun.lock b/bun.lock index 66a1761d6..aa89bdcef 100644 --- a/bun.lock +++ b/bun.lock @@ -17,7 +17,7 @@ }, "apps/api": { "name": "@repo/api", - "version": "0.6.1", + "version": "0.6.5", "dependencies": { "@better-auth/drizzle-adapter": "^1.5.4", "@hono/node-server": "^1.19.15", @@ -51,7 +51,7 @@ }, "apps/cli": { "name": "openship", - "version": "0.6.1", + "version": "0.6.5", "bin": { "openship": "./dist/node-entry.js", }, @@ -109,6 +109,7 @@ "@types/node": "^22.13.0", "@types/react": "^19.1.0", "@types/react-dom": "^19.1.0", + "happy-dom": "^20.11.2", "postcss": "^8.5.8", "tailwindcss": "^4.2.1", "typescript": "^5.9.3", @@ -117,7 +118,7 @@ }, "apps/desktop": { "name": "@repo/desktop", - "version": "0.6.1", + "version": "0.6.5", "dependencies": { "@repo/core": "workspace:*", "@repo/onboarding": "workspace:*", @@ -140,11 +141,11 @@ }, "apps/email": { "name": "@repo/email", - "version": "0.6.1", + "version": "0.6.5", }, "apps/web": { "name": "@repo/web", - "version": "0.6.1", + "version": "0.6.5", "dependencies": { "@repo/core": "workspace:*", "@repo/ui": "workspace:*", @@ -1074,10 +1075,14 @@ "@types/webidl-conversions": ["@types/webidl-conversions@7.0.3", "", {}, "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA=="], + "@types/whatwg-mimetype": ["@types/whatwg-mimetype@3.0.2", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="], + "@types/whatwg-url": ["@types/whatwg-url@13.0.0", "", { "dependencies": { "@types/webidl-conversions": "*" } }, "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q=="], "@types/wrap-ansi": ["@types/wrap-ansi@3.0.0", "", {}, "sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g=="], + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], + "@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="], "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], @@ -1244,6 +1249,8 @@ "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + "buffer-image-size": ["buffer-image-size@0.6.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ=="], + "buildcheck": ["buildcheck@0.0.7", "", {}, "sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA=="], "bullmq": ["bullmq@5.70.4", "", { "dependencies": { "cron-parser": "4.9.0", "ioredis": "5.9.3", "msgpackr": "1.11.5", "node-abort-controller": "3.1.1", "semver": "7.7.4", "tslib": "2.8.1", "uuid": "11.1.0" } }, "sha512-S58YT/tGdhc4pEPcIahtZRBR1TcTLpss1UKiXimF+Vy4yZwF38pW2IvhHqs4j4dEbZqDt8oi0jGGN/WYQHbPDg=="], @@ -1502,7 +1509,7 @@ "enhanced-resolve": ["enhanced-resolve@5.20.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ=="], - "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + "entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], "env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], @@ -1742,6 +1749,8 @@ "gsap": ["gsap@3.14.2", "", {}, "sha512-P8/mMxVLU7o4+55+1TCnQrPmgjPKnwkzkXOK1asnR9Jg2lna4tEY5qBJjMmAaOBDDZWtlRjBXjLa0w53G/uBLA=="], + "happy-dom": ["happy-dom@20.11.2", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "buffer-image-size": "^0.6.4", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.21.0" } }, "sha512-7MB+bJLkxu3SowAfBJbjW+c55kNz5tkR45gu2qzrxznezhLeN5YIlJbwUgSzlGc+qWoZ8Ykg71H5ezz69xixrw=="], + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], @@ -2938,6 +2947,8 @@ "whatwg-fetch": ["whatwg-fetch@3.6.20", "", {}, "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg=="], + "whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="], + "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], @@ -3120,6 +3131,8 @@ "@types/ssh2/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], + "@types/ws/@types/node": ["@types/node@25.3.5", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA=="], + "ansi-escapes/type-fest": ["type-fest@0.21.3", "", {}, "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w=="], "api/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -3136,6 +3149,8 @@ "bl/buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], + "buffer-image-size/@types/node": ["@types/node@25.3.5", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA=="], + "bullmq/ioredis": ["ioredis@5.9.3", "", { "dependencies": { "@ioredis/commands": "1.5.0", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-VI5tMCdeoxZWU5vjHWsiE/Su76JGhBvWF1MJnV9ZtGltHk9BmD48oDq8Tj8haZ85aceXZMxLNDQZRVo5QKNgXA=="], "bullmq/uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], @@ -3228,6 +3243,10 @@ "glob/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + "happy-dom/@types/node": ["@types/node@25.3.5", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA=="], + + "happy-dom/ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="], + "jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], "load-json-file/pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="], @@ -3290,6 +3309,8 @@ "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], + "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + "path-type/pify": ["pify@2.3.0", "", {}, "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog=="], "pkg-dir/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], @@ -3430,6 +3451,8 @@ "@types/ssh2/@types/node/undici-types": ["undici-types@5.26.5", "", {}, "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="], + "@types/ws/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + "api/ora/cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="], "api/ora/cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], @@ -3446,6 +3469,8 @@ "appdmg/execa/npm-run-path": ["npm-run-path@2.0.2", "", { "dependencies": { "path-key": "^2.0.0" } }, "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw=="], + "buffer-image-size/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + "bullmq/ioredis/@ioredis/commands": ["@ioredis/commands@1.5.0", "", {}, "sha512-eUgLqrMf8nJkZxT24JvVRrQya1vZkQh8BBeYNwGDqa5I0VUi8ACx7uFvAaLxintokpTenkK6DASvo/bvNbBGow=="], "cacache/rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], @@ -3540,6 +3565,8 @@ "glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + "happy-dom/@types/node/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + "log-update/ansi-escapes/type-fest": ["type-fest@1.4.0", "", {}, "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA=="], "log-update/cli-cursor/restore-cursor": ["restore-cursor@4.0.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg=="],