create-packkit 0.1.1

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.
@@ -0,0 +1,33 @@
1
+ import { toJson } from '../render.js';
2
+
3
+ export default {
4
+ id: 'vscode',
5
+ active: (cfg) => cfg.vscode,
6
+ apply(cfg) {
7
+ const biome = cfg.lint === 'biome';
8
+ const settings = {
9
+ 'editor.formatOnSave': cfg.lint !== 'none',
10
+ 'editor.defaultFormatter': biome ? 'biomejs.biome' : 'esbenp.prettier-vscode',
11
+ 'editor.insertSpaces': false,
12
+ };
13
+ if (cfg.lint === 'eslint-prettier' || cfg.lint === 'oxlint') {
14
+ settings['editor.codeActionsOnSave'] = { 'source.fixAll': 'explicit' };
15
+ }
16
+
17
+ const recommendations = [];
18
+ if (biome) recommendations.push('biomejs.biome');
19
+ else {
20
+ if (cfg.lint === 'eslint-prettier') recommendations.push('dbaeumer.vscode-eslint');
21
+ if (cfg.lint !== 'none') recommendations.push('esbenp.prettier-vscode');
22
+ }
23
+ if (cfg.test === 'vitest') recommendations.push('vitest.explorer');
24
+
25
+ return {
26
+ files: {
27
+ '.vscode/settings.json': toJson(settings),
28
+ '.vscode/extensions.json': toJson({ recommendations }),
29
+ },
30
+ pkg: {},
31
+ };
32
+ },
33
+ };
@@ -0,0 +1,216 @@
1
+ import { toJson } from '../render.js';
2
+
3
+ // package-manager helpers ----------------------------------------------------
4
+ function pmInstall(cfg) {
5
+ return {
6
+ npm: 'npm ci',
7
+ pnpm: 'pnpm install --frozen-lockfile',
8
+ yarn: 'yarn install --immutable',
9
+ bun: 'bun install --frozen-lockfile',
10
+ }[cfg.packageManager];
11
+ }
12
+ function pmRun(cfg, script) {
13
+ return cfg.packageManager === 'npm' ? `npm run ${script}` : `${cfg.packageManager} ${script}`;
14
+ }
15
+ function setupSteps(cfg) {
16
+ const steps = [' - uses: actions/checkout@v4'];
17
+ if (cfg.packageManager === 'pnpm') steps.push(' - uses: pnpm/action-setup@v4');
18
+ if (cfg.packageManager === 'bun') {
19
+ steps.push(' - uses: oven-sh/setup-bun@v2');
20
+ } else {
21
+ steps.push(
22
+ ' - uses: actions/setup-node@v4',
23
+ ' with:',
24
+ ` node-version: '${cfg.nodeVersion}'`,
25
+ ` cache: '${cfg.packageManager === 'yarn' ? 'yarn' : cfg.packageManager === 'pnpm' ? 'pnpm' : 'npm'}'`,
26
+ );
27
+ }
28
+ steps.push(` - run: ${pmInstall(cfg)}`);
29
+ return steps.join('\n');
30
+ }
31
+
32
+ export default {
33
+ id: 'workflows',
34
+ active: (cfg) => (cfg.workflows && cfg.workflows.length) || cfg.deps !== 'none',
35
+ apply(cfg) {
36
+ const files = {};
37
+ const wf = cfg.workflows || [];
38
+
39
+ if (wf.includes('ci')) files['.github/workflows/ci.yml'] = ciWorkflow(cfg, wf.includes('codecov'));
40
+ if (wf.includes('npm-publish')) files['.github/workflows/release.yml'] = releaseWorkflow(cfg);
41
+ if (wf.includes('pages')) files['.github/workflows/pages.yml'] = pagesWorkflow(cfg);
42
+ if (wf.includes('codeql')) files['.github/workflows/codeql.yml'] = codeqlWorkflow();
43
+ if (wf.includes('stale')) files['.github/workflows/stale.yml'] = staleWorkflow();
44
+
45
+ if (cfg.deps === 'renovate') {
46
+ files['.github/renovate.json'] = toJson({
47
+ $schema: 'https://docs.renovatebot.com/renovate-schema.json',
48
+ extends: ['config:recommended', ':semanticCommits'],
49
+ });
50
+ } else if (cfg.deps === 'dependabot') {
51
+ files['.github/dependabot.yml'] = [
52
+ 'version: 2',
53
+ 'updates:',
54
+ ' - package-ecosystem: npm',
55
+ ' directory: "/"',
56
+ ' schedule:',
57
+ ' interval: weekly',
58
+ ' - package-ecosystem: github-actions',
59
+ ' directory: "/"',
60
+ ' schedule:',
61
+ ' interval: weekly',
62
+ '',
63
+ ].join('\n');
64
+ }
65
+
66
+ return { files, pkg: {} };
67
+ },
68
+ };
69
+
70
+ function ciWorkflow(cfg, codecov) {
71
+ const jobs = [];
72
+ if (cfg.isTs) jobs.push(` - run: ${pmRun(cfg, 'typecheck')}`);
73
+ if (cfg.lint !== 'none') jobs.push(` - run: ${pmRun(cfg, 'lint')}`);
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')}`);
76
+ const cov = codecov
77
+ ? '\n - uses: codecov/codecov-action@v4\n with:\n token: ${{ secrets.CODECOV_TOKEN }}'
78
+ : '';
79
+ return [
80
+ 'name: CI',
81
+ 'on:',
82
+ ' push:',
83
+ ' branches: [main]',
84
+ ' pull_request:',
85
+ 'jobs:',
86
+ ' ci:',
87
+ ' runs-on: ubuntu-latest',
88
+ setupSteps(cfg),
89
+ jobs.join('\n') + cov,
90
+ '',
91
+ ].join('\n');
92
+ }
93
+
94
+ function releaseWorkflow(cfg) {
95
+ if (cfg.release === 'changesets') {
96
+ return [
97
+ 'name: Release',
98
+ 'on:',
99
+ ' push:',
100
+ ' branches: [main]',
101
+ 'concurrency: ${{ github.workflow }}-${{ github.ref }}',
102
+ 'permissions:',
103
+ ' contents: write',
104
+ ' pull-requests: write',
105
+ ' id-token: write',
106
+ 'jobs:',
107
+ ' release:',
108
+ ' runs-on: ubuntu-latest',
109
+ setupSteps(cfg),
110
+ ' - uses: changesets/action@v1',
111
+ ' with:',
112
+ ` publish: ${pmRun(cfg, 'release')}`,
113
+ ' version: ' + pmRun(cfg, 'version'),
114
+ ' env:',
115
+ ' GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}',
116
+ ' NPM_CONFIG_PROVENANCE: "true"',
117
+ ' # add NPM_TOKEN as a repo secret, or use npm Trusted Publishing (OIDC)',
118
+ '',
119
+ ].join('\n');
120
+ }
121
+ // tag-triggered publish with provenance
122
+ return [
123
+ 'name: Publish',
124
+ 'on:',
125
+ ' push:',
126
+ ' tags: ["v*"]',
127
+ 'permissions:',
128
+ ' contents: read',
129
+ ' id-token: write',
130
+ 'jobs:',
131
+ ' publish:',
132
+ ' runs-on: ubuntu-latest',
133
+ setupSteps(cfg),
134
+ cfg.bundler !== 'none' || cfg.isTs ? ` - run: ${pmRun(cfg, 'build')}` : null,
135
+ ' - run: npm publish --provenance --access public',
136
+ ' env:',
137
+ ' NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}',
138
+ '',
139
+ ].filter((l) => l !== null).join('\n');
140
+ }
141
+
142
+ function pagesWorkflow(cfg) {
143
+ return [
144
+ 'name: Deploy Pages',
145
+ 'on:',
146
+ ' push:',
147
+ ' branches: [main]',
148
+ 'permissions:',
149
+ ' contents: read',
150
+ ' pages: write',
151
+ ' id-token: write',
152
+ 'concurrency:',
153
+ ' group: pages',
154
+ ' cancel-in-progress: true',
155
+ 'jobs:',
156
+ ' deploy:',
157
+ ' environment:',
158
+ ' name: github-pages',
159
+ ' url: ${{ steps.deployment.outputs.page_url }}',
160
+ ' runs-on: ubuntu-latest',
161
+ ' steps:',
162
+ ' - uses: actions/checkout@v4',
163
+ ' - uses: actions/configure-pages@v5',
164
+ ' - uses: actions/upload-pages-artifact@v3',
165
+ ' with:',
166
+ ' path: ./docs',
167
+ ' - id: deployment',
168
+ ' uses: actions/deploy-pages@v4',
169
+ '',
170
+ ].join('\n');
171
+ }
172
+
173
+ function codeqlWorkflow() {
174
+ return [
175
+ 'name: CodeQL',
176
+ 'on:',
177
+ ' push:',
178
+ ' branches: [main]',
179
+ ' pull_request:',
180
+ ' schedule:',
181
+ ' - cron: "0 6 * * 1"',
182
+ 'jobs:',
183
+ ' analyze:',
184
+ ' runs-on: ubuntu-latest',
185
+ ' permissions:',
186
+ ' security-events: write',
187
+ ' steps:',
188
+ ' - uses: actions/checkout@v4',
189
+ ' - uses: github/codeql-action/init@v3',
190
+ ' with:',
191
+ ' languages: javascript-typescript',
192
+ ' - uses: github/codeql-action/analyze@v3',
193
+ '',
194
+ ].join('\n');
195
+ }
196
+
197
+ function staleWorkflow() {
198
+ return [
199
+ 'name: Stale',
200
+ 'on:',
201
+ ' schedule:',
202
+ ' - cron: "0 0 * * *"',
203
+ 'jobs:',
204
+ ' stale:',
205
+ ' runs-on: ubuntu-latest',
206
+ ' permissions:',
207
+ ' issues: write',
208
+ ' pull-requests: write',
209
+ ' steps:',
210
+ ' - uses: actions/stale@v9',
211
+ ' with:',
212
+ ' days-before-stale: 60',
213
+ ' days-before-close: 7',
214
+ '',
215
+ ].join('\n');
216
+ }
@@ -0,0 +1,73 @@
1
+ // Packkit core — pure, dependency-free, runs in Node AND the browser.
2
+ // generate(config) -> { config, files: { path: contents }, postCommands, summary }
3
+ // The CLI writes `files` to disk; the web configurator zips them.
4
+
5
+ import { normalizeConfig, OPTIONS, GROUPS, defaultConfig } from './options.js';
6
+ import { deepMerge, toJson } from './render.js';
7
+ import { finalizePackageJson } from './pkg.js';
8
+ import features from './features/index.js';
9
+ import { PRESETS, PRESET_NAMES } from './presets.js';
10
+
11
+ export { OPTIONS, GROUPS, defaultConfig, normalizeConfig, PRESETS, PRESET_NAMES };
12
+
13
+ /** Apply a named preset over the defaults, returning a full config. */
14
+ export function fromPreset(name, overrides = {}) {
15
+ const preset = PRESETS[name];
16
+ if (!preset) throw new Error(`Unknown preset "${name}". Known: ${PRESET_NAMES.join(', ')}`);
17
+ return normalizeConfig({ ...preset, ...overrides });
18
+ }
19
+
20
+ /** Turn a config into a complete set of files. */
21
+ export function generate(input) {
22
+ const cfg = normalizeConfig(input);
23
+ const files = {};
24
+ let pkg = {};
25
+
26
+ for (const feat of features) {
27
+ if (!feat.active(cfg)) continue;
28
+ const out = feat.apply(cfg) || {};
29
+ if (out.files) {
30
+ for (const [path, contents] of Object.entries(out.files)) files[path] = contents;
31
+ }
32
+ if (out.pkg) pkg = deepMerge(pkg, out.pkg);
33
+ }
34
+
35
+ files['package.json'] = toJson(finalizePackageJson(pkg));
36
+
37
+ return {
38
+ config: cfg,
39
+ files,
40
+ postCommands: postCommands(cfg),
41
+ summary: summarize(cfg, files),
42
+ };
43
+ }
44
+
45
+ function postCommands(cfg) {
46
+ const install = {
47
+ npm: 'npm install',
48
+ pnpm: 'pnpm install',
49
+ yarn: 'yarn install',
50
+ bun: 'bun install',
51
+ }[cfg.packageManager];
52
+ const cmds = [];
53
+ if (cfg.gitInit) cmds.push('git init', 'git add -A', 'git commit -m "Initial commit from Packkit"');
54
+ if (cfg.install) cmds.push(install);
55
+ return cmds;
56
+ }
57
+
58
+ function summarize(cfg, files) {
59
+ return {
60
+ name: cfg.name,
61
+ fileCount: Object.keys(files).length,
62
+ stack: [
63
+ cfg.isTs ? 'TypeScript' : 'JavaScript',
64
+ cfg.moduleFormat.toUpperCase(),
65
+ cfg.target.join('+'),
66
+ cfg.bundler !== 'none' ? cfg.bundler : (cfg.isTs ? 'tsc' : 'no-build'),
67
+ cfg.test !== 'none' ? cfg.test : null,
68
+ cfg.lint !== 'none' ? cfg.lint : null,
69
+ cfg.release !== 'none' ? cfg.release : null,
70
+ ].filter(Boolean),
71
+ workflows: cfg.workflows,
72
+ };
73
+ }
@@ -0,0 +1,193 @@
1
+ // The single source of truth for every choice Packkit offers.
2
+ // Both the CLI wizard and the web configurator render from this schema, and
3
+ // `normalizeConfig` turns partial/user input into a complete, valid config.
4
+
5
+ /** @typedef {'select'|'multiselect'|'boolean'|'text'} OptionType */
6
+
7
+ export const OPTIONS = {
8
+ // ---- package metadata ----
9
+ name: { group: 'meta', type: 'text', label: 'Package name', default: 'my-package' },
10
+ description: { group: 'meta', type: 'text', label: 'Description', default: '' },
11
+ author: { group: 'meta', type: 'text', label: 'Author', default: '' },
12
+ keywords: { group: 'meta', type: 'text', label: 'Keywords (comma-separated)', default: '' },
13
+ repo: { group: 'meta', type: 'text', label: 'Repository URL', default: '' },
14
+
15
+ // ---- core shape ----
16
+ language: {
17
+ group: 'core', type: 'select', label: 'Language', default: 'ts',
18
+ choices: [
19
+ { value: 'ts', label: 'TypeScript (strict)' },
20
+ { value: 'js', label: 'JavaScript (ESM)' },
21
+ ],
22
+ },
23
+ moduleFormat: {
24
+ group: 'core', type: 'select', label: 'Module format', default: 'dual',
25
+ choices: [
26
+ { value: 'esm', label: 'ESM only' },
27
+ { value: 'cjs', label: 'CommonJS only' },
28
+ { value: 'dual', label: 'Dual (ESM + CJS)' },
29
+ ],
30
+ },
31
+ target: {
32
+ group: 'core', type: 'multiselect', label: 'What are you building?', default: ['library'],
33
+ choices: [
34
+ { value: 'library', label: 'Library (importable package)' },
35
+ { value: 'cli', label: 'CLI tool (ships a bin)' },
36
+ ],
37
+ },
38
+ packageManager: {
39
+ group: 'core', type: 'select', label: 'Package manager', default: 'npm',
40
+ choices: [
41
+ { value: 'npm', label: 'npm' },
42
+ { value: 'pnpm', label: 'pnpm' },
43
+ { value: 'yarn', label: 'yarn' },
44
+ { value: 'bun', label: 'bun' },
45
+ ],
46
+ },
47
+ nodeVersion: {
48
+ group: 'core', type: 'select', label: 'Node version', default: '20',
49
+ choices: [
50
+ { value: '18', label: '18 (LTS)' },
51
+ { value: '20', label: '20 (LTS)' },
52
+ { value: '22', label: '22 (LTS)' },
53
+ ],
54
+ },
55
+
56
+ // ---- build ----
57
+ bundler: {
58
+ group: 'build', type: 'select', label: 'Bundler / build', default: 'tsup',
59
+ choices: [
60
+ { value: 'tsup', label: 'tsup (esbuild — recommended)' },
61
+ { value: 'tsdown', label: 'tsdown (Rolldown — fast successor)' },
62
+ { value: 'unbuild', label: 'unbuild (UnJS)' },
63
+ { value: 'rollup', label: 'Rollup' },
64
+ { value: 'none', label: 'None (tsc / no build)' },
65
+ ],
66
+ },
67
+
68
+ // ---- testing ----
69
+ test: {
70
+ group: 'quality', type: 'select', label: 'Test runner', default: 'vitest',
71
+ choices: [
72
+ { value: 'vitest', label: 'Vitest (recommended)' },
73
+ { value: 'jest', label: 'Jest' },
74
+ { value: 'node', label: 'node:test (built-in)' },
75
+ { value: 'none', label: 'None' },
76
+ ],
77
+ },
78
+ coverage: { group: 'quality', type: 'boolean', label: 'Coverage reporting', default: true },
79
+
80
+ // ---- lint / format ----
81
+ lint: {
82
+ group: 'quality', type: 'select', label: 'Lint / format', default: 'eslint-prettier',
83
+ choices: [
84
+ { value: 'eslint-prettier', label: 'ESLint + Prettier (recommended)' },
85
+ { value: 'biome', label: 'Biome (all-in-one)' },
86
+ { value: 'oxlint', label: 'oxlint + Prettier (fast)' },
87
+ { value: 'none', label: 'None' },
88
+ ],
89
+ },
90
+
91
+ // ---- git hooks ----
92
+ gitHooks: {
93
+ group: 'quality', type: 'select', label: 'Git hooks', default: 'simple-git-hooks',
94
+ choices: [
95
+ { value: 'simple-git-hooks', label: 'simple-git-hooks (light)' },
96
+ { value: 'husky', label: 'husky + lint-staged' },
97
+ { value: 'lefthook', label: 'lefthook' },
98
+ { value: 'none', label: 'None' },
99
+ ],
100
+ },
101
+
102
+ // ---- release ----
103
+ release: {
104
+ group: 'release', type: 'select', label: 'Release / versioning', default: 'changesets',
105
+ choices: [
106
+ { value: 'changesets', label: 'Changesets (recommended)' },
107
+ { value: 'release-it', label: 'release-it' },
108
+ { value: 'np', label: 'np' },
109
+ { value: 'none', label: 'None' },
110
+ ],
111
+ },
112
+
113
+ // ---- github actions (configurable workflows) ----
114
+ workflows: {
115
+ group: 'ci', type: 'multiselect', label: 'GitHub Actions',
116
+ default: ['ci', 'npm-publish'],
117
+ choices: [
118
+ { value: 'ci', label: 'CI (typecheck + lint + test + build)' },
119
+ { value: 'npm-publish', label: 'Publish to npm (provenance / OIDC)' },
120
+ { value: 'pages', label: 'Deploy GitHub Pages' },
121
+ { value: 'codeql', label: 'CodeQL security scan' },
122
+ { value: 'codecov', label: 'Upload coverage to Codecov' },
123
+ { value: 'stale', label: 'Stale issue/PR bot' },
124
+ ],
125
+ },
126
+ deps: {
127
+ group: 'ci', type: 'select', label: 'Dependency updates', default: 'renovate',
128
+ choices: [
129
+ { value: 'renovate', label: 'Renovate' },
130
+ { value: 'dependabot', label: 'Dependabot' },
131
+ { value: 'none', label: 'None' },
132
+ ],
133
+ },
134
+
135
+ // ---- repo hygiene ----
136
+ license: {
137
+ group: 'repo', type: 'select', label: 'License', default: 'MIT',
138
+ choices: [
139
+ { value: 'MIT', label: 'MIT' },
140
+ { value: 'Apache-2.0', label: 'Apache-2.0' },
141
+ { value: 'ISC', label: 'ISC' },
142
+ { value: 'none', label: 'None' },
143
+ ],
144
+ },
145
+ community: { group: 'repo', type: 'boolean', label: 'Community files (CONTRIBUTING, CoC, SECURITY, templates)', default: true },
146
+ agents: { group: 'repo', type: 'boolean', label: 'AI agent instructions (AGENTS.md + CLAUDE.md)', default: true },
147
+ vscode: { group: 'repo', type: 'boolean', label: 'VS Code settings + extensions', default: true },
148
+ editorconfig: { group: 'repo', type: 'boolean', label: '.editorconfig', default: true },
149
+ gitInit: { group: 'repo', type: 'boolean', label: 'git init + initial commit', default: true },
150
+ install: { group: 'repo', type: 'boolean', label: 'Install dependencies', default: true },
151
+ };
152
+
153
+ export const GROUPS = [
154
+ { id: 'meta', label: 'Package' },
155
+ { id: 'core', label: 'Core' },
156
+ { id: 'build', label: 'Build' },
157
+ { id: 'quality', label: 'Quality' },
158
+ { id: 'release', label: 'Release' },
159
+ { id: 'ci', label: 'CI / CD' },
160
+ { id: 'repo', label: 'Repository' },
161
+ ];
162
+
163
+ /** Build a default config from the schema. */
164
+ export function defaultConfig() {
165
+ const cfg = {};
166
+ for (const [key, opt] of Object.entries(OPTIONS)) {
167
+ cfg[key] = Array.isArray(opt.default) ? [...opt.default] : opt.default;
168
+ }
169
+ return cfg;
170
+ }
171
+
172
+ /** Merge partial input over the defaults and coerce a few fields. */
173
+ export function normalizeConfig(input = {}) {
174
+ const cfg = { ...defaultConfig(), ...input };
175
+
176
+ // A CLI target needs an executable build; JS `strict` isn't a thing, etc.
177
+ if (!Array.isArray(cfg.target) || cfg.target.length === 0) cfg.target = ['library'];
178
+ if (!Array.isArray(cfg.workflows)) cfg.workflows = [];
179
+
180
+ // Coverage only makes sense with a test runner that supports it.
181
+ if (cfg.test === 'none' || cfg.test === 'node') cfg.coverage = false;
182
+ // Codecov workflow implies coverage.
183
+ if (cfg.workflows.includes('codecov')) cfg.coverage = true;
184
+ // npm-publish + changesets are complementary; nothing to coerce, just noted.
185
+
186
+ cfg.isTs = cfg.language === 'ts';
187
+ cfg.ext = cfg.isTs ? 'ts' : 'js';
188
+ cfg.hasLibrary = cfg.target.includes('library');
189
+ cfg.hasCli = cfg.target.includes('cli');
190
+ cfg.hasEsm = cfg.moduleFormat === 'esm' || cfg.moduleFormat === 'dual';
191
+ cfg.hasCjs = cfg.moduleFormat === 'cjs' || cfg.moduleFormat === 'dual';
192
+ return cfg;
193
+ }
@@ -0,0 +1,31 @@
1
+ // Finalize the accumulated package.json: order top-level keys sensibly and
2
+ // sort dependency maps alphabetically (matches what `npm`/formatters produce).
3
+
4
+ const KEY_ORDER = [
5
+ 'name', 'version', 'description', 'keywords', 'homepage', 'bugs', 'repository',
6
+ 'license', 'author', 'type', 'exports', 'main', 'module', 'types', 'bin',
7
+ 'files', 'engines', 'packageManager', 'scripts', 'simple-git-hooks', 'lint-staged',
8
+ 'dependencies', 'peerDependencies', 'devDependencies', 'publishConfig',
9
+ ];
10
+
11
+ export function finalizePackageJson(pkg) {
12
+ const out = {};
13
+ for (const key of KEY_ORDER) {
14
+ if (pkg[key] === undefined) continue;
15
+ out[key] = isDepMap(key) ? sortKeys(pkg[key]) : pkg[key];
16
+ }
17
+ // Carry through anything not in the canonical order (future-proofing).
18
+ for (const key of Object.keys(pkg)) {
19
+ if (!(key in out)) out[key] = pkg[key];
20
+ }
21
+ return out;
22
+ }
23
+
24
+ function isDepMap(key) {
25
+ return key === 'dependencies' || key === 'devDependencies' || key === 'peerDependencies';
26
+ }
27
+
28
+ function sortKeys(obj) {
29
+ if (!obj || typeof obj !== 'object') return obj;
30
+ return Object.fromEntries(Object.keys(obj).sort().map((k) => [k, obj[k]]));
31
+ }
@@ -0,0 +1,23 @@
1
+ // Named presets = partial configs applied over the defaults, so devs can skip
2
+ // the wizard: `npx packkit ts-lib my-project`.
3
+
4
+ export const PRESETS = {
5
+ 'ts-lib': { language: 'ts', target: ['library'], moduleFormat: 'dual' },
6
+ 'js-lib': { language: 'js', target: ['library'], moduleFormat: 'dual', bundler: 'tsup' },
7
+ 'ts-cli': { language: 'ts', target: ['cli', 'library'], moduleFormat: 'esm' },
8
+ cli: { language: 'ts', target: ['cli', 'library'], moduleFormat: 'esm' },
9
+ minimal: {
10
+ language: 'ts', target: ['library'], moduleFormat: 'dual', bundler: 'tsup',
11
+ test: 'none', lint: 'none', gitHooks: 'none', release: 'none',
12
+ workflows: ['ci'], deps: 'none', community: false, agents: false, vscode: false,
13
+ },
14
+ full: {
15
+ language: 'ts', target: ['library', 'cli'], moduleFormat: 'dual', bundler: 'tsup',
16
+ test: 'vitest', coverage: true, lint: 'eslint-prettier', gitHooks: 'simple-git-hooks',
17
+ release: 'changesets',
18
+ workflows: ['ci', 'npm-publish', 'pages', 'codeql', 'codecov', 'stale'],
19
+ deps: 'renovate', community: true, agents: true, vscode: true,
20
+ },
21
+ };
22
+
23
+ export const PRESET_NAMES = Object.keys(PRESETS);
@@ -0,0 +1,35 @@
1
+ // Minimal {{var}} template rendering — feature modules mostly use template
2
+ // literals directly; this is for the few multi-line string templates.
3
+
4
+ /** Replace {{ key }} / {{ a.b }} tokens from `vars`. Missing keys render empty. */
5
+ export function render(template, vars = {}) {
6
+ return template.replace(/\{\{\s*([\w.]+)\s*\}\}/g, (_, key) => {
7
+ const val = key.split('.').reduce((o, k) => (o == null ? o : o[k]), vars);
8
+ return val == null ? '' : String(val);
9
+ });
10
+ }
11
+
12
+ /** Deep-merge plain objects (arrays are concatenated + de-duped). Used to fold
13
+ * each feature's package.json fragment into the accumulator. */
14
+ export function deepMerge(target, source) {
15
+ if (Array.isArray(target) && Array.isArray(source)) {
16
+ return [...new Set([...target, ...source])];
17
+ }
18
+ if (isPlainObject(target) && isPlainObject(source)) {
19
+ const out = { ...target };
20
+ for (const [k, v] of Object.entries(source)) {
21
+ out[k] = k in target ? deepMerge(target[k], v) : v;
22
+ }
23
+ return out;
24
+ }
25
+ return source;
26
+ }
27
+
28
+ function isPlainObject(v) {
29
+ return v != null && typeof v === 'object' && !Array.isArray(v);
30
+ }
31
+
32
+ /** Stable, human-friendly stringify with 2-space indent and trailing newline. */
33
+ export function toJson(obj) {
34
+ return JSON.stringify(obj, null, 2) + '\n';
35
+ }