create-packkit 2.3.0 → 2.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-packkit",
3
- "version": "2.3.0",
3
+ "version": "2.5.0",
4
4
  "description": "Highly configurable scaffolder for modern npm packages and CLIs — pick your stack (TS/JS, bundler, tests, linter, CI, releases) from a CLI or a web configurator.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli/args.js CHANGED
@@ -15,6 +15,7 @@ const OVERRIDE_FLAGS = {
15
15
  license: 'license',
16
16
  pm: 'packageManager',
17
17
  node: 'nodeVersion',
18
+ server: 'serviceFramework',
18
19
  author: 'author',
19
20
  description: 'description',
20
21
  repo: 'repo',
@@ -55,6 +56,7 @@ export function parseCliArgs(argv) {
55
56
  'pkg-checks': { type: 'boolean' },
56
57
  knip: { type: 'boolean' },
57
58
  'size-limit': { type: 'boolean' },
59
+ doctor: { type: 'boolean' },
58
60
  jsr: { type: 'boolean' },
59
61
  help: { type: 'boolean', short: 'h' },
60
62
  version: { type: 'boolean', short: 'v' },
@@ -85,6 +87,7 @@ export function parseCliArgs(argv) {
85
87
  if (values['pkg-checks']) overrides.pkgChecks = true;
86
88
  if (values.knip) overrides.knip = true;
87
89
  if (values['size-limit']) overrides.sizeLimit = true;
90
+ if (values.doctor) overrides.doctor = true;
88
91
  if (values.jsr) overrides.jsr = true;
89
92
  for (const [flag, key] of Object.entries(NEGATABLE)) {
90
93
  if (values[flag]) overrides[key] = false;
package/src/cli/index.js CHANGED
@@ -39,6 +39,7 @@ Options:
39
39
  --pm <manager> npm | pnpm | yarn | bun
40
40
  --language <ts|js> --module <esm|cjs|dual> --framework <none|react|vue|svelte>
41
41
  --target <library|cli|service|app> (repeatable) --storybook
42
+ --server <hono|fastify|express> (HTTP service framework)
42
43
  --bundler <tsup|tsdown|unbuild|rollup|none> --minify
43
44
  --test <vitest|jest|node|none> --e2e (Playwright, for apps)
44
45
  --env (type-safe env validation, services/CLIs) --no-sourcemaps
@@ -47,7 +48,7 @@ Options:
47
48
  --workflows <ci|npm-publish|pages|codeql|codecov|stale> (repeatable)
48
49
  --deps <renovate|dependabot|none> --license <MIT|Apache-2.0|ISC|none>
49
50
  --pkg-checks (publint + are-the-types-wrong) --knip --jsr --canary
50
- --size-limit (bundle-size budget, libraries)
51
+ --size-limit (bundle-size budget, libraries) --doctor (env check)
51
52
  --no-coverage --no-community --no-agents --no-vscode --no-editorconfig
52
53
  --schema Print the full option/preset schema as JSON (for tools/agents)
53
54
  -h, --help Show this help
package/src/cli/wizard.js CHANGED
@@ -36,6 +36,9 @@ export async function runWizard(seed = {}) {
36
36
  cfg.minify = bail(await p.confirm({ message: 'Minify the build output?', initialValue: false }));
37
37
  }
38
38
  cfg.test = bail(await p.select({ message: 'Test runner', options: asOptions('test'), initialValue: OPTIONS.test.default }));
39
+ if (cfg.target.includes('service')) {
40
+ cfg.serviceFramework = bail(await p.select({ message: 'Service framework', options: asOptions('serviceFramework'), initialValue: OPTIONS.serviceFramework.default }));
41
+ }
39
42
  if (cfg.target.includes('app')) {
40
43
  cfg.e2e = bail(await p.confirm({ message: 'Add Playwright end-to-end tests?', initialValue: false }));
41
44
  }
@@ -0,0 +1,54 @@
1
+ // A tiny environment doctor: checks the local Node and package manager match
2
+ // what this project expects, and warns (never fails) otherwise. `npm run doctor`
3
+ // runs it any time; for private projects it also runs on postinstall. It is NOT
4
+ // wired to postinstall for publishable packages — that would run in every
5
+ // consumer's install.
6
+
7
+ export default {
8
+ id: 'doctor',
9
+ active: (cfg) => cfg.doctor && !cfg.monorepo,
10
+ apply(cfg) {
11
+ const files = { 'scripts/doctor.mjs': script(cfg) };
12
+ const pkg = { scripts: { doctor: 'node scripts/doctor.mjs' } };
13
+ if (!cfg.publishable) pkg.scripts.postinstall = 'node scripts/doctor.mjs';
14
+ return { files, pkg };
15
+ },
16
+ };
17
+
18
+ function script(cfg) {
19
+ return [
20
+ `/* global process, console, URL */`,
21
+ `// Checks your environment matches this project. Warns only — never fails.`,
22
+ `import { readFileSync } from 'node:fs';`,
23
+ ``,
24
+ `const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));`,
25
+ `const gte = (a, b) => {`,
26
+ `\tconst pa = String(a).split('.').map(Number);`,
27
+ `\tconst pb = String(b).split('.').map(Number);`,
28
+ `\tfor (let i = 0; i < 3; i++) if ((pa[i] || 0) !== (pb[i] || 0)) return (pa[i] || 0) > (pb[i] || 0);`,
29
+ `\treturn true;`,
30
+ `};`,
31
+ ``,
32
+ `let issues = 0;`,
33
+ `const floor = (pkg.engines?.node || '').replace(/[^0-9.]/g, '');`,
34
+ `const node = process.versions.node;`,
35
+ `if (floor && !gte(node, floor)) {`,
36
+ "\tconsole.warn(`⚠ Node ${node} is below the required >=${floor} — run: nvm install ${floor.split('.')[0]}`);",
37
+ `\tissues++;`,
38
+ `} else {`,
39
+ "\tconsole.log(`✓ Node ${node}`);",
40
+ `}`,
41
+ ``,
42
+ `const expectedPm = '${cfg.packageManager}';`,
43
+ `const usingPm = (process.env.npm_config_user_agent || '').split('/')[0];`,
44
+ `if (usingPm && usingPm !== expectedPm) {`,
45
+ "\tconsole.warn(`⚠ This project uses ${expectedPm}, but you ran ${usingPm}.`);",
46
+ `\tissues++;`,
47
+ `} else {`,
48
+ "\tconsole.log(`✓ Package manager: ${expectedPm}`);",
49
+ `}`,
50
+ ``,
51
+ `if (issues) console.warn('\\nSee the README "Requirements" section to fix these.');`,
52
+ ``,
53
+ ].join('\n');
54
+ }
@@ -22,6 +22,7 @@ import storybook from './storybook.js';
22
22
  import community from './community.js';
23
23
  import agents from './agents.js';
24
24
  import vscode from './vscode.js';
25
+ import doctor from './doctor.js';
25
26
  import gitfiles from './gitfiles.js';
26
27
 
27
28
  export default [
@@ -46,5 +47,6 @@ export default [
46
47
  community,
47
48
  agents,
48
49
  vscode,
50
+ doctor,
49
51
  gitfiles,
50
52
  ];
@@ -1,14 +1,16 @@
1
- // HTTP service target (Hono). Splits the app (testable) from the server entry
2
- // (which listens), adds start/dev scripts, and a Dockerfile.
1
+ // HTTP service target. Supports Hono (default), Fastify and Express. Splits the
2
+ // app (testable) from the server entry (which listens), adds start/dev scripts,
3
+ // and a production Dockerfile.
3
4
 
4
5
  export default {
5
6
  id: 'service',
6
7
  active: (cfg) => cfg.hasService,
7
8
  apply(cfg) {
8
9
  const ext = cfg.ext;
10
+ const fw = cfg.serviceFramework || 'hono';
9
11
  const files = {
10
- [`src/app.${ext}`]: appFile(cfg),
11
- [`src/index.${ext}`]: serverFile(cfg),
12
+ [`src/app.${ext}`]: appFile(cfg, fw),
13
+ [`src/index.${ext}`]: serverFile(cfg, fw),
12
14
  Dockerfile: dockerfile(cfg),
13
15
  '.dockerignore': ['node_modules', 'dist', 'coverage', '.git', '.github', '.env', '.env.*', '!.env.example', '*.log', 'Dockerfile', '.dockerignore', ''].join('\n'),
14
16
  };
@@ -20,15 +22,50 @@ export default {
20
22
  start: 'node dist/index.js',
21
23
  dev: cfg.isTs ? 'tsx watch src/index.ts' : 'node --watch src/index.js',
22
24
  },
23
- dependencies: { hono: '^4.5.0', '@hono/node-server': '^2.0.0' },
24
- ...(cfg.isTs ? { devDependencies: { tsx: '^4.0.0' } } : {}),
25
+ dependencies: deps(fw),
26
+ devDependencies: {
27
+ ...(cfg.isTs ? { tsx: '^4.0.0' } : {}),
28
+ ...(cfg.isTs ? typeDeps(fw) : {}),
29
+ },
25
30
  },
26
31
  };
27
32
  },
28
33
  };
29
34
 
30
- function appFile(cfg) {
31
- const t = cfg.isTs;
35
+ function deps(fw) {
36
+ if (fw === 'fastify') return { fastify: '^5.0.0' };
37
+ if (fw === 'express') return { express: '^5.0.0' };
38
+ return { hono: '^4.5.0', '@hono/node-server': '^2.0.0' };
39
+ }
40
+
41
+ function typeDeps(fw) {
42
+ if (fw === 'express') return { '@types/express': '^5.0.0' };
43
+ return {};
44
+ }
45
+
46
+ function appFile(cfg, fw) {
47
+ if (fw === 'fastify') {
48
+ return [
49
+ `import Fastify from 'fastify';`,
50
+ ``,
51
+ `export const app = Fastify();`,
52
+ ``,
53
+ `app.get('/', async () => ({ ok: true, service: '${cfg.name}' }));`,
54
+ `app.get('/health', async () => 'ok');`,
55
+ ``,
56
+ ].join('\n');
57
+ }
58
+ if (fw === 'express') {
59
+ return [
60
+ `import express from 'express';`,
61
+ ``,
62
+ `export const app = express();`,
63
+ ``,
64
+ `app.get('/', (_req, res) => res.json({ ok: true, service: '${cfg.name}' }));`,
65
+ `app.get('/health', (_req, res) => res.send('ok'));`,
66
+ ``,
67
+ ].join('\n');
68
+ }
32
69
  return [
33
70
  `import { Hono } from 'hono';`,
34
71
  ``,
@@ -40,13 +77,43 @@ function appFile(cfg) {
40
77
  ].join('\n');
41
78
  }
42
79
 
43
- function serverFile(cfg) {
80
+ function serverFile(cfg, fw) {
81
+ const port = cfg.env ? 'env.PORT' : 'Number(process.env.PORT) || 3000';
82
+ const envImport = cfg.env ? `import { env } from './env.js';` : null;
83
+
84
+ if (fw === 'fastify') {
85
+ return [
86
+ `import { app } from './app.js';`,
87
+ envImport,
88
+ ``,
89
+ `const port = ${port};`,
90
+ `app.listen({ port, host: '0.0.0.0' }).then((url) => {`,
91
+ `\tconsole.log(\`Listening on \${url}\`);`,
92
+ `}).catch((err) => {`,
93
+ `\tapp.log.error(err);`,
94
+ `\tprocess.exit(1);`,
95
+ `});`,
96
+ ``,
97
+ ].filter((l) => l !== null).join('\n');
98
+ }
99
+ if (fw === 'express') {
100
+ return [
101
+ `import { app } from './app.js';`,
102
+ envImport,
103
+ ``,
104
+ `const port = ${port};`,
105
+ `app.listen(port, () => {`,
106
+ `\tconsole.log(\`Listening on http://localhost:\${port}\`);`,
107
+ `});`,
108
+ ``,
109
+ ].filter((l) => l !== null).join('\n');
110
+ }
44
111
  return [
45
112
  `import { serve } from '@hono/node-server';`,
46
113
  `import { app } from './app.js';`,
47
- cfg.env ? `import { env } from './env.js';` : null,
114
+ envImport,
48
115
  ``,
49
- `const port = ${cfg.env ? 'env.PORT' : 'Number(process.env.PORT) || 3000'};`,
116
+ `const port = ${port};`,
50
117
  `serve({ fetch: app.fetch, port }, (info) => {`,
51
118
  `\tconsole.log(\`Listening on http://localhost:\${info.port}\`);`,
52
119
  `});`,
@@ -68,6 +68,12 @@ export default {
68
68
  files[`src/index.test.${testExt}`] = exampleTest('node', cfg);
69
69
  }
70
70
 
71
+ // Express services test through supertest (no built-in inject).
72
+ if (cfg.hasService && cfg.serviceFramework === 'express') {
73
+ pkg.devDependencies.supertest = '^7.0.0';
74
+ if (cfg.isTs) pkg.devDependencies['@types/supertest'] = '^6.0.0';
75
+ }
76
+
71
77
  return { files, pkg };
72
78
  },
73
79
  };
@@ -82,13 +88,28 @@ function exampleTest(runner, cfg) {
82
88
  const imp = importPath(runner, cfg);
83
89
  if (cfg.hasService) {
84
90
  const api = runner === 'jest' ? '' : `import { describe, it, expect } from 'vitest';\n`;
91
+ const fw = cfg.serviceFramework || 'hono';
92
+ let imports, call, statusProp;
93
+ if (fw === 'fastify') {
94
+ imports = `import { app } from './app.js';`;
95
+ call = `await app.inject({ method: 'GET', url: '/' })`;
96
+ statusProp = 'statusCode';
97
+ } else if (fw === 'express') {
98
+ imports = `import request from 'supertest';\nimport { app } from './app.js';`;
99
+ call = `await request(app).get('/')`;
100
+ statusProp = 'status';
101
+ } else {
102
+ imports = `import { app } from './app.js';`;
103
+ call = `await app.request('/')`;
104
+ statusProp = 'status';
105
+ }
85
106
  return [
86
- api + `import { app } from './app.js';`,
107
+ api + imports,
87
108
  ``,
88
109
  `describe('app', () => {`,
89
110
  `\tit('responds on /', async () => {`,
90
- `\t\tconst res = await app.request('/');`,
91
- `\t\texpect(res.status).toBe(200);`,
111
+ `\t\tconst res = ${call};`,
112
+ `\t\texpect(res.${statusProp}).toBe(200);`,
92
113
  `\t});`,
93
114
  `});`,
94
115
  ``,
@@ -34,6 +34,14 @@ export const OPTIONS = {
34
34
  { value: 'cjs', label: 'CommonJS only' },
35
35
  ],
36
36
  },
37
+ serviceFramework: {
38
+ group: 'core', type: 'select', label: 'Service framework (HTTP service)', default: 'hono',
39
+ choices: [
40
+ { value: 'hono', label: 'Hono (fast, web-standard)' },
41
+ { value: 'fastify', label: 'Fastify' },
42
+ { value: 'express', label: 'Express' },
43
+ ],
44
+ },
37
45
  target: {
38
46
  group: 'core', type: 'multiselect', label: 'What are you building?', default: ['library'],
39
47
  choices: [
@@ -103,6 +111,7 @@ export const OPTIONS = {
103
111
  pkgChecks: { group: 'quality', type: 'boolean', label: 'Package checks (publint + are-the-types-wrong)', default: false },
104
112
  knip: { group: 'quality', type: 'boolean', label: 'Knip (unused files / deps / exports)', default: false },
105
113
  sizeLimit: { group: 'quality', type: 'boolean', label: 'size-limit (bundle-size budget, libraries)', default: false },
114
+ doctor: { group: 'quality', type: 'boolean', label: 'Env doctor (warn on Node / package-manager mismatch)', default: false },
106
115
 
107
116
  // ---- lint / format ----
108
117
  lint: {