import { existsSync, readFileSync } from 'node:fs'; import { writeFile } from 'node:fs/promises'; import { basename, dirname } from 'node:path/posix'; import { promisify } from 'node:util'; import { GENERATOR, type State, generate } from 'astring'; import type * as estree from 'estree'; import glob from 'glob'; 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; } } state.write(' satisfies '); this[node.reference.type](node.reference, state); }, } type SplitCamel = T extends `${infer XH}${infer XR}` ? XR extends '' ? [...YN, Uncapitalize<`${YC}${XH}`>] : XH extends Uppercase ? SplitCamel, [...YN, YC]> : SplitCamel : YN; // @ts-ignore type SplitKebab = T extends `${infer XH}-${infer XR}` ? [XH, ...SplitKebab] : [T]; type ToKebab = 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}` : ''}` : ''; // @ts-ignore type ToPascal = T extends readonly [infer XH extends string, ...infer XR extends readonly string[]] ? `${Capitalize}${ToPascal}` : ''; function h(component: T['type'], props: Omit): 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 = never; type IntrinsicElements = { [T in keyof typeof generator as ToKebab>>]: { [K in keyof Omit[0], 'type'>]?: Parameters[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); const literal = ( ) as unknown as estree.Literal; const identifier = ( ) as unknown as estree.Identifier; const parameters = ( } value={} kind={'init' as const} />, ...hasMsw ? [ } value={} kind={'init' as const} shorthand />, ] : [], ]} /> ); const program = ( } specifiers={[ } imported={} />, ...hasImplStories ? [] : [ } imported={} />, ], ]} />, ...hasMsw ? [ } specifiers={[ } />, ]} />, ] : [], ...hasImplStories ? [] : [ } specifiers={[ , ]} />, ], } init={ } value={literal} kind={'init' as const} />, } value={identifier} kind={'init' as const} />, ]} /> } reference={`} />} /> } />, ]} />, ...hasImplStories ? [ ] : [ } init={ } value={ } params={[ , } value={} kind={'init' as const} shorthand />, ]} />, ]} body={ } value={ , ]} /> } kind={'init' as const} />, } value={ } property={} /> } arguments={[ , ]} /> } kind={'init' as const} />, } value={`} />} kind={'init' as const} />, ]} /> } />, ]} /> } /> } method kind={'init' as const} />, } value={parameters} kind={'init' as const} />, ]} /> } reference={`} />} /> } />, ]} /> } />, ], } />, ]} /> ) as unknown as estree.Program; return format( generate(program, { generator }) + (hasImplStories ? readFileSync(`${implStories}.ts`, 'utf-8') : ''), { 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)); }) ));