-
-
Notifications
You must be signed in to change notification settings - Fork 32.4k
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
[docs] Add ref forwarding to API docs #15135
Changes from all commits
70d2335
d7f2311
a6a68f6
81fba47
65a2a3a
27fd460
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 |
---|---|---|
|
@@ -273,6 +273,14 @@ function generateProps(reactAPI) { | |
return textProps; | ||
}, text); | ||
|
||
let refHint = 'The `ref` is forwarded to the root element.'; | ||
if (reactAPI.forwardsRefTo == null) { | ||
refHint = 'The component cannot hold a ref.'; | ||
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. This is the case for almost all components that do not forward refs. Even if some of them can hold a ref we want to prevent users from doing that because converting those components to function components would be breaking. |
||
} else if (reactAPI.forwardsRefTo === 'React.Component') { | ||
refHint = 'The `ref` is attached to a component class.'; | ||
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. TouchRipple is the only candidate for that (allows imperative handle). The other components will be covered by #15170. |
||
} | ||
text = `${text}\n${refHint}\n`; | ||
|
||
if (reactAPI.spread) { | ||
text = `${text} | ||
Any other properties supplied will be spread to the root element (${ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
import * as babel from '@babel/core'; | ||
import { readFile } from 'fs-extra'; | ||
import * as path from 'path'; | ||
|
||
const workspaceRoot = path.join(__dirname, '../../../../'); | ||
const babelConfigPath = path.join(workspaceRoot, 'babel.config.js'); | ||
|
||
function withExtension(filepath, extension) { | ||
return path.join( | ||
path.dirname(filepath), | ||
path.basename(filepath, path.extname(filepath)) + extension, | ||
); | ||
} | ||
|
||
/** | ||
* @param {string} filename | ||
* @param {string} configFilePath | ||
*/ | ||
async function parseWithConfig(filename, configFilePath) { | ||
const source = await readFile(filename, { encoding: 'utf8' }); | ||
const partialConfig = babel.loadPartialConfig({ | ||
configFile: configFilePath, | ||
filename, | ||
}); | ||
return babel.parseAsync(source, partialConfig.options); | ||
} | ||
|
||
function findConformanceDescriptor(program) { | ||
const { types: t } = babel; | ||
|
||
let descriptor = {}; | ||
babel.traverse(program, { | ||
CallExpression(babelPath) { | ||
const { node: callExpression } = babelPath; | ||
const { callee } = callExpression; | ||
if (t.isIdentifier(callee) && callee.name === 'describeConformance') { | ||
// describeConformance(element, () => options); | ||
descriptor = callExpression.arguments[1].body; | ||
} | ||
}, | ||
}); | ||
|
||
if (descriptor.type != null && !t.isObjectExpression(descriptor)) { | ||
throw new Error(`Expected an object expression as a descriptor but found ${descriptor.type}`); | ||
} | ||
|
||
return descriptor; | ||
} | ||
|
||
/** | ||
* | ||
* @param {import('@babel/core').Node} valueNode | ||
*/ | ||
function getRefInstance(valueNode) { | ||
if (!babel.types.isMemberExpression(valueNode)) { | ||
throw new Error('Expected a member expression in refInstanceof'); | ||
} | ||
|
||
switch (valueNode.object.name) { | ||
case 'window': | ||
return valueNode.property.name; | ||
case 'React': | ||
return `React.${valueNode.property.name}`; | ||
default: | ||
throw new Error(`Unrecognized member expression starting with '${valueNode.object.name}'`); | ||
} | ||
} | ||
|
||
/** | ||
* @typedef {Object} ParseResult | ||
* @property {string?} forwardsRefTo | ||
*/ | ||
|
||
/** | ||
* | ||
* @param {string} componentFilename | ||
* @returns {ParseResult} | ||
*/ | ||
export default async function parseTest(componentFilename) { | ||
const testFilename = withExtension(componentFilename, '.test.js'); | ||
const babelParseResult = await parseWithConfig(testFilename, babelConfigPath); | ||
const descriptor = findConformanceDescriptor(babelParseResult.program); | ||
|
||
const result = { | ||
forwardsRefTo: undefined, | ||
}; | ||
|
||
const { properties = [] } = descriptor; | ||
properties.forEach(property => { | ||
const key = property.key.name; | ||
|
||
switch (key) { | ||
case 'refInstanceof': | ||
result.forwardsRefTo = getRefInstance(property.value); | ||
eps1lon marked this conversation as resolved.
Show resolved
Hide resolved
|
||
break; | ||
default: | ||
break; | ||
} | ||
}); | ||
|
||
return result; | ||
} |
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.
I'm surprised it hasn't exploded yet 🚀🌕. This is like 200 concurrent promises 💪.
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.
I will celebrate the day uncaught promises will just throw. The current state of async errors in node is really annoying.