796 lines
25 KiB
JavaScript
Executable File
796 lines
25 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
if (process.env.MODULES_PATH) {
|
|
module.paths.push(process.env.MODULES_PATH);
|
|
}
|
|
let ts,
|
|
tsEnabled = true;
|
|
try {
|
|
ts = require('typescript');
|
|
} catch (e) {
|
|
ts = {};
|
|
tsEnabled = false;
|
|
}
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const reactDocs = require('react-docgen');
|
|
|
|
const args = process.argv.slice(2);
|
|
const src = args.slice(2);
|
|
const ignorePattern = args[0] ? new RegExp(args[0]) : null;
|
|
const reservedPatterns = args[1]
|
|
? args[1].split('|').map(part => new RegExp(part))
|
|
: [];
|
|
|
|
let tsconfig = {};
|
|
|
|
function help() {
|
|
console.error('usage: ');
|
|
console.error(
|
|
'extract-meta ^fileIgnorePattern ^forbidden$|^props$|^patterns$' +
|
|
' path/to/component(s) [path/to/more/component(s) ...] > metadata.json'
|
|
);
|
|
}
|
|
|
|
if (!src.length) {
|
|
help();
|
|
process.exit(1);
|
|
}
|
|
|
|
if (fs.existsSync('tsconfig.json')) {
|
|
tsconfig = JSON.parse(fs.readFileSync('tsconfig.json')).compilerOptions;
|
|
// Map moduleResolution to the appropriate enum.
|
|
switch (tsconfig.moduleResolution) {
|
|
case 'node':
|
|
tsconfig.moduleResolution = ts.ModuleResolutionKind.NodeJs;
|
|
break;
|
|
case 'node16':
|
|
tsconfig.moduleResolution = ts.ModuleResolutionKind.Node16;
|
|
break;
|
|
case 'nodenext':
|
|
tsconfig.moduleResolution = ts.ModuleResolutionKind.NodeNext;
|
|
break;
|
|
case 'classic':
|
|
tsconfig.moduleResolution = ts.ModuleResolutionKind.Classic;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
let failedBuild = false;
|
|
const excludedDocProps = ['setProps', 'id', 'className', 'style'];
|
|
|
|
const isOptional = prop => (prop.getFlags() & ts.SymbolFlags.Optional) !== 0;
|
|
|
|
const PRIMITIVES = [
|
|
'string',
|
|
'number',
|
|
'bool',
|
|
'any',
|
|
'array',
|
|
'object',
|
|
'node'
|
|
];
|
|
|
|
// These types take too long to parse because of heavy nesting.
|
|
const BANNED_TYPES = [
|
|
'Document',
|
|
'ShadowRoot',
|
|
'ChildNode',
|
|
'ParentNode',
|
|
];
|
|
const unionSupport = PRIMITIVES.concat('boolean', 'Element');
|
|
|
|
const reArray = new RegExp(`(${unionSupport.join('|')})\\[\\]`);
|
|
|
|
const isArray = rawType => reArray.test(rawType);
|
|
|
|
const isUnionLiteral = typeObj =>
|
|
typeObj.types.every(
|
|
t =>
|
|
t.getFlags() &
|
|
(ts.TypeFlags.StringLiteral |
|
|
ts.TypeFlags.NumberLiteral |
|
|
ts.TypeFlags.EnumLiteral |
|
|
ts.TypeFlags.Undefined)
|
|
);
|
|
|
|
function logError(error, filePath) {
|
|
if (filePath) {
|
|
process.stderr.write(`Error with path ${filePath}`);
|
|
}
|
|
process.stderr.write(error + '\n');
|
|
if (error instanceof Error) {
|
|
process.stderr.write(error.stack + '\n');
|
|
}
|
|
}
|
|
|
|
function isReservedPropName(propName) {
|
|
reservedPatterns.forEach(reservedPattern => {
|
|
if (reservedPattern.test(propName)) {
|
|
process.stderr.write(
|
|
`\nERROR: "${propName}" matches reserved word ` +
|
|
`pattern: ${reservedPattern.toString()}\n`
|
|
);
|
|
failedBuild = true;
|
|
}
|
|
});
|
|
return failedBuild;
|
|
}
|
|
|
|
function checkDocstring(name, value) {
|
|
if (
|
|
!value ||
|
|
(value.length < 1 && !excludedDocProps.includes(name.split('.').pop()))
|
|
) {
|
|
logError(`\nDescription for ${name} is missing!`);
|
|
}
|
|
}
|
|
|
|
function docstringWarning(doc) {
|
|
checkDocstring(doc.displayName, doc.description);
|
|
|
|
Object.entries(doc.props || {}).forEach(([name, p]) =>
|
|
checkDocstring(`${doc.displayName}.${name}`, p.description)
|
|
);
|
|
}
|
|
|
|
function zipArrays(...arrays) {
|
|
const arr = [];
|
|
for (let i = 0; i <= arrays[0].length - 1; i++) {
|
|
arr.push(arrays.map(a => a[i]));
|
|
}
|
|
return arr;
|
|
}
|
|
|
|
function cleanPath(filepath) {
|
|
return filepath.split(path.sep).join('/');
|
|
}
|
|
|
|
function parseJSX(filepath) {
|
|
try {
|
|
const src = fs.readFileSync(filepath);
|
|
const doc = reactDocs.parse(src);
|
|
Object.keys(doc.props).forEach(propName =>
|
|
isReservedPropName(propName)
|
|
);
|
|
docstringWarning(doc);
|
|
return doc;
|
|
} catch (error) {
|
|
logError(error);
|
|
}
|
|
}
|
|
|
|
function gatherComponents(sources, components = {}) {
|
|
const names = [];
|
|
const filepaths = [];
|
|
|
|
const gather = filepath => {
|
|
if (ignorePattern && ignorePattern.test(filepath)) {
|
|
return;
|
|
}
|
|
const extension = path.extname(filepath);
|
|
if (['.jsx', '.js'].includes(extension)) {
|
|
components[cleanPath(filepath)] = parseJSX(filepath);
|
|
} else if (filepath.endsWith('.tsx')) {
|
|
try {
|
|
const name = /(.*)\.tsx/.exec(path.basename(filepath))[1];
|
|
filepaths.push(filepath);
|
|
names.push(name);
|
|
} catch (err) {
|
|
process.stderr.write(
|
|
`ERROR: Invalid component file ${filepath}: ${err}`
|
|
);
|
|
}
|
|
}
|
|
};
|
|
|
|
sources.forEach(sourcePath => {
|
|
if (fs.lstatSync(sourcePath).isDirectory()) {
|
|
fs.readdirSync(sourcePath).forEach(f => {
|
|
const filepath = path.join(sourcePath, f);
|
|
if (fs.lstatSync(filepath).isDirectory()) {
|
|
gatherComponents([filepath], components);
|
|
} else {
|
|
gather(filepath);
|
|
}
|
|
});
|
|
} else {
|
|
gather(sourcePath);
|
|
}
|
|
});
|
|
|
|
if (!tsEnabled) {
|
|
return components;
|
|
}
|
|
|
|
const program = ts.createProgram(filepaths, {...tsconfig, esModuleInterop: true});
|
|
const checker = program.getTypeChecker();
|
|
|
|
const coerceValue = t => {
|
|
// May need to improve for shaped/list literals.
|
|
if (t.isStringLiteral()) return `'${t.value}'`;
|
|
return t.value;
|
|
};
|
|
|
|
const getComponentFromExport = exp => {
|
|
const decl = exp.valueDeclaration || exp.declarations[0];
|
|
const type = checker.getTypeOfSymbolAtLocation(exp, decl);
|
|
const typeSymbol = type.symbol || type.aliasSymbol;
|
|
|
|
if (!typeSymbol) {
|
|
return exp;
|
|
}
|
|
|
|
const symbolName = typeSymbol.getName();
|
|
|
|
if (
|
|
(symbolName === 'MemoExoticComponent' ||
|
|
symbolName === 'ForwardRefExoticComponent') &&
|
|
exp.valueDeclaration &&
|
|
ts.isExportAssignment(exp.valueDeclaration) &&
|
|
ts.isCallExpression(exp.valueDeclaration.expression)
|
|
) {
|
|
const component = checker.getSymbolAtLocation(
|
|
exp.valueDeclaration.expression.arguments[0]
|
|
);
|
|
|
|
if (component) return component;
|
|
}
|
|
return exp;
|
|
};
|
|
|
|
const getParent = node => {
|
|
let parent = node;
|
|
while (parent.parent) {
|
|
if (parent.parent.kind === ts.SyntaxKind.SourceFile) {
|
|
// We want the parent before the source file.
|
|
break;
|
|
}
|
|
parent = parent.parent;
|
|
}
|
|
return parent;
|
|
};
|
|
|
|
const getEnum = typeObj => ({
|
|
name: 'enum',
|
|
value: typeObj.types.map(t => ({
|
|
value: coerceValue(t),
|
|
computed: false
|
|
}))
|
|
});
|
|
|
|
const getUnion = (typeObj, propObj, parentType) => {
|
|
let name = 'union',
|
|
value;
|
|
|
|
// Union only do base types
|
|
value = typeObj.types
|
|
.filter(t => {
|
|
let typeName = t.intrinsicName;
|
|
if (!typeName) {
|
|
if (t.members) {
|
|
typeName = 'object';
|
|
}
|
|
}
|
|
return (
|
|
unionSupport.includes(typeName) ||
|
|
isArray(checker.typeToString(t))
|
|
);
|
|
})
|
|
.map(t => getPropType(t, propObj, parentType));
|
|
|
|
if (!value.length) {
|
|
name = 'any';
|
|
value = undefined;
|
|
}
|
|
return {
|
|
name,
|
|
value
|
|
};
|
|
};
|
|
|
|
const getPropTypeName = propName => {
|
|
if (propName.includes('=>') || propName === 'Function') {
|
|
return 'func';
|
|
} else if (propName === 'boolean') {
|
|
return 'bool';
|
|
} else if (propName === '[]') {
|
|
return 'array';
|
|
} else if (
|
|
propName === 'Element' ||
|
|
propName === 'ReactNode' ||
|
|
propName === 'ReactElement'
|
|
) {
|
|
return 'node';
|
|
}
|
|
return propName;
|
|
};
|
|
|
|
const getPropType = (propType, propObj, parentType = null) => {
|
|
// Types can get namespace prefixes or not.
|
|
let name = checker.typeToString(propType).replace(/^React\./, '');
|
|
let value, elements;
|
|
const raw = name;
|
|
|
|
const newParentType = (parentType || []).concat(raw)
|
|
|
|
if (propType.isUnion()) {
|
|
if (isUnionLiteral(propType)) {
|
|
return {...getEnum(propType), raw};
|
|
} else if (raw.includes('|')) {
|
|
return {...getUnion(propType, propObj, newParentType), raw};
|
|
}
|
|
}
|
|
|
|
name = getPropTypeName(name);
|
|
|
|
// Shapes & array support.
|
|
if (!PRIMITIVES.concat('enum', 'func', 'union').includes(name)) {
|
|
if (
|
|
// Excluding object with arrays in the raw.
|
|
(name.includes('[]') && name.endsWith("]")) ||
|
|
name.includes('Array')
|
|
) {
|
|
name = 'arrayOf';
|
|
const replaced = raw.replace('[]', '');
|
|
if (unionSupport.includes(replaced)) {
|
|
// Simple types are easier.
|
|
value = {
|
|
name: getPropTypeName(replaced),
|
|
raw: replaced
|
|
};
|
|
} else {
|
|
// Complex types get the type parameter (Array<type>)
|
|
const [nodeType] = checker.getTypeArguments(propType);
|
|
|
|
if (nodeType) {
|
|
value = getPropType(
|
|
nodeType, propObj, newParentType,
|
|
);
|
|
} else {
|
|
// Not sure, might be unsupported here.
|
|
name = 'array';
|
|
}
|
|
}
|
|
} else if (
|
|
name === 'tuple' ||
|
|
(name.startsWith('[') && name.endsWith(']'))
|
|
) {
|
|
name = 'tuple';
|
|
elements = propType.resolvedTypeArguments.map(
|
|
t => getPropType(t, propObj, newParentType)
|
|
);
|
|
} else if (
|
|
BANNED_TYPES.includes(name) ||
|
|
(parentType && parentType.includes(name))
|
|
) {
|
|
console.error(`Warning nested type: ${name}`);
|
|
name = 'any';
|
|
} else {
|
|
name = 'shape';
|
|
// If the type is declared as union it will have a types attribute.
|
|
if (propType.types && propType.types.length) {
|
|
if (isUnionLiteral(propType)) {
|
|
return {...getEnum(propType), raw};
|
|
}
|
|
return {
|
|
...getUnion(propType, propObj, newParentType),
|
|
raw
|
|
};
|
|
} else if (propType.indexInfos && propType.indexInfos.length) {
|
|
const {type} = propType.indexInfos[0];
|
|
name = 'objectOf';
|
|
value = getPropType(type, propObj, newParentType);
|
|
} else {
|
|
value = getProps(
|
|
checker.getPropertiesOfType(propType),
|
|
propObj,
|
|
[],
|
|
{},
|
|
true,
|
|
newParentType,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
name,
|
|
value,
|
|
elements,
|
|
raw
|
|
};
|
|
};
|
|
|
|
const getDefaultProps = (symbol, source) => {
|
|
const statements = source.statements.filter(
|
|
stmt =>
|
|
(!!stmt.name &&
|
|
checker.getSymbolAtLocation(stmt.name) === symbol) ||
|
|
ts.isExpressionStatement(stmt) ||
|
|
ts.isVariableStatement(stmt)
|
|
);
|
|
return statements.reduce((acc, statement) => {
|
|
let propMap = {};
|
|
|
|
statement.getChildren().forEach(child => {
|
|
let {right} = child;
|
|
if (right && ts.isIdentifier(right)) {
|
|
const value = source.locals.get(right.escapedText);
|
|
if (
|
|
value &&
|
|
value.valueDeclaration &&
|
|
ts.isVariableDeclaration(value.valueDeclaration) &&
|
|
value.valueDeclaration.initializer
|
|
) {
|
|
right = value.valueDeclaration.initializer;
|
|
}
|
|
}
|
|
if (right) {
|
|
const {properties} = right;
|
|
if (properties) {
|
|
propMap = getDefaultPropsValues(properties);
|
|
}
|
|
}
|
|
});
|
|
|
|
return {
|
|
...acc,
|
|
...propMap
|
|
};
|
|
}, {});
|
|
};
|
|
|
|
const getPropComment = symbol => {
|
|
// Doesn't work too good with the JsDocTags losing indentation.
|
|
// But used only in props should be fine.
|
|
const comment = symbol.getDocumentationComment();
|
|
const tags = symbol.getJsDocTags();
|
|
if (comment && comment.length) {
|
|
return comment
|
|
.map(c => c.text)
|
|
.concat(
|
|
tags.map(t =>
|
|
['@', t.name].concat((t.text || []).map(e => e.text))
|
|
)
|
|
)
|
|
.join('\n');
|
|
}
|
|
return '';
|
|
};
|
|
|
|
const getPropsForFunctionalComponent = type => {
|
|
const callSignatures = type.getCallSignatures();
|
|
|
|
for (const sig of callSignatures) {
|
|
const params = sig.getParameters();
|
|
if (params.length === 0) {
|
|
continue;
|
|
}
|
|
|
|
// There is only one parameter for functional components: props
|
|
const p = params[0];
|
|
if (p.name === 'props' || params.length === 1) {
|
|
return p;
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
|
|
const getPropsForClassComponent = (typeSymbol, source, defaultProps) => {
|
|
const childs = source.getChildAt(0);
|
|
let stop;
|
|
|
|
for (let i = 0, n = childs.getChildCount(); i < n && !stop; i++) {
|
|
const c = childs.getChildAt(i);
|
|
if (!ts.isClassDeclaration(c)) continue;
|
|
|
|
if (!c.heritageClauses) continue;
|
|
|
|
for (const clause of c.heritageClauses) {
|
|
if (clause.token !== ts.SyntaxKind.ExtendsKeyword) continue;
|
|
const t = clause.types[0];
|
|
const propType = t.typeArguments[0];
|
|
|
|
const type = checker.getTypeFromTypeNode(propType);
|
|
|
|
return getProps(
|
|
type.getProperties(),
|
|
typeSymbol,
|
|
[],
|
|
defaultProps
|
|
);
|
|
}
|
|
}
|
|
};
|
|
|
|
const getDefaultPropsValues = properties =>
|
|
properties.reduce((acc, p) => {
|
|
if (!p.name || !p.initializer) {
|
|
return acc;
|
|
}
|
|
let propName, value;
|
|
|
|
switch (p.name.kind) {
|
|
case ts.SyntaxKind.NumericLiteral:
|
|
case ts.SyntaxKind.StringLiteral:
|
|
case ts.SyntaxKind.Identifier:
|
|
propName = p.name.text;
|
|
break;
|
|
case ts.SyntaxKind.ComputedPropertyName:
|
|
propName = p.name.getText();
|
|
break;
|
|
}
|
|
|
|
const {initializer} = p;
|
|
|
|
switch (initializer.kind) {
|
|
case ts.SyntaxKind.StringLiteral:
|
|
value = `'${initializer.text}'`;
|
|
break;
|
|
case ts.SyntaxKind.NumericLiteral:
|
|
value = initializer.text;
|
|
break;
|
|
case ts.SyntaxKind.NullKeyword:
|
|
value = 'null';
|
|
break;
|
|
case ts.SyntaxKind.FalseKeyword:
|
|
value = 'false';
|
|
break;
|
|
case ts.SyntaxKind.TrueKeyword:
|
|
value = 'true';
|
|
break;
|
|
default:
|
|
try {
|
|
value = initializer.getText();
|
|
} catch (e) {
|
|
value = undefined;
|
|
}
|
|
}
|
|
|
|
acc[propName] = {value, computed: false};
|
|
|
|
return acc;
|
|
}, {});
|
|
|
|
const getDefaultPropsForClassComponent = (type, source) => {
|
|
// For class component, the type has its own property, then get the
|
|
// first declaration and one of them will be either
|
|
// an ObjectLiteralExpression or an Identifier which get in the
|
|
// newChild with the proper props.
|
|
const defaultProps = type.getProperty('defaultProps');
|
|
if (!defaultProps) {
|
|
return {};
|
|
}
|
|
const decl = defaultProps.getDeclarations()[0];
|
|
let propValues = {};
|
|
|
|
decl.getChildren().forEach(child => {
|
|
let newChild = child;
|
|
|
|
if (ts.isIdentifier(child)) {
|
|
// There should be two identifier, the first is ignored.
|
|
const value = source.locals.get(child.escapedText);
|
|
if (
|
|
value &&
|
|
value.valueDeclaration &&
|
|
ts.isVariableDeclaration(value.valueDeclaration) &&
|
|
value.valueDeclaration.initializer
|
|
) {
|
|
newChild = value.valueDeclaration.initializer;
|
|
}
|
|
}
|
|
|
|
const {properties} = newChild;
|
|
if (properties) {
|
|
propValues = getDefaultPropsValues(properties);
|
|
}
|
|
});
|
|
return propValues;
|
|
};
|
|
|
|
const getProps = (
|
|
properties,
|
|
propsObj,
|
|
baseProps = [],
|
|
defaultProps = {},
|
|
flat = false,
|
|
parentType = null,
|
|
) => {
|
|
const results = {};
|
|
|
|
properties.forEach(prop => {
|
|
const name = prop.getName();
|
|
if (isReservedPropName(name)) {
|
|
return;
|
|
}
|
|
const propType = checker.getTypeOfSymbolAtLocation(
|
|
prop,
|
|
propsObj.valueDeclaration
|
|
);
|
|
const baseProp = baseProps.find(p => p.getName() === name);
|
|
const defaultValue = defaultProps[name];
|
|
|
|
const required =
|
|
!isOptional(prop) &&
|
|
(!baseProp || !isOptional(baseProp)) &&
|
|
defaultValue === undefined;
|
|
|
|
const description = getPropComment(prop);
|
|
|
|
let result = {
|
|
description,
|
|
required,
|
|
defaultValue
|
|
};
|
|
const type = getPropType(propType, propsObj, parentType);
|
|
// root object is inserted as type,
|
|
// otherwise it's flat in the value prop.
|
|
if (!flat) {
|
|
result.type = type;
|
|
} else {
|
|
result = {...result, ...type};
|
|
}
|
|
|
|
results[name] = result;
|
|
});
|
|
|
|
return results;
|
|
};
|
|
|
|
const getPropInfo = (propsObj, defaultProps) => {
|
|
const propsType = checker.getTypeOfSymbolAtLocation(
|
|
propsObj,
|
|
propsObj.valueDeclaration
|
|
);
|
|
const baseProps = propsType.getApparentProperties();
|
|
let propertiesOfProps = baseProps;
|
|
|
|
if (propsType.isUnionOrIntersection()) {
|
|
propertiesOfProps = [
|
|
...checker.getAllPossiblePropertiesOfTypes(propsType.types),
|
|
...baseProps
|
|
];
|
|
|
|
if (!propertiesOfProps.length) {
|
|
const subTypes = checker.getAllPossiblePropertiesOfTypes(
|
|
propsType.types.reduce(
|
|
(all, t) => [...all, ...(t.types || [])],
|
|
[]
|
|
)
|
|
);
|
|
propertiesOfProps = [...subTypes, ...baseProps];
|
|
}
|
|
}
|
|
|
|
return getProps(propertiesOfProps, propsObj, baseProps, defaultProps);
|
|
};
|
|
|
|
zipArrays(filepaths, names).forEach(([filepath, name]) => {
|
|
const source = program.getSourceFile(filepath);
|
|
const moduleSymbol = checker.getSymbolAtLocation(source);
|
|
const exports = checker.getExportsOfModule(moduleSymbol);
|
|
|
|
exports.forEach(exp => {
|
|
let rootExp = getComponentFromExport(exp);
|
|
const declaration =
|
|
rootExp.valueDeclaration || rootExp.declarations[0];
|
|
const type = checker.getTypeOfSymbolAtLocation(
|
|
rootExp,
|
|
declaration
|
|
);
|
|
|
|
let commentSource = rootExp;
|
|
const typeSymbol = type.symbol || type.aliasSymbol;
|
|
const originalName = rootExp.getName();
|
|
|
|
if (!rootExp.valueDeclaration) {
|
|
if (
|
|
originalName === 'default' &&
|
|
!typeSymbol &&
|
|
(rootExp.flags & ts.SymbolFlags.Alias) !== 0
|
|
) {
|
|
// Some type of Exotic?
|
|
commentSource =
|
|
checker.getAliasedSymbol(
|
|
commentSource
|
|
).valueDeclaration;
|
|
} else if (!typeSymbol) {
|
|
// Invalid component
|
|
return null;
|
|
} else {
|
|
// Function components.
|
|
rootExp = typeSymbol;
|
|
commentSource = rootExp.valueDeclaration || rootExp.declarations[0];
|
|
if (
|
|
commentSource &&
|
|
commentSource.parent
|
|
) {
|
|
// Function with export later like `const MyComponent = (props) => <></>;`
|
|
commentSource = getParent(
|
|
commentSource.parent
|
|
);
|
|
}
|
|
}
|
|
} else if (
|
|
type.symbol &&
|
|
(ts.isPropertyAccessExpression(declaration) ||
|
|
ts.isPropertyDeclaration(declaration))
|
|
) {
|
|
commentSource = type.symbol.declarations[0];
|
|
}
|
|
|
|
if (commentSource.valueDeclaration) {
|
|
commentSource = commentSource.valueDeclaration; // class components
|
|
if (
|
|
commentSource.parent &&
|
|
commentSource.parent.kind !== ts.SyntaxKind.SourceFile
|
|
) {
|
|
// Memo components
|
|
commentSource = commentSource.parent;
|
|
}
|
|
}
|
|
|
|
let defaultProps = getDefaultProps(typeSymbol, source);
|
|
const propsType = getPropsForFunctionalComponent(type);
|
|
const isContext = !!type.getProperty('isContext');
|
|
|
|
let props;
|
|
|
|
if (propsType) {
|
|
props = getPropInfo(propsType, defaultProps);
|
|
} else {
|
|
defaultProps = getDefaultPropsForClassComponent(type, source);
|
|
props = getPropsForClassComponent(
|
|
typeSymbol,
|
|
source,
|
|
defaultProps
|
|
);
|
|
}
|
|
|
|
if (!props) {
|
|
// Ensure empty components has props.
|
|
props = {};
|
|
}
|
|
|
|
const fullText = source.getFullText();
|
|
let description = '';
|
|
const commentRanges = ts.getLeadingCommentRanges(
|
|
fullText,
|
|
commentSource.getFullStart()
|
|
);
|
|
if (commentRanges && commentRanges.length) {
|
|
description = commentRanges
|
|
.map(r =>
|
|
fullText
|
|
.slice(r.pos + 4, r.end - 3)
|
|
.split('\n')
|
|
.map(s => s.slice(3, s.length))
|
|
.filter(e => e)
|
|
.join('\n')
|
|
)
|
|
.join('');
|
|
}
|
|
const doc = {
|
|
displayName: name,
|
|
description,
|
|
props,
|
|
isContext
|
|
};
|
|
docstringWarning(doc);
|
|
components[cleanPath(filepath)] = doc;
|
|
});
|
|
});
|
|
|
|
return components;
|
|
}
|
|
|
|
const metadata = gatherComponents(Array.isArray(src) ? src : [src]);
|
|
if (!failedBuild) {
|
|
process.stdout.write(JSON.stringify(metadata, null, 2));
|
|
} else {
|
|
logError('extract-meta failed');
|
|
process.exit(1);
|
|
}
|