Skip to content

Commit

Permalink
Initial CSF support
Browse files Browse the repository at this point in the history
  • Loading branch information
sapegin committed Jan 19, 2021
1 parent f411ed4 commit 995f5e3
Show file tree
Hide file tree
Showing 20 changed files with 340 additions and 38 deletions.
11 changes: 8 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
"terser-webpack-plugin": "^4.2.3",
"to-ast": "^1.0.0",
"type-detect": "^4.0.8",
"unindent": "^2.0.0",
"unist-util-visit": "^2.0.0",
"webpack-dev-server": "^3.11.0",
"webpack-merge": "^4.2.2"
Expand Down Expand Up @@ -163,7 +164,7 @@
"strip-shebang": "^1.0.2",
"style-loader": "^1.0.0",
"tapable": "^1.1.3",
"typescript": "^3.8.3",
"typescript": "^3.9.0",
"url-loader": "^2.2.0",
"webpack": "^4.44.2",
"webpack-cli": "^3.3.9"
Expand Down
10 changes: 2 additions & 8 deletions src/client/rsg-components/Arguments/ArgumentsRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export const ArgumentsRenderer: React.FunctionComponent<ArgumentsProps> = ({
<Heading level={5}>Arguments</Heading>
</div>
)}
{args.map(arg => (
{args.map((arg) => (
<Argument key={arg.name} {...arg} />
))}
</div>
Expand All @@ -45,13 +45,7 @@ export const ArgumentsRenderer: React.FunctionComponent<ArgumentsProps> = ({

ArgumentsRenderer.propTypes = {
classes: PropTypes.objectOf(PropTypes.string.isRequired).isRequired,
args: PropTypes.arrayOf(
PropTypes.shape({
name: PropTypes.string.isRequired,
type: PropTypes.object,
description: PropTypes.string,
}).isRequired
).isRequired,
args: PropTypes.array.isRequired,
heading: PropTypes.bool,
};

Expand Down
1 change: 1 addition & 0 deletions src/client/rsg-components/Examples/Examples.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const module: Rsg.ExamplesModule = {
__exampleScope: {},
__currentComponent: () => null,
__examples: [],
__namedExamples: {},
};

const context = {
Expand Down
10 changes: 1 addition & 9 deletions src/client/rsg-components/Methods/MethodsRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,7 @@ const MethodsRenderer: React.FunctionComponent<{ methods: MethodDescriptor[] }>
);

MethodsRenderer.propTypes = {
methods: PropTypes.arrayOf(
PropTypes.shape({
name: PropTypes.string.isRequired,
description: PropTypes.string,
returns: PropTypes.object,
params: PropTypes.array,
tags: PropTypes.object,
}).isRequired
).isRequired,
methods: PropTypes.array.isRequired,
};

export default MethodsRenderer;
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const module: Rsg.ExamplesModule = {
__exampleScope: {},
__currentComponent: () => null,
__examples: [],
__namedExamples: {},
};

const context = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const module: Rsg.ExamplesModule = {
__exampleScope: {},
__currentComponent: () => null,
__examples: [],
__namedExamples: {},
};

const components = [
Expand Down
5 changes: 1 addition & 4 deletions src/client/rsg-components/Usage/Usage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,7 @@ const Usage: React.FunctionComponent<{
};

Usage.propTypes = {
props: PropTypes.shape({
props: PropTypes.array,
methods: PropTypes.array,
}).isRequired,
props: PropTypes.object.isRequired,
};

export default Usage;
3 changes: 3 additions & 0 deletions src/client/rsg-components/mdx/MdxContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@ interface MdxContextContents {
documentScope: Record<string, unknown>;
/** Modules imported inside examples */
exampleScope: Record<string, unknown>;
/** Examples, exported from CSF files */
namedExamples: Record<string, string>;
}

const MdxContext = createContext<MdxContextContents>({
documentScope: {},
exampleScope: {},
namedExamples: {},
});

export default MdxContext;
Expand Down
2 changes: 2 additions & 0 deletions src/client/rsg-components/mdx/MdxWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export default function MdxWrapper({
__documentScope: documentScope,
__exampleScope: exampleScope,
__currentComponent: currentComponent,
__namedExamples: namedExamples,
}: Props) {
const expandedDocumentScope = {
// Make React available to examples
Expand All @@ -36,6 +37,7 @@ export default function MdxWrapper({
exampleMode,
documentScope: expandedDocumentScope,
exampleScope,
namedExamples,
}}
>
{children}
Expand Down
57 changes: 57 additions & 0 deletions src/client/rsg-components/mdx/public/Story.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React from 'react';
import { camelCase } from 'lodash';
import Para from 'rsg-components/Para';
import PlaygroundError from 'rsg-components/PlaygroundError';
import Playground from 'rsg-components/Playground';
import { useMdxContext } from '../MdxContext';
import MdxHighlight from '../MdxHighlight';
import MdxCodeStatic from '../MdxCodeStatic';
import * as Rsg from '../../../../typings';

interface Props extends Rsg.Modifiers {
id: string;
}

// XXX: We're assuming that all imported stories are located in a .story.js file
// next to the Mdx file with the same name, so we're ignoring all but the very
// last part of the story ID, which is the name of the exported from the CSF file
// function
const getLocalExampleNameById = (id: string) => camelCase(id.replace(/^.*?--/, ''));

export default function Story({ id, ...modifiers }: Props) {
const {
componentName,
exampleMode,
documentScope,
exampleScope,
namedExamples,
} = useMdxContext();

const code = namedExamples[getLocalExampleNameById(id)];
if (!code) {
return (
<Para>
<PlaygroundError message={`Named example \`${id}\` not found in a story file`} />
</Para>
);
}

if (modifiers.static) {
return (
<MdxCodeStatic>
<MdxHighlight>{code}</MdxHighlight>
</MdxCodeStatic>
);
}

return (
<Playground
code={code}
componentName={componentName || ''} // TODO
exampleMode={exampleMode}
documentScope={documentScope}
exampleScope={exampleScope}
modifiers={modifiers}
/>
);
}
1 change: 1 addition & 0 deletions src/client/utils/__tests__/getRouteData.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const module: Rsg.ExamplesModule = {
__exampleScope: {},
__currentComponent: () => null,
__examples: [],
__namedExamples: {},
};

const sections: Rsg.Section[] = deepfreeze([
Expand Down
16 changes: 11 additions & 5 deletions src/loaders/mdx-loader.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import mdx from '@mdx-js/mdx';
import { transform } from 'sucrase';
import loaderUtils from 'loader-utils';
import markStaticExamples from './rehype/markStaticExamples';
import addReact from './rehype/addReact';
import addExampleIndicies from './rehype/addExampleIndicies';
import updateExamples from './rehype/updateExamples';
import deduplicateImports from './rehype/deduplicateImports';
import exportExamples from './rehype/exportExamples';
import exportStories from './rehype/exportStories';
import markStaticExamples from './rehype/markStaticExamples';
import provideCurrentComponent from './rehype/provideCurrentComponent';
import provideDocumentScope from './rehype/provideDocumentScope';
import provideExampleScope from './rehype/provideExampleScope';
import provideCurrentComponent from './rehype/provideCurrentComponent';
import updateExamples from './rehype/updateExamples';
import * as Rsg from '../typings';

const HEADER = `
import React from 'react';
import { mdx } from '@mdx-js/react';
`;

Expand All @@ -25,14 +27,17 @@ export default async function mdxLoader(this: Rsg.StyleguidistLoaderContext, con
result = await mdx(content, {
filepath: this.resourcePath,
rehypePlugins: [
addReact,
markStaticExamples,
addExampleIndicies,
updateExamples({ updateExample, resourcePath: this.resourcePath }),
exportExamples,
// Sections don't have current components
component && exportStories({ component, resourcePath: this.resourcePath }),
provideDocumentScope({ context }),
provideExampleScope,
// Sections don't have current components
component && provideCurrentComponent({ component }),
deduplicateImports,
],
});
} catch (err) {
Expand All @@ -47,6 +52,7 @@ export default async function mdxLoader(this: Rsg.StyleguidistLoaderContext, con
jsxPragma: 'mdx',
}).code;

// console.log('='.repeat(80));
// console.log('🦜🦜🦜🦜🦜🦜🦜', this.resourcePath, compiledCode);

return callback(null, compiledCode);
Expand Down
24 changes: 24 additions & 0 deletions src/loaders/rehype/addReact.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Parent, Node } from 'unist';

/**
* Import React to make it available in the Mdx document scope
*
* // ...
*
* ->
*
*
* import React from 'react'
* // ...
*/
export default () => (treeRaw: Node) => {
const tree = treeRaw as Parent;

// Generate import
tree.children.push({
type: 'import',
value: `import React from 'react'`,
});

return tree;
};
50 changes: 50 additions & 0 deletions src/loaders/rehype/deduplicateImports.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { uniq } from 'lodash';
import { Parent, Node } from 'unist';

/**
* Deduplicate import nodes.
*
* import React from "react"
* import Button from './Button';
* import React from 'react'
*
* ->
*
* import React from 'react'
* import Button from './Button'
*
* XXX: This is a very naïve approach and will only deduplicate completely
* identical statements (ignoring quotes and semicolons).
*/
export default () => (treeRaw: Node) => {
const tree = treeRaw as Parent;

const importNodes = tree.children.filter((n) => n.type === 'import');

// `import` Mdx node can contain multiple import statements, so join/split
// to have each statement separately
const importStatements = importNodes
.map((x) => x.value)
.join('\n')
.split('\n');

// Normalize semicolons and quotes
const normalizedImportStatements = importStatements.map((x) =>
x.replace(/;\s*$/, '').replace(/"/g, '/')
);

// Remove duplicates
const uniqImportStatements = uniq(normalizedImportStatements);

// Replace all import nodes with deduplicated ones
return {
...tree,
children: [
...tree.children.filter((n) => n.type !== 'import'),
...uniqImportStatements.map((value) => ({
type: 'import',
value,
})),
],
};
};
Loading

0 comments on commit 995f5e3

Please sign in to comment.