@vpnsin/devkit 0.1.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 +318 -0
- package/bin/cli.js +431 -0
- package/commitlint/index.js +7 -0
- package/eslint/base.js +50 -0
- package/eslint/next.js +27 -0
- package/jest/index.js +20 -0
- package/lint-staged/index.js +8 -0
- package/package.json +80 -0
- package/prettier/index.js +11 -0
- package/templates/README.template.md +51 -0
- package/templates/app/backend/Dockerfile +24 -0
- package/templates/app/backend/dockerignore +7 -0
- package/templates/app/backend/env.example +2 -0
- package/templates/app/backend/src/app.ts +22 -0
- package/templates/app/backend/src/env.ts +8 -0
- package/templates/app/backend/src/routes/health.ts +7 -0
- package/templates/app/backend/src/server.ts +19 -0
- package/templates/app/frontend/app/globals.css +28 -0
- package/templates/app/frontend/app/layout.tsx +16 -0
- package/templates/app/frontend/app/page.tsx +10 -0
- package/templates/app/frontend/env.example +5 -0
- package/templates/app/frontend/next.config.mjs +6 -0
- package/templates/claude/skills/design-craft/SKILL.md +226 -0
- package/templates/cspell.json +30 -0
- package/templates/dependabot.yml +18 -0
- package/templates/editorconfig +15 -0
- package/templates/github/CODEOWNERS +12 -0
- package/templates/github/CONTRIBUTING.md +51 -0
- package/templates/github/ISSUE_TEMPLATE/bug_report.yml +34 -0
- package/templates/github/ISSUE_TEMPLATE/config.yml +5 -0
- package/templates/github/ISSUE_TEMPLATE/feature_request.yml +23 -0
- package/templates/github/PULL_REQUEST_TEMPLATE.md +27 -0
- package/templates/github/SECURITY.md +24 -0
- package/templates/github/workflows/ci.yml +55 -0
- package/templates/github/workflows/codeql.yml +35 -0
- package/templates/github/workflows/dependency-review.yml +23 -0
- package/templates/github/workflows/lighthouse.yml +39 -0
- package/templates/github/workflows/publish.yml +38 -0
- package/templates/github/workflows/release-please-publish.yml +54 -0
- package/templates/github/workflows/release-please.yml +22 -0
- package/templates/github/workflows/scorecard.yml +41 -0
- package/templates/github/workflows/sonarqube.yml +31 -0
- package/templates/github/workflows/trivy.yml +43 -0
- package/templates/husky/commit-msg +1 -0
- package/templates/husky/pre-commit +1 -0
- package/templates/lighthouserc.json +23 -0
- package/templates/markdownlint-cli2.jsonc +20 -0
- package/templates/npmrc +9 -0
- package/templates/nvmrc +1 -0
- package/templates/release-please-config.json +14 -0
- package/templates/sonar-project.properties +13 -0
- package/templates/vscode/extensions.json +53 -0
- package/templates/vscode/settings.json +70 -0
- package/tsconfig/base.json +17 -0
- package/tsconfig/next.json +16 -0
- package/tsconfig/node.json +14 -0
- package/vitest/index.js +22 -0
package/bin/cli.js
ADDED
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// devkit CLI — scaffolds shared lint/format/commit/CI/release tooling into
|
|
3
|
+
// a Node.js or Next.js repo. Configs that CAN reference the package (ESLint,
|
|
4
|
+
// Prettier, commitlint, lint-staged) are written as thin shims so they stay in
|
|
5
|
+
// sync; files that CANNOT be referenced (Husky hooks, GitHub workflows, VS Code
|
|
6
|
+
// settings, PR template, release-please config) are copied as templates.
|
|
7
|
+
//
|
|
8
|
+
// Usage:
|
|
9
|
+
// npx devkit init [--node|--next] [--force] [--no-install]
|
|
10
|
+
|
|
11
|
+
import { execSync } from 'node:child_process';
|
|
12
|
+
import {
|
|
13
|
+
mkdirSync,
|
|
14
|
+
copyFileSync,
|
|
15
|
+
writeFileSync,
|
|
16
|
+
readFileSync,
|
|
17
|
+
chmodSync,
|
|
18
|
+
constants,
|
|
19
|
+
} from 'node:fs';
|
|
20
|
+
import { dirname, join, resolve } from 'node:path';
|
|
21
|
+
import { fileURLToPath } from 'node:url';
|
|
22
|
+
|
|
23
|
+
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
24
|
+
const TEMPLATES = resolve(HERE, '..', 'templates');
|
|
25
|
+
const CWD = process.cwd();
|
|
26
|
+
|
|
27
|
+
const argv = process.argv.slice(2);
|
|
28
|
+
const cmd = argv[0] && !argv[0].startsWith('-') ? argv[0] : 'init';
|
|
29
|
+
const has = (flag) => argv.includes(flag);
|
|
30
|
+
|
|
31
|
+
const c = {
|
|
32
|
+
green: (s) => `\x1b[32m${s}\x1b[0m`,
|
|
33
|
+
yellow: (s) => `\x1b[33m${s}\x1b[0m`,
|
|
34
|
+
cyan: (s) => `\x1b[36m${s}\x1b[0m`,
|
|
35
|
+
dim: (s) => `\x1b[2m${s}\x1b[0m`,
|
|
36
|
+
bold: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const log = {
|
|
40
|
+
add: (p) => console.log(` ${c.green('+')} ${p}`),
|
|
41
|
+
skip: (p) => console.log(` ${c.dim('•')} ${c.dim(`${p} (exists, left as-is)`)}`),
|
|
42
|
+
edit: (p) => console.log(` ${c.cyan('~')} ${p}`),
|
|
43
|
+
info: (m) => console.log(` ${c.dim(m)}`),
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
if (cmd === 'help' || has('-h') || has('--help')) {
|
|
47
|
+
console.log(`
|
|
48
|
+
${c.bold('devkit')} — shared Node.js/Next.js dev config
|
|
49
|
+
|
|
50
|
+
${c.bold('Usage:')} npx devkit init [options]
|
|
51
|
+
|
|
52
|
+
${c.bold('Options:')}
|
|
53
|
+
--next force the Next.js ESLint preset
|
|
54
|
+
--node force the base (Node) ESLint preset
|
|
55
|
+
--private private repo: skip GHAS workflows (Dependabot + npm audit instead)
|
|
56
|
+
--public public repo: include GHAS workflows (default; auto-detected via gh)
|
|
57
|
+
--backend scaffold a runnable Express + TypeScript backend (src/, Dockerfile)
|
|
58
|
+
--frontend scaffold a runnable Next.js (App Router) + TypeScript frontend
|
|
59
|
+
--jest also scaffold Jest (ts-jest) config, scripts and deps
|
|
60
|
+
--vitest also scaffold Vitest config, scripts and deps
|
|
61
|
+
--scorecard also add the OSSF Scorecard workflow (public repos)
|
|
62
|
+
--publish auto-publish to npm when the release-please PR merges (needs NPM_TOKEN)
|
|
63
|
+
--sonar also add SonarCloud analysis (needs SONAR_TOKEN)
|
|
64
|
+
--lighthouse also add a Lighthouse CI workflow (web apps)
|
|
65
|
+
--skills also add Claude Code skills (e.g. design-craft for UI/UX)
|
|
66
|
+
--force overwrite existing config/template files
|
|
67
|
+
--no-install skip installing dev dependencies
|
|
68
|
+
-h, --help show this help
|
|
69
|
+
`);
|
|
70
|
+
process.exit(0);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (cmd !== 'init') {
|
|
74
|
+
console.error(`Unknown command "${cmd}". Run: npx devkit --help`);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── Load the consumer package.json ──────────────────────────────────────────
|
|
79
|
+
const pkgPath = join(CWD, 'package.json');
|
|
80
|
+
let pkg;
|
|
81
|
+
try {
|
|
82
|
+
pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
83
|
+
} catch (err) {
|
|
84
|
+
if (err.code === 'ENOENT') {
|
|
85
|
+
console.error(
|
|
86
|
+
'No package.json found in the current directory. Run this inside a Node project.'
|
|
87
|
+
);
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
throw err;
|
|
91
|
+
}
|
|
92
|
+
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
93
|
+
|
|
94
|
+
// App starters (opt-in): scaffold a runnable skeleton, not just tooling config.
|
|
95
|
+
const wantsBackend = has('--backend');
|
|
96
|
+
const wantsFrontend = has('--frontend');
|
|
97
|
+
if (wantsBackend && wantsFrontend) {
|
|
98
|
+
console.error(
|
|
99
|
+
'--backend and --frontend scaffold a single flat app each; run them in separate directories (or set up a monorepo) instead of combining them.'
|
|
100
|
+
);
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// --frontend implies the Next.js preset; --backend implies the Node preset.
|
|
105
|
+
const isNext = has('--next')
|
|
106
|
+
? true
|
|
107
|
+
: has('--node')
|
|
108
|
+
? false
|
|
109
|
+
: wantsFrontend
|
|
110
|
+
? true
|
|
111
|
+
: wantsBackend
|
|
112
|
+
? false
|
|
113
|
+
: Boolean(allDeps.next);
|
|
114
|
+
const force = has('--force');
|
|
115
|
+
|
|
116
|
+
// GHAS code scanning (CodeQL/Trivy/Dependency Review/Scorecard) is free only on
|
|
117
|
+
// PUBLIC repos; private repos need a paid licence. Honour --private/--public, or
|
|
118
|
+
// best-effort auto-detect via the gh CLI (defaults to public if we can't tell).
|
|
119
|
+
function detectPrivate() {
|
|
120
|
+
if (has('--public')) return false;
|
|
121
|
+
if (has('--private')) return true;
|
|
122
|
+
try {
|
|
123
|
+
const out = execSync('gh repo view --json visibility -q .visibility', {
|
|
124
|
+
cwd: CWD,
|
|
125
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
126
|
+
})
|
|
127
|
+
.toString()
|
|
128
|
+
.trim();
|
|
129
|
+
return out === 'PRIVATE';
|
|
130
|
+
} catch {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
const isPrivate = detectPrivate();
|
|
135
|
+
|
|
136
|
+
console.log(
|
|
137
|
+
`\n${c.bold('devkit init')} ${c.dim(`(${isNext ? 'Next.js' : 'Node'} preset, ${isPrivate ? 'private' : 'public'} repo)`)}\n`
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
141
|
+
function ensureDir(file) {
|
|
142
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function copyTemplate(rel, dest, { executable = false } = {}) {
|
|
146
|
+
const target = join(CWD, dest);
|
|
147
|
+
ensureDir(target);
|
|
148
|
+
try {
|
|
149
|
+
// COPYFILE_EXCL fails atomically (EEXIST) if the target exists — this avoids
|
|
150
|
+
// the check-then-write race of a separate existsSync() guard.
|
|
151
|
+
copyFileSync(join(TEMPLATES, rel), target, force ? 0 : constants.COPYFILE_EXCL);
|
|
152
|
+
} catch (err) {
|
|
153
|
+
if (err.code === 'EEXIST') return log.skip(dest);
|
|
154
|
+
throw err;
|
|
155
|
+
}
|
|
156
|
+
if (executable) {
|
|
157
|
+
try {
|
|
158
|
+
chmodSync(target, 0o755);
|
|
159
|
+
} catch {
|
|
160
|
+
/* chmod is a no-op / unsupported on some platforms */
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
log.add(dest);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function writeFileIfAbsent(dest, content) {
|
|
167
|
+
const target = join(CWD, dest);
|
|
168
|
+
ensureDir(target);
|
|
169
|
+
try {
|
|
170
|
+
// The 'wx' flag fails atomically (EEXIST) if the file exists — this avoids
|
|
171
|
+
// the check-then-write race of a separate existsSync() guard.
|
|
172
|
+
writeFileSync(target, content, { flag: force ? 'w' : 'wx' });
|
|
173
|
+
} catch (err) {
|
|
174
|
+
if (err.code === 'EEXIST') return log.skip(dest);
|
|
175
|
+
throw err;
|
|
176
|
+
}
|
|
177
|
+
log.add(dest);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ── 1. Config shims (kept in sync with the package) ─────────────────────────
|
|
181
|
+
console.log(c.bold('Config shims'));
|
|
182
|
+
const eslintPreset = isNext ? '@vpnsin/devkit/eslint/next' : '@vpnsin/devkit/eslint/base';
|
|
183
|
+
// ESLint (needs jiti) and commitlint both load TypeScript config files natively,
|
|
184
|
+
// so these shims are .ts. lint-staged stays .mjs: its .ts auto-detection is
|
|
185
|
+
// unreliable and would silently break the bare `npx lint-staged` pre-commit hook.
|
|
186
|
+
writeFileIfAbsent('eslint.config.ts', `export { default } from '${eslintPreset}';\n`);
|
|
187
|
+
writeFileIfAbsent('commitlint.config.ts', `export { default } from '@vpnsin/devkit/commitlint';\n`);
|
|
188
|
+
writeFileIfAbsent('.lintstagedrc.mjs', `export { default } from '@vpnsin/devkit/lint-staged';\n`);
|
|
189
|
+
|
|
190
|
+
// TypeScript: scaffold a tsconfig that extends the shared base (only if absent).
|
|
191
|
+
const tsconfigBody = isNext
|
|
192
|
+
? {
|
|
193
|
+
extends: '@vpnsin/devkit/tsconfig/next.json',
|
|
194
|
+
include: ['next-env.d.ts', '**/*.ts', '**/*.tsx', '.next/types/**/*.ts'],
|
|
195
|
+
exclude: ['node_modules'],
|
|
196
|
+
}
|
|
197
|
+
: {
|
|
198
|
+
extends: '@vpnsin/devkit/tsconfig/node.json',
|
|
199
|
+
compilerOptions: { outDir: 'dist', rootDir: 'src' },
|
|
200
|
+
include: ['src/**/*.ts'],
|
|
201
|
+
exclude: ['node_modules', 'dist'],
|
|
202
|
+
};
|
|
203
|
+
writeFileIfAbsent('tsconfig.json', `${JSON.stringify(tsconfigBody, null, 2)}\n`);
|
|
204
|
+
|
|
205
|
+
// Test runner (opt-in): shim the shared preset. Jest or Vitest, not both.
|
|
206
|
+
// Vitest loads .ts config natively (esbuild). Jest stays .mjs: its ts-node loader
|
|
207
|
+
// transpiles to CJS and cannot re-export devkit's ESM preset from a .ts config.
|
|
208
|
+
if (has('--jest')) {
|
|
209
|
+
writeFileIfAbsent('jest.config.mjs', `export { default } from '@vpnsin/devkit/jest';\n`);
|
|
210
|
+
}
|
|
211
|
+
if (has('--vitest')) {
|
|
212
|
+
writeFileIfAbsent(
|
|
213
|
+
'vitest.config.ts',
|
|
214
|
+
`import { defineConfig } from 'vitest/config';\nimport base from '@vpnsin/devkit/vitest';\nexport default defineConfig(base);\n`
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ── 1b. App starter (opt-in) ────────────────────────────────────────────────
|
|
219
|
+
if (wantsBackend) {
|
|
220
|
+
console.log(c.bold('\nBackend app (Express + TypeScript)'));
|
|
221
|
+
copyTemplate('app/backend/src/server.ts', 'src/server.ts');
|
|
222
|
+
copyTemplate('app/backend/src/app.ts', 'src/app.ts');
|
|
223
|
+
copyTemplate('app/backend/src/routes/health.ts', 'src/routes/health.ts');
|
|
224
|
+
copyTemplate('app/backend/src/env.ts', 'src/env.ts');
|
|
225
|
+
copyTemplate('app/backend/env.example', '.env.example');
|
|
226
|
+
copyTemplate('app/backend/Dockerfile', 'Dockerfile');
|
|
227
|
+
copyTemplate('app/backend/dockerignore', '.dockerignore');
|
|
228
|
+
}
|
|
229
|
+
if (wantsFrontend) {
|
|
230
|
+
console.log(c.bold('\nFrontend app (Next.js App Router + TypeScript)'));
|
|
231
|
+
copyTemplate('app/frontend/app/layout.tsx', 'app/layout.tsx');
|
|
232
|
+
copyTemplate('app/frontend/app/page.tsx', 'app/page.tsx');
|
|
233
|
+
copyTemplate('app/frontend/app/globals.css', 'app/globals.css');
|
|
234
|
+
copyTemplate('app/frontend/next.config.mjs', 'next.config.mjs');
|
|
235
|
+
copyTemplate('app/frontend/env.example', '.env.example');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ── 2. Copied templates ─────────────────────────────────────────────────────
|
|
239
|
+
console.log(c.bold('\nEditor & hooks'));
|
|
240
|
+
copyTemplate('husky/pre-commit', '.husky/pre-commit', { executable: true });
|
|
241
|
+
copyTemplate('husky/commit-msg', '.husky/commit-msg', { executable: true });
|
|
242
|
+
copyTemplate('vscode/settings.json', '.vscode/settings.json');
|
|
243
|
+
copyTemplate('vscode/extensions.json', '.vscode/extensions.json');
|
|
244
|
+
copyTemplate('editorconfig', '.editorconfig');
|
|
245
|
+
copyTemplate('nvmrc', '.nvmrc');
|
|
246
|
+
copyTemplate('npmrc', '.npmrc');
|
|
247
|
+
copyTemplate('markdownlint-cli2.jsonc', '.markdownlint-cli2.jsonc');
|
|
248
|
+
copyTemplate('cspell.json', 'cspell.json');
|
|
249
|
+
|
|
250
|
+
console.log(c.bold('\nGitHub workflows'));
|
|
251
|
+
// Always free on public + private:
|
|
252
|
+
copyTemplate('github/workflows/ci.yml', '.github/workflows/ci.yml');
|
|
253
|
+
// With --publish, use the release-please workflow that ALSO publishes to npm when
|
|
254
|
+
// the release PR is merged. (A GITHUB_TOKEN-created release can't trigger a
|
|
255
|
+
// separate on:release workflow, so the publish step must be integrated here.)
|
|
256
|
+
copyTemplate(
|
|
257
|
+
has('--publish')
|
|
258
|
+
? 'github/workflows/release-please-publish.yml'
|
|
259
|
+
: 'github/workflows/release-please.yml',
|
|
260
|
+
'.github/workflows/release-please.yml'
|
|
261
|
+
);
|
|
262
|
+
copyTemplate('release-please-config.json', 'release-please-config.json');
|
|
263
|
+
writeFileIfAbsent(
|
|
264
|
+
'.release-please-manifest.json',
|
|
265
|
+
`${JSON.stringify({ '.': pkg.version || '0.0.0' }, null, 2)}\n`
|
|
266
|
+
);
|
|
267
|
+
copyTemplate('dependabot.yml', '.github/dependabot.yml');
|
|
268
|
+
|
|
269
|
+
if (isPrivate) {
|
|
270
|
+
log.info(
|
|
271
|
+
'private repo → skipping GHAS workflows (CodeQL/Trivy/Dependency Review need a paid licence)'
|
|
272
|
+
);
|
|
273
|
+
log.info('dependency security covered by Dependabot + the npm audit step in ci.yml');
|
|
274
|
+
} else {
|
|
275
|
+
// GHAS — free on public repos:
|
|
276
|
+
copyTemplate('github/workflows/codeql.yml', '.github/workflows/codeql.yml');
|
|
277
|
+
copyTemplate('github/workflows/dependency-review.yml', '.github/workflows/dependency-review.yml');
|
|
278
|
+
copyTemplate('github/workflows/trivy.yml', '.github/workflows/trivy.yml');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (has('--scorecard')) {
|
|
282
|
+
if (isPrivate) log.info('Scorecard needs a public repo — skipping');
|
|
283
|
+
else copyTemplate('github/workflows/scorecard.yml', '.github/workflows/scorecard.yml');
|
|
284
|
+
}
|
|
285
|
+
if (has('--publish')) {
|
|
286
|
+
// Manual recovery workflow; auto-publish lives in release-please.yml (above).
|
|
287
|
+
copyTemplate('github/workflows/publish.yml', '.github/workflows/publish.yml');
|
|
288
|
+
}
|
|
289
|
+
if (has('--sonar')) {
|
|
290
|
+
copyTemplate('github/workflows/sonarqube.yml', '.github/workflows/sonarqube.yml');
|
|
291
|
+
copyTemplate('sonar-project.properties', 'sonar-project.properties');
|
|
292
|
+
}
|
|
293
|
+
if (has('--lighthouse')) {
|
|
294
|
+
copyTemplate('github/workflows/lighthouse.yml', '.github/workflows/lighthouse.yml');
|
|
295
|
+
copyTemplate('lighthouserc.json', 'lighthouserc.json');
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
console.log(c.bold('\nGovernance & docs'));
|
|
299
|
+
copyTemplate('github/PULL_REQUEST_TEMPLATE.md', '.github/PULL_REQUEST_TEMPLATE.md');
|
|
300
|
+
copyTemplate('github/SECURITY.md', '.github/SECURITY.md');
|
|
301
|
+
copyTemplate('github/CONTRIBUTING.md', '.github/CONTRIBUTING.md');
|
|
302
|
+
copyTemplate('github/CODEOWNERS', '.github/CODEOWNERS');
|
|
303
|
+
copyTemplate('github/ISSUE_TEMPLATE/bug_report.yml', '.github/ISSUE_TEMPLATE/bug_report.yml');
|
|
304
|
+
copyTemplate(
|
|
305
|
+
'github/ISSUE_TEMPLATE/feature_request.yml',
|
|
306
|
+
'.github/ISSUE_TEMPLATE/feature_request.yml'
|
|
307
|
+
);
|
|
308
|
+
copyTemplate('github/ISSUE_TEMPLATE/config.yml', '.github/ISSUE_TEMPLATE/config.yml');
|
|
309
|
+
copyTemplate('README.template.md', 'README.md'); // only if absent (never clobbers)
|
|
310
|
+
|
|
311
|
+
if (has('--skills')) {
|
|
312
|
+
console.log(c.bold('\nClaude Code skills'));
|
|
313
|
+
copyTemplate('claude/skills/design-craft/SKILL.md', '.claude/skills/design-craft/SKILL.md');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ── 3. Merge package.json (scripts, prettier key) ───────────────────────────
|
|
317
|
+
console.log(c.bold('\npackage.json'));
|
|
318
|
+
const scripts = {
|
|
319
|
+
lint: 'eslint .',
|
|
320
|
+
'lint:fix': 'eslint . --fix',
|
|
321
|
+
'lint:md': 'markdownlint-cli2',
|
|
322
|
+
format: 'prettier --write .',
|
|
323
|
+
'format:check': 'prettier --check .',
|
|
324
|
+
'type-check': 'tsc --noEmit',
|
|
325
|
+
prepare: 'husky',
|
|
326
|
+
...(wantsBackend
|
|
327
|
+
? { dev: 'tsx watch src/server.ts', build: 'tsc', start: 'node dist/server.js' }
|
|
328
|
+
: {}),
|
|
329
|
+
...(wantsFrontend ? { dev: 'next dev', build: 'next build', start: 'next start' } : {}),
|
|
330
|
+
...(has('--jest')
|
|
331
|
+
? { test: 'jest', 'test:watch': 'jest --watch', 'test:coverage': 'jest --coverage' }
|
|
332
|
+
: {}),
|
|
333
|
+
...(has('--vitest')
|
|
334
|
+
? { test: 'vitest run', 'test:watch': 'vitest', 'test:coverage': 'vitest run --coverage' }
|
|
335
|
+
: {}),
|
|
336
|
+
};
|
|
337
|
+
pkg.scripts ??= {};
|
|
338
|
+
let changed = false;
|
|
339
|
+
for (const [k, v] of Object.entries(scripts)) {
|
|
340
|
+
if (!pkg.scripts[k]) {
|
|
341
|
+
pkg.scripts[k] = v;
|
|
342
|
+
changed = true;
|
|
343
|
+
log.add(`scripts.${k}`);
|
|
344
|
+
} else {
|
|
345
|
+
log.skip(`scripts.${k}`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
if (!pkg.prettier) {
|
|
349
|
+
pkg.prettier = '@vpnsin/devkit/prettier';
|
|
350
|
+
changed = true;
|
|
351
|
+
log.add('prettier');
|
|
352
|
+
} else {
|
|
353
|
+
log.skip('prettier');
|
|
354
|
+
}
|
|
355
|
+
if (changed) writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
356
|
+
|
|
357
|
+
// ── 4. Install dev dependencies ─────────────────────────────────────────────
|
|
358
|
+
const devDeps = [
|
|
359
|
+
'@vpnsin/devkit',
|
|
360
|
+
'eslint',
|
|
361
|
+
'prettier',
|
|
362
|
+
'husky',
|
|
363
|
+
'lint-staged',
|
|
364
|
+
'@commitlint/cli',
|
|
365
|
+
'markdownlint-cli2',
|
|
366
|
+
'typescript',
|
|
367
|
+
'jiti', // lets ESLint load the eslint.config.ts shim (Node < 24.3)
|
|
368
|
+
...(isNext ? ['eslint-config-next'] : []),
|
|
369
|
+
...(has('--jest') ? ['jest', 'ts-jest', '@types/jest'] : []),
|
|
370
|
+
...(has('--vitest') ? ['vitest', '@vitest/coverage-v8'] : []),
|
|
371
|
+
...(wantsBackend ? ['tsx', '@types/node', '@types/express', '@types/cors'] : []),
|
|
372
|
+
...(wantsFrontend ? ['@types/react', '@types/react-dom'] : []),
|
|
373
|
+
];
|
|
374
|
+
|
|
375
|
+
// Runtime dependencies for the app starters (installed without -D).
|
|
376
|
+
const prodDeps = [
|
|
377
|
+
...(wantsBackend ? ['express', 'cors', 'helmet', 'dotenv'] : []),
|
|
378
|
+
...(wantsFrontend ? ['next', 'react', 'react-dom'] : []),
|
|
379
|
+
];
|
|
380
|
+
|
|
381
|
+
if (has('--no-install')) {
|
|
382
|
+
console.log(c.bold('\nDependencies'));
|
|
383
|
+
log.info(`Skipped (--no-install). Install manually:`);
|
|
384
|
+
if (prodDeps.length) log.info(`npm i ${prodDeps.join(' ')}`);
|
|
385
|
+
log.info(`npm i -D ${devDeps.join(' ')}`);
|
|
386
|
+
} else {
|
|
387
|
+
console.log(c.bold('\nInstalling dependencies…'));
|
|
388
|
+
if (prodDeps.length) log.info(`npm i ${prodDeps.join(' ')}`);
|
|
389
|
+
log.info(`npm i -D ${devDeps.join(' ')}`);
|
|
390
|
+
try {
|
|
391
|
+
if (prodDeps.length)
|
|
392
|
+
execSync(`npm install ${prodDeps.join(' ')}`, { cwd: CWD, stdio: 'inherit' });
|
|
393
|
+
execSync(`npm install -D ${devDeps.join(' ')}`, { cwd: CWD, stdio: 'inherit' });
|
|
394
|
+
} catch {
|
|
395
|
+
console.log(c.yellow('\n ! Install failed — run it manually:'));
|
|
396
|
+
if (prodDeps.length) log.info(`npm i ${prodDeps.join(' ')}`);
|
|
397
|
+
log.info(`npm i -D ${devDeps.join(' ')}`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ── 5. Initialise Husky ─────────────────────────────────────────────────────
|
|
402
|
+
console.log(c.bold('\nHusky'));
|
|
403
|
+
try {
|
|
404
|
+
execSync('npx husky', { cwd: CWD, stdio: 'ignore' });
|
|
405
|
+
log.info('git hooks installed (core.hooksPath set)');
|
|
406
|
+
} catch {
|
|
407
|
+
log.info('Run "npx husky" once to finish hook installation.');
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// ── Done ────────────────────────────────────────────────────────────────────
|
|
411
|
+
console.log(`\n${c.green('✓ devkit wired up.')}\n`);
|
|
412
|
+
console.log(`${c.bold('Next steps:')}`);
|
|
413
|
+
console.log(
|
|
414
|
+
` 1. Fill placeholders: ${c.cyan('.github/CODEOWNERS')} (@OWNER) and the security contact in ${c.cyan('.github/SECURITY.md')}.`
|
|
415
|
+
);
|
|
416
|
+
console.log(` 2. One-time normalise formatting: ${c.cyan('npm run format')}`);
|
|
417
|
+
console.log(
|
|
418
|
+
` 3. Verify the gates: ${c.cyan('npm run lint && npm run type-check && npm run lint:md')}`
|
|
419
|
+
);
|
|
420
|
+
console.log(
|
|
421
|
+
` 4. In GitHub repo settings, enable Code scanning, Secret scanning & Dependency graph.`
|
|
422
|
+
);
|
|
423
|
+
console.log(
|
|
424
|
+
` 5. Commit with a Conventional Commit ${c.dim('e.g. git commit -m "chore: adopt devkit"')}`
|
|
425
|
+
);
|
|
426
|
+
if (wantsBackend || wantsFrontend) {
|
|
427
|
+
console.log(
|
|
428
|
+
` 6. Run the app: ${c.cyan('npm run dev')} ${c.dim('(copy .env.example → .env first)')}`
|
|
429
|
+
);
|
|
430
|
+
}
|
|
431
|
+
console.log('');
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Shared commitlint config — Conventional Commits, which release-please reads
|
|
2
|
+
// to compute semantic version bumps and the changelog.
|
|
3
|
+
// // commitlint.config.mjs
|
|
4
|
+
// export { default } from 'devkit/commitlint';
|
|
5
|
+
export default {
|
|
6
|
+
extends: ['@commitlint/config-conventional'],
|
|
7
|
+
};
|
package/eslint/base.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Shared base ESLint flat config for Node.js / TypeScript repos.
|
|
2
|
+
// Composes: @eslint/js recommended + typescript-eslint recommended + Prettier
|
|
3
|
+
// (formatting surfaced as warnings, not errors). Import and spread to extend:
|
|
4
|
+
//
|
|
5
|
+
// import base from 'devkit/eslint/base';
|
|
6
|
+
// export default [...base, { rules: { 'no-console': 'off' } }];
|
|
7
|
+
|
|
8
|
+
import js from '@eslint/js';
|
|
9
|
+
import tseslint from 'typescript-eslint';
|
|
10
|
+
import prettierConfig from 'eslint-config-prettier';
|
|
11
|
+
import prettierPlugin from 'eslint-plugin-prettier';
|
|
12
|
+
import globals from 'globals';
|
|
13
|
+
|
|
14
|
+
/** Shared rules applied to JS/TS source files. Reused by the `next` preset. */
|
|
15
|
+
export const sharedRules = {
|
|
16
|
+
'prettier/prettier': 'warn',
|
|
17
|
+
// Prefer the TS-aware unused-vars rule (the base rule mis-flags type params).
|
|
18
|
+
'no-unused-vars': 'off',
|
|
19
|
+
'@typescript-eslint/no-unused-vars': [
|
|
20
|
+
'warn',
|
|
21
|
+
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_' },
|
|
22
|
+
],
|
|
23
|
+
'@typescript-eslint/no-explicit-any': 'warn',
|
|
24
|
+
'no-console': ['warn', { allow: ['warn', 'error', 'info'] }],
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** Directories that should never be linted. */
|
|
28
|
+
export const sharedIgnores = [
|
|
29
|
+
'**/node_modules/**',
|
|
30
|
+
'**/.next/**',
|
|
31
|
+
'**/dist/**',
|
|
32
|
+
'**/build/**',
|
|
33
|
+
'**/out/**',
|
|
34
|
+
'**/coverage/**',
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
export default tseslint.config(
|
|
38
|
+
{ ignores: sharedIgnores },
|
|
39
|
+
js.configs.recommended,
|
|
40
|
+
...tseslint.configs.recommended,
|
|
41
|
+
prettierConfig, // turn off ESLint rules that conflict with Prettier
|
|
42
|
+
{
|
|
43
|
+
files: ['**/*.{ts,tsx,js,jsx,mjs,cjs}'],
|
|
44
|
+
languageOptions: {
|
|
45
|
+
globals: { ...globals.node, ...globals.browser },
|
|
46
|
+
},
|
|
47
|
+
plugins: { prettier: prettierPlugin },
|
|
48
|
+
rules: sharedRules,
|
|
49
|
+
}
|
|
50
|
+
);
|
package/eslint/next.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Shared ESLint flat config for Next.js repos = base preset + eslint-config-next.
|
|
2
|
+
// `eslint-config-next` is an OPTIONAL peer dependency — install it in the
|
|
3
|
+
// consuming Next.js repo (the `devkit init` CLI does this automatically).
|
|
4
|
+
//
|
|
5
|
+
// import next from 'devkit/eslint/next';
|
|
6
|
+
// export default next;
|
|
7
|
+
|
|
8
|
+
import base from './base.js';
|
|
9
|
+
import prettierConfig from 'eslint-config-prettier';
|
|
10
|
+
|
|
11
|
+
let nextConfigs = [];
|
|
12
|
+
try {
|
|
13
|
+
const mod = await import('eslint-config-next');
|
|
14
|
+
// eslint-config-next ships a flat-config array (default export).
|
|
15
|
+
nextConfigs = Array.isArray(mod.default) ? mod.default : (mod.default ?? []);
|
|
16
|
+
} catch {
|
|
17
|
+
throw new Error(
|
|
18
|
+
"devkit/eslint/next requires 'eslint-config-next'. Install it: npm i -D eslint-config-next"
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default [
|
|
23
|
+
...base,
|
|
24
|
+
...nextConfigs,
|
|
25
|
+
// Re-apply last so Prettier always wins over any formatting rules Next re-enables.
|
|
26
|
+
prettierConfig,
|
|
27
|
+
];
|
package/jest/index.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Base Jest config for TypeScript (Node) projects, via ts-jest.
|
|
2
|
+
// // jest.config.mjs
|
|
3
|
+
// export { default } from 'devkit/jest';
|
|
4
|
+
//
|
|
5
|
+
// To extend:
|
|
6
|
+
// import base from 'devkit/jest';
|
|
7
|
+
// export default { ...base, setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'] };
|
|
8
|
+
//
|
|
9
|
+
// Requires `jest`, `ts-jest` and `@types/jest` in the consuming repo (the
|
|
10
|
+
// `devkit init --jest` CLI installs them). Next.js apps should instead use
|
|
11
|
+
// `next/jest` to wire up SWC + module aliases.
|
|
12
|
+
export default {
|
|
13
|
+
preset: 'ts-jest',
|
|
14
|
+
testEnvironment: 'node',
|
|
15
|
+
testMatch: ['**/?(*.)+(test|spec).[tj]s?(x)'],
|
|
16
|
+
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'],
|
|
17
|
+
collectCoverageFrom: ['src/**/*.{ts,tsx}', '!src/**/*.d.ts'],
|
|
18
|
+
coverageDirectory: 'coverage',
|
|
19
|
+
clearMocks: true,
|
|
20
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Shared lint-staged config — runs on staged files in the pre-commit hook.
|
|
2
|
+
// // .lintstagedrc.mjs
|
|
3
|
+
// export { default } from 'devkit/lint-staged';
|
|
4
|
+
export default {
|
|
5
|
+
'*.{ts,tsx,js,jsx,mjs,cjs}': ['eslint --fix', 'prettier --write'],
|
|
6
|
+
'*.{json,jsonc,css,scss,html,yml,yaml}': ['prettier --write'],
|
|
7
|
+
'*.md': ['markdownlint-cli2 --fix', 'prettier --write'],
|
|
8
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vpnsin/devkit",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "Shared ESLint, Prettier, commitlint, markdownlint, lint-staged, Husky and GitHub CI/release config for Node.js & Next.js repos.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"prettier": "./prettier/index.js",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/vpnsin/devkit.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/vpnsin/devkit#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/vpnsin/devkit/issues"
|
|
14
|
+
},
|
|
15
|
+
"bin": {
|
|
16
|
+
"devkit": "bin/cli.js"
|
|
17
|
+
},
|
|
18
|
+
"exports": {
|
|
19
|
+
"./eslint": "./eslint/base.js",
|
|
20
|
+
"./eslint/base": "./eslint/base.js",
|
|
21
|
+
"./eslint/next": "./eslint/next.js",
|
|
22
|
+
"./prettier": "./prettier/index.js",
|
|
23
|
+
"./commitlint": "./commitlint/index.js",
|
|
24
|
+
"./lint-staged": "./lint-staged/index.js",
|
|
25
|
+
"./jest": "./jest/index.js",
|
|
26
|
+
"./vitest": "./vitest/index.js",
|
|
27
|
+
"./tsconfig/*": "./tsconfig/*",
|
|
28
|
+
"./package.json": "./package.json"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"eslint",
|
|
32
|
+
"prettier",
|
|
33
|
+
"commitlint",
|
|
34
|
+
"lint-staged",
|
|
35
|
+
"jest",
|
|
36
|
+
"vitest",
|
|
37
|
+
"tsconfig",
|
|
38
|
+
"templates",
|
|
39
|
+
"bin",
|
|
40
|
+
"README.md"
|
|
41
|
+
],
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=18.18"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"test": "node --check bin/cli.js && node -e \"import('./eslint/base.js').then(()=>console.log('eslint/base ok'))\""
|
|
47
|
+
},
|
|
48
|
+
"keywords": [
|
|
49
|
+
"eslint-config",
|
|
50
|
+
"prettier-config",
|
|
51
|
+
"commitlint-config",
|
|
52
|
+
"markdownlint",
|
|
53
|
+
"husky",
|
|
54
|
+
"lint-staged",
|
|
55
|
+
"release-please",
|
|
56
|
+
"config",
|
|
57
|
+
"nextjs"
|
|
58
|
+
],
|
|
59
|
+
"license": "MIT",
|
|
60
|
+
"dependencies": {
|
|
61
|
+
"@commitlint/config-conventional": "^21.0.2",
|
|
62
|
+
"@eslint/js": "^10.0.1",
|
|
63
|
+
"eslint-config-prettier": "^10.1.8",
|
|
64
|
+
"eslint-plugin-prettier": "^5.5.4",
|
|
65
|
+
"globals": "^17.6.0",
|
|
66
|
+
"typescript-eslint": "^8.21.0"
|
|
67
|
+
},
|
|
68
|
+
"peerDependencies": {
|
|
69
|
+
"eslint": ">=9",
|
|
70
|
+
"prettier": ">=3"
|
|
71
|
+
},
|
|
72
|
+
"peerDependenciesMeta": {
|
|
73
|
+
"eslint-config-next": {
|
|
74
|
+
"optional": true
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
"publishConfig": {
|
|
78
|
+
"access": "public"
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Shared Prettier config. Reference from a consuming repo via package.json:
|
|
2
|
+
// "prettier": "devkit/prettier"
|
|
3
|
+
export default {
|
|
4
|
+
semi: true,
|
|
5
|
+
tabWidth: 2,
|
|
6
|
+
printWidth: 100,
|
|
7
|
+
singleQuote: true,
|
|
8
|
+
trailingComma: 'es5',
|
|
9
|
+
bracketSameLine: false,
|
|
10
|
+
endOfLine: 'auto',
|
|
11
|
+
};
|