create-packkit 2.1.0 → 2.2.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 +5 -0
- package/src/cli/index.js +2 -1
- package/src/cli/wizard.js +6 -0
- package/src/core/features/bundler.js +3 -0
- package/src/core/features/env.js +58 -0
- package/src/core/features/index.js +2 -0
- package/src/core/features/service.js +17 -5
- package/src/core/features/typescript.js +4 -0
- package/src/core/features/workflows.js +29 -0
- 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.2.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
|
@@ -23,6 +23,7 @@ const OVERRIDE_FLAGS = {
|
|
|
23
23
|
// Boolean options that default ON — a --no-<flag> turns them off.
|
|
24
24
|
const NEGATABLE = {
|
|
25
25
|
'no-coverage': 'coverage',
|
|
26
|
+
'no-sourcemaps': 'sourcemaps',
|
|
26
27
|
'no-community': 'community',
|
|
27
28
|
'no-agents': 'agents',
|
|
28
29
|
'no-vscode': 'vscode',
|
|
@@ -49,6 +50,8 @@ export function parseCliArgs(argv) {
|
|
|
49
50
|
monorepo: { type: 'boolean' },
|
|
50
51
|
storybook: { type: 'boolean' },
|
|
51
52
|
e2e: { type: 'boolean' },
|
|
53
|
+
env: { type: 'boolean' },
|
|
54
|
+
canary: { type: 'boolean' },
|
|
52
55
|
'pkg-checks': { type: 'boolean' },
|
|
53
56
|
knip: { type: 'boolean' },
|
|
54
57
|
jsr: { type: 'boolean' },
|
|
@@ -76,6 +79,8 @@ export function parseCliArgs(argv) {
|
|
|
76
79
|
if (values.monorepo) overrides.monorepo = true;
|
|
77
80
|
if (values.storybook) overrides.storybook = true;
|
|
78
81
|
if (values.e2e) overrides.e2e = true;
|
|
82
|
+
if (values.env) overrides.env = true;
|
|
83
|
+
if (values.canary) overrides.canary = true;
|
|
79
84
|
if (values['pkg-checks']) overrides.pkgChecks = true;
|
|
80
85
|
if (values.knip) overrides.knip = true;
|
|
81
86
|
if (values.jsr) overrides.jsr = true;
|
package/src/cli/index.js
CHANGED
|
@@ -41,11 +41,12 @@ Options:
|
|
|
41
41
|
--target <library|cli|service|app> (repeatable) --storybook
|
|
42
42
|
--bundler <tsup|tsdown|unbuild|rollup|none> --minify
|
|
43
43
|
--test <vitest|jest|node|none> --e2e (Playwright, for apps)
|
|
44
|
+
--env (type-safe env validation, services/CLIs) --no-sourcemaps
|
|
44
45
|
--lint <eslint-prettier|biome|oxlint|none>
|
|
45
46
|
--hooks <simple-git-hooks|husky|lefthook|none> --release <changesets|release-it|np|none>
|
|
46
47
|
--workflows <ci|npm-publish|pages|codeql|codecov|stale> (repeatable)
|
|
47
48
|
--deps <renovate|dependabot|none> --license <MIT|Apache-2.0|ISC|none>
|
|
48
|
-
--pkg-checks (publint + are-the-types-wrong) --knip --jsr
|
|
49
|
+
--pkg-checks (publint + are-the-types-wrong) --knip --jsr --canary
|
|
49
50
|
--no-coverage --no-community --no-agents --no-vscode --no-editorconfig
|
|
50
51
|
--schema Print the full option/preset schema as JSON (for tools/agents)
|
|
51
52
|
-h, --help Show this help
|
package/src/cli/wizard.js
CHANGED
|
@@ -41,7 +41,13 @@ export async function runWizard(seed = {}) {
|
|
|
41
41
|
}
|
|
42
42
|
cfg.lint = bail(await p.select({ message: 'Lint / format', options: asOptions('lint'), initialValue: OPTIONS.lint.default }));
|
|
43
43
|
cfg.gitHooks = bail(await p.select({ message: 'Git hooks', options: asOptions('gitHooks'), initialValue: OPTIONS.gitHooks.default }));
|
|
44
|
+
if (cfg.target.includes('service') || cfg.target.includes('cli')) {
|
|
45
|
+
cfg.env = bail(await p.confirm({ message: 'Type-safe env validation (Zod)?', initialValue: false }));
|
|
46
|
+
}
|
|
44
47
|
cfg.release = bail(await p.select({ message: 'Release / versioning', options: asOptions('release'), initialValue: OPTIONS.release.default }));
|
|
48
|
+
if (cfg.release === 'changesets') {
|
|
49
|
+
cfg.canary = bail(await p.confirm({ message: 'Add a canary/snapshot release workflow?', initialValue: false }));
|
|
50
|
+
}
|
|
45
51
|
cfg.workflows = bail(await p.multiselect({ message: 'GitHub Actions (space to toggle)', options: asOptions('workflows'), initialValues: OPTIONS.workflows.default, required: false }));
|
|
46
52
|
cfg.deps = bail(await p.select({ message: 'Dependency updates', options: asOptions('deps'), initialValue: OPTIONS.deps.default }));
|
|
47
53
|
cfg.license = bail(await p.select({ message: 'License', options: asOptions('license'), initialValue: OPTIONS.license.default }));
|
|
@@ -18,6 +18,9 @@ export default {
|
|
|
18
18
|
if (cfg.hasEsm) pkg.module = './src/index.js';
|
|
19
19
|
} else {
|
|
20
20
|
pkg.files = ['dist'];
|
|
21
|
+
// Ship source alongside dist so the JS sourcemaps resolve — consumers can
|
|
22
|
+
// step into and go-to-definition on your original code.
|
|
23
|
+
if (cfg.sourcemaps) pkg.files.push('src');
|
|
21
24
|
const esm = './dist/index.js';
|
|
22
25
|
const cjs = './dist/index.cjs';
|
|
23
26
|
const dtsEsm = './dist/index.d.ts';
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Type-safe environment-variable validation for server-side runtimes (services
|
|
2
|
+
// and CLIs). Validates process.env once at startup with a Zod schema, so a
|
|
3
|
+
// misconfigured deploy fails fast with a clear message instead of a surprise
|
|
4
|
+
// runtime crash. Ships a matching .env.example.
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
id: 'env',
|
|
8
|
+
active: (cfg) => cfg.env && (cfg.hasService || cfg.hasCli),
|
|
9
|
+
apply(cfg) {
|
|
10
|
+
const files = {};
|
|
11
|
+
|
|
12
|
+
files[`src/env.${cfg.ext}`] = cfg.isTs ? envTs() : envJs();
|
|
13
|
+
files['.env.example'] = ['NODE_ENV=development', 'PORT=3000', ''].join('\n');
|
|
14
|
+
|
|
15
|
+
return { files, pkg: { dependencies: { zod: '^4.0.0' } } };
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function envTs() {
|
|
20
|
+
return [
|
|
21
|
+
`import { z } from 'zod';`,
|
|
22
|
+
``,
|
|
23
|
+
`const schema = z.object({`,
|
|
24
|
+
`\tNODE_ENV: z.enum(['development', 'production', 'test']).default('development'),`,
|
|
25
|
+
`\tPORT: z.coerce.number().default(3000),`,
|
|
26
|
+
`});`,
|
|
27
|
+
``,
|
|
28
|
+
`const parsed = schema.safeParse(process.env);`,
|
|
29
|
+
`if (!parsed.success) {`,
|
|
30
|
+
`\tconsole.error('❌ Invalid environment variables:', z.treeifyError(parsed.error));`,
|
|
31
|
+
`\tprocess.exit(1);`,
|
|
32
|
+
`}`,
|
|
33
|
+
``,
|
|
34
|
+
`export const env = parsed.data;`,
|
|
35
|
+
``,
|
|
36
|
+
].join('\n');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function envJs() {
|
|
40
|
+
return [
|
|
41
|
+
`import { z } from 'zod';`,
|
|
42
|
+
``,
|
|
43
|
+
`const schema = z.object({`,
|
|
44
|
+
`\tNODE_ENV: z.enum(['development', 'production', 'test']).default('development'),`,
|
|
45
|
+
`\tPORT: z.coerce.number().default(3000),`,
|
|
46
|
+
`});`,
|
|
47
|
+
``,
|
|
48
|
+
`const parsed = schema.safeParse(process.env);`,
|
|
49
|
+
`if (!parsed.success) {`,
|
|
50
|
+
`\tconsole.error('❌ Invalid environment variables:', z.treeifyError(parsed.error));`,
|
|
51
|
+
`\tprocess.exit(1);`,
|
|
52
|
+
`}`,
|
|
53
|
+
``,
|
|
54
|
+
`/** @type {z.infer<typeof schema>} */`,
|
|
55
|
+
`export const env = parsed.data;`,
|
|
56
|
+
``,
|
|
57
|
+
].join('\n');
|
|
58
|
+
}
|
|
@@ -7,6 +7,7 @@ import typescript from './typescript.js';
|
|
|
7
7
|
import frameworks from './frameworks.js';
|
|
8
8
|
import vite from './vite.js';
|
|
9
9
|
import service from './service.js';
|
|
10
|
+
import env from './env.js';
|
|
10
11
|
import test from './test.js';
|
|
11
12
|
import e2e from './e2e.js';
|
|
12
13
|
import lint from './lint.js';
|
|
@@ -29,6 +30,7 @@ export default [
|
|
|
29
30
|
frameworks,
|
|
30
31
|
vite,
|
|
31
32
|
service,
|
|
33
|
+
env,
|
|
32
34
|
test,
|
|
33
35
|
e2e,
|
|
34
36
|
lint,
|
|
@@ -10,7 +10,7 @@ export default {
|
|
|
10
10
|
[`src/app.${ext}`]: appFile(cfg),
|
|
11
11
|
[`src/index.${ext}`]: serverFile(cfg),
|
|
12
12
|
Dockerfile: dockerfile(cfg),
|
|
13
|
-
'.dockerignore': 'node_modules
|
|
13
|
+
'.dockerignore': ['node_modules', 'dist', 'coverage', '.git', '.github', '.env', '.env.*', '!.env.example', '*.log', 'Dockerfile', '.dockerignore', ''].join('\n'),
|
|
14
14
|
};
|
|
15
15
|
return {
|
|
16
16
|
files,
|
|
@@ -43,22 +43,25 @@ function appFile(cfg) {
|
|
|
43
43
|
function serverFile(cfg) {
|
|
44
44
|
return [
|
|
45
45
|
`import { serve } from '@hono/node-server';`,
|
|
46
|
-
`import { app } from './app
|
|
46
|
+
`import { app } from './app.js';`,
|
|
47
|
+
cfg.env ? `import { env } from './env.js';` : null,
|
|
47
48
|
``,
|
|
48
|
-
`const port = Number(process.env.PORT) || 3000;`,
|
|
49
|
+
`const port = ${cfg.env ? 'env.PORT' : 'Number(process.env.PORT) || 3000'};`,
|
|
49
50
|
`serve({ fetch: app.fetch, port }, (info) => {`,
|
|
50
51
|
`\tconsole.log(\`Listening on http://localhost:\${info.port}\`);`,
|
|
51
52
|
`});`,
|
|
52
53
|
``,
|
|
53
|
-
].join('\n');
|
|
54
|
+
].filter((l) => l !== null).join('\n');
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
function dockerfile(cfg) {
|
|
57
58
|
const node = cfg.nodeVersion;
|
|
58
59
|
const pm = cfg.packageManager;
|
|
59
60
|
const install = pm === 'npm' ? 'npm ci' : `${pm} install --frozen-lockfile`;
|
|
61
|
+
const prune = pm === 'npm' ? 'npm ci --omit=dev' : `${pm} install --prod --frozen-lockfile`;
|
|
60
62
|
const build = pm === 'npm' ? 'npm run build' : `${pm} run build`;
|
|
61
63
|
return [
|
|
64
|
+
`# --- build stage: install everything and compile ---`,
|
|
62
65
|
`FROM node:${node}-slim AS build`,
|
|
63
66
|
`WORKDIR /app`,
|
|
64
67
|
`COPY package*.json ./`,
|
|
@@ -66,13 +69,22 @@ function dockerfile(cfg) {
|
|
|
66
69
|
`COPY . .`,
|
|
67
70
|
`RUN ${build}`,
|
|
68
71
|
``,
|
|
72
|
+
`# --- deps stage: production-only node_modules for a smaller image ---`,
|
|
73
|
+
`FROM node:${node}-slim AS deps`,
|
|
74
|
+
`WORKDIR /app`,
|
|
75
|
+
`COPY package*.json ./`,
|
|
76
|
+
`RUN ${prune}`,
|
|
77
|
+
``,
|
|
78
|
+
`# --- runtime stage: slim, non-root, healthchecked ---`,
|
|
69
79
|
`FROM node:${node}-slim`,
|
|
70
80
|
`WORKDIR /app`,
|
|
71
81
|
`ENV NODE_ENV=production`,
|
|
72
|
-
`COPY --from=
|
|
82
|
+
`COPY --from=deps /app/node_modules ./node_modules`,
|
|
73
83
|
`COPY --from=build /app/dist ./dist`,
|
|
74
84
|
`COPY package.json ./`,
|
|
85
|
+
`USER node`,
|
|
75
86
|
`EXPOSE 3000`,
|
|
87
|
+
`HEALTHCHECK --interval=30s --timeout=3s CMD node -e "fetch('http://localhost:'+(process.env.PORT||3000)+'/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"`,
|
|
76
88
|
`CMD ["node", "dist/index.js"]`,
|
|
77
89
|
``,
|
|
78
90
|
].join('\n');
|
|
@@ -19,6 +19,9 @@ export default {
|
|
|
19
19
|
forceConsistentCasingInFileNames: true,
|
|
20
20
|
verbatimModuleSyntax: cfg.bundler !== 'none' && !cfg.hasFramework,
|
|
21
21
|
declaration: true,
|
|
22
|
+
// Declaration maps let editors jump from the published .d.ts into the
|
|
23
|
+
// shipped .ts source (the bundler emits JS sourcemaps to match).
|
|
24
|
+
...(cfg.sourcemaps ? { declarationMap: true } : {}),
|
|
22
25
|
};
|
|
23
26
|
if (noBuild) {
|
|
24
27
|
// tsc is the build: emit to dist.
|
|
@@ -26,6 +29,7 @@ export default {
|
|
|
26
29
|
compilerOptions.module = 'NodeNext';
|
|
27
30
|
compilerOptions.outDir = 'dist';
|
|
28
31
|
compilerOptions.rootDir = 'src';
|
|
32
|
+
if (cfg.sourcemaps) compilerOptions.sourceMap = true;
|
|
29
33
|
} else {
|
|
30
34
|
compilerOptions.noEmit = true;
|
|
31
35
|
}
|
|
@@ -12,6 +12,10 @@ function pmInstall(cfg) {
|
|
|
12
12
|
function pmRun(cfg, script) {
|
|
13
13
|
return cfg.packageManager === 'npm' ? `npm run ${script}` : `${cfg.packageManager} ${script}`;
|
|
14
14
|
}
|
|
15
|
+
// Run a package binary (not an npm script) with the right runner per pm.
|
|
16
|
+
function pmExec(cfg, cmd) {
|
|
17
|
+
return { npm: `npx ${cmd}`, pnpm: `pnpm exec ${cmd}`, yarn: `yarn ${cmd}`, bun: `bunx ${cmd}` }[cfg.packageManager];
|
|
18
|
+
}
|
|
15
19
|
function setupSteps(cfg) {
|
|
16
20
|
const steps = [' - uses: actions/checkout@v4'];
|
|
17
21
|
if (cfg.packageManager === 'pnpm') steps.push(' - uses: pnpm/action-setup@v4');
|
|
@@ -42,6 +46,7 @@ export default {
|
|
|
42
46
|
if (wf.includes('codeql')) files['.github/workflows/codeql.yml'] = codeqlWorkflow();
|
|
43
47
|
if (wf.includes('stale')) files['.github/workflows/stale.yml'] = staleWorkflow();
|
|
44
48
|
if (cfg.e2e && cfg.hasApp && wf.includes('ci')) files['.github/workflows/e2e.yml'] = e2eWorkflow(cfg);
|
|
49
|
+
if (cfg.canary && cfg.release === 'changesets') files['.github/workflows/canary.yml'] = canaryWorkflow(cfg);
|
|
45
50
|
|
|
46
51
|
if (cfg.deps === 'renovate') {
|
|
47
52
|
files['.github/renovate.json'] = toJson({
|
|
@@ -224,6 +229,30 @@ function e2eWorkflow(cfg) {
|
|
|
224
229
|
].join('\n');
|
|
225
230
|
}
|
|
226
231
|
|
|
232
|
+
function canaryWorkflow(cfg) {
|
|
233
|
+
return [
|
|
234
|
+
'name: Canary',
|
|
235
|
+
'# Manually publish a snapshot (x.y.z-canary-<hash>) to the `canary` dist-tag',
|
|
236
|
+
'# so consumers can test unreleased changes: npm i ' + cfg.name + '@canary',
|
|
237
|
+
'on:',
|
|
238
|
+
' workflow_dispatch:',
|
|
239
|
+
'concurrency: canary-${{ github.ref }}',
|
|
240
|
+
'permissions:',
|
|
241
|
+
' contents: read',
|
|
242
|
+
'jobs:',
|
|
243
|
+
' canary:',
|
|
244
|
+
' runs-on: ubuntu-latest',
|
|
245
|
+
' steps:',
|
|
246
|
+
setupSteps(cfg),
|
|
247
|
+
' - name: Authenticate with npm',
|
|
248
|
+
' run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" >> ~/.npmrc',
|
|
249
|
+
` - run: ${pmExec(cfg, 'changeset version --snapshot canary')}`,
|
|
250
|
+
cfg.hasBuild ? ` - run: ${pmRun(cfg, 'build')}` : null,
|
|
251
|
+
` - run: ${pmExec(cfg, 'changeset publish --no-git-tag --tag canary')}`,
|
|
252
|
+
'',
|
|
253
|
+
].filter((l) => l !== null).join('\n');
|
|
254
|
+
}
|
|
255
|
+
|
|
227
256
|
function staleWorkflow() {
|
|
228
257
|
return [
|
|
229
258
|
'name: Stale',
|
package/src/core/options.js
CHANGED
|
@@ -97,6 +97,9 @@ export const OPTIONS = {
|
|
|
97
97
|
coverage: { group: 'quality', type: 'boolean', label: 'Coverage reporting', default: true },
|
|
98
98
|
storybook: { group: 'quality', type: 'boolean', label: 'Storybook (component libraries)', default: false },
|
|
99
99
|
e2e: { group: 'quality', type: 'boolean', label: 'Playwright end-to-end tests (apps)', default: false },
|
|
100
|
+
sourcemaps: { group: 'build', type: 'boolean', label: 'Sourcemaps + ship source (debug into original code)', default: true },
|
|
101
|
+
env: { group: 'quality', type: 'boolean', label: 'Type-safe env validation (Zod) — services & CLIs', default: false },
|
|
102
|
+
canary: { group: 'release', type: 'boolean', label: 'Snapshot / canary release workflow (Changesets)', default: false },
|
|
100
103
|
pkgChecks: { group: 'quality', type: 'boolean', label: 'Package checks (publint + are-the-types-wrong)', default: false },
|
|
101
104
|
knip: { group: 'quality', type: 'boolean', label: 'Knip (unused files / deps / exports)', default: false },
|
|
102
105
|
|
|
@@ -255,6 +258,12 @@ export function normalizeConfig(input = {}) {
|
|
|
255
258
|
cfg.publishable = (cfg.hasLibrary || cfg.hasCli) && !cfg.hasApp && !cfg.hasService;
|
|
256
259
|
// Package-correctness checks only make sense for a publishable package.
|
|
257
260
|
if (!cfg.publishable) cfg.pkgChecks = false;
|
|
261
|
+
// Sourcemaps + shipped source only matter for a published package.
|
|
262
|
+
if (!cfg.publishable) cfg.sourcemaps = false;
|
|
263
|
+
// Env validation is for server-side runtimes (services / CLIs), not libs/apps.
|
|
264
|
+
if (!(cfg.hasService || cfg.hasCli)) cfg.env = false;
|
|
265
|
+
// Canary snapshots ride on the Changesets flow.
|
|
266
|
+
if (cfg.release !== 'changesets') cfg.canary = false;
|
|
258
267
|
// JSR is TypeScript-first, ESM, and for plain (non-framework) libraries.
|
|
259
268
|
if (!(cfg.isTs && cfg.hasLibrary && !cfg.hasFramework && !cfg.hasApp)) cfg.jsr = false;
|
|
260
269
|
return cfg;
|