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: {}