create-packkit 3.0.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -0
- package/package.json +7 -2
- package/src/cli/args.js +0 -1
- package/src/cli/index.js +55 -14
- package/src/cli/upgrade.js +147 -0
- package/src/core/features/bundler.js +13 -7
- package/src/core/features/checks.js +4 -3
- package/src/core/features/env.js +2 -1
- package/src/core/features/frameworks.js +15 -14
- package/src/core/features/githooks.js +7 -6
- package/src/core/features/index.js +7 -0
- package/src/core/features/lint.js +7 -6
- package/src/core/features/meta.js +2 -1
- package/src/core/features/release.js +4 -3
- package/src/core/features/service.js +6 -5
- package/src/core/features/sizelimit.js +3 -2
- package/src/core/features/storybook.js +5 -4
- package/src/core/features/test.js +14 -15
- package/src/core/features/typescript.js +2 -1
- package/src/core/features/vite.js +9 -8
- package/src/core/monorepo.js +75 -101
- package/src/core/options.js +23 -0
- package/src/core/versions.js +121 -0
- package/src/embedded/contract.js +8 -6
- package/src/embedded/index.js +63 -8
- package/src/embedded/upgrade.js +148 -0
- package/types/embedded.d.ts +21 -1
package/README.md
CHANGED
|
@@ -61,6 +61,22 @@ npx create-packkit ts-lib my-lib --here --merge
|
|
|
61
61
|
|
|
62
62
|
**Existing files are never overwritten.** Anything that collides is left alone and reported, so you can diff at your leisure. (A directory containing only `.git` counts as empty — a fresh clone scaffolds without needing `--merge` at all.)
|
|
63
63
|
|
|
64
|
+
## Keep a project current
|
|
65
|
+
|
|
66
|
+
Every scaffolded project records what it came from in `packkit.json`. Later, from
|
|
67
|
+
inside the project:
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
npx create-packkit upgrade # dry run: what's changed since you scaffolded
|
|
71
|
+
npx create-packkit upgrade --apply # add new files, bump deps, keep your edits
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Upgrade regenerates the project Packkit would produce today and reports the
|
|
75
|
+
difference — new files, added or bumped dependencies, new scripts. `--apply`
|
|
76
|
+
brings in the safe changes (your own files, deps, and scripts are preserved);
|
|
77
|
+
files you've edited are listed for review and never overwritten unless you pass
|
|
78
|
+
`--force`.
|
|
79
|
+
|
|
64
80
|
## Or configure it on the web
|
|
65
81
|
|
|
66
82
|
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 create-packkit` command). Everything runs in your browser.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-packkit",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "Highly configurable scaffolder for modern npm packages and CLIs — pick your stack (TS/JS, bundler, tests, linter, CI, releases) from a CLI or a web configurator.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -39,6 +39,8 @@
|
|
|
39
39
|
"scripts": {
|
|
40
40
|
"start": "node bin/cli.js",
|
|
41
41
|
"test": "node --test",
|
|
42
|
+
"lint": "eslint .",
|
|
43
|
+
"check": "npm run lint && npm test",
|
|
42
44
|
"check:deps": "node scripts/check-template-deps.mjs",
|
|
43
45
|
"integration": "node scripts/integration.mjs",
|
|
44
46
|
"build:web": "esbuild src/core/index.js --bundle --format=esm --outfile=docs/packkit-core.js",
|
|
@@ -74,6 +76,9 @@
|
|
|
74
76
|
"@clack/prompts": "^0.7.0"
|
|
75
77
|
},
|
|
76
78
|
"devDependencies": {
|
|
77
|
-
"
|
|
79
|
+
"@eslint/js": "^10.0.1",
|
|
80
|
+
"esbuild": "^0.23.0",
|
|
81
|
+
"eslint": "^10.8.0",
|
|
82
|
+
"globals": "^17.8.0"
|
|
78
83
|
}
|
|
79
84
|
}
|
package/src/cli/args.js
CHANGED
|
@@ -53,7 +53,6 @@ export function parseCliArgs(argv) {
|
|
|
53
53
|
minify: { type: 'boolean' },
|
|
54
54
|
target: { type: 'string', multiple: true },
|
|
55
55
|
workflows: { type: 'string', multiple: true },
|
|
56
|
-
minify: { type: 'boolean' },
|
|
57
56
|
monorepo: { type: 'boolean' },
|
|
58
57
|
storybook: { type: 'boolean' },
|
|
59
58
|
e2e: { type: 'boolean' },
|
package/src/cli/index.js
CHANGED
|
@@ -2,12 +2,17 @@ import { resolve, basename } from 'node:path';
|
|
|
2
2
|
import { readFileSync, existsSync } from 'node:fs';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import * as p from '@clack/prompts';
|
|
5
|
-
import {
|
|
5
|
+
import { PRESET_NAMES, OPTIONS, OPTION_HELP, PRESET_INFO, PRESET_ALIASES } from '../core/index.js';
|
|
6
6
|
import { engineFloor, meetsNodeFloor } from '../core/node.js';
|
|
7
|
+
// The CLI is an adapter over the embedded API: it resolves + generates through
|
|
8
|
+
// the same pipeline every other surface uses (so it shares diagnostics,
|
|
9
|
+
// collision handling, and path safety), and adds the side effects the embedded
|
|
10
|
+
// API deliberately never performs — git, install, and creating the remote.
|
|
11
|
+
import { resolveProjectConfig, createProjectFromResolvedConfig, PackkitValidationError } from '../embedded/index.js';
|
|
12
|
+
import { writeGeneratedProject } from '../embedded/writer.js';
|
|
7
13
|
import { parseCliArgs } from './args.js';
|
|
8
14
|
import { runWizard } from './wizard.js';
|
|
9
15
|
import {
|
|
10
|
-
writeProject,
|
|
11
16
|
existingEntries,
|
|
12
17
|
gitInit,
|
|
13
18
|
installDeps,
|
|
@@ -34,6 +39,7 @@ packkit — scaffold a modern npm package, CLI, service, or app
|
|
|
34
39
|
Usage:
|
|
35
40
|
npm create packkit@latest [name] [options]
|
|
36
41
|
npx packkit [preset] [name] [options]
|
|
42
|
+
npx packkit upgrade [dir] Pull a scaffolded project up to current templates
|
|
37
43
|
|
|
38
44
|
Run with no options for an interactive wizard, or add -y for recommended
|
|
39
45
|
defaults in one shot. Every option is documented (with why-you'd-use-it) at:
|
|
@@ -109,6 +115,13 @@ Examples:
|
|
|
109
115
|
`;
|
|
110
116
|
|
|
111
117
|
export async function run(argv = process.argv.slice(2)) {
|
|
118
|
+
// Subcommands take the first positional. `upgrade` is a distinct flow (it
|
|
119
|
+
// reads an existing project rather than scaffolding one), so route it early.
|
|
120
|
+
if (argv[0] === 'upgrade') {
|
|
121
|
+
const { runUpgrade } = await import('./upgrade.js');
|
|
122
|
+
return runUpgrade(argv.slice(1));
|
|
123
|
+
}
|
|
124
|
+
|
|
112
125
|
const args = parseCliArgs(argv);
|
|
113
126
|
if (args.help) return void console.log(HELP);
|
|
114
127
|
if (args.version) return void console.log(pkgVersion());
|
|
@@ -125,21 +138,33 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
125
138
|
const profile = loadProfile(args);
|
|
126
139
|
const seed = { ...profile, ...args.overrides };
|
|
127
140
|
|
|
128
|
-
|
|
141
|
+
// Gather raw input for the embedded resolver — don't normalize here, so the
|
|
142
|
+
// resolver captures the coercions it makes (they surface as warnings below).
|
|
143
|
+
let rawInput;
|
|
144
|
+
let presetName;
|
|
129
145
|
if (interactive) {
|
|
130
146
|
p.intro('📦 Packkit');
|
|
131
|
-
|
|
147
|
+
rawInput = await runWizard(seed);
|
|
132
148
|
} else if (args.preset) {
|
|
133
|
-
|
|
149
|
+
presetName = args.preset;
|
|
150
|
+
rawInput = seed;
|
|
134
151
|
} else {
|
|
135
|
-
|
|
152
|
+
rawInput = seed;
|
|
136
153
|
}
|
|
154
|
+
// gitInit/install are real options, so they flow through resolution.
|
|
155
|
+
rawInput = { ...rawInput, gitInit: args.git, install: args.install };
|
|
137
156
|
|
|
138
|
-
config
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
157
|
+
let config;
|
|
158
|
+
let diagnostics;
|
|
159
|
+
try {
|
|
160
|
+
({ config, diagnostics } = resolveProjectConfig({ preset: presetName, config: rawInput }));
|
|
161
|
+
} catch (err) {
|
|
162
|
+
if (err instanceof PackkitValidationError) {
|
|
163
|
+
console.error(err.diagnostics.map((d) => `✖ ${d.message}`).join('\n'));
|
|
164
|
+
process.exit(1);
|
|
165
|
+
}
|
|
166
|
+
throw err;
|
|
167
|
+
}
|
|
143
168
|
|
|
144
169
|
// Node preflight: the generated project's tools (eslint, vite, vitest) hard-
|
|
145
170
|
// require this floor. npm only *warns* on engines, so catch it here — clearly,
|
|
@@ -186,9 +211,25 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
186
211
|
}
|
|
187
212
|
if (remote?.url && /^https?:/.test(remote.url)) config.repo = remote.url;
|
|
188
213
|
|
|
189
|
-
// Generate
|
|
190
|
-
|
|
191
|
-
|
|
214
|
+
// Generate through the embedded pipeline (carrying the resolution diagnostics)
|
|
215
|
+
// and write through the safe writer. --merge maps to the 'skip' policy: write
|
|
216
|
+
// what's absent, keep what's there — never overwrite.
|
|
217
|
+
const project = createProjectFromResolvedConfig(config, { diagnostics });
|
|
218
|
+
const { summary } = project;
|
|
219
|
+
|
|
220
|
+
// Surface anything Packkit changed or flagged during resolution — a coercion
|
|
221
|
+
// (Storybook off for a service), an unknown option — before writing.
|
|
222
|
+
const warnings = project.diagnostics.filter((d) => d.severity === 'warning');
|
|
223
|
+
if (warnings.length) {
|
|
224
|
+
const lines = warnings.map((d) => d.message).join('\n');
|
|
225
|
+
if (interactive) p.log.warn(lines); else console.error('\n⚠ ' + warnings.map((d) => d.message).join('\n '));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const { skippedFiles: skipped } = await writeGeneratedProject({
|
|
229
|
+
project,
|
|
230
|
+
destination: targetDir,
|
|
231
|
+
collisionPolicy: args.merge ? 'skip' : 'error',
|
|
232
|
+
});
|
|
192
233
|
|
|
193
234
|
// Post steps.
|
|
194
235
|
if (config.gitInit) gitInit(targetDir);
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// `packkit upgrade` — regenerate a scaffolded project's current recommended
|
|
2
|
+
// output and report (or apply) what has drifted from Packkit's templates.
|
|
3
|
+
//
|
|
4
|
+
// It reads packkit.json to learn the preset + settings the project came from,
|
|
5
|
+
// regenerates in memory through the embedded API, and diffs against disk. New
|
|
6
|
+
// files and package.json dep/script updates apply cleanly; files that differ
|
|
7
|
+
// are surfaced for review, never overwritten unless explicitly forced.
|
|
8
|
+
|
|
9
|
+
import { parseArgs } from 'node:util';
|
|
10
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
11
|
+
import { resolve, join } from 'node:path';
|
|
12
|
+
import { createProject, planUpgrade, isUpgradeEmpty, buildUpgradeWrite } from '../embedded/index.js';
|
|
13
|
+
import { writeGeneratedProject } from '../embedded/writer.js';
|
|
14
|
+
|
|
15
|
+
const UPGRADE_HELP = `
|
|
16
|
+
packkit upgrade — pull your project up to Packkit's current templates
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
packkit upgrade [directory] [options]
|
|
20
|
+
|
|
21
|
+
Reads packkit.json in the directory (default: current), regenerates the project
|
|
22
|
+
Packkit would produce today, and shows what changed since you scaffolded.
|
|
23
|
+
|
|
24
|
+
Options:
|
|
25
|
+
--apply Write new files and update package.json / packkit.json
|
|
26
|
+
--force With --apply, also overwrite files that differ (review first!)
|
|
27
|
+
-h, --help Show this help
|
|
28
|
+
|
|
29
|
+
Without --apply this is a dry run — it only reports.
|
|
30
|
+
`;
|
|
31
|
+
|
|
32
|
+
export async function runUpgrade(argv) {
|
|
33
|
+
const { values, positionals } = parseArgs({
|
|
34
|
+
args: argv,
|
|
35
|
+
allowPositionals: true,
|
|
36
|
+
options: {
|
|
37
|
+
apply: { type: 'boolean' },
|
|
38
|
+
force: { type: 'boolean' },
|
|
39
|
+
help: { type: 'boolean', short: 'h' },
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
if (values.help) return void console.log(UPGRADE_HELP);
|
|
43
|
+
|
|
44
|
+
const dir = resolve(positionals[0] || '.');
|
|
45
|
+
const provPath = join(dir, 'packkit.json');
|
|
46
|
+
if (!existsSync(provPath)) {
|
|
47
|
+
console.error(`No packkit.json in "${dir}". Upgrade only works on a project Packkit scaffolded.`);
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let provenance;
|
|
52
|
+
try {
|
|
53
|
+
provenance = JSON.parse(readFileSync(provPath, 'utf8'));
|
|
54
|
+
} catch (err) {
|
|
55
|
+
console.error(`Could not read packkit.json: ${err.message}`);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// The project name isn't a "setting", so it lives in package.json, not
|
|
60
|
+
// packkit.json — read it back so regeneration reproduces the same names.
|
|
61
|
+
const name = readName(dir) || provenance.name || 'my-package';
|
|
62
|
+
const fromVersion = provenance.version || 'unknown';
|
|
63
|
+
|
|
64
|
+
let project;
|
|
65
|
+
try {
|
|
66
|
+
project = createProject({ preset: provenance.preset, name, config: provenance.settings || {} });
|
|
67
|
+
} catch (err) {
|
|
68
|
+
console.error(`Could not regenerate from packkit.json: ${err.message}`);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
const toVersion = project.metadata.packkitVersion;
|
|
72
|
+
|
|
73
|
+
const onDisk = {};
|
|
74
|
+
for (const path of Object.keys(project.files)) {
|
|
75
|
+
const full = join(dir, path);
|
|
76
|
+
onDisk[path] = existsSync(full) ? readFileSync(full, 'utf8') : undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const plan = planUpgrade({ generated: project.files, onDisk });
|
|
80
|
+
|
|
81
|
+
if (isUpgradeEmpty(plan)) {
|
|
82
|
+
console.log(`Already current with Packkit ${toVersion}. Nothing to upgrade.`);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
report(plan, fromVersion, toVersion);
|
|
87
|
+
|
|
88
|
+
if (!values.apply) {
|
|
89
|
+
console.log('\nThis was a dry run. Re-run with --apply to bring in the safe changes.');
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const writeMap = buildUpgradeWrite({ generated: project.files, onDisk, plan, includeChanged: !!values.force });
|
|
94
|
+
const paths = Object.keys(writeMap);
|
|
95
|
+
if (paths.length) {
|
|
96
|
+
await writeGeneratedProject({
|
|
97
|
+
project: { config: project.config, files: writeMap },
|
|
98
|
+
destination: dir,
|
|
99
|
+
collisionPolicy: 'overwrite',
|
|
100
|
+
});
|
|
101
|
+
console.log(`\n✓ Applied ${paths.length} file update${paths.length > 1 ? 's' : ''}.`);
|
|
102
|
+
}
|
|
103
|
+
const leftover = values.force ? [] : plan.files.changed;
|
|
104
|
+
if (leftover.length) {
|
|
105
|
+
const plural = leftover.length > 1;
|
|
106
|
+
console.log(
|
|
107
|
+
`\n${leftover.length} file${plural ? 's' : ''} ${plural ? 'differ' : 'differs'} from the current template and ` +
|
|
108
|
+
`${plural ? 'were' : 'was'} left untouched (they may be your edits):\n ` +
|
|
109
|
+
leftover.join('\n ') +
|
|
110
|
+
`\nReview them, then re-run with --force to overwrite the ones you want Packkit's version of.`,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function readName(dir) {
|
|
116
|
+
try {
|
|
117
|
+
return JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')).name;
|
|
118
|
+
} catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function report(plan, fromVersion, toVersion) {
|
|
124
|
+
console.log(`Packkit ${fromVersion} → ${toVersion}\n`);
|
|
125
|
+
const p = plan.packageJson;
|
|
126
|
+
|
|
127
|
+
if (plan.files.added.length) {
|
|
128
|
+
console.log(`New files (${plan.files.added.length}):\n ` + plan.files.added.join('\n '));
|
|
129
|
+
}
|
|
130
|
+
const addedDeps = Object.entries(p.addedDependencies);
|
|
131
|
+
const bumpedDeps = Object.entries(p.updatedDependencies);
|
|
132
|
+
if (addedDeps.length) {
|
|
133
|
+
console.log(`\nNew dependencies (${addedDeps.length}):\n ` + addedDeps.map(([n, d]) => `${n}@${d.version} (${d.map})`).join('\n '));
|
|
134
|
+
}
|
|
135
|
+
if (bumpedDeps.length) {
|
|
136
|
+
console.log(`\nDependency updates (${bumpedDeps.length}):\n ` + bumpedDeps.map(([n, d]) => `${n}: ${d.from} → ${d.to}`).join('\n '));
|
|
137
|
+
}
|
|
138
|
+
const addedScripts = Object.keys(p.addedScripts);
|
|
139
|
+
const changedScripts = Object.entries(p.changedScripts);
|
|
140
|
+
if (addedScripts.length) console.log(`\nNew scripts (${addedScripts.length}):\n ` + addedScripts.join('\n '));
|
|
141
|
+
if (changedScripts.length) {
|
|
142
|
+
console.log(`\nScript changes (${changedScripts.length}):\n ` + changedScripts.map(([n, d]) => `${n}: ${d.from} → ${d.to}`).join('\n '));
|
|
143
|
+
}
|
|
144
|
+
if (plan.files.changed.length) {
|
|
145
|
+
console.log(`\nFiles that differ — review before overwriting (${plan.files.changed.length}):\n ` + plan.files.changed.join('\n '));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { V } from '../versions.js';
|
|
1
2
|
// Always-on. Owns the package entry points (exports/main/module/types/files)
|
|
2
3
|
// and the build tooling for the chosen bundler.
|
|
3
4
|
|
|
@@ -48,24 +49,24 @@ export default {
|
|
|
48
49
|
files[`${tool}.config.${cfg.ext}`] = tsupConfig(cfg, entries, formats, tool);
|
|
49
50
|
pkg.scripts.build = tool;
|
|
50
51
|
pkg.scripts.dev = `${tool} --watch`;
|
|
51
|
-
pkg.devDependencies = { [tool]: tool === 'tsup' ?
|
|
52
|
+
pkg.devDependencies = { [tool]: tool === 'tsup' ? V.tsup : V.tsdown };
|
|
52
53
|
// tsup/tsdown resolve `typescript` even for plain-JS builds; TS projects
|
|
53
54
|
// already get it from the typescript feature.
|
|
54
|
-
if (!cfg.isTs) pkg.devDependencies.typescript =
|
|
55
|
+
if (!cfg.isTs) pkg.devDependencies.typescript = V.typescript;
|
|
55
56
|
} else if (cfg.bundler === 'unbuild') {
|
|
56
57
|
files['build.config.ts'] = unbuildConfig(cfg);
|
|
57
58
|
pkg.scripts.build = 'unbuild';
|
|
58
59
|
pkg.scripts.dev = 'unbuild --stub';
|
|
59
|
-
pkg.devDependencies = { unbuild:
|
|
60
|
+
pkg.devDependencies = { unbuild: V.unbuild };
|
|
60
61
|
} else if (cfg.bundler === 'rollup') {
|
|
61
62
|
// Always a .js config — rollup can't load a .ts config without a loader.
|
|
62
63
|
files['rollup.config.js'] = rollupConfig(cfg, formats);
|
|
63
64
|
pkg.scripts.build = 'rollup -c';
|
|
64
65
|
pkg.scripts.dev = 'rollup -c -w';
|
|
65
66
|
pkg.devDependencies = {
|
|
66
|
-
rollup:
|
|
67
|
-
...(cfg.isTs ? { '@rollup/plugin-typescript': '
|
|
68
|
-
...(cfg.minify ? { '@rollup/plugin-terser': '
|
|
67
|
+
rollup: V.rollup,
|
|
68
|
+
...(cfg.isTs ? { '@rollup/plugin-typescript': V['@rollup/plugin-typescript'], tslib: V.tslib } : {}),
|
|
69
|
+
...(cfg.minify ? { '@rollup/plugin-terser': V['@rollup/plugin-terser'] } : {}),
|
|
69
70
|
};
|
|
70
71
|
} else if (cfg.bundler === 'none' && cfg.isTs) {
|
|
71
72
|
// tsc-only build for TypeScript.
|
|
@@ -73,9 +74,14 @@ export default {
|
|
|
73
74
|
pkg.scripts.dev = 'tsc --watch';
|
|
74
75
|
}
|
|
75
76
|
|
|
77
|
+
// A service owns its `dev` script (tsx/node --watch on the server entry); the
|
|
78
|
+
// bundler only builds it. Don't also contribute a `dev` — that was a silent
|
|
79
|
+
// conflict the two features "resolved" purely by array order.
|
|
80
|
+
if (cfg.hasService) delete pkg.scripts.dev;
|
|
81
|
+
|
|
76
82
|
if (build) pkg.scripts.prepublishOnly = pkg.scripts.build;
|
|
77
83
|
pkg.scripts.clean = 'rimraf dist';
|
|
78
|
-
if (build) pkg.devDependencies = { ...pkg.devDependencies, rimraf:
|
|
84
|
+
if (build) pkg.devDependencies = { ...pkg.devDependencies, rimraf: V.rimraf };
|
|
79
85
|
|
|
80
86
|
return { files, pkg };
|
|
81
87
|
},
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { V } from '../versions.js';
|
|
1
2
|
// Package-correctness + hygiene checks (opt-in):
|
|
2
3
|
// - publint validates package.json shape (exports/main/module)
|
|
3
4
|
// - are-the-types-wrong verifies TS consumers resolve your .d.ts correctly
|
|
@@ -14,12 +15,12 @@ export default {
|
|
|
14
15
|
// node16 profile flags a "false" failure — use the esm-only profile.
|
|
15
16
|
const attw = cfg.moduleFormat === 'esm' ? 'attw --pack --profile esm-only' : 'attw --pack';
|
|
16
17
|
pkg.scripts['check:pkg'] = `publint && ${attw}`;
|
|
17
|
-
pkg.devDependencies.publint =
|
|
18
|
-
pkg.devDependencies['@arethetypeswrong/cli'] = '
|
|
18
|
+
pkg.devDependencies.publint = V.publint;
|
|
19
|
+
pkg.devDependencies['@arethetypeswrong/cli'] = V['@arethetypeswrong/cli'];
|
|
19
20
|
}
|
|
20
21
|
if (cfg.knip) {
|
|
21
22
|
pkg.scripts.knip = 'knip';
|
|
22
|
-
pkg.devDependencies.knip =
|
|
23
|
+
pkg.devDependencies.knip = V.knip;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
return { files: {}, pkg };
|
package/src/core/features/env.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { V } from '../versions.js';
|
|
1
2
|
// Type-safe environment-variable validation for server-side runtimes (services
|
|
2
3
|
// and CLIs). Validates process.env once at startup with a Zod schema, so a
|
|
3
4
|
// misconfigured deploy fails fast with a clear message instead of a surprise
|
|
@@ -12,7 +13,7 @@ export default {
|
|
|
12
13
|
files[`src/env.${cfg.ext}`] = cfg.isTs ? envTs() : envJs();
|
|
13
14
|
files['.env.example'] = ['NODE_ENV=development', 'PORT=3000', ''].join('\n');
|
|
14
15
|
|
|
15
|
-
return { files, pkg: { dependencies: { zod:
|
|
16
|
+
return { files, pkg: { dependencies: { zod: V.zod } } };
|
|
16
17
|
},
|
|
17
18
|
};
|
|
18
19
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { V, PEER } from '../versions.js';
|
|
1
2
|
// React / Vue / Svelte source + dependencies, for both component libraries and
|
|
2
3
|
// Vite apps. The Vite build wiring lives in vite.js; here we emit the source
|
|
3
4
|
// files, the runtime deps (peer for libs, direct for apps), and — for Svelte
|
|
@@ -40,7 +41,7 @@ function react(cfg, files, pkg, forApp) {
|
|
|
40
41
|
`}`,
|
|
41
42
|
``,
|
|
42
43
|
].join('\n');
|
|
43
|
-
pkg.dependencies = { react:
|
|
44
|
+
pkg.dependencies = { react: V.react, 'react-dom': V['react-dom'] };
|
|
44
45
|
} else {
|
|
45
46
|
files[`src/index.${x}`] = cfg.isTs
|
|
46
47
|
? [
|
|
@@ -55,13 +56,13 @@ function react(cfg, files, pkg, forApp) {
|
|
|
55
56
|
``,
|
|
56
57
|
].join('\n')
|
|
57
58
|
: [`export function Button({ label, onClick }) {`, `\treturn <button onClick={onClick}>{label}</button>;`, `}`, ``].join('\n');
|
|
58
|
-
pkg.peerDependencies = { react:
|
|
59
|
-
pkg.devDependencies.react =
|
|
60
|
-
pkg.devDependencies['react-dom'] = '
|
|
59
|
+
pkg.peerDependencies = { react: PEER.react, 'react-dom': PEER['react-dom'] };
|
|
60
|
+
pkg.devDependencies.react = V.react;
|
|
61
|
+
pkg.devDependencies['react-dom'] = V['react-dom'];
|
|
61
62
|
}
|
|
62
63
|
if (cfg.isTs) {
|
|
63
|
-
pkg.devDependencies['@types/react'] = '
|
|
64
|
-
pkg.devDependencies['@types/react-dom'] = '
|
|
64
|
+
pkg.devDependencies['@types/react'] = V['@types/react'];
|
|
65
|
+
pkg.devDependencies['@types/react-dom'] = V['@types/react-dom'];
|
|
65
66
|
}
|
|
66
67
|
}
|
|
67
68
|
|
|
@@ -78,7 +79,7 @@ function vue(cfg, files, pkg, forApp) {
|
|
|
78
79
|
``,
|
|
79
80
|
].join('\n');
|
|
80
81
|
files['src/App.vue'] = [script, `</script>`, ``, `<template>`, `\t<h1>Hello from ${cfg.name}</h1>`, `</template>`, ``].join('\n');
|
|
81
|
-
pkg.dependencies = { vue:
|
|
82
|
+
pkg.dependencies = { vue: V.vue };
|
|
82
83
|
} else {
|
|
83
84
|
files[`src/index.${cfg.ext}`] = `export { default as Button } from './Button.vue';\n`;
|
|
84
85
|
files['src/Button.vue'] = [
|
|
@@ -91,8 +92,8 @@ function vue(cfg, files, pkg, forApp) {
|
|
|
91
92
|
`</template>`,
|
|
92
93
|
``,
|
|
93
94
|
].join('\n');
|
|
94
|
-
pkg.peerDependencies = { vue:
|
|
95
|
-
pkg.devDependencies.vue =
|
|
95
|
+
pkg.peerDependencies = { vue: PEER.vue };
|
|
96
|
+
pkg.devDependencies.vue = V.vue;
|
|
96
97
|
}
|
|
97
98
|
}
|
|
98
99
|
|
|
@@ -101,8 +102,8 @@ function svelte(cfg, files, pkg, forApp) {
|
|
|
101
102
|
const script = cfg.isTs ? `<script lang="ts">` : `<script>`;
|
|
102
103
|
// Shared by the Vite app build and the Vitest (test) config; enables TS in SFCs.
|
|
103
104
|
files['svelte.config.js'] = `import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';\n\nexport default { preprocess: vitePreprocess() };\n`;
|
|
104
|
-
pkg.devDependencies['@sveltejs/vite-plugin-svelte'] = '
|
|
105
|
-
if (cfg.isTs) pkg.devDependencies['svelte-check'] = '
|
|
105
|
+
pkg.devDependencies['@sveltejs/vite-plugin-svelte'] = V['@sveltejs/vite-plugin-svelte'];
|
|
106
|
+
if (cfg.isTs) pkg.devDependencies['svelte-check'] = V['svelte-check'];
|
|
106
107
|
if (forApp) {
|
|
107
108
|
files['index.html'] = htmlShell(cfg, `/src/main.${cfg.ext}`);
|
|
108
109
|
files[`src/main.${cfg.ext}`] = [
|
|
@@ -114,7 +115,7 @@ function svelte(cfg, files, pkg, forApp) {
|
|
|
114
115
|
``,
|
|
115
116
|
].join('\n');
|
|
116
117
|
files['src/App.svelte'] = [script, `</script>`, ``, `<h1>Hello from ${cfg.name}</h1>`, ``].join('\n');
|
|
117
|
-
pkg.dependencies = { svelte:
|
|
118
|
+
pkg.dependencies = { svelte: V.svelte };
|
|
118
119
|
} else {
|
|
119
120
|
// Svelte libraries ship uncompiled source; the consumer's bundler compiles.
|
|
120
121
|
files[`src/index.${cfg.ext}`] = `export { default as Button } from './Button.svelte';\n`;
|
|
@@ -127,8 +128,8 @@ function svelte(cfg, files, pkg, forApp) {
|
|
|
127
128
|
`<button>{label}</button>`,
|
|
128
129
|
``,
|
|
129
130
|
].filter((l) => l !== ``).join('\n') + '\n';
|
|
130
|
-
pkg.peerDependencies = { svelte:
|
|
131
|
-
pkg.devDependencies.svelte =
|
|
131
|
+
pkg.peerDependencies = { svelte: PEER.svelte };
|
|
132
|
+
pkg.devDependencies.svelte = V.svelte;
|
|
132
133
|
// Ship source; point consumers at it.
|
|
133
134
|
pkg.svelte = `./src/index.${cfg.ext}`;
|
|
134
135
|
pkg.exports = { '.': { svelte: `./src/index.${cfg.ext}`, default: `./src/index.${cfg.ext}` } };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { V } from '../versions.js';
|
|
1
2
|
export default {
|
|
2
3
|
id: 'githooks',
|
|
3
4
|
active: (cfg) => cfg.gitHooks !== 'none',
|
|
@@ -21,31 +22,31 @@ export default {
|
|
|
21
22
|
if (cfg.gitHooks === 'simple-git-hooks') {
|
|
22
23
|
pkg['simple-git-hooks'] = { 'pre-commit': needsLintStaged ? 'npx lint-staged' : 'npm test' };
|
|
23
24
|
pkg.scripts.prepare = 'simple-git-hooks';
|
|
24
|
-
pkg.devDependencies['simple-git-hooks'] = '
|
|
25
|
+
pkg.devDependencies['simple-git-hooks'] = V['simple-git-hooks'];
|
|
25
26
|
} else if (cfg.gitHooks === 'husky') {
|
|
26
27
|
files['.husky/pre-commit'] = (needsLintStaged ? 'npx lint-staged\n' : 'npm test\n');
|
|
27
28
|
pkg.scripts.prepare = 'husky';
|
|
28
|
-
pkg.devDependencies.husky =
|
|
29
|
+
pkg.devDependencies.husky = V.husky;
|
|
29
30
|
} else if (cfg.gitHooks === 'lefthook') {
|
|
30
|
-
files['lefthook.yml'] = lefthookYml(cfg
|
|
31
|
+
files['lefthook.yml'] = lefthookYml(cfg);
|
|
31
32
|
// `|| true` so `npm install` doesn't fail before `git init` — lefthook
|
|
32
33
|
// (unlike husky/simple-git-hooks) errors hard without a .git directory.
|
|
33
34
|
pkg.scripts.prepare = 'lefthook install || true';
|
|
34
|
-
pkg.devDependencies.lefthook =
|
|
35
|
+
pkg.devDependencies.lefthook = V.lefthook;
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
if (needsLintStaged) {
|
|
38
39
|
pkg['lint-staged'] = staged;
|
|
39
40
|
// Held at 16: v17 requires Node >=22.22.1, above our Node 22 engines
|
|
40
41
|
// floor (22.13) — it would break the Maintenance-LTS line. See ENGINE_MIN.
|
|
41
|
-
pkg.devDependencies['lint-staged'] = '
|
|
42
|
+
pkg.devDependencies['lint-staged'] = V['lint-staged'];
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
return { files, pkg };
|
|
45
46
|
},
|
|
46
47
|
};
|
|
47
48
|
|
|
48
|
-
function lefthookYml(cfg
|
|
49
|
+
function lefthookYml(cfg) {
|
|
49
50
|
const cmd =
|
|
50
51
|
cfg.lint === 'biome'
|
|
51
52
|
? 'biome check --write --no-errors-on-unmatched {staged_files}'
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
// Feature registry. Order matters only for how package.json fragments merge
|
|
2
2
|
// (later features deep-merge over earlier ones); files are keyed by path.
|
|
3
|
+
//
|
|
4
|
+
// Ordering contract: no two features may own the same file path or the same
|
|
5
|
+
// package.json field for a given config. That invariant is enforced by
|
|
6
|
+
// test/feature-registry.test.js across the whole config matrix, so ordering is
|
|
7
|
+
// never load-bearing for correctness — if a new feature collides with an
|
|
8
|
+
// existing one, the test fails rather than the winner being decided by position
|
|
9
|
+
// here. Each feature declares a unique `id`, surfaced in collision diagnostics.
|
|
3
10
|
|
|
4
11
|
import meta from './meta.js';
|
|
5
12
|
import bundler from './bundler.js';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { toJson } from '../render.js';
|
|
2
|
+
import { V } from '../versions.js';
|
|
2
3
|
|
|
3
4
|
export default {
|
|
4
5
|
id: 'lint',
|
|
@@ -19,19 +20,19 @@ export default {
|
|
|
19
20
|
files['.prettierignore'] = 'dist\ncoverage\n';
|
|
20
21
|
pkg.scripts.format = 'prettier --write .';
|
|
21
22
|
pkg.scripts['format:check'] = 'prettier --check .';
|
|
22
|
-
pkg.devDependencies.prettier =
|
|
23
|
+
pkg.devDependencies.prettier = V.prettier;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
if (cfg.lint === 'eslint-prettier') {
|
|
26
27
|
files['eslint.config.js'] = eslintFlatConfig(cfg);
|
|
27
28
|
pkg.scripts.lint = 'eslint .';
|
|
28
29
|
pkg.scripts['lint:fix'] = 'eslint . --fix';
|
|
29
|
-
pkg.devDependencies.eslint =
|
|
30
|
-
pkg.devDependencies['@eslint/js'] = '
|
|
31
|
-
if (cfg.isTs) pkg.devDependencies['typescript-eslint'] = '
|
|
30
|
+
pkg.devDependencies.eslint = V.eslint;
|
|
31
|
+
pkg.devDependencies['@eslint/js'] = V['@eslint/js'];
|
|
32
|
+
if (cfg.isTs) pkg.devDependencies['typescript-eslint'] = V['typescript-eslint'];
|
|
32
33
|
} else if (cfg.lint === 'oxlint') {
|
|
33
34
|
pkg.scripts.lint = 'oxlint';
|
|
34
|
-
pkg.devDependencies.oxlint =
|
|
35
|
+
pkg.devDependencies.oxlint = V.oxlint;
|
|
35
36
|
} else if (cfg.lint === 'biome') {
|
|
36
37
|
files['biome.json'] = toJson({
|
|
37
38
|
$schema: 'https://biomejs.dev/schemas/2.1.2/schema.json',
|
|
@@ -42,7 +43,7 @@ export default {
|
|
|
42
43
|
pkg.scripts.lint = 'biome lint .';
|
|
43
44
|
pkg.scripts['lint:fix'] = 'biome lint --write .';
|
|
44
45
|
pkg.scripts.format = 'biome format --write .';
|
|
45
|
-
pkg.devDependencies['@biomejs/biome'] = '
|
|
46
|
+
pkg.devDependencies['@biomejs/biome'] = V['@biomejs/biome'];
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
return { files, pkg };
|
|
@@ -54,7 +54,8 @@ export default {
|
|
|
54
54
|
};
|
|
55
55
|
|
|
56
56
|
function libraryEntry(cfg) {
|
|
57
|
-
|
|
57
|
+
// Only reached for a plain library (the caller guards on !cfg.hasFramework),
|
|
58
|
+
// so framework entries never come through here — frameworks.js writes those.
|
|
58
59
|
if (cfg.isTs) {
|
|
59
60
|
return [
|
|
60
61
|
`/** Greet someone by name. */`,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { toJson } from '../render.js';
|
|
2
|
+
import { V } from '../versions.js';
|
|
2
3
|
|
|
3
4
|
export default {
|
|
4
5
|
id: 'release',
|
|
@@ -21,7 +22,7 @@ export default {
|
|
|
21
22
|
pkg.scripts.changeset = 'changeset';
|
|
22
23
|
pkg.scripts.version = 'changeset version';
|
|
23
24
|
pkg.scripts.release = `${buildThen(cfg)}changeset publish`;
|
|
24
|
-
pkg.devDependencies['@changesets/cli'] = '
|
|
25
|
+
pkg.devDependencies['@changesets/cli'] = V['@changesets/cli'];
|
|
25
26
|
} else if (cfg.release === 'release-it') {
|
|
26
27
|
files['.release-it.json'] = toJson({
|
|
27
28
|
git: { commitMessage: 'chore: release v${version}' },
|
|
@@ -29,10 +30,10 @@ export default {
|
|
|
29
30
|
npm: { publish: true },
|
|
30
31
|
});
|
|
31
32
|
pkg.scripts.release = 'release-it';
|
|
32
|
-
pkg.devDependencies['release-it'] = '
|
|
33
|
+
pkg.devDependencies['release-it'] = V['release-it'];
|
|
33
34
|
} else if (cfg.release === 'np') {
|
|
34
35
|
pkg.scripts.release = 'np';
|
|
35
|
-
pkg.devDependencies.np =
|
|
36
|
+
pkg.devDependencies.np = V.np;
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
return { files, pkg };
|