devstarter-tool 0.2.3 → 0.3.0
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 +10 -1
- package/dist/commands/add/registry.js +11 -1
- package/dist/features/ci.js +158 -0
- package/dist/features/docker.js +97 -0
- package/dist/features/tailwind.js +72 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ CLI to generate projects with best practices and predefined configurations.
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
- **Project scaffolding**: Create frontend, backend, or monorepo projects in seconds
|
|
8
|
-
- **`add` command**: Add features like ESLint, Vitest, and
|
|
8
|
+
- **`add` command**: Add features like ESLint, Vitest, Prettier, CI, Docker, and Tailwind CSS to existing projects
|
|
9
9
|
- **Interactive prompts**: Guided project creation or flag-based configuration
|
|
10
10
|
- **Automatic dependency installation**: No need to run `npm install` manually
|
|
11
11
|
- **Package manager detection**: Automatically uses npm, pnpm, or yarn
|
|
@@ -39,6 +39,9 @@ devstarter init my-app --type frontend --vitest
|
|
|
39
39
|
# Add features to an existing project
|
|
40
40
|
devstarter add prettier
|
|
41
41
|
devstarter add eslint
|
|
42
|
+
devstarter add tailwind
|
|
43
|
+
devstarter add docker
|
|
44
|
+
devstarter add ci
|
|
42
45
|
|
|
43
46
|
# Preview without creating files
|
|
44
47
|
devstarter init my-app --dry-run
|
|
@@ -107,6 +110,9 @@ devstarter add [feature] [options]
|
|
|
107
110
|
| `eslint` | Linter for JavaScript and TypeScript | `eslint.config.js`, lint script, ESLint + typescript-eslint deps |
|
|
108
111
|
| `vitest` | Unit testing framework | `vitest.config.ts`, test scripts, Vitest dep |
|
|
109
112
|
| `prettier` | Code formatter | `.prettierrc`, format script, Prettier dep |
|
|
113
|
+
| `ci` | CI/CD pipeline | GitHub Actions workflow (`.github/workflows/ci.yml`) or GitLab CI (`.gitlab-ci.yml`) with lint, test, and build steps |
|
|
114
|
+
| `docker` | Docker containerization | `Dockerfile` (multi-stage build), `docker-compose.yml`, `.dockerignore`. Auto-detects frontend (nginx) vs backend (node) |
|
|
115
|
+
| `tailwind` | Tailwind CSS v4 | `tailwindcss` + `@tailwindcss/vite` deps, Vite plugin config, `@import "tailwindcss"` in CSS |
|
|
110
116
|
|
|
111
117
|
#### Examples
|
|
112
118
|
|
|
@@ -115,6 +121,9 @@ devstarter add [feature] [options]
|
|
|
115
121
|
devstarter add prettier
|
|
116
122
|
devstarter add eslint
|
|
117
123
|
devstarter add vitest
|
|
124
|
+
devstarter add tailwind
|
|
125
|
+
devstarter add docker
|
|
126
|
+
devstarter add ci
|
|
118
127
|
|
|
119
128
|
# Interactive mode - choose from available features
|
|
120
129
|
devstarter add
|
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import { eslintFeature } from '../../features/eslint.js';
|
|
2
2
|
import { vitestFeature } from '../../features/vitest.js';
|
|
3
3
|
import { prettierFeature } from '../../features/prettier.js';
|
|
4
|
-
|
|
4
|
+
import { ciFeature } from '../../features/ci.js';
|
|
5
|
+
import { dockerFeature } from '../../features/docker.js';
|
|
6
|
+
import { tailwindFeature } from '../../features/tailwind.js';
|
|
7
|
+
const features = [
|
|
8
|
+
eslintFeature,
|
|
9
|
+
vitestFeature,
|
|
10
|
+
prettierFeature,
|
|
11
|
+
ciFeature,
|
|
12
|
+
dockerFeature,
|
|
13
|
+
tailwindFeature,
|
|
14
|
+
];
|
|
5
15
|
const featureMap = new Map(features.map((f) => [f.id, f]));
|
|
6
16
|
export function getAvailableFeatureIds() {
|
|
7
17
|
return features.map((f) => f.id);
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import prompts from 'prompts';
|
|
4
|
+
async function detect(context) {
|
|
5
|
+
// Check for GitLab CI
|
|
6
|
+
if (await fs.pathExists(path.join(context.root, '.gitlab-ci.yml'))) {
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
// Check for GitHub Actions (directory must exist and contain at least one .yml)
|
|
10
|
+
const workflowsDir = path.join(context.root, '.github', 'workflows');
|
|
11
|
+
if (await fs.pathExists(workflowsDir)) {
|
|
12
|
+
const files = await fs.readdir(workflowsDir);
|
|
13
|
+
if (files.some((f) => f.endsWith('.yml') || f.endsWith('.yaml'))) {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
function getInstallCommand(context) {
|
|
20
|
+
switch (context.packageManager) {
|
|
21
|
+
case 'pnpm':
|
|
22
|
+
return 'pnpm install --frozen-lockfile';
|
|
23
|
+
case 'yarn':
|
|
24
|
+
return 'yarn install --frozen-lockfile';
|
|
25
|
+
default:
|
|
26
|
+
return 'npm ci';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function getRunCommand(context, script) {
|
|
30
|
+
switch (context.packageManager) {
|
|
31
|
+
case 'pnpm':
|
|
32
|
+
return `pnpm run ${script}`;
|
|
33
|
+
case 'yarn':
|
|
34
|
+
return `yarn ${script}`;
|
|
35
|
+
default:
|
|
36
|
+
return `npm run ${script}`;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function getScripts(packageJson) {
|
|
40
|
+
return packageJson.scripts ?? {};
|
|
41
|
+
}
|
|
42
|
+
function generateGitHubWorkflow(context) {
|
|
43
|
+
const scripts = getScripts(context.packageJson);
|
|
44
|
+
const installCmd = getInstallCommand(context);
|
|
45
|
+
const steps = [
|
|
46
|
+
' - uses: actions/checkout@v4',
|
|
47
|
+
' - uses: actions/setup-node@v4',
|
|
48
|
+
' with:',
|
|
49
|
+
' node-version: 20',
|
|
50
|
+
` - run: ${installCmd}`,
|
|
51
|
+
];
|
|
52
|
+
if (scripts.lint) {
|
|
53
|
+
steps.push(` - run: ${getRunCommand(context, 'lint')}`);
|
|
54
|
+
}
|
|
55
|
+
if (scripts.test) {
|
|
56
|
+
steps.push(` - run: ${getRunCommand(context, 'test')}`);
|
|
57
|
+
}
|
|
58
|
+
if (scripts.build) {
|
|
59
|
+
steps.push(` - run: ${getRunCommand(context, 'build')}`);
|
|
60
|
+
}
|
|
61
|
+
return `name: CI
|
|
62
|
+
on:
|
|
63
|
+
push:
|
|
64
|
+
branches: [main]
|
|
65
|
+
pull_request:
|
|
66
|
+
branches: [main]
|
|
67
|
+
jobs:
|
|
68
|
+
ci:
|
|
69
|
+
runs-on: ubuntu-latest
|
|
70
|
+
steps:
|
|
71
|
+
${steps.join('\n')}
|
|
72
|
+
`;
|
|
73
|
+
}
|
|
74
|
+
function generateGitLabCI(context) {
|
|
75
|
+
const scripts = getScripts(context.packageJson);
|
|
76
|
+
const installCmd = getInstallCommand(context);
|
|
77
|
+
let config = `stages:
|
|
78
|
+
- install
|
|
79
|
+
- lint
|
|
80
|
+
- test
|
|
81
|
+
- build
|
|
82
|
+
|
|
83
|
+
install:
|
|
84
|
+
stage: install
|
|
85
|
+
image: node:20-alpine
|
|
86
|
+
script:
|
|
87
|
+
- ${installCmd}
|
|
88
|
+
cache:
|
|
89
|
+
paths:
|
|
90
|
+
- node_modules/
|
|
91
|
+
`;
|
|
92
|
+
if (scripts.lint) {
|
|
93
|
+
config += `
|
|
94
|
+
lint:
|
|
95
|
+
stage: lint
|
|
96
|
+
image: node:20-alpine
|
|
97
|
+
script:
|
|
98
|
+
- ${getRunCommand(context, 'lint')}
|
|
99
|
+
cache:
|
|
100
|
+
paths:
|
|
101
|
+
- node_modules/
|
|
102
|
+
`;
|
|
103
|
+
}
|
|
104
|
+
if (scripts.test) {
|
|
105
|
+
config += `
|
|
106
|
+
test:
|
|
107
|
+
stage: test
|
|
108
|
+
image: node:20-alpine
|
|
109
|
+
script:
|
|
110
|
+
- ${getRunCommand(context, 'test')}
|
|
111
|
+
cache:
|
|
112
|
+
paths:
|
|
113
|
+
- node_modules/
|
|
114
|
+
`;
|
|
115
|
+
}
|
|
116
|
+
if (scripts.build) {
|
|
117
|
+
config += `
|
|
118
|
+
build:
|
|
119
|
+
stage: build
|
|
120
|
+
image: node:20-alpine
|
|
121
|
+
script:
|
|
122
|
+
- ${getRunCommand(context, 'build')}
|
|
123
|
+
cache:
|
|
124
|
+
paths:
|
|
125
|
+
- node_modules/
|
|
126
|
+
`;
|
|
127
|
+
}
|
|
128
|
+
return config;
|
|
129
|
+
}
|
|
130
|
+
async function apply(context) {
|
|
131
|
+
const { provider } = await prompts({
|
|
132
|
+
type: 'select',
|
|
133
|
+
name: 'provider',
|
|
134
|
+
message: 'Which CI provider?',
|
|
135
|
+
choices: [
|
|
136
|
+
{ title: 'GitHub Actions', value: 'github' },
|
|
137
|
+
{ title: 'GitLab CI', value: 'gitlab' },
|
|
138
|
+
],
|
|
139
|
+
});
|
|
140
|
+
if (!provider) {
|
|
141
|
+
throw new Error('CI provider selection cancelled');
|
|
142
|
+
}
|
|
143
|
+
if (provider === 'github') {
|
|
144
|
+
const workflowDir = path.join(context.root, '.github', 'workflows');
|
|
145
|
+
await fs.ensureDir(workflowDir);
|
|
146
|
+
await fs.writeFile(path.join(workflowDir, 'ci.yml'), generateGitHubWorkflow(context));
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
await fs.writeFile(path.join(context.root, '.gitlab-ci.yml'), generateGitLabCI(context));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
export const ciFeature = {
|
|
153
|
+
id: 'ci',
|
|
154
|
+
name: 'CI',
|
|
155
|
+
description: 'Continuous integration with GitHub Actions or GitLab CI',
|
|
156
|
+
detect,
|
|
157
|
+
apply,
|
|
158
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
async function detect(context) {
|
|
4
|
+
const configFiles = [
|
|
5
|
+
'Dockerfile',
|
|
6
|
+
'docker-compose.yml',
|
|
7
|
+
'docker-compose.yaml',
|
|
8
|
+
'compose.yml',
|
|
9
|
+
'compose.yaml',
|
|
10
|
+
];
|
|
11
|
+
for (const file of configFiles) {
|
|
12
|
+
if (await fs.pathExists(path.join(context.root, file))) {
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
function isFrontend(packageJson) {
|
|
19
|
+
const deps = {
|
|
20
|
+
...(packageJson.dependencies ?? {}),
|
|
21
|
+
...(packageJson.devDependencies ?? {}),
|
|
22
|
+
};
|
|
23
|
+
return 'vite' in deps;
|
|
24
|
+
}
|
|
25
|
+
function getInstallCommand(context) {
|
|
26
|
+
switch (context.packageManager) {
|
|
27
|
+
case 'pnpm':
|
|
28
|
+
return 'pnpm install --frozen-lockfile';
|
|
29
|
+
case 'yarn':
|
|
30
|
+
return 'yarn install --frozen-lockfile';
|
|
31
|
+
default:
|
|
32
|
+
return 'npm ci';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function generateDockerfile(context) {
|
|
36
|
+
const installCmd = getInstallCommand(context);
|
|
37
|
+
const frontend = isFrontend(context.packageJson);
|
|
38
|
+
if (frontend) {
|
|
39
|
+
return `FROM node:20-alpine AS build
|
|
40
|
+
WORKDIR /app
|
|
41
|
+
COPY package*.json ./
|
|
42
|
+
RUN ${installCmd}
|
|
43
|
+
COPY . .
|
|
44
|
+
RUN npm run build
|
|
45
|
+
|
|
46
|
+
FROM nginx:alpine
|
|
47
|
+
COPY --from=build /app/dist /usr/share/nginx/html
|
|
48
|
+
EXPOSE 80
|
|
49
|
+
CMD ["nginx", "-g", "daemon off;"]
|
|
50
|
+
`;
|
|
51
|
+
}
|
|
52
|
+
return `FROM node:20-alpine AS build
|
|
53
|
+
WORKDIR /app
|
|
54
|
+
COPY package*.json ./
|
|
55
|
+
RUN ${installCmd}
|
|
56
|
+
COPY . .
|
|
57
|
+
RUN npm run build
|
|
58
|
+
|
|
59
|
+
FROM node:20-alpine
|
|
60
|
+
WORKDIR /app
|
|
61
|
+
COPY --from=build /app/dist ./dist
|
|
62
|
+
COPY --from=build /app/package*.json ./
|
|
63
|
+
RUN ${installCmd} --omit=dev
|
|
64
|
+
EXPOSE 3000
|
|
65
|
+
CMD ["node", "dist/index.js"]
|
|
66
|
+
`;
|
|
67
|
+
}
|
|
68
|
+
function generateDockerCompose(context) {
|
|
69
|
+
const frontend = isFrontend(context.packageJson);
|
|
70
|
+
const port = frontend ? '8080:80' : '3000:3000';
|
|
71
|
+
return `services:
|
|
72
|
+
app:
|
|
73
|
+
build: .
|
|
74
|
+
ports:
|
|
75
|
+
- "${port}"
|
|
76
|
+
environment:
|
|
77
|
+
- NODE_ENV=production
|
|
78
|
+
`;
|
|
79
|
+
}
|
|
80
|
+
const DOCKERIGNORE = `node_modules
|
|
81
|
+
dist
|
|
82
|
+
.git
|
|
83
|
+
*.log
|
|
84
|
+
.env
|
|
85
|
+
`;
|
|
86
|
+
async function apply(context) {
|
|
87
|
+
await fs.writeFile(path.join(context.root, 'Dockerfile'), generateDockerfile(context));
|
|
88
|
+
await fs.writeFile(path.join(context.root, 'docker-compose.yml'), generateDockerCompose(context));
|
|
89
|
+
await fs.writeFile(path.join(context.root, '.dockerignore'), DOCKERIGNORE);
|
|
90
|
+
}
|
|
91
|
+
export const dockerFeature = {
|
|
92
|
+
id: 'docker',
|
|
93
|
+
name: 'Docker',
|
|
94
|
+
description: 'Dockerfile, docker-compose, and .dockerignore',
|
|
95
|
+
detect,
|
|
96
|
+
apply,
|
|
97
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
async function getWebAppDir(root) {
|
|
4
|
+
const webDir = path.join(root, 'apps', 'web');
|
|
5
|
+
if (await fs.pathExists(path.join(webDir, 'package.json'))) {
|
|
6
|
+
return webDir;
|
|
7
|
+
}
|
|
8
|
+
return root;
|
|
9
|
+
}
|
|
10
|
+
async function detect(context) {
|
|
11
|
+
const targetDir = await getWebAppDir(context.root);
|
|
12
|
+
const configFiles = [
|
|
13
|
+
'tailwind.config.js',
|
|
14
|
+
'tailwind.config.ts',
|
|
15
|
+
'tailwind.config.mjs',
|
|
16
|
+
'tailwind.config.cjs',
|
|
17
|
+
];
|
|
18
|
+
for (const file of configFiles) {
|
|
19
|
+
if (await fs.pathExists(path.join(targetDir, file))) {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// Check deps in the target package.json
|
|
24
|
+
const pkgJson = targetDir === context.root
|
|
25
|
+
? context.packageJson
|
|
26
|
+
: await fs.readJson(path.join(targetDir, 'package.json'));
|
|
27
|
+
const deps = {
|
|
28
|
+
...(pkgJson.dependencies ?? {}),
|
|
29
|
+
...(pkgJson.devDependencies ?? {}),
|
|
30
|
+
};
|
|
31
|
+
return 'tailwindcss' in deps;
|
|
32
|
+
}
|
|
33
|
+
async function apply(context) {
|
|
34
|
+
const targetDir = await getWebAppDir(context.root);
|
|
35
|
+
const packageJsonPath = path.join(targetDir, 'package.json');
|
|
36
|
+
const packageJson = await fs.readJson(packageJsonPath);
|
|
37
|
+
packageJson.devDependencies = {
|
|
38
|
+
...packageJson.devDependencies,
|
|
39
|
+
tailwindcss: '^4.0.0',
|
|
40
|
+
'@tailwindcss/vite': '^4.0.0',
|
|
41
|
+
};
|
|
42
|
+
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
|
|
43
|
+
// Update vite.config.ts to add Tailwind plugin if it exists
|
|
44
|
+
const viteConfigPath = path.join(targetDir, 'vite.config.ts');
|
|
45
|
+
if (await fs.pathExists(viteConfigPath)) {
|
|
46
|
+
const viteConfig = await fs.readFile(viteConfigPath, 'utf-8');
|
|
47
|
+
if (!viteConfig.includes('@tailwindcss/vite')) {
|
|
48
|
+
const updated = `import tailwindcss from '@tailwindcss/vite';\n` +
|
|
49
|
+
viteConfig.replace(/plugins:\s*\[/, 'plugins: [\n tailwindcss(),');
|
|
50
|
+
await fs.writeFile(viteConfigPath, updated);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Create/update src/index.css with Tailwind import
|
|
54
|
+
const cssPath = path.join(targetDir, 'src', 'index.css');
|
|
55
|
+
await fs.ensureDir(path.join(targetDir, 'src'));
|
|
56
|
+
if (await fs.pathExists(cssPath)) {
|
|
57
|
+
const existing = await fs.readFile(cssPath, 'utf-8');
|
|
58
|
+
if (!existing.includes('@import "tailwindcss"')) {
|
|
59
|
+
await fs.writeFile(cssPath, `@import "tailwindcss";\n\n${existing}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
await fs.writeFile(cssPath, '@import "tailwindcss";\n');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export const tailwindFeature = {
|
|
67
|
+
id: 'tailwind',
|
|
68
|
+
name: 'Tailwind CSS',
|
|
69
|
+
description: 'Utility-first CSS framework',
|
|
70
|
+
detect,
|
|
71
|
+
apply,
|
|
72
|
+
};
|