create-packkit 2.0.3 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-packkit",
3
- "version": "2.0.3",
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',
@@ -48,6 +49,9 @@ export function parseCliArgs(argv) {
48
49
  minify: { type: 'boolean' },
49
50
  monorepo: { type: 'boolean' },
50
51
  storybook: { type: 'boolean' },
52
+ e2e: { type: 'boolean' },
53
+ env: { type: 'boolean' },
54
+ canary: { type: 'boolean' },
51
55
  'pkg-checks': { type: 'boolean' },
52
56
  knip: { type: 'boolean' },
53
57
  jsr: { type: 'boolean' },
@@ -74,6 +78,9 @@ export function parseCliArgs(argv) {
74
78
  if (values.minify) overrides.minify = true;
75
79
  if (values.monorepo) overrides.monorepo = true;
76
80
  if (values.storybook) overrides.storybook = true;
81
+ if (values.e2e) overrides.e2e = true;
82
+ if (values.env) overrides.env = true;
83
+ if (values.canary) overrides.canary = true;
77
84
  if (values['pkg-checks']) overrides.pkgChecks = true;
78
85
  if (values.knip) overrides.knip = true;
79
86
  if (values.jsr) overrides.jsr = true;
package/src/cli/index.js CHANGED
@@ -40,11 +40,13 @@ Options:
40
40
  --language <ts|js> --module <esm|cjs|dual> --framework <none|react|vue|svelte>
41
41
  --target <library|cli|service|app> (repeatable) --storybook
42
42
  --bundler <tsup|tsdown|unbuild|rollup|none> --minify
43
- --test <vitest|jest|node|none> --lint <eslint-prettier|biome|oxlint|none>
43
+ --test <vitest|jest|node|none> --e2e (Playwright, for apps)
44
+ --env (type-safe env validation, services/CLIs) --no-sourcemaps
45
+ --lint <eslint-prettier|biome|oxlint|none>
44
46
  --hooks <simple-git-hooks|husky|lefthook|none> --release <changesets|release-it|np|none>
45
47
  --workflows <ci|npm-publish|pages|codeql|codecov|stale> (repeatable)
46
48
  --deps <renovate|dependabot|none> --license <MIT|Apache-2.0|ISC|none>
47
- --pkg-checks (publint + are-the-types-wrong) --knip --jsr
49
+ --pkg-checks (publint + are-the-types-wrong) --knip --jsr --canary
48
50
  --no-coverage --no-community --no-agents --no-vscode --no-editorconfig
49
51
  --schema Print the full option/preset schema as JSON (for tools/agents)
50
52
  -h, --help Show this help
package/src/cli/wizard.js CHANGED
@@ -36,9 +36,18 @@ 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('app')) {
40
+ cfg.e2e = bail(await p.confirm({ message: 'Add Playwright end-to-end tests?', initialValue: false }));
41
+ }
39
42
  cfg.lint = bail(await p.select({ message: 'Lint / format', options: asOptions('lint'), initialValue: OPTIONS.lint.default }));
40
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
+ }
41
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
+ }
42
51
  cfg.workflows = bail(await p.multiselect({ message: 'GitHub Actions (space to toggle)', options: asOptions('workflows'), initialValues: OPTIONS.workflows.default, required: false }));
43
52
  cfg.deps = bail(await p.select({ message: 'Dependency updates', options: asOptions('deps'), initialValue: OPTIONS.deps.default }));
44
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,47 @@
1
+ // Playwright end-to-end tests for app targets: a config that boots the Vite
2
+ // dev server, one smoke spec, the scripts and the dev dependency. The CI job
3
+ // lives in workflows.js, which owns all workflow YAML.
4
+
5
+ const DEV_URL = 'http://localhost:5173'; // Vite's default dev port
6
+
7
+ const runDev = (cfg) => (cfg.packageManager === 'npm' ? 'npm run dev' : `${cfg.packageManager} dev`);
8
+
9
+ export default {
10
+ id: 'e2e',
11
+ active: (cfg) => cfg.e2e && cfg.hasApp,
12
+ apply(cfg) {
13
+ const ext = cfg.ext; // 'ts' | 'js' — config/specs never need JSX
14
+ const files = {};
15
+ const pkg = { scripts: {}, devDependencies: { '@playwright/test': '^1.50.0' } };
16
+
17
+ files[`playwright.config.${ext}`] = [
18
+ `import { defineConfig, devices } from '@playwright/test';`,
19
+ ``,
20
+ `export default defineConfig({`,
21
+ `\ttestDir: './e2e',`,
22
+ `\tuse: { baseURL: '${DEV_URL}', trace: 'on-first-retry' },`,
23
+ `\t// Playwright starts the app for you and waits for it to be ready.`,
24
+ `\twebServer: {`,
25
+ `\t\tcommand: '${runDev(cfg)}',`,
26
+ `\t\turl: '${DEV_URL}',`,
27
+ `\t\treuseExistingServer: !process.env.CI,`,
28
+ `\t},`,
29
+ `\tprojects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }],`,
30
+ `});`,
31
+ ``,
32
+ ].join('\n');
33
+
34
+ files[`e2e/app.spec.${ext}`] = [
35
+ `import { test, expect } from '@playwright/test';`,
36
+ ``,
37
+ `test('renders the app', async ({ page }) => {`,
38
+ `\tawait page.goto('/');`,
39
+ `\tawait expect(page.getByRole('heading', { name: /Hello from/ })).toBeVisible();`,
40
+ `});`,
41
+ ``,
42
+ ].join('\n');
43
+
44
+ pkg.scripts['test:e2e'] = 'playwright test';
45
+ return { files, pkg };
46
+ },
47
+ };
@@ -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
+ }
@@ -10,6 +10,11 @@ dist/
10
10
  coverage/
