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
235 changes: 235 additions & 0 deletions .github/scripts/smoke-v8-release.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
/* eslint-env node */
import { execFileSync } from 'node:child_process';
import { copyFileSync, mkdirSync, mkdtempSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';

const command = process.argv[2];

if (!command) {
throw new Error('Expected a smoke test command.');
}

const env = {
...process.env,
NPM_CONFIG_CACHE:
process.env.NPM_CONFIG_CACHE || join(tmpdir(), 'rdp-v8-npm-cache')
};

function run(binary, args, cwd) {
execFileSync(binary, args, { cwd, env, stdio: 'inherit' });
}

function packTarball() {
const destination =
process.env.RUNNER_TEMP || mkdtempSync(join(tmpdir(), 'rdp-pack-'));
const filename = execFileSync(
'npm',
['pack', '--pack-destination', destination, '--silent'],
{ encoding: 'utf8' }
)
.trim()
.split('\n')
.pop();

return join(destination, filename);
}

function createConsumer(tarball) {
const directory = mkdtempSync(join(tmpdir(), 'rdp-consumer-'));

copyFileSync(tarball, join(directory, 'react-day-picker.tgz'));
writeFileSync(
join(directory, 'package.json'),
`${JSON.stringify({ private: true, type: 'commonjs' }, null, 2)}\n`
);

return directory;
}

function installConsumer(directory, dependencies) {
run(
'npm',
['install', '--ignore-scripts', './react-day-picker.tgz', ...dependencies],
directory
);
}

function writeTypescriptConfig(directory) {
writeFileSync(
join(directory, 'tsconfig.json'),
`${JSON.stringify(
{
compilerOptions: {
strict: true,
jsx: 'react-jsx',
module: 'NodeNext',
moduleResolution: 'NodeNext',
target: 'ES2022',
skipLibCheck: false,
noEmit: true
},
include: ['src']
},
null,
2
)}\n`
);
}

function writeTypescriptFixture(directory, options) {
mkdirSync(join(directory, 'src'));

const multiplePicker = options.includeMultiple
? `
export function MultiplePicker() {
const [selected, setSelected] = useState<Date[]>();
return <DayPicker mode="multiple" selected={selected} onSelect={setSelected} />;
}
`
: '';

const customDayContentPicker = options.includeCustomDayContent
? `
export function CustomDayContentPicker() {
return (
<DayPicker
components={{
DayContent(props) {
return <span>{props.date.getDate()}</span>;
}
}}
/>
);
}
`
: '';

writeFileSync(
join(directory, 'src/index.tsx'),
`import { createRef, useState } from "react";
import { DayPicker, type ButtonProps, type DateRange } from "react-day-picker";
import "react-day-picker/dist/style.css";

const buttonRef = createRef<HTMLButtonElement>();
const buttonProps: ButtonProps = { ref: buttonRef, type: "button" };
void buttonProps;

export function SinglePicker() {
const [selected, setSelected] = useState<Date>();
return <DayPicker mode="single" selected={selected} onSelect={setSelected} />;
}
${multiplePicker}
export function RangePicker() {
const [selected, setSelected] = useState<DateRange>();
return <DayPicker mode="range" selected={selected} onSelect={setSelected} />;
}
${customDayContentPicker}`
);
}

function runTypescriptSmoke(tarball, options) {
const directory = createConsumer(tarball);

installConsumer(directory, [
`react@${options.react}`,
`react-dom@${options.react}`,
`date-fns@${options.dateFns}`,
'typescript@^5',
`@types/react@${options.reactTypes}`,
`@types/react-dom@${options.reactDomTypes}`
]);

writeTypescriptConfig(directory);
writeTypescriptFixture(directory, options);
run('npx', ['tsc', '--noEmit'], directory);
}

function writeRuntimeFixture(directory) {
writeFileSync(
join(directory, 'smoke.mjs'),
`import { createRequire } from "node:module";

const require = createRequire(import.meta.url);
const { JSDOM } = require("jsdom");

const dom = new JSDOM('<div id="root"></div>');
globalThis.window = dom.window;
globalThis.document = dom.window.document;
globalThis.HTMLElement = dom.window.HTMLElement;
globalThis.Node = dom.window.Node;
Object.defineProperty(globalThis, "navigator", {
value: dom.window.navigator,
configurable: true
});

const React = require("react");
const { flushSync } = require("react-dom");
const { createRoot } = require("react-dom/client");
const { DayPicker } = require("react-day-picker");

const container = document.getElementById("root");
const root = createRoot(container);

flushSync(() => {
root.render(React.createElement(DayPicker, { month: new Date(2024, 0, 1) }));
});

if (!container.querySelector(".rdp")) {
throw new Error("DayPicker did not render its root element.");
}

root.unmount();
`
);
}

function runRuntimeSmoke(tarball, options) {
const directory = createConsumer(tarball);

installConsumer(directory, [
`react@${options.react}`,
`react-dom@${options.react}`,
`date-fns@${options.dateFns}`,
'jsdom@^24.1.3'
]);

writeRuntimeFixture(directory);
run('node', ['smoke.mjs'], directory);
}

const tarball = packTarball();

switch (command) {
case 'react19-runtime':
runRuntimeSmoke(tarball, { react: '19', dateFns: '3' });
runRuntimeSmoke(tarball, { react: '19', dateFns: '2' });
break;

case 'react19-types':
runTypescriptSmoke(tarball, {
react: '19',
reactTypes: '19',
reactDomTypes: '19',
dateFns: '3',
includeMultiple: false,
includeCustomDayContent: false
});
break;

case 'react18-types':
for (const dateFns of ['2', '3']) {
runTypescriptSmoke(tarball, {
react: '18',
reactTypes: '18',
reactDomTypes: '18',
dateFns,
includeMultiple: true,
includeCustomDayContent: true
});
}
break;

default:
throw new Error(`Unknown smoke test command: ${command}`);
}
18 changes: 18 additions & 0 deletions .github/scripts/verify-npm-version.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/* eslint-env node */
import { execFileSync } from 'node:child_process';

const minimum = [11, 5, 1];
const version = execFileSync('npm', ['--version'], {
encoding: 'utf8'
}).trim();
const current = version.split('.').map(Number);

for (let index = 0; index < minimum.length; index += 1) {
if (current[index] > minimum[index]) {
process.exit(0);
}

if (current[index] < minimum[index]) {
throw new Error(`npm ${version} does not satisfy >=11.5.1`);
}
}
41 changes: 2 additions & 39 deletions .github/workflows/package.yml
Original file line number Diff line number Diff line change
@@ -1,29 +1,17 @@
name: react-day-picker

on:
release:
types: [published]
pull_request:
branches:
- main
- maintenance/v8
push:
branches:
- main
- maintenance/v8
workflow_dispatch:
inputs:
publish:
description: Publish on npm
required: false
default: false
type: boolean

jobs:
# print-env:
# runs-on: ubuntu-latest
# steps:
# - run: |
# echo "publish=${{ github.event.inputs.publish || false }}"

typecheck:
runs-on: ubuntu-latest
steps:
Expand Down Expand Up @@ -84,28 +72,3 @@ jobs:
with:
name: rdp-dist
path: dist

publish-on-npm:
runs-on: ubuntu-latest
needs: [build, test]
if: ${{ github.event_name == 'release' || github.event.inputs.publish }}
permissions:
id-token: write
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
with:
version: 8.6.2
- uses: actions/setup-node@v4
with:
node-version: 18.16
registry-url: https://registry.npmjs.org/
always-auth: false
- uses: actions/download-artifact@v4
with:
name: rdp-dist
path: dist
- run: echo "//<npm-registry>:8080/:_authToken=$NODE_AUTH_TOKEN" > ~/.npmrc
- run: npm publish --provenance
env:
NODE_AUTH_TOKEN: ${{ secrets.npm_token }}
71 changes: 71 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
name: react-day-picker v8 release

on:
release:
types: [published]

permissions:
contents: read

jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
with:
version: 8.6.2
- uses: actions/setup-node@v4
with:
node-version: 24
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm typecheck
- run: pnpm test
- run: pnpm build
- name: Verify npm trusted publishing support
run: node .github/scripts/verify-npm-version.mjs
- name: Smoke test React 19 consumer installs
run: node .github/scripts/smoke-v8-release.mjs react19-runtime
- name: Smoke test React 19 TypeScript declarations
run: node .github/scripts/smoke-v8-release.mjs react19-types
- name: Smoke test React 18 TypeScript declarations
run: node .github/scripts/smoke-v8-release.mjs react18-types

publish:
needs: validate
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v3
with:
version: 8.6.2
- uses: actions/setup-node@v4
with:
node-version: 24
registry-url: https://registry.npmjs.org/
- name: Block prerelease publishing
env:
RELEASE_PRERELEASE: ${{ github.event.release.prerelease }}
run: |
if [ "$RELEASE_PRERELEASE" = "true" ]; then
echo "Prerelease GitHub Releases cannot publish stable v8 packages." >&2
exit 1
fi
- name: Verify release tag matches package version
env:
RELEASE_TAG: ${{ github.event.release.tag_name }}
run: |
expected_tag="v$(node -p "require('./package.json').version")"
if [ "$RELEASE_TAG" != "$expected_tag" ]; then
echo "Release tag $RELEASE_TAG does not match package version $expected_tag." >&2
exit 1
fi
- run: pnpm install --frozen-lockfile
- run: pnpm build
- name: Verify npm trusted publishing support
run: node .github/scripts/verify-npm-version.mjs
- run: npm publish --tag legacy-v8
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# Changelog

## v8.10.2

_Release date: 2026-05-02_

- Expand React peer dependency support to include React 19. This release has no runtime or API changes.

Full release notes at https://github.com/gpbl/react-day-picker/releases
Loading
Loading