Skip to content
Closed
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
39 changes: 35 additions & 4 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,61 @@ updates:
interval: weekly
day: monday
time: '06:00'
timezone: America/Chicago
open-pull-requests-limit: 10
target-branch: dev
commit-message:
prefix: 'chore'
prefix: chore
include: scope
labels:
- dependencies
- automated
groups:
production-dependencies:
production-minor-patch-dependencies:
dependency-type: production
development-dependencies:
update-types:
- minor
- patch
exclude-patterns:
- react
- react-dom

development-minor-patch-dependencies:
dependency-type: development
update-types:
- minor
- patch
exclude-patterns:
- react
- react-dom
- '@types/react'
- '@types/react-dom'

ignore:
- dependency-name: react
update-types:
- version-update:semver-major
- dependency-name: react-dom
update-types:
- version-update:semver-major
- dependency-name: '@types/react'
update-types:
- version-update:semver-major
- dependency-name: '@types/react-dom'
update-types:
- version-update:semver-major

- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
day: monday
time: '06:30'
timezone: America/Chicago
open-pull-requests-limit: 10
target-branch: dev
commit-message:
prefix: 'ci'
prefix: ci
include: scope
labels:
- dependencies
Expand Down
10 changes: 5 additions & 5 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,11 @@ If this is breaking, describe the change and required migration steps.

What did you do to validate this change?

- [ ] `npm run format`
- [ ] `npm run lint`
- [ ] `npm run typecheck`
- [ ] `npm run test`
- [ ] `npm run build`
- [ ] `pnpm run format`
- [ ] `pnpm run lint`
- [ ] `pnpm run typecheck`
- [ ] `pnpm run test`
- [ ] `pnpm run build`

Describe any additional manual or automated testing performed.

Expand Down
176 changes: 176 additions & 0 deletions .github/scripts/update-react-major-support.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import { execFileSync } from 'node:child_process';
import {
appendFileSync,
existsSync,
readFileSync,
writeFileSync,
} from 'node:fs';

const packageJsonPath = 'package.json';
const readmePath = 'README.md';
const workflowPaths = [
'.github/workflows/ci.yml',
'.github/workflows/release.yml',
];

function setOutput(name, value) {
if (!process.env.GITHUB_OUTPUT) {
return;
}

appendFileSync(process.env.GITHUB_OUTPUT, `${name}=${value}\n`);
}

function readJson(path) {
return JSON.parse(readFileSync(path, 'utf8'));
}

