Skip to content

Commit

Permalink
Added source decorator
Browse files Browse the repository at this point in the history
  • Loading branch information
JonahPlusPlus committed Nov 5, 2024
1 parent a77230a commit 776f06f
Show file tree
Hide file tree
Showing 9 changed files with 253 additions and 30 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,6 @@ node_modules
# build
dist
bench

# vite
vite.config.ts.timestamp-*
2 changes: 1 addition & 1 deletion example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,6 @@
"vite-plugin-solid": "^2.8.2"
},
"dependencies": {
"solid-js": "^1.9.2"
"solid-js": "^1.9.3"
}
}
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"@fal-works/esbuild-plugin-global-externals": "^2.1.2",
"@types/fs-extra": "^11.0.4",
"@types/node": "^18.0.0",
"esbuild": "^0.24.0",
"esbuild-plugin-alias": "^0.2.1",
"esbuild-plugin-solid": "^0.6.0",
"eslint": "^9.13.0",
Expand All @@ -29,6 +30,7 @@
"rollup": "^4.24.0",
"rollup-plugin-dts": "^6.1.1",
"slash": "^5.1.0",
"solid-js": "^1.9.3",
"sort-package-json": "^2.10.1",
"ts-node": "^10.9.2",
"tsup": "^8.3.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/frameworks/solid-vite/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
},
"devDependencies": {
"@storybook/types": "next",
"solid-js": "^1.9.2",
"solid-js": "^1.9.3",
"storybook": "next",
"storybook-solidjs": "workspace:*",
"vite": "^5.4.8"
Expand Down
8 changes: 5 additions & 3 deletions packages/renderers/solid/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,12 @@
"@storybook/preview-api": "next",
"@storybook/test": "next",
"@storybook/types": "next",
"@types/babel__standalone": "link:.yarn/cache/null",
"async-mutex": "^0.5.0",
"esbuild": "^0.24.0",
"esbuild-plugin-solid": "^0.6.0",
"storybook": "next"
},
"peerDependencies": {
"solid-js": "^1.9.2"
"solid-js": "^1.9.3"
},
"engines": {
"node": ">=18.0.0"
Expand All @@ -74,5 +73,8 @@
"./src/entry-preview-docs.tsx"
],
"platform": "browser"
},
"dependencies": {
"@babel/standalone": "^7.26.2"
}
}
14 changes: 0 additions & 14 deletions packages/renderers/solid/src/docs/jsxDecorator.tsx

This file was deleted.

215 changes: 215 additions & 0 deletions packages/renderers/solid/src/docs/sourceDecorator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
/* eslint-disable @typescript-eslint/no-explicit-any */

// @babel/standalone does not export types,
// so this file is a mess of anys.

import type { StoryContext, PartialStoryFn } from '@storybook/types';
import { SolidRenderer } from '../types';

import { SNIPPET_RENDERED, SourceType } from '@storybook/docs-tools';
import { addons, useEffect } from '@storybook/preview-api';

// @ts-expect-error Types are not up to date
import * as Babel from '@babel/standalone';
const parser = Babel.packages.parser;
const generate = Babel.packages.generator.default;
const t = Babel.packages.types;

function skipSourceRender(context: StoryContext<SolidRenderer>): boolean {
const sourceParams = context?.parameters.docs?.source;
const isArgsStory = context?.parameters.__isArgsStory;

// always render if the user forces it
if (sourceParams?.type === SourceType.DYNAMIC) {
return false;
}

// never render if the user is forcing the block to render code, or
// if the user provides code, or if it's not an args story.
return (
!isArgsStory || sourceParams?.code || sourceParams?.type === SourceType.CODE
);
}

