Repository navigation
Add more React internals polyfills #79142
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -174,16 +174,125 @@ function patchReference( refPath, legacyName, ms, filename ) { | |
| } | ||
|
|
||
| /** | ||
| * Applies the legacy-element codemod to a bundled React script. | ||
| * Whether a node is a `<object>.props.ref` member expression. | ||
| * | ||
| * @param {string} code Source code to patch. | ||
| * @param {string} filename Source name, for error messages. | ||
| * @return {string} The patched source code. | ||
| * @param {Object} node AST node. | ||
| * @return {boolean} True when the node matches. | ||
| */ | ||
| export function patchReactLegacyElement( code, filename = 'input.js' ) { | ||
| const ast = parse( code, { sourceType: 'unambiguous' } ); | ||
| const ms = new MagicString( code ); | ||
| function isPropsRefMember( node ) { | ||
| return ( | ||
| !! node && | ||
| node.type === 'MemberExpression' && | ||
| ! node.computed && | ||
| node.property.type === 'Identifier' && | ||
| node.property.name === 'ref' && | ||
| node.object.type === 'MemberExpression' && | ||
| ! node.object.computed && | ||
| node.object.property.type === 'Identifier' && | ||
| node.object.property.name === 'props' | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Returns the `<object>.props.ref` member expression when the statement is an | ||
| * assignment of the form `x = y.props.ref` (or `var x = y.props.ref`). | ||
| * | ||
| * @param {Object} statement AST statement node. | ||
| * @return {Object|undefined} The `props.ref` member expression, if matched. | ||
| */ | ||
| function getPropsRefAssignment( statement ) { | ||
| if ( ! statement ) { | ||
| return undefined; | ||
| } | ||
| if ( statement.type === 'ExpressionStatement' ) { | ||
| let expression = statement.expression; | ||
| // Minifiers join consecutive statements into a single comma sequence; | ||
| // the "first statement" is then the first expression of the sequence. | ||
| if ( expression.type === 'SequenceExpression' ) { | ||
| expression = expression.expressions[ 0 ]; | ||
| } | ||
| if ( | ||
| expression.type === 'AssignmentExpression' && | ||
| expression.operator === '=' && | ||
| isPropsRefMember( expression.right ) | ||
| ) { | ||
| return expression.right; | ||
| } | ||
| return undefined; | ||
| } | ||
| if ( | ||
| statement.type === 'VariableDeclaration' && | ||
| statement.declarations.length === 1 && | ||
| isPropsRefMember( statement.declarations[ 0 ].init ) | ||
| ) { | ||
| return statement.declarations[ 0 ].init; | ||
| } | ||
| return undefined; | ||
| } | ||
|
|
||
| /** | ||
| * Patches React DOM's `coerceRef` so it falls back to the legacy | ||
| * `element.ref` when `element.props.ref` is absent. Elements created by older | ||
| * React runtimes keep the ref on the element itself instead of in props. | ||
| * | ||
| * `coerceRef` is identified structurally: its first body statement assigns | ||
| * `<id>.props.ref` (e.g. `t = t.props.ref`). Bundles without such a function | ||
| * (e.g. the `react` bundle) are left untouched, and more than one match is | ||
| * treated as an error since the assumption no longer holds. | ||
| * | ||
| * @param {Object} ast Parsed AST (File node). | ||
| * @param {MagicString} ms Magic string for the source being edited. | ||
| * @param {string} filename Source name, for error messages. | ||
| */ | ||
| function patchCoerceRef( ast, ms, filename ) { | ||
| const matches = []; | ||
|
|
||
| traverse( ast, { | ||
| Function( path ) { | ||
| const body = path.node.body; | ||
| if ( | ||
| ! body || | ||
| body.type !== 'BlockStatement' || | ||
| body.body.length === 0 | ||
| ) { | ||
| return; | ||
| } | ||
| const propsRef = getPropsRefAssignment( body.body[ 0 ] ); | ||
| if ( propsRef ) { | ||
| matches.push( propsRef ); | ||
| } | ||
| }, | ||
| } ); | ||
|
|
||
| if ( matches.length === 0 ) { | ||
| return; | ||
| } | ||
| if ( matches.length > 1 ) { | ||
| throw new Error( | ||
| `[react-legacy-element] Expected exactly one \`coerceRef\`-like function (first statement \`x = y.props.ref\`) in ${ filename }, found ${ matches.length }.` | ||
| ); | ||
| } | ||
|
|
||
| const propsRef = matches[ 0 ]; | ||
| const object = propsRef.object.object; | ||
| const propsRefText = ms.original.slice( propsRef.start, propsRef.end ); | ||
| const objectText = ms.original.slice( object.start, object.end ); | ||
| ms.overwrite( | ||
| propsRef.start, | ||
| propsRef.end, | ||
| `(${ propsRefText } ?? ${ objectText }.ref)` | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Doesn't React do
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, |
||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * Patches every `Symbol.for('react.transitional.element')` variable so that | ||
| * checks against it also accept the legacy `Symbol.for('react.element')`. | ||
| * | ||
| * @param {Object} ast Parsed AST (File node). | ||
| * @param {MagicString} ms Magic string for the source being edited. | ||
| * @param {string} filename Source name, for error messages. | ||
| */ | ||
| function patchTransitionalSymbols( ast, ms, filename ) { | ||
| const declarators = []; | ||
| traverse( ast, { | ||
| VariableDeclarator( path ) { | ||
|
|
@@ -202,6 +311,21 @@ export function patchReactLegacyElement( code, filename = 'input.js' ) { | |
| for ( const declPath of declarators ) { | ||
| patchTransitionalDeclarator( declPath, ms, filename ); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Applies the legacy-element codemod to a bundled React script. | ||
| * | ||
| * @param {string} code Source code to patch. | ||
| * @param {string} filename Source name, for error messages. | ||
| * @return {string} The patched source code. | ||
| */ | ||
| export function patchReactLegacyElement( code, filename = 'input.js' ) { | ||
| const ast = parse( code, { sourceType: 'unambiguous' } ); | ||
| const ms = new MagicString( code ); | ||
|
|
||
| patchTransitionalSymbols( ast, ms, filename ); | ||
| patchCoerceRef( ast, ms, filename ); | ||
|
|
||
| return ms.toString(); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,4 +2,5 @@ module.exports = { | |
| ...require( 'react-dom' ), | ||
| ...require( 'react-dom/client' ), | ||
| ...require( './react-polyfill' ), | ||
| __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: {}, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just to be sure, are we intentionally putting this declaration at the end? Should we add it above the
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. At this point it's certain that the But the field could be moved into the |
||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,5 +4,9 @@ module.exports = { | |
| __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: { | ||
| ReactCurrentOwner: { current: null }, | ||
| ReactCurrentDispatcher: { current: null }, | ||
| ReactDebugCurrentFrame: { | ||
| setExtraStackFrame: () => {}, | ||
| getStackAddendum: () => '', | ||
|
Comment on lines
+8
to
+9
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's keep an eye on the usage here - we may need to "polyfill" more of the methods here. Or should we just stub more, like
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. At this moment we know quite precisely where these calls come from: it's the legacy It's possible that we'll discover usages of |
||
| }, | ||
| }, | ||
| }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we at least log something in the scenario when no matches are found?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, the codemod should fail when something even slightly unexpected happens.