From 7cbd32f33f734904f0b023adf84e093dc1f36c25 Mon Sep 17 00:00:00 2001 From: gcanti Date: Sat, 16 Jan 2016 17:58:09 +0100 Subject: [PATCH] fix #24, fix #25 --- .babelrc | 5 ++ lib/parse.js | 95 ++++++++++++++++++++++++++ lib/toMarkdown.js | 17 +++++ package.json | 25 ++++--- test/fixtures/parse/1/Actual.js | 33 +++++++++ test/fixtures/parse/1/User.js | 6 ++ test/fixtures/parse/1/expected.json | 20 ++++++ test/fixtures/toMarkdown/1/expected.md | 9 +++ test/test.js | 44 ++++++++++++ 9 files changed, 246 insertions(+), 8 deletions(-) create mode 100644 .babelrc create mode 100644 lib/parse.js create mode 100644 lib/toMarkdown.js create mode 100644 test/fixtures/parse/1/Actual.js create mode 100644 test/fixtures/parse/1/User.js create mode 100644 test/fixtures/parse/1/expected.json create mode 100644 test/fixtures/toMarkdown/1/expected.md diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..4ff711a --- /dev/null +++ b/.babelrc @@ -0,0 +1,5 @@ +{ + stage: 0, + loose: true, + optional: ["runtime"] +} \ No newline at end of file diff --git a/lib/parse.js b/lib/parse.js new file mode 100644 index 0000000..663dfd6 --- /dev/null +++ b/lib/parse.js @@ -0,0 +1,95 @@ +var fs = require('fs'); +var t = require('../').t; +var toObject = require('tcomb-doc').toObject; +var parseComments = require('get-comments'); +var parseJSDocs = require('doctrine').parse; + +// (path: t.String) => { description: t.String, tags: Array } +function getComments(path) { + var source = fs.readFileSync(path, 'utf8'); + var comments = parseComments(source, true); + var values = comments.map(function (comment) { + return comment.value; + }); + return parseJSDocs(values.join('\n'), { unwrap: true }); +} + +// (comments: t.Object) => { component, props: {} } +function getDescriptions(comments) { + var ret = { + component: comments.description, + props: {} + }; + comments.tags.forEach(function (tag) { + if (tag.name) { + ret.props[tag.name] = tag.description; + } + }); + return ret; +} + +// (exports: t.Object) => ReactComponent +function getComponent(defaultExport) { + if (defaultExport['default']) { // eslint-disable-line dot-notation + defaultExport = defaultExport['default']; // eslint-disable-line dot-notation + } + return defaultExport.propTypes ? defaultExport : null; +} + +// (component: ReactComponent) => t.String +function getComponentName(component) { + return component.name; +} + +// (component: ReactComponent) => TcombType +function getPropsType(component) { + var propTypes = component.propTypes; + var props = {}; + Object.keys(propTypes).forEach(function (k) { + if (k !== '__strict__' && k !== '__subtype__') { + props[k] = propTypes[k].tcomb; + } + }); + if (propTypes.hasOwnProperty('__subtype__')) { + return t.refinement(t.struct(props), propTypes.__subtype__.predicate); + } + return t.struct(props); +} + +// (component: ReactComponent) => t.Object +function getDefaultProps(component) { + return component.defaultProps || {}; +} + +// (path: t.String) => { name, description, props } +function parse(path) { + if (t.Array.is(path)) { + return path.map(parse).filter(Boolean); + } + var component = getComponent(require(path)); + if (component) { + var comments = getComments(path); + var descriptions = getDescriptions(comments); + var type = toObject(getPropsType(component)); + var props = type.kind === 'refinement' ? type.type.props : type.props; + var defaultProps = getDefaultProps(component); + var name = getComponentName(component); + for (var prop in props) { + if (props.hasOwnProperty(prop)) { + if (defaultProps.hasOwnProperty(prop)) { + props[prop].defaultValue = defaultProps[prop]; + } + if (descriptions.props.hasOwnProperty(prop)) { + props[prop].description = descriptions.props[prop]; + } + } + } + return { + name: name, + description: descriptions.component, + props: props + }; + } +} + +module.exports = parse; diff --git a/lib/toMarkdown.js b/lib/toMarkdown.js new file mode 100644 index 0000000..26f0855 --- /dev/null +++ b/lib/toMarkdown.js @@ -0,0 +1,17 @@ +var t = require('../').t; + +function getProps(props) { + return Object.keys(props).map(function (k) { + var prop = props[k]; + return '- `' + k + ': ' + prop.name + '` ' + (prop.required ? '' : '(optional' + (t.Nil.is(prop.defaultValue) ? '' : ', default: `' + JSON.stringify(prop.defaultValue) + '`') + ')') + ' ' + ( prop.description || ''); + }).join('\n'); +} + +function toMarkdown(json) { + if (t.Array.is(json)) { + return json.map(toMarkdown).join('\n'); + } + return '# ' + json.name + '\n' + (json.description ? '\n' + json.description + '\n' : '') + '\n**Props**\n\n' + getProps(json.props) + '\n'; +} + +module.exports = toMarkdown; diff --git a/package.json b/package.json index 484db7e..7e2871e 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,15 @@ { "name": "tcomb-react", - "version": "0.8.11", + "version": "0.8.12", "description": "Type checking for React components", "main": "index.js", + "files": [ + "index.js", + "lib" + ], "scripts": { - "lint": "eslint index.js test", - "test": "npm run lint && npm run mocha", - "mocha": "mocha" + "lint": "eslint index.js lib test", + "test": "mocha --compilers js:babel/register" }, "repository": { "type": "git", @@ -19,14 +22,20 @@ }, "homepage": "https://github.com/gcanti/tcomb-react", "dependencies": { + "doctrine": "0.7.2", + "get-comments": "1.0.1", + "tcomb-doc": "^0.4.0", "tcomb-validation": "^2.2.0", "react": ">=0.13.0" }, "devDependencies": { - "babel-eslint": "^3.1.23", - "eslint": "^0.22.1", - "eslint-plugin-react": "^2.7.1", - "mocha": "^2.2.5" + "babel": "5.8.34", + "babel-core": "5.8.34", + "babel-runtime": "5.8.34", + "babel-eslint": "3.1.30", + "eslint": "0.22.1", + "eslint-plugin-react": "2.7.1", + "mocha": "2.3.2" }, "tags": [ "tcomb", diff --git a/test/fixtures/parse/1/Actual.js b/test/fixtures/parse/1/Actual.js new file mode 100644 index 0000000..c7896d0 --- /dev/null +++ b/test/fixtures/parse/1/Actual.js @@ -0,0 +1,33 @@ +import React from 'react' +import { props, t } from '../../../../.' +import User from './User' + +/** + * Component description here + * name and surname must be both nil or both specified + * @param name - name description + * @param surname - surname description + */ + +const Props = t.refinement(t.struct({ + name: t.maybe(User.meta.props.name), + surname: t.maybe(User.meta.props.surname) +}), (x) => t.Nil.is(x.name) === t.Nil.is(x.surname)) + +@props(Props) +export default class Card extends React.Component { + + static defaultProps = { + name: 'Giulio', + surname: 'Canti' + } + + render() { + return ( +
+

