scaffly-cli 1.0.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 +150 -0
- package/bin/scaffly.js +162 -0
- package/package.json +34 -0
- package/src/extras/docker.js +185 -0
- package/src/extras/eslint.js +145 -0
- package/src/extras/github-actions.js +71 -0
- package/src/extras/husky.js +50 -0
- package/src/extras/tailwind.js +97 -0
- package/src/generators/express.js +165 -0
- package/src/generators/fastify.js +160 -0
- package/src/generators/nextjs.js +153 -0
- package/src/generators/vite.js +174 -0
- package/src/prompts/index.js +131 -0
- package/src/utils/index.js +83 -0
package/README.md
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# Scaffly
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/scaffly)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://github.com/ViniLF/scaffly/stargazers)
|
|
6
|
+
[](https://nodejs.org)
|
|
7
|
+
|
|
8
|
+
> A fast, interactive CLI tool for scaffolding modern web and Node.js projects.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Features
|
|
13
|
+
|
|
14
|
+
- **Interactive prompts** — beautiful terminal UI powered by `@clack/prompts`
|
|
15
|
+
- **4 supported stacks** — Next.js, React + Vite, Express, and Fastify
|
|
16
|
+
- **Optional extras** — pick and choose: ESLint, Prettier, Husky, Tailwind CSS, Docker, GitHub Actions
|
|
17
|
+
- **Production-ready boilerplate** — sensible defaults, clean folder structure
|
|
18
|
+
- **Zero config required** — just run and go
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
Run without installing (always uses the latest version):
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npx scaffly
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Or install globally for repeated use:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install -g scaffly
|
|
34
|
+
scaffly
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### What happens next
|
|
38
|
+
|
|
39
|
+
1. Enter your **project name**
|
|
40
|
+
2. Choose a **stack**
|
|
41
|
+
3. Pick any **extras**
|
|
42
|
+
4. Scaffly creates the project, installs dependencies, and configures your tooling
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Supported Stacks
|
|
47
|
+
|
|
48
|
+
| Stack | Language | Best For |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| **Next.js** | React / JSX | Full-stack web applications, SSR, SSG |
|
|
51
|
+
| **React + Vite** | React / JSX | SPAs, client-side frontends |
|
|
52
|
+
| **Node.js + Express** | JavaScript (ESM) | REST APIs, microservices |
|
|
53
|
+
| **Fastify** | JavaScript (ESM) | High-performance APIs |
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Extras
|
|
58
|
+
|
|
59
|
+
| Extra | Description | Stacks |
|
|
60
|
+
|---|---|---|
|
|
61
|
+
| **ESLint + Prettier** | Linting and code formatting with zero-config defaults | All |
|
|
62
|
+
| **Husky + lint-staged** | Pre-commit hooks that run linting before each commit | All |
|
|
63
|
+
| **Tailwind CSS** | Utility-first CSS framework with PostCSS | Next.js, Vite |
|
|
64
|
+
| **Docker** | `Dockerfile` + `docker-compose.yml` optimised per stack | All |
|
|
65
|
+
| **GitHub Actions CI/CD** | Workflow that runs lint, test, and build on push/PR | All |
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Generated Project Structure
|
|
70
|
+
|
|
71
|
+
### Next.js
|
|
72
|
+
```
|
|
73
|
+
my-app/
|
|
74
|
+
├── app/
|
|
75
|
+
│ ├── layout.js
|
|
76
|
+
│ ├── page.js
|
|
77
|
+
│ └── globals.css
|
|
78
|
+
├── next.config.mjs
|
|
79
|
+
├── package.json
|
|
80
|
+
└── .gitignore
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### React + Vite
|
|
84
|
+
```
|
|
85
|
+
my-app/
|
|
86
|
+
├── src/
|
|
87
|
+
│ ├── App.jsx
|
|
88
|
+
│ ├── App.css
|
|
89
|
+
│ ├── main.jsx
|
|
90
|
+
│ └── index.css
|
|
91
|
+
├── index.html
|
|
92
|
+
├── vite.config.js
|
|
93
|
+
├── package.json
|
|
94
|
+
└── .gitignore
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Express / Fastify
|
|
98
|
+
```
|
|
99
|
+
my-app/
|
|
100
|
+
├── src/
|
|
101
|
+
│ ├── index.js
|
|
102
|
+
│ ├── routes/
|
|
103
|
+
│ └── middleware/ (Express only)
|
|
104
|
+
├── .env
|
|
105
|
+
├── .env.example
|
|
106
|
+
├── package.json
|
|
107
|
+
└── .gitignore
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Requirements
|
|
113
|
+
|
|
114
|
+
- **Node.js** `>= 18.0.0`
|
|
115
|
+
- **npm** `>= 8.0.0`
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Contributing
|
|
120
|
+
|
|
121
|
+
Contributions are welcome! Here's how to get started:
|
|
122
|
+
|
|
123
|
+
1. **Fork** the repository
|
|
124
|
+
2. **Clone** your fork: `git clone https://github.com/ViniLF/scaffly.git`
|
|
125
|
+
3. **Install** dependencies: `npm install`
|
|
126
|
+
4. **Create** a feature branch: `git checkout -b feat/your-feature`
|
|
127
|
+
5. **Make** your changes and test them: `node bin/scaffly.js`
|
|
128
|
+
6. **Commit** using [Conventional Commits](https://www.conventionalcommits.org/): `git commit -m "feat: add awesome feature"`
|
|
129
|
+
7. **Push** to your branch: `git push origin feat/your-feature`
|
|
130
|
+
8. **Open** a Pull Request
|
|
131
|
+
|
|
132
|
+
### Adding a new stack
|
|
133
|
+
|
|
134
|
+
1. Create `src/generators/your-stack.js` and export a `generate(projectName, projectPath)` function
|
|
135
|
+
2. Register it in `bin/scaffly.js` under `GENERATORS`
|
|
136
|
+
3. Add a prompt option in `src/prompts/index.js`
|
|
137
|
+
4. Update this README
|
|
138
|
+
|
|
139
|
+
### Adding a new extra
|
|
140
|
+
|
|
141
|
+
1. Create `src/extras/your-extra.js` and export an `apply(projectPath, stack)` function
|
|
142
|
+
2. Register it in `bin/scaffly.js` under `EXTRAS`
|
|
143
|
+
3. Add a prompt option in `src/prompts/index.js`
|
|
144
|
+
4. Update this README
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## License
|
|
149
|
+
|
|
150
|
+
[MIT](./LICENSE) © ViniLF
|
package/bin/scaffly.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Scaffly — CLI entry point
|
|
5
|
+
*
|
|
6
|
+
* Flow:
|
|
7
|
+
* 1. Display intro banner
|
|
8
|
+
* 2. Collect project configuration via interactive prompts
|
|
9
|
+
* 3. Validate the target directory doesn't already exist
|
|
10
|
+
* 4. Run the selected stack generator (creates files + installs base deps)
|
|
11
|
+
* 5. Apply each selected extra (adds tooling on top)
|
|
12
|
+
* 6. Display a success message with next steps
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import * as p from '@clack/prompts';
|
|
16
|
+
import chalk from 'chalk';
|
|
17
|
+
import path from 'path';
|
|
18
|
+
import fse from 'fs-extra';
|
|
19
|
+
|
|
20
|
+
import { collectAnswers } from '../src/prompts/index.js';
|
|
21
|
+
import { generate as generateNextjs } from '../src/generators/nextjs.js';
|
|
22
|
+
import { generate as generateVite } from '../src/generators/vite.js';
|
|
23
|
+
import { generate as generateExpress } from '../src/generators/express.js';
|
|
24
|
+
import { generate as generateFastify } from '../src/generators/fastify.js';
|
|
25
|
+
import { apply as applyEslint } from '../src/extras/eslint.js';
|
|
26
|
+
import { apply as applyHusky } from '../src/extras/husky.js';
|
|
27
|
+
import { apply as applyTailwind } from '../src/extras/tailwind.js';
|
|
28
|
+
import { apply as applyDocker } from '../src/extras/docker.js';
|
|
29
|
+
import { apply as applyGithubActions } from '../src/extras/github-actions.js';
|
|
30
|
+
|
|
31
|
+
// ── Registry maps ─────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
const GENERATORS = {
|
|
34
|
+
nextjs: generateNextjs,
|
|
35
|
+
vite: generateVite,
|
|
36
|
+
express: generateExpress,
|
|
37
|
+
fastify: generateFastify,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const EXTRAS = {
|
|
41
|
+
eslint: applyEslint,
|
|
42
|
+
husky: applyHusky,
|
|
43
|
+
tailwind: applyTailwind,
|
|
44
|
+
docker: applyDocker,
|
|
45
|
+
'github-actions': applyGithubActions,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const STACK_LABELS = {
|
|
49
|
+
nextjs: 'Next.js',
|
|
50
|
+
vite: 'React + Vite',
|
|
51
|
+
express: 'Node.js + Express',
|
|
52
|
+
fastify: 'Fastify',
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const DEV_COMMANDS = {
|
|
56
|
+
nextjs: 'npm run dev',
|
|
57
|
+
vite: 'npm run dev',
|
|
58
|
+
express: 'npm run dev',
|
|
59
|
+
fastify: 'npm run dev',
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
async function main() {
|
|
65
|
+
console.clear();
|
|
66
|
+
|
|
67
|
+
// Welcome banner
|
|
68
|
+
p.intro(
|
|
69
|
+
`${chalk.bgCyan.black(' scaffly ')} ${chalk.dim('— scaffold modern projects in seconds')}`
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
// ── Step 1: Collect answers ───────────────────────────────────────────────
|
|
73
|
+
const { projectName, stack, extras } = await collectAnswers();
|
|
74
|
+
|
|
75
|
+
const projectPath = path.resolve(process.cwd(), projectName);
|
|
76
|
+
|
|
77
|
+
// ── Step 2: Guard against existing directory ──────────────────────────────
|
|
78
|
+
if (await fse.pathExists(projectPath)) {
|
|
79
|
+
p.cancel(
|
|
80
|
+
`Directory ${chalk.cyan(projectName)} already exists. Choose a different project name.`
|
|
81
|
+
);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const s = p.spinner();
|
|
86
|
+
|
|
87
|
+
// ── Step 3: Generate base project ────────────────────────────────────────
|
|
88
|
+
s.start(`Scaffolding ${chalk.cyan(STACK_LABELS[stack])} project...`);
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
await GENERATORS[stack](projectName, projectPath);
|
|
92
|
+
} catch (err) {
|
|
93
|
+
s.stop(chalk.red('Failed to scaffold project'));
|
|
94
|
+
p.cancel(`Generator error: ${err.message}`);
|
|
95
|
+
process.exit(1);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ── Step 4: Apply extras ──────────────────────────────────────────────────
|
|
99
|
+
for (const extra of extras) {
|
|
100
|
+
s.message(`Applying ${chalk.cyan(extra)}...`);
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
await EXTRAS[extra](projectPath, stack);
|
|
104
|
+
} catch (err) {
|
|
105
|
+
// Extras are non-fatal — warn and continue
|
|
106
|
+
s.message(chalk.yellow(`Warning: ${extra} could not be fully applied`));
|
|
107
|
+
p.log.warn(`${extra} failed: ${err.message}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
s.stop(chalk.green('Done!'));
|
|
112
|
+
|
|
113
|
+
// ── Step 5: Show next steps ───────────────────────────────────────────────
|
|
114
|
+
const note = buildNextSteps(projectName, stack, extras);
|
|
115
|
+
p.note(note, chalk.bold('Next steps'));
|
|
116
|
+
|
|
117
|
+
p.outro(
|
|
118
|
+
`Built something cool? Give Scaffly a star on GitHub! ${chalk.dim('github.com/ViniLF/scaffly')}`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Builds the formatted "next steps" message shown after a successful scaffold.
|
|
124
|
+
*
|
|
125
|
+
* @param {string} projectName
|
|
126
|
+
* @param {string} stack
|
|
127
|
+
* @param {string[]} extras
|
|
128
|
+
* @returns {string}
|
|
129
|
+
*/
|
|
130
|
+
function buildNextSteps(projectName, stack, extras) {
|
|
131
|
+
const steps = [
|
|
132
|
+
`${chalk.dim('1.')} cd ${chalk.cyan(projectName)}`,
|
|
133
|
+
`${chalk.dim('2.')} ${chalk.cyan(DEV_COMMANDS[stack])}`,
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
if (stack === 'nextjs' || stack === 'vite') {
|
|
137
|
+
steps.push(`${chalk.dim('3.')} Open ${chalk.cyan('http://localhost:3000')}`);
|
|
138
|
+
} else {
|
|
139
|
+
steps.push(`${chalk.dim('3.')} Test: ${chalk.cyan('curl http://localhost:3000/api/health')}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (extras.includes('husky')) {
|
|
143
|
+
steps.push('');
|
|
144
|
+
steps.push(
|
|
145
|
+
`${chalk.dim('Tip:')} Run ${chalk.cyan('git init && npm run prepare')} to activate Husky hooks`
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (extras.includes('docker')) {
|
|
150
|
+
steps.push('');
|
|
151
|
+
steps.push(`${chalk.dim('Docker:')} ${chalk.cyan('docker compose up --build')}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return steps.join('\n');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ── Run ───────────────────────────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
main().catch((err) => {
|
|
160
|
+
console.error(chalk.red('\nUnexpected error:'), err);
|
|
161
|
+
process.exit(1);
|
|
162
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "scaffly-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A fast, interactive CLI tool for scaffolding modern web and Node.js projects",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"scaffly": "./bin/scaffly.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node bin/scaffly.js",
|
|
11
|
+
"test": "echo \"No tests yet\" && exit 0"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"cli",
|
|
15
|
+
"scaffold",
|
|
16
|
+
"boilerplate",
|
|
17
|
+
"nextjs",
|
|
18
|
+
"react",
|
|
19
|
+
"vite",
|
|
20
|
+
"express",
|
|
21
|
+
"fastify"
|
|
22
|
+
],
|
|
23
|
+
"author": "ViniLF",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@clack/prompts": "^0.7.0",
|
|
27
|
+
"chalk": "^5.3.0",
|
|
28
|
+
"execa": "^9.3.0",
|
|
29
|
+
"fs-extra": "^11.2.0"
|
|
30
|
+
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=18.0.0"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scaffly — Docker extra
|
|
3
|
+
* Adds a Dockerfile, docker-compose.yml, and .dockerignore tailored to the stack.
|
|
4
|
+
*
|
|
5
|
+
* Strategy:
|
|
6
|
+
* - Next.js / Vite: multi-stage build (build → serve)
|
|
7
|
+
* - Express / Fastify: single-stage Node.js image (production deps only)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import { writeFile } from '../utils/index.js';
|
|
12
|
+
|
|
13
|
+
// ── Dockerfiles per stack ─────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
const DOCKERFILES = {
|
|
16
|
+
nextjs: `# ── Stage 1: Install dependencies ────────────────────────────────────────
|
|
17
|
+
FROM node:20-alpine AS deps
|
|
18
|
+
WORKDIR /app
|
|
19
|
+
COPY package*.json ./
|
|
20
|
+
RUN npm ci
|
|
21
|
+
|
|
22
|
+
# ── Stage 2: Build the application ────────────────────────────────────────
|
|
23
|
+
FROM node:20-alpine AS builder
|
|
24
|
+
WORKDIR /app
|
|
25
|
+
COPY --from=deps /app/node_modules ./node_modules
|
|
26
|
+
COPY . .
|
|
27
|
+
RUN npm run build
|
|
28
|
+
|
|
29
|
+
# ── Stage 3: Production image ──────────────────────────────────────────────
|
|
30
|
+
FROM node:20-alpine AS runner
|
|
31
|
+
WORKDIR /app
|
|
32
|
+
|
|
33
|
+
ENV NODE_ENV=production
|
|
34
|
+
|
|
35
|
+
# Copy only what's needed to run the app
|
|
36
|
+
COPY --from=builder /app/public ./public
|
|
37
|
+
COPY --from=builder /app/.next/standalone ./
|
|
38
|
+
COPY --from=builder /app/.next/static ./.next/static
|
|
39
|
+
|
|
40
|
+
EXPOSE 3000
|
|
41
|
+
ENV PORT=3000
|
|
42
|
+
|
|
43
|
+
CMD ["node", "server.js"]
|
|
44
|
+
`,
|
|
45
|
+
|
|
46
|
+
vite: `# ── Stage 1: Build the app ────────────────────────────────────────────────
|
|
47
|
+
FROM node:20-alpine AS builder
|
|
48
|
+
WORKDIR /app
|
|
49
|
+
COPY package*.json ./
|
|
50
|
+
RUN npm ci
|
|
51
|
+
COPY . .
|
|
52
|
+
RUN npm run build
|
|
53
|
+
|
|
54
|
+
# ── Stage 2: Serve with Nginx ──────────────────────────────────────────────
|
|
55
|
+
FROM nginx:alpine AS runner
|
|
56
|
+
COPY --from=builder /app/dist /usr/share/nginx/html
|
|
57
|
+
|
|
58
|
+
# Optional: custom Nginx config for SPA routing
|
|
59
|
+
# COPY nginx.conf /etc/nginx/conf.d/default.conf
|
|
60
|
+
|
|
61
|
+
EXPOSE 80
|
|
62
|
+
|
|
63
|
+
CMD ["nginx", "-g", "daemon off;"]
|
|
64
|
+
`,
|
|
65
|
+
|
|
66
|
+
express: `FROM node:20-alpine
|
|
67
|
+
WORKDIR /app
|
|
68
|
+
|
|
69
|
+
# Install production dependencies only
|
|
70
|
+
COPY package*.json ./
|
|
71
|
+
RUN npm ci --omit=dev
|
|
72
|
+
|
|
73
|
+
# Copy application source
|
|
74
|
+
COPY . .
|
|
75
|
+
|
|
76
|
+
EXPOSE 3000
|
|
77
|
+
ENV NODE_ENV=production
|
|
78
|
+
|
|
79
|
+
CMD ["npm", "start"]
|
|
80
|
+
`,
|
|
81
|
+
|
|
82
|
+
fastify: `FROM node:20-alpine
|
|
83
|
+
WORKDIR /app
|
|
84
|
+
|
|
85
|
+
# Install production dependencies only
|
|
86
|
+
COPY package*.json ./
|
|
87
|
+
RUN npm ci --omit=dev
|
|
88
|
+
|
|
89
|
+
# Copy application source
|
|
90
|
+
COPY . .
|
|
91
|
+
|
|
92
|
+
EXPOSE 3000
|
|
93
|
+
ENV NODE_ENV=production
|
|
94
|
+
|
|
95
|
+
CMD ["npm", "start"]
|
|
96
|
+
`,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
// ── docker-compose.yml per stack ─────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
const COMPOSE_FILES = {
|
|
102
|
+
nextjs: `services:
|
|
103
|
+
app:
|
|
104
|
+
build: .
|
|
105
|
+
ports:
|
|
106
|
+
- "3000:3000"
|
|
107
|
+
environment:
|
|
108
|
+
- NODE_ENV=production
|
|
109
|
+
restart: unless-stopped
|
|
110
|
+
`,
|
|
111
|
+
|
|
112
|
+
vite: `services:
|
|
113
|
+
app:
|
|
114
|
+
build: .
|
|
115
|
+
ports:
|
|
116
|
+
- "80:80"
|
|
117
|
+
restart: unless-stopped
|
|
118
|
+
`,
|
|
119
|
+
|
|
120
|
+
express: `services:
|
|
121
|
+
app:
|
|
122
|
+
build: .
|
|
123
|
+
ports:
|
|
124
|
+
- "3000:3000"
|
|
125
|
+
environment:
|
|
126
|
+
- NODE_ENV=production
|
|
127
|
+
- PORT=3000
|
|
128
|
+
restart: unless-stopped
|
|
129
|
+
`,
|
|
130
|
+
|
|
131
|
+
fastify: `services:
|
|
132
|
+
app:
|
|
133
|
+
build: .
|
|
134
|
+
ports:
|
|
135
|
+
- "3000:3000"
|
|
136
|
+
environment:
|
|
137
|
+
- NODE_ENV=production
|
|
138
|
+
- PORT=3000
|
|
139
|
+
restart: unless-stopped
|
|
140
|
+
`,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// ── .dockerignore (shared across stacks) ──────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
const DOCKERIGNORE = `# Source control
|
|
146
|
+
.git
|
|
147
|
+
.gitignore
|
|
148
|
+
|
|
149
|
+
# Dependencies (will be re-installed inside the container)
|
|
150
|
+
node_modules
|
|
151
|
+
|
|
152
|
+
# Build output (re-built inside the container)
|
|
153
|
+
dist
|
|
154
|
+
build
|
|
155
|
+
.next
|
|
156
|
+
|
|
157
|
+
# Environment files — never bake secrets into an image
|
|
158
|
+
.env
|
|
159
|
+
.env.local
|
|
160
|
+
.env.*.local
|
|
161
|
+
|
|
162
|
+
# Dev tooling
|
|
163
|
+
.eslintrc*
|
|
164
|
+
.prettierrc*
|
|
165
|
+
.husky
|
|
166
|
+
|
|
167
|
+
# OS artifacts
|
|
168
|
+
.DS_Store
|
|
169
|
+
Thumbs.db
|
|
170
|
+
|
|
171
|
+
# Docs
|
|
172
|
+
README.md
|
|
173
|
+
`;
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Adds Docker support files to the project.
|
|
177
|
+
*
|
|
178
|
+
* @param {string} projectPath - Absolute path to the project root
|
|
179
|
+
* @param {string} stack - Selected stack identifier
|
|
180
|
+
*/
|
|
181
|
+
export async function apply(projectPath, stack) {
|
|
182
|
+
await writeFile(path.join(projectPath, 'Dockerfile'), DOCKERFILES[stack]);
|
|
183
|
+
await writeFile(path.join(projectPath, 'docker-compose.yml'), COMPOSE_FILES[stack]);
|
|
184
|
+
await writeFile(path.join(projectPath, '.dockerignore'), DOCKERIGNORE);
|
|
185
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scaffly — ESLint + Prettier extra
|
|
3
|
+
* Adds code linting (ESLint) and formatting (Prettier) to the generated project.
|
|
4
|
+
* Config is tailored to the selected stack.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { writeFile, writeJson, updatePackageJson, runCommand } from '../utils/index.js';
|
|
9
|
+
|
|
10
|
+
// ── ESLint configs per stack ──────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
const ESLINT_CONFIGS = {
|
|
13
|
+
nextjs: {
|
|
14
|
+
env: { browser: true, es2021: true, node: true },
|
|
15
|
+
extends: ['next/core-web-vitals', 'plugin:prettier/recommended'],
|
|
16
|
+
rules: {},
|
|
17
|
+
},
|
|
18
|
+
vite: {
|
|
19
|
+
env: { browser: true, es2021: true },
|
|
20
|
+
extends: [
|
|
21
|
+
'eslint:recommended',
|
|
22
|
+
'plugin:react/recommended',
|
|
23
|
+
'plugin:react-hooks/recommended',
|
|
24
|
+
'plugin:prettier/recommended',
|
|
25
|
+
],
|
|
26
|
+
settings: {
|
|
27
|
+
react: { version: 'detect' },
|
|
28
|
+
},
|
|
29
|
+
rules: {
|
|
30
|
+
'react/react-in-jsx-scope': 'off', // Not needed in React 17+
|
|
31
|
+
'react/prop-types': 'off',
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
express: {
|
|
35
|
+
env: { node: true, es2021: true },
|
|
36
|
+
extends: ['eslint:recommended', 'plugin:prettier/recommended'],
|
|
37
|
+
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
|
|
38
|
+
rules: {},
|
|
39
|
+
},
|
|
40
|
+
fastify: {
|
|
41
|
+
env: { node: true, es2021: true },
|
|
42
|
+
extends: ['eslint:recommended', 'plugin:prettier/recommended'],
|
|
43
|
+
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
|
|
44
|
+
rules: {},
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// ── Packages to install per stack ─────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
const PACKAGES = {
|
|
51
|
+
nextjs: [
|
|
52
|
+
'eslint',
|
|
53
|
+
'eslint-config-next',
|
|
54
|
+
'prettier',
|
|
55
|
+
'eslint-config-prettier',
|
|
56
|
+
'eslint-plugin-prettier',
|
|
57
|
+
],
|
|
58
|
+
vite: [
|
|
59
|
+
'eslint',
|
|
60
|
+
'prettier',
|
|
61
|
+
'eslint-config-prettier',
|
|
62
|
+
'eslint-plugin-prettier',
|
|
63
|
+
'eslint-plugin-react',
|
|
64
|
+
'eslint-plugin-react-hooks',
|
|
65
|
+
],
|
|
66
|
+
express: ['eslint', 'prettier', 'eslint-config-prettier', 'eslint-plugin-prettier'],
|
|
67
|
+
fastify: ['eslint', 'prettier', 'eslint-config-prettier', 'eslint-plugin-prettier'],
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// ── Lint scripts per stack ────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
const LINT_SCRIPTS = {
|
|
73
|
+
nextjs: {
|
|
74
|
+
lint: 'next lint',
|
|
75
|
+
'lint:fix': 'next lint --fix',
|
|
76
|
+
},
|
|
77
|
+
vite: {
|
|
78
|
+
lint: 'eslint . --ext .js,.jsx --max-warnings 0',
|
|
79
|
+
'lint:fix': 'eslint . --ext .js,.jsx --fix',
|
|
80
|
+
},
|
|
81
|
+
express: {
|
|
82
|
+
lint: 'eslint . --ext .js --max-warnings 0',
|
|
83
|
+
'lint:fix': 'eslint . --ext .js --fix',
|
|
84
|
+
},
|
|
85
|
+
fastify: {
|
|
86
|
+
lint: 'eslint . --ext .js --max-warnings 0',
|
|
87
|
+
'lint:fix': 'eslint . --ext .js --fix',
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Applies ESLint + Prettier configuration to the project.
|
|
93
|
+
*
|
|
94
|
+
* @param {string} projectPath - Absolute path to the project root
|
|
95
|
+
* @param {string} stack - Selected stack identifier
|
|
96
|
+
*/
|
|
97
|
+
export async function apply(projectPath, stack) {
|
|
98
|
+
// Write .eslintrc.json
|
|
99
|
+
await writeJson(path.join(projectPath, '.eslintrc.json'), ESLINT_CONFIGS[stack]);
|
|
100
|
+
|
|
101
|
+
// Write .prettierrc
|
|
102
|
+
await writeJson(path.join(projectPath, '.prettierrc'), {
|
|
103
|
+
semi: true,
|
|
104
|
+
singleQuote: true,
|
|
105
|
+
tabWidth: 2,
|
|
106
|
+
trailingComma: 'es5',
|
|
107
|
+
printWidth: 80,
|
|
108
|
+
arrowParens: 'always',
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Write .eslintignore
|
|
112
|
+
await writeFile(
|
|
113
|
+
path.join(projectPath, '.eslintignore'),
|
|
114
|
+
`node_modules
|
|
115
|
+
dist
|
|
116
|
+
build
|
|
117
|
+
.next
|
|
118
|
+
out
|
|
119
|
+
`
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
// Write .prettierignore
|
|
123
|
+
await writeFile(
|
|
124
|
+
path.join(projectPath, '.prettierignore'),
|
|
125
|
+
`node_modules
|
|
126
|
+
dist
|
|
127
|
+
build
|
|
128
|
+
.next
|
|
129
|
+
out
|
|
130
|
+
package-lock.json
|
|
131
|
+
`
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
// Add lint and format scripts to package.json
|
|
135
|
+
await updatePackageJson(projectPath, {
|
|
136
|
+
scripts: {
|
|
137
|
+
...LINT_SCRIPTS[stack],
|
|
138
|
+
format: 'prettier --write .',
|
|
139
|
+
'format:check': 'prettier --check .',
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Install ESLint + Prettier packages as dev dependencies
|
|
144
|
+
await runCommand('npm', ['install', '--save-dev', ...PACKAGES[stack]], projectPath);
|
|
145
|
+
}
|