mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
547 lines
15 KiB
JavaScript
547 lines
15 KiB
JavaScript
/**
|
|
* 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;
|