{this.props.name}

+

{this.props.surname}

+
+ ) + } +} \ No newline at end of file diff --git a/test/fixtures/parse/1/User.js b/test/fixtures/parse/1/User.js new file mode 100644 index 0000000..1c9c2fa --- /dev/null +++ b/test/fixtures/parse/1/User.js @@ -0,0 +1,6 @@ +import { t } from '../../../../.' + +export default t.struct({ + name: t.String, + surname: t.String +}, 'User') \ No newline at end of file diff --git a/test/fixtures/parse/1/expected.json b/test/fixtures/parse/1/expected.json new file mode 100644 index 0000000..fd28bc8 --- /dev/null +++ b/test/fixtures/parse/1/expected.json @@ -0,0 +1,20 @@ +{ + "name": "Card", + "description": "Component description here\nname and surname must be both nil or both specified", + "props": { + "name": { + "kind": "irreducible", + "name": "String", + "required": false, + "defaultValue": "Giulio", + "description": "name description" + }, + "surname": { + "kind": "irreducible", + "name": "String", + "required": false, + "defaultValue": "Canti", + "description": "surname description" + } + } +} \ No newline at end of file diff --git a/test/fixtures/toMarkdown/1/expected.md b/test/fixtures/toMarkdown/1/expected.md new file mode 100644 index 0000000..78acc92 --- /dev/null +++ b/test/fixtures/toMarkdown/1/expected.md @@ -0,0 +1,9 @@ +# Card + +Component description here +name and surname must be both nil or both specified + +**Props** + +- `name: String` (optional, default: `"Giulio"`) name description +- `surname: String` (optional, default: `"Canti"`) surname description \ No newline at end of file diff --git a/test/test.js b/test/test.js index 156818a..8ef5bf3 100644 --- a/test/test.js +++ b/test/test.js @@ -8,6 +8,10 @@ var library = require('../index'); var getPropTypes = library.propTypes; var ReactElement = library.ReactElement; var ReactNode = library.ReactNode; +var path = require('path'); +var fs = require('fs'); +var parse = require('../lib/parse'); +var toMarkdown = require('../lib/toMarkdown'); function throwsWithMessage(f, message) { assert.throws(f, function (err) { @@ -219,3 +223,43 @@ describe('pre-defined types', function () { }); }); + +var skipTests = { + '.DS_Store': 1 +}; + +describe('parse', function () { + var fixturesDir = path.join(__dirname, 'fixtures/parse'); + fs.readdirSync(fixturesDir).map(function (caseName) { + if ((caseName in skipTests)) { + return; + } + it(caseName, function () { + var fixtureDir = path.join(fixturesDir, caseName); + var filepath = path.join(fixtureDir, 'Actual.js'); + var expected = require(path.join(fixtureDir, 'expected.json')); + assert.deepEqual(JSON.stringify(parse(filepath)), JSON.stringify(expected)); + }); + }); +}); + +function trim(str) { + return str.replace(/^\s+|\s+$/, ''); +} + +describe('toMarkdown', function () { + var fixturesDir = path.join(__dirname, 'fixtures/toMarkdown'); + fs.readdirSync(fixturesDir).map(function (caseName) { + if ((caseName in skipTests)) { + return; + } + it(caseName, function () { + var actualFixtureDir = path.join(__dirname, 'fixtures/parse', caseName); + var filepath = path.join(actualFixtureDir, 'Actual.js'); + var fixtureDir = path.join(fixturesDir, caseName); + const expected = fs.readFileSync(path.join(fixtureDir, 'expected.md')).toString(); + assert.equal(trim(toMarkdown(parse(filepath))), trim(expected)); + }); + }); +}); +