From 4d031d1ac0482e2acafe5b83d45dd72defaa4b71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Bartas?= Date: Wed, 1 Mar 2017 13:29:45 +0100 Subject: [PATCH] Add typescript support (#99) * Add typescript support * Add typescript support for boolean and func * Add boolean support * Add custom react-docgen-typescript * Commit libs * Fix package.json * Fix componentsIndex * Add bin support * Update react-docgen * Add typings * Do not include tests folder * bump version * Add exclude option * Fix tests * Add exclude option description and bump version * Remove lib from git * Add lib to gitignore * Update README and bump version * Fix tests * Fix typo * Update README localStorage --- README.md | 17 +++++++++++- bin/bluekit.js | 37 +++++++++++++++++++++++++ circle.yml | 2 +- example/webpack.config.js | 2 +- index.d.ts | 10 +++++++ nunjucks/componentsIndex.nunjucks | 5 ++-- package.json | 8 +++++- src/app/StateProvider.react.js | 2 +- src/app/component/PropsTable.react.js | 10 +++++-- src/createBlueKit.js | 40 +++++++++++++++++++-------- src/libraries/buildProps.js | 6 +++- yarn.lock | 12 +++++++- 12 files changed, 128 insertions(+), 23 deletions(-) create mode 100644 bin/bluekit.js create mode 100644 index.d.ts diff --git a/README.md b/README.md index 758ba49..ba57a97 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,15 @@ DEMO here: [http://bluekit.blueberry.io](http://bluekit.blueberry.io) $ npm install --save react-bluekit ``` +You can use BlueKit via npm script or gulp + +## Npm script +```js +"scripts": { + "bluekit": "bluekit --baseDir ./components --paths . --exclude \"./(Layout|StyledComponent).tsx\"" +} +``` + ## Gulpfile configuration ```js @@ -24,6 +33,8 @@ createBlueKit({ baseDir: `${__dirname}/src/browser`, // relative paths from base dir where to look for components paths: ['./components/', './auth'], + // relative paths from base dir of files or directories you want to exclude from indexing + exclude: ['./components/Foo'], // set to true when providing simple components such as `export default function MyComponent() {
Hello
}` noSpecialReplacements: true }); @@ -75,6 +86,10 @@ You can also pass `children` to BlueKit, which will be displayed above the searc To add jsdoc **descriptions** see example [example_components/Checkbox.react.js](https://github.com/blueberryapps/react-bluekit/blob/master/example_components/Checkbox.react.js). +## Typescript support + +Bluekit automatically finds `.tsx` files and uses [react-docgen-typescript](https://github.com/imtoo/react-docgen-typescript) parser for it. + ## BlueKit development ``` npm install @@ -101,7 +116,7 @@ gulp eslint BlueKit automatically hides props that don’t affect the component’s look. -If you get some kind of weird error and BlueKit doesn't load at all, try to reset localStorage by running `localStorage.clear();`. We are working on automatic checks of localStorage values. +If you get some kind of weird error and BlueKit doesn't load at all, try to reset localStorage by running `localStorage.clear();`. ## License diff --git a/bin/bluekit.js b/bin/bluekit.js new file mode 100644 index 0000000..0e75dff --- /dev/null +++ b/bin/bluekit.js @@ -0,0 +1,37 @@ +#! /usr/bin/env node +const createBlueKit = require('../lib/createBlueKit').default; +const path = require('path'); + +const argv = require('yargs') + .usage('Usage: \nyarn bluekit -- --baseDir [./src/browser] --paths [./components/ ./auth]') + .help('h') + .alias('h', 'help') + + .describe('baseDir', 'your directory where components are located') + + .array('paths') + .describe('paths', 'relative paths from base dir where to look for components') + + .boolean('noSpecialReplacements') + .describe('noSpecialReplacements', 'set to true when providing simple components such as `export default function MyComponent() {
Hello
}`') + + .example('--baseDir ./src --paths ./components') + .demand(['baseDir', 'paths']) + + .array('exclude') + .describe('exclude', 'exclude files or directories from components listing`') + .argv; + +const baseDir = path.join(process.cwd(), argv.baseDir); + +const config = { + baseDir: baseDir, + paths: [].concat(argv.paths), + exclude: [].concat(argv.exclude), + noSpecialReplacements: argv.noSpecialReplacements, + gulp: { + task: function() { } + } +} + +createBlueKit(config)(); diff --git a/circle.yml b/circle.yml index 9f7b71d..bdc5261 100644 --- a/circle.yml +++ b/circle.yml @@ -18,4 +18,4 @@ test: - cd example && gulp: background: true - sleep 10 - - curl --retry 10 --retry-delay 5 -v http://localhost:8001/ + - curl --retry 10 --retry-delay 5 -v http://localhost:3000/ diff --git a/example/webpack.config.js b/example/webpack.config.js index 1338303..61e5a8a 100644 --- a/example/webpack.config.js +++ b/example/webpack.config.js @@ -4,7 +4,7 @@ const webpack = require('webpack'); module.exports = { devtool: 'eval', entry: [ - 'webpack-dev-server/client?http://localhost:8001', + 'webpack-dev-server/client?http://localhost:3000', 'webpack/hot/only-dev-server', './src/index' ], diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000..e932629 --- /dev/null +++ b/index.d.ts @@ -0,0 +1,10 @@ +import * as React from 'react'; + +interface BluekitProps { + componentsIndex: Object; + inline?: boolean; +} + +export default class Bluekit extends React.Component { + +} diff --git a/nunjucks/componentsIndex.nunjucks b/nunjucks/componentsIndex.nunjucks index f59afd8..430968f 100644 --- a/nunjucks/componentsIndex.nunjucks +++ b/nunjucks/componentsIndex.nunjucks @@ -5,9 +5,10 @@ import {{ component.name }} from '{{component.file | escapeJsString}}'; {%- endfor %} function dispatchEvent(data) { - if (typeof window !== 'undefined') + if (typeof window !== 'undefined') { + const BluekitEvent = require('react-bluekit/lib/libraries/BluekitEvent'); document.dispatchEvent(new BluekitEvent('functionTriggered', data)); - else + } else console.log('Bluekit received function triggered', data) } diff --git a/package.json b/package.json index 90f6b02..dba3414 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,12 @@ { "name": "react-bluekit", - "version": "0.3.2", + "version": "0.4.1", "description": "Visualisation and Playground generated from Components", "main": "lib/index.js", "jsnext:main": "src/index.js", + "bin": { + "bluekit": "bin/bluekit.js" + }, "scripts": { "build": "npm run build:lib", "build:lib": "babel src --out-dir lib", @@ -26,6 +29,7 @@ "es7", "babel", "react-component", + "typescript", "visualization", "playground", "development", @@ -35,6 +39,7 @@ "node" ], "author": "Ondrej Bartas", + "typings": "index", "license": "MIT", "bugs": { "url": "https://github.com/blueberryapps/react-bluekit/issues" @@ -82,6 +87,7 @@ "react-color": ">=2", "react-copy-to-clipboard": ">=4", "react-docgen": ">=2.4", + "react-docgen-typescript": "https://github.com/imtoo/react-docgen-typescript.git", "react-highlight": ">=0.5", "react-pure-render": ">=1", "react-responsive": "^1.1.3", diff --git a/src/app/StateProvider.react.js b/src/app/StateProvider.react.js index 38354d7..1ca9b9e 100644 --- a/src/app/StateProvider.react.js +++ b/src/app/StateProvider.react.js @@ -113,7 +113,7 @@ export default function StateProvider(Wrapped) { value = event.value // fix string to valid type - if (type === 'bool' && typeof value !== 'boolean') + if ((type === 'bool' || type === 'boolean') && typeof value !== 'boolean') value = event.target.checked else if (type === 'number') value = parseInt(value, 10) diff --git a/src/app/component/PropsTable.react.js b/src/app/component/PropsTable.react.js index dd8995e..3721380 100644 --- a/src/app/component/PropsTable.react.js +++ b/src/app/component/PropsTable.react.js @@ -93,7 +93,7 @@ export default class PropsTable extends Component { fullWidth && styles.prop.fullWidth, triggered && {backgroundColor: colors.GRAY_BRIGHT}]} > - {name === 'func' + {(name === 'func' || name.match(/\(.*\)\s*=>/)) ? 'func()' : this.renderValueSelection(key, data.get('type').toJS(), scope) } @@ -110,7 +110,7 @@ export default class PropsTable extends Component { renderNameOfProp(name, kind) { const {handlePropsNameClick} = this.props - if (['string', 'number', 'bool', 'enum'].indexOf(kind) === -1) + if (['string', 'number', 'bool', 'boolean', 'enum'].indexOf(kind) === -1) return name else return ( @@ -134,14 +134,18 @@ export default class PropsTable extends Component { value: fromJS(componentProps).getIn(scope.concat([key])) } - switch (type.name) { + switch (type.name.replace('React.', '')) { case 'any': return case 'array': return case 'arrayOf': return case 'bool': return + case 'boolean': return case 'element': return case 'enum' : return this.renderEnum(name, type, defaultProps) case 'node': return + case 'Children': return + case 'ReactNode': return + case 'ReactElement': return case 'number': return case 'object': return case 'shape': return diff --git a/src/createBlueKit.js b/src/createBlueKit.js index 11f0a5a..f86d9c9 100644 --- a/src/createBlueKit.js +++ b/src/createBlueKit.js @@ -10,7 +10,14 @@ import {parse as docgenParse} from 'react-docgen'; const nunjuckEnv = nunjucks.configure(`${__dirname}/../nunjucks/`, {autoescape: false}); nunjuckEnv.addFilter('escapeJsString', input => JSON.stringify(input).replace(/'/g, '\\\'').slice(1, -1)); -function getAllFilesInDir(dir, relativeDirectory = []) { +function isExcluded(filename, exclude) { + const relativeFilename = `./${filename}`; + return exclude.reduce((acc, item) => { + return relativeFilename.match(new RegExp(`^${item}`)) !== null || acc; + }, false); +} + +function getAllFilesInDir(dir, relativeDirectory = [], exclude = []) { const resolvedDir = path.join(dir, relativeDirectory); if (!fs.existsSync(resolvedDir)) return null @@ -19,13 +26,15 @@ function getAllFilesInDir(dir, relativeDirectory = []) { const absolutePath = path.join(dir, relativeDirectory, file); if (fs.lstatSync(absolutePath).isDirectory()) { - return getAllFilesInDir(dir, path.join(relativeDirectory, file)); + return getAllFilesInDir(dir, path.join(relativeDirectory, file), exclude); } const filePath = path.join(`./${relativeDirectory}`, file); - if (!filePath.match(/\.(js|jsx)$/)) + if (!filePath.match(/\.(js|jsx|tsx)$/)) + return null + if (filePath.match(/__tests?__/)) return null - if (filePath.match(/__test__/)) + if (isExcluded(filePath, exclude)) return null return filePath })); @@ -37,7 +46,7 @@ function objectToString(object) { function getImportFile(directory, file) { if (directory.match(/node_modules/)) { - const pathParts = file.replace(/\.(js|jsx)$/, '').split(path.sep) + const pathParts = file.replace(/\.(js|jsx|tsx)$/, '').split(path.sep) pathParts[1] = 'lib' return pathParts.join(path.sep) } @@ -45,8 +54,10 @@ function getImportFile(directory, file) { return file[0] === '.' ? file : `./${file}` } -function generateComponentData(config, file, directory) { - const filePath = path.join(directory, file); +function getDocgen(config, filePath) { + if (filePath.match(/\.tsx$/)) + return require('react-docgen-typescript').parse(filePath); + let content = fs.readFileSync(filePath).toString() if (!config.noSpecialReplacements) { content = content @@ -54,8 +65,15 @@ function generateComponentData(config, file, directory) { .replace(/import Component from ["']react-pure-render\/component["']/, 'import {Component} from "react"') .replace(/export default .*\((\w*)\)+/m, 'export default $1') } + return docgenParse(content); +} + +function generateComponentData(config, file, directory) { + const filePath = path.join(directory, file); + try { - const docgen = docgenParse(content); + const docgen = getDocgen(config, filePath); + const doc = { ...docgen, propsDefinition: objectToString(docgen.props) @@ -65,7 +83,7 @@ function generateComponentData(config, file, directory) { const menu = normalizedFile .replace(/\.\.\//g, '') .replace('.react', '') - .replace(/\.(js|jsx)$/, '') + .replace(/\.(js|jsx|tsx)$/, '') .replace(/(?:^|\/)(\w)/g, (_, c) => c ? ` ${c.toUpperCase()}` : '') .replace(/(?:^|[-_])(\w)/g, (_, c) => c ? `${c.toUpperCase()}` : '') .replace(/\//g, '') @@ -112,7 +130,7 @@ export default function createBlueKit(config) { const watch = function() { const watchPaths = config.paths.map(file => ( - path.relative(process.cwd(), path.join(config.baseDir, file, '**/*.{js,jsx}')) + path.relative(process.cwd(), path.join(config.baseDir, file, '**/*.{js,jsx,tsx}')) )); console.log('Watching BlueKit in and automatically rebuilding on paths:') // eslint-disable-line no-console @@ -131,7 +149,7 @@ export default function createBlueKit(config) { function generate() { const files = config.paths.map(file => ( - getAllFilesInDir(config.baseDir, file) + getAllFilesInDir(config.baseDir, file, config.exclude) )); const components = getValidFiles(files).map(file => { diff --git a/src/libraries/buildProps.js b/src/libraries/buildProps.js index 88140e6..9c38540 100644 --- a/src/libraries/buildProps.js +++ b/src/libraries/buildProps.js @@ -16,9 +16,12 @@ export default function buildProps(propsDefinition, allProps = false) { } function calculateProp(type, prop) { - switch (type.name) { + switch (`${type.name}`.replace('React.', '')) { case 'any': return `ANY ${prop}` case 'node': return `NODE ${prop}` + case 'Children': return `NODE ${prop}` + case 'ReactNode': return `NODE ${prop}` + case 'ReactElement': return `NODE ${prop}` case 'string': return `${prop}` case 'bool': return true case 'boolean': return true @@ -26,6 +29,7 @@ function calculateProp(type, prop) { case 'array': return [] case 'object': return {} case 'func': return eval(`[function () { dispatchEvent({detail: {prop: "${prop}"}}) }][0]`) // eslint-disable-line no-eval + case /\(.*\)\s*=>/: return eval(`[function () { dispatchEvent({detail: {prop: "${prop}"}}) }][0]`) // eslint-disable-line no-eval case 'enum': return (typeof type.value === 'string' ? '' : (type.value[0].value && type.value[0].value.replace(/'/g, ''))) || '' case 'shape': return Map(type.value) .map((subType, name) => calculateProp(subType, name)) diff --git a/yarn.lock b/yarn.lock index 5315527..9622929 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2701,7 +2701,7 @@ ignore@^3.1.5: version "3.2.0" resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.2.0.tgz#8d88f03c3002a0ac52114db25d2c673b0bf1e435" -immutable@3.8.1, immutable@^3.7.6: +immutable@^3.7.6, immutable@^3.8.1: version "3.8.1" resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.1.tgz#200807f11ab0f72710ea485542de088075f68cd2" @@ -4299,6 +4299,12 @@ react-deep-force-update@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/react-deep-force-update/-/react-deep-force-update-1.0.1.tgz#f911b5be1d2a6fe387507dd6e9a767aa2924b4c7" +"react-docgen-typescript@https://github.com/imtoo/react-docgen-typescript.git": + version "0.0.1-m01" + resolved "https://github.com/imtoo/react-docgen-typescript.git#b0b57c9f98cec31ab0e040251aa5a97bb15cdf63" + dependencies: + typescript "^1.8.10" + react-docgen@>=2.4: version "2.12.1" resolved "https://registry.yarnpkg.com/react-docgen/-/react-docgen-2.12.1.tgz#64fe750832967acbc9755f37c25d563d407c9149" @@ -5086,6 +5092,10 @@ typedarray@~0.0.5: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" +typescript@^1.8.10: + version "1.8.10" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-1.8.10.tgz#b475d6e0dff0bf50f296e5ca6ef9fbb5c7320f1e" + ua-parser-js@^0.7.9: version "0.7.10" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.10.tgz#917559ddcce07cbc09ece7d80495e4c268f4ef9f"