babel-plugin-flow-react-proptypes
Version:
converts flow types to react proptypes
376 lines (318 loc) • 16.2 kB
JavaScript
;
function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.setMakePropTypeImportNode = setMakePropTypeImportNode;
exports.makePropTypesAstForPropTypesAssignment = makePropTypesAstForPropTypesAssignment;
exports.makePropTypesAstForExport = makePropTypesAstForExport;
var _util = require("./util");
var t = _interopRequireWildcard(require("@babel/types"));
var _template = _interopRequireDefault(require("@babel/template"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function _getRequireWildcardCache() { return cache; }; return cache; }
function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || _typeof(obj) !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
function makePropTypeImportNode() {
return t.callExpression(t.identifier('require'), [(0, _util.makeLiteral)('prop-types')]);
}
function setMakePropTypeImportNode(fn) {
makePropTypeImportNode = fn;
}
var dontSetTemplate = (0, _template["default"])("\n(props, propName, componentName) => {\n if(props[propName] != null) {\n throw new Error(`Invalid prop \\`${propName}\\` of value \\`${props[propName]}\\` passed to \\`${componentName}\\`. Expected undefined or null.`);\n }\n}\n");
var exactTemplate = (0, _template["default"])("\n(values, prop, displayName) => {\n var props = PROPS;\n var extra = [];\n for (var k in values) {\n if (values.hasOwnProperty(k) && !props.hasOwnProperty(k)) {\n extra.push(k);\n }\n }\n if (extra.length > 0) {\n return new Error('Invalid additional prop(s) ' + JSON.stringify(extra));\n }\n}\n", {
placeholderPattern: /^PROPS$/
});
var anyTemplate = (0, _template["default"])("\n(props, propName, componentName) => {\n if (!Object.prototype.hasOwnProperty.call(props, propName)) {\n throw new Error(`Prop \\`${propName}\\` has type 'any' or 'mixed', but was not provided to \\`${componentName}\\`. Pass undefined or any other value.`);\n }\n}\n");
var deferTpl = (0, _template["default"])("(function() { return (NODE).apply(this, arguments); })");
/**
* Top-level function to generate prop-types AST.
*
* This will return an expression suitable for assignment to the
* propTypes property of an React component. In particular,
* the AST returned will always yield an object. Assignment
* to Foo.propTypes is possible for objects, not simple
* types.
*
* @param propTypeData Intermediate representation
* @returns {*} AST expression always returning an object.
*/
function makePropTypesAstForPropTypesAssignment(propTypeData) {
var node = null;
if (propTypeData.type === 'shape-intersect-runtime') {
// For top-level usage, e.g. Foo.proptype, return
// an expression returning an object.
node = makeObjectMergeAstForShapeIntersectRuntime(propTypeData);
} else if (propTypeData.type === 'shape') {
node = makeObjectAstForShape(propTypeData);
} else if (propTypeData.type === 'raw') {
node = makeObjectAstForRaw(propTypeData);
}
return node;
}
/**
* Top-level function to generate prop-types AST.
*
* This will return an expression suitable for exporting.
*
* This function is similar to makePropTypesAstForPropTypesAssignment, except
* that you may export non-object expressions.
*
* Any items not handled by makePropTypesAstForPropTypesAssignment will be returned
* as an AST invoking the corresponding function from the prop-types package.
*
* @param propTypeData Intermediate representation
* @returns {*} AST for expression resulting in object or function
*/
function makePropTypesAstForExport(propTypeData) {
var ast = makePropTypesAstForPropTypesAssignment(propTypeData);
if (ast == null) {
// Now we handle isRequired on the import side
// https://github.com/brigand/babel-plugin-flow-react-proptypes/issues/113
propTypeData.isRequired = false;
ast = makePropType(propTypeData);
}
return ast;
}
function makeAnyPropTypeAST() {
var importNode = makePropTypeImportNode();
var anyNode = t.memberExpression(importNode, t.identifier('any'));
return anyNode;
}
function makeObjectAstForRaw(propTypeSpec, propTypeObjects) {
var propTypeObject = typeof propTypeSpec.value === 'string' ? t.identifier(propTypeSpec.value) : propTypeSpec.value; // This will just be a variable, referencing an import we
// generated above. This variable may contain prop-types.any,
// which will not work when used in an intersection.
var anyNode = makeAnyPropTypeAST();
var testExpression = t.binaryExpression('===', propTypeObject, anyNode);
propTypeObject = t.conditionalExpression(testExpression, t.objectExpression([]), t.cloneDeep(propTypeObject));
return propTypeObject;
}
/**
* Generates AST for run-time merges involving either import variables,
* local types (shape) and other run-time merges.
*
* The returned AST is an expression that, when evaluated, returns an
* object:
*
* The expression may look like this:
*
* TODO: Does the nested case below actually work?
*
* Object.assign(
* {},
* {foo: bar},
* someImportedType === require('prop-types).any ? {} : someImportedType,
* {qux: 2},
* Object.assign(
* {},
* {nested: 2}
* ),
* {quz: require('prop-types').shape({foo: bar}),
* );
*
* This method is mainly useful when objects are actually required, such as when the
* type is participating in an intersection or when the result of the intersection is
* to be used as the main proptypes, e.g. for Foo.propTypes = {..object}.
*
* For other uses, the returned object must be wrapped in a shape. See
* makeShapeAstForShapeIntersectRuntime().
*
* @param propTypeData intermediate representation
* @returns {*}
*/
function makeObjectMergeAstForShapeIntersectRuntime(propTypeData) {
var propTypeObjects = [];
propTypeData.properties.forEach(function (propTypeSpec) {
if (propTypeSpec.type === 'raw') {
propTypeObjects.push(makeObjectAstForRaw(propTypeSpec));
} else if (propTypeSpec.type === 'shape') {
propTypeObjects.push(makeObjectAstForShape(propTypeSpec));
} else if (propTypeSpec.type === 'shape-intersect-runtime') {
// TODO: simplify all of this recursive code?
// This returns an object.
propTypeObjects.push(makeObjectMergeAstForShapeIntersectRuntime(propTypeSpec));
}
});
var runtimeMerge = t.callExpression(t.memberExpression(t.identifier('Object'), t.identifier('assign')), [t.objectExpression([])].concat(propTypeObjects));
return runtimeMerge;
}
/**
* Like makeShapeAstForShapeIntersectRuntime, but wraps the props in a shape.
*
* This is useful for nested uses.
*
*/
function makeShapeAstForShapeIntersectRuntime(propTypeData) {
var runtimeMerge = makeObjectMergeAstForShapeIntersectRuntime(propTypeData);
return t.callExpression(t.memberExpression(makePropTypeImportNode(), t.identifier('shape')), [runtimeMerge]);
}
function makeObjectAstForShape(propTypeData) {
// TODO: this is almost duplicated with the shape handling below;
// but this code does not generate AST for a shape function,
// but returns the AST for the object instead.
var rootProperties = propTypeData.properties.map(function (_ref) {
var key = _ref.key,
value = _ref.value,
leadingComments = _ref.leadingComments;
var node = t.objectProperty(t.stringLiteral(key), makePropType(value));
if (leadingComments) {
node.leadingComments = leadingComments;
}
return node;
});
return t.objectExpression(rootProperties);
}
function makeFunctionCheckAST(variableNode) {
return t.binaryExpression('===', t.unaryExpression('typeof', variableNode), t.stringLiteral('function'));
}
function makeNullCheckAST(variableNode) {
return t.binaryExpression('==', variableNode, t.nullLiteral());
}
function markNodeAsRequired(node) {
return t.memberExpression(node, t.identifier('isRequired'));
}
function processQualifiedTypeIdentifierIntoMemberExpression(qualifiedTypeIdentifier) {
var qualification = qualifiedTypeIdentifier.qualification;
var objectAST;
if (qualification.type === 'QualifiedTypeIdentifier') {
objectAST = processQualifiedTypeIdentifierIntoMemberExpression(qualification);
} else if (qualification.type === 'Identifier') {
objectAST = t.identifier(qualification.name);
} else {
throw new Error('Cannot handle type of qualification property:', qualification);
}
var propertyAST = t.identifier(qualifiedTypeIdentifier.id.name);
var memberExpression = t.memberExpression(objectAST, propertyAST);
return t.conditionalExpression(makeNullCheckAST(memberExpression), t.objectExpression([]), memberExpression);
}
/**
* Handles all prop types.
*
* Returns what is expected on the right-hand side of a proptype; that is,
* the actual validation function.
*
* Some special cases exist related to top-level proptypes, where an object is required
* instead of a function. This method does not handle these details: it turns the intermediate
* representation into something that can be used inside a propType object:
*
* Foo.propTypes = {
* bar: makePropType(intermediateRepresentation1),
* baz: makePropType(intermediateRepresentation2),
* }
*
* @param data Intermediate representation of one single proptype
* @param isExact ??
* @returns {*} AST for the prop-types validation function
*/
function makePropType(data, isExact) {
var method = data.type;
if (method === 'exact') {
data.properties.isRequired = data.isRequired;
return makePropType(data.properties, true);
}
var node = makePropTypeImportNode();
var markFullExpressionAsRequired = true;
if (method === 'string' || method === 'number' || method === 'bool' || method === 'object' || method === 'array' || method === 'func' || method === 'node') {
node = t.memberExpression(t.cloneDeep(node), t.identifier(method));
} else if (method === 'any' || !method) {
markFullExpressionAsRequired = false;
if (data.isRequired) {
node = anyTemplate().expression;
} else {
node = t.memberExpression(t.cloneDeep(node), t.identifier('any'));
}
} else if (method === 'raw') {
markFullExpressionAsRequired = false; // In 'raw', we handle variables - typically derived from imported types.
// These are either - at run-time - objects or functions. Objects are wrapped in a shape;
// for functions, we assume that the variable already contains a proptype assertion
var variableNode = typeof data.value === 'string' ? t.identifier(data.value) : t.cloneDeep(data.value);
var originalVariableNode = variableNode;
var shapeNode = t.callExpression(t.memberExpression(makePropTypeImportNode(), t.identifier('shape')), [t.cloneDeep(variableNode)]);
if (data.isRequired) {
shapeNode = markNodeAsRequired(shapeNode);
}
if (data.isRequired) {
variableNode = t.conditionalExpression(t.memberExpression(t.cloneDeep(variableNode), t.identifier('isRequired')), t.memberExpression(t.cloneDeep(variableNode), t.identifier('isRequired')), t.cloneDeep(variableNode));
}
var functionCheckNode = makeFunctionCheckAST(originalVariableNode);
node = t.conditionalExpression(functionCheckNode, variableNode, shapeNode);
node = deferTpl({
NODE: node
}).expression;
} else if (method === 'shape') {
var shapeObjectProperties = data.properties.map(function (_ref2) {
var key = _ref2.key,
value = _ref2.value,
leadingComments = _ref2.leadingComments;
var node = t.objectProperty(t.identifier(key), makePropType(value));
if (leadingComments) {
node.leadingComments = leadingComments;
}
return node;
});
if (isExact || data.isExact) {
shapeObjectProperties.push(t.objectProperty(t.identifier('__exact__'), exactTemplate({
'PROPS': t.objectExpression(data.properties.map(function (_ref3) {
var key = _ref3.key;
return t.objectProperty(t.stringLiteral(key), t.booleanLiteral(true));
}))
}).expression));
}
var shapeObjectExpression = t.objectExpression(shapeObjectProperties);
node = t.callExpression(t.memberExpression(node, t.identifier('shape')), [shapeObjectExpression]);
} else if (method === 'shape-intersect-runtime') {
// Return shape, not object
node = makeShapeAstForShapeIntersectRuntime(data);
} else if (method === 'arrayOf') {
node = t.callExpression(t.memberExpression(node, t.identifier('arrayOf')), [makePropType(data.of)]);
} else if (method === 'objectOf') {
node = t.callExpression(t.memberExpression(node, t.identifier('objectOf')), [makePropType(data.of)]);
} else if (method === 'oneOf') {
node = t.callExpression(t.memberExpression(node, t.identifier('oneOf')), [t.arrayExpression(data.options.map(_util.makeLiteral))]);
} else if (method === 'oneOfType') {
node = t.callExpression(t.memberExpression(node, t.identifier('oneOfType')), [t.arrayExpression(data.options.map(function (item) {
return makePropType(item);
}))]);
} else if (method === 'reference') {
var pp = data.propertyPath.slice();
var valueNode = t.identifier(pp.shift());
while (pp.length) {
valueNode = t.memberExpression(valueNode, t.identifier(pp.shift()));
}
node = t.callExpression(t.memberExpression(node, t.identifier('shape')), [valueNode]);
} else if (method === 'void') {
markFullExpressionAsRequired = false;
node = dontSetTemplate().expression;
} else if (method === 'possible-class') {
markFullExpressionAsRequired = false;
var classSpec;
if (data.value.name != null) {
classSpec = t.identifier(data.value.name);
} else if (data.value.type === 'QualifiedTypeIdentifier') {
classSpec = processQualifiedTypeIdentifierIntoMemberExpression(data.value);
} else {
throw new Error('Unknown node type in possible-class for node:', data.value);
}
var instanceOfNode = t.callExpression(t.memberExpression(node, t.identifier('instanceOf')), [classSpec]);
var anyNode = makeAnyPropTypeAST();
if (data.isRequired) {
instanceOfNode = markNodeAsRequired(instanceOfNode);
anyNode = markNodeAsRequired(anyNode);
}
var _functionCheckNode = makeFunctionCheckAST(classSpec);
node = t.conditionalExpression(_functionCheckNode, instanceOfNode, anyNode); // Don't add .required on the full expression; we already handled this ourselves
// for any proptypes we generated
// Wrap it in a function for ES6 imports with circular dependencies
node = deferTpl({
NODE: node
}).expression;
} else {
var bugData = JSON.stringify(data, null, 2);
(0, _util.$debug)('Unknown node ' + bugData);
throw new Error("".concat(_util.PLUGIN_NAME, " processing error: This is an internal error that should never happen. ") + "Report it immediately with the source file and babel config. Data: ".concat(bugData));
}
if (markFullExpressionAsRequired && data.isRequired) {
node = markNodeAsRequired(node);
}
return t.cloneDeep(node);
}