Skip to content

Commit

Permalink
Merge pull request #23 from storybookjs/shilman/convert-to-tsup
Browse files Browse the repository at this point in the history
Prebundle with tsup
  • Loading branch information
shilman authored Nov 3, 2022
2 parents 706a0bb + 5394b4c commit 8412113
Show file tree
Hide file tree
Showing 9 changed files with 613 additions and 194 deletions.
20 changes: 0 additions & 20 deletions compiler.js

This file was deleted.

2 changes: 1 addition & 1 deletion loader.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const { getOptions } = require('loader-utils');
const { compile } = require('./compiler');
const { compile } = require('./dist/index');

const DEFAULT_RENDERER = `
import React from 'react';
Expand Down
29 changes: 12 additions & 17 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,44 +8,38 @@
},
"author": "Michael Shilman <michael@lab80.co>",
"license": "MIT",
"main": "compiler.js",
"module": "dist/esm/index.js",
"types": "dist/ts/index.d.ts",
"main": "dist/index.js",
"module": "dist/index.mjs",
"types": "dist/index.d.ts",
"files": [
"dist/**/*",
"README.md",
"*.js",
"*.d.ts"
],
"scripts": {
"clean": "rimraf ./dist",
"buildBabel": "concurrently \"yarn buildBabel:cjs\" \"yarn buildBabel:esm\"",
"buildBabel:cjs": "babel ./src -d ./dist/cjs --extensions \".js,.jsx,.ts,.tsx\"",
"buildBabel:esm": "babel ./src -d ./dist/esm --env-name esm --extensions \".js,.jsx,.ts,.tsx\"",
"buildTsc": "tsc --declaration --emitDeclarationOnly --outDir ./dist/ts",
"prebuild": "yarn clean",
"build": "concurrently \"yarn buildBabel\" \"yarn buildTsc\"",
"build:watch": "concurrently \"yarn buildBabel:cjs -- --watch\" \"yarn buildTsc -- --watch\"",
"test": "jest",
"start": "concurrently \"yarn build:watch\" \"yarn storybook -- --no-manager-cache --quiet\"",
"build": "tsup",
"start": "yarn build && yarn storybook -- --no-manager-cache --quiet\"",
"release": "yarn build && auto shipit",
"storybook": "start-storybook -p 6006",
"build-storybook": "build-storybook",
"prettier": "prettier",
"prepare": "husky install"
},
"dependencies": {
"loader-utils": "^2.0.0"
},
"devDependencies": {
"@babel/traverse": "^7.12.11",
"@babel/generator": "^7.12.11",
"@babel/parser": "^7.12.11",
"@babel/types": "^7.14.8",
"@mdx-js/mdx": "^2.0.0",
"estree-to-babel": "^4.9.0",
"hast-util-to-estree": "^2.0.2",
"lodash": "^4.17.21",
"js-string-escape": "^1.0.1",
"loader-utils": "^2.0.0",
"lodash": "^4.17.21"
},
"devDependencies": {
"@babel/cli": "^7.12.1",
"@babel/core": "^7.12.3",
"@babel/preset-env": "^7.12.1",
Expand Down Expand Up @@ -83,7 +77,8 @@
"rimraf": "^3.0.2",
"ts-dedent": "^2.2.0",
"ts-jest": "^27.0.4",
"typescript": "^4.2.4"
"typescript": "^4.2.4",
"tsup": "^6.2.2"
},
"lint-staged": {
"*.{ts,js,css,md}": "prettier --write"
Expand Down
82 changes: 73 additions & 9 deletions src/mdx2.test.ts → src/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { dedent } from 'ts-dedent';
import prettier from 'prettier';
import { compileSync, SEPARATOR, wrapperJs } from './mdx2';
import { compileSync, compile, SEPARATOR, wrapperJs } from './index';

// @ts-ignore
expect.addSnapshotSerializer({
Expand Down Expand Up @@ -45,7 +45,19 @@ describe('mdx2', () => {
`);
});

it('full snapshot', () => {
it('standalone jsx expressions', () => {
expect(
clean(dedent`
# Standalone JSX expressions
{3 + 3}
`)
).toMatchInlineSnapshot(`const componentMeta = { includeStories: [] };`);
});
});

describe('full snapshots', () => {
it('compileSync', () => {
const input = dedent`
# hello
Expand Down Expand Up @@ -106,15 +118,67 @@ describe('mdx2', () => {
export default componentMeta;
`);
});
it('compile', async () => {
const input = dedent`
# hello
it('standalone jsx expressions', () => {
expect(
clean(dedent`
# Standalone JSX expressions
<Meta title="foobar" />
{3 + 3}
`)
).toMatchInlineSnapshot(`const componentMeta = { includeStories: [] };`);
world {2 + 1}
<Story name="foo">bar</Story>
`;
// @ts-ignore
expect(await compile(input)).toMatchInlineSnapshot(`
/*@jsxRuntime automatic @jsxImportSource react*/
import {Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs} from "react/jsx-runtime";
import {useMDXComponents as _provideComponents} from "@mdx-js/react";
function MDXContent(props = {}) {
const {wrapper: MDXLayout} = Object.assign({}, _provideComponents(), props.components);
return MDXLayout ? _jsx(MDXLayout, Object.assign({}, props, {
children: _jsx(_createMdxContent, {})
})) : _createMdxContent();
function _createMdxContent() {
const _components = Object.assign({
h1: "h1",
p: "p"
}, _provideComponents(), props.components), {Meta, Story} = _components;
if (!Meta) _missingMdxReference("Meta", true);
if (!Story) _missingMdxReference("Story", true);
return _jsxs(_Fragment, {
children: [_jsx(_components.h1, {
children: "hello"
}), "\\n", _jsx(Meta, {
title: "foobar"
}), "\\n", _jsxs(_components.p, {
children: ["world ", 2 + 1]
}), "\\n", _jsx(Story, {
name: "foo",
children: "bar"
})]
});
}
}
function _missingMdxReference(id, component) {
throw new Error("Expected " + (component ? "component" : "object") + " \`" + id + "\` to be defined: you likely forgot to import, pass, or provide it.");
}
// =========
export const foo = () => (
"bar"
);
foo.storyName = 'foo';
foo.parameters = { storySource: { source: '\\"bar\\"' } };
const componentMeta = { title: 'foobar', tags: ['mdx'], includeStories: ["foo"], };
componentMeta.parameters = componentMeta.parameters || {};
componentMeta.parameters.docs = {
...(componentMeta.parameters.docs || {}),
page: MDXContent,
};
export default componentMeta;
`);
});
});

