Skip to content
Merged
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
20 changes: 20 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,26 @@ jobs:
run: yarn build
working-directory: ./packages/web

check-react-consumers:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 20
cache: yarn
cache-dependency-path: yarn.lock

- name: Install dependencies
run: yarn install --frozen-lockfile

- name: Build all packages
run: yarn build

- name: Install into React 18 and React 19 consumers
run: yarn check:react-consumers

build-playground:
runs-on: ubuntu-latest
steps:
Expand Down
134 changes: 134 additions & 0 deletions bin/check-react-consumers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { execFileSync } from "node:child_process";
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
import { gzipSync } from "node:zlib";
import { tmpdir } from "node:os";
import { join } from "node:path";

// Packs the React packages and installs them, with plain `npm install`,
// into small consumers on each supported React and shiki pair. Each consumer
// type-checks with skipLibCheck off, so a broken .d.ts fails.
const packed = [
"format",
"pointers",
"evm",
"programs-react",
"pointers-react",
];
const pairs = [
{ react: "18", types: "18", shiki: "2.5" },
{ react: "19", types: "19", shiki: "3" },
// the ranges allow shiki next to grammars and themes of the other major
{ react: "19", types: "19", shiki: "2.5", sub: "3" },
{ react: "18", types: "18", shiki: "3", sub: "2.5" },
];

const maxBundleKb = 400;

const run = (cmd: string, args: string[], cwd: string) =>
execFileSync(cmd, args, { cwd, stdio: "inherit" });

const root = mkdtempSync(join(tmpdir(), "react-consumers-"));
const tarballs = packed.map((name) => {
const out = execFileSync("npm", ["pack", "--pack-destination", root], {
cwd: join("packages", name),
encoding: "utf8",
});
return join(root, out.trim().split("\n").pop()!);
});

