From 323ce26d70b0abb4980f5e8ac88db0aa8354b72d Mon Sep 17 00:00:00 2001 From: Andreas Arvidsson Date: Sat, 14 Mar 2026 16:57:07 +0100 Subject: [PATCH 1/3] Replace Preact with react --- packages/cheatsheet-local/package.json | 10 +- .../cheatsheet-local/src/app/app.spec.tsx | 29 +- packages/cheatsheet-local/src/index.tsx | 18 +- packages/cheatsheet-local/tsconfig.json | 1 + packages/cheatsheet-local/vite.config.ts | 4 +- packages/cheatsheet/package.json | 11 +- .../cheatsheet/src/lib/CheatsheetPage.tsx | 4 +- .../cheatsheet/src/lib/cheatsheet.spec.tsx | 21 +- .../components/CheatsheetLegendComponent.tsx | 4 +- .../components/CheatsheetListComponent.tsx | 2 +- .../components/CheatsheetNotesComponent.tsx | 4 +- .../src/lib/components/SmartLink.tsx | 12 +- .../src/lib/components/formatCaptures.tsx | 35 +- packages/cheatsheet/tsconfig.json | 1 + .../src/docs/components/Header.tsx | 4 +- .../src/docs/components/ScopeVisualizer.tsx | 4 +- .../contributing/MissingLanguageScopes.tsx | 17 +- packages/cursorless-org/package.json | 9 +- packages/cursorless-org/src/App.tsx | 2 +- .../cursorless-org/src/embedded-video.tsx | 3 +- packages/cursorless-org/src/index.tsx | 13 +- packages/cursorless-org/src/vite-env.d.ts | 8 +- packages/cursorless-org/tsconfig.json | 1 + packages/cursorless-org/vite.config.ts | 4 +- .../package.json | 5 +- .../src/App.tsx | 4 +- .../src/ArrowLeftIcon.tsx | 2 +- .../src/ArrowRightIcon.tsx | 2 +- .../src/CloseIcon.tsx | 2 +- .../src/Command.tsx | 2 +- .../src/ProgressBar.tsx | 2 +- .../src/TutorialStep.tsx | 2 +- .../src/index.tsx | 14 +- .../tsconfig.json | 3 +- pnpm-lock.yaml | 369 ++++++++---------- 35 files changed, 307 insertions(+), 321 deletions(-) diff --git a/packages/cheatsheet-local/package.json b/packages/cheatsheet-local/package.json index 8898baad17..7eacb64b6c 100644 --- a/packages/cheatsheet-local/package.json +++ b/packages/cheatsheet-local/package.json @@ -19,6 +19,7 @@ } }, "scripts": { + "test": "jest", "compile": "tsc --build", "watch": "tsc --build --watch", "build": "pnpm build:prod", @@ -29,19 +30,14 @@ "dependencies": { "@cursorless/cheatsheet": "workspace:*", "@cursorless/common": "workspace:*", - "react": "^19.2.4", - "react-dom": "^19.2.4", + "preact": "^10.29.0", "tslib": "^2.8.1" }, "devDependencies": { + "@preact/preset-vite": "^2.10.3", "@tailwindcss/postcss": "^4.2.1", - "@testing-library/dom": "^10.4.1", - "@testing-library/react": "^16.3.2", "@types/jest": "^30.0.0", "@types/node": "^24.12.0", - "@types/react": "^19.2.14", - "@types/react-dom": "^19.2.3", - "@vitejs/plugin-react": "^6.0.1", "jest": "^30.3.0", "postcss": "^8.5.8", "tailwindcss": "^4.2.1", diff --git a/packages/cheatsheet-local/src/app/app.spec.tsx b/packages/cheatsheet-local/src/app/app.spec.tsx index 4326c685cf..9ec03ce044 100644 --- a/packages/cheatsheet-local/src/app/app.spec.tsx +++ b/packages/cheatsheet-local/src/app/app.spec.tsx @@ -1,16 +1,31 @@ -import { render } from "@testing-library/react"; +import { render } from "preact"; +import { act } from "preact/test-utils"; import { App } from "./app"; describe("App", () => { - it("should render successfully", () => { - const { baseElement } = render(); + afterEach(() => { + document.body.innerHTML = ""; + }); + + it("should render successfully", async () => { + const container = document.createElement("div"); + document.body.append(container); - expect(baseElement).toBeTruthy(); + await act(() => { + render(, container); + }); + + expect(container).toBeTruthy(); }); - it("should have a greeting as the title", () => { - const { getByText } = render(); + it("should have a greeting as the title", async () => { + const container = document.createElement("div"); + document.body.append(container); + + await act(() => { + render(, container); + }); - expect(getByText(/Welcome cheatsheet-local/gi)).toBeTruthy(); + expect(container.textContent).toMatch(/Welcome cheatsheet-local/gi); }); }); diff --git a/packages/cheatsheet-local/src/index.tsx b/packages/cheatsheet-local/src/index.tsx index ea9956ce43..b08e8d5b1c 100644 --- a/packages/cheatsheet-local/src/index.tsx +++ b/packages/cheatsheet-local/src/index.tsx @@ -1,12 +1,18 @@ -import { StrictMode } from "react"; -import * as ReactDOM from "react-dom/client"; +import { render } from "preact"; +import { StrictMode } from "preact/compat"; import { App } from "./app/app"; -const root = ReactDOM.createRoot( - document.getElementById("root") as HTMLElement, -); -root.render( +render( , + getRoot(), ); + +function getRoot() { + const root = document.getElementById("root"); + if (root == null) { + throw new Error("Missing root container"); + } + return root; +} diff --git a/packages/cheatsheet-local/tsconfig.json b/packages/cheatsheet-local/tsconfig.json index 7c7f5c8486..1af66ad93a 100644 --- a/packages/cheatsheet-local/tsconfig.json +++ b/packages/cheatsheet-local/tsconfig.json @@ -4,6 +4,7 @@ "target": "es2022", "lib": ["dom", "es2022"], "jsx": "react-jsx", + "jsxImportSource": "preact", "allowSyntheticDefaultImports": true, "skipLibCheck": true, "esModuleInterop": true, diff --git a/packages/cheatsheet-local/vite.config.ts b/packages/cheatsheet-local/vite.config.ts index 45a0a5ec21..aae848e987 100644 --- a/packages/cheatsheet-local/vite.config.ts +++ b/packages/cheatsheet-local/vite.config.ts @@ -1,6 +1,6 @@ import { fakeCheatsheetInfo } from "@cursorless/cheatsheet"; import { viteHtmlParams } from "@cursorless/common"; -import react from "@vitejs/plugin-react"; +import preact from "@preact/preset-vite"; import { defineConfig } from "vite"; import { viteSingleFile } from "vite-plugin-singlefile"; @@ -11,7 +11,7 @@ export default defineConfig(() => { }, plugins: [ - react(), + preact(), viteSingleFile(), viteHtmlParams({ FAKE_CHEATSHEET_INFO: JSON.stringify(fakeCheatsheetInfo), diff --git a/packages/cheatsheet/package.json b/packages/cheatsheet/package.json index 4679603d3d..ce0929f80a 100644 --- a/packages/cheatsheet/package.json +++ b/packages/cheatsheet/package.json @@ -1,7 +1,7 @@ { "name": "@cursorless/cheatsheet", "version": "0.1.0", - "description": "Core cheatsheet react component", + "description": "Core cheatsheet Preact component", "license": "MIT", "type": "module", "main": "./out/index.js", @@ -29,18 +29,11 @@ "@fortawesome/fontawesome-svg-core": "^7.2.0", "@fortawesome/free-solid-svg-icons": "^7.2.0", "@fortawesome/react-fontawesome": "^3.2.0", - "react": "^19.2.4", - "react-dom": "^19.2.4", - "react-string-replace": "^2.0.1", + "preact": "^10.29.0", "react-use": "^17.6.0" }, "devDependencies": { - "@testing-library/dom": "^10.4.1", - "@testing-library/react": "^16.3.2", "@types/jest": "^30.0.0", - "@types/react": "^19.2.14", - "@types/react-dom": "^19.2.3", - "@types/react-helmet": "^6.1.11", "jest": "^30.3.0", "jest-environment-jsdom": "^30.3.0", "ts-jest": "^29.4.6", diff --git a/packages/cheatsheet/src/lib/CheatsheetPage.tsx b/packages/cheatsheet/src/lib/CheatsheetPage.tsx index 3e591cca4b..13a82f2649 100644 --- a/packages/cheatsheet/src/lib/CheatsheetPage.tsx +++ b/packages/cheatsheet/src/lib/CheatsheetPage.tsx @@ -1,4 +1,4 @@ -import * as React from "react"; +import type { ComponentChildren } from "preact"; import CheatsheetListComponent from "./components/CheatsheetListComponent"; import CheatsheetLegendComponent from "./components/CheatsheetLegendComponent"; import cheatsheetLegend from "./cheatsheetLegend"; @@ -56,7 +56,7 @@ function Cheatsheet({ cheatsheetInfo }: Props) { } type CheatsheetSectionProps = { - children?: React.ReactNode; + children?: ComponentChildren; }; function CheatsheetSection({ children }: CheatsheetSectionProps) { diff --git a/packages/cheatsheet/src/lib/cheatsheet.spec.tsx b/packages/cheatsheet/src/lib/cheatsheet.spec.tsx index 5b87ae62be..8749aefb87 100644 --- a/packages/cheatsheet/src/lib/cheatsheet.spec.tsx +++ b/packages/cheatsheet/src/lib/cheatsheet.spec.tsx @@ -1,12 +1,21 @@ -import { render } from "@testing-library/react"; +import { render } from "preact"; +import { act } from "preact/test-utils"; import { CheatsheetPage } from "./CheatsheetPage"; import { fakeCheatsheetInfo } from "./fakeCheatsheetInfo"; describe("Cheatsheet", () => { - it("should render successfully", () => { - const { baseElement } = render( - , - ); - expect(baseElement).toBeTruthy(); + afterEach(() => { + document.body.innerHTML = ""; + }); + + it("should render successfully", async () => { + const container = document.createElement("div"); + document.body.append(container); + + await act(() => { + render(, container); + }); + + expect(container).toBeTruthy(); }); }); diff --git a/packages/cheatsheet/src/lib/components/CheatsheetLegendComponent.tsx b/packages/cheatsheet/src/lib/components/CheatsheetLegendComponent.tsx index c73d7bc4bf..5adcb95799 100644 --- a/packages/cheatsheet/src/lib/components/CheatsheetLegendComponent.tsx +++ b/packages/cheatsheet/src/lib/components/CheatsheetLegendComponent.tsx @@ -1,4 +1,4 @@ -import * as React from "react"; +import type { JSX } from "preact"; import type { CheatsheetLegend } from "../cheatsheetLegend"; import useIsHighlighted from "../hooks/useIsHighlighted"; import { formatCaptures } from "./formatCaptures"; @@ -10,7 +10,7 @@ type Props = { export default function CheatsheetLegendComponent({ data, -}: Props): React.JSX.Element { +}: Props): JSX.Element { const isHighlighted = useIsHighlighted("legend"); const borderClassName = isHighlighted diff --git a/packages/cheatsheet/src/lib/components/CheatsheetListComponent.tsx b/packages/cheatsheet/src/lib/components/CheatsheetListComponent.tsx index 0f53149208..7f7bdfcc53 100644 --- a/packages/cheatsheet/src/lib/components/CheatsheetListComponent.tsx +++ b/packages/cheatsheet/src/lib/components/CheatsheetListComponent.tsx @@ -1,4 +1,4 @@ -import type { JSX } from "react"; +import type { JSX } from "preact"; import type { CheatsheetSection, Variation } from "../CheatsheetInfo"; import useIsHighlighted from "../hooks/useIsHighlighted"; import { formatCaptures } from "./formatCaptures"; diff --git a/packages/cheatsheet/src/lib/components/CheatsheetNotesComponent.tsx b/packages/cheatsheet/src/lib/components/CheatsheetNotesComponent.tsx index 3e7246d298..87c8215cb1 100644 --- a/packages/cheatsheet/src/lib/components/CheatsheetNotesComponent.tsx +++ b/packages/cheatsheet/src/lib/components/CheatsheetNotesComponent.tsx @@ -1,7 +1,7 @@ -import * as React from "react"; +import type { JSX } from "preact"; import SmartLink from "./SmartLink"; -export default function CheatsheetNotesComponent(): React.JSX.Element { +export default function CheatsheetNotesComponent(): JSX.Element { return (
= ({ +export default function SmartLink({ to, children, noFormatting = false, -}) => { +}: SmartLinkProps) { const className = noFormatting ? "" : "text-blue-500 hover:text-violet-700 dark:text-cyan-400 dark:hover:text-violet-200"; @@ -35,6 +35,4 @@ const SmartLink: React.FC = ({ )} ); -}; - -export default SmartLink; +} diff --git a/packages/cheatsheet/src/lib/components/formatCaptures.tsx b/packages/cheatsheet/src/lib/components/formatCaptures.tsx index ccb6fdfd6d..a9f46b0e63 100644 --- a/packages/cheatsheet/src/lib/components/formatCaptures.tsx +++ b/packages/cheatsheet/src/lib/components/formatCaptures.tsx @@ -1,20 +1,30 @@ -import reactStringReplace from "react-string-replace"; +import type { ComponentChildren } from "preact"; import SmartLink from "./SmartLink"; export function formatCaptures(input: string) { - return reactStringReplace(input, captureRegex, (match, i) => { + const parts: ComponentChildren[] = []; + let lastIndex = 0; + + for (const match of input.matchAll(captureRegex)) { + const [fullMatch, capture] = match; + const index = match.index ?? 0; + + if (index > lastIndex) { + parts.push(input.slice(lastIndex, index)); + } + const innerElement = - match === "ordinal" ? ( + capture === "ordinal" ? ( nth ) : ( - match + capture ); - return ( + parts.push( @@ -22,8 +32,17 @@ export function formatCaptures(input: string) { {innerElement} {"]"} - + , ); - }); + + lastIndex = index + fullMatch.length; + } + + if (lastIndex < input.length) { + parts.push(input.slice(lastIndex)); + } + + return parts; } + const captureRegex = /<([^>]+)>/g; diff --git a/packages/cheatsheet/tsconfig.json b/packages/cheatsheet/tsconfig.json index c01e663801..91f4c2cbf2 100644 --- a/packages/cheatsheet/tsconfig.json +++ b/packages/cheatsheet/tsconfig.json @@ -4,6 +4,7 @@ "target": "es2022", "lib": ["dom", "es2022"], "jsx": "react-jsx", + "jsxImportSource": "preact", "esModuleInterop": true, "skipLibCheck": true }, diff --git a/packages/cursorless-org-docs/src/docs/components/Header.tsx b/packages/cursorless-org-docs/src/docs/components/Header.tsx index f01ea95d4f..e6942839d1 100644 --- a/packages/cursorless-org-docs/src/docs/components/Header.tsx +++ b/packages/cursorless-org-docs/src/docs/components/Header.tsx @@ -26,10 +26,10 @@ export function H5(props: Props) { } function renderHeader( - level: number, + level: 2 | 3 | 4 | 5, { className, id, title, children }: Props, ): React.JSX.Element { - const Tag = `h${level}` as keyof React.JSX.IntrinsicElements; + const Tag = `h${level}` as "h2" | "h3" | "h4" | "h5"; const encodedId = uriEncodeHashId(id ?? children); return (