diff --git a/README.md b/README.md index 1051792..a455a46 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,18 @@ -# @takuma-ru/auto-story-generator + +> [!NOTE] +> This is a fork of [@takuma-ru/auto-story-generator](https://github.com/takuma-ru/auto-story-generator/assets/49429291/dca65c2c-3384-45c0-a761-e85276cb2393)(which doesn't support Angular). This is meant to extend @takuma-ru/auto-story-generator and support Angular. + +# auto-angular-story-generator ![asg-thumbnail](https://github.com/takuma-ru/auto-story-generator/assets/49429291/dca65c2c-3384-45c0-a761-e85276cb2393) ## Description -Automatic real-time story file generation from React, Vue, and Lit component files +Automatic real-time story file generation from React, Vue, Lit and Angular component files ## Getting Started ### 1. Install the package ```bash -npm i @takuma-ru/auto-story-generator +npm i auto-angular-story-generator ``` ### 2. Add config @@ -37,6 +41,55 @@ const config: StorybookConfig = { export default config; ``` +> [!WARNING] +> Don't run this plugin(Angular part) on your apps right away. Test it on a sample Application or create a new Angular app. + +> [!NOTE] +> Angular feature is a WIP. Only a basic story can be created at this point of time. Modify the created stories as required. Will try to improve story creation. + +For `Angular` [Webpack custom config](https://storybook.js.org/docs/builders/webpack#working-with-webpack-plugins) + +```ts +import type { StorybookConfig } from "@storybook/angular"; + +import autoStoryGenerator from "@takuma-ru/auto-story-generator"; + +const customConfig = { + webpackFinal: async (config) => { + let plugin = autoStoryGenerator.webpack({ + preset: "angular", + imports: ["**/src/**/*.component.ts"], + }); + config.plugins.push(plugin); + return config; + } +} + +const primeConfig: StorybookConfig = { + stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"], + addons: [ + "@storybook/addon-links", + "@storybook/addon-essentials", + "@storybook/addon-interactions", + ], + framework: { + name: "@storybook/angular", + options: {}, + }, + // spread the object here instead of mergeConfig(avaialable for vite) + ...customConfig, + docs: { + autodocs: "tag", + }, +}; + +export default primeConfig; +``` +> [!NOTE] +> In Angular, for first time story creation, a run tme error occurs, can ignore it. + + + ## Supported Frameworks > ✅: Supported > 🚧: Work in progress @@ -48,5 +101,5 @@ export default config; | React | ✅ | | Vue | 🚧 | | Lit | ✅ | -| Angular | ❌ | +| Angular | 🚧 | | Svelte | 📝 | \ No newline at end of file diff --git a/package.json b/package.json index 29fc946..473e5b9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "@takuma-ru/auto-story-generator", + "name": "auto-angular-story-generator", "version": "0.0.0", - "description": "", + "description": "Extends @takuma-ru/auto-story-generator and supports creation of stories for Angular Componenets too", "scripts": { "asg": "pnpm -F \"@takuma-ru/auto-story-generator\"", "doc": "pnpm -F \"docs\"", @@ -9,7 +9,7 @@ "d:next": "pnpm -F \"next\"", "d:lit": "pnpm -F \"lit\"" }, - "author": "takuma-ru (https://github.com/takuma-ru/)", + "author": "gadhikari(https://github.com/GeetaKrishna65/auto-angular-story-generator)", "license": "ISC", "packageManager": "pnpm@8.14.1", "engines": { diff --git a/packages/auto-story-generator/src/index.ts b/packages/auto-story-generator/src/index.ts index d22d972..e8fcd92 100644 --- a/packages/auto-story-generator/src/index.ts +++ b/packages/auto-story-generator/src/index.ts @@ -6,9 +6,10 @@ import { createUnplugin } from "unplugin"; import { genLitStoryFile } from "~/src/presets/lit/genLitStoryFile"; import { genReactStoryFile } from "~/src/presets/react/genReactStoryFile"; +import { genAngularStoryFile } from "~/src/presets/angular/genAngularStoryFile"; export type AsgOptions = { - preset: "lit" | "react" | "vue" | "custom"; + preset: "lit" | "react" | "vue" | "custom" | "angular"; /** * @default undefined * @@ -41,7 +42,10 @@ const unplugin = createUnplugin((options: AsgOptions) => { if (!isMatches.includes(true)) return; const projectRootDir = process.cwd(); - const fileName = file.split("/").pop(); + + // split for either forward or backward slash + const fileName = file.split(/[\\\/]/).pop(); + const fileType = fileName?.split(".").slice(1).join("."); const componentName = fileName?.replace(`.${fileType}`, "") === "index" @@ -90,6 +94,20 @@ const unplugin = createUnplugin((options: AsgOptions) => { break; } + case "angular": { + await genAngularStoryFile({ + componentName, + fileName: fileName, + path: file, + type: `.${fileType}`, + relativeSourceFilePath, + sourceFile, + prettierConfigPath: options.prettierConfigPath, + }); + + break; + } + case "vue": { consola.error("Not yet supported."); break; diff --git a/packages/auto-story-generator/src/presets/angular/genAngularStoryFile.ts b/packages/auto-story-generator/src/presets/angular/genAngularStoryFile.ts new file mode 100644 index 0000000..ad61d9b --- /dev/null +++ b/packages/auto-story-generator/src/presets/angular/genAngularStoryFile.ts @@ -0,0 +1,124 @@ +import { consola } from "consola"; +import { pascalCase } from "scule"; +import { getAngularPropTypes } from "~/src/presets/angular/getAngularPropTypes"; + +import { GenStoryFileOptions } from "~/src/types/GenStoryFileType"; +import { genStoryFile } from "~/src/utils/genStoryFile"; + +export const genAngularStoryFile = async ({ + componentName, + fileName, + path, + type, + relativeSourceFilePath, + sourceFile, + prettierConfigPath, +}: GenStoryFileOptions["fileOptions"]) => { + // const { propTypes } = + // getAngularPropTypes({ + // sourceFile, + // componentName, + // }); + const pascalComponentName = pascalCase(componentName + 'Component'); + // if (!propTypes) return consola.error("Could not find argTypes"); + + // Angular doesn't need file with extension + let file = fileName.split('.'); + file.pop(); + + const initialCode = ` +import type { Meta, StoryObj } from "@storybook/angular"; + +import { ${pascalComponentName} } from "./${file.join('.')}"; + +const meta: Meta<${pascalComponentName}> = { + title: "components/${pascalComponentName}", + component: ${pascalComponentName}, + tags: ["autodocs"], + render: (args: ${pascalComponentName}) => ({ + props: { + ...args + } + }) +}; + +export default meta; +type Story = StoryObj<${pascalComponentName}>; + +export const Primary: Story = {}; +`; + + const componentCode = `${pascalComponentName}`; + + // const args: GenStoryFileOptions["generateOptions"]["meta"]["args"] = {}; + + // propTypes.forEach((prop) => { + // if (prop.isOptional) { + // return (args[prop.name] = "undefined"); + // } + + // let value: string | boolean | undefined = + // prop.value.length > 0 ? prop.value[0] : "undefined"; + + // if (prop.type.includes("boolean")) { + // value = true; + // } + + // args[prop.name] = value; + // }); + + // const argTypes: GenStoryFileOptions["generateOptions"]["meta"]["argTypes"] = + // {}; + + // propTypes.forEach((prop) => { + // if (prop.type[0] === "boolean") { + // return (argTypes[prop.name] = { + // control: "boolean", + // }); + // } + + // if (prop.type[0] === "object") { + // return (argTypes[prop.name] = { + // control: "object", + // }); + // } + + // if (prop.value.length > 1) { + // return (argTypes[prop.name] = { + // control: "select", + // options: prop.value, + // }); + // } else { + // if (prop.type[0] === "string") { + // return (argTypes[prop.name] = { + // control: "text", + // }); + // } + + // if (prop.type[0] === "number") { + // return (argTypes[prop.name] = { + // control: "number", + // }); + // } + // } + // }); + + genStoryFile({ + fileOptions: { + componentName, + fileName, + path, + type, + relativeSourceFilePath, + sourceFile, + prettierConfigPath, + }, + generateOptions: { + fileType: ".stories.ts", + initialCode, + meta: { + // component: componentCode, + }, + }, + }); +}; diff --git a/packages/auto-story-generator/src/presets/angular/getAngularPropTypes.ts b/packages/auto-story-generator/src/presets/angular/getAngularPropTypes.ts new file mode 100644 index 0000000..be785aa --- /dev/null +++ b/packages/auto-story-generator/src/presets/angular/getAngularPropTypes.ts @@ -0,0 +1,120 @@ +import { pascalCase } from "scule"; +import { TypeFlags, ts, Symbol, SourceFile } from "ts-morph"; + +import { + GenReactPropTypesOptions, + GenReactPropTypesReturn, +} from "~/src/types/GenPropTypeType"; + +const getTypeFlagsName = (flags: TypeFlags) => { + // Get all the keys of TypeFlags + const keys = Object.keys(TypeFlags) as (keyof typeof TypeFlags)[]; + + // Filter the keys where the flag is set + const setFlags = keys.find((key) => flags === TypeFlags[key]); + + return setFlags || "err"; +}; + +// Didn't understnd the Props yet, will add this after proper understanding is reached. +export const getAngularPropTypes = ({sourceFile, componentName}: { sourceFile: SourceFile; componentName: string; })=> { + const pascalComponentName = pascalCase(componentName); +} + +// export const getAngularPropTypes = ({ +// sourceFile, +// componentName, +// }: GenReactPropTypesOptions): { +// propsPattern?: "component-props" | "props" | "inline"; +// propTypes: GenReactPropTypesReturn; +// } => { +// let propsPattern: "component-props" | "props" | "inline" = "component-props"; + + +// const props = +// propsType?.getType() || +// propsInterface?.getType() || +// propsOnlyType?.getType() || +// propsOnlyInterface?.getType() || +// propsInline?.getParameters()[0].getType(); + +// if (!props) { +// return { +// propTypes: undefined, +// }; +// } + +// // eslint-disable-next-line @typescript-eslint/ban-types +// let propsProperties: Symbol[] = []; +// const isPropsIntersection = props.isIntersection(); +// if (isPropsIntersection) { +// propsProperties = []; + +// const intersectionTypes = props.getIntersectionTypes(); + +// intersectionTypes.forEach((intersectionType) => { +// const intersectionTypeText = intersectionType.getText(); + +// if (intersectionTypeText.includes("HTMLAttributes")) { +// return; +// } + +// return propsProperties.push(...intersectionType.getProperties()); +// }); +// } else { +// propsProperties = props.getProperties(); +// } + +// if (propsOnlyType || propsOnlyInterface) { +// propsPattern = "props"; +// } + +// if (propsInline) { +// propsPattern = "inline"; +// } + +// const propTypes = propsProperties.map((prop) => { +// const propName = prop.getName(); +// const propType = prop.getValueDeclaration()?.getType(); + +// if (!propType) { +// return { +// name: propName, +// type: ["err"], +// isOptional: prop.isOptional(), +// value: [], +// }; +// } + +// if (propType.isUnion() && !propType.isBoolean()) { +// const unionTypes = propType.getUnionTypes(); + +// const type = Array.from( +// new Set( +// unionTypes.map((union) => +// getTypeFlagsName(union.getFlags().valueOf()), +// ), +// ), +// ); + +// return { +// name: propName, +// type, +// isOptional: prop.isOptional(), +// value: unionTypes.map((union) => union.getText().replace(/"/g, "")), +// }; +// } + +// return { +// name: propName, +// type: [prop.getValueDeclaration()!.getType().getText()], +// isOptional: prop.isOptional(), +// value: [], +// }; +// }); + +// return { +// propsPattern, +// propTypes, +// }; +// };