create-packkit 0.3.0 → 0.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/README.md CHANGED
@@ -34,9 +34,9 @@ No install needed: **[danmat.github.io/create-packkit](https://danmat.github.io/
34
34
  |---|---|
35
35
  | **Language** | TypeScript (strict) · JavaScript (ESM) |
36
36
  | **Module format** | ESM · CJS · dual (proper `exports` map) |
37
- | **Target** | Library · CLI tool · both |
37
+ | **Target** | Library · CLI tool · **HTTP service (Hono)** · any combination |
38
38
  | **Framework** | None · **React** (component library — JSX, peer deps, jsdom tests) |
39
- | **Bundler** | tsup · tsdown · unbuild · rollup · none (tsc) |
39
+ | **Bundler** | tsup · tsdown · unbuild · rollup · none (tsc) · optional **minify** |
40
40
  | **Tests** | Vitest · Jest · node:test · none (+ coverage) |
41
41
  | **Lint/format** | ESLint + Prettier · Biome · oxlint · none |
42
42
  | **Git hooks** | simple-git-hooks · husky + lint-staged · lefthook · none |
@@ -48,7 +48,7 @@ No install needed: **[danmat.github.io/create-packkit](https://danmat.github.io/
48
48
 
49
49
  ## Presets
50
50
 
51
- `ts-lib` · `js-lib` · `ts-cli` / `cli` · `react-lib` · `react-lib-js` · `oss` · `minimal` · `full` — named bundles of the options above. See the [roadmap](ROADMAP.md) for what's next.
51
+ `ts-lib` · `js-lib` · `ts-cli` / `cli` · `react-lib` · `react-lib-js` · `node-service` · `oss` · `minimal` · `full` — named bundles of the options above. See the [roadmap](ROADMAP.md) for what's next.
52
52
 
53
53
  ## How it works
54
54
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-packkit",
3
- "version": "0.3.0",
3
+ "version": "0.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
@@ -11,6 +11,7 @@ const OVERRIDE_FLAGS = {
11
11
  lint: 'lint',
12
12
  hooks: 'gitHooks',
13
13
  release: 'release',
14
+ deps: 'deps',
14
15
  license: 'license',
15
16
  pm: 'packageManager',
16
17
  node: 'nodeVersion',
@@ -18,6 +19,15 @@ const OVERRIDE_FLAGS = {
18
19
  description: 'description',
19
20
  };
20
21
 
22
+ // Boolean options that default ON — a --no-<flag> turns them off.
23
+ const NEGATABLE = {
24
+ 'no-coverage': 'coverage',
25
+ 'no-community': 'community',
26
+ 'no-agents': 'agents',
27
+ 'no-vscode': 'vscode',
28
+ 'no-editorconfig': 'editorconfig',
29
+ };
30
+
21
31
  export function parseCliArgs(argv) {
22
32
  const { values, positionals } = parseArgs({
23
33
  args: argv,
@@ -29,10 +39,13 @@ export function parseCliArgs(argv) {
29
39
  here: { type: 'boolean' },
30
40
  'no-install': { type: 'boolean' },
31
41
  'no-git': { type: 'boolean' },
42
+ minify: { type: 'boolean' },
32
43
  target: { type: 'string', multiple: true },
44
+ workflows: { type: 'string', multiple: true },
33
45
  help: { type: 'boolean', short: 'h' },
34
46
  version: { type: 'boolean', short: 'v' },
35
47
  ...Object.fromEntries(Object.keys(OVERRIDE_FLAGS).map((k) => [k, { type: 'string' }])),
48
+ ...Object.fromEntries(Object.keys(NEGATABLE).map((k) => [k, { type: 'boolean' }])),
36
49
  },
37
50
  });
38
51
 
@@ -47,6 +60,11 @@ export function parseCliArgs(argv) {
47
60
  if (values[flag] != null) overrides[key] = values[flag];
48
61
  }
49
62
  if (values.target) overrides.target = values.target;
63
+ if (values.workflows) overrides.workflows = values.workflows;
64
+ if (values.minify) overrides.minify = true;
65
+ for (const [flag, key] of Object.entries(NEGATABLE)) {
66
+ if (values[flag]) overrides[key] = false;
67
+ }
50
68
  if (name) overrides.name = name;
51
69
 
52
70
  return {
package/src/cli/index.js CHANGED
@@ -33,9 +33,14 @@ Options:
33
33
  --no-install Skip dependency install
34
34
  --no-git Skip git init
35
35
  --pm <manager> npm | pnpm | yarn | bun
36
- --language <ts|js> --module <esm|cjs|dual> --bundler <tsup|tsdown|unbuild|rollup|none>
36
+ --language <ts|js> --module <esm|cjs|dual> --framework <none|react>
37
+ --target <library|cli|service> (repeatable)
38
+ --bundler <tsup|tsdown|unbuild|rollup|none> --minify
37
39
  --test <vitest|jest|node|none> --lint <eslint-prettier|biome|oxlint|none>
38
- --license <MIT|Apache-2.0|ISC|none>
40
+ --hooks <simple-git-hooks|husky|lefthook|none> --release <changesets|release-it|np|none>
41
+ --workflows <ci|npm-publish|pages|codeql|codecov|stale> (repeatable)
42
+ --deps <renovate|dependabot|none> --license <MIT|Apache-2.0|ISC|none>
43
+ --no-coverage --no-community --no-agents --no-vscode --no-editorconfig
39
44
  -h, --help Show this help
40
45
  -v, --version Show version
41
46
 
package/src/cli/wizard.js CHANGED
@@ -32,6 +32,9 @@ export async function runWizard(seed = {}) {
32
32
  cfg.moduleFormat = bail(await p.select({ message: 'Module format', options: asOptions('moduleFormat'), initialValue: OPTIONS.moduleFormat.default }));
33
33
  cfg.packageManager = bail(await p.select({ message: 'Package manager', options: asOptions('packageManager'), initialValue: OPTIONS.packageManager.default }));
34
34
  cfg.bundler = bail(await p.select({ message: 'Build / bundler', options: asOptions('bundler'), initialValue: OPTIONS.bundler.default }));
35
+ if (cfg.bundler !== 'none') {
36
+ cfg.minify = bail(await p.confirm({ message: 'Minify the build output?', initialValue: false }));
37
+ }
35
38
  cfg.test = bail(await p.select({ message: 'Test runner', options: asOptions('test'), initialValue: OPTIONS.test.default }));
36
39
  cfg.lint = bail(await p.select({ message: 'Lint / format', options: asOptions('lint'), initialValue: OPTIONS.lint.default }));
37
40
  cfg.gitHooks = bail(await p.select({ message: 'Git hooks', options: asOptions('gitHooks'), initialValue: OPTIONS.gitHooks.default }));
@@ -55,6 +55,7 @@ export default {
55
55
  pkg.devDependencies = {
56
56
  rollup: '^4.0.0',
57
57
  ...(cfg.isTs ? { '@rollup/plugin-typescript': '^11.0.0', tslib: '^2.6.0' } : {}),
58
+ ...(cfg.minify ? { '@rollup/plugin-terser': '^0.4.0' } : {}),
58
59
  };
59
60
  } else if (cfg.bundler === 'none' && cfg.isTs) {
60
61
  // tsc-only build for TypeScript.
@@ -81,6 +82,7 @@ function tsupConfig(cfg, entries, formats, tool) {
81
82
  `\tsourcemap: true,`,
82
83
  `\tclean: true,`,
83
84
  `\ttreeshake: true,`,
85
+ cfg.minify ? `\tminify: true,` : null,
84
86
  `});`,
85
87
  ``,
86
88
  ].filter((l) => l !== null).join('\n');
@@ -94,7 +96,7 @@ function unbuildConfig(cfg) {
94
96
  `\tentries: ['src/index'],`,
95
97
  `\tdeclaration: ${cfg.isTs},`,
96
98
  `\tclean: true,`,
97
- `\trollup: { emitCJS: ${cfg.hasCjs} },`,
99
+ `\trollup: { emitCJS: ${cfg.hasCjs}${cfg.minify ? ', esbuild: { minify: true }' : ''} },`,
98
100
  `});`,
99
101
  ``,
100
102
  ].join('\n');
@@ -104,14 +106,18 @@ function rollupConfig(cfg, formats) {
104
106
  const out = formats
105
107
  .map((f) => `\t\t{ file: 'dist/index.${f === 'cjs' ? 'cjs' : 'js'}', format: '${f}', sourcemap: true }`)
106
108
  .join(',\n');
107
- const tsPlugin = cfg.isTs ? `\n\tplugins: [typescript()],` : '';
108
- const tsImport = cfg.isTs ? `import typescript from '@rollup/plugin-typescript';\n` : '';
109
+ const imports = [
110
+ cfg.isTs ? `import typescript from '@rollup/plugin-typescript';` : null,
111
+ cfg.minify ? `import terser from '@rollup/plugin-terser';` : null,
112
+ ].filter(Boolean);
113
+ const plugins = [cfg.isTs ? 'typescript()' : null, cfg.minify ? 'terser()' : null].filter(Boolean);
114
+ const pluginLine = plugins.length ? `\n\tplugins: [${plugins.join(', ')}],` : '';
109
115
  return [
110
- tsImport + `export default {`,
116
+ (imports.length ? imports.join('\n') + '\n' : '') + `export default {`,
111
117
  `\tinput: 'src/index.${cfg.ext}',`,
112
118
  `\toutput: [`,
113
119
  out,
114
- `\t],${tsPlugin}`,
120
+ `\t],${pluginLine}`,
115
121
  `};`,
116
122
  ``,
117
123
  ].join('\n');
@@ -5,6 +5,7 @@ import meta from './meta.js';
5
5
  import bundler from './bundler.js';
6
6
  import typescript from './typescript.js';
7
7
  import react from './react.js';
8
+ import service from './service.js';
8
9
  import test from './test.js';
9
10
  import lint from './lint.js';
10
11
  import githooks from './githooks.js';
@@ -21,6 +22,7 @@ export default [
21
22
  bundler,
22
23
  typescript,
23
24
  react,
25
+ service,
24
26
  test,
25
27
  lint,
26
28
  githooks,
@@ -0,0 +1,79 @@
1
+ // HTTP service target (Hono). Splits the app (testable) from the server entry
2
+ // (which listens), adds start/dev scripts, and a Dockerfile.
3
+
4
+ export default {
5
+ id: 'service',
6
+ active: (cfg) => cfg.hasService,
7
+ apply(cfg) {
8
+ const ext = cfg.ext;
9
+ const files = {
10
+ [`src/app.${ext}`]: appFile(cfg),
11
+ [`src/index.${ext}`]: serverFile(cfg),
12
+ Dockerfile: dockerfile(cfg),
13
+ '.dockerignore': 'node_modules\ndist\n.git\n.env\n',
14
+ };
15
+ return {
16
+ files,
17
+ pkg: {
18
+ private: true,
19
+ scripts: {
20
+ start: 'node dist/index.js',
21
+ dev: cfg.isTs ? 'tsx watch src/index.ts' : 'node --watch src/index.js',
22
+ },
23
+ dependencies: { hono: '^4.5.0', '@hono/node-server': '^1.12.0' },
24
+ ...(cfg.isTs ? { devDependencies: { tsx: '^4.0.0' } } : {}),
25
+ },
26
+ };
27
+ },
28
+ };
29
+
30
+ function appFile(cfg) {
31
+ const t = cfg.isTs;
32
+ return [
33
+ `import { Hono } from 'hono';`,
34
+ ``,
35
+ `export const app = new Hono();`,
36
+ ``,
37
+ `app.get('/', (c) => c.json({ ok: true, service: '${cfg.name}' }));`,
38
+ `app.get('/health', (c) => c.text('ok'));`,
39
+ ``,
40
+ ].join('\n');
41
+ }
42
+
43
+ function serverFile(cfg) {
44
+ return [
45
+ `import { serve } from '@hono/node-server';`,
46
+ `import { app } from './app${cfg.isTs ? '.js' : '.js'}';`,
47
+ ``,
48
+ `const port = Number(process.env.PORT) || 3000;`,
49
+ `serve({ fetch: app.fetch, port }, (info) => {`,
50
+ `\tconsole.log(\`Listening on http://localhost:\${info.port}\`);`,
51
+ `});`,
52
+ ``,
53
+ ].join('\n');
54
+ }
55
+
56
+ function dockerfile(cfg) {
57
+ const node = cfg.nodeVersion;
58
+ const pm = cfg.packageManager;
59
+ const install = pm === 'npm' ? 'npm ci' : `${pm} install --frozen-lockfile`;
60
+ const build = pm === 'npm' ? 'npm run build' : `${pm} run build`;
61
+ return [
62
+ `FROM node:${node}-slim AS build`,
63
+ `WORKDIR /app`,
64
+ `COPY package*.json ./`,
65
+ `RUN ${install}`,
66
+ `COPY . .`,
67
+ `RUN ${build}`,
68
+ ``,
69
+ `FROM node:${node}-slim`,
70
+ `WORKDIR /app`,
71
+ `ENV NODE_ENV=production`,
72
+ `COPY --from=build /app/node_modules ./node_modules`,
73
+ `COPY --from=build /app/dist ./dist`,
74
+ `COPY package.json ./`,
75
+ `EXPOSE 3000`,
76
+ `CMD ["node", "dist/index.js"]`,
77
+ ``,
78
+ ].join('\n');
79
+ }
@@ -65,6 +65,20 @@ function importPath(runner, cfg) {
65
65
 
66
66
  function exampleTest(runner, cfg) {
67
67
  const imp = importPath(runner, cfg);
68
+ if (cfg.hasService) {
69
+ const api = runner === 'jest' ? '' : `import { describe, it, expect } from 'vitest';\n`;
70
+ return [
71
+ api + `import { app } from './app.js';`,
72
+ ``,
73
+ `describe('app', () => {`,
74
+ `\tit('responds on /', async () => {`,
75
+ `\t\tconst res = await app.request('/');`,
76
+ `\t\texpect(res.status).toBe(200);`,
77
+ `\t});`,
78
+ `});`,
79
+ ``,
80
+ ].join('\n');
81
+ }
68
82
  if (cfg.isReact) {
69
83
  const api = runner === 'jest' ? '' : `import { describe, it, expect } from 'vitest';\n`;
70
84
  return [
package/src/core/index.js CHANGED
@@ -6,9 +6,9 @@ import { normalizeConfig, OPTIONS, GROUPS, defaultConfig } from './options.js';
6
6
  import { deepMerge, toJson } from './render.js';
7
7
  import { finalizePackageJson } from './pkg.js';
8
8
  import features from './features/index.js';
9
- import { PRESETS, PRESET_NAMES } from './presets.js';
9
+ import { PRESETS, PRESET_NAMES, PRESET_INFO } from './presets.js';
10
10
 
11
- export { OPTIONS, GROUPS, defaultConfig, normalizeConfig, PRESETS, PRESET_NAMES };
11
+ export { OPTIONS, GROUPS, defaultConfig, normalizeConfig, PRESETS, PRESET_NAMES, PRESET_INFO };
12
12
 
13
13
  /** Apply a named preset over the defaults, returning a full config. */
14
14
  export function fromPreset(name, overrides = {}) {
@@ -33,6 +33,7 @@ export const OPTIONS = {
33
33
  choices: [
34
34
  { value: 'library', label: 'Library (importable package)' },
35
35
  { value: 'cli', label: 'CLI tool (ships a bin)' },
36
+ { value: 'service', label: 'HTTP service (Hono)' },
36
37
  ],
37
38
  },
38
39
  framework: {
@@ -71,6 +72,9 @@ export const OPTIONS = {
71
72
  { value: 'none', label: 'None (tsc / no build)' },
72
73
  ],
73
74
  },
75
+ minify: {
76
+ group: 'build', type: 'boolean', label: 'Minify output (best for CLIs / browser bundles)', default: false,
77
+ },
74
78
 
75
79
  // ---- testing ----
76
80
  test: {
@@ -184,6 +188,8 @@ export function normalizeConfig(input = {}) {
184
188
  if (!Array.isArray(cfg.target) || cfg.target.length === 0) cfg.target = ['library'];
185
189
  if (!Array.isArray(cfg.workflows)) cfg.workflows = [];
186
190
 
191
+ // Minify needs a bundler.
192
+ if (cfg.bundler === 'none') cfg.minify = false;
187
193
  // Coverage only makes sense with a test runner that supports it.
188
194
  if (cfg.test === 'none' || cfg.test === 'node') cfg.coverage = false;
189
195
  // Codecov workflow implies coverage.
@@ -200,6 +206,7 @@ export function normalizeConfig(input = {}) {
200
206
  cfg.srcExt = cfg.isReact ? (cfg.isTs ? 'tsx' : 'jsx') : cfg.ext;
201
207
  cfg.hasLibrary = cfg.target.includes('library');
202
208
  cfg.hasCli = cfg.target.includes('cli');
209
+ cfg.hasService = cfg.target.includes('service');
203
210
  cfg.hasEsm = cfg.moduleFormat === 'esm' || cfg.moduleFormat === 'dual';
204
211
  cfg.hasCjs = cfg.moduleFormat === 'cjs' || cfg.moduleFormat === 'dual';
205
212
  return cfg;
@@ -8,6 +8,11 @@ export const PRESETS = {
8
8
  cli: { language: 'ts', target: ['cli', 'library'], moduleFormat: 'esm' },
9
9
  'react-lib': { language: 'ts', framework: 'react', target: ['library'], moduleFormat: 'dual', test: 'vitest' },
10
10
  'react-lib-js': { language: 'js', framework: 'react', target: ['library'], moduleFormat: 'dual', bundler: 'tsup', test: 'vitest' },
11
+ 'node-service': {
12
+ language: 'ts', target: ['service'], moduleFormat: 'esm', bundler: 'tsup',
13
+ test: 'vitest', lint: 'eslint-prettier', gitHooks: 'simple-git-hooks',
14
+ release: 'none', workflows: ['ci'], deps: 'renovate', agents: true, vscode: true,
15
+ },
11
16
  oss: {
12
17
  language: 'ts', target: ['library'], moduleFormat: 'dual', bundler: 'tsup',
13
18
  test: 'vitest', coverage: true, lint: 'eslint-prettier', gitHooks: 'simple-git-hooks',
@@ -29,3 +34,17 @@ export const PRESETS = {
29
34
  };
30
35
 
31
36
  export const PRESET_NAMES = Object.keys(PRESETS);
37
+
38
+ // Short gists for the CLI help and the web configurator (tooltips + description).
39
+ export const PRESET_INFO = {
40
+ 'ts-lib': 'TypeScript library — dual ESM/CJS, tsup, Vitest, ESLint.',
41
+ 'js-lib': 'JavaScript (ESM) library — tsup, Vitest, ESLint.',
42
+ 'ts-cli': 'TypeScript CLI + library — ESM, ships a bin.',
43
+ cli: 'TypeScript CLI tool — ESM, ships a bin.',
44
+ 'react-lib': 'React component library (TS) — JSX, peer deps, jsdom tests.',
45
+ 'react-lib-js': 'React component library (JS) — JSX, peer deps, jsdom tests.',
46
+ 'node-service': 'Node HTTP service (Hono) — tsx dev, tsup build, Dockerfile.',
47
+ oss: 'Full open-source library — coverage, CodeQL, Codecov, Renovate, Changesets.',
48
+ minimal: 'Bare TS library — tsup only, no tests/lint/CI.',
49
+ full: 'Everything on — library + CLI, all workflows and extras.',
50
+ };