/**
* Generate JSX source code from stories.
*/
export const sourceDecorator = (
storyFn: PartialStoryFn<SolidRenderer>,
ctx: StoryContext<SolidRenderer>,
) => {
// Strategy: Since SolidJS doesn't have a VDOM,
// it isn't possible to get information directly about inner components.
// Instead, there needs to be an altered render function
// that records information about component properties,
// or source code extraction from files.
// This decorator uses the latter technique.
// By using the source code string generated by CSF-tools,
// we can then parse the properties of the `args` object,
// and return the source slices.

// Note: this also means we are limited in how we can
// get the component name.
// Since Storybook doesn't do source code extraction for
// story metas (yet), we can use the title for now.
const channel = addons.getChannel();
const story = storyFn();
const skip = skipSourceRender(ctx);

// eslint-disable-next-line prefer-const
let source: string | null;

useEffect(() => {
if (!skip && source) {
const { id, unmappedArgs } = ctx;
channel.emit(SNIPPET_RENDERED, { id, args: unmappedArgs, source });
}
});

if (skip) return story;

const docs = ctx?.parameters?.docs;
const src = docs?.source?.originalSource;
const name = ctx.title.split('/').at(-1)!;

source = generateSolidSource(name, src);
console.log(source);

return story;
};

/**
* Generate Solid JSX from story source.
*/
function generateSolidSource(name: string, src: string): string | null {
try {
const { attributes, children } = parseProps(src);

const selfClosing = children == null || children.length == 0;

const component = {
type: 'JSXElement',
openingElement: {
type: 'JSXOpeningElement',
name: {
type: 'JSXIdentifier',
name,
},
attributes: attributes,
selfClosing,
},
children: children ?? [],
closingElement: selfClosing
? undefined
: {
type: 'JSXClosingElement',
name: {
type: 'JSXIdentifier',
name,
},
},
};

console.log(component);

return generate(component, { compact: false }).code;
} catch (e) {
console.error(e);
return null;
}
}

function toJSXChild(node: any): object {
if (
t.isJSXElement(node) ||
t.isJSXText(node) ||
t.isJSXExpressionContainer(node) ||
t.isJSXSpreadChild(node) ||
t.isJSXFragment(node)
) {
return node;
}

if (t.isStringLiteral(node)) {
return {
type: 'JSXText',
value: node.value,
};
}

if (t.isExpression(node)) {
return {
type: 'JSXExpressionContainer',
value: node,
};
}

return {
type: 'JSXExpressionContainer',
value: t.jsxEmptyExpression(),
};
}

interface SolidProps {
attributes: object[];
children: object[] | null;
}

/**
* Parses component properties from source expression.
*
* The source code will be in the form of a `Story` object.
*/
function parseProps(src: string): SolidProps {
const ast = parser.parseExpression(src, { plugins: ['jsx'] });
console.log(ast);
if (ast.type != 'ObjectExpression') throw 'Expected `ObjectExpression` type';
// Find args property.
const args_prop = ast.properties.find((v: any) => {
if (v.type != 'ObjectProperty') return false;
if (v.key.type != 'Identifier') return false;
return v.key.name == 'args';
}) as any | undefined;
// No args just there aren't any properties or children.
if (!args_prop)
return {
attributes: [],
children: null,
};
// Get arguments.
const args = args_prop.value;
if (args.type != 'ObjectExpression') throw 'Expected `ObjectExpression` type';

// Construct props object, where values are source code slices.
const attributes: object[] = [];
let children: object[] | null = null;
for (const el of args.properties) {
if (el.type != 'ObjectProperty') continue;
if (el.key.type != 'Identifier') continue;

if (el.key.name == 'children') {
children = [toJSXChild(el.value)];
continue;
}

let value: any = {
type: 'JSXExpressionContainer',
expression: el.value,
};

if (el.value.type == 'BooleanLiteral' && el.value.value == true) {
value = undefined;
}

attributes.push({
type: 'JSXAttribute',
name: {
type: 'JSXIdentifier',
name: el.key.name,
},
value,
});
}

return { attributes, children };
}
4 changes: 2 additions & 2 deletions packages/renderers/solid/src/entry-preview-docs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
enhanceArgTypes,
extractComponentDescription,
} from '@storybook/docs-tools';
import { jsxDecorator } from './docs/jsxDecorator';
import { sourceDecorator } from './docs/sourceDecorator';
import type { Decorator, SolidRenderer } from './public-types';
import { ArgTypesEnhancer } from '@storybook/types';

Expand All @@ -15,7 +15,7 @@ export const parameters = {
},
};

export const decorators: Decorator[] = [jsxDecorator];
export const decorators: Decorator[] = [sourceDecorator];

export const argTypesEnhancers: ArgTypesEnhancer<SolidRenderer>[] = [
enhanceArgTypes,
Expand Down
Loading

0 comments on commit 776f06f

Please sign in to comment.