Skip to content

Add limited support for named exports. Add docs relating to this support. #825

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

Merged
merged 10 commits into from
Mar 21, 2018
58 changes: 58 additions & 0 deletions docs/Components.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<!-- toc -->

* [Finding components](#finding-components)
* [Loading and exposing components](#loading-and-exposing-components)
* [Sections](#sections)
* [Limitations](#limitations)

Expand Down Expand Up @@ -41,6 +42,63 @@ module.exports = {

> **Note:** Use [getComponentPathLine](Configuration.md#getcomponentpathline) option to change a path you see below a component name.

## Loading and exposing components

Styleguidist _loads_ your components and _exposes_ them globally for your examples to consume.

### Identifier
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be nice to make this section shorted and easier to scan. Maybe use one big table instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resorting to HTML table format here to play nicely with the code blocks. It makes this file a little ugly but the output is good.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be simpler:

| foo | bar |
| --- | --- | 
| `foo`<br>`bar` | bar |

Also you can remove the last column and use the same component name everywhere, and merge displayName and fallback columns since they are mutually exclusive.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yeah nice. Done.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow, it's like 10 times smaller now ;-)


It will try to use the `displayName` of your component as the identifier. If it cannot understand a `displayName` (for example if it is dynamically generated), it will fall back to something it can understand.

In each of the following cases, the global identifier will be `Component`.

| Path | Code | Styleguidist understands |
| ---- | ---- | ------------------------ |
| /whatever.js | `export default function Component() { ... }` | displayName |
| /whatever.js | `export default function SomeName() { ... }`<br>`SomeName.displayName = 'Component';` | displayName |
| /whatever.js | `export default function Component() { ... }`<br>`Component.displayName = dynamicNamer();` | displayName at declaration
| /component.js | `const name = 'SomeName';`<br>`const componentMap = {`<br>`[name]: function() { ... }`<br>`};`<br>`export default componentMap[name];` | File name |
| /component/index.js | `const name = 'SomeName';`<br>`const componentMap = {`<br>`[name]: function() { ... }`<br>`};`<br>`export default componentMap[name];` | Folder name |


### Default vs named exports

Stylegudist will use an ECMAScript module’s `default` export or CommonJS `module.exports` if they are defined.

```javascript
// /component.js
export default function Component() { ... }
// will be exposed globally as Component

// /component.js
function Component() { ... }
module.exports = Component;
// will be exposed globally as Component
```

If you use only named exports, Styleguidist will expose named exports from modules as follows...

If there is only one named export, it will expose that.

```javascript
// /component.js
export function Component() { ... }
// will be exposed globally as Component
```

If there are several named exports, it will expose the named export which has the same name as the understood identifier.

```javascript
// /component.js
export function someUtil() { ... }
// will not be exposed

export function Component() { ... }
// will be exposed globally as Component
```

If you export several React components as named exports from a single module, Styleguidist is likely to behave unreliably. If it cannot understand which named export to expose, you may not be able to access that export.

## Sections

Group components into sections or add extra Markdown documents to your style guide.
Expand Down
2 changes: 1 addition & 1 deletion docs/Thirdparties.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

## How Styleguidist works

Styleguidist always uses the default export to _load_ your components but it uses [react-docgen](https://github.com/reactjs/react-docgen) to _generate documentation_ which may require changes in your code to work properly.
Styleguidist _loads_ your components (see [Loading and exposing components](Components.md#loading-and-exposing-components) for more) but it uses [react-docgen](https://github.com/reactjs/react-docgen) to _generate documentation_ which may require changes in your code to work properly.

React-docgen reads your components as static text files and looks for patterns like class or function declarations that looks like React components. It does not run any JavaScript code, so, if your component is dynamically generated, is wrapped in a higher-order component, or is split into several files, then react-docgen may not understand it.

Expand Down
47 changes: 47 additions & 0 deletions src/utils/__tests__/getComponent.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import getComponent from '../getComponent';

describe('getComponent', () => {
describe('if there is a default export in the module', () => {
it('should return that', () => {
const module = { default: 'useMe' };
const actual = getComponent(module);
expect(actual).toBe(module.default);
});
});

describe('if it is a CommonJS module and exports a function', () => {
it('should return the module', () => {
const testCases = [() => {}, function() {}, class Class {}];
testCases.forEach(testCase => {
const actual = getComponent(testCase);
expect(actual).toBe(testCase);
});
});
});

describe('if there is only one named export in the module', () => {
it('should return that', () => {
const module = { oneNamedExport: 'isLonely' };
const actual = getComponent(module);
expect(actual).toBe(module.oneNamedExport);
});
});

describe('if there is a named export whose name matches the name argument', () => {
it('should return that', () => {
const name = 'Component';
const module = { [name]: 'isNamed', OtherComponent: 'isAlsoNamed' };
const actual = getComponent(module, name);
expect(actual).toBe(module[name]);
});
});

describe('if there is more than one named export and no matching name', () => {
it('should fall back on returning the module as a whole', () => {
const name = 'Component';
const module = { RandomName: 'isNamed', confusingExport: 123 };
const actual = getComponent(module, name);
expect(actual).toBe(module);
});
});
});
27 changes: 15 additions & 12 deletions src/utils/__tests__/globalizeComponent.spec.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import globalizeComponent from '../globalizeComponent';

afterEach(() => {
delete global.Foo;
});
const component = { module: 'someModule', name: 'SomeName' };

describe('globalizeComponent', () => {
it('should set component’s module as a global variable', () => {
const globalsCount = Object.keys(global).length;
globalizeComponent({
name: 'Foo',
props: {},
module: 13,
});
expect(Object.keys(global).length).toBe(globalsCount + 1);
expect(global.Foo).toBe(13);
afterEach(() => {
delete global[component.name];
});

it('should not add anything as a global variable if there is no component name', () => {
expect(global[component.name]).toBeUndefined();
globalizeComponent({});
expect(global[component.name]).toBeUndefined();
});

it('should set the return value of getComponent as a global variable', () => {
expect(global[component.name]).toBeUndefined();
globalizeComponent(component);
expect(global[component.name]).toBe(component.module);
});
});
60 changes: 60 additions & 0 deletions src/utils/getComponent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* Given a component module and a name,
* return the appropriate export.
* See /docs/Components.md
*
* @param {object} module
* @param {string} name
* @return {function|object}
*/
export default function getComponent(module, name) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess your understanding of this code is very good now, could you please add comments to each variation? Preferably with code examples. This is one of the most obscure parts of the code base ;-/

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! Please add a blank line before each comment to make it easier to read.

//
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had to leave this little 'comment' in here to stop Prettier complaining about the blank line...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I wasn't clear enough: blank lines to separate code “paragraphs”, not just empty line before each comment ;-)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So you mean changing this

	// If the module defines a default export, return that
	// e.g.
	// ```
	// export default function Component() { ... }
	// ```
	if (module.default) { ...

to this

	// If the module defines a default export, return that
	// e.g.
        //
	// ```
	// export default function Component() { ... }
	// ```
        //
	if (module.default) { ...

Or something else? :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly! Just to separate different sections of code.

// If the module defines a default export, return that
// e.g.
//
// ```
// export default function Component() { ... }
// ```
//
if (module.default) {
return module.default;
}

// If it is a CommonJS module which exports a function, return that
// e.g.
//
// ```
// function Component() { ... }
// module.exports = Component;
// ```
//
if (!module.__esModule && typeof module === 'function') {
return module;
}
const moduleKeys = Object.keys(module);

// If the module exports just one named export, return that
// e.g.
//
// ```
// export function Component() { ... }
// ```
//
if (moduleKeys.length === 1) {
return module[moduleKeys[0]];
}

// If the module exports a named export with the same name as the
// understood Component identifier, return that
// e.g.
//
// ```
// // /component.js
// export function someUtil() { ... }
// export Component() { ... } // if identifier is Component, return this named export
// ```
//
// Else return the module itself
//
return module[name] || module;
}
7 changes: 3 additions & 4 deletions src/utils/globalizeComponent.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import getComponent from './getComponent';

/**
* Expose component as global variables.
*
Expand All @@ -8,8 +10,5 @@ export default function globalizeComponent(component) {
return;
}

global[component.name] =
!component.props.path || component.props.path === 'default'
? component.module.default || component.module
: component.module[component.props.path];
global[component.name] = getComponent(component.module, component.name);
}