11
11
  *.tsbuildinfo
12
12
 
13
+ # test artifacts (Playwright)
14
+ test-results/
15
+ playwright-report/
16
+ playwright/.cache/
17
+
13
18
  # logs
14
19
  *.log
15
20
  npm-debug.log*
@@ -7,7 +7,9 @@ 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';
12
+ import e2e from './e2e.js';
11
13
  import lint from './lint.js';
12
14
  import githooks from './githooks.js';
13
15
  import release from './release.js';
@@ -28,7 +30,9 @@ export default [
28
30
  frameworks,
29
31
  vite,
30
32
  service,
33
+ env,
31
34
  test,
35
+ e2e,
32
36
  lint,
33
37
  githooks,
34
38
  release,
@@ -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\ndist\n.git\n.env\n',
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${cfg.isTs ? '.js' : '.js'}';`,
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=build /app/node_modules ./node_modules`,
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');
@@ -26,6 +26,9 @@ export default {
26
26
  `export default defineConfig({`,
27
27
  fw ? `\tplugins: [${fw.call}],` : null,
28
28
  `\ttest: {`,
29
+ // Keep Vitest to unit tests under src/ so it never tries to run the
30
+ // Playwright specs in e2e/ (they share the *.spec.ts glob).
31
+ cfg.e2e ? `\t\tinclude: ['src/**/*.{test,spec}.{js,jsx,ts,tsx}'],` : null,
29
32
  cfg.hasFramework ? `\t\tenvironment: 'jsdom',` : null,
30
33
  cfg.hasFramework ? `\t\tglobals: true,` : null,
31
34
  cfg.coverage ? `\t\tcoverage: { provider: 'v8', reporter: ['text', 'lcov'] },` : null,
@@ -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');
@@ -41,6 +45,8 @@ export default {
41
45
  if (wf.includes('pages')) files['.github/workflows/pages.yml'] = pagesWorkflow(cfg);
42
46
  if (wf.includes('codeql')) files['.github/workflows/codeql.yml'] = codeqlWorkflow();
43
47
  if (wf.includes('stale')) files['.github/workflows/stale.yml'] = staleWorkflow();
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);
44
50
 
45
51
  if (cfg.deps === 'renovate') {
46
52
  files['.github/renovate.json'] = toJson({
@@ -199,6 +205,54 @@ function codeqlWorkflow() {
199
205
  ].join('\n');
200
206
  }
201
207
 
208
+ function e2eWorkflow(cfg) {
209
+ return [
210
+ 'name: E2E',
211
+ 'on:',
212
+ ' push:',
213
+ ' branches: [main]',
214
+ ' pull_request:',
215
+ 'jobs:',
216
+ ' e2e:',
217
+ ' runs-on: ubuntu-latest',
218
+ ' steps:',
219
+ setupSteps(cfg),
220
+ ' - run: npx playwright install --with-deps chromium',
221
+ ` - run: ${pmRun(cfg, 'test:e2e')}`,
222
+ ' - uses: actions/upload-artifact@v4',
223
+ ' if: ${{ !cancelled() }}',
224
+ ' with:',
225
+ ' name: playwright-report',
226
+ ' path: playwright-report/',
227
+ ' retention-days: 7',
228
+ '',
229
+ ].join('\n');
230
+ }
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
+
202
256
  function staleWorkflow() {
203
257
  return [
204
258
  'name: Stale',
@@ -96,6 +96,10 @@ export const OPTIONS = {
96
96
  },
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
+ 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 },
99
103
  pkgChecks: { group: 'quality', type: 'boolean', label: 'Package checks (publint + are-the-types-wrong)', default: false },
100
104
  knip: { group: 'quality', type: 'boolean', label: 'Knip (unused files / deps / exports)', default: false },
101
105
 
@@ -245,12 +249,21 @@ export function normalizeConfig(input = {}) {
245
249
  // Storybook only applies to component libraries.
246
250
  if (!cfg.hasFramework || cfg.hasApp || !cfg.hasLibrary) cfg.storybook = false;
247
251
 
252
+ // Playwright E2E only applies to app targets.
253
+ if (!cfg.hasApp) cfg.e2e = false;
254
+
248
255
  // A monorepo is its own generation path (see buildMonorepo); it has a build.
249
256
  if (cfg.monorepo) cfg.hasBuild = true;
250
257
 
251
258
  cfg.publishable = (cfg.hasLibrary || cfg.hasCli) && !cfg.hasApp && !cfg.hasService;
252
259
  // Package-correctness checks only make sense for a publishable package.
253
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;
254
267
  // JSR is TypeScript-first, ESM, and for plain (non-framework) libraries.
255
268
  if (!(cfg.isTs && cfg.hasLibrary && !cfg.hasFramework && !cfg.hasApp)) cfg.jsr = false;
256
269
  return cfg;