Sharkey/packages/frontend/.storybook/generate.tsx

321 lines
9.7 KiB
TypeScript
Raw Normal View History

import { existsSync, readFileSync } from 'node:fs';
import { writeFile } from 'node:fs/promises';
import { basename, dirname } from 'node:path/posix';
2023-03-19 15:22:14 +02:00
import { promisify } from 'node:util';
import { GENERATOR, type State, generate } from 'astring';
2023-03-19 15:22:14 +02:00
import type * as estree from 'estree';
import glob from 'glob';
2023-03-19 15:22:14 +02:00
import { format } from 'prettier';
interface SatisfiesExpression extends estree.BaseExpression {
type: 'SatisfiesExpression';
expression: estree.Expression;
reference: estree.Identifier;
}
const generator = {
...GENERATOR,
SatisfiesExpression(node: SatisfiesExpression, state: State) {
switch (node.expression.type) {
case 'ArrowFunctionExpression': {
state.write('(');
this[node.expression.type](node.expression, state);
state.write(')');
break;
}
default: {
// @ts-ignore
this[node.expression.type](node.expression, state);
break;
}
2023-03-21 04:58:58 +02:00
}
2023-03-21 17:25:17 +02:00
state.write(' satisfies ', node as unknown as estree.Expression);
this[node.reference.type](node.reference, state);
},
}
type SplitCamel<T extends string, YC extends string = '', YN extends readonly string[] = []> = T extends `${infer XH}${infer XR}`
? XR extends ''
? [...YN, Uncapitalize<`${YC}${XH}`>]
: XH extends Uppercase<XH>
? SplitCamel<XR, Lowercase<XH>, [...YN, YC]>
: SplitCamel<XR, `${YC}${XH}`, YN>
: YN;
// @ts-ignore
type SplitKebab<T extends string> = T extends `${infer XH}-${infer XR}`
? [XH, ...SplitKebab<XR>]
: [T];
type ToKebab<T extends readonly string[]> = T extends readonly [infer XO extends string]
? XO
: T extends readonly [infer XH extends string, ...infer XR extends readonly string[]]
? `${XH}${XR extends readonly string[] ? `-${ToKebab<XR>}` : ''}`
: '';
// @ts-ignore
type ToPascal<T extends readonly string[]> = T extends readonly [infer XH extends string, ...infer XR extends readonly string[]]
? `${Capitalize<XH>}${ToPascal<XR>}`
: '';
2023-03-19 15:22:14 +02:00
function h<T extends estree.Node>(component: T['type'], props: Omit<T, 'type'>): T {
const type = component.replace(/(?:^|-)([a-z])/g, (_, c) => c.toUpperCase());
return Object.assign(props, { type }) as T;
}
declare global {
namespace JSX {
type Element = never;
type ElementClass = never;
type ElementAttributesProperty = never;
type ElementChildrenAttribute = never;
type IntrinsicAttributes = never;
type IntrinsicClassAttributes<T> = never;
type IntrinsicElements = {
[T in keyof typeof generator as ToKebab<SplitCamel<Uncapitalize<T>>>]: {
[K in keyof Omit<Parameters<typeof generator[T]>[0], 'type'>]?: Parameters<typeof generator[T]>[0][K];
};
};
}
}
function toStories(component: string): string {
const msw = `${component.slice(0, -'.vue'.length)}.msw`;
const implStories = `${component.slice(0, -'.vue'.length)}.stories.impl`;
const hasMsw = existsSync(`${msw}.ts`);
const hasImplStories = existsSync(`${implStories}.ts`);
const base = basename(component);
const dir = dirname(component);
2023-03-19 15:22:14 +02:00
const literal = (
<literal value={component.slice('src/'.length, -'.vue'.length)} />
2023-03-19 15:22:14 +02:00
) as unknown as estree.Literal;
const identifier = (
<identifier name={base.slice(0, -'.vue'.length).replace(/[-.]|^(?=\d)/g, '_').replace(/(?<=^[^A-Z_]*$)/, '_')} />
2023-03-19 15:22:14 +02:00
) as unknown as estree.Identifier;
const parameters = (
<object-expression
properties={[
<property
2023-03-21 06:05:40 +02:00
key={<identifier name='layout' />}
2023-03-20 09:15:03 +02:00
value={<literal value={`${dir}/`.startsWith('src/pages/') ? 'fullscreen' : 'centered'} />}
2023-03-21 06:05:40 +02:00
kind={'init' as const}
/>,
...hasMsw
? [
<property
2023-03-21 06:05:40 +02:00
key={<identifier name='msw' />}
value={<identifier name='msw' />}
kind={'init' as const}
shorthand
/>,
]
: [],
]}
/>
);
2023-03-19 15:22:14 +02:00
const program = (
<program
body={[
<import-declaration
2023-03-21 06:05:40 +02:00
source={<literal value='@storybook/vue3' />}
2023-03-19 15:22:14 +02:00
specifiers={[
<import-specifier
2023-03-21 06:05:40 +02:00
local={<identifier name='Meta' />}
imported={<identifier name='Meta' />}
2023-03-19 15:22:14 +02:00
/>,
...hasImplStories
? []
: [
<import-specifier
2023-03-21 06:05:40 +02:00
local={<identifier name='StoryObj' />}
imported={<identifier name='StoryObj' />}
/>,
],
2023-03-19 15:22:14 +02:00
]}
/>,
...hasMsw
? [
<import-declaration
source={<literal value={`./${basename(msw)}`} />}
specifiers={[
<import-namespace-specifier
2023-03-21 06:05:40 +02:00
local={<identifier name='msw' />}
/>,
]}
/>,
]
: [],
2023-03-20 09:27:40 +02:00
...hasImplStories
? []
: [
<import-declaration
source={<literal value={`./${base}`} />}
specifiers={[
<import-default-specifier
local={identifier}
/>,
]}
/>,
],
2023-03-19 15:22:14 +02:00
<variable-declaration
2023-03-21 06:05:40 +02:00
kind={'const' as const}
2023-03-19 15:22:14 +02:00
declarations={[
<variable-declarator
2023-03-21 06:05:40 +02:00
id={<identifier name='meta' />}
2023-03-19 15:22:14 +02:00
init={
<satisfies-expression
expression={
<object-expression
properties={[
<property
2023-03-21 06:05:40 +02:00
key={<identifier name='title' />}
value={literal}
2023-03-21 06:05:40 +02:00
kind={'init' as const}
/>,
<property
2023-03-21 06:05:40 +02:00
key={<identifier name='component' />}
value={identifier}
2023-03-21 06:05:40 +02:00
kind={'init' as const}
/>,
]}
/>
}
reference={<identifier name={`Meta<typeof ${identifier.name}>`} />}
2023-03-19 15:22:14 +02:00
/>
}
/>,
]}
/>,
...hasImplStories
? [
]
: [
<export-named-declaration
declaration={
<variable-declaration
2023-03-21 06:05:40 +02:00
kind={'const' as const}
declarations={[
<variable-declarator
2023-03-21 06:05:40 +02:00
id={<identifier name='Default' />}
init={
<satisfies-expression
expression={
<object-expression
properties={[
<property
2023-03-21 06:05:40 +02:00
key={<identifier name='render' />}
value={
<function-expression
params={[
2023-03-21 06:05:40 +02:00
<identifier name='args' />,
<object-pattern
properties={[
<property
2023-03-21 06:05:40 +02:00
key={<identifier name='argTypes' />}
value={<identifier name='argTypes' />}
kind={'init' as const}
shorthand
/>,
]}
/>,
]}
body={
<block-statement
body={[
<return-statement
argument={
<object-expression
properties={[
<property
2023-03-21 06:05:40 +02:00
key={<identifier name='components' />}
value={
<object-expression
properties={[
<property
key={identifier}
value={identifier}
2023-03-21 06:05:40 +02:00
kind={'init' as const}
shorthand
/>,
]}
/>
}
2023-03-21 06:05:40 +02:00
kind={'init' as const}
/>,
<property
2023-03-21 06:05:40 +02:00
key={<identifier name='props' />}
value={
<call-expression
callee={
<member-expression
2023-03-21 06:05:40 +02:00
object={<identifier name='Object' />}
property={<identifier name='keys' />}
/>
}
arguments={[
2023-03-21 06:05:40 +02:00
<identifier name='argTypes' />,
]}
/>
}
2023-03-21 06:05:40 +02:00
kind={'init' as const}
/>,
<property
2023-03-21 06:05:40 +02:00
key={<identifier name='template' />}
value={<literal value={`<${identifier.name} v-bind="$props" />`} />}
2023-03-21 06:05:40 +02:00
kind={'init' as const}
/>,
]}
/>
}
/>,
]}
/>
}
/>
}
method
2023-03-21 06:05:40 +02:00
kind={'init' as const}
/>,
<property
2023-03-21 06:05:40 +02:00
key={<identifier name='parameters' />}
value={parameters}
2023-03-21 06:05:40 +02:00
kind={'init' as const}
/>,
]}
/>
}
reference={<identifier name={`StoryObj<typeof ${identifier.name}>`} />}
/>
}
/>,
]}
/>
}
/>,
],
2023-03-19 15:22:14 +02:00
<export-default-declaration
2023-03-21 06:05:40 +02:00
declaration={<identifier name='meta' />}
2023-03-19 15:22:14 +02:00
/>,
]}
/>
) as unknown as estree.Program;
return format(
2023-03-21 17:25:17 +02:00
'/* eslint-disable @typescript-eslint/explicit-function-return-type */\n' +
'/* eslint-disable import/no-default-export */\n' +
generate(program, { generator }) +
(hasImplStories ? readFileSync(`${implStories}.ts`, 'utf-8') : ''),
2023-03-19 15:22:14 +02:00
{
parser: 'babel-ts',
singleQuote: true,
useTabs: true,
}
);
}
promisify(glob)('src/{components,pages,ui,widgets}/**/*.vue').then((components) => Promise.all(
components.map((component) => {
const stories = component.replace(/\.vue$/, '.stories.ts');
return writeFile(stories, toStories(component));
2023-03-19 15:22:14 +02:00
})
));