/** * Copyright (c) 2015-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ "use strict"; const babel = require("babel-core"); const deepAssign = require("deep-assign"); const docgen = require("react-docgen"); const docgenHelpers = require("./docgenHelpers"); const docsList = require("./docsList"); // TODO: Use the actual docsList from the checked out tag... const fs = require("fs"); const jsDocs = require("../jsdocs/jsdocs.js"); const jsdocApi = require("jsdoc-api"); const path = require("path"); const recast = require("recast"); const slugify = require("../core/slugify"); const ANDROID_SUFFIX = "android"; const CROSS_SUFFIX = "cross"; const IOS_SUFFIX = "ios"; function endsWith(str, suffix) { return str.indexOf(suffix, str.length - suffix.length) !== -1; } function removeExtName(filepath) { let ext = path.extname(filepath); while (ext) { filepath = path.basename(filepath, ext); ext = path.extname(filepath); } return filepath; } function getNameFromPath(filepath) { filepath = removeExtName(filepath); if (filepath === "LayoutPropTypes") { return "Layout Props"; } else if (filepath === "ShadowPropTypesIOS") { return "Shadow Props"; } else if (filepath === "TransformPropTypes") { return "Transforms"; } else if (filepath === "TabBarItemIOS") { return "TabBarIOS.Item"; } else if (filepath === "AnimatedImplementation") { return "Animated"; } return filepath; } function getPlatformFromPath(filepath) { filepath = removeExtName(filepath); if (endsWith(filepath, "Android")) { return ANDROID_SUFFIX; } else if (endsWith(filepath, "IOS")) { return IOS_SUFFIX; } return CROSS_SUFFIX; } // Add methods that should not appear in the components documentation. const methodsBlacklist = [ // Native methods mixin. "getInnerViewNode", "setNativeProps", // Touchable mixin. "touchableHandlePress", "touchableHandleActivePressIn", "touchableHandleActivePressOut", "touchableHandleLongPress", "touchableGetPressRectOffset", "touchableGetHitSlop", "touchableGetHighlightDelayMS", "touchableGetLongPressDelayMS", "touchableGetPressOutDelayMS", // Scrollable mixin. "getScrollableNode", "getScrollResponder" ]; function filterMethods(method) { return method.name[0] !== "_" && methodsBlacklist.indexOf(method.name) === -1; } // Hide a component from the sidebar by making it return false from // this function const HIDDEN_COMPONENTS = ["Transforms", "ListViewDataSource"]; function shouldDisplayInSidebar(componentName) { return HIDDEN_COMPONENTS.indexOf(componentName) === -1; } function getNextComponent(idx) { if (all[idx + 1]) { const nextComponentName = getNameFromPath(all[idx + 1]); if (shouldDisplayInSidebar(nextComponentName)) { return slugify(nextComponentName); } else { return getNextComponent(idx + 1); } } return null; } function getPreviousComponent(idx) { if (all[idx - 1]) { const previousComponentName = getNameFromPath(all[idx - 1]); if (shouldDisplayInSidebar(previousComponentName)) { return slugify(previousComponentName); } else { return getPreviousComponent(idx - 1); } } return null; } function componentsToMarkdown(type, json, filepath, idx, styles) { const componentName = getNameFromPath(filepath); const componentPlatform = getPlatformFromPath(filepath); const docFilePath = "../docs/" + componentName + ".md"; if (fs.existsSync(docFilePath)) { json.fullDescription = fs.readFileSync(docFilePath).toString(); } json.type = type; json.filepath = filepath.replace(/^\.\.\//, ""); json.componentName = componentName; json.componentPlatform = componentPlatform; if (styles) { json.styles = styles; } if (json.methods) { json.methods = json.methods.filter(filterMethods); } if (type === "api") { type = "API"; } // Put styles (e.g. Flexbox) into the API category const category = type === "style" ? "APIs" : type + "s"; const next = getNextComponent(idx); const previous = getPreviousComponent(idx); const res = [ "---", "id: " + slugify(componentName), "title: " + componentName, "layout: autodocs", "category: " + category, "permalink: docs/" + slugify(componentName) + ".html", "platform: " + componentPlatform, "next: " + next, "previous: " + previous, "sidebar: " + shouldDisplayInSidebar(componentName), "path:" + json.filepath, "---", JSON.stringify(json, null, 2) ] .filter(function(line) { return line; }) .join("\n"); return res; } let componentCount; function getTypedef(filepath, fileContent, json) { let typedefDocgen; try { typedefDocgen = docgen .parse(fileContent, docgenHelpers.findExportedType, [ docgenHelpers.typedefHandler ]) .map(type => type.typedef); } catch (e) { // Ignore errors due to missing exported type definitions if (e.message.indexOf(docgen.ERROR_MISSING_DEFINITION) !== -1) { console.error("Cannot parse file", filepath, e); } } if (!json) { return typedefDocgen; } const typedef = typedefDocgen; if (json.typedef && json.typedef.length !== 0) { json.typedef.forEach(def => { const typedefMatch = typedefDocgen.find(t => t.name === def.name); if (typedefMatch) { typedef.name = Object.assign(typedefMatch, def); } else { typedef.push(def); } }); } return typedef; } /** * Load and parse ViewPropTypes data. * This method returns a Documentation object that's empty except for 'props'. * It should be merged with a component Documentation object. */ function getViewPropTypes() { // Finds default export of ViewPropTypes (the propTypes object expression). function viewPropTypesResolver(ast, recast) { let definition; recast.visit(ast, { visitAssignmentExpression: function(astPath) { if (!definition && docgen.utils.isExportsOrModuleAssignment(astPath)) { definition = docgen.utils.resolveToValue(astPath.get("right")); } return false; } }); return definition; } // Wrap ViewPropTypes export in a propTypes property inside of a fake class. // This way the default docgen handlers will parse the properties and docs. // The alternative would be to duplicate more of the parsing logic here. function viewPropTypesConversionHandler(documentation, astPath) { const builders = recast.types.builders; // This is broken because babylon@7 and estree introduced SpreadElement, and ast-types has not been updated to support it // (we are broken by react-docgen broken by recast broken by ast-types) astPath.get("properties").value.forEach(n => { if (n.type === "SpreadElement") { n.type = "SpreadProperty"; } }); const FauxView = builders.classDeclaration( builders.identifier("View"), builders.classBody([ builders.classProperty( builders.identifier("propTypes"), builders.objectExpression(astPath.get("properties").value), null, // TypeAnnotation true // static ) ]) ); astPath.replace(FauxView); } return docgen.parse( fs.readFileSync(docsList.viewPropTypes), viewPropTypesResolver, [viewPropTypesConversionHandler].concat(docgen.defaultHandlers) ); } function renderComponent(filepath) { try { const fileContent = fs.readFileSync(filepath); const handlers = docgen.defaultHandlers.concat([ docgenHelpers.stylePropTypeHandler, docgenHelpers.deprecatedPropTypeHandler, docgenHelpers.jsDocFormatHandler ]); const json = docgen.parse( fileContent, docgenHelpers.findExportedOrFirst, handlers ); json.typedef = getTypedef(filepath, fileContent); // ReactNative View component imports its propTypes from ViewPropTypes. // This trips up docgen though since it expects them to be defined on View. // We need to wire them up by manually importing and parsing ViewPropTypes. if (filepath.match(/View\/View\.js/)) { const viewPropTypesJSON = getViewPropTypes(); json.props = viewPropTypesJSON.props; } return componentsToMarkdown( "component", json, filepath, componentCount++, styleDocs ); } catch (e) { console.log("error in renderComponent for", filepath); throw e; } } function isJsDocFormat(fileContent) { const reComment = /\/\*\*[\s\S]+?\*\//g; const comments = fileContent.match(reComment); if (!comments) { return false; } return !!comments[0].match(/\s*\*\s+@jsdoc/); } function parseAPIJsDocFormat(filepath, fileContent) { const fileName = path.basename(filepath); const babelRC = { filename: fileName, sourceFileName: fileName, plugins: ["transform-flow-strip-types"] }; // Babel transform const code = babel.transform(fileContent, babelRC).code; // Parse via jsdoc-api let jsonParsed = jsdocApi.explainSync({ source: code, configure: "./jsdocs/jsdoc-conf.json" }); // Clean up jsdoc-api return jsonParsed = jsonParsed.filter(i => { return !i.undocumented && !/package|file/.test(i.kind); }); jsonParsed = jsonParsed.map(identifier => { delete identifier.comment; return identifier; }); jsonParsed.forEach((identifier, index) => { identifier.order = index; }); // Group by "kind" const json = {}; jsonParsed.forEach((identifier, index) => { let kind = identifier.kind; if (kind === "function") { kind = "methods"; } if (!json[kind]) { json[kind] = []; } delete identifier.kind; json[kind].push(identifier); }); json.typedef = getTypedef(filepath, fileContent, json); return json; } function parseAPIInferred(filepath, fileContent) { let json; try { json = jsDocs(fileContent); if (!json) { throw new Error("parseSource returned falsy"); } } catch (e) { console.error("Cannot parse file", filepath, e); json = {}; } return json; } function getTypeName(type) { let typeName; switch (type.name) { case "signature": typeName = type.type; break; case "union": typeName = type.value ? type.value.map(getTypeName) : type.elements.map(getTypeName); break; case "enum": if (typeof type.value === "string") { typeName = type.value; } else { typeName = "enum"; } break; case "$Enum": if (type.elements[0].signature.properties) { typeName = type.elements[0].signature.properties.map(p => p.key); } break; case "arrayOf": typeName = getTypeName(type.value); break; case "instanceOf": typeName = type.value; break; case "func": typeName = "function"; break; default: typeName = type.alias ? type.alias : type.name; break; } return typeName; } function getTypehintRec(typehint) { if (typehint.type === "simple") { return typehint.value; } if (typehint.type === "generic") { return ( getTypehintRec(typehint.value[0]) + "<" + getTypehintRec(typehint.value[1]) + ">" ); } return JSON.stringify(typehint); } function getTypehint(typehint) { if (typeof typehint === "object" && typehint.name) { return getTypeName(typehint); } try { var typehint = JSON.parse(typehint); } catch (e) { return typehint .toString() .split("|") .map(type => type.trim()); } return getTypehintRec(typehint); } function getJsDocFormatType(entities) { const modEntities = entities; if (entities) { if (typeof entities === "object" && entities.length) { entities.map((entity, entityIndex) => { if (entity.typehint) { const typeNames = [].concat(getTypehint(entity.typehint)); modEntities[entityIndex].type = { names: typeNames }; delete modEntities[entityIndex].typehint; } if (entity.name) { const regexOptionalType = /\?$/; if (regexOptionalType.test(entity.name)) { modEntities[entityIndex].optional = true; modEntities[entityIndex].name = entity.name.replace( regexOptionalType, "" ); } } }); } else { const typeNames = [].concat(getTypehint(entities)); return { type: { names: typeNames } }; } } return modEntities; } function renderAPI(filepath, type) { try { const fileContent = fs.readFileSync(filepath).toString(); let json = parseAPIInferred(filepath, fileContent); if (isJsDocFormat(fileContent)) { const jsonJsDoc = parseAPIJsDocFormat(filepath, fileContent); // Combine method info with jsdoc formatted content const methods = json.methods; if (methods && methods.length) { const modMethods = methods; methods.map((method, methodIndex) => { modMethods[methodIndex].params = getJsDocFormatType(method.params); modMethods[methodIndex].returns = getJsDocFormatType( method.returntypehint ); delete modMethods[methodIndex].returntypehint; }); json.methods = modMethods; // Use deep Object.assign so duplicate properties are overwritten. deepAssign(jsonJsDoc.methods, json.methods); } json = jsonJsDoc; } return componentsToMarkdown(type, json, filepath, componentCount++); } catch (e) { console.log("error in renderAPI for", filepath); throw e; } } function renderStyle(filepath) { const json = docgen.parse( fs.readFileSync(filepath), docgenHelpers.findExportedObject, [docgen.handlers.propTypeHandler, docgen.handlers.propDocBlockHandler] ); // Remove deprecated transform props from docs if (filepath === "../Libraries/StyleSheet/TransformPropTypes.js") { [ "rotation", "scaleX", "scaleY", "translateX", "translateY" ].forEach(function(key) { delete json.props[key]; }); } return componentsToMarkdown("style", json, filepath, componentCount++); } const all = docsList.components .concat(docsList.apis) .concat(docsList.stylesWithPermalink); const styleDocs = docsList.stylesForEmbed.reduce(function(docs, filepath) { docs[ path.basename(filepath).replace(path.extname(filepath), "") ] = docgen.parse( fs.readFileSync(filepath), docgenHelpers.findExportedObject, [ docgen.handlers.propTypeHandler, docgen.handlers.propTypeCompositionHandler, docgen.handlers.propDocBlockHandler ] ); return docs; }, {}); function extractDocs() { componentCount = 0; var components = docsList.components.map(renderComponent); var apis = docsList.apis.map(filepath => { return renderAPI(filepath, "api"); }); var styles = docsList.stylesWithPermalink.map(renderStyle); return [].concat(components, apis, styles); } module.exports = extractDocs;