Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion app/(app)/home/course-search.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
'use client';

import React, { Activity, addTransitionType, startTransition, useDeferredValue, useEffect, useMemo, useRef, useState } from 'react';
import React, { Activity, startTransition, useDeferredValue, useEffect, useMemo, useRef, useState } from 'react';
import Image from "@/app/components/common/app-image";
import Link from "next/link";
import { useRouter } from "next/navigation";
import SearchIcon from "@/app/components/assets/seacrh.svg";
import VoiceAgentButton from "@/app/components/voice/voice-agent-button";
import { addTransitionType } from "@/app/components/common/react-transition";
import { getAliasCourseCodes } from "@/lib/course-aliases";
import { createCourseFuse } from "@/lib/course-search-fuse";
import { normalizeCourseCode } from "@/lib/course-tags";
Expand Down
2 changes: 1 addition & 1 deletion app/components/command-palette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import React, {
Activity,
addTransitionType,
startTransition,
useEffect,
useEffectEvent,
Expand All @@ -18,6 +17,7 @@ import {
invalidateAuthSessionCache,
useGuestPrompt,
} from "@/app/components/auth-gate";
import { addTransitionType } from "@/app/components/common/react-transition";
import {
getCommandCatalogAction,
getCommandSessionAction,
Expand Down
7 changes: 4 additions & 3 deletions app/components/common/directional-transition.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"use client";

import React, { ViewTransition, useEffect, useState } from "react";
import React, { useEffect, useState } from "react";
import type { ViewTransitionClass } from "react";
import { OptionalViewTransition } from "@/app/components/common/react-transition";

// `<ViewTransition>` is an experimental React API. It used to be keyed on
// `pathname`, which forced the whole page subtree to remount on every
Expand Down Expand Up @@ -70,12 +71,12 @@ export default function DirectionalTransition({
}

return (
<ViewTransition
<OptionalViewTransition
enter={NAV_TRANSITION_CLASSES}
update={NAV_TRANSITION_CLASSES}
default="none"
>
{children}
</ViewTransition>
</OptionalViewTransition>
);
}
35 changes: 35 additions & 0 deletions app/components/common/react-transition.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"use client";

import * as React from "react";
import type { ViewTransitionClass } from "react";

type ReactTransitionRuntime = typeof React & {
addTransitionType?: (type: string) => void;
ViewTransition?: React.ComponentType<OptionalViewTransitionProps>;
};

type OptionalViewTransitionProps = {
children: React.ReactNode;
name?: string;
enter?: ViewTransitionClass;
exit?: ViewTransitionClass;
update?: ViewTransitionClass;
share?: ViewTransitionClass | string;
default?: string;
};

const reactTransitionRuntime = React as ReactTransitionRuntime;

export function addTransitionType(type: string) {
reactTransitionRuntime.addTransitionType?.(type);
}

export function OptionalViewTransition(props: OptionalViewTransitionProps) {
const ViewTransition = reactTransitionRuntime.ViewTransition;

if (typeof ViewTransition !== "function") {
return <>{props.children}</>;
}

return <ViewTransition {...props} />;
}
3 changes: 2 additions & 1 deletion app/components/mobile-tab-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
import Link from "next/link";
import Image from "@/app/components/common/app-image";
import { usePathname, useRouter } from "next/navigation";
import { addTransitionType, startTransition, useEffect, useReducer, type MouseEvent } from "react";
import { startTransition, useEffect, useReducer, type MouseEvent } from "react";
import { addTransitionType } from "@/app/components/common/react-transition";
import { APP_NAV_LINKS } from "@/lib/app-nav-links";
import c2cStyles from "./mobile-tab-bar.module.css";

Expand Down
3 changes: 2 additions & 1 deletion app/components/nav-bar.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
"use client";
import React, { addTransitionType, startTransition, useCallback, useEffect, useEffectEvent, useReducer, useRef, useSyncExternalStore } from "react";
import React, { startTransition, useCallback, useEffect, useEffectEvent, useReducer, useRef, useSyncExternalStore } from "react";
import dynamic from "next/dynamic";
import Link from "next/link";
import { createPortal } from "react-dom";
import Image from "@/app/components/common/app-image";
import { usePathname, useRouter } from "next/navigation";
import { addTransitionType } from "@/app/components/common/react-transition";
import ThemeToggleSwitch from "@/app/components/common/theme-toggle";
import { SignOut } from "@/app/components/sign-out";
import VoiceAgentButton from "@/app/components/voice/voice-agent-button";
Expand Down
3 changes: 2 additions & 1 deletion app/components/notes/notes-course-search.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
"use client";

import React, { Activity, addTransitionType, startTransition, useDeferredValue, useEffect, useMemo, useRef, useState } from "react";
import React, { Activity, startTransition, useDeferredValue, useEffect, useMemo, useRef, useState } from "react";
import Image from "@/app/components/common/app-image";
import SearchIcon from "@/app/components/assets/seacrh.svg";
import { useRouter } from "next/navigation";
import { addTransitionType } from "@/app/components/common/react-transition";
import { getAliasCourseCodes } from "@/lib/course-aliases";
import { createCourseFuse } from "@/lib/course-search-fuse";
import { normalizeCourseCode } from "@/lib/course-tags";
Expand Down
3 changes: 2 additions & 1 deletion app/components/past_papers/answer-key-toggle.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"use client";

import React, { addTransitionType, useTransition } from "react";
import React, { useTransition } from "react";
import { useRouter } from "next/navigation";
import { addTransitionType } from "@/app/components/common/react-transition";

type Props = {
basePath: string;
Expand Down
7 changes: 4 additions & 3 deletions app/components/past_papers/course-paper-grid.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import React, { ViewTransition, useCallback, useEffect, useEffectEvent, useMemo, useReducer, useRef } from "react";
import React, { useCallback, useEffect, useEffectEvent, useMemo, useReducer, useRef } from "react";
import { createPortal } from "react-dom";
import { useRouter } from "next/navigation";
import {
Expand All @@ -13,6 +13,7 @@ import {
X,
} from "lucide-react";
import { useToast } from "@/app/components/ui/use-toast";
import { OptionalViewTransition } from "@/app/components/common/react-transition";
import CoursePaperCard from "./course-paper-card";
import type { CoursePaperListItem } from "@/lib/data/course-papers";
import { downloadPdfFile, downloadPdfZip } from "@/lib/downloads/browser-downloads";
Expand Down Expand Up @@ -669,7 +670,7 @@ export default function CoursePaperGrid({

<div className="course-paper-grid flex flex-wrap gap-3">
{papers.map((paper, index) => (
<ViewTransition
<OptionalViewTransition
key={paper.id}
enter={{
"filter-results": "paper-card-enter",
Expand Down Expand Up @@ -704,7 +705,7 @@ export default function CoursePaperGrid({
onContextMenuOpen={openContextMenu}
/>
</div>
</ViewTransition>
</OptionalViewTransition>
))}
</div>

Expand Down
3 changes: 2 additions & 1 deletion app/components/past_papers/filter-bar.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"use client";

import React, { addTransitionType, memo, useCallback, useMemo, useTransition } from "react";
import React, { memo, useCallback, useMemo, useTransition } from "react";
import { useRouter } from "next/navigation";
import { addTransitionType } from "@/app/components/common/react-transition";
import { examTypeLabel, examTypeToSlug, examSlugToType } from "@/lib/exam-slug";
import type { Campus, ExamType, Semester } from "@/db";

Expand Down
3 changes: 2 additions & 1 deletion app/components/past_papers/past-papers-course-search.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
"use client";

import React, { Activity, addTransitionType, startTransition, useDeferredValue, useEffect, useMemo, useRef, useState } from "react";
import React, { Activity, startTransition, useDeferredValue, useEffect, useMemo, useRef, useState } from "react";
import Image from "@/app/components/common/app-image";
import Link from "next/link";
import SearchIcon from "@/app/components/assets/seacrh.svg";
import { useRouter } from "next/navigation";
import { addTransitionType } from "@/app/components/common/react-transition";
import { getAliasCourseCodes } from "@/lib/course-aliases";
import { createCourseFuse } from "@/lib/course-search-fuse";
import { normalizeCourseCode } from "@/lib/course-tags";
Expand Down
3 changes: 2 additions & 1 deletion app/components/past_papers/sort-dropdown.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"use client";

import React, { addTransitionType, useTransition } from "react";
import React, { useTransition } from "react";
import { useRouter } from "next/navigation";
import { addTransitionType } from "@/app/components/common/react-transition";

const OPTIONS = [
{ value: "seasonal", label: "Current exam first" },
Expand Down
2 changes: 1 addition & 1 deletion app/components/voice/voice-agent-provider.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"use client";

import React, {
addTransitionType,
createContext,
startTransition,
useCallback,
Expand All @@ -12,6 +11,7 @@ import React, {
useState,
} from "react";
import { useRouter } from "next/navigation";
import { addTransitionType } from "@/app/components/common/react-transition";
import {
createVoiceControlController,
defineVoiceTool,
Expand Down
57 changes: 57 additions & 0 deletions scripts/test-react-transition-runtime-imports.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { readFileSync } from "node:fs";
import { join, relative } from "node:path";
import React from "react";

const repoRoot = process.cwd();

const filesToCheck = [
"app/(app)/home/course-search.tsx",
"app/components/command-palette.tsx",
"app/components/common/directional-transition.tsx",
"app/components/mobile-tab-bar.tsx",
"app/components/nav-bar.tsx",
"app/components/notes/notes-course-search.tsx",
"app/components/past_papers/answer-key-toggle.tsx",
"app/components/past_papers/course-paper-grid.tsx",
"app/components/past_papers/filter-bar.tsx",
"app/components/past_papers/past-papers-course-search.tsx",
"app/components/past_papers/sort-dropdown.tsx",
"app/components/voice/voice-agent-provider.tsx",
];
Comment on lines +7 to +20

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 React transition import checker misses new client modules

This fixed list does not cover future client modules. A temporary unlisted client module containing import { addTransitionType } from "react" was added, but this checker still exited successfully with No unsafe React transition runtime imports found. The check is also absent from the configured package and CI commands, so that unsafe import can be introduced without an automatic failure. Discover applicable client source files instead of maintaining an allowlist, and run the check from an enforced command.

Artifacts

Temporary unsafe React import fixture reproduction script

  • The executed shell harness creates an unlisted client fixture, runs the checker, and removes the fixture afterward; takeaway: the claimed bypass is reproducible without persisting source changes.

Configured package and CI invocation search output

  • The captured search of both package manifests and both CI workflows returned exit code 1 with no checker references; takeaway: configured project commands do not invoke the checker.

Unsafe unlisted client fixture checker output

  • The captured fixture run exited 0 despite an unsafe named React import in a new client module and verified fixture removal; takeaway: the fixed-file checker misses newly added modules.

View artifacts

T-Rex Ran code and verified through T-Rex

Fix in Codex Fix in Claude Code Fix in Cursor


const unsafeRuntimeImportPattern =
/import\s+(?!type\b)(?:[\s\S]*?\{[\s\S]*?\b(?:addTransitionType|ViewTransition)\b[\s\S]*?\}[\s\S]*?|(?:addTransitionType|ViewTransition))\s+from\s+["']react["'];?/m;

const reactRuntime = React as typeof React & {
addTransitionType?: unknown;
ViewTransition?: unknown;
};

const failures: string[] = [];

for (const file of filesToCheck) {
const absolutePath = join(repoRoot, file);
const source = readFileSync(absolutePath, "utf8");

if (unsafeRuntimeImportPattern.test(source)) {
failures.push(relative(repoRoot, absolutePath));
}
}

console.log(
[
`addTransitionType=${typeof reactRuntime.addTransitionType}`,
`ViewTransition=${typeof reactRuntime.ViewTransition}`,
`Activity=${typeof React.Activity}`,
].join(" "),
);

if (failures.length > 0) {
throw new Error(
`Unsafe React transition runtime imports found:\n${failures
.map((file) => `- ${file}`)
.join("\n")}`,
);
}

console.log("No unsafe React transition runtime imports found.");