create-packkit 2.0.2 → 2.1.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.2",
3
+ "version": "2.1.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": {
@@ -26,7 +26,8 @@
26
26
  "test": "node --test",
27
27
  "check:deps": "node scripts/check-template-deps.mjs",
28
28
  "integration": "node scripts/integration.mjs",
29
- "build:web": "esbuild src/core/index.js --bundle --format=esm --outfile=docs/packkit-core.js"
29
+ "build:web": "esbuild src/core/index.js --bundle --format=esm --outfile=docs/packkit-core.js",
30
+ "update:node": "node scripts/update-node-versions.mjs"
30
31
  },
31
32
  "repository": {
32
33
  "type": "git",
package/src/cli/args.js CHANGED
@@ -48,6 +48,7 @@ export function parseCliArgs(argv) {
48
48
  minify: { type: 'boolean' },
49
49
  monorepo: { type: 'boolean' },
50
50
  storybook: { type: 'boolean' },
51
+ e2e: { type: 'boolean' },
51
52
  'pkg-checks': { type: 'boolean' },
52
53
  knip: { type: 'boolean' },
53
54
  jsr: { type: 'boolean' },
@@ -74,6 +75,7 @@ export function parseCliArgs(argv) {
74
75
  if (values.minify) overrides.minify = true;
75
76
  if (values.monorepo) overrides.monorepo = true;
76
77
  if (values.storybook) overrides.storybook = true;
78
+ if (values.e2e) overrides.e2e = true;
77
79
  if (values['pkg-checks']) overrides.pkgChecks = true;
78
80
  if (values.knip) overrides.knip = true;
79
81
  if (values.jsr) overrides.jsr = true;
package/src/cli/index.js CHANGED
@@ -3,7 +3,7 @@ import { readFileSync, existsSync } from 'node:fs';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import * as p from '@clack/prompts';
5
5
  import { generate, fromPreset, normalizeConfig, PRESET_NAMES, OPTIONS, PRESET_INFO, PRESET_ALIASES } from '../core/index.js';
6
- import { nodeFloor, meetsNodeFloor } from '../core/node.js';
6
+ import { engineFloor, meetsNodeFloor } from '../core/node.js';
7
7
  import { parseCliArgs } from './args.js';
8
8
  import { runWizard } from './wizard.js';
9
9
  import { writeProject, dirIsEmptyOrMissing, gitInit, installDeps } from './write.js';
@@ -40,7 +40,8 @@ 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
+ --lint <eslint-prettier|biome|oxlint|none>
44
45
  --hooks <simple-git-hooks|husky|lefthook|none> --release <changesets|release-it|np|none>
45
46
  --workflows <ci|npm-publish|pages|codeql|codecov|stale> (repeatable)
46
47
  --deps <renovate|dependabot|none> --license <MIT|Apache-2.0|ISC|none>
@@ -92,7 +93,7 @@ export async function run(argv = process.argv.slice(2)) {
92
93
  // require this floor. npm only *warns* on engines, so catch it here — clearly,
93
94
  // once — instead of letting a doomed install spew EBADENGINE and leave a broken
94
95
  // project. This is the signal both humans and agents need up front.
95
- const floor = nodeFloor(config.nodeVersion);
96
+ const floor = engineFloor(config.nodeVersion);
96
97
  const nodeOk = meetsNodeFloor(process.version, floor);
97
98
  if (!nodeOk) {
98
99
  const lines = [
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('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 }));
41
44
  cfg.release = bail(await p.select({ message: 'Release / versioning', options: asOptions('release'), initialValue: OPTIONS.release.default }));
@@ -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
+ };
@@ -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*
@@ -34,8 +34,8 @@ export default {
34
34
 
35
35
  if (needsLintStaged) {
36
36
  pkg['lint-staged'] = staged;
37
- // Held at 16: v17 requires Node >=22.22.1, which would silently drop
38
- // Node 20 LTS support for every generated project. See NODE_FLOOR.
37
+ // Held at 16: v17 requires Node >=22.22.1, above our Node 22 engines
38
+ // floor (22.13) it would break the Maintenance-LTS line. See ENGINE_MIN.
39
39
  pkg.devDependencies['lint-staged'] = '^16.2.0';
40
40
  }
41
41
 
@@ -8,6 +8,7 @@ import frameworks from './frameworks.js';
8
8
  import vite from './vite.js';
9
9
  import service from './service.js';
10
10
  import test from './test.js';
11
+ import e2e from './e2e.js';
11
12
  import lint from './lint.js';
12
13
  import githooks from './githooks.js';
13
14
  import release from './release.js';
@@ -29,6 +30,7 @@ export default [
29
30
  vite,
30
31
  service,
31
32
  test,
33
+ e2e,
32
34
  lint,
33
35
  githooks,
34
36
  release,
@@ -1,6 +1,6 @@
1
1
  // Always-on: base package.json descriptive fields + the source entry + README.
2
2
 
3
- import { nodeFloor } from '../node.js';
3
+ import { engineFloor, nodePin } from '../node.js';
4
4
 
5
5
  export default {
6
6
  id: 'meta',
@@ -12,7 +12,7 @@ export default {
12
12
  version: '0.0.0',
13
13
  description: cfg.description || '',
14
14
  type: cfg.moduleFormat === 'cjs' ? 'commonjs' : 'module',
15
- engines: { node: `>=${nodeFloor(cfg.nodeVersion)}` },
15
+ engines: { node: `>=${engineFloor(cfg.nodeVersion)}` },
16
16
  scripts: {},
17
17
  };
18
18
 
@@ -35,8 +35,9 @@ export default {
35
35
  // README
36
36
  files['README.md'] = readme(cfg);
37
37
 
38
- // Node version pin — the honest floor, so `nvm use` can't land below it.
39
- files['.nvmrc'] = `${nodeFloor(cfg.nodeVersion)}\n`;
38
+ // Node version pin — that line's newest patch (from Node's own data), so
39
+ // `nvm use` lands on a current, real release.
40
+ files['.nvmrc'] = `${nodePin(cfg.nodeVersion)}\n`;
40
41
 
41
42
  // A typecheck script for TS projects — framework-aware (plain tsc can't
42
43
  // resolve .vue/.svelte modules).
@@ -118,7 +119,7 @@ function readme(cfg) {
118
119
  lines.push(
119
120
  '## Requirements',
120
121
  '',
121
- `Node.js >= ${nodeFloor(cfg.nodeVersion)} (\`.nvmrc\` pins it; run \`nvm use\`). Enforced via \`engine-strict\`, so installs fail fast on an unsupported version.`,
122
+ `Node.js >= ${engineFloor(cfg.nodeVersion)} (\`.nvmrc\` pins ${nodePin(cfg.nodeVersion)}; run \`nvm use\`). Enforced via \`engine-strict\`, so installs fail fast on an unsupported version.`,
122
123
  '',
123
124
  );
124
125
 
@@ -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,
@@ -41,6 +41,7 @@ export default {
41
41
  if (wf.includes('pages')) files['.github/workflows/pages.yml'] = pagesWorkflow(cfg);
42
42
  if (wf.includes('codeql')) files['.github/workflows/codeql.yml'] = codeqlWorkflow();
43
43
  if (wf.includes('stale')) files['.github/workflows/stale.yml'] = staleWorkflow();
44
+ if (cfg.e2e && cfg.hasApp && wf.includes('ci')) files['.github/workflows/e2e.yml'] = e2eWorkflow(cfg);
44
45
 
45
46
  if (cfg.deps === 'renovate') {
46
47
  files['.github/renovate.json'] = toJson({
@@ -199,6 +200,30 @@ function codeqlWorkflow() {
199
200
  ].join('\n');
200
201
  }
201
202
 
203
+ function e2eWorkflow(cfg) {
204
+ return [
205
+ 'name: E2E',
206
+ 'on:',
207
+ ' push:',
208
+ ' branches: [main]',
209
+ ' pull_request:',
210
+ 'jobs:',
211
+ ' e2e:',
212
+ ' runs-on: ubuntu-latest',
213
+ ' steps:',
214
+ setupSteps(cfg),
215
+ ' - run: npx playwright install --with-deps chromium',
216
+ ` - run: ${pmRun(cfg, 'test:e2e')}`,
217
+ ' - uses: actions/upload-artifact@v4',
218
+ ' if: ${{ !cancelled() }}',
219
+ ' with:',
220
+ ' name: playwright-report',
221
+ ' path: playwright-report/',
222
+ ' retention-days: 7',
223
+ '',
224
+ ].join('\n');
225
+ }
226
+
202
227
  function staleWorkflow() {
203
228
  return [
204
229
  'name: Stale',
@@ -0,0 +1,12 @@
1
+ // AUTO-GENERATED by scripts/update-node-versions.mjs — do not edit by hand.
2
+ // Source: nodejs.org/dist + nodejs/Release schedule, refreshed 2026-07-14.
3
+ // Offered lines track the Node release schedule; each version is that line's
4
+ // newest published patch. Run `node scripts/update-node-versions.mjs` to refresh.
5
+
6
+ export const NODE_LINES = {
7
+ '24': { version: '24.18.0', status: 'active-lts', codename: 'Krypton', label: '24 (Active LTS, "Krypton")' },
8
+ '22': { version: '22.23.1', status: 'maintenance', codename: 'Jod', label: '22 (Maintenance LTS, "Jod")' },
9
+ '26': { version: '26.5.0', status: 'current', codename: null, label: '26 (Current)' },
10
+ };
11
+
12
+ export const DEFAULT_NODE = '24';
package/src/core/node.js CHANGED
@@ -1,18 +1,30 @@
1
- // Shared Node-version floor logic. Browser-safe (no node builtins) so both the
2
- // generator and the CLI preflight agree on exactly one source of truth.
1
+ // Shared Node-version logic. Browser-safe (no node builtins) so the generator
2
+ // and the CLI preflight agree on exactly one source of truth.
3
+ //
4
+ // Two concerns, two sources:
5
+ // - which majors we offer + the exact patch to pin -> node-versions.js,
6
+ // auto-derived from Node's release schedule + dist index (never guessed).
7
+ // - the minimum patch *within a major* our template deps need -> ENGINE_MIN,
8
+ // below, driven by our deps (eslint/vite), governed by the template-deps
9
+ // freshness workflow. These change rarely and independently of Node.
3
10
 
4
- // The real minimum patch for each supported major, driven by our template deps
5
- // (eslint 10, jsdom 29, vite 8 need ^20.19; vite/others need ^22.12). Writing a
6
- // bare ">=20" would be a lie — a user on 20.17 hits EBADENGINE and transitive
7
- // syntax errors. Keep this honest.
8
- export const NODE_FLOOR = { 18: '18.18.0', 20: '20.19.0', 22: '22.12.0', 24: '24.0.0' };
11
+ import { NODE_LINES, DEFAULT_NODE } from './node-versions.js';
9
12
 
10
- export const nodeFloor = (v) => NODE_FLOOR[v] || `${v}.0.0`;
13
+ export { NODE_LINES, DEFAULT_NODE };
14
+
15
+ // eslint 10 needs ^20.19/^22.13; vite 8 needs ^20.19/^22.12 — take the max.
16
+ export const ENGINE_MIN = { 18: '18.18.0', 20: '20.19.0', 22: '22.13.0' };
17
+
18
+ /** The `engines` minimum for a target major (a floor, not a pin). */
19
+ export const engineFloor = (v) => ENGINE_MIN[v] || `${v}.0.0`;
20
+
21
+ /** The exact version to pin in `.nvmrc` — that line's newest patch, or the floor. */
22
+ export const nodePin = (v) => NODE_LINES[v]?.version || engineFloor(v);
11
23
 
12
24
  const parts = (s) =>
13
25
  String(s).replace(/^v/, '').split('.').map((n) => parseInt(n, 10) || 0);
14
26
 
15
- /** True if `current` (e.g. "v20.17.0" or process.version) is >= `floor` ("20.19.0"). */
27
+ /** True if `current` (e.g. "v22.5.0" or process.version) is >= `floor` ("22.13.0"). */
16
28
  export function meetsNodeFloor(current, floor) {
17
29
  const [a, b, c] = parts(current);
18
30
  const [x, y, z] = parts(floor);
@@ -2,8 +2,14 @@
2
2
  // Both the CLI wizard and the web configurator render from this schema, and
3
3
  // `normalizeConfig` turns partial/user input into a complete, valid config.
4
4
 
5
+ import { NODE_LINES, DEFAULT_NODE } from './node.js';
6
+
5
7
  /** @typedef {'select'|'multiselect'|'boolean'|'text'} OptionType */
6
8
 
9
+ // Node choices are derived from Node's own release schedule (see node-versions.js)
10
+ // so the LTS/Current lines and their patches stay current without hand-editing.
11
+ const NODE_CHOICES = Object.entries(NODE_LINES).map(([value, info]) => ({ value, label: info.label }));
12
+
7
13
  export const OPTIONS = {
8
14
  // ---- package metadata ----
9
15
  name: { group: 'meta', type: 'text', label: 'Package name', default: 'my-package' },
@@ -59,12 +65,8 @@ export const OPTIONS = {
59
65
  ],
60
66
  },
61
67
  nodeVersion: {
62
- group: 'core', type: 'select', label: 'Node version', default: '20',
63
- choices: [
64
- { value: '20', label: '20 (LTS, ≥20.19)' },
65
- { value: '22', label: '22 (LTS, ≥22.12)' },
66
- { value: '24', label: '24 (Current)' },
67
- ],
68
+ group: 'core', type: 'select', label: 'Node version', default: DEFAULT_NODE,
69
+ choices: NODE_CHOICES,
68
70
  },
69
71
 
70
72
  // ---- build ----
@@ -94,6 +96,7 @@ export const OPTIONS = {
94
96
  },
95
97
  coverage: { group: 'quality', type: 'boolean', label: 'Coverage reporting', default: true },
96
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 },
97
100
  pkgChecks: { group: 'quality', type: 'boolean', label: 'Package checks (publint + are-the-types-wrong)', default: false },
98
101
  knip: { group: 'quality', type: 'boolean', label: 'Knip (unused files / deps / exports)', default: false },
99
102
 
@@ -243,6 +246,9 @@ export function normalizeConfig(input = {}) {
243
246
  // Storybook only applies to component libraries.
244
247
  if (!cfg.hasFramework || cfg.hasApp || !cfg.hasLibrary) cfg.storybook = false;
245
248
 
249
+ // Playwright E2E only applies to app targets.
250
+ if (!cfg.hasApp) cfg.e2e = false;
251
+
246
252
  // A monorepo is its own generation path (see buildMonorepo); it has a build.
247
253
  if (cfg.monorepo) cfg.hasBuild = true;
248
254