function writeJson(path, value) {
writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`);
}

function getLatestReactVersion() {
return execFileSync('npm', ['view', 'react', 'version'], {
encoding: 'utf8',
}).trim();
}

function getMajor(version) {
const major = Number.parseInt(version.split('.')[0] ?? '', 10);

if (!Number.isInteger(major)) {
throw new Error(`Could not parse major version from "${version}".`);
}

return major;
}

function getMinimumPeerMajor(peerRange) {
const match = peerRange.match(/>=\s*(\d+)/);

if (!match) {
throw new Error(
`Could not parse minimum React peer major from "${peerRange}".`,
);
}

return Number.parseInt(match[1], 10);
}

function getMaximumSupportedMajor(peerRange) {
const match = peerRange.match(/<\s*(\d+)/);

if (!match) {
throw new Error(
`Could not parse upper React peer bound from "${peerRange}".`,
);
}

return Number.parseInt(match[1], 10) - 1;
}

function getMajorRange(minimumMajor, maximumMajor) {
return Array.from({ length: maximumMajor - minimumMajor + 1 }, (_, index) =>
String(minimumMajor + index),
);
}

function formatYamlArray(values) {
return `[${values.map((value) => `'${value}'`).join(', ')}]`;
}

function formatReadmeCompatibility(values) {
if (values.length === 1) {
return values[0];
}

if (values.length === 2) {
return `${values[0]} or ${values[1]}`;
}

return `${values.slice(0, -1).join(', ')}, or ${values.at(-1)}`;
}

function updateWorkflowReactMatrix(path, supportedMajors) {
if (!existsSync(path)) {
return;
}

const original = readFileSync(path, 'utf8');
const updated = original.replace(
/react:\s*\[[^\]]+\]/g,
`react: ${formatYamlArray(supportedMajors)}`,
);

if (updated === original) {
throw new Error(`Could not find an inline React matrix in ${path}.`);
}

writeFileSync(path, updated);
}

function updateReadmeCompatibility(path, supportedMajors) {
if (!existsSync(path)) {
return;
}

const original = readFileSync(path, 'utf8');
const compatibility = formatReadmeCompatibility(supportedMajors);
const updated = original.replace(
/Requires React and react-dom .+?\./,
`Requires React and react-dom ${compatibility}.`,
);

if (updated !== original) {
writeFileSync(path, updated);
}
}

const packageJson = readJson(packageJsonPath);
const latestReactVersion = getLatestReactVersion();
const latestReactMajor = getMajor(latestReactVersion);
const currentReactPeerRange = packageJson.peerDependencies?.react;

if (!currentReactPeerRange) {
throw new Error('package.json is missing peerDependencies.react.');
}

const minimumSupportedMajor = getMinimumPeerMajor(currentReactPeerRange);
const maximumSupportedMajor = getMaximumSupportedMajor(currentReactPeerRange);

setOutput('latest-react-version', latestReactVersion);
setOutput('latest-react-major', String(latestReactMajor));

if (latestReactMajor <= maximumSupportedMajor) {
setOutput('changed', 'false');
setOutput('candidate-react-major', String(maximumSupportedMajor));
console.log(
`React ${latestReactVersion} is already covered by ${currentReactPeerRange}.`,
);
process.exit(0);
}

const candidateReactMajor = maximumSupportedMajor + 1;
const supportedMajors = getMajorRange(
minimumSupportedMajor,
candidateReactMajor,
);
const nextPeerRange = `>=${minimumSupportedMajor} <${candidateReactMajor + 1}`;

packageJson.peerDependencies.react = nextPeerRange;
packageJson.peerDependencies['react-dom'] = nextPeerRange;

writeJson(packageJsonPath, packageJson);

for (const workflowPath of workflowPaths) {
updateWorkflowReactMatrix(workflowPath, supportedMajors);
}

updateReadmeCompatibility(readmePath, supportedMajors);

setOutput('changed', 'true');
setOutput('candidate-react-major', String(candidateReactMajor));

console.log(
`Prepared React ${candidateReactMajor} compatibility candidate using latest React ${latestReactVersion}.`,
);
59 changes: 42 additions & 17 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,59 +33,84 @@ jobs:

steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
persist-credentials: false

- name: Enable Corepack
run: corepack enable

- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 24
cache: pnpm
cache-dependency-path: pnpm-lock.yaml

- name: Install dependencies
run: npm install
run: pnpm install --frozen-lockfile

- name: Verify dependency signatures
run: npm audit signatures
run: pnpm audit

- name: Check formatting
run: npm run format:check
run: pnpm run format:check

- name: Lint
run: npm run lint

- name: Typecheck
run: npm run typecheck
run: pnpm run lint

- name: Build
run: npm run build

test:
name: Test (Node ${{ matrix.node }})
compatibility:
name: Compatibility (Node ${{ matrix.node }}, React ${{ matrix.react }})
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }}
needs: quality
runs-on: ubuntu-latest
timeout-minutes: 20

strategy:
fail-fast: false
matrix:
node:
- '20'
- '22'
- '24'
react:
- '18'
- '19'

steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
persist-credentials: false

- name: Enable Corepack
run: corepack enable

- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node }}
cache: pnpm
cache-dependency-path: pnpm-lock.yaml

- name: Install dependencies
run: npm install
run: pnpm install --frozen-lockfile

- name: Install React compatibility version
run: >
npm install
--no-save
--package-lock=false
react@${{ matrix.react }}
react-dom@${{ matrix.react }}
@types/react@${{ matrix.react }}
@types/react-dom@${{ matrix.react }}

- name: Typecheck
run: pnpm run typecheck

- name: Build
run: pnpm run build

- name: Run tests
run: npm test
- name: Test
run: pnpm test
Loading
Loading