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.
@@ -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 &mdash; 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
+ }