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

Prebundle with tsup #23

Merged
merged 2 commits into from
Nov 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 <[email protected]>",
"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