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
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scaffly — GitHub Actions CI/CD extra
|
|
3
|
+
* Adds a CI workflow that runs on every push/PR to main.
|
|
4
|
+
*
|
|
5
|
+
* Steps:
|
|
6
|
+
* 1. Checkout code
|
|
7
|
+
* 2. Set up Node.js with npm cache
|
|
8
|
+
* 3. Install dependencies (npm ci)
|
|
9
|
+
* 4. Run linter (if a lint script is present)
|
|
10
|
+
* 5. Run tests (if a test script is present)
|
|
11
|
+
* 6. Build the project (if a build script is present)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import path from 'path';
|
|
15
|
+
import { writeFile } from '../utils/index.js';
|
|
16
|
+
|
|
17
|
+
const CI_WORKFLOW = `name: CI
|
|
18
|
+
|
|
19
|
+
on:
|
|
20
|
+
push:
|
|
21
|
+
branches:
|
|
22
|
+
- main
|
|
23
|
+
- develop
|
|
24
|
+
pull_request:
|
|
25
|
+
branches:
|
|
26
|
+
- main
|
|
27
|
+
|
|
28
|
+
jobs:
|
|
29
|
+
ci:
|
|
30
|
+
name: Lint, Test & Build
|
|
31
|
+
runs-on: ubuntu-latest
|
|
32
|
+
|
|
33
|
+
strategy:
|
|
34
|
+
matrix:
|
|
35
|
+
node-version: [20.x]
|
|
36
|
+
|
|
37
|
+
steps:
|
|
38
|
+
- name: Checkout code
|
|
39
|
+
uses: actions/checkout@v4
|
|
40
|
+
|
|
41
|
+
- name: Set up Node.js \${{ matrix.node-version }}
|
|
42
|
+
uses: actions/setup-node@v4
|
|
43
|
+
with:
|
|
44
|
+
node-version: \${{ matrix.node-version }}
|
|
45
|
+
cache: npm
|
|
46
|
+
|
|
47
|
+
- name: Install dependencies
|
|
48
|
+
run: npm ci
|
|
49
|
+
|
|
50
|
+
- name: Lint
|
|
51
|
+
run: npm run lint --if-present
|
|
52
|
+
|
|
53
|
+
- name: Run tests
|
|
54
|
+
run: npm test --if-present
|
|
55
|
+
|
|
56
|
+
- name: Build
|
|
57
|
+
run: npm run build --if-present
|
|
58
|
+
`;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Adds a GitHub Actions CI workflow to the project.
|
|
62
|
+
*
|
|
63
|
+
* @param {string} projectPath - Absolute path to the project root
|
|
64
|
+
* @param {string} stack - Selected stack identifier (unused but kept for API consistency)
|
|
65
|
+
*/
|
|
66
|
+
export async function apply(projectPath, stack) {
|
|
67
|
+
await writeFile(
|
|
68
|
+
path.join(projectPath, '.github/workflows/ci.yml'),
|
|
69
|
+
CI_WORKFLOW
|
|
70
|
+
);
|
|
71
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scaffly — Husky + lint-staged extra
|
|
3
|
+
* Sets up pre-commit Git hooks that run linting and formatting before each commit.
|
|
4
|
+
*
|
|
5
|
+
* Requires ESLint + Prettier to be installed to fully leverage lint-staged.
|
|
6
|
+
* If the ESLint extra was not selected, the hooks will still run but may warn
|
|
7
|
+
* about missing binaries.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import { writeFile, updatePackageJson, runCommand } from '../utils/index.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Applies Husky + lint-staged configuration to the project.
|
|
15
|
+
*
|
|
16
|
+
* @param {string} projectPath - Absolute path to the project root
|
|
17
|
+
* @param {string} stack - Selected stack identifier (unused but kept for API consistency)
|
|
18
|
+
*/
|
|
19
|
+
export async function apply(projectPath, stack) {
|
|
20
|
+
// ── Install packages ──────────────────────────────────────────────────────
|
|
21
|
+
await runCommand(
|
|
22
|
+
'npm',
|
|
23
|
+
['install', '--save-dev', 'husky', 'lint-staged'],
|
|
24
|
+
projectPath
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
// ── Initialize Husky ──────────────────────────────────────────────────────
|
|
28
|
+
// Creates .husky/ directory and wires up git hooks via the prepare script.
|
|
29
|
+
// Note: requires an initialised git repository to take full effect.
|
|
30
|
+
await runCommand('npx', ['husky', 'init'], projectPath);
|
|
31
|
+
|
|
32
|
+
// ── Update package.json ───────────────────────────────────────────────────
|
|
33
|
+
// husky init already adds "prepare": "husky"; we add lint-staged config here.
|
|
34
|
+
await updatePackageJson(projectPath, {
|
|
35
|
+
'lint-staged': {
|
|
36
|
+
// JS/JSX files: lint then format
|
|
37
|
+
'*.{js,jsx}': ['eslint --fix --max-warnings 0', 'prettier --write'],
|
|
38
|
+
// Other files: format only
|
|
39
|
+
'*.{json,md,css,html}': ['prettier --write'],
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// ── Write pre-commit hook ─────────────────────────────────────────────────
|
|
44
|
+
// Override the default hook created by `husky init` (which runs `npm test`)
|
|
45
|
+
// with our lint-staged runner.
|
|
46
|
+
await writeFile(
|
|
47
|
+
path.join(projectPath, '.husky/pre-commit'),
|
|
48
|
+
'npx lint-staged\n'
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scaffly — Tailwind CSS extra
|
|
3
|
+
* Adds Tailwind CSS (with PostCSS and Autoprefixer) to frontend projects.
|
|
4
|
+
* Only applicable to Next.js and React + Vite stacks.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import fse from 'fs-extra';
|
|
9
|
+
import { writeFile, runCommand } from '../utils/index.js';
|
|
10
|
+
|
|
11
|
+
// ── Tailwind config content per stack ─────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
const TAILWIND_CONFIGS = {
|
|
14
|
+
nextjs: `/** @type {import('tailwindcss').Config} */
|
|
15
|
+
const config = {
|
|
16
|
+
content: [
|
|
17
|
+
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
|
|
18
|
+
'./components/**/*.{js,ts,jsx,tsx,mdx}',
|
|
19
|
+
'./app/**/*.{js,ts,jsx,tsx,mdx}',
|
|
20
|
+
],
|
|
21
|
+
theme: {
|
|
22
|
+
extend: {},
|
|
23
|
+
},
|
|
24
|
+
plugins: [],
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export default config;
|
|
28
|
+
`,
|
|
29
|
+
vite: `/** @type {import('tailwindcss').Config} */
|
|
30
|
+
const config = {
|
|
31
|
+
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
|
32
|
+
theme: {
|
|
33
|
+
extend: {},
|
|
34
|
+
},
|
|
35
|
+
plugins: [],
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export default config;
|
|
39
|
+
`,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// ── CSS file paths per stack ──────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
const CSS_PATHS = {
|
|
45
|
+
nextjs: 'app/globals.css',
|
|
46
|
+
vite: 'src/index.css',
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// ── Tailwind directives to prepend ────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
const TAILWIND_DIRECTIVES = `@tailwind base;
|
|
52
|
+
@tailwind components;
|
|
53
|
+
@tailwind utilities;
|
|
54
|
+
|
|
55
|
+
`;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Applies Tailwind CSS configuration to the project.
|
|
59
|
+
*
|
|
60
|
+
* @param {string} projectPath - Absolute path to the project root
|
|
61
|
+
* @param {string} stack - Selected stack ('nextjs' or 'vite')
|
|
62
|
+
*/
|
|
63
|
+
export async function apply(projectPath, stack) {
|
|
64
|
+
// ── tailwind.config.js ────────────────────────────────────────────────────
|
|
65
|
+
await writeFile(
|
|
66
|
+
path.join(projectPath, 'tailwind.config.js'),
|
|
67
|
+
TAILWIND_CONFIGS[stack]
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
// ── postcss.config.js ─────────────────────────────────────────────────────
|
|
71
|
+
await writeFile(
|
|
72
|
+
path.join(projectPath, 'postcss.config.js'),
|
|
73
|
+
`export default {
|
|
74
|
+
plugins: {
|
|
75
|
+
tailwindcss: {},
|
|
76
|
+
autoprefixer: {},
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
`
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
// ── Prepend Tailwind directives to the main CSS file ─────────────────────
|
|
83
|
+
const cssFilePath = path.join(projectPath, CSS_PATHS[stack]);
|
|
84
|
+
const existingCss = await fse.readFile(cssFilePath, 'utf-8').catch(() => '');
|
|
85
|
+
|
|
86
|
+
// Only prepend if directives aren't already present
|
|
87
|
+
if (!existingCss.includes('@tailwind base')) {
|
|
88
|
+
await fse.writeFile(cssFilePath, TAILWIND_DIRECTIVES + existingCss, 'utf-8');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── Install packages ──────────────────────────────────────────────────────
|
|
92
|
+
await runCommand(
|
|
93
|
+
'npm',
|
|
94
|
+
['install', '--save-dev', 'tailwindcss', 'postcss', 'autoprefixer'],
|
|
95
|
+
projectPath
|
|
96
|
+
);
|
|
97
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scaffly — Express generator
|
|
3
|
+
* Creates a Node.js REST API using Express with a layered folder structure.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { writeFile, writeJson, runCommand } from '../utils/index.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Generates a complete Express REST API project at the given path.
|
|
11
|
+
*
|
|
12
|
+
* @param {string} projectName - Used as the package name and in welcome messages
|
|
13
|
+
* @param {string} projectPath - Absolute path where the project will be created
|
|
14
|
+
*/
|
|
15
|
+
export async function generate(projectName, projectPath) {
|
|
16
|
+
// ── package.json ───────────────────────────────────────────────────────────
|
|
17
|
+
await writeJson(path.join(projectPath, 'package.json'), {
|
|
18
|
+
name: projectName,
|
|
19
|
+
version: '1.0.0',
|
|
20
|
+
type: 'module',
|
|
21
|
+
description: 'A REST API built with Express',
|
|
22
|
+
main: 'src/index.js',
|
|
23
|
+
scripts: {
|
|
24
|
+
start: 'node src/index.js',
|
|
25
|
+
dev: 'node --watch src/index.js',
|
|
26
|
+
test: 'echo "No tests yet" && exit 0',
|
|
27
|
+
},
|
|
28
|
+
dependencies: {
|
|
29
|
+
express: '^4.19.2',
|
|
30
|
+
},
|
|
31
|
+
devDependencies: {},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// ── src/index.js ──────────────────────────────────────────────────────────
|
|
35
|
+
await writeFile(
|
|
36
|
+
path.join(projectPath, 'src/index.js'),
|
|
37
|
+
`import express from 'express';
|
|
38
|
+
import { router } from './routes/index.js';
|
|
39
|
+
import { errorHandler } from './middleware/error-handler.js';
|
|
40
|
+
|
|
41
|
+
const app = express();
|
|
42
|
+
const PORT = process.env.PORT || 3000;
|
|
43
|
+
|
|
44
|
+
// ── Middleware ─────────────────────────────────────────────────────────────
|
|
45
|
+
app.use(express.json());
|
|
46
|
+
app.use(express.urlencoded({ extended: true }));
|
|
47
|
+
|
|
48
|
+
// ── Routes ─────────────────────────────────────────────────────────────────
|
|
49
|
+
app.use('/api', router);
|
|
50
|
+
|
|
51
|
+
// ── Error handling (must be the last middleware) ───────────────────────────
|
|
52
|
+
app.use(errorHandler);
|
|
53
|
+
|
|
54
|
+
// ── Start server ───────────────────────────────────────────────────────────
|
|
55
|
+
app.listen(PORT, () => {
|
|
56
|
+
console.log(\`Server running at http://localhost:\${PORT}\`);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
export default app;
|
|
60
|
+
`
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
// ── src/routes/index.js ───────────────────────────────────────────────────
|
|
64
|
+
await writeFile(
|
|
65
|
+
path.join(projectPath, 'src/routes/index.js'),
|
|
66
|
+
`import { Router } from 'express';
|
|
67
|
+
|
|
68
|
+
export const router = Router();
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* GET /api/health
|
|
72
|
+
* Health-check endpoint — used by load balancers and uptime monitors.
|
|
73
|
+
*/
|
|
74
|
+
router.get('/health', (req, res) => {
|
|
75
|
+
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* GET /api/hello
|
|
80
|
+
* Example endpoint — replace or remove this in your real project.
|
|
81
|
+
*/
|
|
82
|
+
router.get('/hello', (req, res) => {
|
|
83
|
+
res.json({ message: 'Hello from ${projectName}!' });
|
|
84
|
+
});
|
|
85
|
+
`
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
// ── src/middleware/error-handler.js ───────────────────────────────────────
|
|
89
|
+
await writeFile(
|
|
90
|
+
path.join(projectPath, 'src/middleware/error-handler.js'),
|
|
91
|
+
`/**
|
|
92
|
+
* Global error-handling middleware.
|
|
93
|
+
* Must have exactly 4 parameters so Express recognises it as an error handler.
|
|
94
|
+
*
|
|
95
|
+
* @param {Error} err
|
|
96
|
+
* @param {import('express').Request} req
|
|
97
|
+
* @param {import('express').Response} res
|
|
98
|
+
* @param {import('express').NextFunction} next
|
|
99
|
+
*/
|
|
100
|
+
export function errorHandler(err, req, res, next) {
|
|
101
|
+
const status = err.status || err.statusCode || 500;
|
|
102
|
+
const message = err.message || 'Internal Server Error';
|
|
103
|
+
|
|
104
|
+
console.error(\`[Error] \${req.method} \${req.path} → \${status}: \${message}\`);
|
|
105
|
+
|
|
106
|
+
res.status(status).json({
|
|
107
|
+
error: {
|
|
108
|
+
message,
|
|
109
|
+
// Include stack trace in development only
|
|
110
|
+
...(process.env.NODE_ENV !== 'production' && { stack: err.stack }),
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
`
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
// ── .env ──────────────────────────────────────────────────────────────────
|
|
118
|
+
await writeFile(
|
|
119
|
+
path.join(projectPath, '.env'),
|
|
120
|
+
`# Local environment variables — do NOT commit this file
|
|
121
|
+
NODE_ENV=development
|
|
122
|
+
PORT=3000
|
|
123
|
+
`
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
// ── .env.example ─────────────────────────────────────────────────────────
|
|
127
|
+
await writeFile(
|
|
128
|
+
path.join(projectPath, '.env.example'),
|
|
129
|
+
`# Copy this file to .env and fill in your own values
|
|
130
|
+
NODE_ENV=development
|
|
131
|
+
PORT=3000
|
|
132
|
+
`
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
// ── .gitignore ────────────────────────────────────────────────────────────
|
|
136
|
+
await writeFile(
|
|
137
|
+
path.join(projectPath, '.gitignore'),
|
|
138
|
+
`# Dependencies
|
|
139
|
+
node_modules/
|
|
140
|
+
|
|
141
|
+
# Environment — never commit secrets
|
|
142
|
+
.env
|
|
143
|
+
.env.local
|
|
144
|
+
.env.*.local
|
|
145
|
+
|
|
146
|
+
# Build artifacts
|
|
147
|
+
dist/
|
|
148
|
+
build/
|
|
149
|
+
|
|
150
|
+
# Debug
|
|
151
|
+
npm-debug.log*
|
|
152
|
+
yarn-debug.log*
|
|
153
|
+
|
|
154
|
+
# Editor
|
|
155
|
+
.vscode/
|
|
156
|
+
.idea/
|
|
157
|
+
|
|
158
|
+
# Misc
|
|
159
|
+
.DS_Store
|
|
160
|
+
`
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
// ── Install dependencies ──────────────────────────────────────────────────
|
|
164
|
+
await runCommand('npm', ['install'], projectPath);
|
|
165
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scaffly — Fastify generator
|
|
3
|
+
* Creates a high-performance Node.js REST API using Fastify with plugin architecture.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { writeFile, writeJson, runCommand } from '../utils/index.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Generates a complete Fastify REST API project at the given path.
|
|
11
|
+
*
|
|
12
|
+
* @param {string} projectName - Used as the package name and in welcome messages
|
|
13
|
+
* @param {string} projectPath - Absolute path where the project will be created
|
|
14
|
+
*/
|
|
15
|
+
export async function generate(projectName, projectPath) {
|
|
16
|
+
// ── package.json ───────────────────────────────────────────────────────────
|
|
17
|
+
await writeJson(path.join(projectPath, 'package.json'), {
|
|
18
|
+
name: projectName,
|
|
19
|
+
version: '1.0.0',
|
|
20
|
+
type: 'module',
|
|
21
|
+
description: 'A REST API built with Fastify',
|
|
22
|
+
main: 'src/index.js',
|
|
23
|
+
scripts: {
|
|
24
|
+
start: 'node src/index.js',
|
|
25
|
+
dev: 'node --watch src/index.js',
|
|
26
|
+
test: 'echo "No tests yet" && exit 0',
|
|
27
|
+
},
|
|
28
|
+
dependencies: {
|
|
29
|
+
fastify: '^4.28.1',
|
|
30
|
+
'@fastify/cors': '^9.0.1',
|
|
31
|
+
},
|
|
32
|
+
devDependencies: {},
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// ── src/index.js ──────────────────────────────────────────────────────────
|
|
36
|
+
// Uses top-level await (available in ES modules, Node 18+)
|
|
37
|
+
await writeFile(
|
|
38
|
+
path.join(projectPath, 'src/index.js'),
|
|
39
|
+
`import Fastify from 'fastify';
|
|
40
|
+
import cors from '@fastify/cors';
|
|
41
|
+
import { healthRoutes } from './routes/health.js';
|
|
42
|
+
import { helloRoutes } from './routes/hello.js';
|
|
43
|
+
|
|
44
|
+
const fastify = Fastify({
|
|
45
|
+
// Enable built-in logger (pretty-prints in development)
|
|
46
|
+
logger: process.env.NODE_ENV !== 'production',
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// ── Plugins ────────────────────────────────────────────────────────────────
|
|
50
|
+
await fastify.register(cors, { origin: true });
|
|
51
|
+
|
|
52
|
+
// ── Routes ─────────────────────────────────────────────────────────────────
|
|
53
|
+
await fastify.register(healthRoutes, { prefix: '/api' });
|
|
54
|
+
await fastify.register(helloRoutes, { prefix: '/api' });
|
|
55
|
+
|
|
56
|
+
// ── Start server ───────────────────────────────────────────────────────────
|
|
57
|
+
try {
|
|
58
|
+
const PORT = Number(process.env.PORT) || 3000;
|
|
59
|
+
await fastify.listen({ port: PORT, host: '0.0.0.0' });
|
|
60
|
+
console.log(\`Server running at http://localhost:\${PORT}\`);
|
|
61
|
+
} catch (err) {
|
|
62
|
+
fastify.log.error(err);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
`
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
// ── src/routes/health.js ──────────────────────────────────────────────────
|
|
69
|
+
await writeFile(
|
|
70
|
+
path.join(projectPath, 'src/routes/health.js'),
|
|
71
|
+
`/**
|
|
72
|
+
* Health-check route — used by load balancers and uptime monitors.
|
|
73
|
+
*
|
|
74
|
+
* @param {import('fastify').FastifyInstance} fastify
|
|
75
|
+
*/
|
|
76
|
+
export async function healthRoutes(fastify) {
|
|
77
|
+
fastify.get('/health', async () => {
|
|
78
|
+
return { status: 'ok', timestamp: new Date().toISOString() };
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
`
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// ── src/routes/hello.js ───────────────────────────────────────────────────
|
|
85
|
+
await writeFile(
|
|
86
|
+
path.join(projectPath, 'src/routes/hello.js'),
|
|
87
|
+
`/**
|
|
88
|
+
* Example route — replace or extend this in your real project.
|
|
89
|
+
*
|
|
90
|
+
* @param {import('fastify').FastifyInstance} fastify
|
|
91
|
+
*/
|
|
92
|
+
export async function helloRoutes(fastify) {
|
|
93
|
+
// Schema validates the response and enables Fastify's fast serialization
|
|
94
|
+
const schema = {
|
|
95
|
+
response: {
|
|
96
|
+
200: {
|
|
97
|
+
type: 'object',
|
|
98
|
+
properties: {
|
|
99
|
+
message: { type: 'string' },
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
fastify.get('/hello', { schema }, async () => {
|
|
106
|
+
return { message: 'Hello from ${projectName}!' };
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
`
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// ── .env ──────────────────────────────────────────────────────────────────
|
|
113
|
+
await writeFile(
|
|
114
|
+
path.join(projectPath, '.env'),
|
|
115
|
+
`# Local environment variables — do NOT commit this file
|
|
116
|
+
NODE_ENV=development
|
|
117
|
+
PORT=3000
|
|
118
|
+
`
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
// ── .env.example ─────────────────────────────────────────────────────────
|
|
122
|
+
await writeFile(
|
|
123
|
+
path.join(projectPath, '.env.example'),
|
|
124
|
+
`# Copy this file to .env and fill in your own values
|
|
125
|
+
NODE_ENV=development
|
|
126
|
+
PORT=3000
|
|
127
|
+
`
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
// ── .gitignore ────────────────────────────────────────────────────────────
|
|
131
|
+
await writeFile(
|
|
132
|
+
path.join(projectPath, '.gitignore'),
|
|
133
|
+
`# Dependencies
|
|
134
|
+
node_modules/
|
|
135
|
+
|
|
136
|
+
# Environment — never commit secrets
|
|
137
|
+
.env
|
|
138
|
+
.env.local
|
|
139
|
+
.env.*.local
|
|
140
|
+
|
|
141
|
+
# Build artifacts
|
|
142
|
+
dist/
|
|
143
|
+
build/
|
|
144
|
+
|
|
145
|
+
# Debug
|
|
146
|
+
npm-debug.log*
|
|
147
|
+
yarn-debug.log*
|
|
148
|
+
|
|
149
|
+
# Editor
|
|
150
|
+
.vscode/
|
|
151
|
+
.idea/
|
|
152
|
+
|
|
153
|
+
# Misc
|
|
154
|
+
.DS_Store
|
|
155
|
+
`
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
// ── Install dependencies ──────────────────────────────────────────────────
|
|
159
|
+
await runCommand('npm', ['install'], projectPath);
|
|
160
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scaffly — Next.js generator
|
|
3
|
+
* Creates a Next.js project using the App Router with sensible defaults.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { writeFile, writeJson, runCommand } from '../utils/index.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Generates a complete Next.js (App Router) project at the given path.
|
|
11
|
+
*
|
|
12
|
+
* @param {string} projectName - Used as the package name and page title
|
|
13
|
+
* @param {string} projectPath - Absolute path where the project will be created
|
|
14
|
+
*/
|
|
15
|
+
export async function generate(projectName, projectPath) {
|
|
16
|
+
// ── package.json ───────────────────────────────────────────────────────────
|
|
17
|
+
await writeJson(path.join(projectPath, 'package.json'), {
|
|
18
|
+
name: projectName,
|
|
19
|
+
version: '0.1.0',
|
|
20
|
+
private: true,
|
|
21
|
+
scripts: {
|
|
22
|
+
dev: 'next dev',
|
|
23
|
+
build: 'next build',
|
|
24
|
+
start: 'next start',
|
|
25
|
+
},
|
|
26
|
+
dependencies: {
|
|
27
|
+
next: '^14.2.5',
|
|
28
|
+
react: '^18.3.1',
|
|
29
|
+
'react-dom': '^18.3.1',
|
|
30
|
+
},
|
|
31
|
+
devDependencies: {},
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// ── next.config.mjs ────────────────────────────────────────────────────────
|
|
35
|
+
await writeFile(
|
|
36
|
+
path.join(projectPath, 'next.config.mjs'),
|
|
37
|
+
`/** @type {import('next').NextConfig} */
|
|
38
|
+
const nextConfig = {};
|
|
39
|
+
|
|
40
|
+
export default nextConfig;
|
|
41
|
+
`
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
// ── app/layout.js ─────────────────────────────────────────────────────────
|
|
45
|
+
await writeFile(
|
|
46
|
+
path.join(projectPath, 'app/layout.js'),
|
|
47
|
+
`import './globals.css';
|
|
48
|
+
|
|
49
|
+
export const metadata = {
|
|
50
|
+
title: '${projectName}',
|
|
51
|
+
description: 'Generated by Scaffly',
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export default function RootLayout({ children }) {
|
|
55
|
+
return (
|
|
56
|
+
<html lang="en">
|
|
57
|
+
<body>{children}</body>
|
|
58
|
+
</html>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
`
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
// ── app/page.js ───────────────────────────────────────────────────────────
|
|
65
|
+
await writeFile(
|
|
66
|
+
path.join(projectPath, 'app/page.js'),
|
|
67
|
+
`export default function Home() {
|
|
68
|
+
return (
|
|
69
|
+
<main className="container">
|
|
70
|
+
<h1>Welcome to ${projectName}</h1>
|
|
71
|
+
<p>Built with Next.js — scaffolded by Scaffly</p>
|
|
72
|
+
</main>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
`
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// ── app/globals.css ───────────────────────────────────────────────────────
|
|
79
|
+
await writeFile(
|
|
80
|
+
path.join(projectPath, 'app/globals.css'),
|
|
81
|
+
`*,
|
|
82
|
+
*::before,
|
|
83
|
+
*::after {
|
|
84
|
+
box-sizing: border-box;
|
|
85
|
+
margin: 0;
|
|
86
|
+
padding: 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
body {
|
|
90
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
|
|
91
|
+
Ubuntu, sans-serif;
|
|
92
|
+
background: #ffffff;
|
|
93
|
+
color: #111111;
|
|
94
|
+
line-height: 1.6;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.container {
|
|
98
|
+
max-width: 960px;
|
|
99
|
+
margin: 4rem auto;
|
|
100
|
+
padding: 0 1.5rem;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
h1 {
|
|
104
|
+
font-size: 2.5rem;
|
|
105
|
+
margin-bottom: 1rem;
|
|
106
|
+
}
|
|
107
|
+
`
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
// ── .env.local ────────────────────────────────────────────────────────────
|
|
111
|
+
await writeFile(
|
|
112
|
+
path.join(projectPath, '.env.local'),
|
|
113
|
+
`# Local environment variables — do NOT commit this file
|
|
114
|
+
# EXAMPLE_API_KEY=your_key_here
|
|
115
|
+
`
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
// ── .gitignore ────────────────────────────────────────────────────────────
|
|
119
|
+
await writeFile(
|
|
120
|
+
path.join(projectPath, '.gitignore'),
|
|
121
|
+
`# Dependencies
|
|
122
|
+
node_modules/
|
|
123
|
+
|
|
124
|
+
# Next.js build output
|
|
125
|
+
.next/
|
|
126
|
+
out/
|
|
127
|
+
|
|
128
|
+
# Production
|
|
129
|
+
build/
|
|
130
|
+
dist/
|
|
131
|
+
|
|
132
|
+
# Environment variables — never commit secrets
|
|
133
|
+
.env
|
|
134
|
+
.env.local
|
|
135
|
+
.env.*.local
|
|
136
|
+
|
|
137
|
+
# Debug
|
|
138
|
+
npm-debug.log*
|
|
139
|
+
yarn-debug.log*
|
|
140
|
+
yarn-error.log*
|
|
141
|
+
|
|
142
|
+
# Vercel
|
|
143
|
+
.vercel
|
|
144
|
+
|
|
145
|
+
# Misc
|
|
146
|
+
.DS_Store
|
|
147
|
+
*.pem
|
|
148
|
+
`
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
// ── Install dependencies ──────────────────────────────────────────────────
|
|
152
|
+
await runCommand('npm', ['install'], projectPath);
|
|
153
|
+
}
|