spin-comp
v0.1.0
Published
Zero-config component scaffolder. Spin up React/Preact/Solid component folders (.tsx, styles, tests, stories, barrel) in seconds with interactive prompts.
Maintainers
Readme
spin-comp
Zero-config component scaffolder. Spin up a complete component folder — source, styles, tests, a Storybook story, and a barrel export — in about 5 seconds instead of 3 minutes of tedious copy-paste.
npx spin-comp Buttonsrc/components/Button/
├── Button.tsx
├── Button.module.css
├── Button.test.tsx
├── Button.stories.tsx # optional
└── index.tsNo config required to start. When you do want to lock in your team's conventions, drop a .spincomprc.json and every future run is a single keystroke.
Why
Creating a new UI component usually means: make a folder, make a .tsx file, make a .css file, make a .test.tsx file, then paste the same boilerplate into all of them and rename things. spin-comp turns that ritual into one command with a few quick prompts.
- Fast — full folder in seconds, interactive or fully flag-driven.
- Flexible — React, Preact, or Solid · TS or JS · CSS Modules, SCSS, styled-components, Tailwind, or none · Vitest or Jest.
- Consistent — a config file / presets keep every component in your codebase identical in shape.
- Extensible — override any generated file with your own
.tmpltemplates. - Scriptable — a typed programmatic API for your own tooling.
Install
Run ad-hoc with npx (nothing to install):
npx spin-comp ButtonOr add it to a project:
npm install --save-dev spin-compRequires Node.js >= 18.
Usage
Interactive (default)
npx spin-comp ButtonYou'll be asked a handful of questions (kind, framework, language, styling, tests, story, export style, barrel, directory). Values from your config file are pre-selected, so you can blast through with Enter.
Non-interactive
Pass --yes to skip all prompts and use your config + flags + defaults. Great for scripts and muscle memory:
npx spin-comp Button --yes --style css-module --story
npx spin-comp useToggle --yes --kind hook --js
npx spin-comp Theme --yes --kind contextNested paths
The name accepts a path. Everything before the last segment becomes a sub-directory:
npx spin-comp forms/inputs/TextField --yes
# -> src/components/forms/inputs/TextField/TextField.tsxPreview without writing
npx spin-comp Button --yes --dry-runWhat it generates
| Kind | Files | Default directory |
| ----------- | ----------------------------------------------------------- | ----------------- |
| component | Name.tsx, styles, Name.test.tsx, Name.stories.tsx, index.ts | src/components |
| hook | useName.ts, useName.test.ts, index.ts | src/hooks |
| context | Name.tsx (provider + useName hook), test, index.ts | src/context |
Example component (--framework react --style css-module):
import type { ReactNode } from 'react';
import styles from './Button.module.css';
export interface ButtonProps {
children?: ReactNode;
className?: string;
}
export function Button({ children, className }: ButtonProps) {
return <div className={[styles.button, className].filter(Boolean).join(' ')}>{children}</div>;
}CLI options
Usage: spin-comp [name] [options]
spin-comp init
Arguments:
name Component/hook/context name (supports nested paths)
Options:
-k, --kind <kind> component | hook | context
-f, --framework <name> react | preact | solid
-l, --language <language> ts | js
--ts / --js Shorthand for --language
-s, --style <style> css-module | css | scss-module | scss |
styled-components | tailwind | none
-t, --test <runner> vitest | jest | none
--testing-library Include Testing Library assertions
--no-testing-library Skip them
--story / --no-story Generate a Storybook story
--barrel / --no-barrel Generate an index barrel file
-e, --export <style> named | default
-d, --dir <path> Base directory for generated files
--flat / --no-flat Write into the directory without a named subfolder
--props-interface Generate a typed props interface (TS)
--no-props-interface Skip it
--preset <name> Use a named preset from your config
--templates <dir> Directory of custom .tmpl overrides
--force Overwrite files that already exist
--dry-run Show what would be generated without writing
-y, --yes Skip interactive prompts
--cwd <dir> Working directory
-v, --version Print version
-h, --help Show helpConfiguration
Run spin-comp init to drop a starter config, or create one by hand. Config is discovered by walking up from the current directory, so a single file at the repo root covers the whole project. Supported locations (first match wins):
.spincomprc/.spincomprc.jsonspin-comp.config.json.config/spin-comp.json- a
spinCompkey insidepackage.json
{
"framework": "react",
"language": "ts",
"style": "css-module",
"test": "vitest",
"testingLibrary": true,
"story": false,
"barrel": true,
"exportStyle": "named",
"directory": "src/components",
"presets": {
"ui": { "directory": "src/components/ui", "story": true },
"page": { "directory": "src/pages", "style": "css-module" }
}
}Select a preset at run time:
npx spin-comp Modal --preset ui --yesPrecedence (lowest to highest): built-in defaults → config file → selected preset → CLI flags → interactive answers.
Custom templates
Point --templates <dir> (or templatesDir in config) at a folder of .tmpl files to override any generated file. Recognized names: component.tmpl, hook.tmpl, context.tmpl, style.tmpl, test.tmpl, story.tmpl, barrel.tmpl.
Placeholders are substituted with {{ ... }}:
| Placeholder | Example (user-card) |
| ------------------ | --------------------- |
| {{name}} | UserCard |
| {{camelName}} | userCard |
| {{kebabName}} | user-card |
| {{constantName}} | USER_CARD |
| {{ext}} | tsx |
| {{styleFileName}}| UserCard.module.css |
| {{framework}} | react |
templates/component.tmpl
---
import styles from './{{styleFileName}}';
export function {{name}}() {
return <div className={styles.{{camelName}}} />;
}Programmatic API
import { scaffold, planScaffoldFor } from 'spin-comp/api';
// Write files to disk
await scaffold('Button', {
framework: 'react',
style: 'css-module',
story: true,
cwd: process.cwd(),
});
// Or just plan (no writes) — useful in tests
const { files } = await planScaffoldFor('Button', { style: 'none' });
files.forEach((f) => console.log(f.relativePath));scaffold(name, options) and planScaffoldFor(name, options) accept every config field plus cwd, force, dryRun, templatesDir, preset, and loadConfigFile.
Development
npm install
npm run build # compile TypeScript to dist/
npm test # run the vitest suite
npm run typecheck # type-check without emittingLicense
MIT © phemymii
