Skip to content

Commit 9a5763b

Browse files
authored
fix: support React 19 in v8 (#2979)
1 parent 9a25636 commit 9a5763b

53 files changed

Lines changed: 458 additions & 123 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
/* eslint-env node */
2+
import { execFileSync } from 'node:child_process';
3+
import { copyFileSync, mkdirSync, mkdtempSync, writeFileSync } from 'node:fs';
4+
import { tmpdir } from 'node:os';
5+
import { join } from 'node:path';
6+
7+
const command = process.argv[2];
8+
9+
if (!command) {
10+
throw new Error('Expected a smoke test command.');
11+
}
12+
13+
const env = {
14+
...process.env,
15+
NPM_CONFIG_CACHE:
16+
process.env.NPM_CONFIG_CACHE || join(tmpdir(), 'rdp-v8-npm-cache')
17+
};
18+
19+
function run(binary, args, cwd) {
20+
execFileSync(binary, args, { cwd, env, stdio: 'inherit' });
21+
}
22+
23+
function packTarball() {
24+
const destination =
25+
process.env.RUNNER_TEMP || mkdtempSync(join(tmpdir(), 'rdp-pack-'));
26+
const filename = execFileSync(
27+
'npm',
28+
['pack', '--pack-destination', destination, '--silent'],
29+
{ encoding: 'utf8' }
30+
)
31+
.trim()
32+
.split('\n')
33+
.pop();
34+
35+
return join(destination, filename);
36+
}
37+
38+
function createConsumer(tarball) {
39+
const directory = mkdtempSync(join(tmpdir(), 'rdp-consumer-'));
40+
41+
copyFileSync(tarball, join(directory, 'react-day-picker.tgz'));
42+
writeFileSync(
43+
join(directory, 'package.json'),
44+
`${JSON.stringify({ private: true, type: 'commonjs' }, null, 2)}\n`
45+
);
46+
47+
return directory;
48+
}
49+
50+
function installConsumer(directory, dependencies) {
51+
run(
52+
'npm',
53+
['install', '--ignore-scripts', './react-day-picker.tgz', ...dependencies],
54+
directory
55+
);
56+
}
57+
58+
function writeTypescriptConfig(directory) {
59+
writeFileSync(
60+
join(directory, 'tsconfig.json'),
61+
`${JSON.stringify(
62+
{
63+
compilerOptions: {
64+
strict: true,
65+
jsx: 'react-jsx',
66+
module: 'NodeNext',
67+
moduleResolution: 'NodeNext',
68+
target: 'ES2022',
69+
skipLibCheck: false,
70+
noEmit: true
71+
},
72+
include: ['src']
73+
},
74+
null,
75+
2
76+
)}\n`
77+
);
78+
}
79+
80+
function writeTypescriptFixture(directory, options) {
81+
mkdirSync(join(directory, 'src'));
82+
83+
const multiplePicker = options.includeMultiple
84+
? `
85+
export function MultiplePicker() {
86+
const [selected, setSelected] = useState<Date[]>();
87+
return <DayPicker mode="multiple" selected={selected} onSelect={setSelected} />;
88+
}
89+
`
90+
: '';
91+
92+
const customDayContentPicker = options.includeCustomDayContent
93+
? `
94+
export function CustomDayContentPicker() {
95+
return (
96+
<DayPicker
97+
components={{
98+
DayContent(props) {
99+
return <span>{props.date.getDate()}</span>;
100+
}
101+
}}
102+
/>
103+
);
104+
}
105+
`
106+
: '';
107+
108+
writeFileSync(
109+
join(directory, 'src/index.tsx'),
110+
`import { createRef, useState } from "react";
111+
import { DayPicker, type ButtonProps, type DateRange } from "react-day-picker";
112+
import "react-day-picker/dist/style.css";
113+
114+
const buttonRef = createRef<HTMLButtonElement>();
115+
const buttonProps: ButtonProps = { ref: buttonRef, type: "button" };
116+
void buttonProps;
117+
118+
export function SinglePicker() {
119+
const [selected, setSelected] = useState<Date>();
120+
return <DayPicker mode="single" selected={selected} onSelect={setSelected} />;
121+
}
122+
${multiplePicker}
123+
export function RangePicker() {
124+
const [selected, setSelected] = useState<DateRange>();
125+
return <DayPicker mode="range" selected={selected} onSelect={setSelected} />;
126+
}
127+
${customDayContentPicker}`
128+
);
129+
}
130+
131+
function runTypescriptSmoke(tarball, options) {
132+
const directory = createConsumer(tarball);
133+
134+
installConsumer(directory, [
135+
`react@${options.react}`,
136+
`react-dom@${options.react}`,
137+
`date-fns@${options.dateFns}`,
138+
'typescript@^5',
139+
`@types/react@${options.reactTypes}`,
140+
`@types/react-dom@${options.reactDomTypes}`
141+
]);
142+
143+
writeTypescriptConfig(directory);
144+
writeTypescriptFixture(directory, options);
145+
run('npx', ['tsc', '--noEmit'], directory);
146+
}
147+
148+
function writeRuntimeFixture(directory) {
149+
writeFileSync(
150+
join(directory, 'smoke.mjs'),
151+
`import { createRequire } from "node:module";
152+
153+
const require = createRequire(import.meta.url);
154+
const { JSDOM } = require("jsdom");
155+
156+
const dom = new JSDOM('<div id="root"></div>');
157+
globalThis.window = dom.window;
158+
globalThis.document = dom.window.document;
159+
globalThis.HTMLElement = dom.window.HTMLElement;
160+
globalThis.Node = dom.window.Node;
161+
Object.defineProperty(globalThis, "navigator", {
162+
value: dom.window.navigator,
163+
configurable: true
164+
});
165+
166+
const React = require("react");
167+
const { flushSync } = require("react-dom");
168+
const { createRoot } = require("react-dom/client");
169+
const { DayPicker } = require("react-day-picker");
170+
171+
const container = document.getElementById("root");
172+
const root = createRoot(container);
173+
174+
flushSync(() => {
175+
root.render(React.createElement(DayPicker, { month: new Date(2024, 0, 1) }));
176+
});
177+
178+
if (!container.querySelector(".rdp")) {
179+
throw new Error("DayPicker did not render its root element.");
180+
}
181+
182+
root.unmount();
183+
`
184+
);
185+
}
186+
187+
function runRuntimeSmoke(tarball, options) {
188+
const directory = createConsumer(tarball);
189+
190+
installConsumer(directory, [
191+
`react@${options.react}`,
192+
`react-dom@${options.react}`,
193+
`date-fns@${options.dateFns}`,
194+
'jsdom@^24.1.3'
195+
]);
196+
197+
writeRuntimeFixture(directory);
198+
run('node', ['smoke.mjs'], directory);
199+
}
200+
201+
const tarball = packTarball();
202+
203+
switch (command) {
204+
case 'react19-runtime':
205+
runRuntimeSmoke(tarball, { react: '19', dateFns: '3' });
206+
runRuntimeSmoke(tarball, { react: '19', dateFns: '2' });
207+
break;
208+
209+
case 'react19-types':
210+
runTypescriptSmoke(tarball, {
211+
react: '19',
212+
reactTypes: '19',
213+
reactDomTypes: '19',
214+
dateFns: '3',
215+
includeMultiple: false,
216+
includeCustomDayContent: false
217+
});
218+
break;
219+
220+
case 'react18-types':
221+
for (const dateFns of ['2', '3']) {
222+
runTypescriptSmoke(tarball, {
223+
react: '18',
224+
reactTypes: '18',
225+
reactDomTypes: '18',
226+
dateFns,
227+
includeMultiple: true,
228+
includeCustomDayContent: true
229+
});
230+
}
231+
break;
232+
233+
default:
234+
throw new Error(`Unknown smoke test command: ${command}`);
235+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/* eslint-env node */
2+
import { execFileSync } from 'node:child_process';
3+
4+
const minimum = [11, 5, 1];
5+
const version = execFileSync('npm', ['--version'], {
6+
encoding: 'utf8'
7+
}).trim();
8+
const current = version.split('.').map(Number);
9+
10+
for (let index = 0; index < minimum.length; index += 1) {
11+
if (current[index] > minimum[index]) {
12+
process.exit(0);
13+
}
14+
15+
if (current[index] < minimum[index]) {
16+
throw new Error(`npm ${version} does not satisfy >=11.5.1`);
17+
}
18+
}

.github/workflows/package.yml

Lines changed: 2 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,17 @@
11
name: react-day-picker
22

33
on:
4-
release:
5-
types: [published]
64
pull_request:
75
branches:
86
- main
7+
- maintenance/v8
98
push:
109
branches:
1110
- main
11+
- maintenance/v8
1212
workflow_dispatch:
13-
inputs:
14-
publish:
15-
description: Publish on npm
16-
required: false
17-
default: false
18-
type: boolean
1913

2014
jobs:
21-
# print-env:
22-
# runs-on: ubuntu-latest
23-
# steps:
24-
# - run: |
25-
# echo "publish=${{ github.event.inputs.publish || false }}"
26-
2715
typecheck:
2816
runs-on: ubuntu-latest
2917
steps:
@@ -84,28 +72,3 @@ jobs:
8472
with:
8573
name: rdp-dist
8674
path: dist
87-
88-
publish-on-npm:
89-
runs-on: ubuntu-latest
90-
needs: [build, test]
91-
if: ${{ github.event_name == 'release' || github.event.inputs.publish }}
92-
permissions:
93-
id-token: write
94-
steps:
95-
- uses: actions/checkout@v4
96-
- uses: pnpm/action-setup@v3
97-
with:
98-
version: 8.6.2
99-
- uses: actions/setup-node@v4
100-
with:
101-
node-version: 18.16
102-
registry-url: https://registry.npmjs.org/
103-
always-auth: false
104-
- uses: actions/download-artifact@v4
105-
with:
106-
name: rdp-dist
107-
path: dist
108-
- run: echo "//<npm-registry>:8080/:_authToken=$NODE_AUTH_TOKEN" > ~/.npmrc
109-
- run: npm publish --provenance
110-
env:
111-
NODE_AUTH_TOKEN: ${{ secrets.npm_token }}

.github/workflows/release.yml

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
name: react-day-picker v8 release
2+
3+
on:
4+
release:
5+
types: [published]
6+
7+
permissions:
8+
contents: read
9+
10+
jobs:
11+
validate:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: actions/checkout@v4
15+
- uses: pnpm/action-setup@v3
16+
with:
17+
version: 8.6.2
18+
- uses: actions/setup-node@v4
19+
with:
20+
node-version: 24
21+
cache: pnpm
22+
- run: pnpm install --frozen-lockfile
23+
- run: pnpm typecheck
24+
- run: pnpm test
25+
- run: pnpm build
26+
- name: Verify npm trusted publishing support
27+
run: node .github/scripts/verify-npm-version.mjs
28+
- name: Smoke test React 19 consumer installs
29+
run: node .github/scripts/smoke-v8-release.mjs react19-runtime
30+
- name: Smoke test React 19 TypeScript declarations
31+
run: node .github/scripts/smoke-v8-release.mjs react19-types
32+
- name: Smoke test React 18 TypeScript declarations
33+
run: node .github/scripts/smoke-v8-release.mjs react18-types
34+
35+
publish:
36+
needs: validate
37+
runs-on: ubuntu-latest
38+
permissions:
39+
contents: read
40+
id-token: write
41+
steps:
42+
- uses: actions/checkout@v4
43+
- uses: pnpm/action-setup@v3
44+
with:
45+
version: 8.6.2
46+
- uses: actions/setup-node@v4
47+
with:
48+
node-version: 24
49+
registry-url: https://registry.npmjs.org/
50+
- name: Block prerelease publishing
51+
env:
52+
RELEASE_PRERELEASE: ${{ github.event.release.prerelease }}
53+
run: |
54+
if [ "$RELEASE_PRERELEASE" = "true" ]; then
55+
echo "Prerelease GitHub Releases cannot publish stable v8 packages." >&2
56+
exit 1
57+
fi
58+
- name: Verify release tag matches package version
59+
env:
60+
RELEASE_TAG: ${{ github.event.release.tag_name }}
61+
run: |
62+
expected_tag="v$(node -p "require('./package.json').version")"
63+
if [ "$RELEASE_TAG" != "$expected_tag" ]; then
64+
echo "Release tag $RELEASE_TAG does not match package version $expected_tag." >&2
65+
exit 1
66+
fi
67+
- run: pnpm install --frozen-lockfile
68+
- run: pnpm build
69+
- name: Verify npm trusted publishing support
70+
run: node .github/scripts/verify-npm-version.mjs
71+
- run: npm publish --tag legacy-v8

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
11
# Changelog
22

3+
## v8.10.2
4+
5+
_Release date: 2026-05-02_
6+
7+
- Expand React peer dependency support to include React 19. This release has no runtime or API changes.
8+
39
Full release notes at https://github.com/gpbl/react-day-picker/releases

0 commit comments

Comments
 (0)