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 +1 -1
- package/src/cli/args.js +3 -0
- package/src/cli/index.js +2 -1
- package/src/cli/wizard.js +3 -0
- package/src/core/features/doctor.js +54 -0
- package/src/core/features/index.js +2 -0
- package/src/core/features/service.js +78 -11
- package/src/core/features/test.js +24 -3
- package/src/core/options.js +9 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-packkit",
|
|
3
|
-
"version": "2.
|
|
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
|
|
2
|
-
// (which listens), adds start/dev scripts,
|
|
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:
|
|
24
|
-
|
|
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
|
|
31
|
-
|
|
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
|
-
|
|
114
|
+
envImport,
|
|
48
115
|
``,
|
|
49
|
-
`const port = ${
|
|
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 +
|
|
107
|
+
api + imports,
|
|
87
108
|
``,
|
|
88
109
|
`describe('app', () => {`,
|
|
89
110
|
`\tit('responds on /', async () => {`,
|
|
90
|
-
`\t\tconst res =
|
|
91
|
-
`\t\texpect(res
|
|
111
|
+
`\t\tconst res = ${call};`,
|
|
112
|
+
`\t\texpect(res.${statusProp}).toBe(200);`,
|
|
92
113
|
`\t});`,
|
|
93
114
|
`});`,
|
|
94
115
|
``,
|
package/src/core/options.js
CHANGED
|
@@ -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: {
|