Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sort properties by 'required' and 'name' attributes #784

Merged
merged 11 commits into from
Mar 20, 2018
15 changes: 15 additions & 0 deletions docs/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ By default, Styleguidist will look for `styleguide.config.js` file in your proje
* [`template`](#template)
* [`theme`](#theme)
* [`title`](#title)
* [`transformProps`](#transformprops)
* [`updateExample`](#updateexample)
* [`verbose`](#verbose)
* [`webpackConfig`](#webpackconfig)
Expand Down Expand Up @@ -426,6 +427,20 @@ Type: `String`, default: `<app name from package.json> Style Guide`

Style guide title.

#### `transformProps`

Type: `Function`, optional

Function that transforms component properties. By default properties are sorted such that required properties come first, optional prameters come second. Properties in both groups are sorted by their property names.

To disable sorting the identity function can be used:

```javascript
module.exports = {
transformProps: props => props
}
```

#### `updateExample`

Type: `Function`, optional
Expand Down
2 changes: 1 addition & 1 deletion docs/Documenting.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export default class Button extends React.Component {

> **Note:** You can change its behavior using [propsParser](Configuration.md#propsparser) and [resolver](Configuration.md#resolver) options.

> **Note:** Component’s `PropTypes` and documentation comments are parsed by the [react-docgen](https://github.com/reactjs/react-docgen) library.
> **Note:** Component’s `PropTypes` and documentation comments are parsed by the [react-docgen](https://github.com/reactjs/react-docgen) library. They can be modified using the [transformProps](Configuration.md#transformprops) function.

## Usage examples and Readme files

Expand Down
63 changes: 63 additions & 0 deletions loaders/__tests__/props-loader.spec.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import vm from 'vm';
import { readFileSync } from 'fs';
import glogg from 'glogg';

import sortBy from 'lodash/sortBy';
import config from '../../scripts/schemas/config';
import propsLoader from '../props-loader';

Expand Down Expand Up @@ -44,6 +46,67 @@ it('should extract doclets', () => {
expect(result).toMatch(/require\('!!.*?\/loaders\/examples-loader\.js!\.\/examples.md'\)/);
});

describe('property sorting', () => {
it('should sort properties by default', () => {
const file = './test/components/Price/Price.js';
const result = propsLoader.call(
{
request: file,
_styleguidist,
},
readFileSync(file, 'utf8')
);
expect(result).toBeTruthy();

expect(() => new vm.Script(result)).not.toThrow();
expect(result.includes('makeABarrelRoll')).toBe(false);
expect(result).toMatch(
/props[\s\S]*?name': 'symbol'[\s\S]*?name': 'value'[\s\S]*?name': 'emphasize'[\s\S]*?name': 'unit'/m
);
});

it('should be possible to disable sorting', () => {
const file = './test/components/Price/Price.js';
const result = propsLoader.call(
{
request: file,
_styleguidist: { ..._styleguidist, propsTransform: props => props },
},
readFileSync(file, 'utf8')
);
expect(result).toBeTruthy();

expect(() => new vm.Script(result)).not.toThrow();
expect(result.includes('makeABarrelRoll')).toBe(false);
expect(result).toMatch(
/props[\s\S]*?name': 'value'[\s\S]*?name': 'unit'[\s\S]*?name': 'emphasize'[\s\S]*?name': 'symbol'/m
);
});

it('should be possible to write custom sort function', () => {
const sortFn = props => {
const requiredProps = sortBy(props.filter(prop => prop.required), 'name').reverse();
const optionalProps = sortBy(props.filter(prop => !prop.required), 'name').reverse();
return optionalProps.concat(requiredProps);
};
const file = './test/components/Price/Price.js';
const result = propsLoader.call(
{
request: file,
_styleguidist: { ..._styleguidist, propsTransform: sortFn },
},
readFileSync(file, 'utf8')
);
expect(result).toBeTruthy();

expect(() => new vm.Script(result)).not.toThrow();
expect(result.includes('makeABarrelRoll')).toBe(false);
expect(result).toMatch(
/props[\s\S]*?name': 'unit'[\s\S]*?name': 'emphasize'[\s\S]*?name': 'value'[\s\S]*?name': 'symbol'/m
);
});
});

it('should work with JSDoc annnotated components', () => {
const file = './test/components/Annotation/Annotation.js';
const result = propsLoader.call(
Expand Down
15 changes: 15 additions & 0 deletions loaders/props-loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const toAst = require('to-ast');
const logger = require('glogg')('rsg');
const getExamples = require('./utils/getExamples');
const getProps = require('./utils/getProps');
const sortProps = require('./utils/sortProps');
const consts = require('../scripts/consts');

const ERROR_MISSING_DEFINITION = 'No suitable component definition found.';
Expand All @@ -24,6 +25,7 @@ module.exports = function(source) {
const defaultParser = (filePath, source, resolver, handlers) =>
reactDocs.parse(source, resolver, handlers);
const propsParser = config.propsParser || defaultParser;
const propsTransform = config.propsTransform || sortProps;

let props = {};
try {
Expand Down Expand Up @@ -53,6 +55,19 @@ module.exports = function(source) {

props = getProps(props, file);

const componentProps = props.props;
if (componentProps) {
// Transform the properties to an array. This will allow for sorting.
const propsAsArray = Object.keys(componentProps).reduce((acc, name) => {
componentProps[name].name = name;
acc.push(componentProps[name]);
return acc;
}, []);

// Pipe through transform method and override component properties.
props.props = propsTransform(propsAsArray);
}

// Examples from Markdown file
const examplesFile = config.getExampleFilename(file);
props.examples = getExamples(examplesFile, props.displayName, config.defaultExample);
Expand Down
52 changes: 52 additions & 0 deletions loaders/utils/__tests__/__snapshots__/getSections.spec.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,19 @@ Array [
},
"slug": "placeholder-2",
},
Object {
"filepath": "components/Price/Price.js",
"hasExamples": true,
"metadata": Object {},
"module": Object {
"require": "~/test/components/Price/Price.js",
},
"pathLine": "components/Price/Price.js",
"props": Object {
"require": "!!~/loaders/props-loader.js!~/test/components/Price/Price.js",
},
"slug": "price-2",
},
Object {
"filepath": "components/RandomButton/RandomButton.js",
"hasExamples": true,
Expand Down Expand Up @@ -105,6 +118,19 @@ Array [
},
"slug": "placeholder-3",
},
Object {
"filepath": "components/Price/Price.js",
"hasExamples": true,
"metadata": Object {},
"module": Object {
"require": "~/test/components/Price/Price.js",
},
"pathLine": "components/Price/Price.js",
"props": Object {
"require": "!!~/loaders/props-loader.js!~/test/components/Price/Price.js",
},
"slug": "price-3",
},
Object {
"filepath": "components/RandomButton/RandomButton.js",
"hasExamples": true,
Expand Down Expand Up @@ -172,6 +198,19 @@ Object {
},
"slug": "placeholder",
},
Object {
"filepath": "components/Price/Price.js",
"hasExamples": true,
"metadata": Object {},
"module": Object {
"require": "~/test/components/Price/Price.js",
},
"pathLine": "components/Price/Price.js",
"props": Object {
"require": "!!~/loaders/props-loader.js!~/test/components/Price/Price.js",
},
"slug": "price",
},
Object {
"filepath": "components/RandomButton/RandomButton.js",
"hasExamples": true,
Expand Down Expand Up @@ -238,6 +277,19 @@ Object {
},
"slug": "placeholder-1",
},
Object {
"filepath": "components/Price/Price.js",
"hasExamples": true,
"metadata": Object {},
"module": Object {
"require": "~/test/components/Price/Price.js",
},
"pathLine": "components/Price/Price.js",
"props": Object {
"require": "!!~/loaders/props-loader.js!~/test/components/Price/Price.js",
},
"slug": "price-1",
},
Object {
"filepath": "components/RandomButton/RandomButton.js",
"hasExamples": true,
Expand Down
2 changes: 2 additions & 0 deletions loaders/utils/__tests__/getComponentFiles.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ it('getComponentFiles() should accept components as a glob', () => {
'~/components/Annotation/Annotation.js',
'~/components/Button/Button.js',
'~/components/Placeholder/Placeholder.js',
'~/components/Price/Price.js',
'~/components/RandomButton/RandomButton.js',
]);
});
Expand All @@ -39,6 +40,7 @@ it('getComponentFiles() should ignore specified patterns', () => {
expect(deabs(result)).toEqual([
'~/components/Annotation/Annotation.js',
'~/components/Placeholder/Placeholder.js',
'~/components/Price/Price.js',
]);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,14 @@ it('getComponentFilesFromSections() should return a list of files', () => {
expect(deabs(result)).toEqual([
'~/components/Button/Button.js',
'~/components/Placeholder/Placeholder.js',
'~/components/Price/Price.js',
]);
});

it('getComponentFilesFromSections() should ignore specified patterns', () => {
const result = getComponentFilesFromSections(sections, configDir, ['**/*Button*']);
expect(deabs(result)).toEqual(['~/components/Placeholder/Placeholder.js']);
expect(deabs(result)).toEqual([
'~/components/Placeholder/Placeholder.js',
'~/components/Price/Price.js',
]);
});
37 changes: 37 additions & 0 deletions loaders/utils/__tests__/sortProps.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import sortProps from '../sortProps';

function makeProp(name, required, defaultValue, type) {
// Default values.
required = required === undefined ? false : required;
type = type === undefined ? { name: 'string' } : type;

return {
name,
required,
defaultValue,
type,
};
}

it('should sort required props', () => {
const props = [makeProp('prop2', true), makeProp('prop1', true)];
const result = sortProps(props);
expect(result.map(prop => prop.name)).toEqual(['prop1', 'prop2']);
});

it('should sort optional props', () => {
const props = [makeProp('prop2', false), makeProp('prop1', false)];
const result = sortProps(props);
expect(result.map(prop => prop.name)).toEqual(['prop1', 'prop2']);
});

it('should sort mixed props (required props should come first)', () => {
const props = [
makeProp('prop2', false),
makeProp('prop1', true),
makeProp('prop3', true),
makeProp('prop4', false),
];
const result = sortProps(props);
expect(result.map(prop => prop.name)).toEqual(['prop1', 'prop3', 'prop2', 'prop4']);
});
19 changes: 19 additions & 0 deletions loaders/utils/sortProps.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
'use strict';

const sortBy = require('lodash/sortBy');

/**
* Sorts an array of properties by their 'required' property first and 'name'
* property second.
*
* @param {array} props
* @return {array} Sorted properties
*/
function sortProps(props) {
const requiredPropNames = sortBy(props.filter(prop => prop.required), 'name');
const optionalPropNames = sortBy(props.filter(prop => !prop.required), 'name');
const sortedProps = requiredPropNames.concat(optionalPropNames);
return sortedProps;
}

module.exports = sortProps;
3 changes: 3 additions & 0 deletions scripts/schemas/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,9 @@ module.exports = {
},
example: 'My Style Guide',
},
transformProps: {
type: 'function',
},
updateExample: {
type: 'function',
default: props => {
Expand Down
Loading