Skip to content

Commit

Permalink
feat: add React 19 support (#2941)
Browse files Browse the repository at this point in the history
  • Loading branch information
diegomura authored Nov 16, 2024
1 parent 8d1946c commit a2665cf
Show file tree
Hide file tree
Showing 19 changed files with 232 additions and 116 deletions.
6 changes: 6 additions & 0 deletions .changeset/curly-pugs-hope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@react-pdf/reconciler": minor
"@react-pdf/renderer": minor
---

feat: add React 19 support
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
matrix:
# We aim to test all maintained LTS versions of Node.js as well as the latest stable version
node_version: [18, 20, 21]
react_version: [16, 17, 18]
react_version: [16, 17, 18, 19]

steps:
- name: Checkout
Expand Down
2 changes: 1 addition & 1 deletion lerna.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "independent",
"packages": ["packages/*", "e2e/*"],
"packages": ["packages/*", "packages/examples/*", "e2e/*"],
"npmClient": "yarn"
}
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,11 @@
"react": "^18.2.0",
"react-16": "npm:react@^16.8.0",
"react-17": "npm:react@^17.0.0",
"react-19": "npm:react@19.0.0-rc-66855b96-20241106",
"react-dom": "^18.2.0",
"react-dom-16": "npm:react-dom@^16.8.0",
"react-dom-17": "npm:react-dom@^17.0.0",
"react-dom-19": "npm:react-dom@19.0.0-rc-66855b96-20241106",
"rimraf": "^2.6.3",
"rollup": "^4.9.0",
"rollup-plugin-copy": "^3.5.0",
Expand Down
1 change: 0 additions & 1 deletion packages/examples/next-14/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
Expand Down
1 change: 0 additions & 1 deletion packages/examples/next-15/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
Expand Down
11 changes: 6 additions & 5 deletions packages/examples/vite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,21 @@
"homepage": "https://github.com/diegomura/react-pdf#readme",
"repository": "git@github.com:diegomura/react-pdf.git",
"scripts": {
"dev": "vite ./src --open"
"dev": "vite ./src --open",
"build:site": "vite build ./src"
},
"dependencies": {
"@react-pdf/renderer": "^4.0.2"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.20",
"postcss": "^8.4.47",
"tailwindcss": "^3.4.12",
"vite": "^5.0.11"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
}
5 changes: 1 addition & 4 deletions packages/examples/vite/src/examples/svg/index.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import ReactPDF, { Document, Page, StyleSheet } from '@react-pdf/renderer';
import { Document, Page, StyleSheet } from '@react-pdf/renderer';

import Svg0 from './svg';
import Svg1 from './Svg1';
Expand All @@ -8,9 +8,6 @@ import Svg4 from './Svg4';
import Star from './Star';
import Heart from './Heart';

console.log(`React version: ${React.version}`);
console.log(`React-pdf version: ${ReactPDF.version}`);

