├── .gitignore ├── .github ├── FUNDING.yml ├── workflows │ ├── pull-request.yml │ └── npm-publish.yml └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.yaml ├── src ├── stubs │ ├── astro │ │ └── component.astro │ ├── react │ │ ├── function-component.jsx │ │ ├── function-component-tailwind.jsx │ │ ├── function-component.tsx │ │ ├── function-component-css-module.jsx │ │ ├── function-component-tailwind.tsx │ │ ├── function-component-css-module.tsx │ │ ├── function-component-styled-components.jsx │ │ └── function-component-styled-components.tsx │ ├── qwik │ │ ├── hello-world-component.tsx │ │ ├── usestyles-component.tsx │ │ └── usestore-component.tsx │ ├── svelte │ │ ├── component-js.svelte │ │ └── component-ts.svelte │ ├── vue │ │ ├── component-composition.vue │ │ ├── component-options.vue │ │ └── advanced-component.vue │ └── angular │ │ ├── component.component.ts │ │ └── component.component.spec.ts └── utils │ ├── frameworks │ ├── angular │ │ ├── angular.mjs │ │ ├── angular.mts │ │ ├── make-angular-component.mjs │ │ └── make-angular-component.mts │ ├── astro │ │ ├── astro.mjs │ │ └── astro.mts │ ├── svelte │ │ ├── svelte.mts │ │ └── svelte.mjs │ ├── qwik │ │ ├── qwik.mts │ │ └── qwik.mjs │ ├── vue │ │ ├── vue.mts │ │ ├── vue.mjs │ │ ├── helper.mts │ │ └── helper.mjs │ └── react │ │ ├── react.mts │ │ └── react.mjs │ ├── configs.cts │ ├── configs.cjs │ ├── wizard.mjs │ ├── utils.mts │ ├── utils.mjs │ └── wizard.mts ├── setup.jest.js ├── biome.json ├── cmd ├── make-js-component.mts └── make-js-component.mjs ├── LICENSE ├── package.json ├── __tests__ └── menu.test.cjs ├── CONTRIBUTING.md ├── README.md ├── CODE_OF_CONDUCT.md ├── jest.config.js └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | node_modules 3 | components 4 | src/stubs/angular/*.js -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: Giuliano1993 4 | -------------------------------------------------------------------------------- /src/stubs/astro/component.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const name = "ComponentName"; 3 | --- 4 |
5 |

Hello {name}!

6 |
-------------------------------------------------------------------------------- /src/stubs/react/function-component.jsx: -------------------------------------------------------------------------------- 1 | export default function ComponentName({}) { 2 | return
Hello ComponentName
; 3 | } 4 | -------------------------------------------------------------------------------- /src/stubs/react/function-component-tailwind.jsx: -------------------------------------------------------------------------------- 1 | export default function ComponentName({}) { 2 | return
Hello ComponentName
; 3 | } 4 | -------------------------------------------------------------------------------- /src/stubs/qwik/hello-world-component.tsx: -------------------------------------------------------------------------------- 1 | import { component$ } from "@builder.io/qwik"; 2 | 3 | export default component$(() => { 4 | return
Hello ComponentName
; 5 | }); 6 | -------------------------------------------------------------------------------- /src/stubs/react/function-component.tsx: -------------------------------------------------------------------------------- 1 | interface ComponentNameProps {} 2 | 3 | export default function ComponentName({}: ComponentNameProps) { 4 | return
Hello ComponentName
; 5 | } 6 | -------------------------------------------------------------------------------- /src/stubs/svelte/component-js.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 |
7 | Hello ComponentName 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /src/stubs/react/function-component-css-module.jsx: -------------------------------------------------------------------------------- 1 | import styles from "./ComponentName.module.css"; 2 | 3 | export default function ComponentName({}) { 4 | return
Hello ComponentName
; 5 | } 6 | -------------------------------------------------------------------------------- /src/stubs/react/function-component-tailwind.tsx: -------------------------------------------------------------------------------- 1 | interface ComponentNameProps {} 2 | 3 | export default function ComponentName({}: ComponentNameProps) { 4 | return
Hello ComponentName
; 5 | } 6 | -------------------------------------------------------------------------------- /src/stubs/vue/component-composition.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /setup.jest.js: -------------------------------------------------------------------------------- 1 | 2 | const matchers = require('jest-extended'); 3 | const {toBeInTheConsole ,toHaveErrorMessage} = require('cli-testing-library/extend-expect') 4 | expect.extend(matchers); 5 | 6 | afterEach(() => { 7 | jest.useRealTimers(); 8 | }); -------------------------------------------------------------------------------- /src/stubs/svelte/component-ts.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 |
8 | Hello ComponentName 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /src/stubs/react/function-component-css-module.tsx: -------------------------------------------------------------------------------- 1 | import styles from "./ComponentName.module.css"; 2 | 3 | interface ComponentNameProps {} 4 | 5 | export default function ComponentName({}: ComponentNameProps) { 6 | return
Hello ComponentName
; 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/frameworks/angular/angular.mjs: -------------------------------------------------------------------------------- 1 | const framework = "angular"; 2 | 3 | export default function (componentName, folder) { 4 | return { 5 | componentName, 6 | framework: framework.toLowerCase(), 7 | template: "component.component.ts", 8 | folder: folder, 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/frameworks/astro/astro.mjs: -------------------------------------------------------------------------------- 1 | const framework = "astro"; 2 | export default function (componentName, folder) { 3 | return { 4 | componentName: componentName, 5 | framework: framework.toLowerCase(), 6 | template: "component.astro", 7 | folder: folder, 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /src/stubs/react/function-component-styled-components.jsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | const Title = styled.div` 4 | font-size: x-large; 5 | font-weight: bold; 6 | `; 7 | 8 | export default function ComponentName({}) { 9 | return Hello ComponentName; 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/frameworks/angular/angular.mts: -------------------------------------------------------------------------------- 1 | const framework = "angular"; 2 | 3 | export default function (componentName: string, folder: string) { 4 | return { 5 | componentName, 6 | framework: framework.toLowerCase(), 7 | template: "component.component.ts", 8 | folder: folder, 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/frameworks/astro/astro.mts: -------------------------------------------------------------------------------- 1 | const framework = "astro"; 2 | 3 | export default function (componentName: string, folder: string) { 4 | return { 5 | componentName: componentName, 6 | framework: framework.toLowerCase(), 7 | template: "component.astro", 8 | folder: folder, 9 | }; 10 | } 11 | -------------------------------------------------------------------------------- /src/utils/configs.cts: -------------------------------------------------------------------------------- 1 | import * as path from "node:path"; 2 | 3 | const mainFilename = path.dirname(module?.filename || ""); 4 | const dir = path.join(mainFilename, "../.."); 5 | 6 | export const configs = { 7 | INIT_PATH: dir, 8 | BASE_DIR: "./src", 9 | STUBS_DIR: "stubs", 10 | COMPONENT_FOLDER: "/components", 11 | }; 12 | -------------------------------------------------------------------------------- /src/stubs/react/function-component-styled-components.tsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | interface ComponentNameProps {} 4 | 5 | const Title = styled.div` 6 | font-size: x-large; 7 | font-weight: bold; 8 | `; 9 | 10 | export default function ComponentName({}: ComponentNameProps) { 11 | return Hello ComponentName; 12 | } 13 | -------------------------------------------------------------------------------- /src/stubs/vue/component-options.vue: -------------------------------------------------------------------------------- 1 | 17 | 20 | -------------------------------------------------------------------------------- /.github/workflows/pull-request.yml: -------------------------------------------------------------------------------- 1 | name: Code quality 2 | 3 | on: 4 | push: 5 | #pull_request: 6 | 7 | jobs: 8 | quality: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v4 13 | - name: Setup Biome 14 | uses: biomejs/setup-biome@v1 15 | with: 16 | version: 1.4.1 17 | - name: Run Biome 18 | run: biome ci . 19 | -------------------------------------------------------------------------------- /src/stubs/vue/advanced-component.vue: -------------------------------------------------------------------------------- 1 | 21 | 24 | -------------------------------------------------------------------------------- /src/stubs/angular/component.component.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from "@angular/common"; 2 | 3 | Component({ 4 | selector: "SelectorName", 5 | standalone: true, 6 | imports: [CommonModule], 7 | 8 | template: ` 9 |

Hello, {{ title }}

10 |

Congratulations! Your component has been created. 🎉

11 | `, 12 | 13 | styleUrls: ` `, 14 | }); 15 | export class ComponentName { 16 | public title: string = "ComponentName"; 17 | 18 | constructor() {} 19 | } 20 | -------------------------------------------------------------------------------- /src/stubs/qwik/usestyles-component.tsx: -------------------------------------------------------------------------------- 1 | import { component$, useStyles$ } from "@builder.io/qwik"; 2 | 3 | export default component$(() => { 4 | // useStyles$ hook to apply styles to your component 5 | useStyles$(` 6 | .custom-style { 7 | color: blue; 8 | font-size: 20px; 9 | padding: 10px; 10 | border: 1px solid black; 11 | margin: 5px; 12 | } 13 | `); 14 | 15 | return ( 16 |
17 |

ComponentName

18 |
19 | ); 20 | }); 21 | -------------------------------------------------------------------------------- /src/stubs/qwik/usestore-component.tsx: -------------------------------------------------------------------------------- 1 | import { component$, useStore } from "@builder.io/qwik"; 2 | 3 | export default component$(() => { 4 | // useStore is a hook that creates a reactive state object in Qwik 5 | // Here, a store with a single property 'count' initialized to 0 is created 6 | // The store is reactive, meaning any changes to its properties will 7 | // trigger a re-render of the component that uses it 8 | const store = useStore({ count: 0 }); 9 | 10 | return ( 11 |
12 |

Count: {store.count}

13 |

14 | 15 |

16 |
17 | ); 18 | }); 19 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.4.1/schema.json", 3 | "organizeImports": { 4 | "enabled": true 5 | }, 6 | "files": { 7 | "ignore": ["src/stubs/*", "*.js", "*.cjs", "*.mjs", "src/components"] 8 | }, 9 | "linter": { 10 | "enabled": true, 11 | "rules": { 12 | "recommended": true 13 | } 14 | }, 15 | "javascript": { 16 | "formatter": { 17 | "enabled": true, 18 | "lineWidth": 120, 19 | "arrowParentheses": "asNeeded", 20 | "jsxQuoteStyle": "double", 21 | "semicolons": "always", 22 | "trailingComma": "es5", 23 | "quoteProperties": "asNeeded", 24 | "bracketSameLine": true 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/utils/frameworks/svelte/svelte.mts: -------------------------------------------------------------------------------- 1 | import inquirer from "inquirer"; 2 | 3 | const framework = "svelte"; 4 | export default function (componentName: string, folder: string) { 5 | return inquirer 6 | .prompt([ 7 | { 8 | type: "confirm", 9 | name: "typescript", 10 | message: "Do you want to use Typescript?", 11 | default: true, 12 | }, 13 | ]) 14 | .then((answers: { typescript: boolean }) => { 15 | return { 16 | componentName: componentName, 17 | framework: framework.toLowerCase(), 18 | template: answers.typescript ? "component-ts.svelte" : "component-js.svelte", 19 | folder: folder, 20 | }; 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /src/utils/frameworks/svelte/svelte.mjs: -------------------------------------------------------------------------------- 1 | import inquirer from "inquirer"; 2 | const framework = "svelte"; 3 | export default function (componentName, folder) { 4 | return inquirer 5 | .prompt([ 6 | { 7 | type: "confirm", 8 | name: "typescript", 9 | message: "Do you want to use Typescript?", 10 | default: true, 11 | }, 12 | ]) 13 | .then((answers) => { 14 | return { 15 | componentName: componentName, 16 | framework: framework.toLowerCase(), 17 | template: answers.typescript 18 | ? "component-ts.svelte" 19 | : "component-js.svelte", 20 | folder: folder, 21 | }; 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yaml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: Create a report to help us improve 3 | body: 4 | - type: markdown 5 | attributes: 6 | value: Thanks for taking the time to fill out this bug! If you need real-time help, join us on Discord. 7 | - type: textarea 8 | id: repro 9 | attributes: 10 | label: Reproduction steps 11 | description: "How do you trigger this bug? Please walk us through it step by step." 12 | value: 13 | render: bash 14 | validations: 15 | required: true 16 | - type: dropdown 17 | id: assignee 18 | attributes: 19 | label: Do you want to work on this issue? 20 | multiple: false 21 | options: 22 | - "No" 23 | - "Yes" 24 | default: 0 25 | -------------------------------------------------------------------------------- /src/utils/frameworks/qwik/qwik.mts: -------------------------------------------------------------------------------- 1 | import inquirer from "inquirer"; 2 | 3 | const framework = "qwik"; 4 | export default function (componentName: string, folder: string) { 5 | return inquirer 6 | .prompt([ 7 | { 8 | type: "list", 9 | name: "type", 10 | message: "Choose wich type of component to create", 11 | choices: ["Hello World", "useStore", "useStyles"], 12 | default: "Hello World", 13 | }, 14 | ]) 15 | .then((answers: { type: string }) => { 16 | return { 17 | componentName: componentName, 18 | framework: framework.toLowerCase(), 19 | template: 20 | answers.type === "Hello World" 21 | ? "hello-world-component.tsx" 22 | : answers.type === "useStore" 23 | ? "usestore-component.tsx" 24 | : answers.type === "useStyles" 25 | ? "usestyles-component.tsx" 26 | : "hello-world-component.tsx", 27 | folder: folder, 28 | }; 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created 2 | # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages 3 | 4 | name: Node.js Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions/setup-node@v3 16 | with: 17 | node-version: 18 18 | - run: npm ci 19 | - run: npx tsc 20 | 21 | publish-npm: 22 | needs: build 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v3 26 | - uses: actions/setup-node@v3 27 | with: 28 | node-version: 18 29 | registry-url: https://registry.npmjs.org/ 30 | - run: npm ci 31 | - run: npx tsc 32 | - run: npm publish 33 | env: 34 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 35 | -------------------------------------------------------------------------------- /cmd/make-js-component.mts: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | import createComponent, { createAnotherComponent } from "../src/utils/utils.mjs"; 4 | import wizard, { Answers } from "../src/utils/wizard.mjs"; 5 | 6 | enum vueApi { 7 | Composition = "composition", 8 | Option = "option", 9 | } 10 | 11 | wizard() 12 | .then((answers: Answers) => { 13 | const { componentName, framework, template, folder, anotherComponent, advancedOpts, advanced } = answers; 14 | const api = template.indexOf("composition") !== -1 ? vueApi.Composition : vueApi.Option; 15 | const t = advanced ? "advanced-component.vue" : template; 16 | if (anotherComponent) { 17 | createComponent(componentName, framework, t, folder, api, advancedOpts).then(() => { 18 | console.log("✅ Component created"); 19 | createAnotherComponent(); 20 | }); 21 | } else 22 | createComponent(componentName, framework, t, folder, api, advancedOpts).then(() => 23 | console.log("✅ Component created") 24 | ); 25 | }) 26 | .catch((e: Error) => { 27 | console.error(e.message); 28 | }); 29 | -------------------------------------------------------------------------------- /src/utils/frameworks/qwik/qwik.mjs: -------------------------------------------------------------------------------- 1 | import inquirer from "inquirer"; 2 | const framework = "qwik"; 3 | export default function (componentName, folder) { 4 | return inquirer 5 | .prompt([ 6 | { 7 | type: "list", 8 | name: "type", 9 | message: "Choose wich type of component to create", 10 | choices: ["Hello World", "useStore", "useStyles"], 11 | default: "Hello World", 12 | }, 13 | ]) 14 | .then((answers) => { 15 | return { 16 | componentName: componentName, 17 | framework: framework.toLowerCase(), 18 | template: answers.type === "Hello World" 19 | ? "hello-world-component.tsx" 20 | : answers.type === "useStore" 21 | ? "usestore-component.tsx" 22 | : answers.type === "useStyles" 23 | ? "usestyles-component.tsx" 24 | : "hello-world-component.tsx", 25 | folder: folder, 26 | }; 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /src/stubs/angular/component.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from "@angular/core/testing"; 2 | import { ComponentName } from "./component.component"; 3 | 4 | describe("ComponentName", () => { 5 | beforeEach(async () => { 6 | await TestBed.configureTestingModule({ 7 | imports: [ComponentName], 8 | }).compileComponents(); 9 | }); 10 | 11 | it("should create the app", () => { 12 | const fixture = TestBed.createComponent(ComponentName); 13 | const app = fixture.componentInstance; 14 | expect(app).toBeTruthy(); 15 | }); 16 | 17 | it(`should have the 'ComponentName' title`, () => { 18 | const fixture = TestBed.createComponent(ComponentName); 19 | const app = fixture.componentInstance; 20 | expect(app.title).toEqual("ComponentName"); 21 | }); 22 | 23 | it("should render title", () => { 24 | const fixture = TestBed.createComponent(ComponentName); 25 | fixture.detectChanges(); 26 | const compiled = fixture.nativeElement as HTMLElement; 27 | expect(compiled.querySelector("h2")?.textContent).toContain("Hello, ComponentName"); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /cmd/make-js-component.mjs: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | import createComponent, { createAnotherComponent } from "../src/utils/utils.mjs"; 3 | import wizard from "../src/utils/wizard.mjs"; 4 | var vueApi; 5 | (function (vueApi) { 6 | vueApi["Composition"] = "composition"; 7 | vueApi["Option"] = "option"; 8 | })(vueApi || (vueApi = {})); 9 | wizard() 10 | .then((answers) => { 11 | const { componentName, framework, template, folder, anotherComponent, advancedOpts, advanced } = answers; 12 | const api = template.indexOf("composition") !== -1 ? vueApi.Composition : vueApi.Option; 13 | const t = advanced ? "advanced-component.vue" : template; 14 | if (anotherComponent) { 15 | createComponent(componentName, framework, t, folder, api, advancedOpts).then(() => { 16 | console.log("✅ Component created"); 17 | createAnotherComponent(); 18 | }); 19 | } 20 | else 21 | createComponent(componentName, framework, t, folder, api, advancedOpts).then(() => console.log("✅ Component created")); 22 | }) 23 | .catch((e) => { 24 | console.error(e.message); 25 | }); 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Ghostylab 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "make-js-component", 3 | "version": "0.4.0", 4 | "description": "Easily create your js framework component in one command", 5 | "repository": "https://github.com/Giuliano1993/make-js-component", 6 | "bin": "./cmd/make-js-component.mjs", 7 | "scripts": { 8 | "dev": "npx tsc -w", 9 | "test": "jest", 10 | "biome-ci": "npx @biomejs/biome ci .", 11 | "biome-check": "npx @biomejs/biome check . --apply", 12 | "biome-lint": "npx @biomejs/biome lint . --apply", 13 | "biome-format": "npx @biomejs/biome format . --write" 14 | 15 | }, 16 | "keywords": [ 17 | "node", 18 | "js", 19 | "framework", 20 | "vue", 21 | "svelte", 22 | "react", 23 | "qwik", 24 | "astro", 25 | "command", 26 | "npx" 27 | ], 28 | "author": "Giuliano Gostinfini (Ghostylab)", 29 | "license": "ISC", 30 | "devDependencies": { 31 | "@biomejs/biome": "1.4.1", 32 | "@types/inquirer": "^9.0.7", 33 | "@types/node": "^20.9.0", 34 | "cli-testing-library": "^2.0.2", 35 | "jest": "^29.7.0", 36 | "jest-extended": "^4.0.2", 37 | "typescript": "^5.3.2" 38 | }, 39 | "dependencies": { 40 | "commander": "^11.1.0", 41 | "inquirer": "^9.2.12" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/utils/frameworks/vue/vue.mts: -------------------------------------------------------------------------------- 1 | import inquirer from "inquirer"; 2 | import { prepareAdvanced } from "../../utils.mjs"; 3 | 4 | const framework = "vue"; 5 | export default function (componentName: string, folder: string) { 6 | return inquirer 7 | .prompt([ 8 | { 9 | type: "list", 10 | name: "nuxt", 11 | message: "Do you use Nuxt? The destination folder will be (./components)", 12 | choices: ["yes", "No"], 13 | default: "No", 14 | }, 15 | { 16 | type: "list", 17 | name: "api", 18 | message: "Choose wich api to use", 19 | choices: ["Composition", "Options"], 20 | default: "Composition", 21 | }, 22 | ...prepareAdvanced(["props", "refs", "data", "mounted", "emits", "components"]), 23 | ]) 24 | .then( 25 | (answers: { 26 | nuxt: string; 27 | api: string; 28 | advanced: boolean; 29 | advancedOpts?: string[]; 30 | }) => { 31 | return { 32 | componentName: componentName, 33 | framework: framework, 34 | template: answers.api === "Composition" ? "component-composition.vue" : "component-options.vue", 35 | folder: answers.nuxt === "yes" ? (folder === "" ? "../../components" : `../../components/${folder}`) : folder, 36 | advanced: answers.advanced, 37 | api: answers.api.toLocaleLowerCase(), 38 | advancedOpts: answers.advancedOpts || [], 39 | }; 40 | } 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /src/utils/frameworks/react/react.mts: -------------------------------------------------------------------------------- 1 | import inquirer from "inquirer"; 2 | 3 | const framework = "react"; 4 | 5 | export default function (componentName: string, folder: string) { 6 | return inquirer 7 | .prompt([ 8 | { 9 | type: "confirm", 10 | name: "typescript", 11 | message: "Do you want to use Typescript?", 12 | default: true, 13 | }, 14 | { 15 | type: "list", 16 | name: "css", 17 | message: "Do you want to use any CSS framework?", 18 | choices: ["Tailwind", "Styled Components", "CSS Module", "No"], 19 | default: "No", 20 | }, 21 | ]) 22 | .then((answers: { typescript: boolean; css: string }) => { 23 | const { typescript } = answers; 24 | const { css } = answers; 25 | const extension = typescript ? "tsx" : "jsx"; 26 | let templateBase = "function-component"; 27 | 28 | switch (css) { 29 | case "Tailwind": 30 | templateBase += "-tailwind"; 31 | break; 32 | case "Styled Components": 33 | templateBase += "-styled-components"; 34 | break; 35 | case "CSS Module": 36 | templateBase += "-css-module"; 37 | break; 38 | default: 39 | break; 40 | } 41 | const template = `${templateBase}.${extension}`; 42 | 43 | return { 44 | componentName: componentName, 45 | framework: framework.toLowerCase(), 46 | template: template, 47 | folder: folder, 48 | }; 49 | }); 50 | } 51 | -------------------------------------------------------------------------------- /src/utils/frameworks/react/react.mjs: -------------------------------------------------------------------------------- 1 | import inquirer from "inquirer"; 2 | const framework = "react"; 3 | export default function (componentName, folder) { 4 | return inquirer 5 | .prompt([ 6 | { 7 | type: "confirm", 8 | name: "typescript", 9 | message: "Do you want to use Typescript?", 10 | default: true, 11 | }, 12 | { 13 | type: "list", 14 | name: "css", 15 | message: "Do you want to use any CSS framework?", 16 | choices: ["Tailwind", "Styled Components", "CSS Module", "No"], 17 | default: "No", 18 | }, 19 | ]) 20 | .then((answers) => { 21 | const { typescript } = answers; 22 | const { css } = answers; 23 | const extension = typescript ? "tsx" : "jsx"; 24 | let templateBase = "function-component"; 25 | 26 | switch (css) { 27 | case "Tailwind": 28 | templateBase += "-tailwind"; 29 | break; 30 | case "Styled Components": 31 | templateBase += "-styled-components"; 32 | break; 33 | case "CSS Module": 34 | templateBase += "-css-module"; 35 | break; 36 | default: 37 | break; 38 | } 39 | const template = `${templateBase}.${extension}`; 40 | 41 | return { 42 | componentName: componentName, 43 | framework: framework.toLowerCase(), 44 | template: template, 45 | folder: folder, 46 | }; 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /__tests__/menu.test.cjs: -------------------------------------------------------------------------------- 1 | const {render} = require('cli-testing-library'); 2 | const { default: exp } = require('constants'); 3 | const {resolve} = require('path') 4 | 5 | 6 | 7 | test('Open tool', async () => { 8 | const path = resolve(__dirname, "../cmd/make-js-component.mjs") 9 | const {clear, findByText, queryByText, userEvent, stdoutArr, debug} = await render('node', [ 10 | path 11 | ]) 12 | //console.log(path) 13 | const instance = await findByText('component') 14 | expect(instance).toBeInTheConsole() 15 | }) 16 | 17 | test('Component a name', async () => { 18 | const path = resolve(__dirname, "../cmd/make-js-component.mjs") 19 | const {clear, findByText, queryByText, userEvent, stdoutArr, debug} = await render('node', [ 20 | path 21 | ]) 22 | //console.log(path) 23 | userEvent.keyboard('componentName[Enter]'); 24 | expect(await findByText('src/components')).toBeInTheConsole() 25 | }) 26 | 27 | test('Default folder and pick React', async () => { 28 | const path = resolve(__dirname, "../cmd/make-js-component.mjs") 29 | const {clear, findByText, queryByText, userEvent, stdoutArr, debug} = await render('node', [ 30 | path 31 | ]) 32 | //console.log(path) 33 | userEvent.keyboard('componentName[Enter]'); 34 | expect(await findByText('src/components')).toBeInTheConsole() 35 | userEvent.keyboard('[Enter]'); 36 | userEvent.keyboard('[ArrowDown]') 37 | userEvent.keyboard('[ArrowDown]') 38 | expect(await findByText('❯ React')).toBeInTheConsole() 39 | 40 | }) 41 | -------------------------------------------------------------------------------- /src/utils/frameworks/vue/vue.mjs: -------------------------------------------------------------------------------- 1 | import inquirer from "inquirer"; 2 | import { prepareAdvanced } from "../../utils.mjs"; 3 | const framework = "vue"; 4 | export default function (componentName, folder) { 5 | return inquirer 6 | .prompt([ 7 | { 8 | type: "list", 9 | name: "nuxt", 10 | message: 11 | "Do you use Nuxt? The destination folder will be (./components)", 12 | choices: ["yes", "No"], 13 | default: "No", 14 | }, 15 | { 16 | type: "list", 17 | name: "api", 18 | message: "Choose wich api to use", 19 | choices: ["Composition", "Options"], 20 | default: "Composition", 21 | }, 22 | ...prepareAdvanced([ 23 | "props", 24 | "refs", 25 | "data", 26 | "mounted", 27 | "emits", 28 | "components", 29 | ]), 30 | ]) 31 | .then((answers) => { 32 | return { 33 | componentName: componentName, 34 | framework: framework, 35 | template: 36 | answers.api === "Composition" 37 | ? "component-composition.vue" 38 | : "component-options.vue", 39 | folder: 40 | answers.nuxt === "yes" 41 | ? folder === "" 42 | ? "../../components" 43 | : `../../components/${folder}` 44 | : folder, 45 | advanced: answers.advanced, 46 | api: answers.api.toLocaleLowerCase(), 47 | advancedOpts: answers.advancedOpts || [], 48 | }; 49 | }); 50 | } 51 | -------------------------------------------------------------------------------- /src/utils/configs.cjs: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { 3 | if (k2 === undefined) k2 = k; 4 | var desc = Object.getOwnPropertyDescriptor(m, k); 5 | if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { 6 | desc = { enumerable: true, get: function() { return m[k]; } }; 7 | } 8 | Object.defineProperty(o, k2, desc); 9 | }) : (function(o, m, k, k2) { 10 | if (k2 === undefined) k2 = k; 11 | o[k2] = m[k]; 12 | })); 13 | var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { 14 | Object.defineProperty(o, "default", { enumerable: true, value: v }); 15 | }) : function(o, v) { 16 | o["default"] = v; 17 | }); 18 | var __importStar = (this && this.__importStar) || function (mod) { 19 | if (mod && mod.__esModule) return mod; 20 | var result = {}; 21 | if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); 22 | __setModuleDefault(result, mod); 23 | return result; 24 | }; 25 | Object.defineProperty(exports, "__esModule", { value: true }); 26 | exports.configs = void 0; 27 | const path = __importStar(require("node:path")); 28 | const mainFilename = path.dirname(module?.filename || ""); 29 | const dir = path.join(mainFilename, "../.."); 30 | exports.configs = { 31 | INIT_PATH: dir, 32 | BASE_DIR: "./src", 33 | STUBS_DIR: "stubs", 34 | COMPONENT_FOLDER: "/components", 35 | }; 36 | -------------------------------------------------------------------------------- /src/utils/frameworks/angular/make-angular-component.mjs: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import path from "path"; 3 | import { configs } from "../../configs.cjs"; 4 | import { checkFileExists } from "../../utils.mjs"; 5 | export function makeAngularComponent(filePathDestination, component, componentName) { 6 | let componentContent = component.replace(/selector: 'SelectorName'/, `selector: 'app-${convertFromCamelCase(componentName)}'`); 7 | componentContent = replaceComponentName(componentContent, componentName); 8 | checkFileExists(filePathDestination, componentContent); 9 | makeAngularComponentTest(componentName); 10 | } 11 | function makeAngularComponentTest(componentName) { 12 | const templateFileTestPath = path.join(configs.INIT_PATH, "src", configs.STUBS_DIR, "angular", "component.component.spec.ts"); 13 | fs.readFile(templateFileTestPath, "utf8", (err, component) => { 14 | const componentContent = replaceComponentName(component, componentName); 15 | const filePathDestination = path.join(configs.BASE_DIR, configs.COMPONENT_FOLDER, `${componentName}.component.spec.ts`); 16 | checkFileExists(filePathDestination, componentContent); 17 | }); 18 | } 19 | function convertToCamelCase(string) { 20 | return string 21 | .replace(/-([a-z])/g, (s) => { 22 | return s.toUpperCase(); 23 | }) 24 | .replace(/^[a-z]/, s => { 25 | return s.toUpperCase(); 26 | }); 27 | } 28 | function convertFromCamelCase(string) { 29 | return string.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase(); 30 | } 31 | function replaceComponentName(data, componentName) { 32 | return data.replace(/ComponentName/g, `${convertToCamelCase(componentName)}Component`); 33 | } 34 | -------------------------------------------------------------------------------- /src/utils/frameworks/angular/make-angular-component.mts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import path from "path"; 3 | import { configs } from "../../configs.cjs"; 4 | import { ErrnoException, checkFileExists } from "../../utils.mjs"; 5 | 6 | export function makeAngularComponent(filePathDestination: string, component: string, componentName: string): void { 7 | let componentContent = component.replace( 8 | /selector: 'SelectorName'/, 9 | `selector: 'app-${convertFromCamelCase(componentName)}'` 10 | ); 11 | componentContent = replaceComponentName(componentContent, componentName); 12 | 13 | checkFileExists(filePathDestination, componentContent); 14 | makeAngularComponentTest(componentName); 15 | } 16 | 17 | function makeAngularComponentTest(componentName: string): void { 18 | const templateFileTestPath: string = path.join( 19 | configs.INIT_PATH, 20 | "src", 21 | configs.STUBS_DIR, 22 | "angular", 23 | "component.component.spec.ts" 24 | ); 25 | fs.readFile(templateFileTestPath, "utf8", (err: ErrnoException | null, component: string) => { 26 | const componentContent = replaceComponentName(component, componentName); 27 | const filePathDestination: string = path.join( 28 | configs.BASE_DIR, 29 | configs.COMPONENT_FOLDER, 30 | `${componentName}.component.spec.ts` 31 | ); 32 | checkFileExists(filePathDestination, componentContent); 33 | }); 34 | } 35 | 36 | function convertToCamelCase(string: string): string { 37 | return string 38 | .replace(/-([a-z])/g, (s: string) => { 39 | return s.toUpperCase(); 40 | }) 41 | .replace(/^[a-z]/, s => { 42 | return s.toUpperCase(); 43 | }); 44 | } 45 | 46 | function convertFromCamelCase(string: string): string { 47 | return string.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase(); 48 | } 49 | 50 | function replaceComponentName(data: string, componentName: string): string { 51 | return data.replace(/ComponentName/g, `${convertToCamelCase(componentName)}Component`); 52 | } 53 | -------------------------------------------------------------------------------- /src/utils/frameworks/vue/helper.mts: -------------------------------------------------------------------------------- 1 | export enum vueApi { 2 | Composition = "composition", 3 | Option = "option", 4 | } 5 | 6 | export default function advancedVueBuilder( 7 | data: string, 8 | componentType: vueApi, 9 | advancedOpts: string[] | undefined 10 | ): string { 11 | if (typeof advancedOpts === "undefined") return ""; 12 | let output = data; 13 | if (componentType === vueApi.Composition) { 14 | const replacable = { 15 | props: "const props = defineProps(['foo'])", 16 | emits: "const emit = defineEmits(['inFocus', 'submit'])", 17 | refs: "const element = ref(null)", 18 | mounted: `onMounted(() => { 19 | console.log("the component is now mounted.") 20 | })`, 21 | data: "", 22 | components: "", 23 | }; 24 | const importsFunctions: string[] = []; 25 | for (const key in replacable) { 26 | const codeInject = advancedOpts.indexOf(key) !== -1 ? replacable[key as keyof typeof replacable] : ""; 27 | const replacePattern = `__${key}__`; 28 | output = output.replaceAll(replacePattern, codeInject); 29 | if (key === "refs" && advancedOpts.indexOf(key) !== -1) { 30 | importsFunctions.push("ref"); 31 | } else if (key === "mounted" && advancedOpts.indexOf(key) !== -1) { 32 | importsFunctions.push("onMounted"); 33 | } 34 | } 35 | 36 | let imports = ""; 37 | if (importsFunctions.length > 0) { 38 | imports = `import { ${importsFunctions.join(", ")} } from 'vue'`; 39 | } 40 | output = output.replace("__refimport__", imports); 41 | } else if (componentType === vueApi.Option) { 42 | const replacable = { 43 | props: "props: ['foo'],", 44 | emits: "emits: ['inFocus', 'submit'],", 45 | data: "data:{},", 46 | mounted: "mounted(){},", 47 | refs: "", 48 | components: "components: {},", 49 | }; 50 | for (const key in replacable) { 51 | const codeInject = advancedOpts.indexOf(key) !== -1 ? replacable[key as keyof typeof replacable] : ""; 52 | const replacePattern = `__${key}__`; 53 | output = output.replaceAll(replacePattern, codeInject); 54 | } 55 | } 56 | output = cleanVueData(output, componentType); 57 | 58 | return output; 59 | } 60 | 61 | function cleanVueData(data: string, api: vueApi): string { 62 | const apiStart = api === vueApi.Composition ? "__compositionstart__" : "__optionsstart__"; 63 | const apiEnd = api === vueApi.Composition ? "__compositionend__" : "__optionsend__"; 64 | const deleteStart = api === vueApi.Composition ? "__optionsstart__" : "__compositionstart__"; 65 | const deleteEnd = api === vueApi.Composition ? "__optionsend__" : "__compositionend__"; 66 | 67 | const output = data.replace(apiStart, "").replace(apiEnd, ""); 68 | 69 | const start = output.indexOf(deleteStart); 70 | const end = output.indexOf(deleteEnd); 71 | return output.slice(0, start) + output.slice(end + deleteEnd.length); 72 | } 73 | -------------------------------------------------------------------------------- /src/utils/frameworks/vue/helper.mjs: -------------------------------------------------------------------------------- 1 | export var vueApi; 2 | (function (vueApi) { 3 | vueApi["Composition"] = "composition"; 4 | vueApi["Option"] = "option"; 5 | })(vueApi || (vueApi = {})); 6 | export default function advancedVueBuilder(data, componentType, advancedOpts) { 7 | if (typeof advancedOpts === "undefined") 8 | return ""; 9 | let output = data; 10 | if (componentType === vueApi.Composition) { 11 | const replacable = { 12 | props: "const props = defineProps(['foo'])", 13 | emits: "const emit = defineEmits(['inFocus', 'submit'])", 14 | refs: "const element = ref(null)", 15 | mounted: `onMounted(() => { 16 | console.log("the component is now mounted.") 17 | })`, 18 | data: "", 19 | components: "", 20 | }; 21 | const importsFunctions = []; 22 | for (const key in replacable) { 23 | const codeInject = advancedOpts.indexOf(key) !== -1 ? replacable[key] : ""; 24 | const replacePattern = `__${key}__`; 25 | output = output.replaceAll(replacePattern, codeInject); 26 | if (key === "refs" && advancedOpts.indexOf(key) !== -1) { 27 | importsFunctions.push("ref"); 28 | } 29 | else if (key === "mounted" && advancedOpts.indexOf(key) !== -1) { 30 | importsFunctions.push("onMounted"); 31 | } 32 | } 33 | let imports = ""; 34 | if (importsFunctions.length > 0) { 35 | imports = `import { ${importsFunctions.join(", ")} } from 'vue'`; 36 | } 37 | output = output.replace("__refimport__", imports); 38 | } 39 | else if (componentType === vueApi.Option) { 40 | const replacable = { 41 | props: "props: ['foo'],", 42 | emits: "emits: ['inFocus', 'submit'],", 43 | data: "data:{},", 44 | mounted: "mounted(){},", 45 | refs: "", 46 | components: "components: {},", 47 | }; 48 | for (const key in replacable) { 49 | const codeInject = advancedOpts.indexOf(key) !== -1 ? replacable[key] : ""; 50 | const replacePattern = `__${key}__`; 51 | output = output.replaceAll(replacePattern, codeInject); 52 | } 53 | } 54 | output = cleanVueData(output, componentType); 55 | return output; 56 | } 57 | function cleanVueData(data, api) { 58 | const apiStart = api === vueApi.Composition ? "__compositionstart__" : "__optionsstart__"; 59 | const apiEnd = api === vueApi.Composition ? "__compositionend__" : "__optionsend__"; 60 | const deleteStart = api === vueApi.Composition ? "__optionsstart__" : "__compositionstart__"; 61 | const deleteEnd = api === vueApi.Composition ? "__optionsend__" : "__compositionend__"; 62 | const output = data.replace(apiStart, "").replace(apiEnd, ""); 63 | const start = output.indexOf(deleteStart); 64 | const end = output.indexOf(deleteEnd); 65 | return output.slice(0, start) + output.slice(end + deleteEnd.length); 66 | } 67 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Issues & Pull Requests 2 | 3 | - [Issues \& Pull Requests](#issues--pull-requests) 4 | - [Getting started locally](#getting-started-locally) 5 | - [Creating an Issue](#creating-an-issue) 6 | - [Working on an Issue](#working-on-an-issue) 7 | - [Pull requests](#pull-requests) 8 | - [Adding a New Framework](#adding-a-new-framework) 9 | 10 | ### Getting started locally 11 | 12 | 1. Clone the repo 13 | 2. In the repo folder run `npm run dev`; this will start watching ts files and transpile them to js 14 | 3. Now you can run locally the command: just type `npx .` in your terminal to execute it 15 | 16 | ### Creating an Issue 17 | 18 | Before **creating** an Issue please follow these steps: 19 | 20 | 1. search existing Issues before creating a new issue (has someone raised this already) 21 | 2. if it doesn't exist create a new issue giving as much context as possible 22 | 3. if you wish to work on the Issue please check the relative checkbox 23 | 24 | ### Working on an Issue 25 | 26 | Before working on an existing Issue please follow these steps: 27 | 28 | 1. comment asking for the issue to be assigned to you 29 | 2. after the Issue is assigned to you, you can start working on it 30 | 3. **only** start working on this Issue (and open a Pull Request) when it has been assigned to you. 31 | 4. when forking the issue, create a branch for your edits 32 | 5. before pushing run `npm run biome-ci` to be sure that code formatting is correct and it will pass the PR workflow. 33 | 1. If some errors are highlighted, you can fix them by running the following commands: 34 | 1. `npm run biome-check` 35 | 2. `npm run biome-lint` ( in this case, some errors may remain, so you may need to address them individually) 36 | 3. `npm run biome-format` 37 | 6. reference the Issue in your Pull Request (for example `closes #123`) 38 | 7. please do **not** force push to your PR branch, this makes it very difficult to re-review - commits will be squashed when merged 39 | 40 | ### Pull requests 41 | 42 | Remember, before opening a PR, to have an issue assigned to work on! If you have an idea but you don't find any issue for it, first open an issue and ask to have it assigned! This way you don't risk to work on something which is already being worked on or that isn't needed right now! 43 | When the issue is assigned to you, you're welcome to start working on it, I'll be glad to merge it! 44 | 45 | ## Adding a New Framework 46 | 47 | Missing your favorite js framework? You can add it! 48 | 49 | 1. **Modify the Wizard:** 50 | 51 | - Create a file `.mts` for the framework inside the `src/utils/frameworks/{frameworkname}` folder. 52 | It should import `inquirer` and export a function that take `componentName` and `folder` as parameters. 53 | Here you can add some eventual extra questions specifics to this framework. Check the existing framework files as an example 54 | - Open the `wizard.mts`. 55 | - Import the file you previously created 56 | - Add the framework name to the `frameworks` array 57 | - Add the framework to the `switch-case` 58 | 59 | 2. **Create Stubs:** 60 | 61 | - Create a folder with the framework name insiede the `src/stubs` folder 62 | - Inside the folder add add as many files as the options made available by the wizard 63 | - Add templates for the new framework. These will serve as the initial structure for a component of that framework. 64 | 65 | 3. **Test the Command:** 66 | 67 | - Run the command with the new framework to ensure that it works as expected. 68 | 69 | 4. **Update Documentation:** 70 | - Add a new section in the README.md file under "Available Frameworks" to provide information about the newly added framework. 71 | - Include any specific instructions or choices related to the new framework or open an issue for this purpouse. 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![RepoRater](https://repo-rater.eddiehub.io/api/badge?owner=Giuliano1993&name=make-js-component)](https://repo-rater.eddiehub.io/rate?owner=Giuliano1993&name=make-js-component) 2 | 3 |

4 | 5 |

6 | 7 | # Make Js Component 8 | 9 | Make Js Component is an NPX command born with the purpose of streamline the process for developers of creating components with the many FE frameworks around here. 10 | 11 | Since some frameworks have standard commands, some had them, some really don't, the quickest thing is usually copy pasting compononent after component and then edit it. 12 | 13 | MJC allows you to just call a command and have your JS component ready to use, and edit, with also a bunch of options available in order to start with the perfect boilerplate. 14 | 15 | Can't find the framework or the options you need? Checkout the [Contributing guide](./CONTRIBUTING.md) and open an issue to let us know and, if you wish, you can open a PR to have the feature inclued in the command! 16 | 17 | - [Make Js Component](#make-js-component) 18 | - [Basic Usage](#basic-usage) 19 | - [Options](#options) 20 | - [--name](#--name) 21 | - [--folder](#--folder) 22 | - [--framework](#--framework) 23 | - [--\[framework\]](#--framework-1) 24 | - [--multiple](#--multiple) 25 | - [Available Frameworks](#available-frameworks) 26 | - [Vue](#vue) 27 | - [React](#react) 28 | - [Angular](#angular) 29 | - [Qwik](#qwik) 30 | - [Svelte](#svelte) 31 | - [Astro](#astro) 32 | - [Contributing](#contributing) 33 | - [Setup locally](#setup-locally) 34 | 35 | ## Basic Usage 36 | 37 | ```bash 38 | npx make-js-component 39 | ``` 40 | This command will start a short wizard that will create your component in a few steps. 41 | 42 | ### Options 43 | 44 | #### --name 45 | 46 | Specify the component name 47 | 48 | ```bash 49 | npx make-js-component --name 50 | ``` 51 | 52 | #### --folder 53 | 54 | Set the /components subfolder in which to create the new component/s. 55 | 56 | ```bash 57 | npx make-js-component --folder 58 | ``` 59 | 60 | #### --framework 61 | 62 | Set which framework your component is for. 63 | 64 | ```bash 65 | npx make-js-component --framework [vue|angular|react|svelte|qwik|astro] 66 | ``` 67 | 68 | #### --[framework] 69 | 70 | You can specify the desired framework directly by adding a flag. The available flags are the same as the options for --framework flag. 71 | 72 | ```bash 73 | #this will create a vue component 74 | npx make-js-component --vue 75 | ``` 76 | 77 | #### --multiple 78 | 79 | Using this flag allows you to create multiple components in succession. If you type “exit” while in the naming your component phase, it will exit the prompt. 80 | 81 | ```bash 82 | npx make-js-component --multiple 83 | ``` 84 | 85 | ## Available Frameworks 86 | 87 | ### Vue 88 | > Want to help with vue components? Check out [Vue related issues](https://github.com/Giuliano1993/make-js-component/issues?q=is%3Aissue+is%3Aopen+label%3AVue) 89 | 90 | When choosing Vue, the wizard will ask you whether you prefer to use the **Options API** or the **Composition API**, and you can make your selection using the arrow keys. 91 | 92 | ### React 93 | > Want to help with React components? Check out [React related issues](https://github.com/Giuliano1993/make-js-component/issues?q=is%3Aissue+is%3Aopen+label%3AReact) 94 | 95 | When choosing React, the wizard will ask you if you want to use **TypeScript** or not, and you can make your selection using the arrow keys. 96 | 97 | ### Angular 98 | > Want to help with Angular components? Check out [Angular related issues](https://github.com/Giuliano1993/make-js-component/issues?q=is%3Aissue+is%3Aopen+label%3AAngular) 99 | 100 | ### Qwik 101 | > Want to help with Qwik components? Check out [Qwik related issues](https://github.com/Giuliano1993/make-js-component/issues?q=is%3Aissue+is%3Aopen+label%3AQwik) 102 | 103 | ### Svelte 104 | > Want to help with Svelte components? Check out [Svelte related issues](https://github.com/Giuliano1993/make-js-component/issues?q=is%3Aissue+is%3Aopen+label%3ASvelte) 105 | 106 | ### Astro 107 | > Want to help with Astro components? Check out [Astro related issues](https://github.com/Giuliano1993/make-js-component/issues?q=is%3Aissue+is%3Aopen+label%3AAstro) 108 | 109 | ## Contributing 110 | 111 | Read the [Contributing guide](./CONTRIBUTING.md) for the contribution process 112 | 113 | ## Setup locally 114 | 115 | If you're cloning the repo, both for contributing or just to start taking confidence with the code just follow these steps: 116 | 117 | 1. clone the repo with `git clone https://github.com/Giuliano1993/make-js-component` 118 | 2. inside the folder run `npm install` 119 | 3. then to transpile ts files into js and watch them, run `npm run dev` 120 | 121 | To run your local version of the package and test it, run 122 | 123 | ```bash 124 | npx . 125 | ``` 126 | -------------------------------------------------------------------------------- /src/utils/wizard.mjs: -------------------------------------------------------------------------------- 1 | import { Command } from "commander"; 2 | import inquirer from "inquirer"; 3 | import angularWizard from "./frameworks/angular/angular.mjs"; 4 | import astroWizard from "./frameworks/astro/astro.mjs"; 5 | import qwikWizard from "./frameworks/qwik/qwik.mjs"; 6 | import reactWizard from "./frameworks/react/react.mjs"; 7 | import svelteWizard from "./frameworks/svelte/svelte.mjs"; 8 | import vueWizard from "./frameworks/vue/vue.mjs"; 9 | import { capitalizeFirstLetter } from "./utils.mjs"; 10 | const program = new Command(); 11 | const wizard = async () => { 12 | // Parse command line arguments using commander 13 | const frameworks = ["Vue", "Angular", "React", "Svelte", "Qwik", "Astro"]; 14 | program 15 | .option("--name ", "Specify a name") 16 | .option( 17 | "-f, --framework ", 18 | `Specify framework [${frameworks.join("|")}]` 19 | ) 20 | .option("--vue", "Create a Vue component") 21 | .option("--angular", "Create an Angular component") 22 | .option("--react", "Create a React component") 23 | .option("--svelte", "Create a Svelte component") 24 | .option("--qwik", "Create a Qwik component") 25 | .option("--astro", "Create an Astro component") 26 | .option("--folder ", "Specify the subfolder") 27 | .option("--multiple", "Creating multiple components at once") 28 | .parse(process.argv); 29 | const options = program.opts(); 30 | const componentNameFromFlag = options.name || ""; 31 | const frameworkFromFlag = 32 | options.framework || options.vue 33 | ? "vue" 34 | : null || options.angular 35 | ? "angular" 36 | : null || options.react 37 | ? "react" 38 | : null || options.svelte 39 | ? "svelte" 40 | : null || options.qwik 41 | ? "qwik" 42 | : null || options.astro 43 | ? "astro" 44 | : null || ""; 45 | const folderFromFlag = options.folder || ""; 46 | const multipleFromFlag = options.multiple || false; 47 | 48 | const prompts = []; 49 | // Only ask for componentName if --name argument is not provided 50 | if (!componentNameFromFlag) { 51 | prompts.push({ 52 | type: "input", 53 | name: "componentName", 54 | message: "Give a name to your component", 55 | validate: (input) => { 56 | const trimmedInput = input.trim(); 57 | if (trimmedInput === "") { 58 | return "Component name cannot be empty"; 59 | } 60 | if (multipleFromFlag && trimmedInput === "exit") { 61 | process.exit(); 62 | } 63 | // Use a regular expression to check for only alphanumeric characters 64 | const isValid = /^[A-Za-z0-9]+(-[A-Za-z0-9]+)*$/.test(trimmedInput); 65 | return ( 66 | isValid || "Component name can only contain alphanumeric characters" 67 | ); 68 | }, 69 | }); 70 | } 71 | if (!folderFromFlag) { 72 | prompts.push({ 73 | type: "input", 74 | name: "folder", 75 | message: "Custom path for the component (default: src/components)", 76 | default: "", 77 | }); 78 | } 79 | if (!frameworkFromFlag) { 80 | prompts.push({ 81 | type: "list", 82 | name: "framework", 83 | message: "Pick a framework to create the component for", 84 | choices: frameworks, 85 | }); 86 | } 87 | 88 | return inquirer 89 | .prompt(prompts) 90 | .then((answers) => { 91 | const folder = answers.folder || folderFromFlag; 92 | const framework = 93 | answers.framework || capitalizeFirstLetter(frameworkFromFlag); 94 | const componentName = answers.componentName || componentNameFromFlag; 95 | switch (framework) { 96 | case "Vue": 97 | return vueWizard(componentName, folder); 98 | case "Angular": 99 | return angularWizard(componentName, folder); 100 | case "React": 101 | return reactWizard(componentName, folder); 102 | case "Svelte": 103 | return svelteWizard(componentName, folder); 104 | case "Qwik": 105 | return qwikWizard(componentName, folder); 106 | case "Astro": 107 | return astroWizard(componentName, folder); 108 | default: 109 | throw new Error("A valid framework must be selected"); 110 | } 111 | }) 112 | .then((values) => { 113 | if (!multipleFromFlag) { 114 | return inquirer 115 | .prompt([ 116 | { 117 | type: "confirm", 118 | name: "anotherComponent", 119 | message: "Do you want to create another component?", 120 | default: false, 121 | }, 122 | ]) 123 | .then((answers) => { 124 | const { anotherComponent } = answers; 125 | const completeValues = { 126 | ...values, 127 | anotherComponent: anotherComponent, 128 | }; 129 | return completeValues; 130 | }); 131 | } 132 | return { ...values, anotherComponent: true }; 133 | }) 134 | .catch((e) => { 135 | throw new Error(e.message); 136 | }); 137 | }; 138 | export default wizard; 139 | -------------------------------------------------------------------------------- /src/utils/utils.mts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as path from "node:path"; 3 | import { configs } from "./configs.cjs"; 4 | import { makeAngularComponent } from "./frameworks/angular/make-angular-component.mjs"; 5 | 6 | import inquirer from "inquirer"; 7 | import advancedVueBuilder, { vueApi } from "./frameworks/vue/helper.mjs"; 8 | import wizard, { Answers } from "./wizard.mjs"; 9 | 10 | export interface ErrnoException extends Error { 11 | errno?: number | undefined; 12 | code?: string | undefined; 13 | path?: string | undefined; 14 | syscall?: string | undefined; 15 | } 16 | 17 | export default async function createComponent( 18 | componentName: string, 19 | framework: string, 20 | template: string, 21 | customFolder: string, 22 | api: vueApi, 23 | advancedOpts: string[] | undefined 24 | ) { 25 | const destinationFolder: string = `${configs.BASE_DIR}${configs.COMPONENT_FOLDER}`; 26 | if (!fs.existsSync(destinationFolder)) { 27 | fs.mkdirSync(destinationFolder); 28 | } 29 | 30 | const templateFilePath: string = path.join(configs.INIT_PATH, "src", configs.STUBS_DIR, framework, template); 31 | fs.readFile(templateFilePath, "utf8", async (err: ErrnoException | null, data: string) => { 32 | const customDestinationFolder: string = path.join(configs.BASE_DIR, configs.COMPONENT_FOLDER, customFolder); 33 | const extension = template.substring(template.indexOf(".")); 34 | const compFileName = `${componentName}${extension}`; 35 | 36 | if (!fs.existsSync(customDestinationFolder)) { 37 | fs.mkdirSync(customDestinationFolder, { recursive: true }); 38 | } 39 | 40 | const filePathDestination: string = path.join( 41 | configs.BASE_DIR, 42 | configs.COMPONENT_FOLDER, 43 | customFolder, 44 | compFileName 45 | ); 46 | let output = data; 47 | if (framework === "angular") { 48 | makeAngularComponent(filePathDestination, output, componentName); 49 | } else { 50 | if (template.indexOf("advanced") !== -1) { 51 | switch (framework) { 52 | case "vue": 53 | output = advancedVueBuilder(output, api, advancedOpts); 54 | break; 55 | default: 56 | break; 57 | } 58 | } 59 | output = output.replaceAll("ComponentName", capitalizeFirstLetter(componentName)); 60 | await checkFileExists(filePathDestination, output); 61 | return filePathDestination; 62 | } 63 | if (path.parse(template).name === "function-component-css-module") { 64 | const styleFileName: string = `${componentName}.module.css`; 65 | const styleFilePathDestination: string = path.join( 66 | configs.BASE_DIR, 67 | configs.COMPONENT_FOLDER, 68 | customFolder, 69 | styleFileName 70 | ); 71 | await checkFileExists( 72 | styleFilePathDestination, 73 | `.${componentName} {\n\tfont-size: 1.125rem; /* 18px */\n\tline-height: 1.75rem; /* 28px */\n\tfont-weight: bold;\n}\n` 74 | ); 75 | return filePathDestination; 76 | } 77 | }); 78 | } 79 | 80 | export async function checkFileExists(filePathDestination: string, data: string) { 81 | if (fs.existsSync(filePathDestination)) { 82 | console.log(`⚠️ A component with this name and extension already exists in ${filePathDestination}`); 83 | inquirer 84 | .prompt([ 85 | { 86 | type: "confirm", 87 | name: "duplicateFile", 88 | message: "Do you want to continue with component creation? NOTE: this action will override the existing file", 89 | default: false, 90 | }, 91 | ]) 92 | .then((answer: { duplicateFile: boolean }) => { 93 | if (answer.duplicateFile) { 94 | (async () => { 95 | await writeFile(filePathDestination, data); 96 | })(); 97 | } else { 98 | return console.log("❌ File not created"); 99 | } 100 | }); 101 | } else { 102 | await writeFile(filePathDestination, data); 103 | } 104 | } 105 | 106 | async function writeFile(filePathDestination: string, data: string) { 107 | fs.writeFile(filePathDestination, data, (err: ErrnoException | null) => { 108 | if (err) { 109 | console.error(err); 110 | } 111 | }); 112 | } 113 | 114 | export function createAnotherComponent() { 115 | enum vueApi { 116 | Composition = "composition", 117 | Option = "option", 118 | } 119 | 120 | wizard() 121 | .then((answers: Answers) => { 122 | const { componentName, framework, template, folder, anotherComponent, advancedOpts, advanced } = answers; 123 | const api = template.indexOf("composition") !== -1 ? vueApi.Composition : vueApi.Option; 124 | const t = advanced ? "advanced-component.vue" : template; 125 | createComponent(componentName, framework, t, folder, api, advancedOpts); 126 | if (anotherComponent) { 127 | createAnotherComponent(); 128 | } 129 | }) 130 | .catch((e: Error) => { 131 | console.error(e.message); 132 | }); 133 | return; 134 | } 135 | 136 | export function capitalizeFirstLetter(string: string): string { 137 | return string.charAt(0).toUpperCase() + string.slice(1); 138 | } 139 | 140 | export function prepareAdvanced(options: string[]) { 141 | const arr = [ 142 | { 143 | type: "confirm", 144 | name: "advanced", 145 | message: "Do you want to check for advanced options?", 146 | default: false, 147 | }, 148 | { 149 | type: "checkbox", 150 | name: "advancedOpts", 151 | message: "Pick the parts you want in your component?", 152 | choices: options, 153 | when: (answers: { nuxt: string; api: string; advanced: boolean }) => { 154 | return answers.advanced; 155 | }, 156 | default: false, 157 | }, 158 | ]; 159 | 160 | return [...arr]; 161 | } 162 | -------------------------------------------------------------------------------- /src/utils/utils.mjs: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as path from "node:path"; 3 | import { configs } from "./configs.cjs"; 4 | import { makeAngularComponent } from "./frameworks/angular/make-angular-component.mjs"; 5 | import inquirer from "inquirer"; 6 | import advancedVueBuilder from "./frameworks/vue/helper.mjs"; 7 | import wizard from "./wizard.mjs"; 8 | export default async function createComponent( 9 | componentName, 10 | framework, 11 | template, 12 | customFolder, 13 | api, 14 | advancedOpts 15 | ) { 16 | const destinationFolder = `${configs.BASE_DIR}${configs.COMPONENT_FOLDER}`; 17 | if (!fs.existsSync(destinationFolder)) { 18 | fs.mkdirSync(destinationFolder); 19 | } 20 | const templateFilePath = path.join( 21 | configs.INIT_PATH, 22 | "src", 23 | configs.STUBS_DIR, 24 | framework, 25 | template 26 | ); 27 | fs.readFile(templateFilePath, "utf8", async (err, data) => { 28 | const customDestinationFolder = path.join( 29 | configs.BASE_DIR, 30 | configs.COMPONENT_FOLDER, 31 | customFolder 32 | ); 33 | const extension = template.substring(template.indexOf(".")); 34 | const compFileName = `${componentName}${extension}`; 35 | if (!fs.existsSync(customDestinationFolder)) { 36 | fs.mkdirSync(customDestinationFolder, { recursive: true }); 37 | } 38 | const filePathDestination = path.join( 39 | configs.BASE_DIR, 40 | configs.COMPONENT_FOLDER, 41 | customFolder, 42 | compFileName 43 | ); 44 | let output = data; 45 | if (framework === "angular") { 46 | makeAngularComponent(filePathDestination, output, componentName); 47 | } else { 48 | if (template.indexOf("advanced") !== -1) { 49 | switch (framework) { 50 | case "vue": 51 | output = advancedVueBuilder(output, api, advancedOpts); 52 | break; 53 | default: 54 | break; 55 | } 56 | } 57 | output = output.replaceAll( 58 | "ComponentName", 59 | capitalizeFirstLetter(componentName) 60 | ); 61 | await checkFileExists(filePathDestination, output); 62 | return filePathDestination; 63 | } 64 | if (path.parse(template).name === "function-component-css-module") { 65 | const styleFileName = `${componentName}.module.css`; 66 | const styleFilePathDestination = path.join( 67 | configs.BASE_DIR, 68 | configs.COMPONENT_FOLDER, 69 | customFolder, 70 | styleFileName 71 | ); 72 | await checkFileExists( 73 | styleFilePathDestination, 74 | `.${componentName} {\n\tfont-size: 1.125rem; /* 18px */\n\tline-height: 1.75rem; /* 28px */\n\tfont-weight: bold;\n}\n` 75 | ); 76 | return filePathDestination; 77 | } 78 | }); 79 | } 80 | export async function checkFileExists(filePathDestination, data) { 81 | if (fs.existsSync(filePathDestination)) { 82 | console.log( 83 | `⚠️ A component with this name and extension already exists in ${filePathDestination}` 84 | ); 85 | inquirer 86 | .prompt([ 87 | { 88 | type: "confirm", 89 | name: "duplicateFile", 90 | message: 91 | "Do you want to continue with component creation? NOTE: this action will override the existing file", 92 | default: false, 93 | }, 94 | ]) 95 | .then((answer) => { 96 | if (answer.duplicateFile) { 97 | (async () => { 98 | await writeFile(filePathDestination, data); 99 | })(); 100 | } else { 101 | return console.log("❌ File not created"); 102 | } 103 | }); 104 | } else { 105 | await writeFile(filePathDestination, data); 106 | } 107 | } 108 | async function writeFile(filePathDestination, data) { 109 | fs.writeFile(filePathDestination, data, (err) => { 110 | if (err) { 111 | console.error(err); 112 | } 113 | }); 114 | } 115 | export function createAnotherComponent() { 116 | let vueApi; 117 | (function (vueApi) { 118 | vueApi["Composition"] = "composition"; 119 | vueApi["Option"] = "option"; 120 | })(vueApi || (vueApi = {})); 121 | wizard() 122 | .then((answers) => { 123 | const { 124 | componentName, 125 | framework, 126 | template, 127 | folder, 128 | anotherComponent, 129 | advancedOpts, 130 | advanced, 131 | } = answers; 132 | const api = 133 | template.indexOf("composition") !== -1 134 | ? vueApi.Composition 135 | : vueApi.Option; 136 | const t = advanced ? "advanced-component.vue" : template; 137 | createComponent(componentName, framework, t, folder, api, advancedOpts); 138 | if (anotherComponent) { 139 | createAnotherComponent(); 140 | } 141 | }) 142 | .catch((e) => { 143 | console.error(e.message); 144 | }); 145 | return; 146 | } 147 | export function capitalizeFirstLetter(string) { 148 | return string.charAt(0).toUpperCase() + string.slice(1); 149 | } 150 | export function prepareAdvanced(options) { 151 | const arr = [ 152 | { 153 | type: "confirm", 154 | name: "advanced", 155 | message: "Do you want to check for advanced options?", 156 | default: false, 157 | }, 158 | { 159 | type: "checkbox", 160 | name: "advancedOpts", 161 | message: "Pick the parts you want in your component?", 162 | choices: options, 163 | when: (answers) => { 164 | return answers.advanced; 165 | }, 166 | default: false, 167 | }, 168 | ]; 169 | return [...arr]; 170 | } 171 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | ghostylab@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /src/utils/wizard.mts: -------------------------------------------------------------------------------- 1 | import { Command, OptionValues } from "commander"; 2 | import inquirer from "inquirer"; 3 | import angularWizard from "./frameworks/angular/angular.mjs"; 4 | import astroWizard from "./frameworks/astro/astro.mjs"; 5 | import qwikWizard from "./frameworks/qwik/qwik.mjs"; 6 | import reactWizard from "./frameworks/react/react.mjs"; 7 | import svelteWizard from "./frameworks/svelte/svelte.mjs"; 8 | import vueWizard from "./frameworks/vue/vue.mjs"; 9 | import { capitalizeFirstLetter } from "./utils.mjs"; 10 | 11 | const program = new Command(); 12 | 13 | export type Answers = { 14 | componentName: string; 15 | framework: string; 16 | template: string; 17 | folder: string; 18 | anotherComponent?: boolean; 19 | advanced?: boolean; 20 | advancedOpts?: string[]; 21 | api?: string; 22 | }; 23 | 24 | type FrameworkFromFlagType = "vue" | "angular" | "react" | "svelte" | "qwik" | "astro" | ""; 25 | 26 | type FrameworksType = "Vue" | "Angular" | "React" | "Svelte" | "Qwik" | "Astro"; 27 | 28 | interface PromptProps { 29 | readonly type: string; 30 | readonly name: string; 31 | readonly message: string; 32 | readonly validate?: (input: string) => boolean | string; 33 | readonly default?: string | boolean; 34 | readonly choices?: FrameworksType[]; 35 | } 36 | 37 | const wizard: () => Promise = async () => { 38 | // Parse command line arguments using commander 39 | const frameworks: FrameworksType[] = ["Vue", "Angular", "React", "Svelte", "Qwik", "Astro"]; 40 | 41 | program 42 | .option("--name ", "Specify a name") 43 | .option("-f, --framework ", `Specify framework [${frameworks.join("|")}]`) 44 | .option("--vue", "Create a Vue component") 45 | .option("--angular", "Create an Angular component") 46 | .option("--react", "Create a React component") 47 | .option("--svelte", "Create a Svelte component") 48 | .option("--qwik", "Create a Qwik component") 49 | .option("--astro", "Create an Astro component") 50 | .option("--folder ", "Specify the subfolder") 51 | .option("--multiple", "Creating multiple components at once") 52 | .parse(process.argv); 53 | 54 | const options: OptionValues = program.opts(); 55 | const componentNameFromFlag: string = options.name || ""; 56 | 57 | const frameworkFromFlag: FrameworkFromFlagType = 58 | options.framework || options.vue 59 | ? "vue" 60 | : null || options.angular 61 | ? "angular" 62 | : null || options.react 63 | ? "react" 64 | : null || options.svelte 65 | ? "svelte" 66 | : null || options.qwik 67 | ? "qwik" 68 | : null || options.astro 69 | ? "astro" 70 | : null || ""; 71 | 72 | const folderFromFlag: string = options.folder || ""; 73 | const multipleFromFlag: boolean = options.multiple || false; 74 | 75 | const prompts: PromptProps[] = []; 76 | 77 | // Only ask for componentName if --name argument is not provided 78 | if (!componentNameFromFlag) { 79 | prompts.push({ 80 | type: "input", 81 | name: "componentName", 82 | message: "Give a name to your component", 83 | validate: (input: string) => { 84 | const trimmedInput: string = input.trim(); 85 | if (trimmedInput === "") { 86 | return "Component name cannot be empty"; 87 | } 88 | if (multipleFromFlag && trimmedInput === "exit") { 89 | process.exit(); 90 | } 91 | // Use a regular expression to check for only alphanumeric characters 92 | const isValid: boolean = /^[A-Za-z0-9]+(-[A-Za-z0-9]+)*$/.test(trimmedInput); 93 | return isValid || "Component name can only contain alphanumeric characters"; 94 | }, 95 | }); 96 | } 97 | 98 | if (!folderFromFlag) { 99 | prompts.push({ 100 | type: "input", 101 | name: "folder", 102 | message: "Custom path for the component (default: src/components)", 103 | default: "", 104 | }); 105 | } 106 | 107 | if (!frameworkFromFlag) { 108 | prompts.push({ 109 | type: "list", 110 | name: "framework", 111 | message: "Pick a framework to create the component for", 112 | choices: frameworks, 113 | }); 114 | } 115 | 116 | return inquirer 117 | .prompt(prompts) 118 | .then( 119 | (answers: { 120 | componentName: string; 121 | folder: string; 122 | framework: string; 123 | }) => { 124 | const folder: string = answers.folder || folderFromFlag; 125 | const framework: string = answers.framework || capitalizeFirstLetter(frameworkFromFlag); 126 | const componentName: string = answers.componentName || componentNameFromFlag; 127 | 128 | switch (framework) { 129 | case "Vue": 130 | return vueWizard(componentName, folder); 131 | case "Angular": 132 | return angularWizard(componentName, folder); 133 | case "React": 134 | return reactWizard(componentName, folder); 135 | case "Svelte": 136 | return svelteWizard(componentName, folder); 137 | case "Qwik": 138 | return qwikWizard(componentName, folder); 139 | case "Astro": 140 | return astroWizard(componentName, folder); 141 | default: 142 | throw new Error("A valid framework must be selected"); 143 | } 144 | } 145 | ) 146 | .then((values: Answers) => { 147 | if (!multipleFromFlag) { 148 | return inquirer 149 | .prompt([ 150 | { 151 | type: "confirm", 152 | name: "anotherComponent", 153 | message: "Do you want to create another component?", 154 | default: false, 155 | }, 156 | ]) 157 | .then((answers: { values: Answers; anotherComponent: boolean }) => { 158 | const { anotherComponent } = answers; 159 | const completeValues = { 160 | ...values, 161 | anotherComponent: anotherComponent, 162 | }; 163 | return completeValues; 164 | }); 165 | } 166 | return { ...values, anotherComponent: true }; 167 | }) 168 | .catch((e: Error) => { 169 | throw new Error(e.message); 170 | }); 171 | }; 172 | export default wizard; 173 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * For a detailed explanation regarding each configuration property, visit: 3 | * https://jestjs.io/docs/configuration 4 | */ 5 | 6 | /** @type {import('jest').Config} */ 7 | const config = { 8 | // All imported modules in your tests should be mocked automatically 9 | // automock: false, 10 | 11 | // Stop running tests after `n` failures 12 | // bail: 0, 13 | 14 | // The directory where Jest should store its cached dependency information 15 | // cacheDirectory: "C:\\Users\\giuli\\AppData\\Local\\Temp\\jest", 16 | 17 | // Automatically clear mock calls, instances, contexts and results before every test 18 | // clearMocks: false, 19 | 20 | // Indicates whether the coverage information should be collected while executing the test 21 | // collectCoverage: false, 22 | 23 | // An array of glob patterns indicating a set of files for which coverage information should be collected 24 | // collectCoverageFrom: undefined, 25 | 26 | // The directory where Jest should output its coverage files 27 | // coverageDirectory: undefined, 28 | 29 | // An array of regexp pattern strings used to skip coverage collection 30 | // coveragePathIgnorePatterns: [ 31 | // "\\\\node_modules\\\\" 32 | // ], 33 | 34 | // Indicates which provider should be used to instrument code for coverage 35 | coverageProvider: "v8", 36 | 37 | // A list of reporter names that Jest uses when writing coverage reports 38 | // coverageReporters: [ 39 | // "json", 40 | // "text", 41 | // "lcov", 42 | // "clover" 43 | // ], 44 | 45 | // An object that configures minimum threshold enforcement for coverage results 46 | // coverageThreshold: undefined, 47 | 48 | // A path to a custom dependency extractor 49 | // dependencyExtractor: undefined, 50 | 51 | // Make calling deprecated APIs throw helpful error messages 52 | // errorOnDeprecated: false, 53 | 54 | // The default configuration for fake timers 55 | // fakeTimers: { 56 | // "enableGlobally": false 57 | // }, 58 | 59 | // Force coverage collection from ignored files using an array of glob patterns 60 | // forceCoverageMatch: [], 61 | 62 | // A path to a module which exports an async function that is triggered once before all test suites 63 | // globalSetup: undefined, 64 | 65 | // A path to a module which exports an async function that is triggered once after all test suites 66 | // globalTeardown: undefined, 67 | 68 | // A set of global variables that need to be available in all test environments 69 | // globals: {}, 70 | 71 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 72 | // maxWorkers: "50%", 73 | 74 | // An array of directory names to be searched recursively up from the requiring module's location 75 | // moduleDirectories: [ 76 | // "node_modules" 77 | // ], 78 | 79 | // An array of file extensions your modules use 80 | // moduleFileExtensions: [ 81 | // "js", 82 | // "mjs", 83 | // "cjs", 84 | // "jsx", 85 | // "ts", 86 | // "tsx", 87 | // "json", 88 | // "node" 89 | // ], 90 | 91 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module 92 | // moduleNameMapper: {}, 93 | 94 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 95 | // modulePathIgnorePatterns: [], 96 | 97 | // Activates notifications for test results 98 | // notify: false, 99 | 100 | // An enum that specifies notification mode. Requires { notify: true } 101 | // notifyMode: "failure-change", 102 | 103 | // A preset that is used as a base for Jest's configuration 104 | // preset: undefined, 105 | 106 | // Run tests from one or more projects 107 | // projects: undefined, 108 | 109 | // Use this configuration option to add custom reporters to Jest 110 | // reporters: undefined, 111 | 112 | // Automatically reset mock state before every test 113 | // resetMocks: false, 114 | 115 | // Reset the module registry before running each individual test 116 | // resetModules: false, 117 | 118 | // A path to a custom resolver 119 | // resolver: undefined, 120 | 121 | // Automatically restore mock state and implementation before every test 122 | // restoreMocks: false, 123 | 124 | // The root directory that Jest should scan for tests and modules within 125 | // rootDir: undefined, 126 | 127 | // A list of paths to directories that Jest should use to search for files in 128 | // roots: [ 129 | // "" 130 | // ], 131 | 132 | // Allows you to use a custom runner instead of Jest's default test runner 133 | // runner: "jest-runner", 134 | 135 | // The paths to modules that run some code to configure or set up the testing environment before each test 136 | // setupFiles: [], 137 | 138 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 139 | setupFilesAfterEnv: ["./setup.jest.js"], 140 | 141 | // The number of seconds after which a test is considered as slow and reported as such in the results. 142 | // slowTestThreshold: 5, 143 | 144 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 145 | // snapshotSerializers: [], 146 | 147 | // The test environment that will be used for testing 148 | // testEnvironment: "jest-environment-node", 149 | 150 | // Options that will be passed to the testEnvironment 151 | // testEnvironmentOptions: {}, 152 | 153 | // Adds a location field to test results 154 | // testLocationInResults: false, 155 | 156 | // The glob patterns Jest uses to detect test files 157 | testMatch: [ 158 | "**/__tests__/**/*.(c)[jt]s?(x)", 159 | "**/?(*.)+(spec|test).(c)[tj]s?(x)" 160 | ], 161 | 162 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 163 | // testPathIgnorePatterns: [ 164 | // "\\\\node_modules\\\\" 165 | // ], 166 | 167 | // The regexp pattern or array of patterns that Jest uses to detect test files 168 | // testRegex: [], 169 | 170 | // This option allows the use of a custom results processor 171 | // testResultsProcessor: undefined, 172 | 173 | // This option allows use of a custom test runner 174 | // testRunner: "jest-circus/runner", 175 | 176 | // A map from regular expressions to paths to transformers 177 | // transform: undefined, 178 | 179 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 180 | // transformIgnorePatterns: [ 181 | // "\\\\node_modules\\\\", 182 | // "\\.pnp\\.[^\\\\]+$" 183 | // ], 184 | 185 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 186 | // unmockedModulePathPatterns: undefined, 187 | 188 | // Indicates whether each individual test should be reported during the run 189 | // verbose: undefined, 190 | 191 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 192 | // watchPathIgnorePatterns: [], 193 | 194 | // Whether to use watchman for file crawling 195 | // watchman: true, 196 | }; 197 | 198 | module.exports = config; 199 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "ES2022", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | "lib": [ 16 | "ES2022" 17 | ], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 18 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 19 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ 20 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 21 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 22 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 23 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 24 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 25 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 26 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 27 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 28 | 29 | /* Modules */ 30 | "module": "NodeNext", /* Specify what module code is generated. */ 31 | // "rootDir": "./", /* Specify the root folder within your source files. */ 32 | "moduleResolution": "nodenext", /* Specify how TypeScript looks up a file from a given module specifier. */ 33 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 34 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 35 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 36 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 37 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 38 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 39 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 40 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 41 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 42 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 43 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 44 | // "resolveJsonModule": true, /* Enable importing .json files. */ 45 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 46 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 47 | 48 | /* JavaScript Support */ 49 | //"allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 50 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 51 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 52 | 53 | /* Emit */ 54 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 55 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 56 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 57 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 58 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 59 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 60 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 61 | // "removeComments": true, /* Disable emitting comments. */ 62 | // "noEmit": true, /* Disable emitting files from a compilation. */ 63 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 64 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 65 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 66 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 67 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 68 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 69 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 70 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 71 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 72 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 73 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 74 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 75 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 76 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 77 | 78 | /* Interop Constraints */ 79 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 80 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 81 | "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 82 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 83 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 84 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 85 | 86 | /* Type Checking */ 87 | "strict": true, /* Enable all strict type-checking options. */ 88 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 89 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 90 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 91 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 92 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 93 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 94 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 95 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 96 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 97 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 98 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 99 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 100 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 101 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 102 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 103 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 104 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 105 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 106 | 107 | /* Completeness */ 108 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 109 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 110 | }, 111 | "exclude": [ 112 | "./src/**/*.tsx", 113 | "./src/components/*", 114 | "./src/stubs/angular/*", 115 | ], 116 | } 117 | --------------------------------------------------------------------------------