mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
3848f48943
Summary: ## Rationale **Disclaimer**: This is an incremental step towards more maintainable/readable react-native-codegen generators. In the future, we may want to replace these templates/string concat logic with *something better*. But until we decide what that *something better* is, let's at least get rid of all this gross string find/replace. Benefits of using Function templates over String.prototype.replace. - **Self-documenting**: Template Functions enumerate/describe their exact data dependencies in their signature. You no longer have to read the template implementation to see what data you need to pass into the template. - **Improved Readability**: JavaScript syntax highlighting makes it really easy to see where/how the data is inserted into the templates. Also template variables used be prefixed/suffixed with ::, which made things really confusing in C++ code (e.g: wtf is `::_CLASSNAME_::EventEmitter::::_EVENT_NAME_::`?). - **Simpler Interpolation**: Don't have to worry about .replaceAll vs .replace, or calling these replace functions with regexes or strings. - **Template Type-safety**: Ensure that the correct data types are passed to the component templates (e.g: flow will complain if you accidentally pass null/undefined when a template expects a string). - **Template Type-safety**: Ensure that we don't pass in extra data to templates (this diff catches/fixes instances of this error). Ensure that we don't forget to pass in data to the template. - etc. After this diff, both our Component and NativeModule generators will be using template functions. This string find/replace exists no more in react-native-codegen. This is also a very surface-level change. I made no efforts to simplify these templates. Let's take a look at that later, as necessary. Changelog: [Internal] Reviewed By: yungsters Differential Revision: D32021441 fbshipit-source-id: f8f27069bcbf9d66dcafb7d1411da1f938eb6dcd
443 lines
13 KiB
JavaScript
443 lines
13 KiB
JavaScript
/**
|
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @flow
|
|
* @format
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const j = require('jscodeshift');
|
|
|
|
import type {SchemaType} from '../../CodegenSchema';
|
|
|
|
// File path -> contents
|
|
type FilesOutput = Map<string, string>;
|
|
|
|
const FileTemplate = ({
|
|
imports,
|
|
componentConfig,
|
|
}: {
|
|
imports: string,
|
|
componentConfig: string,
|
|
}) => `
|
|
/**
|
|
* ${'C'}opyright (c) Facebook, Inc. and its affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @flow
|
|
*
|
|
* ${'@'}generated by codegen project: GenerateViewConfigJs.js
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
${imports}
|
|
|
|
${componentConfig}
|
|
`;
|
|
|
|
// We use this to add to a set. Need to make sure we aren't importing
|
|
// this multiple times.
|
|
const UIMANAGER_IMPORT = 'const {UIManager} = require("react-native")';
|
|
|
|
function getReactDiffProcessValue(typeAnnotation) {
|
|
switch (typeAnnotation.type) {
|
|
case 'BooleanTypeAnnotation':
|
|
case 'StringTypeAnnotation':
|
|
case 'Int32TypeAnnotation':
|
|
case 'DoubleTypeAnnotation':
|
|
case 'FloatTypeAnnotation':
|
|
case 'ObjectTypeAnnotation':
|
|
case 'StringEnumTypeAnnotation':
|
|
case 'Int32EnumTypeAnnotation':
|
|
return j.literal(true);
|
|
case 'ReservedPropTypeAnnotation':
|
|
switch (typeAnnotation.name) {
|
|
case 'ColorPrimitive':
|
|
return j.template
|
|
.expression`{ process: require('react-native/Libraries/StyleSheet/processColor') }`;
|
|
case 'ImageSourcePrimitive':
|
|
return j.template
|
|
.expression`{ process: require('react-native/Libraries/Image/resolveAssetSource') }`;
|
|
case 'PointPrimitive':
|
|
return j.template
|
|
.expression`{ diff: require('react-native/Libraries/Utilities/differ/pointsDiffer') }`;
|
|
case 'EdgeInsetsPrimitive':
|
|
return j.template
|
|
.expression`{ diff: require('react-native/Libraries/Utilities/differ/insetsDiffer') }`;
|
|
default:
|
|
(typeAnnotation.name: empty);
|
|
throw new Error(
|
|
`Received unknown native typeAnnotation: "${typeAnnotation.name}"`,
|
|
);
|
|
}
|
|
case 'ArrayTypeAnnotation':
|
|
if (typeAnnotation.elementType.type === 'ReservedPropTypeAnnotation') {
|
|
switch (typeAnnotation.elementType.name) {
|
|
case 'ColorPrimitive':
|
|
return j.template
|
|
.expression`{ process: require('react-native/Libraries/StyleSheet/processColorArray') }`;
|
|
case 'ImageSourcePrimitive':
|
|
return j.literal(true);
|
|
case 'PointPrimitive':
|
|
return j.literal(true);
|
|
default:
|
|
throw new Error(
|
|
`Received unknown array native typeAnnotation: "${typeAnnotation.elementType.name}"`,
|
|
);
|
|
}
|
|
}
|
|
return j.literal(true);
|
|
default:
|
|
(typeAnnotation: empty);
|
|
throw new Error(
|
|
`Received unknown typeAnnotation: "${typeAnnotation.type}"`,
|
|
);
|
|
}
|
|
}
|
|
|
|
const ComponentTemplate = ({
|
|
componentNameWithCompatSupport,
|
|
deprecationCheck,
|
|
}: {
|
|
componentNameWithCompatSupport: string,
|
|
deprecationCheck: string,
|
|
}) =>
|
|
`
|
|
let nativeComponentName = '${componentNameWithCompatSupport}';
|
|
${deprecationCheck}
|
|
export default NativeComponentRegistry.get(nativeComponentName, () => VIEW_CONFIG);
|
|
`.trim();
|
|
|
|
const DeprecatedComponentTemplate = ({
|
|
componentName,
|
|
componentNameDeprecated,
|
|
}: {
|
|
componentName: string,
|
|
componentNameDeprecated: string,
|
|
}) =>
|
|
`
|
|
if (UIManager.getViewManagerConfig('${componentName}')) {
|
|
nativeComponentName = '${componentName}';
|
|
} else if (UIManager.getViewManagerConfig('${componentNameDeprecated}')) {
|
|
nativeComponentName = '${componentNameDeprecated}';
|
|
} else {
|
|
throw new Error('Failed to find native component for either "${componentName}" or "${componentNameDeprecated}"');
|
|
}
|
|
`.trim();
|
|
|
|
// Replicates the behavior of RCTNormalizeInputEventName in RCTEventDispatcher.m
|
|
function normalizeInputEventName(name) {
|
|
if (name.startsWith('on')) {
|
|
return name.replace(/^on/, 'top');
|
|
} else if (!name.startsWith('top')) {
|
|
return `top${name[0].toUpperCase()}${name.slice(1)}`;
|
|
}
|
|
|
|
return name;
|
|
}
|
|
|
|
// Replicates the behavior of viewConfig in RCTComponentData.m
|
|
function getValidAttributesForEvents(events) {
|
|
return events.map(eventType => {
|
|
return j.property('init', j.identifier(eventType.name), j.literal(true));
|
|
});
|
|
}
|
|
|
|
function generateBubblingEventInfo(event, nameOveride) {
|
|
return j.property(
|
|
'init',
|
|
j.identifier(nameOveride || normalizeInputEventName(event.name)),
|
|
j.objectExpression([
|
|
j.property(
|
|
'init',
|
|
j.identifier('phasedRegistrationNames'),
|
|
j.objectExpression([
|
|
j.property(
|
|
'init',
|
|
j.identifier('captured'),
|
|
j.literal(`${event.name}Capture`),
|
|
),
|
|
j.property('init', j.identifier('bubbled'), j.literal(event.name)),
|
|
]),
|
|
),
|
|
]),
|
|
);
|
|
}
|
|
|
|
function generateDirectEventInfo(event, nameOveride) {
|
|
return j.property(
|
|
'init',
|
|
j.identifier(nameOveride || normalizeInputEventName(event.name)),
|
|
j.objectExpression([
|
|
j.property(
|
|
'init',
|
|
j.identifier('registrationName'),
|
|
j.literal(event.name),
|
|
),
|
|
]),
|
|
);
|
|
}
|
|
|
|
function buildViewConfig(
|
|
schema: SchemaType,
|
|
componentName: string,
|
|
component,
|
|
imports,
|
|
) {
|
|
const componentProps = component.props;
|
|
const componentEvents = component.events;
|
|
|
|
component.extendsProps.forEach(extendProps => {
|
|
switch (extendProps.type) {
|
|
case 'ReactNativeBuiltInType':
|
|
switch (extendProps.knownTypeName) {
|
|
case 'ReactNativeCoreViewProps':
|
|
imports.add(
|
|
"const NativeComponentRegistry = require('react-native/Libraries/NativeComponent/NativeComponentRegistry');",
|
|
);
|
|
|
|
return;
|
|
default:
|
|
(extendProps.knownTypeName: empty);
|
|
throw new Error('Invalid knownTypeName');
|
|
}
|
|
default:
|
|
(extendProps.type: empty);
|
|
throw new Error('Invalid extended type');
|
|
}
|
|
});
|
|
|
|
const validAttributes = j.objectExpression([
|
|
...componentProps.map(schemaProp => {
|
|
return j.property(
|
|
'init',
|
|
j.identifier(schemaProp.name),
|
|
getReactDiffProcessValue(schemaProp.typeAnnotation),
|
|
);
|
|
}),
|
|
...getValidAttributesForEvents(componentEvents),
|
|
]);
|
|
|
|
const bubblingEventNames = component.events
|
|
.filter(event => event.bubblingType === 'bubble')
|
|
.reduce((bubblingEvents, event) => {
|
|
// We add in the deprecated paper name so that it is in the view config.
|
|
// This means either the old event name or the new event name can fire
|
|
// and be sent to the listener until the old top level name is removed.
|
|
if (event.paperTopLevelNameDeprecated) {
|
|
bubblingEvents.push(
|
|
generateBubblingEventInfo(event, event.paperTopLevelNameDeprecated),
|
|
);
|
|
}
|
|
bubblingEvents.push(generateBubblingEventInfo(event));
|
|
return bubblingEvents;
|
|
}, []);
|
|
|
|
const bubblingEvents =
|
|
bubblingEventNames.length > 0
|
|
? j.property(
|
|
'init',
|
|
j.identifier('bubblingEventTypes'),
|
|
j.objectExpression(bubblingEventNames),
|
|
)
|
|
: null;
|
|
|
|
const directEventNames = component.events
|
|
.filter(event => event.bubblingType === 'direct')
|
|
.reduce((directEvents, event) => {
|
|
// We add in the deprecated paper name so that it is in the view config.
|
|
// This means either the old event name or the new event name can fire
|
|
// and be sent to the listener until the old top level name is removed.
|
|
if (event.paperTopLevelNameDeprecated) {
|
|
directEvents.push(
|
|
generateDirectEventInfo(event, event.paperTopLevelNameDeprecated),
|
|
);
|
|
}
|
|
directEvents.push(generateDirectEventInfo(event));
|
|
return directEvents;
|
|
}, []);
|
|
|
|
const directEvents =
|
|
directEventNames.length > 0
|
|
? j.property(
|
|
'init',
|
|
j.identifier('directEventTypes'),
|
|
j.objectExpression(directEventNames),
|
|
)
|
|
: null;
|
|
|
|
const properties = [
|
|
j.property(
|
|
'init',
|
|
j.identifier('uiViewClassName'),
|
|
j.literal(componentName),
|
|
),
|
|
bubblingEvents,
|
|
directEvents,
|
|
j.property('init', j.identifier('validAttributes'), validAttributes),
|
|
].filter(Boolean);
|
|
|
|
return j.objectExpression(properties);
|
|
}
|
|
|
|
function buildCommands(
|
|
schema: SchemaType,
|
|
componentName: string,
|
|
component,
|
|
imports,
|
|
) {
|
|
const commands = component.commands;
|
|
|
|
if (commands.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
imports.add(
|
|
'const {dispatchCommand} = require("react-native/Libraries/Renderer/shims/ReactNative");',
|
|
);
|
|
|
|
const properties = commands.map(command => {
|
|
const commandName = command.name;
|
|
const params = command.typeAnnotation.params;
|
|
|
|
const commandNameLiteral = j.literal(commandName);
|
|
const commandNameIdentifier = j.identifier(commandName);
|
|
const arrayParams = j.arrayExpression(
|
|
params.map(param => {
|
|
return j.identifier(param.name);
|
|
}),
|
|
);
|
|
|
|
const expression = j.template
|
|
.expression`dispatchCommand(ref, ${commandNameLiteral}, ${arrayParams})`;
|
|
|
|
const functionParams = params.map(param => {
|
|
return j.identifier(param.name);
|
|
});
|
|
|
|
const property = j.property(
|
|
'init',
|
|
commandNameIdentifier,
|
|
j.functionExpression(
|
|
null,
|
|
[j.identifier('ref'), ...functionParams],
|
|
j.blockStatement([j.expressionStatement(expression)]),
|
|
),
|
|
);
|
|
property.method = true;
|
|
|
|
return property;
|
|
});
|
|
|
|
return j.exportNamedDeclaration(
|
|
j.variableDeclaration('const', [
|
|
j.variableDeclarator(
|
|
j.identifier('Commands'),
|
|
j.objectExpression(properties),
|
|
),
|
|
]),
|
|
);
|
|
}
|
|
|
|
module.exports = {
|
|
generate(libraryName: string, schema: SchemaType): FilesOutput {
|
|
try {
|
|
const fileName = `${libraryName}NativeViewConfig.js`;
|
|
const imports: Set<string> = new Set();
|
|
|
|
const moduleResults = Object.keys(schema.modules)
|
|
.map(moduleName => {
|
|
const module = schema.modules[moduleName];
|
|
if (module.type !== 'Component') {
|
|
return;
|
|
}
|
|
|
|
const {components} = module;
|
|
|
|
return Object.keys(components)
|
|
.map((componentName: string) => {
|
|
const component = components[componentName];
|
|
|
|
const paperComponentName = component.paperComponentName
|
|
? component.paperComponentName
|
|
: componentName;
|
|
|
|
if (component.paperComponentNameDeprecated) {
|
|
imports.add(UIMANAGER_IMPORT);
|
|
}
|
|
|
|
const deprecatedCheckBlock = component.paperComponentNameDeprecated
|
|
? DeprecatedComponentTemplate({
|
|
componentName,
|
|
componentNameDeprecated:
|
|
component.paperComponentNameDeprecated || '',
|
|
})
|
|
: '';
|
|
|
|
const replacedTemplate = ComponentTemplate({
|
|
componentNameWithCompatSupport: paperComponentName,
|
|
deprecationCheck: deprecatedCheckBlock,
|
|
});
|
|
|
|
const replacedSourceRoot = j.withParser('flow')(replacedTemplate);
|
|
|
|
replacedSourceRoot
|
|
.find(j.Identifier, {
|
|
name: 'VIEW_CONFIG',
|
|
})
|
|
.replaceWith(
|
|
buildViewConfig(
|
|
schema,
|
|
paperComponentName,
|
|
component,
|
|
imports,
|
|
),
|
|
);
|
|
|
|
const commands = buildCommands(
|
|
schema,
|
|
paperComponentName,
|
|
component,
|
|
imports,
|
|
);
|
|
if (commands) {
|
|
replacedSourceRoot
|
|
.find(j.ExportDefaultDeclaration)
|
|
.insertAfter(j(commands).toSource());
|
|
}
|
|
|
|
const replacedSource: string = replacedSourceRoot.toSource({
|
|
quote: 'single',
|
|
trailingComma: true,
|
|
});
|
|
|
|
return replacedSource;
|
|
})
|
|
.join('\n\n');
|
|
})
|
|
.filter(Boolean)
|
|
.join('\n\n');
|
|
|
|
const replacedTemplate = FileTemplate({
|
|
componentConfig: moduleResults,
|
|
imports: Array.from(imports)
|
|
.sort()
|
|
.join('\n'),
|
|
});
|
|
|
|
return new Map([[fileName, replacedTemplate]]);
|
|
} catch (error) {
|
|
console.error(`\nError parsing schema for ${libraryName}\n`);
|
|
console.error(JSON.stringify(schema));
|
|
throw error;
|
|
}
|
|
},
|
|
};
|