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 ', 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 ? 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 = estree.Node; 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< 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); const literal = ( ) as estree.Literal; const identifier = ( ) as estree.Identifier; const parameters = ( ) as estree.Identifier} value={ ( ) as estree.Literal } kind={'init' as const} /> ) as estree.Property, ...(hasMsw ? [ ( ) as estree.Identifier} value={() as estree.Identifier} kind={'init' as const} shorthand /> ) as estree.Property, ] : []), ]} /> ) as estree.ObjectExpression; const program = ( ) as estree.Literal} specifiers={[ ( ) as estree.Identifier} imported={() as estree.Identifier} /> ) as estree.ImportSpecifier, ...(hasImplStories ? [] : [ ( ) as estree.Identifier } imported={ () as estree.Identifier } /> ) as estree.ImportSpecifier, ]), ]} /> ) as estree.ImportDeclaration, ...(hasMsw ? [ ( ) as estree.Literal } specifiers={[ ( ) as estree.Identifier} /> ) as estree.ImportNamespaceSpecifier, ]} /> ) as estree.ImportDeclaration, ] : []), ...(hasImplStories ? [] : [ ( ) as estree.Literal} specifiers={[ ( ) as estree.ImportDefaultSpecifier, ]} /> ) as estree.ImportDeclaration, ]), ( ) as estree.Identifier} init={ ( ) as estree.Identifier } value={literal} kind={'init' as const} /> ) as estree.Property, ( ) as estree.Identifier } value={identifier} kind={'init' as const} /> ) as estree.Property, ]} /> ) as estree.ObjectExpression } reference={ ( `} /> ) as estree.Identifier } /> ) as estree.Expression } /> ) as estree.VariableDeclarator, ]} /> ) as estree.VariableDeclaration, ...(hasImplStories ? [] : [ ( ) as estree.Identifier } init={ ( ) as estree.Identifier } value={ ( ) as estree.Identifier, ( ) as estree.Identifier } value={ ( ) as estree.Identifier } kind={ 'init' as const } shorthand /> ) as estree.AssignmentProperty, ]} /> ) as estree.ObjectPattern, ]} body={ ( ) as estree.Identifier } value={ ( ) as estree.Property, ]} /> ) as estree.ObjectExpression } kind={ 'init' as const } /> ) as estree.Property, ( ) as estree.Identifier } value={ ( ) as estree.Identifier } property={ ( ) as estree.Identifier } /> ) as estree.MemberExpression } arguments={[ ( ) as estree.Identifier, ]} /> ) as estree.CallExpression } kind={ 'init' as const } /> ) as estree.Property, ( ) as estree.Identifier } value={ ( `} /> ) as estree.Literal } kind={ 'init' as const } /> ) as estree.Property, ]} /> ) as estree.ObjectExpression } /> ) as estree.ReturnStatement, ]} /> ) as estree.BlockStatement } /> ) as estree.FunctionExpression } method kind={'init' as const} /> ) as estree.Property, ( ) as estree.Identifier } value={parameters} kind={'init' as const} /> ) as estree.Property, ]} /> ) as estree.ObjectExpression } reference={ ( `} /> ) as estree.Identifier } /> ) as estree.Expression } /> ) as estree.VariableDeclarator, ]} /> ) as estree.VariableDeclaration } /> ) as estree.ExportNamedDeclaration, ]), ( ) as estree.Identifier} /> ) as estree.ExportDefaultDeclaration, ]} /> ) as estree.Program; return format( '/* 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') : ''), { 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)); }) ) );