create-packkit 2.9.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 +36 -2
- package/package.json +23 -4
- 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/index.js +41 -7
- package/src/core/monorepo.js +75 -101
- package/src/core/options.js +67 -22
- package/src/core/render.js +6 -0
- package/src/core/versions.js +121 -0
- package/src/embedded/contract.js +66 -0
- package/src/embedded/index.js +506 -0
- package/src/embedded/paths.js +88 -0
- package/src/embedded/pkg-merge.js +89 -0
- package/src/embedded/upgrade.js +148 -0
- package/src/embedded/writer.js +151 -0
- package/src/index.js +12 -0
- package/types/core.d.ts +96 -0
- package/types/embedded.d.ts +112 -0
- package/types/index.d.ts +6 -0
- package/types/writer.d.ts +25 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// Upgrade planning.
|
|
2
|
+
//
|
|
3
|
+
// A project scaffolded by Packkit records what it came from in packkit.json.
|
|
4
|
+
// That lets us regenerate the *current* recommended output and compare it to
|
|
5
|
+
// what's on disk — so a project can be told what has drifted from Packkit's
|
|
6
|
+
// current templates and choose what to pull in.
|
|
7
|
+
//
|
|
8
|
+
// This module is pure: it takes two file maps (freshly generated vs. on disk)
|
|
9
|
+
// and returns a classified plan. Reading the disk and applying the plan live in
|
|
10
|
+
// the CLI; keeping the decision logic here makes it testable and reusable.
|
|
11
|
+
|
|
12
|
+
import { finalizePackageJson } from '../core/pkg.js';
|
|
13
|
+
import { deepMerge, toJson } from '../core/render.js';
|
|
14
|
+
|
|
15
|
+
// Files Packkit owns but that get structural, not whole-file, treatment.
|
|
16
|
+
// package.json is co-owned (the host adds its own deps/scripts); packkit.json
|
|
17
|
+
// is expected to change every version (it records the generator version).
|
|
18
|
+
const STRUCTURAL = new Set(['package.json', 'packkit.json']);
|
|
19
|
+
const DEP_MAPS = ['dependencies', 'devDependencies', 'peerDependencies'];
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Classify how a freshly-generated project differs from what's on disk.
|
|
23
|
+
*
|
|
24
|
+
* @param {object} input
|
|
25
|
+
* @param {Record<string,string>} input.generated the current createProject().files
|
|
26
|
+
* @param {Record<string,string|undefined>} input.onDisk the on-disk content for
|
|
27
|
+
* each generated path (undefined when the file doesn't exist)
|
|
28
|
+
* @returns an upgrade plan: which files are new/changed/current, and the
|
|
29
|
+
* structural package.json delta.
|
|
30
|
+
*/
|
|
31
|
+
export function planUpgrade({ generated, onDisk }) {
|
|
32
|
+
const added = [];
|
|
33
|
+
const changed = [];
|
|
34
|
+
const unchanged = [];
|
|
35
|
+
|
|
36
|
+
for (const [path, content] of Object.entries(generated)) {
|
|
37
|
+
if (STRUCTURAL.has(path)) continue;
|
|
38
|
+
const disk = onDisk[path];
|
|
39
|
+
if (disk === undefined) added.push(path);
|
|
40
|
+
else if (disk === content) unchanged.push(path);
|
|
41
|
+
else changed.push(path);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
files: {
|
|
46
|
+
added: added.sort(),
|
|
47
|
+
// "changed" means differs — could be a Packkit template change or the
|
|
48
|
+
// user's own edit. Without a stored baseline we can't tell which, so these
|
|
49
|
+
// are surfaced for review, never overwritten automatically.
|
|
50
|
+
changed: changed.sort(),
|
|
51
|
+
unchanged: unchanged.sort(),
|
|
52
|
+
},
|
|
53
|
+
packageJson: diffPackageJson(onDisk['package.json'], generated['package.json']),
|
|
54
|
+
// packkit.json records the generator version, so it always "changes" on an
|
|
55
|
+
// upgrade; applying the upgrade refreshes it.
|
|
56
|
+
provenanceOutdated: onDisk['packkit.json'] !== generated['packkit.json'],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** True when a plan found nothing to bring in. */
|
|
61
|
+
export function isUpgradeEmpty(plan) {
|
|
62
|
+
const p = plan.packageJson;
|
|
63
|
+
return (
|
|
64
|
+
plan.files.added.length === 0 &&
|
|
65
|
+
plan.files.changed.length === 0 &&
|
|
66
|
+
Object.keys(p.addedDependencies).length === 0 &&
|
|
67
|
+
Object.keys(p.updatedDependencies).length === 0 &&
|
|
68
|
+
Object.keys(p.addedScripts).length === 0 &&
|
|
69
|
+
Object.keys(p.changedScripts).length === 0 &&
|
|
70
|
+
!plan.provenanceOutdated
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Build the { path: content } map to write for a plan. Always includes new
|
|
76
|
+
* files, the refreshed package.json (deps bumped / added, scripts added or
|
|
77
|
+
* updated — the user's own extras preserved), and packkit.json. Changed files
|
|
78
|
+
* are included only when `includeChanged` is set (an explicit overwrite).
|
|
79
|
+
*/
|
|
80
|
+
export function buildUpgradeWrite({ generated, onDisk, plan, includeChanged = false }) {
|
|
81
|
+
const out = {};
|
|
82
|
+
for (const path of plan.files.added) out[path] = generated[path];
|
|
83
|
+
if (includeChanged) for (const path of plan.files.changed) out[path] = generated[path];
|
|
84
|
+
|
|
85
|
+
if (onDisk['package.json'] && generated['package.json']) {
|
|
86
|
+
const merged = mergePackageJson(onDisk['package.json'], generated['package.json'], plan.packageJson);
|
|
87
|
+
if (merged !== onDisk['package.json']) out['package.json'] = merged;
|
|
88
|
+
}
|
|
89
|
+
if (plan.provenanceOutdated && generated['packkit.json']) out['packkit.json'] = generated['packkit.json'];
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Apply only the deps/scripts the plan flagged onto the on-disk package.json,
|
|
94
|
+
// so a bump lands but the user's own additions and field ordering survive.
|
|
95
|
+
function mergePackageJson(diskStr, genStr, pkgPlan) {
|
|
96
|
+
const disk = JSON.parse(diskStr);
|
|
97
|
+
const gen = JSON.parse(genStr);
|
|
98
|
+
|
|
99
|
+
const patch = {};
|
|
100
|
+
for (const [name, { map }] of Object.entries(pkgPlan.addedDependencies)) {
|
|
101
|
+
(patch[map] ||= {})[name] = gen[map][name];
|
|
102
|
+
}
|
|
103
|
+
for (const [name, { map }] of Object.entries(pkgPlan.updatedDependencies)) {
|
|
104
|
+
(patch[map] ||= {})[name] = gen[map][name];
|
|
105
|
+
}
|
|
106
|
+
const scripts = {};
|
|
107
|
+
for (const name of Object.keys(pkgPlan.addedScripts)) scripts[name] = gen.scripts[name];
|
|
108
|
+
for (const name of Object.keys(pkgPlan.changedScripts)) scripts[name] = gen.scripts[name];
|
|
109
|
+
if (Object.keys(scripts).length) patch.scripts = scripts;
|
|
110
|
+
|
|
111
|
+
return toJson(finalizePackageJson(deepMerge(disk, patch)));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Structural package.json diff: what Packkit would add or bump, without ever
|
|
115
|
+
// treating the user's own extra deps/scripts as "removed".
|
|
116
|
+
function diffPackageJson(diskStr, genStr) {
|
|
117
|
+
const empty = { addedDependencies: {}, updatedDependencies: {}, addedScripts: {}, changedScripts: {} };
|
|
118
|
+
if (!diskStr || !genStr) return empty;
|
|
119
|
+
|
|
120
|
+
let disk;
|
|
121
|
+
let gen;
|
|
122
|
+
try {
|
|
123
|
+
disk = JSON.parse(diskStr);
|
|
124
|
+
gen = JSON.parse(genStr);
|
|
125
|
+
} catch {
|
|
126
|
+
return empty; // a hand-broken package.json — leave it alone
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const addedDependencies = {};
|
|
130
|
+
const updatedDependencies = {};
|
|
131
|
+
for (const map of DEP_MAPS) {
|
|
132
|
+
for (const [name, version] of Object.entries(gen[map] || {})) {
|
|
133
|
+
const current = disk[map]?.[name];
|
|
134
|
+
if (current === undefined) addedDependencies[name] = { map, version };
|
|
135
|
+
else if (current !== version) updatedDependencies[name] = { map, from: current, to: version };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const addedScripts = {};
|
|
140
|
+
const changedScripts = {};
|
|
141
|
+
for (const [name, cmd] of Object.entries(gen.scripts || {})) {
|
|
142
|
+
const current = disk.scripts?.[name];
|
|
143
|
+
if (current === undefined) addedScripts[name] = cmd;
|
|
144
|
+
else if (current !== cmd) changedScripts[name] = { from: current, to: cmd };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return { addedDependencies, updatedDependencies, addedScripts, changedScripts };
|
|
148
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// Controlled filesystem output for a GeneratedProject.
|
|
2
|
+
//
|
|
3
|
+
// This is the only place the embedded API touches disk, and it does nothing
|
|
4
|
+
// else: no install, no git, no lifecycle scripts, no command execution. Every
|
|
5
|
+
// path is validated again here — not just when the project was built — and the
|
|
6
|
+
// real on-disk path is checked for symlinks, so a project that reached this
|
|
7
|
+
// boundary from an untrusted source still can't escape the destination.
|
|
8
|
+
|
|
9
|
+
import { mkdir, writeFile, stat, lstat } from 'node:fs/promises';
|
|
10
|
+
import { dirname, join, resolve, sep } from 'node:path';
|
|
11
|
+
import { validateRelativePath } from './paths.js';
|
|
12
|
+
|
|
13
|
+
export class PackkitWriteError extends Error {
|
|
14
|
+
constructor(message, { code, path, destination, cause } = {}) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = 'PackkitWriteError';
|
|
17
|
+
this.code = code;
|
|
18
|
+
if (path !== undefined) this.path = path;
|
|
19
|
+
if (destination !== undefined) this.destination = destination;
|
|
20
|
+
if (cause !== undefined) this.cause = cause;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Write a project's files under `destination`. Returns a WriteResult; never
|
|
26
|
+
* installs, inits git, or runs anything. Validates and preflights everything
|
|
27
|
+
* before the first write, so a bad file map or a policy collision fails cleanly
|
|
28
|
+
* rather than leaving a half-written project.
|
|
29
|
+
*
|
|
30
|
+
* @param {{ project: object, destination: string, collisionPolicy?: 'error'|'skip'|'overwrite' }} input
|
|
31
|
+
*/
|
|
32
|
+
export async function writeGeneratedProject(input) {
|
|
33
|
+
const { project, destination, collisionPolicy = 'error' } = input || {};
|
|
34
|
+
if (!project || typeof project !== 'object' || !project.files) {
|
|
35
|
+
throw new TypeError('writeGeneratedProject needs a project with a files map.');
|
|
36
|
+
}
|
|
37
|
+
if (typeof destination !== 'string' || destination.length === 0) {
|
|
38
|
+
throw new TypeError('A destination path is required.');
|
|
39
|
+
}
|
|
40
|
+
if (!['error', 'skip', 'overwrite'].includes(collisionPolicy)) {
|
|
41
|
+
throw new TypeError(`Unknown collisionPolicy "${collisionPolicy}".`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const root = resolve(destination);
|
|
45
|
+
const prefix = root.endsWith(sep) ? root : root + sep;
|
|
46
|
+
|
|
47
|
+
// A symlinked destination could redirect the whole write outside where the
|
|
48
|
+
// caller thinks it's going. Reject it — a host that truly wants this can
|
|
49
|
+
// resolve the real path itself before calling.
|
|
50
|
+
await assertNotSymlink(root, false, root, root);
|
|
51
|
+
|
|
52
|
+
// 1) Lexical validation: any traversal/absolute path means we write nothing.
|
|
53
|
+
const planned = [];
|
|
54
|
+
for (const [path, contents] of Object.entries(project.files)) {
|
|
55
|
+
const res = validateRelativePath(path);
|
|
56
|
+
if (!res.ok) {
|
|
57
|
+
throw new PackkitWriteError(`Refusing to write invalid path "${path}": ${res.message}`, { code: res.code, path, destination: root });
|
|
58
|
+
}
|
|
59
|
+
const target = join(root, res.normalized);
|
|
60
|
+
if (target !== root && !target.startsWith(prefix)) {
|
|
61
|
+
throw new PackkitWriteError(`Refusing to write outside the destination: "${path}"`, { code: 'PATH_ESCAPE', path, destination: root });
|
|
62
|
+
}
|
|
63
|
+
planned.push({ path: res.normalized, target, contents });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 2) Symlink + collision preflight against the real filesystem, before any
|
|
67
|
+
// write. Under the 'error' policy, one existing target aborts the whole
|
|
68
|
+
// operation with every collision listed — no partial output.
|
|
69
|
+
const collisions = [];
|
|
70
|
+
for (const { path, target } of planned) {
|
|
71
|
+
await assertNoSymlinkComponents(root, target, path);
|
|
72
|
+
if (await exists(target)) {
|
|
73
|
+
if (collisionPolicy === 'error') collisions.push(path);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (collisions.length) {
|
|
77
|
+
throw new PackkitWriteError(
|
|
78
|
+
`Refusing to overwrite existing file(s): ${collisions.join(', ')}`,
|
|
79
|
+
{ code: 'FILE_EXISTS', destination: root },
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 3) Write.
|
|
84
|
+
const writtenFiles = [];
|
|
85
|
+
const skippedFiles = [];
|
|
86
|
+
const diagnostics = [];
|
|
87
|
+
for (const { path, target, contents } of planned) {
|
|
88
|
+
try {
|
|
89
|
+
if ((collisionPolicy === 'skip') && (await exists(target))) {
|
|
90
|
+
skippedFiles.push(path);
|
|
91
|
+
diagnostics.push({ severity: 'info', code: 'FILE_SKIPPED', field: path, message: `"${path}" already existed and was left in place.`, source: 'writer' });
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
await mkdir(dirname(target), { recursive: true });
|
|
95
|
+
await writeFile(target, contents);
|
|
96
|
+
writtenFiles.push(path);
|
|
97
|
+
} catch (err) {
|
|
98
|
+
// A real filesystem failure on one file: record it, keep going, so the
|
|
99
|
+
// caller sees exactly what landed and what didn't.
|
|
100
|
+
diagnostics.push({ severity: 'error', code: 'WRITE_FAILED', field: path, message: `Could not write "${path}": ${err.message}`, source: 'writer' });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return { destination: root, writtenFiles, skippedFiles, diagnostics };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Reject any existing symlink among the path components from root to target,
|
|
108
|
+
// including the target itself — a lexical containment check can't catch
|
|
109
|
+
// `dest/link -> /elsewhere`, but following real inodes can. Writing through an
|
|
110
|
+
// existing symlink (even the final file, under 'overwrite') would escape.
|
|
111
|
+
async function assertNoSymlinkComponents(root, target, path) {
|
|
112
|
+
const rel = target.slice(root.length + 1);
|
|
113
|
+
if (!rel) return;
|
|
114
|
+
const segments = rel.split(sep);
|
|
115
|
+
let current = root;
|
|
116
|
+
for (let i = 0; i < segments.length; i++) {
|
|
117
|
+
current = join(current, segments[i]);
|
|
118
|
+
const isFinal = i === segments.length - 1;
|
|
119
|
+
await assertNotSymlink(current, isFinal, path, root);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function assertNotSymlink(component, isFinal, path, destination) {
|
|
124
|
+
try {
|
|
125
|
+
const info = await lstat(component);
|
|
126
|
+
if (info.isSymbolicLink()) {
|
|
127
|
+
throw new PackkitWriteError(`Refusing to write through a symbolic link: "${component}"`, { code: 'SYMLINK_PATH', path, destination });
|
|
128
|
+
}
|
|
129
|
+
// Intermediate components must be directories; the final one may already
|
|
130
|
+
// exist as a regular file (collision handling decides what to do with it).
|
|
131
|
+
if (!info.isDirectory() && !isFinal) {
|
|
132
|
+
throw new PackkitWriteError(`A parent path component is not a directory: "${component}"`, { code: 'PARENT_NOT_DIRECTORY', path, destination });
|
|
133
|
+
}
|
|
134
|
+
} catch (err) {
|
|
135
|
+
if (err instanceof PackkitWriteError) throw err;
|
|
136
|
+
if (err.code === 'ENOENT') return; // doesn't exist yet; nothing to follow
|
|
137
|
+
throw new PackkitWriteError(`Could not inspect "${component}": ${err.message}`, { code: 'STAT_FAILED', path, destination, cause: err });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Only ENOENT means "not there" — permission and I/O errors are real problems
|
|
142
|
+
// and must surface, not be silently read as a missing file.
|
|
143
|
+
async function exists(p) {
|
|
144
|
+
try {
|
|
145
|
+
await stat(p);
|
|
146
|
+
return true;
|
|
147
|
+
} catch (err) {
|
|
148
|
+
if (err.code === 'ENOENT') return false;
|
|
149
|
+
throw new PackkitWriteError(`Could not stat "${p}": ${err.message}`, { code: 'STAT_FAILED', cause: err });
|
|
150
|
+
}
|
|
151
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Package root. Re-exports the pure generation core plus the Node embedded API,
|
|
2
|
+
// so `import { generate, createProject, writeGeneratedProject } from 'create-packkit'`
|
|
3
|
+
// works for host applications.
|
|
4
|
+
//
|
|
5
|
+
// The browser configurator does NOT import this file — it bundles
|
|
6
|
+
// src/core/index.js directly (see build:web) — so pulling the Node-only
|
|
7
|
+
// embedded modules in here doesn't affect browser compatibility of the core.
|
|
8
|
+
// Consumers who want the core in isolation can import 'create-packkit/core'.
|
|
9
|
+
|
|
10
|
+
export * from './core/index.js';
|
|
11
|
+
export * from './embedded/index.js';
|
|
12
|
+
export { writeGeneratedProject, PackkitWriteError } from './embedded/writer.js';
|
package/types/core.d.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Public types for the pure generation core.
|
|
2
|
+
|
|
3
|
+
export type Language = 'ts' | 'js';
|
|
4
|
+
export type ModuleFormat = 'esm' | 'dual' | 'cjs';
|
|
5
|
+
export type Framework = 'none' | 'react' | 'vue' | 'svelte';
|
|
6
|
+
export type Target = 'library' | 'cli' | 'service' | 'app';
|
|
7
|
+
export type Bundler = 'tsup' | 'tsdown' | 'unbuild' | 'rollup' | 'none';
|
|
8
|
+
export type TestRunner = 'vitest' | 'jest' | 'node' | 'none';
|
|
9
|
+
export type Linter = 'eslint-prettier' | 'biome' | 'oxlint' | 'none';
|
|
10
|
+
export type ReleaseTool = 'changesets' | 'release-it' | 'np' | 'none';
|
|
11
|
+
export type PackageManager = 'npm' | 'pnpm' | 'yarn' | 'bun';
|
|
12
|
+
|
|
13
|
+
/** User-facing configuration. All fields optional on input; a preset or the
|
|
14
|
+
* defaults fill the rest. */
|
|
15
|
+
export interface PackkitConfig {
|
|
16
|
+
name?: string;
|
|
17
|
+
description?: string;
|
|
18
|
+
author?: string;
|
|
19
|
+
keywords?: string;
|
|
20
|
+
repo?: string;
|
|
21
|
+
language?: Language;
|
|
22
|
+
moduleFormat?: ModuleFormat;
|
|
23
|
+
framework?: Framework;
|
|
24
|
+
target?: Target[];
|
|
25
|
+
serviceFramework?: 'hono' | 'fastify' | 'express';
|
|
26
|
+
monorepo?: boolean;
|
|
27
|
+
monorepoLayout?: 'libraries' | 'fullstack';
|
|
28
|
+
packageManager?: PackageManager;
|
|
29
|
+
nodeVersion?: string;
|
|
30
|
+
bundler?: Bundler;
|
|
31
|
+
minify?: boolean;
|
|
32
|
+
test?: TestRunner;
|
|
33
|
+
coverage?: boolean;
|
|
34
|
+
storybook?: boolean;
|
|
35
|
+
e2e?: boolean;
|
|
36
|
+
sourcemaps?: boolean;
|
|
37
|
+
env?: boolean;
|
|
38
|
+
canary?: boolean;
|
|
39
|
+
pkgChecks?: boolean;
|
|
40
|
+
knip?: boolean;
|
|
41
|
+
sizeLimit?: boolean;
|
|
42
|
+
doctor?: boolean;
|
|
43
|
+
lint?: Linter;
|
|
44
|
+
gitHooks?: 'simple-git-hooks' | 'husky' | 'lefthook' | 'none';
|
|
45
|
+
release?: ReleaseTool;
|
|
46
|
+
jsr?: boolean;
|
|
47
|
+
workflows?: string[];
|
|
48
|
+
deps?: 'renovate' | 'dependabot' | 'none';
|
|
49
|
+
license?: string;
|
|
50
|
+
community?: boolean;
|
|
51
|
+
agents?: boolean;
|
|
52
|
+
vscode?: boolean;
|
|
53
|
+
editorconfig?: boolean;
|
|
54
|
+
gitInit?: boolean;
|
|
55
|
+
install?: boolean;
|
|
56
|
+
[key: string]: unknown;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The config after normalization: every field resolved, plus derived flags. */
|
|
60
|
+
export interface ResolvedPackkitConfig extends PackkitConfig {
|
|
61
|
+
isTs: boolean;
|
|
62
|
+
isReact: boolean;
|
|
63
|
+
isVue: boolean;
|
|
64
|
+
isSvelte: boolean;
|
|
65
|
+
hasFramework: boolean;
|
|
66
|
+
hasApp: boolean;
|
|
67
|
+
hasLibrary: boolean;
|
|
68
|
+
hasCli: boolean;
|
|
69
|
+
hasService: boolean;
|
|
70
|
+
hasBuild: boolean;
|
|
71
|
+
publishable: boolean;
|
|
72
|
+
preset?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface ProjectSummary {
|
|
76
|
+
name: string;
|
|
77
|
+
fileCount: number;
|
|
78
|
+
stack: string[];
|
|
79
|
+
workflows: string[];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface GenerateResult {
|
|
83
|
+
config: ResolvedPackkitConfig;
|
|
84
|
+
files: Record<string, string>;
|
|
85
|
+
postCommands: string[];
|
|
86
|
+
summary: ProjectSummary;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function generate(input?: PackkitConfig): GenerateResult;
|
|
90
|
+
export function fromPreset(name: string, overrides?: PackkitConfig): ResolvedPackkitConfig;
|
|
91
|
+
export function normalizeConfig(input?: PackkitConfig, diagnostics?: unknown[]): ResolvedPackkitConfig;
|
|
92
|
+
export function resolvePreset(name: string): string | undefined;
|
|
93
|
+
|
|
94
|
+
export const OPTIONS: Record<string, unknown>;
|
|
95
|
+
export const PRESET_NAMES: string[];
|
|
96
|
+
export const PRESET_ALIASES: Record<string, string>;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// Public types for the embedded API (create-packkit/embedded).
|
|
2
|
+
|
|
3
|
+
import type { PackkitConfig, ResolvedPackkitConfig, ProjectSummary } from './core.js';
|
|
4
|
+
|
|
5
|
+
export type DiagnosticSeverity = 'info' | 'warning' | 'error';
|
|
6
|
+
|
|
7
|
+
export interface Diagnostic {
|
|
8
|
+
severity: DiagnosticSeverity;
|
|
9
|
+
code: string;
|
|
10
|
+
message: string;
|
|
11
|
+
field?: string;
|
|
12
|
+
source?: string;
|
|
13
|
+
previousValue?: unknown;
|
|
14
|
+
resolvedValue?: unknown;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface GeneratedProjectMetadata {
|
|
18
|
+
packkitVersion: string;
|
|
19
|
+
schemaVersion: number;
|
|
20
|
+
preset?: string;
|
|
21
|
+
generatedAt?: string;
|
|
22
|
+
extension?: Record<string, unknown>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type DeploymentType = 'static' | 'node-service' | 'library' | 'cli';
|
|
26
|
+
|
|
27
|
+
export interface DeploymentContract {
|
|
28
|
+
type: DeploymentType;
|
|
29
|
+
buildCommand?: string;
|
|
30
|
+
outputDirectory?: string;
|
|
31
|
+
startCommand?: string;
|
|
32
|
+
port?: number;
|
|
33
|
+
healthCheckPath?: string;
|
|
34
|
+
requiredEnvironmentVariables?: string[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface GeneratedProject {
|
|
38
|
+
config: ResolvedPackkitConfig;
|
|
39
|
+
files: Record<string, string>;
|
|
40
|
+
summary: ProjectSummary;
|
|
41
|
+
diagnostics: Diagnostic[];
|
|
42
|
+
metadata: GeneratedProjectMetadata;
|
|
43
|
+
deploymentContract: DeploymentContract;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface CreateProjectInput {
|
|
47
|
+
name?: string;
|
|
48
|
+
preset?: string;
|
|
49
|
+
config?: PackkitConfig;
|
|
50
|
+
overrides?: PackkitConfig;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type CollisionPolicy = 'error' | 'skip' | 'overwrite';
|
|
54
|
+
|
|
55
|
+
export interface ProjectExtension {
|
|
56
|
+
files?: Record<string, string>;
|
|
57
|
+
packageJson?: Record<string, unknown>;
|
|
58
|
+
metadata?: Record<string, unknown>;
|
|
59
|
+
collisionPolicy?: CollisionPolicy;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** How a stored extension file relates to generated output: `add` = the host
|
|
63
|
+
* introduced a new path; `replace` = it deliberately overrode a generated one. */
|
|
64
|
+
export interface StoredExtensionFile {
|
|
65
|
+
content: string;
|
|
66
|
+
mode: 'add' | 'replace';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface PackkitProjectDefinition {
|
|
70
|
+
schemaVersion: number;
|
|
71
|
+
packkitVersion: string;
|
|
72
|
+
preset?: string;
|
|
73
|
+
config: PackkitConfig;
|
|
74
|
+
extensions?: {
|
|
75
|
+
files?: Record<string, StoredExtensionFile>;
|
|
76
|
+
packageJson?: Record<string, unknown>;
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export class PackkitValidationError extends Error {
|
|
81
|
+
readonly code: 'PACKKIT_VALIDATION_FAILED';
|
|
82
|
+
diagnostics: Diagnostic[];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const SCHEMA_VERSION: number;
|
|
86
|
+
|
|
87
|
+
export function createProject(input?: CreateProjectInput): GeneratedProject;
|
|
88
|
+
export function resolveProjectConfig(input?: CreateProjectInput): { config: ResolvedPackkitConfig; diagnostics: Diagnostic[] };
|
|
89
|
+
export function createProjectFromResolvedConfig(config: ResolvedPackkitConfig, options?: { diagnostics?: Diagnostic[] }): GeneratedProject;
|
|
90
|
+
export function extendProject(project: GeneratedProject, extension?: ProjectExtension): GeneratedProject;
|
|
91
|
+
export function exportProjectDefinition(project: GeneratedProject): PackkitProjectDefinition;
|
|
92
|
+
export function createProjectFromDefinition(
|
|
93
|
+
definition: PackkitProjectDefinition,
|
|
94
|
+
options?: { driftPolicy?: 'report' | 'error' },
|
|
95
|
+
): GeneratedProject;
|
|
96
|
+
export function calculateProjectDigest(project: GeneratedProject): string;
|
|
97
|
+
export function deriveDeploymentContract(config: ResolvedPackkitConfig): DeploymentContract;
|
|
98
|
+
|
|
99
|
+
export interface UpgradePlan {
|
|
100
|
+
files: { added: string[]; changed: string[]; unchanged: string[] };
|
|
101
|
+
packageJson: {
|
|
102
|
+
addedDependencies: Record<string, { map: string; version: string }>;
|
|
103
|
+
updatedDependencies: Record<string, { map: string; from: string; to: string }>;
|
|
104
|
+
addedScripts: Record<string, string>;
|
|
105
|
+
changedScripts: Record<string, { from: string; to: string }>;
|
|
106
|
+
};
|
|
107
|
+
provenanceOutdated: boolean;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function planUpgrade(input: { generated: Record<string, string>; onDisk: Record<string, string | undefined> }): UpgradePlan;
|
|
111
|
+
export function isUpgradeEmpty(plan: UpgradePlan): boolean;
|
|
112
|
+
export function buildUpgradeWrite(input: { generated: Record<string, string>; onDisk: Record<string, string | undefined>; plan: UpgradePlan; includeChanged?: boolean }): Record<string, string>;
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Package root types: the generation core plus the Node embedded API.
|
|
2
|
+
|
|
3
|
+
export * from './core.js';
|
|
4
|
+
export * from './embedded.js';
|
|
5
|
+
export { writeGeneratedProject, PackkitWriteError } from './writer.js';
|
|
6
|
+
export type { WriteGeneratedProjectInput, WriteResult } from './writer.js';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Public types for the writer (create-packkit/writer).
|
|
2
|
+
|
|
3
|
+
import type { GeneratedProject, Diagnostic, CollisionPolicy } from './embedded.js';
|
|
4
|
+
|
|
5
|
+
export interface WriteGeneratedProjectInput {
|
|
6
|
+
project: GeneratedProject;
|
|
7
|
+
destination: string;
|
|
8
|
+
collisionPolicy?: CollisionPolicy;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface WriteResult {
|
|
12
|
+
destination: string;
|
|
13
|
+
writtenFiles: string[];
|
|
14
|
+
skippedFiles: string[];
|
|
15
|
+
diagnostics: Diagnostic[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class PackkitWriteError extends Error {
|
|
19
|
+
code: string;
|
|
20
|
+
path?: string;
|
|
21
|
+
destination?: string;
|
|
22
|
+
cause?: unknown;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function writeGeneratedProject(input: WriteGeneratedProjectInput): Promise<WriteResult>;
|