/* * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ import { existsSync, readFileSync } from 'node:fs'; import { writeFile } from 'node:fs/promises'; import { basename, dirname } from 'node:path/posix'; import { GENERATOR, type State, generate } from 'astring'; import type * as estree from 'estree'; import glob from 'fast-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): Promise { const msw = `${component.slice(0, -'.vue'.length)}.msw`; const implStories = `${component.slice(0, -'.vue'.length)}.stories.impl`; const metaStories = `${component.slice(0, -'.vue'.length)}.stories.meta`; const hasMsw = existsSync(`${msw}.ts`); const hasImplStories = existsSync(`${implStories}.ts`); const hasMetaStories = existsSync(`${metaStories}.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, ]), ...(hasMetaStories ? [ as estree.Literal} specifiers={[ as estree.Identifier} /> as estree.ImportNamespaceSpecifier, ]} /> 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, ...(hasMetaStories ? [ as estree.Identifier} /> as estree.SpreadElement, ] : []) ]} /> 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, ]} 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} value={ as estree.Identifier} kind={'init' as const} shorthand /> 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={ as estree.Identifier} value={ as estree.ThisExpression} property={ as estree.Identifier} /> as estree.MemberExpression } /> as estree.SpreadElement, ]} /> as estree.ObjectExpression } /> as estree.ReturnStatement, ]} /> as estree.BlockStatement } /> as estree.FunctionExpression } method kind={'init' as const} /> as estree.Property, ]} /> as estree.ObjectExpression } 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' + '/* eslint-disable import/no-duplicates */\n' + generate(program, { generator }) + (hasImplStories ? readFileSync(`${implStories}.ts`, 'utf-8') : ''), { parser: 'babel-ts', singleQuote: true, useTabs: true, } ); } // glob('src/{components,pages,ui,widgets}/**/*.vue') (async () => { const globs = await Promise.all([ glob('src/components/global/*.vue'), glob('src/components/Mk{A,B}*.vue'), glob('src/components/MkDigitalClock.vue'), glob('src/components/MkGalleryPostPreview.vue'), glob('src/components/MkSignupServerRules.vue'), glob('src/components/MkUserSetupDialog.vue'), glob('src/components/MkUserSetupDialog.*.vue'), glob('src/components/MkInviteCode.vue'), glob('src/components/MkTagItem.vue'), glob('src/components/MkRoleSelectDialog.vue'), glob('src/pages/user/home.vue'), ]); const components = globs.flat(); await Promise.all(components.map(async (component) => { const stories = component.replace(/\.vue$/, '.stories.ts'); await writeFile(stories, await toStories(component)); })) })();