-
Notifications
You must be signed in to change notification settings - Fork 2k
/
Copy pathgenerate-proptypes-index.js
executable file
·161 lines (138 loc) · 5.24 KB
/
generate-proptypes-index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
#!/usr/bin/env node
/**
* This file generates an index of proptypes by component displayname, slug and folder name
*/
const startTime = process.hrtime();
/**
* External Dependencies
*/
const fs = require( 'fs' );
const path = require( 'path' );
const reactDocgen = require( 'react-docgen' );
const { getPropertyName, getMemberValuePath, resolveToValue } = require( 'react-docgen/dist/utils' );
const _ = require( 'lodash' );
const globby = require( 'globby' );
const async = require( 'async' );
const root = path.dirname( path.join( __dirname, '..', '..' ) );
const handlers = [ ...reactDocgen.defaultHandlers, commentHandler ];
/**
* Replaces **'s in comment blocks and trims comments
* @param {string} str The doc string to clean
*/
function parseDocblock( str ) {
const lines = str.split( '\n' );
for ( let i = 0, l = lines.length; i < l; i++ ) {
lines[ i ] = lines[ i ].replace( /^\s*\*\s?/, '' );
}
return lines.join( '\n' ).trim();
}
/**
* Given a path, this function returns the closest preceding comment, if it exists
* @param {NodePath} path The node path from react-docgen
*/
function getComments( path ) {
let comments = [];
if ( path.node.leadingComments ) {
// if there are comments before this property node, use the ones leading, not following a previous node
comments = path.node.leadingComments.filter(
comment => comment.leading === true
);
} else if (path.node.comments) {
// if there are comments after this property node, use the ones following this node
comments = path.node.comments.filter(
comment => comment.leading === false
);
}
if ( comments.length > 0 ) {
return parseDocblock( comments[ comments.length - 1 ].value );
}
return null;
}
/**
* Handler for react-docgen to use in order to discover
* @param {Documentation} documentation The object to mutate that will eventually be passed back to us from parse()
* @param {NodePath} path The node we are handling
*/
function commentHandler(documentation, path) {
// retrieve the proptypes for this node, if they exist
let propTypesPath = getMemberValuePath(path, 'propTypes');
if (!propTypesPath) {
return;
}
// resolve a path to a value, if possible, will be ObjectExpression type if it can
propTypesPath = resolveToValue( propTypesPath );
if ( !propTypesPath || propTypesPath.value.type !== 'ObjectExpression' ) {
return;
}
// Iterate over all the properties in the proptypes object
propTypesPath.get( 'properties' ).each( ( propertyPath ) => {
// ensure that this node is a property
if ( propertyPath.value.type !== 'Property' ) {
return;
}
// get the prop name and description, ensuring that it either doesn't exist or is empty before continuing
const propName = getPropertyName(propertyPath);
const propDescriptor = documentation.getPropDescriptor(propName);
if ( propDescriptor.description && propDescriptor.description !== '' ) {
return;
}
// if we don't have anything, see if there are inline comments for this property
propDescriptor.description = getComments( propertyPath ) || '';
} );
}
/**
* Calculates a filepath's include path and begins reading the file for parsing
* Calls back with null, if an error occurs or an object if it succeeds
* @param {string} filePath The path to read
*/
const processFile = ( filePath, callback ) => {
const filename = path.basename( filePath );
const includePathRegEx = new RegExp(`^client/(.*?)/${ filename }$`);
const includePathSuffix = ( filename === 'index.jsx' ? '' : '/' + path.basename( filename, '.jsx' ) );
const includePath = includePathRegEx.exec( filePath )[1] + includePathSuffix;
const usePath = path.join( process.cwd(), filePath );
fs.readFile( usePath, { encoding: 'utf8' }, ( err, document ) => {
if ( err ) {
console.log(`Skipping ${ filePath } due to fs error: ${ err }`);
return callback( null, null );
}
if ( ! document.includes( "from 'react';" ) ) {
return callback( null, null ); // Not a React component, skip
}
try {
const parsed = reactDocgen.parse( document, undefined, handlers );
parsed.includePath = includePath;
if ( parsed.displayName ) {
parsed.slug = _.kebabCase( parsed.displayName );
} else {
// we have to figure it out -- use the directory name to get the slug
parsed.slug = path.basename( includePath );
parsed.displayName = _.capitalize( _.camelCase( parsed.slug ) );
}
return callback( null, parsed );
} catch ( error ) {
// skipping, probably because the file couldn't be parsed for many reasons (there are lots of them!)
return callback( null, null );
}
} );
};
/**
* Write the file
* @param {Object} contents The contents of the file
*/
const writeFile = ( contents ) => {
fs.writeFileSync( path.join( root, 'server/devdocs/proptypes-index.json' ), JSON.stringify( contents ) );
};
const main = ( () => {
console.log( 'Building: proptypes-index.json' );
const fileList = globby.sync( process.argv.slice( 2 ) );
if ( fileList.length === 0 ) {
process.stderr.write( 'You must pass a list of files to process' );
process.exit( 1 );
}
async.map( fileList, processFile, ( err, results ) => {
writeFile( results.filter( Boolean ) );
const elapsed = process.hrtime( startTime )[ 1 ] / 1000000;
console.log( `Time: ${ process.hrtime( startTime )[0] }s ${ elapsed.toFixed( 3 ) }ms` );
} );
} )();