const styles = StyleSheet.create({
page: {
fontSize: 20,
Expand Down
11 changes: 10 additions & 1 deletion packages/reconciler/build/trim-reconciler.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ const KEEP_OPTIONS = {
createTextInstance: true,
createInstance: true,
appendInitialChild: true,
noTimeout: true,
getCurrentUpdatePriority: true,
setCurrentUpdatePriority: true,
resolveUpdatePriority: true,
shouldAttemptEagerTransition: true,
requestPostPaintCallback: true,
maySuspendCommit: true,
};

const STATIC_OPTIONS = {
Expand All @@ -39,7 +46,9 @@ const STATIC_OPTIONS = {

const METHOD_KEYS = {
updateContainer: true,
updateContainerSync: true,
createContainer: true,
flushSyncWork: true,
};

function clearReconcilerOptions(path) {
Expand All @@ -48,7 +57,7 @@ function clearReconcilerOptions(path) {
const optionName = node.property?.name;

// If we are not visiting config object, skip.
if (objectName !== '$$$hostConfig') return;
if (objectName !== '$$$hostConfig' && objectName !== '$$$config') return;

// If it's an option we want to keep, skip.
if (KEEP_OPTIONS[optionName]) return;
Expand Down
3 changes: 2 additions & 1 deletion packages/reconciler/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
],
"devDependencies": {
"ast-types": "^0.14.2",
"react-reconciler": "0.26.0",
"react-reconciler-26": "npm:react-reconciler@0.26.0",
"react-reconciler-31": "npm:react-reconciler@0.31.0-rc-603e6108-20241029",
"recast": "^0.23.9"
}
}
37 changes: 27 additions & 10 deletions packages/reconciler/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,30 @@ import terser from '@rollup/plugin-terser';

import trimReconciler from './build/trim-reconciler.js';

export default {
input: 'src/index.js',
output: { format: 'es', file: 'lib/index.js' },
plugins: [
resolve({ resolveOnly: ['react-reconciler'] }),
commonjs({ esmExternals: (id) => id === 'scheduler' }),
trimReconciler(),
terser({ compress: { dead_code: true } }),
],
};
export default [
{
input: 'src/index.js',
output: { format: 'es', file: 'lib/index.js' },
external: ['./reconciler-26', './reconciler-31'],
},
{
input: 'src/reconciler-26.js',
output: { format: 'es', file: 'lib/reconciler-26.js' },
plugins: [
resolve({ resolveOnly: ['react-reconciler-26'] }),
commonjs({ esmExternals: (id) => id === 'scheduler' }),
trimReconciler(),
terser({ compress: { dead_code: true } }),
],
},
{
input: 'src/reconciler-31.js',
output: { format: 'es', file: 'lib/reconciler-31.js' },
plugins: [
resolve({ resolveOnly: ['react-reconciler-31'] }),
commonjs({ esmExternals: (id) => id === 'scheduler' }),
trimReconciler(),
terser({ compress: { dead_code: true } }),
],
},
];
49 changes: 7 additions & 42 deletions packages/reconciler/src/index.js
Original file line number Diff line number Diff line change
@@ -1,46 +1,11 @@
/* eslint-disable import/extensions */
/* eslint-disable import/no-extraneous-dependencies */
import Reconciler from 'react-reconciler/cjs/react-reconciler.production.min.js';

import propsEqual from './propsEqual';
import React from 'react';
import createRendererForReact19 from './reconciler-31';
import createRendererForReact18AndLess from './reconciler-26';

const emptyObject = {};
const isReact19 = React.version.startsWith('19');

const createRenderer = ({
appendChild,
appendChildToContainer,
commitTextUpdate,
commitUpdate,
createInstance,
createTextInstance,
insertBefore,
removeChild,
removeChildFromContainer,
resetAfterCommit,
}) => {
return Reconciler({
appendChild,
appendChildToContainer,
appendInitialChild: appendChild,
createInstance,
createTextInstance,
insertBefore,
commitUpdate,
commitTextUpdate,
removeChild,
removeChildFromContainer,
resetAfterCommit,
shouldSetTextContent: () => false,
finalizeInitialChildren: () => false,
getPublicInstance: (instance) => instance,
getRootHostContext: () => emptyObject,
getChildHostContext: () => emptyObject,
prepareForCommit() {},
clearContainer() {},
resetTextContent() {},
prepareUpdate: (element, type, oldProps, newProps) =>
!propsEqual(oldProps, newProps),
});
};

export default createRenderer;
export default isReact19
? createRendererForReact19
: createRendererForReact18AndLess;
46 changes: 46 additions & 0 deletions packages/reconciler/src/reconciler-26.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/* eslint-disable import/extensions */
/* eslint-disable import/no-extraneous-dependencies */
import Reconciler from 'react-reconciler-26/cjs/react-reconciler.production.min.js';

import propsEqual from './propsEqual';

const emptyObject = {};

const createRenderer = ({
appendChild,
appendChildToContainer,
commitTextUpdate,
commitUpdate,
createInstance,
createTextInstance,
insertBefore,
removeChild,
removeChildFromContainer,
resetAfterCommit,
}) => {
return Reconciler({
appendChild,
appendChildToContainer,
appendInitialChild: appendChild,
createInstance,
createTextInstance,
insertBefore,
commitUpdate,
commitTextUpdate,
removeChild,
removeChildFromContainer,
resetAfterCommit,
shouldSetTextContent: () => false,
finalizeInitialChildren: () => false,
getPublicInstance: (instance) => instance,
getRootHostContext: () => emptyObject,
getChildHostContext: () => emptyObject,
prepareForCommit() {},
clearContainer() {},
resetTextContent() {},
prepareUpdate: (element, type, oldProps, newProps) =>
!propsEqual(oldProps, newProps),
});
};

export default createRenderer;
88 changes: 88 additions & 0 deletions packages/reconciler/src/reconciler-31.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/* eslint-disable import/extensions */
/* eslint-disable import/no-extraneous-dependencies */

import Reconciler from 'react-reconciler-31';
import {
ConcurrentRoot,
DefaultEventPriority,
} from 'react-reconciler-31/constants';

import propsEqual from './propsEqual';

const emptyObject = {};

const logRecoverableError = console.error;

const createRenderer = ({
appendChild,
appendChildToContainer,
commitTextUpdate,
commitUpdate,
createInstance,
createTextInstance,
insertBefore,
removeChild,
removeChildFromContainer,
resetAfterCommit,
}) => {
const _commitUpdate = (instance, type, oldProps, newProps) => {
if (propsEqual(oldProps, newProps)) return;
commitUpdate(instance, null, type, oldProps, newProps);
};

const reconciler = Reconciler({
appendChild,
appendChildToContainer,
appendInitialChild: appendChild,
createInstance,
createTextInstance,
insertBefore,
commitUpdate: _commitUpdate,
commitTextUpdate,
removeChild,
removeChildFromContainer,
resetAfterCommit,
noTimeout: -1,
shouldSetTextContent: () => false,
finalizeInitialChildren: () => false,
getPublicInstance: (instance) => instance,
getRootHostContext: () => emptyObject,
getChildHostContext: () => emptyObject,
prepareForCommit() {},
clearContainer() {},
resetTextContent() {},
getCurrentUpdatePriority: () => DefaultEventPriority,
maySuspendCommit: () => false,
requestPostPaintCallback: () => {},
resolveUpdatePriority: () => DefaultEventPriority,
setCurrentUpdatePriority: () => {},
shouldAttemptEagerTransition: () => false,
});

const createContainer = (container) => {
return reconciler.createContainer(
container,
ConcurrentRoot, // tag
null, // hydration callbacks
false, // isStrictMode
null, // concurrentUpdatesByDefaultOverride
'', // identifierPrefix
logRecoverableError, // onUncaughtError
logRecoverableError, // onCaughtError
logRecoverableError, // onRecoverableError
null, // transitionCallbacks
);
};

const updateContainer = (doc, mountNode, parentComponent, callback) => {
reconciler.updateContainerSync(doc, mountNode, parentComponent, callback);
reconciler.flushSyncWork();
};

return {
createContainer,
updateContainer,
};
};

export default createRenderer;
5 changes: 2 additions & 3 deletions packages/renderer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,10 @@
"events": "^3.3.0",
"object-assign": "^4.1.1",
"prop-types": "^15.6.2",
"queue": "^6.0.1",
"scheduler": "^0.17.0"
"queue": "^6.0.1"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
},
"lint-staged": {
"*.js": [
Expand Down
4 changes: 1 addition & 3 deletions packages/renderer/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,7 @@ const getPlugins = ({ browser, declarationDests, minify = false }) => [
json(),
...(browser ? [ignore(['fs', 'path', 'url'])] : []),
babel(babelConfig()),
commonjs({
esmExternals: ['scheduler'],
}),
commonjs(),
nodeResolve({ browser, preferBuiltins: !browser }),
replace({
preventAssignment: true,
Expand Down
Loading

0 comments on commit a2665cf

Please sign in to comment.