create-packkit 0.3.1 → 0.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/README.md CHANGED
@@ -34,8 +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 |
38
- | **Framework** | None · **React** (component library JSX, peer deps, jsdom tests) |
37
+ | **Target** | Library · CLI tool · **HTTP service (Hono)** · **App (Vite SPA)** · any combination |
38
+ | **Framework** | None · **React** · **Vue** · **Svelte** (component libraries or apps) |
39
+ | **Storybook** | optional, for React / Vue / Svelte component libraries |
39
40
  | **Bundler** | tsup · tsdown · unbuild · rollup · none (tsc) · optional **minify** |
40
41
  | **Tests** | Vitest · Jest · node:test · none (+ coverage) |
41
42
  | **Lint/format** | ESLint + Prettier · Biome · oxlint · none |
@@ -48,7 +49,7 @@ No install needed: **[danmat.github.io/create-packkit](https://danmat.github.io/
48
49
 
49
50
  ## Presets
50
51
 
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.
52
+ `ts-lib` · `js-lib` · `ts-cli` / `cli` · `react-lib` · `react-lib-js` · `react-app` · `vue-lib` · `svelte-lib` · `node-service` · `oss` · `minimal` · `full` — named bundles of the options above. See the [roadmap](ROADMAP.md) for what's next.
52
53
 
53
54
  ## How it works
54
55
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-packkit",
3
- "version": "0.3.1",
3
+ "version": "0.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
@@ -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,
@@ -31,9 +41,13 @@ export function parseCliArgs(argv) {
31
41
  'no-git': { type: 'boolean' },
32
42
  minify: { type: 'boolean' },
33
43
  target: { type: 'string', multiple: true },
44
+ workflows: { type: 'string', multiple: true },
45
+ minify: { type: 'boolean' },
46
+ storybook: { type: 'boolean' },
34
47
  help: { type: 'boolean', short: 'h' },
35
48
  version: { type: 'boolean', short: 'v' },
36
49
  ...Object.fromEntries(Object.keys(OVERRIDE_FLAGS).map((k) => [k, { type: 'string' }])),
50
+ ...Object.fromEntries(Object.keys(NEGATABLE).map((k) => [k, { type: 'boolean' }])),
37
51
  },
38
52
  });
39
53
 
@@ -48,7 +62,12 @@ export function parseCliArgs(argv) {
48
62
  if (values[flag] != null) overrides[key] = values[flag];
49
63
  }
50
64
  if (values.target) overrides.target = values.target;
65
+ if (values.workflows) overrides.workflows = values.workflows;
51
66
  if (values.minify) overrides.minify = true;
67
+ if (values.storybook) overrides.storybook = true;
68
+ for (const [flag, key] of Object.entries(NEGATABLE)) {
69
+ if (values[flag]) overrides[key] = false;
70
+ }
52
71
  if (name) overrides.name = name;
53
72
 
54
73
  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|vue|svelte>
37
+ --target <library|cli|service|app> (repeatable) --storybook
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
 
