create-packkit 2.2.0 → 2.4.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.2.0",
3
+ "version": "2.4.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',
@@ -54,6 +55,7 @@ export function parseCliArgs(argv) {
54
55
  canary: { type: 'boolean' },
55
56
  'pkg-checks': { type: 'boolean' },
56
57
  knip: { type: 'boolean' },
58
+ 'size-limit': { type: 'boolean' },
57
59
  jsr: { type: 'boolean' },
58
60
  help: { type: 'boolean', short: 'h' },
59
61
  version: { type: 'boolean', short: 'v' },
@@ -83,6 +85,7 @@ export function parseCliArgs(argv) {
83
85
  if (values.canary) overrides.canary = true;
84
86
  if (values['pkg-checks']) overrides.pkgChecks = true;
85
87
  if (values.knip) overrides.knip = true;
88
+ if (values['size-limit']) overrides.sizeLimit = true;
86
89
  if (values.jsr) overrides.jsr = true;
87
90
  for (const [flag, key] of Object.entries(NEGATABLE)) {
88
91
  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,6 +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
51
+ --size-limit (bundle-size budget, libraries)
50
52
  --no-coverage --no-community --no-agents --no-vscode --no-editorconfig
51
53
  --schema Print the full option/preset schema as JSON (for tools/agents)
52
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
  }
@@ -15,6 +15,7 @@ import githooks from './githooks.js';
15
15
  import release from './release.js';
16
16
  import cli from './cli.js';
17
17
  import checks from './checks.js';
18
+ import sizelimit from './sizelimit.js';
18
19
  import jsr from './jsr.js';
19
20
  import workflows from './workflows.js';
20
21
  import storybook from './storybook.js';
@@ -38,6 +39,7 @@ export default [
38
39
  release,
39
40
  cli,
40
41
  checks,
42
+ sizelimit,
41
43
  jsr,
42
44
  workflows,
43
45
  storybook,
@@ -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
  `});`,
@@ -0,0 +1,23 @@
1
+ // size-limit: a bundle-size budget for published libraries. Measures the
2
+ // brotli-compressed size of the built entry and fails when it exceeds the
3
+ // limit — cheap insurance against a dependency silently bloating the package.
4
+ import { toJson } from '../render.js';
5
+
6
+ export default {
7
+ id: 'sizelimit',
8
+ active: (cfg) => cfg.sizeLimit,
9
+ apply(cfg) {
10
+ return {
11
+ files: {
12
+ '.size-limit.json': toJson([{ name: cfg.name, path: 'dist/index.js', limit: '10 kB' }]),
13
+ },
14
+ pkg: {
15
+ scripts: { size: 'size-limit' },
16
+ devDependencies: {
17
+ 'size-limit': '^11.0.0',
18
+ '@size-limit/preset-small-lib': '^11.0.0',
19
+ },
20
+ },
21
+ };
22
+ },
23
+ };
@@ -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
  ``,
@@ -80,6 +80,7 @@ function ciWorkflow(cfg, codecov) {
80
80
  if (cfg.test !== 'none') jobs.push(` - run: ${pmRun(cfg, codecov ? 'coverage' : 'test')}`);
81
81
  if (cfg.knip) jobs.push(` - run: ${pmRun(cfg, 'knip')}`);
82
82
  if (cfg.hasBuild) jobs.push(` - run: ${pmRun(cfg, 'build')}`);
83
+ if (cfg.sizeLimit) jobs.push(` - run: ${pmRun(cfg, 'size')}`); // after build (measures dist)
83
84
  if (cfg.pkgChecks) jobs.push(` - run: ${pmRun(cfg, 'check:pkg')}`); // after build (attw packs dist)
84
85
  const cov = codecov
85
86
  ? '\n - uses: codecov/codecov-action@v4\n with:\n token: ${{ secrets.CODECOV_TOKEN }}'
@@ -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: [
@@ -102,6 +110,7 @@ export const OPTIONS = {
102
110
  canary: { group: 'release', type: 'boolean', label: 'Snapshot / canary release workflow (Changesets)', default: false },
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 },
113
+ sizeLimit: { group: 'quality', type: 'boolean', label: 'size-limit (bundle-size budget, libraries)', default: false },
105
114
 
106
115
  // ---- lint / format ----
107
116
  lint: {
@@ -260,6 +269,8 @@ export function normalizeConfig(input = {}) {
260
269
  if (!cfg.publishable) cfg.pkgChecks = false;
261
270
  // Sourcemaps + shipped source only matter for a published package.
262
271
  if (!cfg.publishable) cfg.sourcemaps = false;
272
+ // A bundle-size budget needs a published package with a built entry.
273
+ if (!(cfg.publishable && cfg.hasBuild)) cfg.sizeLimit = false;
263
274
  // Env validation is for server-side runtimes (services / CLIs), not libs/apps.
264
275
  if (!(cfg.hasService || cfg.hasCli)) cfg.env = false;
265
276
  // Canary snapshots ride on the Changesets flow.