├── .gitignore ├── .prettierrc ├── README.md ├── package-lock.json ├── package.json ├── reset.d.ts ├── src ├── cli │ ├── constants.ts │ ├── index.ts │ └── types.ts ├── index.ts └── utils │ ├── createProject.ts │ ├── getUserPkgManager.ts │ ├── git.ts │ ├── index.ts │ ├── installDependencies.ts │ ├── logNextSteps.ts │ ├── logger.ts │ ├── packageJson.ts │ ├── path.ts │ ├── scaffoldProject.ts │ └── validators.ts ├── template ├── .prettierrc ├── LICENSE ├── README.md ├── _.eslintrc │ └── eslint │ │ └── yes ├── _.storybook │ └── docsEngine │ │ └── storybook │ │ ├── main.ts │ │ └── preview.ts ├── _gitignore ├── _package.json │ ├── docsEngine │ │ ├── ladle.json │ │ └── storybook.json │ └── eslint │ │ └── yes.json ├── _vite-ladle.config.ts │ └── docsEngine │ │ └── ladle.ts ├── package-lock.json ├── package.json ├── src │ ├── _Index.mdx │ │ └── docsEngine │ │ │ └── storybook.mdx │ ├── components │ │ ├── DemoComponent │ │ │ ├── DemoComponent.tsx │ │ │ ├── _DemoComponent.stories.tsx │ │ │ │ └── docsEngine │ │ │ │ │ ├── ladle.tsx │ │ │ │ │ └── storybook.tsx │ │ │ └── index.ts │ │ └── index.ts │ ├── index.tsx │ └── vite-env.d.ts ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts ├── tsconfig.json └── tsup.config.ts /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | .DS_Store 4 | react-ui-library-template/ 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "singleQuote": true, 4 | "trailingComma": "all", 5 | "printWidth": 120 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Create React UI Lib 2 | 3 | [![npm version](https://badge.fury.io/js/create-react-ui-lib.svg)](https://www.npmjs.com/package/create-react-ui-lib) 4 | 5 | A CLI tool that bootstraps simple [Vite](https://vitejs.dev/) template for painless [React](https://reactjs.org/) UI library development. 6 | 7 | - Unopinionated: no default styling, mandatory ESLint, pre-commit hooks — bring your own stuff if you need it. 8 | - Type definitions are extracted using [vite-plugin-dts](https://github.com/qmhc/vite-plugin-dts). 9 | - Bundles to ES and UMD modules, generates sourcemaps. 10 | - Uses [Storybook](https://storybook.js.org/) or [Ladle](https://ladle.dev/) for docs which are easily deployed as GitHub pages. 11 | - Optional ESLint with recommended settings for each of these plugins: [typescript](https://typescript-eslint.io/), [prettier](https://github.com/prettier/eslint-plugin-prettier), [react](https://github.com/jsx-eslint/eslint-plugin-react), [react-hooks](https://github.com/facebook/react/tree/main/packages/eslint-plugin-react-hooks), [jsx-a11y](https://github.com/jsx-eslint/eslint-plugin-jsx-a11y). 12 | 13 | ## Motivation 14 | 15 | [Create React UI Lib: Component Library Speedrun](https://dev.to/topcat/create-react-ui-lib-component-library-speedrun-25bp) 16 | 17 | 18 | ## Getting started 19 | 20 | Run the command: 21 | 22 | ```shell 23 | npm create react-ui-lib@latest 24 | ``` 25 | 26 | ## Publishing the library 27 | 28 | 1. Build the package: `npm run build` 29 | 2. Open `package.json`, update package description, author, repository, remove `"private": true`. 30 | 3. Run `npm publish` 31 | 32 | ## Publishing Storybook / Ladle to GitHub pages 33 | 34 | Storybook static is built to `docs` directory which is under git. To publish it to GitHub Pages do this: 35 | 36 | - Publish this repo to GitHub. 37 | - Run `npm run build-storybook`, commit `docs` folder and push. 38 | - [Create a separate GitHub Pages repo](https://docs.github.com/en/pages/getting-started-with-github-pages/creating-a-github-pages-site#creating-a-repository-for-your-site) if you haven't yet. 39 | - [Set up GitHub pages for this project](https://docs.github.com/en/pages/getting-started-with-github-pages/creating-a-github-pages-site#creating-your-site) to build from `docs` folder from `main` branch. 40 | - To do this go to this repo's settings and open `Pages` section (menu on the left side). Select `Source` -> `Deploy from a branch`, select `Branch` -> `main` and `/docs` folder. 41 | 42 | ## Contributing 43 | 44 | Feel free to [open an issue](https://github.com/mlshv/create-react-ui-lib/issues/new) or create a PR if you'd like to contribute 🙌 45 | 46 | ## License 47 | 48 | The project is available as open source under the terms of the [MIT License](LICENSE). 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-react-ui-lib", 3 | "version": "1.1.0", 4 | "description": "A CLI tool to spin up React.js UI library development", 5 | "main": "index.js", 6 | "type": "module", 7 | "exports": "./dist/index.js", 8 | "bin": { 9 | "create-react-ui-lib": "./dist/index.js" 10 | }, 11 | "engines": { 12 | "node": ">=14.16" 13 | }, 14 | "scripts": { 15 | "typecheck": "tsc", 16 | "build": "tsup", 17 | "dev": "tsup --watch", 18 | "start": "node dist/index.js" 19 | }, 20 | "keywords": [ 21 | "CLI", 22 | "React", 23 | "TypeScript", 24 | "UI", 25 | "library", 26 | "boilerplate" 27 | ], 28 | "author": "Mikhail Malyshev ", 29 | "license": "MIT", 30 | "repository": { 31 | "type": "git", 32 | "url": "git+https://github.com/mlshv/create-react-ui-lib.git" 33 | }, 34 | "devDependencies": { 35 | "@total-typescript/ts-reset": "^0.4.2", 36 | "@types/fs-extra": "^11.0.1", 37 | "@types/inquirer": "^9.0.3", 38 | "@types/node": "^20.3.1", 39 | "tsup": "^6.7.0", 40 | "type-fest": "^3.12.0" 41 | }, 42 | "dependencies": { 43 | "chalk": "^5.2.0", 44 | "commander": "^11.0.0", 45 | "deepmerge": "^4.3.1", 46 | "execa": "^7.1.1", 47 | "fs-extra": "^11.1.1", 48 | "inquirer": "^9.2.7", 49 | "ora": "^6.3.1", 50 | "typescript": "^5.1.3" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /reset.d.ts: -------------------------------------------------------------------------------- 1 | import '@total-typescript/ts-reset' 2 | -------------------------------------------------------------------------------- /src/cli/constants.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { fileURLToPath } from 'url' 3 | 4 | // With the move to TSUP as a build tool, this keeps path routes in other files (installers, loaders, etc) in check more easily. 5 | // Path is in relation to a single index.js file inside ./dist 6 | const __filename = fileURLToPath(import.meta.url) 7 | const srcCliPath = path.dirname(__filename) 8 | export const CLI_ROOT = path.join(srcCliPath) 9 | 10 | export const DEFAULT_LIBRARY_NAME = 'react-ui-library-template' 11 | export const DEFAULT_UMD_NAMESPACE = 'ViteReactLibraryTemplate' 12 | export const DEFAULT_DOCS_ENGINE = 'storybook' 13 | -------------------------------------------------------------------------------- /src/cli/index.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk' 2 | import { Command } from 'commander' 3 | import inquirer from 'inquirer' 4 | import { getVersion, validateAppName, getUserPkgManager, logger, validateUmdNamespace } from '~/utils' 5 | import { DEFAULT_LIBRARY_NAME, DEFAULT_UMD_NAMESPACE, DEFAULT_DOCS_ENGINE } from './constants' 6 | 7 | type CliFlags = { 8 | noGit: boolean 9 | noInstall: boolean 10 | default: boolean 11 | } 12 | 13 | type CliResults = { 14 | appName: string 15 | umdNamespace: string 16 | flags: CliFlags 17 | docsEngine: 'storybook' | 'ladle' 18 | eslint: boolean 19 | } 20 | 21 | const defaultOptions: CliResults = { 22 | appName: DEFAULT_LIBRARY_NAME, 23 | umdNamespace: DEFAULT_UMD_NAMESPACE, 24 | docsEngine: DEFAULT_DOCS_ENGINE, 25 | eslint: true, 26 | flags: { 27 | noGit: false, 28 | noInstall: false, 29 | default: false, 30 | }, 31 | } 32 | 33 | export const runCli = async () => { 34 | const cliResults = defaultOptions 35 | 36 | const program = new Command().name('create-react-ui-lib') 37 | 38 | program 39 | .description('A CLI tool to spin up React.js UI library development') 40 | .argument('[dir]', 'The name of the library, as well as the name of the directory to create') 41 | .option('-y, --default', 'Bypass the CLI and use all default options to bootstrap a new React UI library', false) 42 | 43 | .version(getVersion(), '-v, --version', 'Display the version number') 44 | .addHelpText('afterAll', `\n The CLI was inspired by ${chalk.hex('#E8DCFF').bold('create-t3-app')} CLI \n`) 45 | .parse(process.argv) 46 | 47 | // Needs to be separated outside the if statement to correctly infer the type as string | undefined 48 | const cliProvidedName = program.args[0] 49 | 50 | if (!cliResults.flags.default) { 51 | if (cliProvidedName) { 52 | cliResults.appName = cliProvidedName 53 | } 54 | 55 | if (!cliProvidedName) { 56 | cliResults.appName = await promptAppName() 57 | } 58 | 59 | cliResults.umdNamespace = await promptUmdNamespace() 60 | cliResults.docsEngine = await promptDocsEngine() 61 | cliResults.eslint = await promptEslint() 62 | 63 | if (!cliResults.flags.noGit) { 64 | cliResults.flags.noGit = !(await promptGit()) 65 | } 66 | 67 | if (!cliResults.flags.noInstall) { 68 | cliResults.flags.noInstall = !(await promptInstall()) 69 | } 70 | } 71 | 72 | return cliResults 73 | } 74 | 75 | const promptAppName = async (): Promise => { 76 | const { appName } = await inquirer.prompt>({ 77 | name: 'appName', 78 | type: 'input', 79 | message: 'Input package name for npm (use kebab-case):', 80 | default: defaultOptions.appName, 81 | validate: validateAppName, 82 | transformer: (input: string) => { 83 | return input.trim() 84 | }, 85 | }) 86 | 87 | return appName 88 | } 89 | 90 | const promptUmdNamespace = async (): Promise => { 91 | const { umdNamespace } = await inquirer.prompt>({ 92 | name: 'umdNamespace', 93 | type: 'input', 94 | message: 'Input namespace for UMD build (use PascalCase):', 95 | default: defaultOptions.umdNamespace, 96 | validate: validateUmdNamespace, 97 | transformer: (input: string) => { 98 | return input.trim() 99 | }, 100 | }) 101 | 102 | return umdNamespace 103 | } 104 | 105 | const promptDocsEngine = async (): Promise<'storybook' | 'ladle'> => { 106 | const { docsEngine } = await inquirer.prompt>({ 107 | name: 'docsEngine', 108 | type: 'list', 109 | message: 'Choose a documentation engine:', 110 | choices: [ 111 | { 112 | name: 'Storybook (https://storybook.js.org/)', 113 | value: 'storybook', 114 | }, 115 | { 116 | name: 'Ladle (https://ladle.dev/)', 117 | value: 'ladle', 118 | }, 119 | ], 120 | default: DEFAULT_DOCS_ENGINE, 121 | }) 122 | 123 | return docsEngine 124 | } 125 | 126 | const promptEslint = async (): Promise => { 127 | const { eslint } = await inquirer.prompt<{ eslint: boolean }>({ 128 | name: 'eslint', 129 | type: 'confirm', 130 | message: 'Would you like to use ESLint?', 131 | default: true, 132 | }) 133 | 134 | return eslint 135 | } 136 | 137 | const promptInstall = async (): Promise => { 138 | const pkgManager = getUserPkgManager() 139 | 140 | const { install } = await inquirer.prompt<{ install: boolean }>({ 141 | name: 'install', 142 | type: 'confirm', 143 | message: `Would you like us to run '${pkgManager}` + (pkgManager === 'yarn' ? `'?` : ` install'?`), 144 | default: true, 145 | }) 146 | 147 | if (install) { 148 | logger.success("Alright. We'll install the dependencies for you!") 149 | } else { 150 | if (pkgManager === 'yarn') { 151 | logger.info(`No worries. You can run '${pkgManager}' later to install the dependencies.`) 152 | } else { 153 | logger.info(`No worries. You can run '${pkgManager} install' later to install the dependencies.`) 154 | } 155 | } 156 | 157 | return install 158 | } 159 | 160 | const promptGit = async (): Promise => { 161 | const { git } = await inquirer.prompt<{ git: boolean }>({ 162 | name: 'git', 163 | type: 'confirm', 164 | message: 'Initialize a new git repository?', 165 | default: true, 166 | }) 167 | 168 | if (git) { 169 | logger.success('Nice one! Initializing repository!') 170 | } else { 171 | logger.info('Sounds good! You can come back and run git init later.') 172 | } 173 | 174 | return git 175 | } 176 | -------------------------------------------------------------------------------- /src/cli/types.ts: -------------------------------------------------------------------------------- 1 | import type { PackageManager } from '../utils' 2 | 3 | export type DocsEngine = 'storybook' | 'ladle' 4 | 5 | export type InstallerOptions = { 6 | projectDir: string 7 | umdNamespace: string 8 | pkgManager: PackageManager 9 | noInstall: boolean 10 | docsEngine: DocsEngine 11 | eslint: boolean 12 | projectName?: string 13 | } 14 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import fs from 'fs-extra' 3 | import path from 'path' 4 | 5 | import { runCli } from './cli' 6 | import { logger, parseNameAndPath, installDependencies, initializeGit, logNextSteps, createProject } from './utils' 7 | 8 | const main = async () => { 9 | const { 10 | appName, 11 | umdNamespace, 12 | docsEngine, 13 | eslint, 14 | flags: { noGit, noInstall }, 15 | } = await runCli() 16 | 17 | // e.g. dir/@mono/app returns ["@mono/app", "dir/app"] 18 | const [scopedAppName, appDir] = parseNameAndPath(appName) 19 | 20 | const projectDir = await createProject({ 21 | projectName: appDir, 22 | umdNamespace, 23 | docsEngine, 24 | noInstall, 25 | eslint, 26 | }) 27 | 28 | const pkgJson = fs.readJSONSync(path.join(projectDir, 'package.json')) 29 | pkgJson.name = scopedAppName 30 | fs.writeJSONSync(path.join(projectDir, 'package.json'), pkgJson, { 31 | spaces: 2, 32 | }) 33 | 34 | if (!noInstall) { 35 | await installDependencies({ projectDir }) 36 | } 37 | 38 | if (!noGit) { 39 | await initializeGit(projectDir) 40 | } 41 | 42 | logNextSteps({ projectName: appDir, noInstall }) 43 | 44 | process.exit(0) 45 | } 46 | 47 | main().catch((err) => { 48 | logger.error('Aborting installation...') 49 | if (err instanceof Error) { 50 | logger.error(err) 51 | } else { 52 | logger.error('An unknown error has occurred. Please open an issue on GitHub with the below:') 53 | console.log(err) 54 | } 55 | process.exit(1) 56 | }) 57 | -------------------------------------------------------------------------------- /src/utils/createProject.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | import { DocsEngine } from '~/cli/types' 4 | import { scaffoldProject } from '~/utils' 5 | import { getUserPkgManager } from '~/utils' 6 | 7 | type CreateProjectOptions = { 8 | projectName: string 9 | umdNamespace: string 10 | eslint: boolean 11 | noInstall: boolean 12 | docsEngine: DocsEngine 13 | } 14 | 15 | export const createProject = async ({ 16 | projectName, 17 | umdNamespace, 18 | docsEngine, 19 | eslint, 20 | noInstall, 21 | }: CreateProjectOptions) => { 22 | const pkgManager = getUserPkgManager() 23 | const projectDir = path.resolve(process.cwd(), projectName) 24 | 25 | await scaffoldProject({ 26 | projectName, 27 | umdNamespace, 28 | projectDir, 29 | docsEngine, 30 | eslint, 31 | pkgManager, 32 | noInstall, 33 | }) 34 | 35 | return projectDir 36 | } 37 | -------------------------------------------------------------------------------- /src/utils/getUserPkgManager.ts: -------------------------------------------------------------------------------- 1 | export type PackageManager = 'npm' | 'pnpm' | 'yarn' 2 | 3 | export const getUserPkgManager: () => PackageManager = () => { 4 | // This environment variable is set by npm and yarn but pnpm seems less consistent 5 | const userAgent = process.env.npm_config_user_agent 6 | 7 | if (userAgent) { 8 | if (userAgent.startsWith('yarn')) { 9 | return 'yarn' 10 | } else if (userAgent.startsWith('pnpm')) { 11 | return 'pnpm' 12 | } else { 13 | return 'npm' 14 | } 15 | } else { 16 | // If no user agent is set, assume npm 17 | return 'npm' 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/git.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk' 2 | import { execSync } from 'child_process' 3 | import { execa } from 'execa' 4 | import fs from 'fs-extra' 5 | import inquirer from 'inquirer' 6 | import ora from 'ora' 7 | import path from 'path' 8 | import { logger } from '~/utils/logger.js' 9 | 10 | const isGitInstalled = (dir: string): boolean => { 11 | try { 12 | execSync('git --version', { cwd: dir }) 13 | return true 14 | } catch (_e) { 15 | return false 16 | } 17 | } 18 | 19 | /** @returns Whether or not the provided directory has a `.git` subdirectory in it. */ 20 | const isRootGitRepo = (dir: string): boolean => { 21 | return fs.existsSync(path.join(dir, '.git')) 22 | } 23 | 24 | /** @returns Whether or not this directory or a parent directory has a `.git` directory. */ 25 | const isInsideGitRepo = async (dir: string): Promise => { 26 | try { 27 | // If this command succeeds, we're inside a git repo 28 | await execa('git', ['rev-parse', '--is-inside-work-tree'], { 29 | cwd: dir, 30 | stdout: 'ignore', 31 | }) 32 | return true 33 | } catch (_e) { 34 | // Else, it will throw a git-error and we return false 35 | return false 36 | } 37 | } 38 | 39 | const getGitVersion = () => { 40 | const stdout = execSync('git --version').toString().trim() 41 | const gitVersionTag = stdout.split(' ')[2] 42 | const major = gitVersionTag?.split('.')[0] 43 | const minor = gitVersionTag?.split('.')[1] 44 | return { major: Number(major), minor: Number(minor) } 45 | } 46 | 47 | /** @returns The git config value of "init.defaultBranch". If it is not set, returns "main". */ 48 | const getDefaultBranch = () => { 49 | const stdout = execSync('git config --global init.defaultBranch || echo main').toString().trim() 50 | 51 | return stdout 52 | } 53 | 54 | // This initializes the Git-repository for the project 55 | export const initializeGit = async (projectDir: string) => { 56 | logger.info('Initializing Git...') 57 | 58 | if (!isGitInstalled(projectDir)) { 59 | logger.warn('Git is not installed. Skipping Git initialization.') 60 | return 61 | } 62 | 63 | const spinner = ora('Creating a new git repo...\n').start() 64 | 65 | const isRoot = isRootGitRepo(projectDir) 66 | const isInside = await isInsideGitRepo(projectDir) 67 | const dirName = path.parse(projectDir).name // skip full path for logging 68 | 69 | if (isInside && isRoot) { 70 | // Dir is a root git repo 71 | spinner.stop() 72 | const { overwriteGit } = await inquirer.prompt<{ 73 | overwriteGit: boolean 74 | }>({ 75 | name: 'overwriteGit', 76 | type: 'confirm', 77 | message: `${chalk.redBright.bold( 78 | 'Warning:', 79 | )} Git is already initialized in "${dirName}". Initializing a new git repository would delete the previous history. Would you like to continue anyways?`, 80 | default: false, 81 | }) 82 | if (!overwriteGit) { 83 | spinner.info('Skipping Git initialization.') 84 | return 85 | } 86 | // Deleting the .git folder 87 | fs.removeSync(path.join(projectDir, '.git')) 88 | } else if (isInside && !isRoot) { 89 | // Dir is inside a git worktree 90 | spinner.stop() 91 | const { initializeChildGitRepo } = await inquirer.prompt<{ 92 | initializeChildGitRepo: boolean 93 | }>({ 94 | name: 'initializeChildGitRepo', 95 | type: 'confirm', 96 | message: `${chalk.redBright.bold( 97 | 'Warning:', 98 | )} "${dirName}" is already in a git worktree. Would you still like to initialize a new git repository in this directory?`, 99 | default: false, 100 | }) 101 | if (!initializeChildGitRepo) { 102 | spinner.info('Skipping Git initialization.') 103 | return 104 | } 105 | } 106 | 107 | // We're good to go, initializing the git repo 108 | try { 109 | const branchName = getDefaultBranch() 110 | 111 | // --initial-branch flag was added in git v2.28.0 112 | const { major, minor } = getGitVersion() 113 | if (major < 2 || (major == 2 && minor < 28)) { 114 | await execa('git', ['init'], { cwd: projectDir }) 115 | // symbolic-ref is used here due to refs/heads/master not existing 116 | // It is only created after the first commit 117 | // https://superuser.com/a/1419674 118 | await execa('git', ['symbolic-ref', 'HEAD', `refs/heads/${branchName}`], { 119 | cwd: projectDir, 120 | }) 121 | } else { 122 | await execa('git', ['init', `--initial-branch=${branchName}`], { 123 | cwd: projectDir, 124 | }) 125 | } 126 | await execa('git', ['add', '.'], { cwd: projectDir }) 127 | spinner.succeed(`${chalk.green('Successfully initialized and staged')} ${chalk.green.bold('git')}\n`) 128 | } catch (error) { 129 | // Safeguard, should be unreachable 130 | spinner.fail(`${chalk.bold.red('Failed:')} could not initialize git. Update git to the latest version!\n`) 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './createProject' 2 | export * from './getUserPkgManager' 3 | export * from './git' 4 | export * from './installDependencies' 5 | export * from './logger' 6 | export * from './logNextSteps' 7 | export * from './packageJson' 8 | export * from './path' 9 | export * from './scaffoldProject' 10 | export * from './validators' 11 | -------------------------------------------------------------------------------- /src/utils/installDependencies.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk' 2 | import { execa } from 'execa' 3 | import ora, { type Ora } from 'ora' 4 | import { getUserPkgManager, type PackageManager } from '~/utils/getUserPkgManager.js' 5 | import { logger } from '~/utils/logger.js' 6 | 7 | type Options = { 8 | projectDir: string 9 | } 10 | 11 | /*eslint-disable @typescript-eslint/no-floating-promises*/ 12 | const runInstallCommand = async (pkgManager: PackageManager, projectDir: string): Promise => { 13 | switch (pkgManager) { 14 | // When using npm, inherit the stderr stream so that the progress bar is shown 15 | case 'npm': 16 | await execa(pkgManager, ['install'], { 17 | cwd: projectDir, 18 | stderr: 'inherit', 19 | }) 20 | 21 | return null 22 | // When using yarn or pnpm, use the stdout stream and ora spinner to show the progress 23 | case 'pnpm': 24 | const pnpmSpinner = ora('Running pnpm install...').start() 25 | const pnpmSubprocess = execa(pkgManager, ['install'], { 26 | cwd: projectDir, 27 | stdout: 'pipe', 28 | }) 29 | 30 | await new Promise((res, rej) => { 31 | pnpmSubprocess.stdout?.on('data', (data: Buffer) => { 32 | const text = data.toString() 33 | 34 | if (text.includes('Progress')) { 35 | pnpmSpinner.text = text.includes('|') ? text.split(' | ')[1] ?? '' : text 36 | } 37 | }) 38 | pnpmSubprocess.on('error', (e) => rej(e)) 39 | pnpmSubprocess.on('close', () => res()) 40 | }) 41 | 42 | return pnpmSpinner 43 | case 'yarn': 44 | const yarnSpinner = ora('Running yarn...').start() 45 | const yarnSubprocess = execa(pkgManager, [], { 46 | cwd: projectDir, 47 | stdout: 'pipe', 48 | }) 49 | 50 | await new Promise((res, rej) => { 51 | yarnSubprocess.stdout?.on('data', (data: Buffer) => { 52 | yarnSpinner.text = data.toString() 53 | }) 54 | yarnSubprocess.on('error', (e) => rej(e)) 55 | yarnSubprocess.on('close', () => res()) 56 | }) 57 | 58 | return yarnSpinner 59 | } 60 | } 61 | /*eslint-enable @typescript-eslint/no-floating-promises*/ 62 | 63 | export const installDependencies = async ({ projectDir }: Options) => { 64 | logger.info('Installing dependencies...') 65 | const pkgManager = getUserPkgManager() 66 | 67 | const installSpinner = await runInstallCommand(pkgManager, projectDir) 68 | 69 | // If the spinner was used to show the progress, use succeed method on it 70 | // If not, use the succeed on a new spinner 71 | ;(installSpinner || ora()).succeed(chalk.green('Successfully installed dependencies!\n')) 72 | } 73 | -------------------------------------------------------------------------------- /src/utils/logNextSteps.ts: -------------------------------------------------------------------------------- 1 | import { getUserPkgManager } from '~/utils/getUserPkgManager.js' 2 | import { logger } from '~/utils/logger.js' 3 | 4 | // This logs the next steps that the user should take in order to advance the project 5 | export const logNextSteps = ({ projectName, noInstall }: { projectName: string; noInstall: boolean }) => { 6 | const pkgManager = getUserPkgManager() 7 | 8 | logger.info('Next steps:') 9 | projectName !== '.' && logger.info(` cd ${projectName}`) 10 | if (noInstall) { 11 | // To reflect yarn's default behavior of installing packages when no additional args provided 12 | if (pkgManager === 'yarn') { 13 | logger.info(` ${pkgManager}`) 14 | } else { 15 | logger.info(` ${pkgManager} install`) 16 | } 17 | } 18 | 19 | logger.info(` ${pkgManager === 'npm' ? 'npm run' : pkgManager} dev`) 20 | } 21 | -------------------------------------------------------------------------------- /src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk' 2 | 3 | export const logger = { 4 | error(...args: unknown[]) { 5 | console.log(chalk.red(...args)) 6 | }, 7 | warn(...args: unknown[]) { 8 | console.log(chalk.yellow(...args)) 9 | }, 10 | info(...args: unknown[]) { 11 | console.log(chalk.cyan(...args)) 12 | }, 13 | success(...args: unknown[]) { 14 | console.log(chalk.green(...args)) 15 | }, 16 | } 17 | -------------------------------------------------------------------------------- /src/utils/packageJson.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs-extra' 2 | import path from 'path' 3 | import { type PackageJson } from 'type-fest' 4 | 5 | import { CLI_ROOT } from '~/cli/constants' 6 | 7 | export const getVersion = () => { 8 | const packageJsonPath = path.join(CLI_ROOT, '..', 'package.json') 9 | 10 | const packageJsonContent = fs.readJSONSync(packageJsonPath) as PackageJson 11 | 12 | return packageJsonContent.version ?? '1.0.0' 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/path.ts: -------------------------------------------------------------------------------- 1 | import pathModule from 'path' 2 | 3 | /** 4 | * Parses the appName and its path from the user input. 5 | * 6 | * Returns a tuple of of `[appName, path]`, where `appName` is the name put in the "package.json" 7 | * file and `path` is the path to the directory where the app will be created. 8 | * 9 | * If `appName` is ".", the name of the directory will be used instead. Handles the case where the 10 | * input includes a scoped package name in which case that is being parsed as the name, but not 11 | * included as the path. 12 | * 13 | * For example: 14 | * 15 | * - dir/@mono/app => ["@mono/app", "dir/app"] 16 | * - dir/app => ["app", "dir/app"] 17 | */ 18 | export const parseNameAndPath = (input: string) => { 19 | const paths = input.split('/') 20 | 21 | let appName = paths[paths.length - 1] 22 | 23 | // If the user ran `npx create-react-ui-lib .`, the appName should be the current directory 24 | if (appName === '.') { 25 | const parsedCwd = pathModule.resolve(process.cwd()) 26 | appName = pathModule.basename(parsedCwd) 27 | } 28 | 29 | // If the first part is a @, it's a scoped package 30 | const indexOfDelimiter = paths.findIndex((p) => p.startsWith('@')) 31 | if (paths.findIndex((p) => p.startsWith('@')) !== -1) { 32 | appName = paths.slice(indexOfDelimiter).join('/') 33 | } 34 | 35 | const path = paths.filter((p) => !p.startsWith('@')).join('/') 36 | 37 | return [appName, path] as const 38 | } 39 | -------------------------------------------------------------------------------- /src/utils/scaffoldProject.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk' 2 | import deepmerge from 'deepmerge' 3 | import fs from 'fs-extra' 4 | import inquirer from 'inquirer' 5 | import ora from 'ora' 6 | import path from 'path' 7 | 8 | import { type InstallerOptions } from '~/cli/types' 9 | import { logger } from '~/utils' 10 | import { CLI_ROOT, DEFAULT_UMD_NAMESPACE } from '~/cli/constants' 11 | 12 | export const scaffoldProject = async ({ 13 | projectName, 14 | umdNamespace, 15 | projectDir, 16 | docsEngine, 17 | eslint, 18 | pkgManager, 19 | noInstall, 20 | }: InstallerOptions) => { 21 | const srcDir = path.join(CLI_ROOT, '..', 'template') 22 | 23 | if (!noInstall) { 24 | logger.info(`\nUsing: ${chalk.cyan.bold(pkgManager)}\n`) 25 | } else { 26 | logger.info('') 27 | } 28 | 29 | const spinner = ora(`Scaffolding in: ${projectDir}...\n`).start() 30 | 31 | if (fs.existsSync(projectDir)) { 32 | if (fs.readdirSync(projectDir).length === 0) { 33 | if (projectName !== '.') spinner.info(`${chalk.cyan.bold(projectName)} exists but is empty, continuing...\n`) 34 | } else { 35 | spinner.stopAndPersist() 36 | const { overwriteDir } = await inquirer.prompt<{ 37 | overwriteDir: 'abort' | 'clear' | 'overwrite' 38 | }>({ 39 | name: 'overwriteDir', 40 | type: 'list', 41 | message: `${chalk.redBright.bold('Warning:')} ${chalk.cyan.bold( 42 | projectName, 43 | )} already exists and isn't empty. How would you like to proceed?`, 44 | choices: [ 45 | { 46 | name: 'Abort installation (recommended)', 47 | value: 'abort', 48 | short: 'Abort', 49 | }, 50 | { 51 | name: 'Clear the directory and continue installation', 52 | value: 'clear', 53 | short: 'Clear', 54 | }, 55 | { 56 | name: 'Continue installation and overwrite conflicting files', 57 | value: 'overwrite', 58 | short: 'Overwrite', 59 | }, 60 | ], 61 | default: 'abort', 62 | }) 63 | 64 | if (overwriteDir === 'abort') { 65 | spinner.fail('Aborting installation...') 66 | process.exit(1) 67 | } 68 | 69 | const overwriteAction = overwriteDir === 'clear' ? 'clear the directory' : 'overwrite conflicting files' 70 | 71 | const { confirmOverwriteDir } = await inquirer.prompt<{ 72 | confirmOverwriteDir: boolean 73 | }>({ 74 | name: 'confirmOverwriteDir', 75 | type: 'confirm', 76 | message: `Are you sure you want to ${overwriteAction}?`, 77 | default: false, 78 | }) 79 | 80 | if (!confirmOverwriteDir) { 81 | spinner.fail('Aborting installation...') 82 | process.exit(1) 83 | } 84 | 85 | if (overwriteDir === 'clear') { 86 | spinner.info(`Emptying ${chalk.cyan.bold(projectName)} and creating the library project...\n`) 87 | fs.emptyDirSync(projectDir) 88 | } 89 | } 90 | } 91 | 92 | spinner.start() 93 | 94 | fs.copySync(srcDir, projectDir) 95 | 96 | fs.renameSync(path.join(projectDir, '_gitignore'), path.join(projectDir, '.gitignore')) 97 | replaceUmdNamespace(projectDir, umdNamespace) 98 | 99 | handleFileContentVariations(projectDir, { docsEngine, eslint: eslint ? 'yes' : 'no' }) 100 | 101 | const scaffoldedName = projectName === '.' ? 'App' : chalk.cyan.bold(projectName) 102 | 103 | spinner.succeed(`${scaffoldedName} ${chalk.green('scaffolded successfully!')}\n`) 104 | } 105 | 106 | const replaceUmdNamespace = (projectDir: string, umdNamespace: string) => { 107 | const viteConfigPath = path.join(projectDir, 'vite.config.ts') 108 | const data = fs.readFileSync(viteConfigPath, 'utf-8') 109 | const result = data.replace(DEFAULT_UMD_NAMESPACE, umdNamespace) 110 | 111 | fs.writeFileSync(viteConfigPath, result, 'utf8') 112 | } 113 | 114 | const processDirectory = (dirPath: string, variations: Record) => { 115 | const entries = fs.readdirSync(dirPath, { withFileTypes: true }) 116 | 117 | entries.forEach((entry) => { 118 | if (!entry.isDirectory()) { 119 | return 120 | } 121 | 122 | const fullPath = path.join(dirPath, entry.name) 123 | 124 | if (entry.name.startsWith('_')) { 125 | const subFolders = fs.readdirSync(fullPath, { withFileTypes: true }) 126 | 127 | subFolders.forEach((subFolder) => { 128 | const variationFileBase = variations[subFolder.name] 129 | 130 | if (!variationFileBase || !subFolder.isDirectory()) return 131 | 132 | const variationFolderPath = path.join(fullPath, subFolder.name) 133 | const variationFiles = fs.readdirSync(variationFolderPath) 134 | 135 | const fileName = variationFiles.find((fileName) => fileName.startsWith(variationFileBase)) 136 | 137 | if (!fileName) return 138 | 139 | const variationFilePath = path.join(variationFolderPath, fileName) 140 | const newFilePath = path.join(dirPath, entry.name.replace('_', '')) 141 | 142 | const isDirectory = fs.lstatSync(variationFilePath).isDirectory() 143 | 144 | if (isDirectory) { 145 | fs.copySync(variationFilePath, newFilePath) 146 | } else if (path.extname(fileName) === '.json' && fs.existsSync(newFilePath)) { 147 | const existingContent = fs.readFileSync(newFilePath, 'utf-8') 148 | const variationContent = fs.readFileSync(variationFilePath, 'utf-8') 149 | const mergedContent = JSON.stringify( 150 | deepmerge(JSON.parse(existingContent), JSON.parse(variationContent)), 151 | null, 152 | 2, 153 | ) 154 | fs.writeFileSync(newFilePath, mergedContent) 155 | } else { 156 | const variationContent = fs.readFileSync(variationFilePath) 157 | fs.writeFileSync(newFilePath, variationContent) 158 | } 159 | }) 160 | 161 | fs.rmdirSync(fullPath, { recursive: true }) 162 | } else { 163 | processDirectory(fullPath, variations) // recursive call 164 | } 165 | }) 166 | } 167 | 168 | const handleFileContentVariations = (projectDir: string, variations: Record) => { 169 | processDirectory(projectDir, variations) 170 | } 171 | -------------------------------------------------------------------------------- /src/utils/validators.ts: -------------------------------------------------------------------------------- 1 | //Validate a string against allowed package.json names 2 | export const validateAppName = (input: string) => { 3 | const validationRegExp = /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/ 4 | const paths = input.split('/') 5 | 6 | // If the first part is a @, it's a scoped package 7 | const indexOfDelimiter = paths.findIndex((p) => p.startsWith('@')) 8 | 9 | let appName = paths[paths.length - 1] 10 | if (paths.findIndex((p) => p.startsWith('@')) !== -1) { 11 | appName = paths.slice(indexOfDelimiter).join('/') 12 | } 13 | 14 | if (input === '.' || validationRegExp.test(appName ?? '')) { 15 | return true 16 | } else { 17 | return "App name must consist of only lowercase alphanumeric characters, '-', and '_'" 18 | } 19 | } 20 | 21 | export const validateUmdNamespace = (input: string) => { 22 | const pascalCaseValidationRegExp = /^[A-Z][a-zA-Z0-9]*$/ 23 | 24 | if (input === '') { 25 | return true 26 | } 27 | 28 | if (pascalCaseValidationRegExp.test(input)) { 29 | return true 30 | } else { 31 | return "UMD namespace must consist of only lowercase alphanumeric characters, '-', and '_'" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /template/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "semi": false, 4 | "singleQuote": true, 5 | "printWidth": 120, 6 | "bracketSpacing": true 7 | } 8 | -------------------------------------------------------------------------------- /template/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Mikhail Malyshev 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. -------------------------------------------------------------------------------- /template/README.md: -------------------------------------------------------------------------------- 1 | # Create React UI Lib 2 | 3 | A CLI tool that bootstraps simple [Vite](https://vitejs.dev/) template for instant [React](https://reactjs.org/) UI library development. 4 | 5 | - Unopinionated: no default styling, ESLint, pre-commit hooks — bring your own stuff if you need it. 6 | - Type definitions are extracted using [vite-plugin-dts](https://github.com/qmhc/vite-plugin-dts). 7 | - Bundles to ES and UMD modules, generates sourcemaps. 8 | - Offers [Storybook](https://storybook.js.org/) or [Ladle](https://ladle.dev/) for docs which are easily deployed as GitHub pages. 9 | 10 | ## Getting started 11 | 12 | Run the command: 13 | 14 | ```shell 15 | npm create react-ui-lib@latest 16 | ``` 17 | 18 | ## Publishing the library 19 | 20 | 1. Build the package: `npm run build` 21 | 2. Open `package.json`, update package description, author, repository, remove `"private": true`. 22 | 3. Run `npm publish` 23 | 24 | ## Publishing docs to GitHub pages 25 | 26 | Storybook or Ladle static is built to `docs` directory which is under git. To publish it to GitHub Pages do this: 27 | 28 | - Publish this repo to GitHub. 29 | - Run `npm run build-docs`, commit `docs` folder and push. 30 | - [Create a separate GitHub Pages repo](https://docs.github.com/en/pages/getting-started-with-github-pages/creating-a-github-pages-site#creating-a-repository-for-your-site) if you haven't yet. 31 | - [Set up GitHub pages for this project](https://docs.github.com/en/pages/getting-started-with-github-pages/creating-a-github-pages-site#creating-your-site) to build from `docs` folder from `main` branch. 32 | - To do this go to this repo's settings and open `Pages` section (menu on the left side). Select `Source` -> `Deploy from a branch`, select `Branch` -> `main` and `/docs` folder. 33 | 34 | ## Feedback 35 | 36 | [Tell me](https://github.com/mlshv/create-react-ui-lib/issues/new) about your experience with Create React UI Lib. [Support the project](https://github.com/mlshv/create-react-ui-lib) by giving it a start on GitHub. 37 | -------------------------------------------------------------------------------- /template/_.eslintrc/eslint/yes: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:@typescript-eslint/recommended", 5 | "plugin:react/recommended", 6 | "plugin:react-hooks/recommended", 7 | "plugin:jsx-a11y/recommended", 8 | "plugin:react/jsx-runtime", 9 | "prettier" 10 | ], 11 | "settings": { 12 | "react": { 13 | "version": "detect" 14 | } 15 | }, 16 | "parser": "@typescript-eslint/parser" 17 | } 18 | -------------------------------------------------------------------------------- /template/_.storybook/docsEngine/storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type { StorybookConfig } from '@storybook/react-vite' 2 | const config: StorybookConfig = { 3 | stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'], 4 | addons: ['@storybook/addon-links', '@storybook/addon-essentials', '@storybook/addon-interactions'], 5 | framework: { 6 | name: '@storybook/react-vite', 7 | options: {}, 8 | }, 9 | docs: { 10 | autodocs: 'tag', 11 | }, 12 | } 13 | export default config 14 | -------------------------------------------------------------------------------- /template/_.storybook/docsEngine/storybook/preview.ts: -------------------------------------------------------------------------------- 1 | import type { Preview } from '@storybook/react' 2 | 3 | const preview: Preview = { 4 | parameters: { 5 | actions: { argTypesRegex: '^on[A-Z].*' }, 6 | controls: { 7 | matchers: { 8 | color: /(background|color)$/i, 9 | date: /Date$/, 10 | }, 11 | }, 12 | }, 13 | } 14 | 15 | export default preview 16 | -------------------------------------------------------------------------------- /template/_gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .vscode/* 4 | !.vscode/extensions.json 5 | .idea 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /template/_package.json/docsEngine/ladle.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "start": "ladle serve", 4 | "build-docs": "ladle build --viteConfig vite-ladle.config.ts -o docs && touch ./docs/.nojekyll" 5 | }, 6 | "devDependencies": { 7 | "@ladle/react": "^2.15.0" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /template/_package.json/docsEngine/storybook.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "start": "storybook dev -p 6006", 4 | "storybook": "npm start", 5 | "build-docs": "storybook build -o docs && touch ./docs/.nojekyll" 6 | }, 7 | "devDependencies": { 8 | "@storybook/addon-essentials": "^7.0.20", 9 | "@storybook/addon-interactions": "^7.0.20", 10 | "@storybook/addon-links": "^7.0.20", 11 | "@storybook/blocks": "^7.0.20", 12 | "@storybook/react": "^7.0.20", 13 | "@storybook/react-vite": "^7.0.20", 14 | "@storybook/testing-library": "^0.0.14-next.2", 15 | "storybook": "^7.0.20" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /template/_package.json/eslint/yes.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "lint": "eslint . --ext .ts,.tsx" 4 | }, 5 | "devDependencies": { 6 | "@typescript-eslint/eslint-plugin": "^5.60.1", 7 | "@typescript-eslint/parser": "^5.60.1", 8 | "eslint": "^8.43.0", 9 | "eslint-config-prettier": "^8.8.0", 10 | "eslint-plugin-jsx-a11y": "^6.7.1", 11 | "eslint-plugin-react": "^7.32.2", 12 | "eslint-plugin-react-hooks": "^4.6.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /template/_vite-ladle.config.ts/docsEngine/ladle.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | 3 | export default defineConfig({}); 4 | -------------------------------------------------------------------------------- /template/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-ui-library-template", 3 | "description": "Template to quick-start React library development with TypeScript.", 4 | "private": true, 5 | "version": "1.0.0", 6 | "type": "module", 7 | "author": "Your Name ", 8 | "license": "MIT", 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/mlshv/create-react-ui-lib.git" 12 | }, 13 | "exports": { 14 | ".": { 15 | "import": "./dist/index.es.js", 16 | "require": "./dist/index.umd.js" 17 | } 18 | }, 19 | "main": "./dist/index.umd.js", 20 | "module": "./dist/index.es.js", 21 | "types": "./dist/index.d.ts", 22 | "scripts": { 23 | "build": "tsc && vite build", 24 | "dev": "npm start" 25 | }, 26 | "peerDependencies": { 27 | "react": "^18.2.0", 28 | "react-dom": "^18.2.0" 29 | }, 30 | "devDependencies": { 31 | "@types/node": "^20.3.0", 32 | "@types/react": "^18.0.27", 33 | "@types/react-dom": "^18.0.10", 34 | "@vitejs/plugin-react": "^3.1.0", 35 | "typescript": "^4.9.3", 36 | "vite": "^4.1.0", 37 | "vite-plugin-dts": "^2.3.0" 38 | }, 39 | "files": [ 40 | "dist" 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /template/src/_Index.mdx/docsEngine/storybook.mdx: -------------------------------------------------------------------------------- 1 | import { Meta } from '@storybook/blocks'; 2 | 3 | 4 | 5 | # Create React UI Lib Template 6 | 7 | Thank you for using Create React UI Lib! Now, open the project in your favorite code editor and start hacking on your components. 8 | 9 | There is a demo component, you can use it as a reference. You can also use MDX, just as this file — `Index.mdx`. 10 | 11 | ## About Storybook 12 | 13 | [Storybook](https://storybook.js.org/) helps you build UI components in isolation from your app's business logic, data, and context. 14 | That makes it easy to develop components as well as writing documentation for them. 15 | -------------------------------------------------------------------------------- /template/src/components/DemoComponent/DemoComponent.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | 3 | type Props = { 4 | initialCount?: number 5 | } 6 | 7 | export function DemoComponent({ initialCount = 0 }: Props) { 8 | const [count, setCount] = useState(initialCount) 9 | 10 | return 11 | } 12 | -------------------------------------------------------------------------------- /template/src/components/DemoComponent/_DemoComponent.stories.tsx/docsEngine/ladle.tsx: -------------------------------------------------------------------------------- 1 | import type { StoryDefault, Story } from '@ladle/react' 2 | 3 | import { DemoComponent } from './DemoComponent' 4 | 5 | // More on how to set up stories at: https://ladle.dev/docs/stories 6 | export default { 7 | title: 'Example/DemoComponent', 8 | } satisfies StoryDefault 9 | 10 | export const Default: Story = () => 11 | 12 | export const WithInitialCount: Story = () => 13 | -------------------------------------------------------------------------------- /template/src/components/DemoComponent/_DemoComponent.stories.tsx/docsEngine/storybook.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from '@storybook/react' 2 | 3 | import { DemoComponent } from './DemoComponent' 4 | 5 | // More on how to set up stories at: https://storybook.js.org/docs/react/writing-stories/introduction 6 | const meta = { 7 | title: 'Example/DemoComponent', 8 | component: DemoComponent, 9 | tags: ['autodocs'], 10 | argTypes: { 11 | initialCount: { control: 'number' }, 12 | }, 13 | } satisfies Meta 14 | 15 | export default meta 16 | type Story = StoryObj 17 | 18 | // More on writing stories with args: https://storybook.js.org/docs/react/writing-stories/args 19 | export const Default: Story = { 20 | args: { 21 | initialCount: 0, 22 | }, 23 | } 24 | 25 | export const WithInitialCount: Story = { 26 | args: { 27 | initialCount: 42, 28 | }, 29 | } 30 | -------------------------------------------------------------------------------- /template/src/components/DemoComponent/index.ts: -------------------------------------------------------------------------------- 1 | export * from './DemoComponent' 2 | -------------------------------------------------------------------------------- /template/src/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './DemoComponent' 2 | -------------------------------------------------------------------------------- /template/src/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './components' 2 | -------------------------------------------------------------------------------- /template/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /template/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /template/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "exclude": ["template"], 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /template/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react'; 2 | import path from 'node:path'; 3 | import { defineConfig } from 'vite'; 4 | import dts from 'vite-plugin-dts'; 5 | 6 | export default defineConfig({ 7 | plugins: [ 8 | react(), 9 | dts({ 10 | insertTypesEntry: true, 11 | }), 12 | ], 13 | build: { 14 | sourcemap: true, 15 | lib: { 16 | entry: path.resolve(__dirname, 'src/index.tsx'), 17 | name: 'ViteReactLibraryTemplate', 18 | formats: ['es', 'umd'], 19 | fileName: (format) => `index.${format}.js`, 20 | }, 21 | rollupOptions: { 22 | external: ['react', 'react-dom'], 23 | output: { 24 | globals: { 25 | react: 'React', 26 | 'react-dom': 'ReactDOM', 27 | }, 28 | }, 29 | }, 30 | }, 31 | }); 32 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["DOM", "DOM.Iterable", "ES2020"], 4 | "module": "es2022", 5 | "moduleResolution": "Node", 6 | "resolveJsonModule": true, 7 | "allowJs": true, 8 | "checkJs": true, 9 | "baseUrl": "./", 10 | "paths": { 11 | "~/*": ["./src/*"] 12 | }, 13 | "outDir": "./dist", 14 | "noEmit": true, 15 | "declaration": true, 16 | "declarationMap": true, 17 | "sourceMap": true, 18 | "removeComments": true, 19 | "strict": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noImplicitOverride": true, 22 | "noImplicitReturns": true, 23 | "noUnusedLocals": true, 24 | "noUnusedParameters": true, 25 | "useUnknownInCatchVariables": true, 26 | "noUncheckedIndexedAccess": true, 27 | "allowSyntheticDefaultImports": true, 28 | "esModuleInterop": true, 29 | "forceConsistentCasingInFileNames": true, 30 | "skipLibCheck": true, 31 | "useDefineForClassFields": true 32 | }, 33 | "exclude": ["template"], 34 | "include": ["src", "tsup.config.ts", "./reset.d.ts"] 35 | } 36 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | const isDev = process.env.npm_lifecycle_event === 'dev' 4 | 5 | export default defineConfig({ 6 | clean: true, 7 | dts: true, 8 | entry: ['src/index.ts'], 9 | format: ['esm'], 10 | minify: !isDev, 11 | metafile: !isDev, 12 | sourcemap: true, 13 | target: 'esnext', 14 | outDir: 'dist', 15 | onSuccess: isDev ? 'node dist/index.js' : undefined, 16 | }) 17 | --------------------------------------------------------------------------------