Expand Down
149 changes: 148 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,148 @@
export * from './mdx2';
import { compile as mdxCompile, compileSync } from '@mdx-js/mdx';
import generate from '@babel/generator';
import * as t from '@babel/types';
import cloneDeep from 'lodash/cloneDeep';
import toBabel from 'estree-to-babel';
import { toEstree } from 'hast-util-to-estree';

// Keeping as much code as possible from the original compiler to avoid breaking changes
import {
genCanvasExports,
genStoryExport,
genMeta,
CompilerOptions,
Context,
MetaExport,
wrapperJs,
stringifyMeta,
} from './sb-mdx-plugin';

export const SEPARATOR = '// =========';

export { wrapperJs };

function extractExports(root: t.File, options: CompilerOptions) {
const context: Context = {
counter: 0,
storyNameToKey: {},
namedExports: {},
};
const storyExports = [];
const includeStories = [];
let metaExport: MetaExport | null = null;
const { code } = generate(root, {});
let contents: t.ExpressionStatement;
root.program.body.forEach((child) => {
if (t.isExpressionStatement(child) && t.isJSXFragment(child.expression)) {
if (contents) throw new Error('duplicate contents');
contents = child;
} else if (
t.isExportNamedDeclaration(child) &&
t.isVariableDeclaration(child.declaration) &&
child.declaration.declarations.length === 1
) {
const declaration = child.declaration.declarations[0];
if (t.isVariableDeclarator(declaration) && t.isIdentifier(declaration.id)) {
const { name } = declaration.id;
context.namedExports[name] = declaration.init;
}
}
});
if (contents) {
const jsx = contents.expression as t.JSXFragment;
jsx.children.forEach((child) => {
if (t.isJSXElement(child)) {
if (t.isJSXIdentifier(child.openingElement.name)) {
const name = child.openingElement.name.name;
let stories;
if (['Canvas', 'Preview'].includes(name)) {
stories = genCanvasExports(child, context);
} else if (name === 'Story') {
stories = genStoryExport(child, context);
} else if (name === 'Meta') {
const meta = genMeta(child, options);
if (meta) {
if (metaExport) {
throw new Error('Meta can only be declared once');
}
metaExport = meta;
}
}
if (stories) {
Object.entries(stories).forEach(([key, story]) => {
includeStories.push(key);
storyExports.push(story);
});
}
}
} else if (t.isJSXExpressionContainer(child)) {
// Skip string literals & other JSX expressions
} else {
throw new Error(`Unexpected JSX child: ${child.type}`);
}
});
}

if (metaExport) {
if (!storyExports.length) {
storyExports.push('export const __page = () => { throw new Error("Docs-only story"); };');
storyExports.push('__page.parameters = { docsOnly: true };');
includeStories.push('__page');
}
} else {
metaExport = {};
}
metaExport.includeStories = JSON.stringify(includeStories);

const fullJsx = [
...storyExports,
`const componentMeta = ${stringifyMeta(metaExport)};`,
wrapperJs,
'export default componentMeta;',
].join('\n\n');

return fullJsx;
}

export const plugin = (store: any) => (root: any) => {
const estree = store.toEstree(root);
// toBabel mutates root, so we need to clone it
const clone = cloneDeep(estree);
const babel = toBabel(clone);
store.exports = extractExports(babel, {});

return root;
};

export const postprocess = (code: string, extractedExports: string) => {
const lines = code.toString().trim().split('\n');

// /*@jsxRuntime automatic @jsxImportSource react*/
const first = lines.shift();

return [
first,
...lines.filter((line) => !line.match(/^export default/)),
SEPARATOR,
extractedExports,
].join('\n');
};

export const mdxSync = (code: string) => {
const store = { exports: '', toEstree };
const output = compileSync(code, {
rehypePlugins: [[plugin, store]],
});
return postprocess(output.toString(), store.exports);
};

export { mdxSync as compileSync };

export const compile = async (code: string, { skipCsf }: { skipCsf?: boolean } = {}) => {
const store = { exports: '', toEstree };
const output = await mdxCompile(code, {
rehypePlugins: skipCsf ? [] : [[plugin, store]],
providerImportSource: '@mdx-js/react',
});
return skipCsf ? output.toString() : postprocess(output.toString(), store.exports);
};
Loading

0 comments on commit 8412113

Please sign in to comment.