for (const { react, types, shiki, sub = shiki } of pairs) {
const dir = join(root, `react-${react}-shiki-${shiki}-sub-${sub}`);
mkdirSync(dir);
writeFileSync(
join(dir, "package.json"),
JSON.stringify({ name: "consumer", private: true, type: "module" }),
);
writeFileSync(
join(dir, "tsconfig.json"),
JSON.stringify({
compilerOptions: {
module: "nodenext",
moduleResolution: "nodenext",
target: "esnext",
jsx: "react-jsx",
strict: true,
types: ["node"],
skipLibCheck: false,
noEmit: true,
},
include: ["index.tsx"],
}),
);
writeFileSync(
join(dir, "index.tsx"),
`import { ShikiCodeBlock } from "@ethdebug/programs-react";
import * as Pointers from "@ethdebug/pointers-react";
export const a = <ShikiCodeBlock code="" language="solidity" />;
export const b = Pointers;
`,
);
run(
"npm",
[
"install",
`react@${react}`,
`react-dom@${react}`,
`@types/react@${types}`,
`@types/react-dom@${types}`,
`shiki@${shiki}`,
`@shikijs/langs@${sub}`,
`@shikijs/themes@${sub}`,
"typescript",
"esbuild",
"@types/node@20",
...tarballs,
],
dir,
);
run("node", [join(dir, "node_modules/typescript/bin/tsc")], dir);
// Without code splitting, every dynamic import is inlined, so this is
// the worst-case size of the highlighter loader. Importing all grammars
// and themes (`shiki/langs`, `shiki/themes`) once made it about 1.6 MB.
writeFileSync(
join(dir, "entry.js"),
'import { ShikiCodeBlock } from "@ethdebug/programs-react";\nconsole.log(ShikiCodeBlock);\n',
);
const bundle = execFileSync(
join(dir, "node_modules/.bin/esbuild"),
[
"entry.js",
"--bundle",
"--minify",
"--format=esm",
"--platform=browser",
"--external:react",
"--external:react-dom",
"--external:react/jsx-runtime",
"--external:node:*",
],
{ cwd: dir, maxBuffer: 1 << 28 },
);
const kb = Math.round(gzipSync(bundle).length / 1024);
if (kb > maxBundleKb) {
throw new Error(`bundle is ${kb} KB gzipped; limit is ${maxBundleKb}`);
}
// highlight Solidity the way the package's highlighter does
writeFileSync(
join(dir, "highlight.mjs"),
`import * as Shiki from "shiki/core";
import { createOnigurumaEngine } from "shiki/engine/oniguruma";
const h = await Shiki.createHighlighterCore({
themes: [import("@shikijs/themes/github-light")],
langs: [import("@shikijs/langs/solidity")],
engine: createOnigurumaEngine(import("shiki/wasm")),
});
const html = h.codeToHtml("contract A { uint x; }", {
lang: "solidity",
theme: "github-light",
});
if (!/<span style="color:#/.test(html)) throw new Error("no styled spans");
`,
);
run("node", ["highlight.mjs"], dir);
console.log(`ok: React ${react} + shiki ${shiki}/${sub}, bundle ${kb} KB gz`);

Check warning on line 133 in bin/check-react-consumers.ts

View workflow job for this annotation

GitHub Actions / lint-and-format

Unexpected console statement
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
],
"scripts": {
"build": "yarn lerna run build --ignore @ethdebug/format-web --ignore @ethdebug/bug-playground",
"check:react-consumers": "tsx ./bin/check-react-consumers.ts",
"bundle": "tsx ./bin/bundle-schema.ts",
"typecheck": "yarn lerna run typecheck --ignore @ethdebug/format-web --ignore @ethdebug/bug-playground",
"test": "vitest run",
Expand Down
7 changes: 7 additions & 0 deletions packages/bugc-react/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ specification itself are tracked in the root

## Unreleased

### Changed

- The components' return types now read `React.JSX.Element` instead of the
global `JSX.Element`, which `@types/react` 19 removes ([#374]). The
peer ranges stay on React 18.

## 0.1.0-preview.2 — 2026-10-07

### Added
Expand Down Expand Up @@ -50,3 +56,4 @@ First publication.
[#359]: https://github.com/ethdebug/format/pull/359
[#363]: https://github.com/ethdebug/format/pull/363
[#365]: https://github.com/ethdebug/format/pull/365
[#374]: https://github.com/ethdebug/format/pull/374
2 changes: 1 addition & 1 deletion packages/bugc-react/src/components/AstView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export interface AstViewProps {
* <AstView ast={compileResult.ast} />
* ```
*/
export function AstView({ ast }: AstViewProps): JSX.Element {
export function AstView({ ast }: AstViewProps): React.JSX.Element {
// Format AST as JSON, excluding parent references to avoid circular structure
const astJson = JSON.stringify(
ast,
Expand Down
4 changes: 2 additions & 2 deletions packages/bugc-react/src/components/BytecodeView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ function InstructionsView({
instructions,
onOpcodeHover,
onDeclarationClick,
}: InstructionsViewProps): JSX.Element {
}: InstructionsViewProps): React.JSX.Element {
const {
tooltip,
setTooltip,
Expand Down Expand Up @@ -225,7 +225,7 @@ export function BytecodeView({
bytecode,
onOpcodeHover,
onDeclarationClick,
}: BytecodeViewProps): JSX.Element {
}: BytecodeViewProps): React.JSX.Element {
const runtimeHex = Array.from(bytecode.runtime)
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
Expand Down
6 changes: 3 additions & 3 deletions packages/bugc-react/src/components/CfgView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ async function loadDependencies(): Promise<boolean> {
function BlockNodeComponent(props: {
data: BlockNodeData;
selected: boolean;
}): JSX.Element {
}): React.JSX.Element {
const { data, selected } = props;

if (!rfModule) {
Expand Down Expand Up @@ -84,7 +84,7 @@ function BlockNodeComponent(props: {
);
}

function CfgViewContent({ ir }: CfgViewProps): JSX.Element {
function CfgViewContent({ ir }: CfgViewProps): React.JSX.Element {
const [selectedNode, setSelectedNode] = useState<string | null>(null);

if (!rfModule || !dagreModule) {
Expand Down Expand Up @@ -503,7 +503,7 @@ function CfgViewContent({ ir }: CfgViewProps): JSX.Element {
* <CfgView ir={compileResult.ir} />
* ```
*/
export function CfgView(props: CfgViewProps): JSX.Element {
export function CfgView(props: CfgViewProps): React.JSX.Element {
const [loaded, setLoaded] = useState(dependenciesLoaded);
const [error, setError] = useState<string | null>(null);

Expand Down
2 changes: 1 addition & 1 deletion packages/bugc-react/src/components/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export function Editor({
highlightedRanges = [],
theme = "auto",
height = "100%",
}: EditorProps): JSX.Element {
}: EditorProps): React.JSX.Element {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const editorRef = useRef<any>(null);
const decorationsRef = useRef<string[]>([]);
Expand Down
6 changes: 3 additions & 3 deletions packages/bugc-react/src/components/EthdebugTooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export function EthdebugTooltip({
tooltip,
onUpdate,
onClose,
}: EthdebugTooltipProps): JSX.Element | null {
}: EthdebugTooltipProps): React.JSX.Element | null {
const tooltipRef = useRef<HTMLDivElement>(null);

useEffect(() => {
Expand Down Expand Up @@ -128,7 +128,7 @@ export function DebugInfoIcon({
onMouseEnter,
onMouseLeave,
onClick,
}: DebugInfoIconProps): JSX.Element {
}: DebugInfoIconProps): React.JSX.Element {
return (
<span
className="debug-info-icon"
Expand All @@ -145,6 +145,6 @@ export function DebugInfoIcon({
/**
* Spacer element to maintain alignment when no debug icon is shown.
*/
export function DebugInfoSpacer(): JSX.Element {
export function DebugInfoSpacer(): React.JSX.Element {
return <span className="debug-info-spacer" />;
}
14 changes: 7 additions & 7 deletions packages/bugc-react/src/components/IrView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ function HoverablePart({
onLeave: () => void;
onDebugIconHover?: (e: React.MouseEvent<HTMLSpanElement>) => void;
showDebugIcon?: boolean;
}): JSX.Element {
}): React.JSX.Element {
return (
<span
className={`hoverable-part ${part.className || ""} ${part.ranges.length > 0 ? "has-debug" : ""}`}
Expand Down Expand Up @@ -112,7 +112,7 @@ function InstructionRenderer({
showTooltip: (e: React.MouseEvent<HTMLElement>, content: string) => void;
pinTooltip: (e: React.MouseEvent<HTMLElement>, content: string) => void;
hideTooltip: () => void;
}): JSX.Element {
}): React.JSX.Element {
const debugInfo = extractInstructionDebug(instruction);
const operationRanges = debugInfo.operation?.context
? extractAllSourceRanges({ operation: debugInfo.operation, operands: [] })
Expand Down Expand Up @@ -416,7 +416,7 @@ function TerminatorRenderer({
showTooltip: (e: React.MouseEvent<HTMLElement>, content: string) => void;
pinTooltip: (e: React.MouseEvent<HTMLElement>, content: string) => void;
hideTooltip: () => void;
}): JSX.Element {
}): React.JSX.Element {
const debugInfo = extractTerminatorDebug(terminator);
const operationRanges = debugInfo.operation?.context
? extractAllSourceRanges({ operation: debugInfo.operation, operands: [] })
Expand Down Expand Up @@ -529,7 +529,7 @@ function PhiRenderer({
showTooltip: (e: React.MouseEvent<HTMLElement>, content: string) => void;
pinTooltip: (e: React.MouseEvent<HTMLElement>, content: string) => void;
hideTooltip: () => void;
}): JSX.Element {
}): React.JSX.Element {
const debugInfo = extractPhiDebug(phi);
const operationRanges = debugInfo.operation?.context
? extractAllSourceRanges({ operation: debugInfo.operation, operands: [] })
Expand Down Expand Up @@ -621,7 +621,7 @@ function BlockRenderer({
showTooltip: (e: React.MouseEvent<HTMLElement>, content: string) => void;
pinTooltip: (e: React.MouseEvent<HTMLElement>, content: string) => void;
hideTooltip: () => void;
}): JSX.Element {
}): React.JSX.Element {
return (
<div className="ir-block">
<div className="block-header">
Expand Down Expand Up @@ -680,7 +680,7 @@ function FunctionRenderer({
showTooltip: (e: React.MouseEvent<HTMLElement>, content: string) => void;
pinTooltip: (e: React.MouseEvent<HTMLElement>, content: string) => void;
hideTooltip: () => void;
}): JSX.Element {
}): React.JSX.Element {
const sortedBlocks = useMemo(() => {
const result: [string, Ir.Block][] = [];
const visited = new Set<string>();
Expand Down Expand Up @@ -760,7 +760,7 @@ function FunctionRenderer({
* />
* ```
*/
export function IrView({ ir, onOpcodeHover }: IrViewProps): JSX.Element {
export function IrView({ ir, onOpcodeHover }: IrViewProps): React.JSX.Element {
const {
tooltip,
setTooltip,
Expand Down
8 changes: 8 additions & 0 deletions packages/pointers-react/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ root [`CHANGELOG.md`](../../CHANGELOG.md).

## Unreleased

### Changed

- The `react` and `react-dom` peer ranges now include React 19
(`^18.0.0 || ^19.0.0`) ([#374]). The components' return types now
read `React.JSX.Element`, which both React 18 and 19 types define;
the global `JSX` they used before is gone in `@types/react` 19.

## 0.1.0-preview.2 — 2026-10-07

### Breaking
Expand Down Expand Up @@ -46,3 +53,4 @@ First publication.
[#299]: https://github.com/ethdebug/format/pull/299
[#300]: https://github.com/ethdebug/format/pull/300
[#323]: https://github.com/ethdebug/format/pull/323
[#374]: https://github.com/ethdebug/format/pull/374
4 changes: 2 additions & 2 deletions packages/pointers-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@
"vitest": "^3.2.4"
},
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
},
"publishConfig": {
"access": "public"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export interface PointerResolverProviderProps extends UsePointerResolutionOption
export function PointerResolverProvider({
children,
...options
}: PointerResolverProviderProps): JSX.Element {
}: PointerResolverProviderProps): React.JSX.Element {
const state = usePointerResolution(options);

return (
Expand Down
2 changes: 1 addition & 1 deletion packages/pointers-react/src/components/RegionMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export function RegionMap({
regions,
selectedName,
onRegionClick,
}: RegionMapProps): JSX.Element {
}: RegionMapProps): React.JSX.Element {
const names = Object.keys(regions).sort();

if (names.length === 0) {
Expand Down
Loading
Loading