diff --git a/app/[filename]/ServerRulePage.tsx b/app/[filename]/ServerRulePage.tsx
index 32c51709f..576eda6d6 100644
--- a/app/[filename]/ServerRulePage.tsx
+++ b/app/[filename]/ServerRulePage.tsx
@@ -14,6 +14,7 @@ import { useIsAdminPage } from "@/components/hooks/useIsAdminPage";
import GitHubMetadata from "@/components/last-updated-by";
import RelatedRulesCard from "@/components/RelatedRulesCard";
import RuleActionButtons from "@/components/RuleActionButtons";
+import RuleStatusBadge from "@/components/RuleStatusBadge";
import { SocialVideoEmbed } from "@/components/shared/SocialVideoEmbed";
import { getMarkdownComponentMapping } from "@/components/tina-markdown/markdown-component-mapping";
import { Card } from "@/components/ui/card";
@@ -21,6 +22,7 @@ import { Card } from "@/components/ui/card";
export interface ServerRulePageProps {
rule: any;
brokenReferences?: BrokenReferences | null;
+ pageGeneratedAt?: number;
}
export type ServerRulePagePropsWithTinaProps = {
@@ -33,7 +35,7 @@ export default function ServerRulePage({ serverRulePageProps, tinaProps }: Serve
const rule = data?.rule;
const { isAdmin: isAdminPage, isLoading: isAdminLoading } = useIsAdminPage();
- const { brokenReferences } = serverRulePageProps;
+ const { brokenReferences, pageGeneratedAt } = serverRulePageProps;
const allCategories = rule.categories
?.map((c: any) => {
const cat = c?.category;
@@ -87,7 +89,10 @@ export default function ServerRulePage({ serverRulePageProps, tinaProps }: Serve
diff --git a/app/[filename]/page.tsx b/app/[filename]/page.tsx
index c114d2347..25777fb96 100644
--- a/app/[filename]/page.tsx
+++ b/app/[filename]/page.tsx
@@ -2,6 +2,7 @@ import React from "react";
import categoryTitleIndex from "@/category-uri-title-map.json";
import { Section } from "@/components/layout/section";
import { extractBodyPreview } from "@/lib/bodyUtils";
+import { recordPageGeneration } from "@/lib/revalidation-store";
import { siteUrl } from "@/site-config";
import client from "@/tina/__generated__/client";
import { CategoryWithRulesQueryDocument } from "@/tina/__generated__/types";
@@ -313,6 +314,9 @@ export default async function Page({
const rule = await getRuleData(filename);
if (rule?.data) {
+ const pageGeneratedAt = Date.now();
+ recordPageGeneration(filename, pageGeneratedAt);
+
return (
diff --git a/app/api/github-open-prs/route.ts b/app/api/github-open-prs/route.ts
new file mode 100644
index 000000000..564b42b51
--- /dev/null
+++ b/app/api/github-open-prs/route.ts
@@ -0,0 +1,76 @@
+import { NextRequest, NextResponse } from "next/server";
+import { getGitHubAppToken } from "@/lib/services/github/github.utils";
+import { GITHUB_API_BASE_URL } from "@/lib/services/github/github.constants";
+
+const CACHE_TTL = 300; // 5 minutes
+const GITHUB_ACTIVE_BRANCH = process.env.NEXT_PUBLIC_TINA_BRANCH || "main";
+
+const OPEN_PRS_QUERY = `
+ query SearchOpenPRs($query: String!, $first: Int!) {
+ search(query: $query, type: ISSUE, first: $first) {
+ issueCount
+ }
+ }
+`;
+
+type CacheEntry = { expiresAt: number; hasOpenPRs: boolean };
+const cache = new Map();
+
+export async function GET(request: NextRequest) {
+ const { searchParams } = request.nextUrl;
+ const owner = searchParams.get("owner") || "SSWConsulting";
+ const repo = searchParams.get("repo") || "SSW.Rules.Content";
+ const path = searchParams.get("path");
+
+ if (!path) {
+ return NextResponse.json({ hasOpenPRs: false });
+ }
+
+ // Extract the rule folder name from the path (e.g. "content/rule/do-something/rule.mdx" -> "do-something")
+ const ruleFolder = path.split("/").find((_, i, arr) => arr[i + 1] === "rule.mdx") || path.split("/").slice(-2, -1)[0] || "";
+
+ if (!ruleFolder) {
+ return NextResponse.json({ hasOpenPRs: false });
+ }
+
+ const cacheKey = `${owner}/${repo}/${ruleFolder}`;
+ const cached = cache.get(cacheKey);
+ if (cached && cached.expiresAt > Date.now()) {
+ return NextResponse.json({ hasOpenPRs: cached.hasOpenPRs });
+ }
+
+ try {
+ const token = await getGitHubAppToken();
+
+ const query = `repo:${owner}/${repo} is:pr is:open ${ruleFolder}`;
+ const response = await fetch(GITHUB_API_BASE_URL, {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${token}`,
+ "Content-Type": "application/json",
+ "User-Agent": "Rules.V3",
+ },
+ body: JSON.stringify({
+ query: OPEN_PRS_QUERY,
+ variables: { query, first: 1 },
+ }),
+ next: { revalidate: CACHE_TTL },
+ });
+
+ if (!response.ok) {
+ console.error("GitHub open PRs API error:", response.status);
+ return NextResponse.json({ hasOpenPRs: false });
+ }
+
+ const result = await response.json();
+ const issueCount = result?.data?.search?.issueCount ?? 0;
+ const hasOpenPRs = issueCount > 0;
+
+ cache.set(cacheKey, { hasOpenPRs, expiresAt: Date.now() + CACHE_TTL * 1000 });
+
+ return NextResponse.json({ hasOpenPRs });
+ } catch (err) {
+ console.error("Error checking open PRs:", err);
+ return NextResponse.json({ hasOpenPRs: false });
+ }
+}
diff --git a/app/api/isr-status/route.ts b/app/api/isr-status/route.ts
new file mode 100644
index 000000000..f3dd59b8d
--- /dev/null
+++ b/app/api/isr-status/route.ts
@@ -0,0 +1,17 @@
+import { NextResponse } from "next/server";
+import { getSlugStatus } from "@/lib/revalidation-store";
+
+export async function GET(req: Request) {
+ const { searchParams } = new URL(req.url);
+ const slug = searchParams.get("slug");
+
+ if (!slug || typeof slug !== "string") {
+ return NextResponse.json({ error: "slug parameter is required" }, { status: 400 });
+ }
+
+ const status = getSlugStatus(slug);
+
+ return NextResponse.json(status, {
+ headers: { "Cache-Control": "no-store" },
+ });
+}
diff --git a/app/api/revalidate/route.ts b/app/api/revalidate/route.ts
index d93fa7257..1e253c985 100644
--- a/app/api/revalidate/route.ts
+++ b/app/api/revalidate/route.ts
@@ -1,5 +1,6 @@
import { revalidatePath, revalidateTag } from "next/cache";
import { NextResponse } from "next/server";
+import { recordRevalidation } from "@/lib/revalidation-store";
enum TINA_CONTENT_CHANGE_TYPE {
Modified = "content.modified",
@@ -36,6 +37,7 @@ export async function POST(req: Request) {
const slug = changedPath.replace("public/uploads/rules/", "").replace("/rule.mdx", "").replace(/\/+$/, "");
if (slug) {
routesToRevalidate.add(`/${slug}`);
+ recordRevalidation(slug);
}
// If change type is add then we also need to revalidate the /api/rules route
if (eventType === TINA_CONTENT_CHANGE_TYPE.Added) {
diff --git a/components/RuleStatusBadge.tsx b/components/RuleStatusBadge.tsx
new file mode 100644
index 000000000..299fbf20e
--- /dev/null
+++ b/components/RuleStatusBadge.tsx
@@ -0,0 +1,54 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import Tooltip from "@/components/tooltip/tooltip";
+import { getRuleStatus, getRuleStatusDescription, type RuleStatus } from "@/lib/ruleStatus";
+import { cn } from "@/lib/utils";
+
+interface RuleStatusBadgeProps {
+ pageGeneratedAt?: number;
+ ruleSlug?: string;
+ className?: string;
+}
+
+const dotStyles = {
+ fresh: "bg-green-500",
+ stale: "bg-orange-500",
+ rebuilding: "bg-yellow-500",
+} as const;
+
+export default function RuleStatusBadge({ pageGeneratedAt, ruleSlug, className }: RuleStatusBadgeProps) {
+ const [status, setStatus] = useState("fresh");
+ const [loaded, setLoaded] = useState(false);
+
+ useEffect(() => {
+ if (!ruleSlug) return;
+
+ const fetchStatus = async () => {
+ try {
+ const params = new URLSearchParams({ slug: ruleSlug });
+ const response = await fetch(`${process.env.NEXT_PUBLIC_BASE_PATH ?? ""}/api/isr-status?${params.toString()}`);
+ if (response.ok) {
+ const { lastWebhookAt } = await response.json();
+ setStatus(getRuleStatus(pageGeneratedAt, lastWebhookAt));
+ }
+ } catch {
+ // Silently fail — default to "fresh"
+ } finally {
+ setLoaded(true);
+ }
+ };
+
+ fetchStatus();
+ }, [ruleSlug, pageGeneratedAt]);
+
+ if (!loaded) return null;
+
+ const description = getRuleStatusDescription(status);
+
+ return (
+
+
+
+ );
+}
diff --git a/lib/revalidation-store.ts b/lib/revalidation-store.ts
new file mode 100644
index 000000000..19e22737b
--- /dev/null
+++ b/lib/revalidation-store.ts
@@ -0,0 +1,37 @@
+// In-memory store for tracking ISR revalidation events per rule slug.
+// NOTE: This store is not shared across server instances. In a multi-instance
+// deployment (e.g. multiple Azure containers), each instance maintains its own
+// copy. For production at scale, consider a shared store (Redis, Azure Table Storage).
+
+interface SlugStatus {
+ lastWebhookAt: number | null;
+ lastGeneratedAt: number | null;
+}
+
+const store = new Map();
+
+function getOrCreate(slug: string): SlugStatus {
+ let entry = store.get(slug);
+ if (!entry) {
+ entry = { lastWebhookAt: null, lastGeneratedAt: null };
+ store.set(slug, entry);
+ }
+ return entry;
+}
+
+/** Record that a TinaCMS webhook triggered revalidation for a rule slug. */
+export function recordRevalidation(slug: string): void {
+ const entry = getOrCreate(slug);
+ entry.lastWebhookAt = Date.now();
+}
+
+/** Record that a rule page was generated (ISR) at the given timestamp. */
+export function recordPageGeneration(slug: string, timestamp: number): void {
+ const entry = getOrCreate(slug);
+ entry.lastGeneratedAt = timestamp;
+}
+
+/** Get the revalidation/generation timestamps for a rule slug. */
+export function getSlugStatus(slug: string): SlugStatus {
+ return store.get(slug) ?? { lastWebhookAt: null, lastGeneratedAt: null };
+}
diff --git a/lib/ruleStatus.ts b/lib/ruleStatus.ts
new file mode 100644
index 000000000..b30591a14
--- /dev/null
+++ b/lib/ruleStatus.ts
@@ -0,0 +1,45 @@
+export type RuleStatus = "fresh" | "stale" | "rebuilding";
+
+/** If a webhook was received but the page hasn't regenerated within this window, consider it "rebuilding". Beyond this, it's "stale". */
+const REBUILDING_THRESHOLD_MS = 5 * 60 * 1000; // 5 minutes
+
+/**
+ * Determine the ISR status of a rule page by comparing timestamps.
+ *
+ * @param pageGeneratedAt - When this static page was last rendered (baked into HTML at ISR time)
+ * @param lastWebhookAt - When the most recent TinaCMS webhook fired for this slug (from revalidation-store)
+ * @param now - Current time in ms (defaults to Date.now(), injectable for testing)
+ */
+export function getRuleStatus(pageGeneratedAt: number | undefined, lastWebhookAt: number | null, now: number = Date.now()): RuleStatus {
+ // No webhook recorded → page is as fresh as it can be
+ if (!lastWebhookAt) return "fresh";
+
+ // Page was generated after the webhook → content is up to date
+ if (pageGeneratedAt && pageGeneratedAt >= lastWebhookAt) return "fresh";
+
+ // Webhook fired but page hasn't regenerated yet
+ const elapsed = now - lastWebhookAt;
+ return elapsed < REBUILDING_THRESHOLD_MS ? "rebuilding" : "stale";
+}
+
+export function getRuleStatusLabel(status: RuleStatus): string {
+ switch (status) {
+ case "fresh":
+ return "Fresh";
+ case "stale":
+ return "Stale";
+ case "rebuilding":
+ return "Being Rebuilt";
+ }
+}
+
+export function getRuleStatusDescription(status: RuleStatus): string {
+ switch (status) {
+ case "fresh":
+ return "This page is up to date with the latest content";
+ case "stale":
+ return "Content has changed but this page has not been regenerated yet";
+ case "rebuilding":
+ return "Content has changed and the page is being regenerated";
+ }
+}
diff --git a/package.json b/package.json
index 892ce7df7..0423eb271 100644
--- a/package.json
+++ b/package.json
@@ -36,6 +36,7 @@
"postcss": "^8.5.3",
"postcss-import": "^16.1.0",
"postcss-nesting": "^13.0.1",
+ "ts-node": "^10.9.2",
"typescript": "^5.8.2"
},
"dependencies": {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 20ec6c114..ed449ca0f 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -218,7 +218,7 @@ importers:
version: 10.1.0
jest:
specifier: ^30.2.0
- version: 30.3.0(@types/node@22.19.15)
+ version: 30.3.0(@types/node@22.19.15)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3))
jest-environment-jsdom:
specifier: ^30.2.0
version: 30.3.0
@@ -231,6 +231,9 @@ importers:
postcss-nesting:
specifier: ^13.0.1
version: 13.0.2(postcss@8.5.8)
+ ts-node:
+ specifier: ^10.9.2
+ version: 10.9.2(@types/node@22.19.15)(typescript@5.9.3)
typescript:
specifier: ^5.8.2
version: 5.9.3
@@ -1179,6 +1182,10 @@ packages:
'@corex/deepmerge@4.0.43':
resolution: {integrity: sha512-N8uEMrMPL0cu/bdboEWpQYb/0i2K5Qn8eCsxzOmxSggJbbQte7ljMRoXm917AbntqTGOzdTu+vP3KOOzoC70HQ==}
+ '@cspotcode/source-map-support@0.8.1':
+ resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
+ engines: {node: '>=12'}
+
'@csstools/color-helpers@5.1.0':
resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==}
engines: {node: '>=18'}
@@ -2093,6 +2100,9 @@ packages:
'@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
+ '@jridgewell/trace-mapping@0.3.9':
+ resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
+
'@js-sdsl/ordered-map@4.4.2':
resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==}
@@ -3681,6 +3691,18 @@ packages:
resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==}
engines: {node: '>=10.13.0'}
+ '@tsconfig/node10@1.0.12':
+ resolution: {integrity: sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==}
+
+ '@tsconfig/node12@1.0.11':
+ resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==}
+
+ '@tsconfig/node14@1.0.3':
+ resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==}
+
+ '@tsconfig/node16@1.0.4':
+ resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==}
+
'@tybys/wasm-util@0.10.1':
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
@@ -4297,10 +4319,9 @@ packages:
peerDependencies:
acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
- acorn@8.15.0:
- resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
+ acorn-walk@8.3.5:
+ resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==}
engines: {node: '>=0.4.0'}
- hasBin: true
acorn@8.16.0:
resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==}
@@ -4371,6 +4392,9 @@ packages:
resolution: {integrity: sha512-S84oAM3By//EL98JhO9nYOgnvamEQxWzJyDGDVGMMJ1PdjFdxAuyy47SPm8kvY9fQdtLTM0vXMuG1SeT39ONxw==}
engines: {node: '>=18.0.0'}
+ arg@4.1.3:
+ resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==}
+
arg@5.0.2:
resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
@@ -4840,6 +4864,9 @@ packages:
engines: {node: '>=0.8'}
hasBin: true
+ create-require@1.1.1:
+ resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
+
crelt@1.0.6:
resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
@@ -5205,6 +5232,10 @@ packages:
diff3@0.0.3:
resolution: {integrity: sha512-iSq8ngPOt0K53A6eVr4d5Kn6GNrM2nQZtC740pzIriHtn4pOQ2lyzEXQMBeVcWERN0ye7fhBsk9PbLLQOnUx/g==}
+ diff@4.0.4:
+ resolution: {integrity: sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==}
+ engines: {node: '>=0.3.1'}
+
diff@5.2.2:
resolution: {integrity: sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==}
engines: {node: '>=0.3.1'}
@@ -6552,6 +6583,9 @@ packages:
resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==}
engines: {node: '>=10'}
+ make-error@1.3.6:
+ resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
+
makeerror@1.0.12:
resolution: {integrity: sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==}
@@ -8435,6 +8469,20 @@ packages:
ts-interface-checker@0.1.13:
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
+ ts-node@10.9.2:
+ resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==}
+ hasBin: true
+ peerDependencies:
+ '@swc/core': '>=1.2.50'
+ '@swc/wasm': '>=1.2.50'
+ '@types/node': '*'
+ typescript: '>=2.7'
+ peerDependenciesMeta:
+ '@swc/core':
+ optional: true
+ '@swc/wasm':
+ optional: true
+
tslib@1.10.0:
resolution: {integrity: sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==}
@@ -8724,6 +8772,9 @@ packages:
engines: {node: '>=8'}
hasBin: true
+ v8-compile-cache-lib@3.0.1:
+ resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
+
v8-to-istanbul@9.3.0:
resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==}
engines: {node: '>=10.12.0'}
@@ -8933,6 +8984,10 @@ packages:
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
engines: {node: '>=12'}
+ yn@3.1.1:
+ resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==}
+ engines: {node: '>=6'}
+
yocto-queue@0.1.0:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
@@ -10277,6 +10332,10 @@ snapshots:
'@corex/deepmerge@4.0.43': {}
+ '@cspotcode/source-map-support@0.8.1':
+ dependencies:
+ '@jridgewell/trace-mapping': 0.3.9
+
'@csstools/color-helpers@5.1.0': {}
'@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
@@ -10986,7 +11045,7 @@ snapshots:
jest-util: 30.3.0
slash: 3.0.0
- '@jest/core@30.3.0':
+ '@jest/core@30.3.0(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3))':
dependencies:
'@jest/console': 30.3.0
'@jest/pattern': 30.0.1
@@ -11001,7 +11060,7 @@ snapshots:
exit-x: 0.2.2
graceful-fs: 4.2.11
jest-changed-files: 30.3.0
- jest-config: 30.3.0(@types/node@22.19.15)
+ jest-config: 30.3.0(@types/node@22.19.15)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3))
jest-haste-map: 30.3.0
jest-message-util: 30.3.0
jest-regex-util: 30.0.1
@@ -11200,6 +11259,11 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
+ '@jridgewell/trace-mapping@0.3.9':
+ dependencies:
+ '@jridgewell/resolve-uri': 3.1.2
+ '@jridgewell/sourcemap-codec': 1.5.5
+
'@js-sdsl/ordered-map@4.4.2': {}
'@jsep-plugin/assignment@1.3.0(jsep@1.4.0)':
@@ -13124,6 +13188,14 @@ snapshots:
'@trysound/sax@0.2.0': {}
+ '@tsconfig/node10@1.0.12': {}
+
+ '@tsconfig/node12@1.0.11': {}
+
+ '@tsconfig/node14@1.0.3': {}
+
+ '@tsconfig/node16@1.0.4': {}
+
'@tybys/wasm-util@0.10.1':
dependencies:
tslib: 2.8.1
@@ -13829,7 +13901,9 @@ snapshots:
dependencies:
acorn: 8.16.0
- acorn@8.15.0: {}
+ acorn-walk@8.3.5:
+ dependencies:
+ acorn: 8.16.0
acorn@8.16.0: {}
@@ -13921,6 +13995,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ arg@4.1.3: {}
+
arg@5.0.2: {}
argparse@1.0.10:
@@ -14431,6 +14507,8 @@ snapshots:
crc-32@1.2.2: {}
+ create-require@1.1.1: {}
+
crelt@1.0.6: {}
cross-env@10.1.0:
@@ -14819,6 +14897,8 @@ snapshots:
diff3@0.0.3: {}
+ diff@4.0.4: {}
+
diff@5.2.2: {}
dir-glob@3.0.1:
@@ -15771,15 +15851,15 @@ snapshots:
- babel-plugin-macros
- supports-color
- jest-cli@30.3.0(@types/node@22.19.15):
+ jest-cli@30.3.0(@types/node@22.19.15)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)):
dependencies:
- '@jest/core': 30.3.0
+ '@jest/core': 30.3.0(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3))
'@jest/test-result': 30.3.0
'@jest/types': 30.3.0
chalk: 4.1.2
exit-x: 0.2.2
import-local: 3.2.0
- jest-config: 30.3.0(@types/node@22.19.15)
+ jest-config: 30.3.0(@types/node@22.19.15)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3))
jest-util: 30.3.0
jest-validate: 30.3.0
yargs: 17.7.2
@@ -15790,7 +15870,7 @@ snapshots:
- supports-color
- ts-node
- jest-config@30.3.0(@types/node@22.19.15):
+ jest-config@30.3.0(@types/node@22.19.15)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)):
dependencies:
'@babel/core': 7.29.0
'@jest/get-type': 30.1.0
@@ -15817,6 +15897,7 @@ snapshots:
strip-json-comments: 3.1.1
optionalDependencies:
'@types/node': 22.19.15
+ ts-node: 10.9.2(@types/node@22.19.15)(typescript@5.9.3)
transitivePeerDependencies:
- babel-plugin-macros
- supports-color
@@ -16087,12 +16168,12 @@ snapshots:
merge-stream: 2.0.0
supports-color: 8.1.1
- jest@30.3.0(@types/node@22.19.15):
+ jest@30.3.0(@types/node@22.19.15)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3)):
dependencies:
- '@jest/core': 30.3.0
+ '@jest/core': 30.3.0(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3))
'@jest/types': 30.3.0
import-local: 3.2.0
- jest-cli: 30.3.0(@types/node@22.19.15)
+ jest-cli: 30.3.0(@types/node@22.19.15)(ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3))
transitivePeerDependencies:
- '@types/node'
- babel-plugin-macros
@@ -16442,6 +16523,8 @@ snapshots:
dependencies:
semver: 7.7.4
+ make-error@1.3.6: {}
+
makeerror@1.0.12:
dependencies:
tmpl: 1.0.5
@@ -17075,7 +17158,7 @@ snapshots:
mlly@1.8.0:
dependencies:
- acorn: 8.15.0
+ acorn: 8.16.0
pathe: 2.0.3
pkg-types: 1.3.1
ufo: 1.6.3
@@ -18914,6 +18997,24 @@ snapshots:
ts-interface-checker@0.1.13: {}
+ ts-node@10.9.2(@types/node@22.19.15)(typescript@5.9.3):
+ dependencies:
+ '@cspotcode/source-map-support': 0.8.1
+ '@tsconfig/node10': 1.0.12
+ '@tsconfig/node12': 1.0.11
+ '@tsconfig/node14': 1.0.3
+ '@tsconfig/node16': 1.0.4
+ '@types/node': 22.19.15
+ acorn: 8.16.0
+ acorn-walk: 8.3.5
+ arg: 4.1.3
+ create-require: 1.1.1
+ diff: 4.0.4
+ make-error: 1.3.6
+ typescript: 5.9.3
+ v8-compile-cache-lib: 3.0.1
+ yn: 3.1.1
+
tslib@1.10.0: {}
tslib@1.14.1: {}
@@ -19084,7 +19185,7 @@ snapshots:
unplugin@2.3.11:
dependencies:
'@jridgewell/remapping': 2.3.5
- acorn: 8.15.0
+ acorn: 8.16.0
picomatch: 4.0.3
webpack-virtual-modules: 0.6.2
@@ -19187,6 +19288,8 @@ snapshots:
kleur: 4.1.5
sade: 1.8.1
+ v8-compile-cache-lib@3.0.1: {}
+
v8-to-istanbul@9.3.0:
dependencies:
'@jridgewell/trace-mapping': 0.3.31
@@ -19372,6 +19475,8 @@ snapshots:
y18n: 5.0.8
yargs-parser: 21.1.1
+ yn@3.1.1: {}
+
yocto-queue@0.1.0: {}
youtube-video-element@1.8.1: {}