@@ -93,8 +98,8 @@ export async function run(argv = process.argv.slice(2)) {
93
98
  const next = [
94
99
  args.here ? null : `cd ${rel}`,
95
100
  config.install ? null : `${config.packageManager} install`,
96
- summary.stack.includes('tsup') || summary.stack.includes('tsdown') ? `${runWord(config)} build` : null,
97
- summary.stack.some((s) => ['vitest', 'jest'].includes(s)) ? `${runWord(config)} test` : null,
101
+ config.hasApp ? `${runWord(config)} dev` : config.hasBuild ? `${runWord(config)} build` : null,
102
+ config.test !== 'none' ? `${runWord(config)} test` : null,
98
103
  ].filter(Boolean);
99
104
 
100
105
  const done = `Created ${summary.name} — ${summary.fileCount} files · ${summary.stack.join(' · ')}`;
@@ -12,7 +12,7 @@ export default {
12
12
  if (cfg.isTs) commands.push(`- Type-check: \`${run('typecheck')}\``);
13
13
  if (cfg.lint !== 'none') commands.push(`- Lint: \`${run('lint')}\``);
14
14
  if (cfg.test !== 'none') commands.push(`- Test: \`${test}\``);
15
- if (cfg.bundler !== 'none' || cfg.isTs) commands.push(`- Build: \`${run('build')}\``);
15
+ if (cfg.hasBuild) commands.push(`- Build: \`${run('build')}\``);
16
16
 
17
17
  const stack = [
18
18
  `- Language: ${cfg.isTs ? 'TypeScript (strict)' : 'JavaScript (ESM)'}`,
@@ -3,7 +3,7 @@
3
3
 
4
4
  export default {
5
5
  id: 'bundler',
6
- active: () => true,
6
+ active: (cfg) => !cfg.customBuild, // Vite / Svelte-lib own their own build wiring
7
7
  apply(cfg) {
8
8
  const files = {};
9
9
  const pkg = { scripts: {} };
@@ -0,0 +1,156 @@
1
+ // React / Vue / Svelte source + dependencies, for both component libraries and
2
+ // Vite apps. The Vite build wiring lives in vite.js; here we emit the source
3
+ // files, the runtime deps (peer for libs, direct for apps), and — for Svelte
4
+ // libraries, which ship uncompiled source — the package entry points.
5
+
6
+ export default {
7
+ id: 'frameworks',
8
+ active: (cfg) => cfg.hasFramework,
9
+ apply(cfg) {
10
+ const files = {};
11
+ const pkg = { devDependencies: {}, scripts: {} };
12
+ const forApp = cfg.hasApp;
13
+
14
+ if (cfg.isReact) react(cfg, files, pkg, forApp);
15
+ else if (cfg.isVue) vue(cfg, files, pkg, forApp);
16
+ else if (cfg.isSvelte) svelte(cfg, files, pkg, forApp);
17
+
18
+ return { files, pkg };
19
+ },
20
+ };
21
+
22
+ /* --------------------------------- React --------------------------------- */
23
+ function react(cfg, files, pkg, forApp) {
24
+ const x = cfg.isTs ? 'tsx' : 'jsx';
25
+ if (forApp) {
26
+ files['index.html'] = htmlShell(cfg, `/src/main.${x}`);
27
+ files[`src/main.${x}`] = [
28
+ `import { StrictMode } from 'react';`,
29
+ `import { createRoot } from 'react-dom/client';`,
30
+ `import { App } from './App.${x === 'tsx' ? 'js' : 'js'}';`,
31
+ ``,
32
+ `createRoot(document.getElementById('root')${cfg.isTs ? '!' : ''}).render(`,
33
+ `\t<StrictMode><App /></StrictMode>,`,
34
+ `);`,
35
+ ``,
36
+ ].join('\n');
37
+ files[`src/App.${x}`] = [
38
+ `export function App() {`,
39
+ `\treturn <h1>Hello from ${cfg.name}</h1>;`,
40
+ `}`,
41
+ ``,
42
+ ].join('\n');
43
+ pkg.dependencies = { react: '^18.3.0', 'react-dom': '^18.3.0' };
44
+ } else {
45
+ files[`src/index.${x}`] = cfg.isTs
46
+ ? [
47
+ `export interface ButtonProps {`,
48
+ `\tlabel: string;`,
49
+ `\tonClick?: () => void;`,
50
+ `}`,
51
+ ``,
52
+ `export function Button({ label, onClick }: ButtonProps) {`,
53
+ `\treturn <button onClick={onClick}>{label}</button>;`,
54
+ `}`,
55
+ ``,
56
+ ].join('\n')
57
+ : [`export function Button({ label, onClick }) {`, `\treturn <button onClick={onClick}>{label}</button>;`, `}`, ``].join('\n');
58
+ pkg.peerDependencies = { react: '>=18', 'react-dom': '>=18' };
59
+ pkg.devDependencies.react = '^18.3.0';
60
+ pkg.devDependencies['react-dom'] = '^18.3.0';
61
+ }
62
+ if (cfg.isTs) {
63
+ pkg.devDependencies['@types/react'] = '^18.3.0';
64
+ pkg.devDependencies['@types/react-dom'] = '^18.3.0';
65
+ }
66
+ }
67
+
68
+ /* ---------------------------------- Vue ---------------------------------- */
69
+ function vue(cfg, files, pkg, forApp) {
70
+ const script = cfg.isTs ? `<script setup lang="ts">` : `<script setup>`;
71
+ if (forApp) {
72
+ files['index.html'] = htmlShell(cfg, `/src/main.${cfg.ext}`);
73
+ files[`src/main.${cfg.ext}`] = [
74
+ `import { createApp } from 'vue';`,
75
+ `import App from './App.vue';`,
76
+ ``,
77
+ `createApp(App).mount('#root');`,
78
+ ``,
79
+ ].join('\n');
80
+ files['src/App.vue'] = [script, `</script>`, ``, `<template>`, `\t<h1>Hello from ${cfg.name}</h1>`, `</template>`, ``].join('\n');
81
+ pkg.dependencies = { vue: '^3.4.0' };
82
+ } else {
83
+ files[`src/index.${cfg.ext}`] = `export { default as Button } from './Button.vue';\n`;
84
+ files['src/Button.vue'] = [
85
+ script,
86
+ `defineProps${cfg.isTs ? '<{ label: string }>()' : "(['label'])"};`,
87
+ `</script>`,
88
+ ``,
89
+ `<template>`,
90
+ `\t<button><slot>{{ label }}</slot></button>`,
91
+ `</template>`,
92
+ ``,
93
+ ].join('\n');
94
+ pkg.peerDependencies = { vue: '>=3' };
95
+ pkg.devDependencies.vue = '^3.4.0';
96
+ }
97
+ }
98
+
99
+ /* -------------------------------- Svelte --------------------------------- */
100
+ function svelte(cfg, files, pkg, forApp) {
101
+ const script = cfg.isTs ? `<script lang="ts">` : `<script>`;
102
+ // Shared by the Vite app build and the Vitest (test) config; enables TS in SFCs.
103
+ files['svelte.config.js'] = `import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';\n\nexport default { preprocess: vitePreprocess() };\n`;
104
+ pkg.devDependencies['@sveltejs/vite-plugin-svelte'] = '^4.0.0';
105
+ if (cfg.isTs) pkg.devDependencies['svelte-check'] = '^4.0.0';
106
+ if (forApp) {
107
+ files['index.html'] = htmlShell(cfg, `/src/main.${cfg.ext}`);
108
+ files[`src/main.${cfg.ext}`] = [
109
+ `import { mount } from 'svelte';`,
110
+ `import App from './App.svelte';`,
111
+ ``,
112
+ `const app = mount(App, { target: document.getElementById('root')${cfg.isTs ? '!' : ''} });`,
113
+ `export default app;`,
114
+ ``,
115
+ ].join('\n');
116
+ files['src/App.svelte'] = [script, `</script>`, ``, `<h1>Hello from ${cfg.name}</h1>`, ``].join('\n');
117
+ pkg.dependencies = { svelte: '^5.0.0' };
118
+ } else {
119
+ // Svelte libraries ship uncompiled source; the consumer's bundler compiles.
120
+ files[`src/index.${cfg.ext}`] = `export { default as Button } from './Button.svelte';\n`;
121
+ files['src/Button.svelte'] = [
122
+ script,
123
+ cfg.isTs ? `\tinterface Props { label: string; }` : ``,
124
+ cfg.isTs ? `\tconst { label }: Props = $props();` : `\tconst { label } = $props();`,
125
+ `</script>`,
126
+ ``,
127
+ `<button>{label}</button>`,
128
+ ``,
129
+ ].filter((l) => l !== ``).join('\n') + '\n';
130
+ pkg.peerDependencies = { svelte: '>=5' };
131
+ pkg.devDependencies.svelte = '^5.0.0';
132
+ // Ship source; point consumers at it.
133
+ pkg.svelte = `./src/index.${cfg.ext}`;
134
+ pkg.exports = { '.': { svelte: `./src/index.${cfg.ext}`, default: `./src/index.${cfg.ext}` } };
135
+ pkg.files = ['src'];
136
+ pkg.type = 'module';
137
+ }
138
+ }
139
+
140
+ function htmlShell(cfg, entry) {
141
+ return [
142
+ `<!doctype html>`,
143
+ `<html lang="en">`,
144
+ `\t<head>`,
145
+ `\t\t<meta charset="UTF-8" />`,
146
+ `\t\t<meta name="viewport" content="width=device-width, initial-scale=1.0" />`,
147
+ `\t\t<title>${cfg.name}</title>`,
148
+ `\t</head>`,
149
+ `\t<body>`,
150
+ `\t\t<div id="root"></div>`,
151
+ `\t\t<script type="module" src="${entry}"></script>`,
152
+ `\t</body>`,
153
+ `</html>`,
154
+ ``,
155
+ ].join('\n');
156
+ }
@@ -4,13 +4,16 @@
4
4
  import meta from './meta.js';
5
5
  import bundler from './bundler.js';
6
6
  import typescript from './typescript.js';
7
- import react from './react.js';
7
+ import frameworks from './frameworks.js';
8
+ import vite from './vite.js';
9
+ import service from './service.js';
8
10
  import test from './test.js';
9
11
  import lint from './lint.js';
10
12
  import githooks from './githooks.js';
11
13
  import release from './release.js';
12
14
  import cli from './cli.js';
13
15
  import workflows from './workflows.js';
16
+ import storybook from './storybook.js';
14
17
  import community from './community.js';
15
18
  import agents from './agents.js';
16
19
  import vscode from './vscode.js';
@@ -20,13 +23,16 @@ export default [
20
23
  meta,
21
24
  bundler,
22
25
  typescript,
23
- react,
26
+ frameworks,
27
+ vite,
28
+ service,
24
29
  test,
25
30
  lint,
26
31
  githooks,
27
32
  release,
28
33
  cli,
29
34
  workflows,
35
+ storybook,
30
36
  community,
31
37
  agents,
32
38
  vscode,
@@ -23,10 +23,11 @@ export default {
23
23
  pkg.homepage = `${cfg.repo.replace(/\.git$/, '')}#readme`;
24
24
  }
25
25
 
26
- // Source entry point.
27
- files[`src/index.${cfg.srcExt}`] = cfg.hasLibrary
28
- ? libraryEntry(cfg)
29
- : `// ${cfg.name}\n`;
26
+ // Source entry point — only the plain library entry lives here; framework
27
+ // and app/cli/service modules write their own source.
28
+ if (cfg.hasLibrary && !cfg.hasFramework) {
29
+ files[`src/index.${cfg.ext}`] = libraryEntry(cfg);
30
+ }
30
31
 
31
32
  // README
32
33
  files['README.md'] = readme(cfg);
@@ -34,8 +35,15 @@ export default {
34
35
  // Node version pin
35
36
  files['.nvmrc'] = `${cfg.nodeVersion}\n`;
36
37
 
37
- // A typecheck script for TS projects regardless of bundler.
38
- if (cfg.isTs) pkg.scripts.typecheck = 'tsc --noEmit';
38
+ // A typecheck script for TS projects framework-aware (plain tsc can't
39
+ // resolve .vue/.svelte modules).
40
+ if (cfg.isTs) {
41
+ pkg.scripts.typecheck = cfg.isVue
42
+ ? 'vue-tsc --noEmit'
43
+ : cfg.isSvelte
44
+ ? 'svelte-check --tsconfig ./tsconfig.json'
45
+ : 'tsc --noEmit';
46
+ }
39
47
 
40
48
  return { files, pkg };
41
49
  },
@@ -65,32 +73,8 @@ function libraryEntry(cfg) {
65
73
  ].join('\n');
66
74
  }
67
75
 
68
- function reactEntry(cfg) {
69
- if (cfg.isTs) {
70
- return [
71
- `export interface ButtonProps {`,
72
- `\t/** Text shown inside the button. */`,
73
- `\tlabel: string;`,
74
- `\tonClick?: () => void;`,
75
- `}`,
76
- ``,
77
- `/** A tiny example component — replace me. */`,
78
- `export function Button({ label, onClick }: ButtonProps) {`,
79
- `\treturn <button onClick={onClick}>{label}</button>;`,
80
- `}`,
81
- ``,
82
- ].join('\n');
83
- }
84
- return [
85
- `/**`,
86
- ` * A tiny example component — replace me.`,
87
- ` * @param {{ label: string, onClick?: () => void }} props`,
88
- ` */`,
89
- `export function Button({ label, onClick }) {`,
90
- `\treturn <button onClick={onClick}>{label}</button>;`,
91
- `}`,
92
- ``,
93
- ].join('\n');
76
+ function run(cfg, script) {
77
+ return cfg.packageManager === 'npm' ? `npm run ${script}` : `${cfg.packageManager} ${script}`;
94
78
  }
95
79
 
96
80
  function readme(cfg) {
@@ -114,10 +98,17 @@ function readme(cfg) {
114
98
  '',
115
99
  ];
116
100
 
117
- if (cfg.hasLibrary && cfg.isReact) {
101
+ if (cfg.hasApp) {
102
+ lines.push('## Develop', '', '```sh', run(cfg, 'dev') + ' # start the dev server', run(cfg, 'build') + ' # production build', '```', '');
103
+ } else if (cfg.hasLibrary && cfg.isReact) {
118
104
  lines.push('## Usage', '', '```' + (cfg.isTs ? 'tsx' : 'jsx'),
119
- `import { Button } from '${cfg.name}';`, '',
120
- `<Button label="Click me" onClick={() => alert('hi')} />`, '```', '');
105
+ `import { Button } from '${cfg.name}';`, '', `<Button label="Click me" />`, '```', '');
106
+ } else if (cfg.hasLibrary && cfg.isVue) {
107
+ lines.push('## Usage', '', '```' + (cfg.isTs ? 'ts' : 'js'),
108
+ `import { Button } from '${cfg.name}';`, '// then <Button label="Click me" /> in your template', '```', '');
109
+ } else if (cfg.hasLibrary && cfg.isSvelte) {
110
+ lines.push('## Usage', '', '```svelte',
111
+ `<script>import { Button } from '${cfg.name}';</script>`, '', `<Button label="Click me" />`, '```', '');
121
112
  } else if (cfg.hasLibrary) {
122
113
  const imp = cfg.isTs || cfg.hasEsm ? `import { greet } from '${cfg.name}';` : `const { greet } = require('${cfg.name}');`;
123
114
  lines.push('## Usage', '', '```' + (cfg.isTs ? 'ts' : 'js'), imp, '', `greet('world'); // "Hello, world!"`, '```', '');
@@ -40,5 +40,5 @@ export default {
40
40
  };
41
41
 
42
42
  function buildThen(cfg) {
43
- return cfg.bundler !== 'none' || cfg.isTs ? 'npm run build && ' : '';
43
+ return cfg.hasBuild ? 'npm run build && ' : '';
44
44
  }
@@ -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
+ }
@@ -0,0 +1,87 @@
1
+ // Storybook for component libraries (React / Vue / Svelte), on the Vite builder.
2
+
3
+ const FW = {
4
+ react: { builder: '@storybook/react-vite', renderer: '@storybook/react', storyExt: 'tsx' },
5
+ vue: { builder: '@storybook/vue3-vite', renderer: '@storybook/vue3', storyExt: 'ts' },
6
+ svelte: { builder: '@storybook/svelte-vite', renderer: '@storybook/svelte', storyExt: 'ts' },
7
+ };
8
+
9
+ export default {
10
+ id: 'storybook',
11
+ active: (cfg) => cfg.storybook,
12
+ apply(cfg) {
13
+ const fw = FW[cfg.framework];
14
+ const files = {};
15
+
16
+ files['.storybook/main.ts'] = [
17
+ `import type { StorybookConfig } from '${fw.builder}';`,
18
+ ``,
19
+ `const config: StorybookConfig = {`,
20
+ `\tstories: ['../src/**/*.stories.@(ts|tsx|svelte)'],`,
21
+ `\taddons: ['@storybook/addon-essentials'],`,
22
+ `\tframework: '${fw.builder}',`,
23
+ `};`,
24
+ `export default config;`,
25
+ ``,
26
+ ].join('\n');
27
+
28
+ files['.storybook/preview.ts'] = `export default { parameters: {} };\n`;
29
+
30
+ files[`src/Button.stories.${fw.storyExt}`] = story(cfg, fw);
31
+
32
+ return {
33
+ files,
34
+ pkg: {
35
+ scripts: {
36
+ storybook: 'storybook dev -p 6006',
37
+ 'build-storybook': 'storybook build',
38
+ },
39
+ devDependencies: {
40
+ storybook: '^8.2.0',
41
+ [fw.builder]: '^8.2.0',
42
+ [fw.renderer]: '^8.2.0',
43
+ '@storybook/addon-essentials': '^8.2.0',
44
+ vite: '^5.4.0',
45
+ },
46
+ },
47
+ };
48
+ },
49
+ };
50
+
51
+ function story(cfg, fw) {
52
+ if (cfg.isReact) {
53
+ return [
54
+ `import type { Meta, StoryObj } from '${fw.renderer}';`,
55
+ `import { Button } from './index';`,
56
+ ``,
57
+ `const meta: Meta<typeof Button> = { component: Button };`,
58
+ `export default meta;`,
59
+ ``,
60
+ `export const Default: StoryObj<typeof Button> = { args: { label: 'Click me' } };`,
61
+ ``,
62
+ ].join('\n');
63
+ }
64
+ if (cfg.isVue) {
65
+ return [
66
+ `import type { Meta, StoryObj } from '${fw.renderer}';`,
67
+ `import { Button } from './index';`,
68
+ ``,
69
+ `const meta = { component: Button } satisfies Meta<typeof Button>;`,
70
+ `export default meta;`,
71
+ ``,
72
+ `export const Default: StoryObj<typeof meta> = { args: { label: 'Click me' } };`,
73
+ ``,
74
+ ].join('\n');
75
+ }
76
+ // svelte
77
+ return [
78
+ `import type { Meta, StoryObj } from '${fw.renderer}';`,
79
+ `import Button from './Button.svelte';`,
80
+ ``,
81
+ `const meta = { component: Button } satisfies Meta<Button>;`,
82
+ `export default meta;`,
83
+ ``,
84
+ `export const Default: StoryObj<typeof meta> = { args: { label: 'Click me' } };`,
85
+ ``,
86
+ ].join('\n');
87
+ }
@@ -11,13 +11,23 @@ export default {
11
11
  const testExt = cfg.isReact ? cfg.srcExt : ext;
12
12
 
13
13
  if (cfg.test === 'vitest') {
14
+ const fw = cfg.isVue
15
+ ? { imp: `import vue from '@vitejs/plugin-vue';`, call: 'vue()' }
16
+ : cfg.isSvelte
17
+ ? {
18
+ imp: `import { svelte } from '@sveltejs/vite-plugin-svelte';\nimport { svelteTesting } from '@testing-library/svelte/vite';`,
19
+ call: 'svelte(), svelteTesting()',
20
+ }
21
+ : null;
14
22
  files[`vitest.config.${ext}`] = [
23
+ fw ? fw.imp : null,
15
24
  `import { defineConfig } from 'vitest/config';`,
16
25
  ``,
17
26
  `export default defineConfig({`,
27
+ fw ? `\tplugins: [${fw.call}],` : null,
18
28
  `\ttest: {`,
19
- cfg.isReact ? `\t\tenvironment: 'jsdom',` : null,
20
- cfg.isReact ? `\t\tglobals: true,` : null,
29
+ cfg.hasFramework ? `\t\tenvironment: 'jsdom',` : null,
30
+ cfg.hasFramework ? `\t\tglobals: true,` : null,
21
31
  cfg.coverage ? `\t\tcoverage: { provider: 'v8', reporter: ['text', 'lcov'] },` : null,
22
32
  `\t},`,
23
33
  `});`,
@@ -26,10 +36,12 @@ export default {
26
36
  pkg.scripts.test = 'vitest run';
27
37
  pkg.scripts['test:watch'] = 'vitest';
28
38
  pkg.devDependencies.vitest = '^2.0.0';
29
- if (cfg.isReact) {
39
+ if (cfg.hasFramework) {
30
40
  pkg.devDependencies.jsdom = '^25.0.0';
31
- pkg.devDependencies['@testing-library/react'] = '^16.0.0';
32
41
  pkg.devDependencies['@testing-library/dom'] = '^10.0.0';
42
+ if (cfg.isReact) pkg.devDependencies['@testing-library/react'] = '^16.0.0';
43
+ if (cfg.isVue) pkg.devDependencies['@testing-library/vue'] = '^8.1.0';
44
+ if (cfg.isSvelte) pkg.devDependencies['@testing-library/svelte'] = '^5.2.0';
33
45
  }
34
46
  if (cfg.coverage) {
35
47
  pkg.scripts.coverage = 'vitest run --coverage';
@@ -65,16 +77,46 @@ function importPath(runner, cfg) {
65
77
 
66
78
  function exampleTest(runner, cfg) {
67
79
  const imp = importPath(runner, cfg);
68
- if (cfg.isReact) {
80
+ if (cfg.hasService) {
69
81
  const api = runner === 'jest' ? '' : `import { describe, it, expect } from 'vitest';\n`;
70
82
  return [
71
- api + `import { render, screen } from '@testing-library/react';`,
72
- `import { Button } from '${imp}';`,
83
+ api + `import { app } from './app.js';`,
73
84
  ``,
74
- `describe('Button', () => {`,
75
- `\tit('renders its label', () => {`,
76
- `\t\trender(<Button label="Click me" />);`,
77
- `\t\texpect(screen.getByText('Click me')).toBeDefined();`,
85
+ `describe('app', () => {`,
86
+ `\tit('responds on /', async () => {`,
87
+ `\t\tconst res = await app.request('/');`,
88
+ `\t\texpect(res.status).toBe(200);`,
89
+ `\t});`,
90
+ `});`,
91
+ ``,
92
+ ].join('\n');
93
+ }
94
+ if (cfg.hasFramework) {
95
+ const api = runner === 'jest' ? '' : `import { describe, it, expect } from 'vitest';\n`;
96
+ const app = cfg.hasApp;
97
+ const label = app ? '/Hello from/' : `'Click me'`;
98
+ let lib, importLine, renderCall;
99
+ if (cfg.isReact) {
100
+ lib = '@testing-library/react';
101
+ importLine = app ? `import { App } from './App.js';` : `import { Button } from './index.js';`;
102
+ renderCall = app ? `render(<App />)` : `render(<Button label="Click me" />)`;
103
+ } else if (cfg.isVue) {
104
+ lib = '@testing-library/vue';
105
+ importLine = app ? `import App from './App.vue';` : `import { Button } from './index.js';`;
106
+ renderCall = app ? `render(App)` : `render(Button, { props: { label: 'Click me' } })`;
107
+ } else {
108
+ lib = '@testing-library/svelte';
109
+ importLine = app ? `import App from './App.svelte';` : `import Button from './Button.svelte';`;
110
+ renderCall = app ? `render(App)` : `render(Button, { props: { label: 'Click me' } })`;
111
+ }
112
+ return [
113
+ api + `import { render, screen } from '${lib}';`,
114
+ importLine,
115
+ ``,
116
+ `describe('${app ? 'App' : 'Button'}', () => {`,
117
+ `\tit('renders', () => {`,
118
+ `\t\t${renderCall};`,
119
+ `\t\texpect(screen.getByText(${label})).toBeDefined();`,
78
120
  `\t});`,
79
121
  `});`,
80
122
  ``,
@@ -4,19 +4,20 @@ export default {
4
4
  id: 'typescript',
5
5
  active: (cfg) => cfg.isTs,
6
6
  apply(cfg) {
7
- const noBuild = cfg.bundler === 'none';
7
+ const noBuild = cfg.bundler === 'none' && !cfg.customBuild;
8
+ const webLibs = cfg.hasFramework || cfg.hasApp;
8
9
  const compilerOptions = {
9
10
  target: 'ES2022',
10
11
  module: 'ESNext',
11
12
  moduleResolution: 'Bundler',
12
- lib: cfg.isReact ? ['ES2022', 'DOM', 'DOM.Iterable'] : ['ES2022'],
13
+ lib: webLibs ? ['ES2022', 'DOM', 'DOM.Iterable'] : ['ES2022'],
13
14
  ...(cfg.isReact ? { jsx: 'react-jsx' } : {}),
14
15
  strict: true,
15
16
  noUncheckedIndexedAccess: true,
16
17
  esModuleInterop: true,
17
18
  skipLibCheck: true,
18
19
  forceConsistentCasingInFileNames: true,
19
- verbatimModuleSyntax: cfg.bundler !== 'none',
20
+ verbatimModuleSyntax: cfg.bundler !== 'none' && !cfg.hasFramework,
20
21
  declaration: true,
21
22
  };
22
23
  if (noBuild) {
@@ -0,0 +1,66 @@
1
+ // Vite build path: powers Vite SPA apps (React/Vue/Svelte) and Vue component
2
+ // libraries (SFCs need a real compiler). Owns the vite config, build scripts,
3
+ // and — for libraries — the package entry points. index.html + source come
4
+ // from frameworks.js. Svelte libraries ship source and don't come through here.
5
+
6
+ const PLUGIN = {
7
+ react: { import: `import react from '@vitejs/plugin-react';`, call: 'react()', dep: { '@vitejs/plugin-react': '^4.3.0' } },
8
+ vue: { import: `import vue from '@vitejs/plugin-vue';`, call: 'vue()', dep: { '@vitejs/plugin-vue': '^5.1.0' } },
9
+ svelte: { import: `import { svelte } from '@sveltejs/vite-plugin-svelte';`, call: 'svelte()', dep: { '@sveltejs/vite-plugin-svelte': '^4.0.0' } },
10
+ };
11
+
12
+ export default {
13
+ id: 'vite',
14
+ active: (cfg) => cfg.viteBuild,
15
+ apply(cfg) {
16
+ const files = {};
17
+ const pkg = { scripts: {}, devDependencies: { vite: '^5.4.0' } };
18
+ const p = PLUGIN[cfg.framework];
19
+ Object.assign(pkg.devDependencies, p.dep);
20
+ if (cfg.isVue && cfg.isTs) pkg.devDependencies['vue-tsc'] = '^2.0.0';
21
+
22
+ if (cfg.hasApp) {
23
+ // Front-end SPA — not a published package.
24
+ files[`vite.config.${cfg.ext}`] = [p.import, ``, `import { defineConfig } from 'vite';`, ``, `export default defineConfig({`, `\tplugins: [${p.call}],`, `});`, ``].join('\n');
25
+ pkg.private = true;
26
+ pkg.scripts.dev = 'vite';
27
+ // React/Vue get a type-check before build; Svelte types need svelte-check
28
+ // (heavier), so its app just builds.
29
+ const precheck = cfg.isTs && cfg.isReact ? 'tsc --noEmit && ' : cfg.isTs && cfg.isVue ? 'vue-tsc --noEmit && ' : '';
30
+ pkg.scripts.build = precheck + 'vite build';
31
+ pkg.scripts.preview = 'vite preview';
32
+ } else {
33
+ // Vue component library (lib build + declarations).
34
+ files[`vite.config.${cfg.ext}`] = [
35
+ p.import,
36
+ `import dts from 'vite-plugin-dts';`,
37
+ ``,
38
+ `import { defineConfig } from 'vite';`,
39
+ ``,
40
+ `export default defineConfig({`,
41
+ `\tplugins: [${p.call}, dts({ rollupTypes: true })],`,
42
+ `\tbuild: {`,
43
+ `\t\tlib: { entry: 'src/index.${cfg.ext}', formats: ['es', 'cjs'], fileName: (f) => (f === 'es' ? 'index.js' : 'index.cjs') },`,
44
+ `\t\trollupOptions: { external: ['vue'] },`,
45
+ `\t},`,
46
+ `});`,
47
+ ``,
48
+ ].join('\n');
49
+ pkg.scripts.build = 'vite build';
50
+ pkg.scripts.dev = 'vite build --watch';
51
+ pkg.devDependencies['vite-plugin-dts'] = '^4.0.0';
52
+ if (cfg.isVue) pkg.devDependencies['vue-tsc'] = '^2.0.0';
53
+ // entry points
54
+ pkg.files = ['dist'];
55
+ pkg.type = 'module';
56
+ pkg.main = './dist/index.cjs';
57
+ pkg.module = './dist/index.js';
58
+ pkg.types = './dist/index.d.ts';
59
+ pkg.exports = { '.': { types: './dist/index.d.ts', import: './dist/index.js', require: './dist/index.cjs' } };
60
+ }
61
+
62
+ pkg.scripts.clean = 'rimraf dist';
63
+ pkg.devDependencies.rimraf = '^6.0.0';
64
+ return { files, pkg };
65
+ },
66
+ };
@@ -72,7 +72,7 @@ function ciWorkflow(cfg, codecov) {
72
72
  if (cfg.isTs) jobs.push(` - run: ${pmRun(cfg, 'typecheck')}`);
73
73
  if (cfg.lint !== 'none') jobs.push(` - run: ${pmRun(cfg, 'lint')}`);
74
74
  if (cfg.test !== 'none') jobs.push(` - run: ${pmRun(cfg, codecov ? 'coverage' : 'test')}`);
75
- if (cfg.bundler !== 'none' || cfg.isTs) jobs.push(` - run: ${pmRun(cfg, 'build')}`);
75
+ if (cfg.hasBuild) jobs.push(` - run: ${pmRun(cfg, 'build')}`);
76
76
  const cov = codecov
77
77
  ? '\n - uses: codecov/codecov-action@v4\n with:\n token: ${{ secrets.CODECOV_TOKEN }}'
78
78
  : '';
@@ -131,7 +131,7 @@ function releaseWorkflow(cfg) {
131
131
  ' publish:',
132
132
  ' runs-on: ubuntu-latest',
133
133
  setupSteps(cfg),
134
- cfg.bundler !== 'none' || cfg.isTs ? ` - run: ${pmRun(cfg, 'build')}` : null,
134
+ cfg.hasBuild ? ` - run: ${pmRun(cfg, 'build')}` : null,
135
135
  ' - run: npm publish --provenance --access public',
136
136
  ' env:',
137
137
  ' NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}',
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,13 +33,17 @@ 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)' },
37
+ { value: 'app', label: 'App (Vite SPA)' },
36
38
  ],
37
39
  },
38
40
  framework: {
39
41
  group: 'core', type: 'select', label: 'Framework', default: 'none',
40
42
  choices: [
41
43
  { value: 'none', label: 'None (plain package)' },
42
- { value: 'react', label: 'React (component library)' },
44
+ { value: 'react', label: 'React' },
45
+ { value: 'vue', label: 'Vue' },
46
+ { value: 'svelte', label: 'Svelte' },
43
47
  ],
44
48
  },
45
49
  packageManager: {
@@ -86,6 +90,7 @@ export const OPTIONS = {
86
90
  ],
87
91
  },
88
92
  coverage: { group: 'quality', type: 'boolean', label: 'Coverage reporting', default: true },
93
+ storybook: { group: 'quality', type: 'boolean', label: 'Storybook (component libraries)', default: false },
89
94
 
90
95
  // ---- lint / format ----
91
96
  lint: {
@@ -196,16 +201,40 @@ export function normalizeConfig(input = {}) {
196
201
  // npm-publish + changesets are complementary; nothing to coerce, just noted.
197
202
 
198
203
  cfg.isReact = cfg.framework === 'react';
199
- // A React component library is a library; make sure it's targeted.
200
- if (cfg.isReact && !cfg.target.includes('library')) cfg.target = ['library', ...cfg.target];
204
+ cfg.isVue = cfg.framework === 'vue';
205
+ cfg.isSvelte = cfg.framework === 'svelte';
206
+ cfg.hasFramework = cfg.framework !== 'none';
207
+
208
+ cfg.hasApp = cfg.target.includes('app');
209
+ // A component-framework package that isn't an app is a library.
210
+ if (cfg.hasFramework && !cfg.hasApp && !cfg.target.includes('library')) {
211
+ cfg.target = ['library', ...cfg.target];
212
+ }
201
213
 
202
214
  cfg.isTs = cfg.language === 'ts';
203
215
  cfg.ext = cfg.isTs ? 'ts' : 'js';
204
- // Source files carry JSX for React; config files keep .ts/.js.
216
+ // JSX source extension for React; Vue/Svelte use their own component files.
205
217
  cfg.srcExt = cfg.isReact ? (cfg.isTs ? 'tsx' : 'jsx') : cfg.ext;
218
+
206
219
  cfg.hasLibrary = cfg.target.includes('library');
207
220
  cfg.hasCli = cfg.target.includes('cli');
221
+ cfg.hasService = cfg.target.includes('service');
222
+
223
+ // Vite builds apps and Vue libraries (SFCs); Svelte libraries ship source
224
+ // (no build); React libraries use tsup (JSX is native to esbuild).
225
+ cfg.viteBuild = cfg.hasApp || cfg.isVue;
226
+ cfg.svelteLib = cfg.isSvelte && !cfg.hasApp;
227
+ cfg.customBuild = cfg.viteBuild || cfg.svelteLib; // bundler.js steps aside
228
+ cfg.usesVite = cfg.viteBuild || cfg.isSvelte; // Svelte tests need the vite plugin too
229
+ // Whether a `build` script exists (Svelte libraries ship source, no build).
230
+ cfg.hasBuild = cfg.viteBuild || (!cfg.svelteLib && (cfg.bundler !== 'none' || cfg.isTs));
231
+
232
+ // Apps aren't published packages.
233
+ if (cfg.hasApp) cfg.moduleFormat = 'esm';
208
234
  cfg.hasEsm = cfg.moduleFormat === 'esm' || cfg.moduleFormat === 'dual';
209
235
  cfg.hasCjs = cfg.moduleFormat === 'cjs' || cfg.moduleFormat === 'dual';
236
+
237
+ // Storybook only applies to component libraries.
238
+ if (!cfg.hasFramework || cfg.hasApp || !cfg.hasLibrary) cfg.storybook = false;
210
239
  return cfg;
211
240
  }
@@ -8,6 +8,14 @@ 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
+ 'react-app': { language: 'ts', framework: 'react', target: ['app'], test: 'vitest', release: 'none', workflows: ['ci'] },
12
+ 'vue-lib': { language: 'ts', framework: 'vue', target: ['library'], test: 'vitest' },
13
+ 'svelte-lib': { language: 'ts', framework: 'svelte', target: ['library'], test: 'vitest' },
14
+ 'node-service': {
15
+ language: 'ts', target: ['service'], moduleFormat: 'esm', bundler: 'tsup',
16
+ test: 'vitest', lint: 'eslint-prettier', gitHooks: 'simple-git-hooks',
17
+ release: 'none', workflows: ['ci'], deps: 'renovate', agents: true, vscode: true,
18
+ },
11
19
  oss: {
12
20
  language: 'ts', target: ['library'], moduleFormat: 'dual', bundler: 'tsup',
13
21
  test: 'vitest', coverage: true, lint: 'eslint-prettier', gitHooks: 'simple-git-hooks',
@@ -29,3 +37,20 @@ export const PRESETS = {
29
37
  };
30
38
 
31
39
  export const PRESET_NAMES = Object.keys(PRESETS);
40
+
41
+ // Short gists for the CLI help and the web configurator (tooltips + description).
42
+ export const PRESET_INFO = {
43
+ 'ts-lib': 'TypeScript library — dual ESM/CJS, tsup, Vitest, ESLint.',
44
+ 'js-lib': 'JavaScript (ESM) library — tsup, Vitest, ESLint.',
45
+ 'ts-cli': 'TypeScript CLI + library — ESM, ships a bin.',
46
+ cli: 'TypeScript CLI tool — ESM, ships a bin.',
47
+ 'react-lib': 'React component library (TS) — JSX, peer deps, jsdom tests.',
48
+ 'react-lib-js': 'React component library (JS) — JSX, peer deps, jsdom tests.',
49
+ 'react-app': 'React SPA — Vite dev server, build, Testing Library.',
50
+ 'vue-lib': 'Vue component library — Vite lib build (SFCs), dual + types.',
51
+ 'svelte-lib': 'Svelte component library — ships source, peer svelte, jsdom tests.',
52
+ 'node-service': 'Node HTTP service (Hono) — tsx dev, tsup build, Dockerfile.',
53
+ oss: 'Full open-source library — coverage, CodeQL, Codecov, Renovate, Changesets.',
54
+ minimal: 'Bare TS library — tsup only, no tests/lint/CI.',
55
+ full: 'Everything on — library + CLI, all workflows and extras.',
56
+ };
@@ -1,25 +0,0 @@
1
- // React component-library support: react/react-dom as peers (so consumers bring
2
- // their own), plus dev copies for building and testing. tsup/rollup externalize
3
- // peerDependencies automatically, so React is never bundled into your package.
4
-
5
- export default {
6
- id: 'react',
7
- active: (cfg) => cfg.isReact,
8
- apply(cfg) {
9
- const pkg = {
10
- peerDependencies: {
11
- react: '>=18',
12
- 'react-dom': '>=18',
13
- },
14
- devDependencies: {
15
- react: '^18.3.0',
16
- 'react-dom': '^18.3.0',
17
- },
18
- };
19
- if (cfg.isTs) {
20
- pkg.devDependencies['@types/react'] = '^18.3.0';
21
- pkg.devDependencies['@types/react-dom'] = '^18.3.0';
22
- }
23
- return { files: {}, pkg };
24
- },
25
- };