Skip to content

Commit be07d7b

Browse files
authored
Complete React 19 migration and add dual React 18/19 package compatibility validation (#77)
This PR moves the repo’s development workspace to React 19 while preserving published React 18 support for @trrack/vis-react. It upgrades the example apps and docs app to React 19, including moving the docs site onto a React-19-capable Next/Nextra stack. It also removes remaining React-18-only blockers in the example apps, including replacing react-hyper-tree with a local history tree component and removing Mantine from the dummy testing app in favor of local UI and sizing logic. For the published packages, @trrack/vis-react keeps its peer range at react / react-dom >=18 <20, and package compatibility is now validated against both React 18 and React 19. This PR adds a dedicated CI workflow plus package-only scripts to typecheck, build, and test @trrack/core and @trrack/vis-react under both React majors. It also updates the package/docs messaging to state the tested support policy clearly.
2 parents 3e98696 + a0c2a4d commit be07d7b

157 files changed

Lines changed: 6234 additions & 4196 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
name: Package Compatibility
2+
3+
on:
4+
push:
5+
branches-ignore:
6+
- main
7+
- next
8+
- alpha
9+
- beta
10+
- '*.x*'
11+
pull_request:
12+
branches:
13+
- main
14+
- next
15+
- alpha
16+
- beta
17+
- '*.x*'
18+
19+
jobs:
20+
package-compat:
21+
name: Packages -> React ${{ matrix.react_label }}
22+
runs-on: ubuntu-latest
23+
strategy:
24+
fail-fast: false
25+
matrix:
26+
include:
27+
- react_label: 18
28+
react: 18.3.1
29+
react_dom: 18.3.1
30+
types_react: 18.3.12
31+
types_react_dom: 18.3.1
32+
- react_label: 19
33+
react: 19.2.4
34+
react_dom: 19.2.4
35+
types_react: 19.2.14
36+
types_react_dom: 19.2.3
37+
38+
steps:
39+
- name: Checkout
40+
uses: actions/checkout@v6
41+
with:
42+
fetch-depth: 0
43+
44+
- name: Setup LTS Node
45+
uses: actions/setup-node@v6
46+
with:
47+
node-version: 'lts/*'
48+
cache: 'yarn'
49+
50+
- name: Pin React toolchain
51+
run: >
52+
node scripts/set-react-toolchain.mjs
53+
${{ matrix.react }}
54+
${{ matrix.react_dom }}
55+
${{ matrix.types_react }}
56+
${{ matrix.types_react_dom }}
57+
58+
- name: Install dependencies
59+
run: yarn install
60+
61+
- name: Typecheck published packages
62+
run: yarn typecheck:packages
63+
64+
- name: Build published packages
65+
run: yarn build
66+
67+
- name: Test published packages
68+
run: yarn test:packages

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ Thumbs.db
4040

4141
# Next.js
4242
.next
43+
apps/docs/out
4344
*.tsbuildinfo
4445

4546
# Nx
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
import { generateStaticParamsFor, importPage } from 'nextra/pages';
2+
import { SkipNavContent } from 'nextra/components';
3+
import { getPageMap } from 'nextra/page-map';
4+
import { Layout } from 'nextra-theme-docs';
5+
import { useMDXComponents as getMDXComponents } from '../../mdx-components';
6+
import {
7+
banner,
8+
docsRepositoryBase,
9+
footer,
10+
navbar,
11+
search,
12+
} from '../../theme.config';
13+
14+
type PageParams = {
15+
mdxPath?: string[];
16+
};
17+
18+
type ContentSection = 'docs' | 'api-reference' | 'top-level';
19+
type SidebarPageMapItem = {
20+
name: string;
21+
route: string;
22+
title?: string;
23+
children?: SidebarPageMapItem[];
24+
frontMatter?: unknown;
25+
theme?: {
26+
collapsed?: boolean;
27+
};
28+
};
29+
type SidebarPageMap = SidebarPageMapItem[];
30+
31+
export const generateStaticParams = generateStaticParamsFor('mdxPath');
32+
33+
export async function generateMetadata(props: {
34+
params: Promise<PageParams>;
35+
}) {
36+
const params = await props.params;
37+
const { metadata } = await importPage(params.mdxPath);
38+
return metadata;
39+
}
40+
41+
const Wrapper = getMDXComponents().wrapper;
42+
43+
function getContentSection(mdxPath: string[] = []): ContentSection {
44+
const [section] = mdxPath;
45+
46+
if (section === 'docs') {
47+
return 'docs';
48+
}
49+
50+
if (section === 'api-reference') {
51+
return 'api-reference';
52+
}
53+
54+
return 'top-level';
55+
}
56+
57+
function getPageMapRoute(section: ContentSection) {
58+
if (section === 'docs') {
59+
return '/docs';
60+
}
61+
62+
if (section === 'api-reference') {
63+
return '/api-reference';
64+
}
65+
66+
return null;
67+
}
68+
69+
function getArticleClass(mdxPath: string[] = []) {
70+
const [section] = mdxPath;
71+
const isArticlePage = section === 'about' || section === 'showcase';
72+
73+
return [
74+
'x:w-full x:min-w-0 x:break-words',
75+
'x:min-h-[calc(100vh-var(--nextra-navbar-height))]',
76+
'x:text-slate-700 x:dark:text-slate-200 x:pb-8 x:px-4 x:pt-4 x:md:px-12',
77+
isArticlePage ? 'nextra-body-typesetting-article' : '',
78+
]
79+
.filter(Boolean)
80+
.join(' ');
81+
}
82+
83+
function findPageMapItem(
84+
items: SidebarPageMapItem[],
85+
name: string,
86+
options?: { children?: boolean }
87+
) {
88+
const item = items.find((candidate) => candidate.name === name);
89+
90+
if (!item) {
91+
throw new Error(`Missing page map item "${name}"`);
92+
}
93+
94+
if (options?.children && !('children' in item)) {
95+
throw new Error(`Expected "${name}" to have children`);
96+
}
97+
98+
return item;
99+
}
100+
101+
function withTitle(item: SidebarPageMapItem, title: string) {
102+
return {
103+
...item,
104+
title,
105+
};
106+
}
107+
108+
async function getDocsSidebarPageMap(): Promise<SidebarPageMap> {
109+
const docsPageMap = (await getPageMap('/docs')) as SidebarPageMap;
110+
const tutorial = findPageMapItem(docsPageMap, 'tutorial', { children: true });
111+
const visualization = findPageMapItem(docsPageMap, 'visualization');
112+
113+
const tutorialChildren = tutorial.children!;
114+
const examples = findPageMapItem(tutorialChildren, 'basic', { children: true });
115+
const advanced = findPageMapItem(tutorialChildren, 'advanced', {
116+
children: true,
117+
});
118+
const gettingStarted = findPageMapItem(tutorialChildren, 'getting-started');
119+
const usage = findPageMapItem(tutorialChildren, 'usage');
120+
121+
const exampleChildren = [
122+
findPageMapItem(examples.children!, 'state'),
123+
findPageMapItem(examples.children!, 'action'),
124+
findPageMapItem(examples.children!, 'hybrid'),
125+
];
126+
127+
return [
128+
{
129+
...withTitle(tutorial, 'Tutorial'),
130+
children: [
131+
withTitle(gettingStarted, 'Getting Started'),
132+
withTitle(usage, 'Usage'),
133+
{
134+
...withTitle(examples, 'Examples'),
135+
theme: {
136+
...examples.theme,
137+
collapsed: false,
138+
},
139+
children: exampleChildren,
140+
},
141+
{
142+
...withTitle(advanced, 'Advanced'),
143+
theme: {
144+
...advanced.theme,
145+
collapsed: false,
146+
},
147+
},
148+
],
149+
},
150+
withTitle(visualization, 'Visualization'),
151+
];
152+
}
153+
154+
export default async function Page(props: {
155+
params: Promise<PageParams>;
156+
}) {
157+
const params = await props.params;
158+
const section = getContentSection(params.mdxPath);
159+
const pageMapRoute = getPageMapRoute(section);
160+
const { default: MDXContent, toc, metadata, sourceCode } = await importPage(
161+
params.mdxPath
162+
);
163+
164+
if (section === 'top-level') {
165+
return (
166+
<Layout
167+
banner={banner}
168+
navbar={navbar}
169+
search={search}
170+
pageMap={await getPageMap('/')}
171+
docsRepositoryBase={docsRepositoryBase}
172+
footer={footer}
173+
>
174+
<div className="x:mx-auto x:flex x:max-w-(--nextra-content-width)">
175+
<article className={getArticleClass(params.mdxPath)}>
176+
<SkipNavContent />
177+
<main data-pagefind-body>
178+
<MDXContent {...props} params={params} />
179+
</main>
180+
</article>
181+
</div>
182+
</Layout>
183+
);
184+
}
185+
186+
if (section === 'docs') {
187+
return (
188+
<Layout
189+
banner={banner}
190+
navbar={navbar}
191+
search={search}
192+
pageMap={await getDocsSidebarPageMap()}
193+
docsRepositoryBase={docsRepositoryBase}
194+
footer={footer}
195+
>
196+
<Wrapper toc={toc} metadata={metadata} sourceCode={sourceCode}>
197+
<MDXContent {...props} params={params} />
198+
</Wrapper>
199+
</Layout>
200+
);
201+
}
202+
203+
return (
204+
<Layout
205+
banner={banner}
206+
navbar={navbar}
207+
search={search}
208+
pageMap={await getPageMap(pageMapRoute)}
209+
docsRepositoryBase={docsRepositoryBase}
210+
footer={footer}
211+
>
212+
<Wrapper toc={toc} metadata={metadata} sourceCode={sourceCode}>
213+
<MDXContent {...props} params={params} />
214+
</Wrapper>
215+
</Layout>
216+
);
217+
}

apps/docs/app/layout.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { Metadata } from 'next';
2+
import type { ReactNode } from 'react';
3+
import '../styles/globals.css';
4+
import { head, siteMetadata } from '../theme.config';
5+
6+
export const metadata: Metadata = siteMetadata;
7+
8+
export default async function RootLayout({
9+
children,
10+
}: {
11+
children: ReactNode;
12+
}) {
13+
return (
14+
<html lang="en" dir="ltr" suppressHydrationWarning>
15+
{head}
16+
<body>{children}</body>
17+
</html>
18+
);
19+
}

apps/docs/components/HeaderNav.tsx

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
'use client';
2+
3+
import clsx from 'clsx';
4+
import { Anchor } from 'nextra/components';
5+
import { useFSRoute } from 'nextra/hooks';
6+
7+
const NAV_ITEMS = [
8+
{
9+
label: 'Documentation',
10+
href: '/docs/tutorial/getting-started',
11+
activePrefixes: ['/docs'],
12+
},
13+
{
14+
label: 'Showcase',
15+
href: '/showcase',
16+
activePrefixes: ['/showcase'],
17+
},
18+
{
19+
label: 'API Reference',
20+
href: '/api-reference',
21+
activePrefixes: ['/api-reference'],
22+
},
23+
{
24+
label: 'About',
25+
href: '/about',
26+
activePrefixes: ['/about'],
27+
},
28+
] as const;
29+
30+
function isCurrentRoute(route: string, activePrefixes: readonly string[]) {
31+
return activePrefixes.some(
32+
(prefix) => route === prefix || route.startsWith(`${prefix}/`),
33+
);
34+
}
35+
36+
export function HeaderNav() {
37+
const route = useFSRoute().split('#', 1)[0];
38+
39+
return (
40+
<div className="x:flex x:gap-4 x:overflow-x-auto nextra-scrollbar x:py-1.5">
41+
{NAV_ITEMS.map(({ label, href, activePrefixes }) => {
42+
const isActive = isCurrentRoute(route, activePrefixes);
43+
44+
return (
45+
<Anchor
46+
key={href}
47+
href={href}
48+
aria-current={isActive ? 'page' : undefined}
49+
className={clsx(
50+
'x:text-sm x:whitespace-nowrap x:ring-inset x:transition-colors',
51+
'x:text-gray-600 x:hover:text-black',
52+
'x:dark:text-gray-400 x:dark:hover:text-gray-200',
53+
'x:contrast-more:text-gray-700 x:contrast-more:dark:text-gray-100',
54+
isActive && 'x:font-medium x:subpixel-antialiased x:text-current',
55+
)}
56+
>
57+
{label}
58+
</Anchor>
59+
);
60+
})}
61+
</div>
62+
);
63+
}

0 commit comments

Comments
 (0)