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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 DanMat
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # Packkit 📦
2
+
3
+ > A highly configurable scaffolder for modern **npm packages and CLIs** — pick your stack from a CLI **or** a web configurator, and get a ready-to-ship repo.
4
+
5
+ [![Configure on the web](https://img.shields.io/badge/configure-on%20the%20web-00e5ff)](https://danmat.github.io/create-packkit/)
6
+ [![npm](https://img.shields.io/npm/v/create-packkit)](https://www.npmjs.com/package/create-packkit)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
8
+
9
+ Most scaffolders lock you into one stack, one language, and the terminal. Packkit lets you **choose** — TypeScript or JavaScript, library or CLI, ESM/CJS/dual, your bundler, test runner, linter, git hooks, release flow, GitHub Actions and more — and it works from a CLI **or** a browser page that downloads your project as a zip.
10
+
11
+ ## Quick start
12
+
13
+ ```sh
14
+ # interactive wizard
15
+ npm create packkit@latest
16
+ # or the short alias
17
+ npx packkit
18
+
19
+ # skip the wizard with a preset
20
+ npx packkit ts-lib my-lib
21
+ npx packkit cli my-tool
22
+ npx packkit --preset full my-pkg --pm pnpm
23
+ ```
24
+
25
+ Then `cd`, and you already have a working project — `build`, `test`, and `lint` all pass out of the box.
26
+
27
+ ## Or configure it on the web
28
+
29
+ No install needed: **[danmat.github.io/create-packkit](https://danmat.github.io/create-packkit/)** — tick the options, preview the file tree, and **download a zip** (or copy the equivalent `npx packkit` command). Everything runs in your browser.
30
+
31
+ ## What you can pick
32
+
33
+ | Area | Options |
34
+ |---|---|
35
+ | **Language** | TypeScript (strict) · JavaScript (ESM) |
36
+ | **Module format** | ESM · CJS · dual (proper `exports` map) |
37
+ | **Target** | Library · CLI tool · both |
38
+ | **Bundler** | tsup · tsdown · unbuild · rollup · none (tsc) |
39
+ | **Tests** | Vitest · Jest · node:test · none (+ coverage) |
40
+ | **Lint/format** | ESLint + Prettier · Biome · oxlint · none |
41
+ | **Git hooks** | simple-git-hooks · husky + lint-staged · lefthook · none |
42
+ | **Release** | Changesets · release-it · np · none |
43
+ | **GitHub Actions** | CI · npm publish (provenance) · Pages · CodeQL · Codecov · stale bot |
44
+ | **Deps** | Renovate · Dependabot · none |
45
+ | **Repo** | LICENSE · community files · **AGENTS.md + CLAUDE.md** · VS Code · `.editorconfig` |
46
+ | **Package manager** | npm · pnpm · yarn · bun |
47
+
48
+ ## Presets
49
+
50
+ `ts-lib` · `js-lib` · `ts-cli` / `cli` · `minimal` · `full` — named bundles of the options above.
51
+
52
+ ## How it works
53
+
54
+ Packkit is a pure `config → { files }` **core** that runs in both Node and the browser:
55
+
56
+ - the **CLI** writes the files to disk, runs `git init`, and installs dependencies;
57
+ - the **web configurator** zips the same files client-side (no server).
58
+
59
+ Both drive from one options schema ([`src/core/options.js`](src/core/options.js)), so the CLI and the web page always stay in sync.
60
+
61
+ ## License
62
+
63
+ [MIT](LICENSE) © DanMat
package/bin/cli.js ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { run } from '../src/cli/index.js';
3
+
4
+ run().catch((err) => {
5
+ console.error(err instanceof Error ? err.message : err);
6
+ process.exit(1);
7
+ });
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "create-packkit",
3
+ "version": "0.1.1",
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
+ "type": "module",
6
+ "bin": {
7
+ "create-packkit": "bin/cli.js",
8
+ "packkit": "bin/cli.js"
9
+ },
10
+ "exports": {
11
+ ".": "./src/core/index.js",
12
+ "./core": "./src/core/index.js",
13
+ "./cli": "./src/cli/index.js"
14
+ },
15
+ "files": [
16
+ "bin",
17
+ "src",
18
+ "README.md",
19
+ "LICENSE"
20
+ ],
21
+ "engines": {
22
+ "node": ">=18"
23
+ },
24
+ "scripts": {
25
+ "start": "node bin/cli.js",
26
+ "test": "node --test",
27
+ "build:web": "esbuild src/core/index.js --bundle --format=esm --outfile=docs/packkit-core.js"
28
+ },
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/DanMat/create-packkit.git"
32
+ },
33
+ "keywords": [
34
+ "create",
35
+ "scaffold",
36
+ "scaffolding",
37
+ "generator",
38
+ "boilerplate",
39
+ "starter",
40
+ "template",
41
+ "npm-package",
42
+ "typescript",
43
+ "cli",
44
+ "tsup",
45
+ "vitest"
46
+ ],
47
+ "author": "DanMat <dannymatthew@gmail.com>",
48
+ "license": "MIT",
49
+ "bugs": {
50
+ "url": "https://github.com/DanMat/create-packkit/issues"
51
+ },
52
+ "homepage": "https://danmat.github.io/create-packkit/",
53
+ "dependencies": {
54
+ "@clack/prompts": "^0.7.0"
55
+ },
56
+ "devDependencies": {
57
+ "esbuild": "^0.23.0"
58
+ }
59
+ }
@@ -0,0 +1,62 @@
1
+ import { parseArgs } from 'node:util';
2
+ import { PRESET_NAMES } from '../core/presets.js';
3
+
4
+ // Map friendly flag names to config keys for non-interactive use.
5
+ const OVERRIDE_FLAGS = {
6
+ language: 'language',
7
+ module: 'moduleFormat',
8
+ bundler: 'bundler',
9
+ test: 'test',
10
+ lint: 'lint',
11
+ hooks: 'gitHooks',
12
+ release: 'release',
13
+ license: 'license',
14
+ pm: 'packageManager',
15
+ node: 'nodeVersion',
16
+ author: 'author',
17
+ description: 'description',
18
+ };
19
+
20
+ export function parseCliArgs(argv) {
21
+ const { values, positionals } = parseArgs({
22
+ args: argv,
23
+ allowPositionals: true,
24
+ options: {
25
+ preset: { type: 'string' },
26
+ name: { type: 'string' },
27
+ yes: { type: 'boolean', short: 'y' },
28
+ here: { type: 'boolean' },
29
+ 'no-install': { type: 'boolean' },
30
+ 'no-git': { type: 'boolean' },
31
+ target: { type: 'string', multiple: true },
32
+ help: { type: 'boolean', short: 'h' },
33
+ version: { type: 'boolean', short: 'v' },
34
+ ...Object.fromEntries(Object.keys(OVERRIDE_FLAGS).map((k) => [k, { type: 'string' }])),
35
+ },
36
+ });
37
+
38
+ // First positional may be a known preset; otherwise it's the project name.
39
+ let preset = values.preset;
40
+ const pos = [...positionals];
41
+ if (!preset && pos.length && PRESET_NAMES.includes(pos[0])) preset = pos.shift();
42
+ const name = values.name || pos[0];
43
+
44
+ const overrides = {};
45
+ for (const [flag, key] of Object.entries(OVERRIDE_FLAGS)) {
46
+ if (values[flag] != null) overrides[key] = values[flag];
47
+ }
48
+ if (values.target) overrides.target = values.target;
49
+ if (name) overrides.name = name;
50
+
51
+ return {
52
+ preset,
53
+ name,
54
+ here: !!values.here,
55
+ yes: !!values.yes,
56
+ install: !values['no-install'],
57
+ git: !values['no-git'],
58
+ help: !!values.help,
59
+ version: !!values.version,
60
+ overrides,
61
+ };
62
+ }
@@ -0,0 +1,112 @@
1
+ import { resolve, basename } from 'node:path';
2
+ import { readFileSync } from 'node:fs';
3
+ import { fileURLToPath } from 'node:url';
4
+ import * as p from '@clack/prompts';
5
+ import { generate, fromPreset, normalizeConfig, PRESET_NAMES } from '../core/index.js';
6
+ import { parseCliArgs } from './args.js';
7
+ import { runWizard } from './wizard.js';
8
+ import { writeProject, dirIsEmptyOrMissing, gitInit, installDeps } from './write.js';
9
+
10
+ const pkgVersion = () => {
11
+ try {
12
+ const url = new URL('../../package.json', import.meta.url);
13
+ return JSON.parse(readFileSync(fileURLToPath(url), 'utf8')).version;
14
+ } catch {
15
+ return '0.0.0';
16
+ }
17
+ };
18
+
19
+ const HELP = `
20
+ packkit — scaffold a modern npm package or CLI
21
+
22
+ Usage:
23
+ npm create packkit@latest [name] [options]
24
+ npx packkit [preset] [name] [options]
25
+
26
+ Presets: ${PRESET_NAMES.join(', ')}
27
+
28
+ Options:
29
+ --preset <name> Use a preset (skips the wizard)
30
+ --name <name> Package name
31
+ --here Scaffold into the current directory
32
+ -y, --yes Accept defaults / preset, no prompts
33
+ --no-install Skip dependency install
34
+ --no-git Skip git init
35
+ --pm <manager> npm | pnpm | yarn | bun
36
+ --language <ts|js> --module <esm|cjs|dual> --bundler <tsup|tsdown|unbuild|rollup|none>
37
+ --test <vitest|jest|node|none> --lint <eslint-prettier|biome|oxlint|none>
38
+ --license <MIT|Apache-2.0|ISC|none>
39
+ -h, --help Show this help
40
+ -v, --version Show version
41
+
42
+ Examples:
43
+ npx packkit ts-lib my-lib
44
+ npm create packkit@latest my-cli -- --preset cli
45
+ npx packkit --preset full my-pkg --pm pnpm
46
+ `;
47
+
48
+ export async function run(argv = process.argv.slice(2)) {
49
+ const args = parseCliArgs(argv);
50
+ if (args.help) return void console.log(HELP);
51
+ if (args.version) return void console.log(pkgVersion());
52
+
53
+ const interactive = !args.preset && !args.yes && process.stdout.isTTY;
54
+
55
+ let config;
56
+ if (interactive) {
57
+ p.intro('📦 Packkit');
58
+ config = normalizeConfig(await runWizard(args.overrides));
59
+ } else if (args.preset) {
60
+ config = fromPreset(args.preset, args.overrides);
61
+ } else {
62
+ config = normalizeConfig(args.overrides);
63
+ }
64
+
65
+ config.gitInit = args.git;
66
+ config.install = args.install;
67
+
68
+ // Resolve the target directory.
69
+ const targetDir = args.here ? process.cwd() : resolve(process.cwd(), config.name);
70
+ if (!args.here && !config.name) {
71
+ console.error('A package name is required (pass one, or run interactively).');
72
+ process.exit(1);
73
+ }
74
+ if (!dirIsEmptyOrMissing(targetDir)) {
75
+ console.error(`Target directory "${basename(targetDir)}" is not empty. Aborting.`);
76
+ process.exit(1);
77
+ }
78
+
79
+ // Generate + write.
80
+ const { files, summary } = generate(config);
81
+ await writeProject(targetDir, files);
82
+
83
+ // Post steps.
84
+ if (config.gitInit) gitInit(targetDir);
85
+ if (config.install) {
86
+ const s = p.spinner();
87
+ s.start(`Installing dependencies with ${config.packageManager}`);
88
+ const ok = installDeps(config.packageManager, targetDir);
89
+ s.stop(ok ? 'Dependencies installed' : 'Install skipped (run it manually)');
90
+ }
91
+
92
+ const rel = args.here ? '.' : config.name;
93
+ const next = [
94
+ args.here ? null : `cd ${rel}`,
95
+ 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,
98
+ ].filter(Boolean);
99
+
100
+ const done = `Created ${summary.name} — ${summary.fileCount} files · ${summary.stack.join(' · ')}`;
101
+ if (interactive) {
102
+ p.note(next.join('\n') || 'You are all set.', 'Next steps');
103
+ p.outro(done);
104
+ } else {
105
+ console.log(done);
106
+ if (next.length) console.log('\nNext steps:\n ' + next.join('\n '));
107
+ }
108
+ }
109
+
110
+ function runWord(config) {
111
+ return config.packageManager === 'npm' ? 'npm run' : config.packageManager;
112
+ }
@@ -0,0 +1,59 @@
1
+ import * as p from '@clack/prompts';
2
+ import { OPTIONS } from '../core/options.js';
3
+
4
+ function bail(value) {
5
+ if (p.isCancel(value)) {
6
+ p.cancel('Cancelled.');
7
+ process.exit(0);
8
+ }
9
+ return value;
10
+ }
11
+
12
+ const asOptions = (key) => OPTIONS[key].choices.map((c) => ({ value: c.value, label: c.label }));
13
+
14
+ /** Run the interactive wizard, returning a partial config. `seed` pre-fills. */
15
+ export async function runWizard(seed = {}) {
16
+ const cfg = { ...seed };
17
+
18
+ cfg.name = bail(
19
+ await p.text({
20
+ message: 'Package name',
21
+ placeholder: 'my-package',
22
+ initialValue: seed.name || '',
23
+ validate: (v) => (!v ? 'Required' : /^(@[\w.-]+\/)?[\w.-]+$/.test(v) ? undefined : 'Invalid npm name'),
24
+ }),
25
+ );
26
+
27
+ cfg.description = bail(await p.text({ message: 'Description', placeholder: '(optional)', defaultValue: '' }));
28
+ cfg.author = bail(await p.text({ message: 'Author', placeholder: '(optional)', defaultValue: seed.author || '' }));
29
+
30
+ cfg.language = bail(await p.select({ message: 'Language', options: asOptions('language'), initialValue: OPTIONS.language.default }));
31
+ cfg.target = bail(await p.multiselect({ message: 'What are you building?', options: asOptions('target'), initialValues: ['library'], required: true }));
32
+ cfg.moduleFormat = bail(await p.select({ message: 'Module format', options: asOptions('moduleFormat'), initialValue: OPTIONS.moduleFormat.default }));
33
+ cfg.packageManager = bail(await p.select({ message: 'Package manager', options: asOptions('packageManager'), initialValue: OPTIONS.packageManager.default }));
34
+ cfg.bundler = bail(await p.select({ message: 'Build / bundler', options: asOptions('bundler'), initialValue: OPTIONS.bundler.default }));
35
+ cfg.test = bail(await p.select({ message: 'Test runner', options: asOptions('test'), initialValue: OPTIONS.test.default }));
36
+ cfg.lint = bail(await p.select({ message: 'Lint / format', options: asOptions('lint'), initialValue: OPTIONS.lint.default }));
37
+ cfg.gitHooks = bail(await p.select({ message: 'Git hooks', options: asOptions('gitHooks'), initialValue: OPTIONS.gitHooks.default }));
38
+ cfg.release = bail(await p.select({ message: 'Release / versioning', options: asOptions('release'), initialValue: OPTIONS.release.default }));
39
+ cfg.workflows = bail(await p.multiselect({ message: 'GitHub Actions (space to toggle)', options: asOptions('workflows'), initialValues: OPTIONS.workflows.default, required: false }));
40
+ cfg.deps = bail(await p.select({ message: 'Dependency updates', options: asOptions('deps'), initialValue: OPTIONS.deps.default }));
41
+ cfg.license = bail(await p.select({ message: 'License', options: asOptions('license'), initialValue: OPTIONS.license.default }));
42
+
43
+ const extras = bail(
44
+ await p.multiselect({
45
+ message: 'Extras (space to toggle)',
46
+ options: [
47
+ { value: 'community', label: 'Community files (CONTRIBUTING, CoC, SECURITY, templates)' },
48
+ { value: 'agents', label: 'AI agent instructions (AGENTS.md + CLAUDE.md)' },
49
+ { value: 'vscode', label: 'VS Code settings' },
50
+ { value: 'editorconfig', label: '.editorconfig' },
51
+ ],
52
+ initialValues: ['community', 'agents', 'vscode', 'editorconfig'],
53
+ required: false,
54
+ }),
55
+ );
56
+ for (const key of ['community', 'agents', 'vscode', 'editorconfig']) cfg[key] = extras.includes(key);
57
+
58
+ return cfg;
59
+ }
@@ -0,0 +1,46 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises';
2
+ import { existsSync, readdirSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+ import { spawnSync } from 'node:child_process';
5
+
6
+ /** Write a { path: contents } map under `targetDir`. Returns the count. */
7
+ export async function writeProject(targetDir, files) {
8
+ for (const [rel, contents] of Object.entries(files)) {
9
+ const full = join(targetDir, rel);
10
+ await mkdir(dirname(full), { recursive: true });
11
+ await writeFile(full, contents);
12
+ }
13
+ return Object.keys(files).length;
14
+ }
15
+
16
+ /** True if the directory doesn't exist or contains no entries. */
17
+ export function dirIsEmptyOrMissing(dir) {
18
+ if (!existsSync(dir)) return true;
19
+ try {
20
+ return readdirSync(dir).length === 0;
21
+ } catch {
22
+ return false;
23
+ }
24
+ }
25
+
26
+ /** Run a command in `cwd` (arg array — no shell parsing). Never throws. */
27
+ export function run(cmd, args, cwd, { quiet = true } = {}) {
28
+ const res = spawnSync(cmd, args, {
29
+ cwd,
30
+ stdio: quiet ? 'ignore' : 'inherit',
31
+ shell: process.platform === 'win32',
32
+ });
33
+ return res.status === 0;
34
+ }
35
+
36
+ /** git init + initial commit. Returns true on success. */
37
+ export function gitInit(cwd) {
38
+ if (!run('git', ['init', '--quiet'], cwd)) return false;
39
+ run('git', ['add', '-A'], cwd);
40
+ return run('git', ['commit', '-m', 'Initial commit from Packkit', '--quiet'], cwd);
41
+ }
42
+
43
+ /** Install dependencies with the chosen package manager. */
44
+ export function installDeps(pm, cwd) {
45
+ return run(pm, ['install'], cwd, { quiet: false });
46
+ }
@@ -0,0 +1,57 @@
1
+ // AGENTS.md (the emerging cross-tool standard) + CLAUDE.md pointer. Describes
2
+ // the stack and the exact commands so coding agents work correctly out of the box.
3
+
4
+ export default {
5
+ id: 'agents',
6
+ active: (cfg) => cfg.agents,
7
+ apply(cfg) {
8
+ const run = (s) => (cfg.packageManager === 'npm' ? `npm run ${s}` : `${cfg.packageManager} ${s}`);
9
+ const test = cfg.packageManager === 'npm' ? 'npm test' : `${cfg.packageManager} test`;
10
+
11
+ const commands = [];
12
+ if (cfg.isTs) commands.push(`- Type-check: \`${run('typecheck')}\``);
13
+ if (cfg.lint !== 'none') commands.push(`- Lint: \`${run('lint')}\``);
14
+ if (cfg.test !== 'none') commands.push(`- Test: \`${test}\``);
15
+ if (cfg.bundler !== 'none' || cfg.isTs) commands.push(`- Build: \`${run('build')}\``);
16
+
17
+ const stack = [
18
+ `- Language: ${cfg.isTs ? 'TypeScript (strict)' : 'JavaScript (ESM)'}`,
19
+ `- Module format: ${cfg.moduleFormat.toUpperCase()}`,
20
+ `- Package manager: ${cfg.packageManager}`,
21
+ cfg.bundler !== 'none' ? `- Bundler: ${cfg.bundler}` : `- Build: ${cfg.isTs ? 'tsc' : 'none'}`,
22
+ cfg.test !== 'none' ? `- Tests: ${cfg.test}` : null,
23
+ cfg.lint !== 'none' ? `- Lint/format: ${cfg.lint}` : null,
24
+ ].filter(Boolean);
25
+
26
+ const agents = [
27
+ `# AGENTS.md`,
28
+ ``,
29
+ `Guidance for AI coding agents working in **${cfg.name}**.`,
30
+ ``,
31
+ `## Stack`,
32
+ ``,
33
+ ...stack,
34
+ ``,
35
+ `## Commands`,
36
+ ``,
37
+ ...commands,
38
+ ``,
39
+ `## Conventions`,
40
+ ``,
41
+ `- Source lives in \`src/\`. Keep the public API in \`src/index.${cfg.ext}\`.`,
42
+ `- Add or update tests for any behavior change.`,
43
+ cfg.isTs ? `- Prefer explicit types on exported functions; keep \`strict\` passing.` : `- Use ESM syntax and JSDoc types where helpful.`,
44
+ cfg.release === 'changesets' ? `- Run \`npx changeset\` after a user-facing change.` : null,
45
+ `- Do not commit \`dist/\` or \`node_modules/\`.`,
46
+ ``,
47
+ ].filter((l) => l !== null).join('\n');
48
+
49
+ return {
50
+ files: {
51
+ 'AGENTS.md': agents,
52
+ 'CLAUDE.md': `See [AGENTS.md](./AGENTS.md) for build/test commands and conventions.\n`,
53
+ },
54
+ pkg: {},
55
+ };
56
+ },
57
+ };
@@ -0,0 +1,118 @@
1
+ // Always-on. Owns the package entry points (exports/main/module/types/files)
2
+ // and the build tooling for the chosen bundler.
3
+
4
+ export default {
5
+ id: 'bundler',
6
+ active: () => true,
7
+ apply(cfg) {
8
+ const files = {};
9
+ const pkg = { scripts: {} };
10
+ const build = cfg.bundler !== 'none';
11
+
12
+ // ---- entry points ----
13
+ if (!build && !cfg.isTs) {
14
+ // Plain JS, no build: ship source directly.
15
+ pkg.files = ['src'];
16
+ pkg.exports = { '.': `./src/index.js` };
17
+ pkg.main = './src/index.js';
18
+ if (cfg.hasEsm) pkg.module = './src/index.js';
19
+ } else {
20
+ pkg.files = ['dist'];
21
+ const esm = './dist/index.js';
22
+ const cjs = './dist/index.cjs';
23
+ const dts = './dist/index.d.ts';
24
+ const exp = {};
25
+ if (cfg.isTs) exp.types = dts;
26
+ if (cfg.hasEsm) exp.import = esm;
27
+ if (cfg.hasCjs) exp.require = build ? cjs : './dist/index.cjs';
28
+ pkg.exports = { '.': exp };
29
+ if (cfg.hasCjs) pkg.main = exp.require;
30
+ else pkg.main = esm;
31
+ if (cfg.hasEsm) pkg.module = esm;
32
+ if (cfg.isTs) pkg.types = dts;
33
+ }
34
+
35
+ // ---- build tooling ----
36
+ const entries = ['src/index.' + cfg.ext];
37
+ if (cfg.hasCli) entries.push('src/cli.' + cfg.ext);
38
+ const formats = [cfg.hasEsm && 'esm', cfg.hasCjs && 'cjs'].filter(Boolean);
39
+
40
+ if (cfg.bundler === 'tsup' || cfg.bundler === 'tsdown') {
41
+ const tool = cfg.bundler;
42
+ files[`${tool}.config.${cfg.ext}`] = tsupConfig(cfg, entries, formats, tool);
43
+ pkg.scripts.build = tool;
44
+ pkg.scripts.dev = `${tool} --watch`;
45
+ pkg.devDependencies = { [tool]: tool === 'tsup' ? '^8.0.0' : '^0.6.0' };
46
+ } else if (cfg.bundler === 'unbuild') {
47
+ files['build.config.ts'] = unbuildConfig(cfg);
48
+ pkg.scripts.build = 'unbuild';
49
+ pkg.scripts.dev = 'unbuild --stub';
50
+ pkg.devDependencies = { unbuild: '^2.0.0' };
51
+ } else if (cfg.bundler === 'rollup') {
52
+ files[`rollup.config.${cfg.ext === 'ts' ? 'ts' : 'js'}`] = rollupConfig(cfg, formats);
53
+ pkg.scripts.build = 'rollup -c';
54
+ pkg.scripts.dev = 'rollup -c -w';
55
+ pkg.devDependencies = {
56
+ rollup: '^4.0.0',
57
+ ...(cfg.isTs ? { '@rollup/plugin-typescript': '^11.0.0', tslib: '^2.6.0' } : {}),
58
+ };
59
+ } else if (cfg.bundler === 'none' && cfg.isTs) {
60
+ // tsc-only build for TypeScript.
61
+ pkg.scripts.build = 'tsc';
62
+ pkg.scripts.dev = 'tsc --watch';
63
+ }
64
+
65
+ if (build) pkg.scripts.prepublishOnly = pkg.scripts.build;
66
+ pkg.scripts.clean = 'rimraf dist';
67
+ if (build) pkg.devDependencies = { ...pkg.devDependencies, rimraf: '^6.0.0' };
68
+
69
+ return { files, pkg };
70
+ },
71
+ };
72
+
73
+ function tsupConfig(cfg, entries, formats, tool) {
74
+ return [
75
+ `import { defineConfig } from '${tool}';`,
76
+ ``,
77
+ `export default defineConfig({`,
78
+ `\tentry: [${entries.map((e) => `'${e}'`).join(', ')}],`,
79
+ `\tformat: [${formats.map((f) => `'${f}'`).join(', ')}],`,
80
+ cfg.isTs ? `\tdts: true,` : null,
81
+ `\tsourcemap: true,`,
82
+ `\tclean: true,`,
83
+ `\ttreeshake: true,`,
84
+ `});`,
85
+ ``,
86
+ ].filter((l) => l !== null).join('\n');
87
+ }
88
+
89
+ function unbuildConfig(cfg) {
90
+ return [
91
+ `import { defineBuildConfig } from 'unbuild';`,
92
+ ``,
93
+ `export default defineBuildConfig({`,
94
+ `\tentries: ['src/index'],`,
95
+ `\tdeclaration: ${cfg.isTs},`,
96
+ `\tclean: true,`,
97
+ `\trollup: { emitCJS: ${cfg.hasCjs} },`,
98
+ `});`,
99
+ ``,
100
+ ].join('\n');
101
+ }
102
+
103
+ function rollupConfig(cfg, formats) {
104
+ const out = formats
105
+ .map((f) => `\t\t{ file: 'dist/index.${f === 'cjs' ? 'cjs' : 'js'}', format: '${f}', sourcemap: true }`)
106
+ .join(',\n');
107
+ const tsPlugin = cfg.isTs ? `\n\tplugins: [typescript()],` : '';
108
+ const tsImport = cfg.isTs ? `import typescript from '@rollup/plugin-typescript';\n` : '';
109
+ return [
110
+ tsImport + `export default {`,
111
+ `\tinput: 'src/index.${cfg.ext}',`,
112
+ `\toutput: [`,
113
+ out,
114
+ `\t],${tsPlugin}`,
115
+ `};`,
116
+ ``,
117
+ ].join('\n');
118
+ }
@@ -0,0 +1,35 @@
1
+ // CLI target: a `bin` entry + a minimal command scaffold with a shebang.
2
+
3
+ export default {
4
+ id: 'cli',
5
+ active: (cfg) => cfg.hasCli,
6
+ apply(cfg) {
7
+ const build = cfg.bundler !== 'none';
8
+ const binPath = build || cfg.isTs ? './dist/cli.js' : './src/cli.js';
9
+ const files = {};
10
+
11
+ files[`src/cli.${cfg.ext}`] = cliScaffold(cfg);
12
+
13
+ return {
14
+ files,
15
+ pkg: {
16
+ bin: { [binName(cfg.name)]: binPath },
17
+ },
18
+ };
19
+ },
20
+ };
21
+
22
+ function binName(name) {
23
+ // scoped packages -> use the part after the slash as the command
24
+ return name.startsWith('@') ? name.split('/')[1] : name;
25
+ }
26
+
27
+ function cliScaffold(cfg) {
28
+ const importLine = cfg.hasLibrary
29
+ ? `import { greet } from './index.${cfg.isTs ? 'js' : 'js'}';\n`
30
+ : '';
31
+ const body = cfg.hasLibrary
32
+ ? `const name = process.argv[2] ?? 'world';\nconsole.log(greet(name));\n`
33
+ : `console.log('${cfg.name} — hello from your CLI');\n`;
34
+ return `#!/usr/bin/env node\n${importLine}\n${body}`;
35
+ }