devstarter-tool 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +112 -61
- package/dist/cli.js +9 -0
- package/dist/commands/add/collector.js +47 -0
- package/dist/commands/add/registry.js +14 -0
- package/dist/commands/add/resolvers.js +8 -0
- package/dist/commands/add.js +65 -0
- package/dist/commands/init/collector.js +27 -1
- package/dist/features/eslint.js +74 -0
- package/dist/features/prettier.js +55 -0
- package/dist/features/vitest.js +31 -0
- package/dist/generators/createMonorepo.js +11 -3
- package/dist/generators/createProject.js +10 -2
- package/dist/prompts/addPrompts.js +20 -0
- package/dist/prompts/initPrompts.js +8 -0
- package/dist/types/feature.js +1 -0
- package/dist/types/project.js +1 -0
- package/dist/utils/detectProjectContext.js +18 -0
- package/dist/utils/installDependencies.js +8 -0
- package/dist/utils/printSummary.js +0 -1
- package/dist/utils/setupVitest.js +31 -0
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
CLI to generate projects with best practices and predefined configurations.
|
|
4
4
|
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Project scaffolding**: Create frontend, backend, or monorepo projects in seconds
|
|
8
|
+
- **`add` command**: Add features like ESLint, Vitest, and Prettier to existing projects
|
|
9
|
+
- **Interactive prompts**: Guided project creation or flag-based configuration
|
|
10
|
+
- **Automatic dependency installation**: No need to run `npm install` manually
|
|
11
|
+
- **Package manager detection**: Automatically uses npm, pnpm, or yarn
|
|
12
|
+
- **Git initialization**: Optional repository setup
|
|
13
|
+
- **Dry-run mode**: Preview changes before creating files
|
|
14
|
+
|
|
5
15
|
## Installation
|
|
6
16
|
|
|
7
17
|
```bash
|
|
@@ -14,26 +24,47 @@ Or run directly with npx:
|
|
|
14
24
|
npx devstarter-tool init my-app
|
|
15
25
|
```
|
|
16
26
|
|
|
17
|
-
##
|
|
18
|
-
|
|
19
|
-
### Basic command
|
|
27
|
+
## Quick Start
|
|
20
28
|
|
|
21
29
|
```bash
|
|
22
|
-
|
|
30
|
+
# Interactive mode - guided setup
|
|
31
|
+
devstarter init
|
|
32
|
+
|
|
33
|
+
# Quick project with defaults
|
|
34
|
+
devstarter init my-app -y
|
|
35
|
+
|
|
36
|
+
# Frontend project with Vitest
|
|
37
|
+
devstarter init my-app --type frontend --vitest
|
|
38
|
+
|
|
39
|
+
# Add features to an existing project
|
|
40
|
+
devstarter add prettier
|
|
41
|
+
devstarter add eslint
|
|
42
|
+
|
|
43
|
+
# Preview without creating files
|
|
44
|
+
devstarter init my-app --dry-run
|
|
23
45
|
```
|
|
24
46
|
|
|
25
|
-
|
|
47
|
+
## Commands
|
|
48
|
+
|
|
49
|
+
### `devstarter init`
|
|
50
|
+
|
|
51
|
+
Scaffolds a new project from a template.
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
devstarter init [project-name] [options]
|
|
55
|
+
```
|
|
26
56
|
|
|
27
57
|
| Option | Description |
|
|
28
58
|
|--------|-------------|
|
|
29
59
|
| `-y, --yes` | Use default values without prompting |
|
|
30
60
|
| `-t, --type <type>` | Project type: `frontend` or `backend` |
|
|
31
|
-
| `--template <name>` | Template to use |
|
|
32
|
-
| `--
|
|
33
|
-
| `--no-git` | Skip
|
|
61
|
+
| `--template <name>` | Template to use (e.g., `basic`, `react`) |
|
|
62
|
+
| `--vitest` | Add Vitest for testing |
|
|
63
|
+
| `--no-git` | Skip Git repository initialization |
|
|
34
64
|
| `--no-vitest` | Skip Vitest testing framework setup |
|
|
65
|
+
| `--dry-run` | Preview changes without creating files |
|
|
35
66
|
|
|
36
|
-
|
|
67
|
+
#### Examples
|
|
37
68
|
|
|
38
69
|
```bash
|
|
39
70
|
# Full interactive mode
|
|
@@ -42,17 +73,60 @@ devstarter init
|
|
|
42
73
|
# Create project with specific name
|
|
43
74
|
devstarter init my-app
|
|
44
75
|
|
|
45
|
-
#
|
|
46
|
-
devstarter init my-app --type frontend
|
|
76
|
+
# Frontend with React template
|
|
77
|
+
devstarter init my-app --type frontend --template react
|
|
78
|
+
|
|
79
|
+
# Backend with testing setup
|
|
80
|
+
devstarter init my-api --type backend --vitest
|
|
47
81
|
|
|
48
|
-
#
|
|
49
|
-
devstarter init my-app --type frontend
|
|
82
|
+
# Quick frontend with all defaults
|
|
83
|
+
devstarter init my-app --type frontend -y
|
|
50
84
|
|
|
51
|
-
# Create
|
|
85
|
+
# Create without Git
|
|
52
86
|
devstarter init my-app --no-git
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### `devstarter add`
|
|
90
|
+
|
|
91
|
+
Adds features to an existing project. Automatically detects features already configured and skips them.
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
devstarter add [feature] [options]
|
|
95
|
+
```
|
|
53
96
|
|
|
54
|
-
|
|
55
|
-
|
|
97
|
+
| Option | Description |
|
|
98
|
+
|--------|-------------|
|
|
99
|
+
| `--list` | List all available features |
|
|
100
|
+
| `-y, --yes` | Add all available features without prompting |
|
|
101
|
+
| `--dry-run` | Show what would be added without making changes |
|
|
102
|
+
|
|
103
|
+
#### Available Features
|
|
104
|
+
|
|
105
|
+
| Feature | Description | What it adds |
|
|
106
|
+
|---------|-------------|--------------|
|
|
107
|
+
| `eslint` | Linter for JavaScript and TypeScript | `eslint.config.js`, lint script, ESLint + typescript-eslint deps |
|
|
108
|
+
| `vitest` | Unit testing framework | `vitest.config.ts`, test scripts, Vitest dep |
|
|
109
|
+
| `prettier` | Code formatter | `.prettierrc`, format script, Prettier dep |
|
|
110
|
+
|
|
111
|
+
#### Examples
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
# Add a specific feature
|
|
115
|
+
devstarter add prettier
|
|
116
|
+
devstarter add eslint
|
|
117
|
+
devstarter add vitest
|
|
118
|
+
|
|
119
|
+
# Interactive mode - choose from available features
|
|
120
|
+
devstarter add
|
|
121
|
+
|
|
122
|
+
# Add all available features at once
|
|
123
|
+
devstarter add -y
|
|
124
|
+
|
|
125
|
+
# List available features
|
|
126
|
+
devstarter add --list
|
|
127
|
+
|
|
128
|
+
# Preview changes
|
|
129
|
+
devstarter add prettier --dry-run
|
|
56
130
|
```
|
|
57
131
|
|
|
58
132
|
## Project Structures
|
|
@@ -62,13 +136,12 @@ devstarter init my-app --no-vitest
|
|
|
62
136
|
```
|
|
63
137
|
my-app/
|
|
64
138
|
├── src/
|
|
65
|
-
│
|
|
66
|
-
│ └── __tests__/
|
|
67
|
-
│ └── example.test.ts
|
|
68
|
-
├── vitest.config.ts
|
|
139
|
+
│ └── main.ts
|
|
69
140
|
├── package.json
|
|
70
|
-
├──
|
|
71
|
-
|
|
141
|
+
├── tsconfig.json
|
|
142
|
+
├── vitest.config.ts # if --vitest
|
|
143
|
+
├── node_modules/
|
|
144
|
+
└── .git/ # if git initialized
|
|
72
145
|
```
|
|
73
146
|
|
|
74
147
|
### Monorepo (full-stack)
|
|
@@ -76,10 +149,16 @@ my-app/
|
|
|
76
149
|
```
|
|
77
150
|
my-app/
|
|
78
151
|
├── apps/
|
|
79
|
-
│ ├── web/
|
|
80
|
-
│
|
|
152
|
+
│ ├── web/ # frontend template
|
|
153
|
+
│ │ ├── src/
|
|
154
|
+
│ │ ├── package.json
|
|
155
|
+
│ │ └── vitest.config.ts
|
|
156
|
+
│ └── api/ # backend template
|
|
157
|
+
│ ├── src/
|
|
158
|
+
│ ├── package.json
|
|
159
|
+
│ └── vitest.config.ts
|
|
81
160
|
├── packages/
|
|
82
|
-
│ └── shared/
|
|
161
|
+
│ └── shared/ # shared code
|
|
83
162
|
├── package.json
|
|
84
163
|
├── pnpm-workspace.yaml
|
|
85
164
|
├── tsconfig.base.json
|
|
@@ -92,7 +171,7 @@ my-app/
|
|
|
92
171
|
|
|
93
172
|
| Template | Description |
|
|
94
173
|
|----------|-------------|
|
|
95
|
-
| `basic` |
|
|
174
|
+
| `basic` | Vite + TypeScript |
|
|
96
175
|
| `react` | React 18 + Vite + TypeScript |
|
|
97
176
|
|
|
98
177
|
### Backend
|
|
@@ -101,42 +180,12 @@ my-app/
|
|
|
101
180
|
|----------|-------------|
|
|
102
181
|
| `basic` | Express + TypeScript |
|
|
103
182
|
|
|
104
|
-
##
|
|
105
|
-
|
|
106
|
-
- Project structure selection (basic or monorepo)
|
|
107
|
-
- Automatic package manager detection (npm, pnpm, yarn)
|
|
108
|
-
- Interactive template selection
|
|
109
|
-
- Optional Git repository initialization
|
|
110
|
-
- Optional Vitest testing framework setup
|
|
111
|
-
- Dry-run mode to preview changes
|
|
112
|
-
- Automatic project name normalization (kebab-case)
|
|
113
|
-
- Colored output for better readability
|
|
114
|
-
|
|
115
|
-
## Testing Setup (Vitest)
|
|
116
|
-
|
|
117
|
-
By default, projects are created with Vitest configured. This includes:
|
|
118
|
-
|
|
119
|
-
- `vitest` as a dev dependency
|
|
120
|
-
- `vitest.config.ts` configuration file
|
|
121
|
-
- Example test file in `src/__tests__/example.test.ts`
|
|
122
|
-
- `test` script in package.json
|
|
123
|
-
|
|
124
|
-
For React projects, it also adds `jsdom` for DOM testing.
|
|
125
|
-
|
|
126
|
-
To skip Vitest setup, use the `--no-vitest` flag:
|
|
127
|
-
|
|
128
|
-
```bash
|
|
129
|
-
devstarter init my-app --no-vitest
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
## Development
|
|
133
|
-
|
|
134
|
-
### Requirements
|
|
183
|
+
## Requirements
|
|
135
184
|
|
|
136
185
|
- Node.js 18+
|
|
137
|
-
- npm, pnpm or yarn
|
|
186
|
+
- npm, pnpm, or yarn
|
|
138
187
|
|
|
139
|
-
|
|
188
|
+
## Development
|
|
140
189
|
|
|
141
190
|
```bash
|
|
142
191
|
# Clone repository
|
|
@@ -153,13 +202,15 @@ npm run build
|
|
|
153
202
|
node dist/cli.js init test-app --dry-run
|
|
154
203
|
```
|
|
155
204
|
|
|
156
|
-
###
|
|
205
|
+
### Scripts
|
|
157
206
|
|
|
158
207
|
| Script | Description |
|
|
159
208
|
|--------|-------------|
|
|
160
209
|
| `npm run build` | Compile TypeScript and copy templates |
|
|
161
210
|
| `npm run dev` | Watch mode for development |
|
|
162
|
-
| `npm run test` | Run tests |
|
|
211
|
+
| `npm run test` | Run tests in watch mode |
|
|
212
|
+
| `npm run test:run` | Single test run |
|
|
213
|
+
| `npm run test:coverage` | Coverage with HTML + text reports |
|
|
163
214
|
| `npm run lint` | Run ESLint |
|
|
164
215
|
| `npm run format` | Format code with Prettier |
|
|
165
216
|
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from 'commander';
|
|
3
3
|
import { initCommand } from './commands/init.js';
|
|
4
|
+
import { addCommand } from './commands/add.js';
|
|
4
5
|
const program = new Command();
|
|
5
6
|
program
|
|
6
7
|
.name('devstarter')
|
|
@@ -14,5 +15,13 @@ program
|
|
|
14
15
|
.option('--template <name>', 'Template variant (e.g. basic, react)')
|
|
15
16
|
.option('--dry-run', 'Show what would be generated without creating files')
|
|
16
17
|
.option('--no-git', 'Skip git repository initialization')
|
|
18
|
+
.option('--vitest', 'Add Vitest for testing')
|
|
17
19
|
.action(initCommand);
|
|
20
|
+
program
|
|
21
|
+
.command('add [feature]')
|
|
22
|
+
.description('Add a feature to an existing project')
|
|
23
|
+
.option('--dry-run', 'Show what would be added without making changes')
|
|
24
|
+
.option('--list', 'List available features')
|
|
25
|
+
.option('-y, --yes', 'Add all available features without prompting')
|
|
26
|
+
.action(addCommand);
|
|
18
27
|
program.parse(process.argv);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { detectProjectContext } from '../../utils/detectProjectContext.js';
|
|
2
|
+
import { PromptCancelledError } from '../../prompts/initPrompts.js';
|
|
3
|
+
import { getAllFeatures } from './registry.js';
|
|
4
|
+
import { resolveFeatureArg } from './resolvers.js';
|
|
5
|
+
import { askFeatures } from '../../prompts/addPrompts.js';
|
|
6
|
+
export async function collectAddContext(featureArg, options) {
|
|
7
|
+
const projectContext = await detectProjectContext();
|
|
8
|
+
if (featureArg) {
|
|
9
|
+
const featureId = resolveFeatureArg(featureArg);
|
|
10
|
+
return {
|
|
11
|
+
projectRoot: projectContext.root,
|
|
12
|
+
features: [featureId],
|
|
13
|
+
isDryRun: Boolean(options.dryRun),
|
|
14
|
+
packageManager: projectContext.packageManager,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
// Filter out already-detected features
|
|
18
|
+
const allFeatures = getAllFeatures();
|
|
19
|
+
const available = [];
|
|
20
|
+
for (const feature of allFeatures) {
|
|
21
|
+
const detected = await feature.detect(projectContext);
|
|
22
|
+
if (!detected) {
|
|
23
|
+
available.push(feature);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (available.length === 0) {
|
|
27
|
+
throw new Error('All available features are already configured.');
|
|
28
|
+
}
|
|
29
|
+
if (options.yes) {
|
|
30
|
+
return {
|
|
31
|
+
projectRoot: projectContext.root,
|
|
32
|
+
features: available.map((f) => f.id),
|
|
33
|
+
isDryRun: Boolean(options.dryRun),
|
|
34
|
+
packageManager: projectContext.packageManager,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const answer = await askFeatures(available);
|
|
38
|
+
if (answer.features.length === 0) {
|
|
39
|
+
throw new PromptCancelledError();
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
projectRoot: projectContext.root,
|
|
43
|
+
features: answer.features,
|
|
44
|
+
isDryRun: Boolean(options.dryRun),
|
|
45
|
+
packageManager: projectContext.packageManager,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { eslintFeature } from '../../features/eslint.js';
|
|
2
|
+
import { vitestFeature } from '../../features/vitest.js';
|
|
3
|
+
import { prettierFeature } from '../../features/prettier.js';
|
|
4
|
+
const features = [eslintFeature, vitestFeature, prettierFeature];
|
|
5
|
+
const featureMap = new Map(features.map((f) => [f.id, f]));
|
|
6
|
+
export function getAvailableFeatureIds() {
|
|
7
|
+
return features.map((f) => f.id);
|
|
8
|
+
}
|
|
9
|
+
export function getFeature(id) {
|
|
10
|
+
return featureMap.get(id);
|
|
11
|
+
}
|
|
12
|
+
export function getAllFeatures() {
|
|
13
|
+
return features;
|
|
14
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { getAvailableFeatureIds } from './registry.js';
|
|
2
|
+
export function resolveFeatureArg(arg) {
|
|
3
|
+
const available = getAvailableFeatureIds();
|
|
4
|
+
if (!available.includes(arg)) {
|
|
5
|
+
throw new Error(`Unknown feature "${arg}". Available: ${available.join(', ')}`);
|
|
6
|
+
}
|
|
7
|
+
return arg;
|
|
8
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { PromptCancelledError } from '../prompts/initPrompts.js';
|
|
2
|
+
import { styles } from '../utils/styles.js';
|
|
3
|
+
import { detectProjectContext } from '../utils/detectProjectContext.js';
|
|
4
|
+
import { installDependencies } from '../utils/installDependencies.js';
|
|
5
|
+
import { getAllFeatures, getFeature } from './add/registry.js';
|
|
6
|
+
import { collectAddContext } from './add/collector.js';
|
|
7
|
+
export async function addCommand(featureArg, options) {
|
|
8
|
+
try {
|
|
9
|
+
if (options.list) {
|
|
10
|
+
printFeatureList();
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const context = await collectAddContext(featureArg, options);
|
|
14
|
+
if (context.isDryRun) {
|
|
15
|
+
printDryRun(context.features);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const projectContext = await detectProjectContext(context.projectRoot);
|
|
19
|
+
for (const featureId of context.features) {
|
|
20
|
+
const feature = getFeature(featureId);
|
|
21
|
+
console.log(`${styles.info('Adding')} ${feature.name}...`);
|
|
22
|
+
await feature.apply(projectContext);
|
|
23
|
+
console.log(`${styles.success('Added')} ${feature.name}`);
|
|
24
|
+
}
|
|
25
|
+
console.log(`\n${styles.info('Installing dependencies...')}`);
|
|
26
|
+
installDependencies(context.projectRoot, context.packageManager);
|
|
27
|
+
printAddSummary(context.features);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
handleError(error);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function printFeatureList() {
|
|
34
|
+
const features = getAllFeatures();
|
|
35
|
+
console.log(`\n${styles.title('Available features')}\n`);
|
|
36
|
+
for (const feature of features) {
|
|
37
|
+
console.log(` ${styles.highlight(feature.id)} - ${feature.description}`);
|
|
38
|
+
}
|
|
39
|
+
console.log('');
|
|
40
|
+
}
|
|
41
|
+
function printDryRun(featureIds) {
|
|
42
|
+
console.log(`\n${styles.warning('Dry run – no changes will be made')}\n`);
|
|
43
|
+
console.log(styles.title('Features to add'));
|
|
44
|
+
for (const id of featureIds) {
|
|
45
|
+
const feature = getFeature(id);
|
|
46
|
+
console.log(` ${styles.info('-')} ${feature.name}: ${feature.description}`);
|
|
47
|
+
}
|
|
48
|
+
console.log('');
|
|
49
|
+
}
|
|
50
|
+
function printAddSummary(featureIds) {
|
|
51
|
+
console.log(`\n${styles.success('Done!')}\n`);
|
|
52
|
+
console.log(styles.title('Added features'));
|
|
53
|
+
for (const id of featureIds) {
|
|
54
|
+
const feature = getFeature(id);
|
|
55
|
+
console.log(` ${styles.success('-')} ${feature.name}`);
|
|
56
|
+
}
|
|
57
|
+
console.log('');
|
|
58
|
+
}
|
|
59
|
+
function handleError(error) {
|
|
60
|
+
if (error instanceof PromptCancelledError) {
|
|
61
|
+
console.log(`\n${styles.muted('Operation cancelled')}`);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
console.error(`\n${styles.error('Error:')} ${error.message}`);
|
|
65
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DEFAULT_INIT_OPTIONS } from '../../types/project.js';
|
|
2
|
-
import { askProjectName, askProjectStructure, askInitQuestions, askTemplate, askInitGit, } from '../../prompts/initPrompts.js';
|
|
2
|
+
import { askProjectName, askProjectStructure, askInitQuestions, askTemplate, askInitGit, askUseVitest, } from '../../prompts/initPrompts.js';
|
|
3
3
|
import { normalizeProjectName } from '../../utils/normalize.js';
|
|
4
4
|
import { detectPackageManager } from '../../utils/detectPackageManager.js';
|
|
5
5
|
import { listTemplates } from '../../utils/listTemplate.js';
|
|
@@ -60,12 +60,25 @@ async function collectBasicContext(projectName, options, useDefaults) {
|
|
|
60
60
|
const templates = listTemplates(projectType);
|
|
61
61
|
const templateFromFlag = resolveTemplateFlag(options.template, templates);
|
|
62
62
|
const template = await collectTemplate(templateFromFlag, templates, useDefaults);
|
|
63
|
+
// Obtener useVitest
|
|
64
|
+
const vitestFlagProvided = options.vitest !== undefined;
|
|
65
|
+
let useVitest;
|
|
66
|
+
if (vitestFlagProvided) {
|
|
67
|
+
useVitest = options.vitest;
|
|
68
|
+
}
|
|
69
|
+
else if (useDefaults) {
|
|
70
|
+
useVitest = DEFAULT_INIT_OPTIONS.useVitest;
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
useVitest = (await askUseVitest()).useVitest;
|
|
74
|
+
}
|
|
63
75
|
return {
|
|
64
76
|
structure: 'basic',
|
|
65
77
|
projectName,
|
|
66
78
|
projectType,
|
|
67
79
|
template,
|
|
68
80
|
initGit,
|
|
81
|
+
useVitest,
|
|
69
82
|
packageManager: detectPackageManager(),
|
|
70
83
|
isDryRun: Boolean(options.dryRun),
|
|
71
84
|
};
|
|
@@ -102,12 +115,25 @@ async function collectMonorepoContext(projectName, useDefaults, options) {
|
|
|
102
115
|
initGit = gitAnswer.initGit;
|
|
103
116
|
}
|
|
104
117
|
}
|
|
118
|
+
// Obtener useVitest
|
|
119
|
+
const vitestFlagProvided = options.vitest !== undefined;
|
|
120
|
+
let useVitest;
|
|
121
|
+
if (vitestFlagProvided) {
|
|
122
|
+
useVitest = options.vitest;
|
|
123
|
+
}
|
|
124
|
+
else if (useDefaults) {
|
|
125
|
+
useVitest = DEFAULT_INIT_OPTIONS.useVitest;
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
useVitest = (await askUseVitest()).useVitest;
|
|
129
|
+
}
|
|
105
130
|
return {
|
|
106
131
|
structure: 'monorepo',
|
|
107
132
|
projectName,
|
|
108
133
|
webTemplate,
|
|
109
134
|
apiTemplate,
|
|
110
135
|
initGit,
|
|
136
|
+
useVitest,
|
|
111
137
|
packageManager: 'pnpm', // Monorepo usa pnpm por defecto
|
|
112
138
|
isDryRun: Boolean(options.dryRun),
|
|
113
139
|
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const ESLINT_CONFIG_TS = `import eslint from '@eslint/js';
|
|
4
|
+
import tseslint from 'typescript-eslint';
|
|
5
|
+
|
|
6
|
+
export default tseslint.config(
|
|
7
|
+
eslint.configs.recommended,
|
|
8
|
+
...tseslint.configs.recommended,
|
|
9
|
+
{
|
|
10
|
+
ignores: ['dist/'],
|
|
11
|
+
},
|
|
12
|
+
);
|
|
13
|
+
`;
|
|
14
|
+
const ESLINT_CONFIG_JS = `import eslint from '@eslint/js';
|
|
15
|
+
|
|
16
|
+
export default [
|
|
17
|
+
eslint.configs.recommended,
|
|
18
|
+
{
|
|
19
|
+
ignores: ['dist/'],
|
|
20
|
+
},
|
|
21
|
+
];
|
|
22
|
+
`;
|
|
23
|
+
async function detect(context) {
|
|
24
|
+
const configFiles = [
|
|
25
|
+
'eslint.config.js',
|
|
26
|
+
'eslint.config.mjs',
|
|
27
|
+
'eslint.config.cjs',
|
|
28
|
+
'.eslintrc.js',
|
|
29
|
+
'.eslintrc.json',
|
|
30
|
+
'.eslintrc.yml',
|
|
31
|
+
'.eslintrc',
|
|
32
|
+
];
|
|
33
|
+
for (const file of configFiles) {
|
|
34
|
+
if (await fs.pathExists(path.join(context.root, file))) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const deps = {
|
|
39
|
+
...(context.packageJson.dependencies ?? {}),
|
|
40
|
+
...(context.packageJson.devDependencies ?? {}),
|
|
41
|
+
};
|
|
42
|
+
return 'eslint' in deps;
|
|
43
|
+
}
|
|
44
|
+
async function apply(context) {
|
|
45
|
+
const packageJsonPath = path.join(context.root, 'package.json');
|
|
46
|
+
const packageJson = await fs.readJson(packageJsonPath);
|
|
47
|
+
const devDeps = {
|
|
48
|
+
eslint: '^9.0.0',
|
|
49
|
+
'@eslint/js': '^9.0.0',
|
|
50
|
+
};
|
|
51
|
+
if (context.hasTypescript) {
|
|
52
|
+
devDeps['typescript-eslint'] = '^8.0.0';
|
|
53
|
+
}
|
|
54
|
+
packageJson.devDependencies = {
|
|
55
|
+
...packageJson.devDependencies,
|
|
56
|
+
...devDeps,
|
|
57
|
+
};
|
|
58
|
+
packageJson.scripts = {
|
|
59
|
+
...packageJson.scripts,
|
|
60
|
+
lint: 'eslint .',
|
|
61
|
+
};
|
|
62
|
+
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
|
63
|
+
const configContent = context.hasTypescript
|
|
64
|
+
? ESLINT_CONFIG_TS
|
|
65
|
+
: ESLINT_CONFIG_JS;
|
|
66
|
+
await fs.writeFile(path.join(context.root, 'eslint.config.js'), configContent);
|
|
67
|
+
}
|
|
68
|
+
export const eslintFeature = {
|
|
69
|
+
id: 'eslint',
|
|
70
|
+
name: 'ESLint',
|
|
71
|
+
description: 'Linter for JavaScript and TypeScript',
|
|
72
|
+
detect,
|
|
73
|
+
apply,
|
|
74
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const PRETTIER_CONFIG = `{
|
|
4
|
+
"semi": true,
|
|
5
|
+
"singleQuote": true,
|
|
6
|
+
"trailingComma": "all",
|
|
7
|
+
"printWidth": 80,
|
|
8
|
+
"tabWidth": 2
|
|
9
|
+
}
|
|
10
|
+
`;
|
|
11
|
+
async function detect(context) {
|
|
12
|
+
const configFiles = [
|
|
13
|
+
'.prettierrc',
|
|
14
|
+
'.prettierrc.json',
|
|
15
|
+
'.prettierrc.yaml',
|
|
16
|
+
'.prettierrc.yml',
|
|
17
|
+
'.prettierrc.js',
|
|
18
|
+
'.prettierrc.cjs',
|
|
19
|
+
'.prettierrc.mjs',
|
|
20
|
+
'prettier.config.js',
|
|
21
|
+
'prettier.config.cjs',
|
|
22
|
+
'prettier.config.mjs',
|
|
23
|
+
];
|
|
24
|
+
for (const file of configFiles) {
|
|
25
|
+
if (await fs.pathExists(path.join(context.root, file))) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const deps = {
|
|
30
|
+
...(context.packageJson.dependencies ?? {}),
|
|
31
|
+
...(context.packageJson.devDependencies ?? {}),
|
|
32
|
+
};
|
|
33
|
+
return 'prettier' in deps;
|
|
34
|
+
}
|
|
35
|
+
async function apply(context) {
|
|
36
|
+
const packageJsonPath = path.join(context.root, 'package.json');
|
|
37
|
+
const packageJson = await fs.readJson(packageJsonPath);
|
|
38
|
+
packageJson.devDependencies = {
|
|
39
|
+
...packageJson.devDependencies,
|
|
40
|
+
prettier: '^3.0.0',
|
|
41
|
+
};
|
|
42
|
+
packageJson.scripts = {
|
|
43
|
+
...packageJson.scripts,
|
|
44
|
+
format: 'prettier --write .',
|
|
45
|
+
};
|
|
46
|
+
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
|
47
|
+
await fs.writeFile(path.join(context.root, '.prettierrc'), PRETTIER_CONFIG);
|
|
48
|
+
}
|
|
49
|
+
export const prettierFeature = {
|
|
50
|
+
id: 'prettier',
|
|
51
|
+
name: 'Prettier',
|
|
52
|
+
description: 'Code formatter for JavaScript and TypeScript',
|
|
53
|
+
detect,
|
|
54
|
+
apply,
|
|
55
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { setupVitest } from '../utils/setupVitest.js';
|
|
4
|
+
async function detect(context) {
|
|
5
|
+
const configFiles = [
|
|
6
|
+
'vitest.config.ts',
|
|
7
|
+
'vitest.config.js',
|
|
8
|
+
'vitest.config.mts',
|
|
9
|
+
'vitest.config.mjs',
|
|
10
|
+
];
|
|
11
|
+
for (const file of configFiles) {
|
|
12
|
+
if (await fs.pathExists(path.join(context.root, file))) {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
const deps = {
|
|
17
|
+
...(context.packageJson.dependencies ?? {}),
|
|
18
|
+
...(context.packageJson.devDependencies ?? {}),
|
|
19
|
+
};
|
|
20
|
+
return 'vitest' in deps;
|
|
21
|
+
}
|
|
22
|
+
async function apply(context) {
|
|
23
|
+
await setupVitest(context.root);
|
|
24
|
+
}
|
|
25
|
+
export const vitestFeature = {
|
|
26
|
+
id: 'vitest',
|
|
27
|
+
name: 'Vitest',
|
|
28
|
+
description: 'Unit testing framework for JavaScript and TypeScript',
|
|
29
|
+
detect,
|
|
30
|
+
apply,
|
|
31
|
+
};
|
|
@@ -3,7 +3,9 @@ import path from 'node:path';
|
|
|
3
3
|
import { getTemplatePath } from '../utils/getTemplatePath.js';
|
|
4
4
|
import { copyTemplate } from '../utils/copyTemplate.js';
|
|
5
5
|
import { initGitRepo } from '../utils/git.js';
|
|
6
|
-
|
|
6
|
+
import { setupVitest } from '../utils/setupVitest.js';
|
|
7
|
+
import { installDependencies } from '../utils/installDependencies.js';
|
|
8
|
+
export async function createMonorepo({ projectName, webTemplate, apiTemplate, initGit, useVitest, packageManager, }) {
|
|
7
9
|
// 1. Resolver ruta absoluta del proyecto
|
|
8
10
|
const projectRoot = path.resolve(process.cwd(), projectName);
|
|
9
11
|
// 2. Evitar sobrescribir carpetas existentes
|
|
@@ -32,7 +34,14 @@ export async function createMonorepo({ projectName, webTemplate, apiTemplate, in
|
|
|
32
34
|
}
|
|
33
35
|
// 7. Crear package shared básico
|
|
34
36
|
await createSharedPackage(projectRoot, projectName);
|
|
35
|
-
// 8.
|
|
37
|
+
// 8. Configurar Vitest en apps (si aplica)
|
|
38
|
+
if (useVitest) {
|
|
39
|
+
await setupVitest(path.join(projectRoot, 'apps', 'web'));
|
|
40
|
+
await setupVitest(path.join(projectRoot, 'apps', 'api'));
|
|
41
|
+
}
|
|
42
|
+
// 9. Instalar dependencias
|
|
43
|
+
installDependencies(projectRoot, packageManager);
|
|
44
|
+
// 10. Inicializar Git (si aplica)
|
|
36
45
|
if (initGit) {
|
|
37
46
|
initGitRepo(projectRoot);
|
|
38
47
|
}
|
|
@@ -97,7 +106,6 @@ ${projectName}/
|
|
|
97
106
|
## Getting Started
|
|
98
107
|
|
|
99
108
|
\`\`\`bash
|
|
100
|
-
pnpm install
|
|
101
109
|
pnpm dev
|
|
102
110
|
\`\`\`
|
|
103
111
|
`;
|
|
@@ -3,7 +3,9 @@ import path from 'node:path';
|
|
|
3
3
|
import { getTemplatePath } from '../utils/getTemplatePath.js';
|
|
4
4
|
import { copyTemplate } from '../utils/copyTemplate.js';
|
|
5
5
|
import { initGitRepo } from '../utils/git.js';
|
|
6
|
-
|
|
6
|
+
import { setupVitest } from '../utils/setupVitest.js';
|
|
7
|
+
import { installDependencies } from '../utils/installDependencies.js';
|
|
8
|
+
export async function createProject({ projectName, projectType, template, initGit, useVitest, packageManager, }) {
|
|
7
9
|
// 1. Resolver ruta absoluta del proyecto
|
|
8
10
|
const projectRoot = path.resolve(process.cwd(), projectName);
|
|
9
11
|
// 2. Evitar sobrescribir carpetas existentes
|
|
@@ -21,7 +23,13 @@ export async function createProject({ projectName, projectType, template, initGi
|
|
|
21
23
|
await copyTemplate(templatePath, projectRoot, {
|
|
22
24
|
projectName,
|
|
23
25
|
});
|
|
24
|
-
// 6.
|
|
26
|
+
// 6. Configurar Vitest (si aplica)
|
|
27
|
+
if (useVitest) {
|
|
28
|
+
await setupVitest(projectRoot);
|
|
29
|
+
}
|
|
30
|
+
// 7. Instalar dependencias
|
|
31
|
+
installDependencies(projectRoot, packageManager);
|
|
32
|
+
// 8. Inicializar Git (si aplica)
|
|
25
33
|
if (initGit) {
|
|
26
34
|
initGitRepo(projectRoot);
|
|
27
35
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import prompts from 'prompts';
|
|
2
|
+
import { PromptCancelledError } from './initPrompts.js';
|
|
3
|
+
const onCancel = () => {
|
|
4
|
+
throw new PromptCancelledError();
|
|
5
|
+
};
|
|
6
|
+
export async function askFeatures(available) {
|
|
7
|
+
return prompts({
|
|
8
|
+
type: 'multiselect',
|
|
9
|
+
name: 'features',
|
|
10
|
+
message: 'Select features to add:',
|
|
11
|
+
choices: available.map((f) => ({
|
|
12
|
+
title: f.name,
|
|
13
|
+
value: f.id,
|
|
14
|
+
description: f.description,
|
|
15
|
+
})),
|
|
16
|
+
instructions: false,
|
|
17
|
+
hint: '- Space to select. Return to submit',
|
|
18
|
+
min: 1,
|
|
19
|
+
}, { onCancel });
|
|
20
|
+
}
|
|
@@ -80,3 +80,11 @@ export async function askInitGit() {
|
|
|
80
80
|
initial: true,
|
|
81
81
|
}, { onCancel });
|
|
82
82
|
}
|
|
83
|
+
export async function askUseVitest() {
|
|
84
|
+
return prompts({
|
|
85
|
+
type: 'confirm',
|
|
86
|
+
name: 'useVitest',
|
|
87
|
+
message: 'Add Vitest for testing?',
|
|
88
|
+
initial: false,
|
|
89
|
+
}, { onCancel });
|
|
90
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/types/project.js
CHANGED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { detectPackageManager } from './detectPackageManager.js';
|
|
4
|
+
export async function detectProjectContext(cwd = process.cwd()) {
|
|
5
|
+
const packageJsonPath = path.join(cwd, 'package.json');
|
|
6
|
+
if (!(await fs.pathExists(packageJsonPath))) {
|
|
7
|
+
throw new Error('No package.json found in current directory. Run this command from a project root.');
|
|
8
|
+
}
|
|
9
|
+
const packageJson = await fs.readJson(packageJsonPath);
|
|
10
|
+
const hasTypescript = await fs.pathExists(path.join(cwd, 'tsconfig.json'));
|
|
11
|
+
const packageManager = detectPackageManager(cwd);
|
|
12
|
+
return {
|
|
13
|
+
root: cwd,
|
|
14
|
+
packageJson,
|
|
15
|
+
hasTypescript,
|
|
16
|
+
packageManager,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
export function installDependencies(projectRoot, packageManager) {
|
|
3
|
+
const command = packageManager === 'yarn' ? 'yarn' : `${packageManager} install`;
|
|
4
|
+
execSync(command, {
|
|
5
|
+
cwd: projectRoot,
|
|
6
|
+
stdio: 'inherit',
|
|
7
|
+
});
|
|
8
|
+
}
|
|
@@ -14,7 +14,6 @@ export function printSummary(context) {
|
|
|
14
14
|
console.log(`${styles.info('- Git:')} ${context.initGit ? styles.success('initialized') : styles.muted('not initialized')}\n`);
|
|
15
15
|
console.log(styles.title('Next steps'));
|
|
16
16
|
console.log(` ${styles.highlight(`cd ${context.projectName}`)}`);
|
|
17
|
-
console.log(` ${styles.highlight(`${context.packageManager} install`)}`);
|
|
18
17
|
console.log(` ${styles.highlight(`${context.packageManager} run dev`)}`);
|
|
19
18
|
console.log('');
|
|
20
19
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const VITEST_CONFIG = `import { defineConfig } from 'vitest/config';
|
|
4
|
+
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
test: {
|
|
7
|
+
globals: true,
|
|
8
|
+
environment: 'node',
|
|
9
|
+
},
|
|
10
|
+
});
|
|
11
|
+
`;
|
|
12
|
+
export async function setupVitest(projectRoot) {
|
|
13
|
+
const packageJsonPath = path.join(projectRoot, 'package.json');
|
|
14
|
+
// Leer package.json existente
|
|
15
|
+
const packageJson = await fs.readJson(packageJsonPath);
|
|
16
|
+
// Añadir devDependencies
|
|
17
|
+
packageJson.devDependencies = {
|
|
18
|
+
...packageJson.devDependencies,
|
|
19
|
+
vitest: '^3.0.0',
|
|
20
|
+
};
|
|
21
|
+
// Añadir scripts de test
|
|
22
|
+
packageJson.scripts = {
|
|
23
|
+
...packageJson.scripts,
|
|
24
|
+
test: 'vitest',
|
|
25
|
+
'test:run': 'vitest run',
|
|
26
|
+
};
|
|
27
|
+
// Escribir package.json actualizado
|
|
28
|
+
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
|
29
|
+
// Crear vitest.config.ts
|
|
30
|
+
await fs.writeFile(path.join(projectRoot, 'vitest.config.ts'), VITEST_CONFIG);
|
|
31
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devstarter-tool",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "CLI to generate projects with best practices (basic or monorepo)",
|
|
6
6
|
"author": "abraham-diaz",
|
|
@@ -22,7 +22,9 @@
|
|
|
22
22
|
"backend",
|
|
23
23
|
"boilerplate",
|
|
24
24
|
"starter",
|
|
25
|
-
"devstarter"
|
|
25
|
+
"devstarter",
|
|
26
|
+
"vitest",
|
|
27
|
+
"testing"
|
|
26
28
|
],
|
|
27
29
|
"engines": {
|
|
28
30
|
"node": ">=18.0.0"
|