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/src/embedded/contract.js
CHANGED
|
@@ -13,15 +13,17 @@
|
|
|
13
13
|
export function deriveDeploymentContract(cfg) {
|
|
14
14
|
const run = (script) => (cfg.packageManager === 'npm' ? `npm run ${script}` : `${cfg.packageManager} ${script}`);
|
|
15
15
|
const start = cfg.packageManager === 'npm' ? 'npm start' : `${cfg.packageManager} start`;
|
|
16
|
+
// Read the structured resolved view rather than re-interpreting raw selections.
|
|
17
|
+
const { targets, build } = cfg.resolved;
|
|
16
18
|
|
|
17
|
-
if (
|
|
19
|
+
if (targets.service) {
|
|
18
20
|
// The generated server reads PORT but defaults to 3000, so PORT is optional,
|
|
19
21
|
// not required — `port` communicates the default and the `PORT` env var
|
|
20
22
|
// overrides it. `healthCheckPath` is only asserted because every service
|
|
21
23
|
// framework Packkit generates (Hono/Fastify/Express) defines /health.
|
|
22
24
|
return prune({
|
|
23
25
|
type: 'node-service',
|
|
24
|
-
buildCommand:
|
|
26
|
+
buildCommand: build.has ? run('build') : undefined,
|
|
25
27
|
startCommand: start,
|
|
26
28
|
port: 3000,
|
|
27
29
|
healthCheckPath: '/health',
|
|
@@ -29,7 +31,7 @@ export function deriveDeploymentContract(cfg) {
|
|
|
29
31
|
});
|
|
30
32
|
}
|
|
31
33
|
|
|
32
|
-
if (
|
|
34
|
+
if (targets.app) {
|
|
33
35
|
return prune({
|
|
34
36
|
type: 'static',
|
|
35
37
|
buildCommand: run('build'),
|
|
@@ -38,16 +40,16 @@ export function deriveDeploymentContract(cfg) {
|
|
|
38
40
|
});
|
|
39
41
|
}
|
|
40
42
|
|
|
41
|
-
if (
|
|
43
|
+
if (targets.cli) {
|
|
42
44
|
return prune({
|
|
43
45
|
type: 'cli',
|
|
44
|
-
buildCommand:
|
|
46
|
+
buildCommand: build.has ? run('build') : undefined,
|
|
45
47
|
});
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
return prune({
|
|
49
51
|
type: 'library',
|
|
50
|
-
buildCommand:
|
|
52
|
+
buildCommand: build.has ? run('build') : undefined,
|
|
51
53
|
});
|
|
52
54
|
}
|
|
53
55
|
|
package/src/embedded/index.js
CHANGED
|
@@ -24,6 +24,7 @@ import { analyzePkgFragments } from './pkg-merge.js';
|
|
|
24
24
|
import { deriveDeploymentContract } from './contract.js';
|
|
25
25
|
|
|
26
26
|
export { deriveDeploymentContract };
|
|
27
|
+
export { planUpgrade, isUpgradeEmpty, buildUpgradeWrite } from './upgrade.js';
|
|
27
28
|
|
|
28
29
|
// Bumped when the shape of PackkitProjectDefinition changes incompatibly.
|
|
29
30
|
export const SCHEMA_VERSION = 2;
|
|
@@ -37,7 +38,7 @@ const MAX_DEFINITION_BYTES = 50_000_000;
|
|
|
37
38
|
const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
38
39
|
|
|
39
40
|
/** Non-OPTIONS config keys the pipeline sets itself; not "unknown". */
|
|
40
|
-
const KNOWN_EXTRA_KEYS = new Set(['preset', 'generatorVersion']);
|
|
41
|
+
const KNOWN_EXTRA_KEYS = new Set(['preset', 'generatorVersion', 'resolved']);
|
|
41
42
|
|
|
42
43
|
// Fields where a host override silently changing an existing value is worth a
|
|
43
44
|
// diagnostic (matches pkg-merge's protected set + dependency maps).
|
|
@@ -72,12 +73,18 @@ function packkitVersion() {
|
|
|
72
73
|
}
|
|
73
74
|
|
|
74
75
|
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
76
|
+
* Resolve a preset + overrides into a complete, validated config, collecting
|
|
77
|
+
* the diagnostics normalization produced — without generating anything.
|
|
78
|
+
*
|
|
79
|
+
* Generation and resolution are split so a trusted caller (the CLI) can make
|
|
80
|
+
* decisions on the resolved config — a Node-floor check, creating the remote —
|
|
81
|
+
* and then generate, while a host app just uses createProject() which does both.
|
|
77
82
|
* Throws PackkitValidationError if the config is fatally invalid.
|
|
83
|
+
*
|
|
84
|
+
* @returns {{ config: object, diagnostics: object[] }}
|
|
78
85
|
*/
|
|
79
|
-
export function
|
|
80
|
-
if (!input || typeof input !== 'object') throw new TypeError('
|
|
86
|
+
export function resolveProjectConfig(input = {}) {
|
|
87
|
+
if (!input || typeof input !== 'object') throw new TypeError('resolveProjectConfig expects an input object.');
|
|
81
88
|
|
|
82
89
|
const merged = { ...(input.config || {}), ...(input.overrides || {}) };
|
|
83
90
|
if (input.name != null) merged.name = input.name;
|
|
@@ -106,7 +113,36 @@ export function createProject(input = {}) {
|
|
|
106
113
|
const raw = canonicalPreset
|
|
107
114
|
? { ...PRESETS[canonicalPreset], ...merged, preset: canonicalPreset }
|
|
108
115
|
: merged;
|
|
109
|
-
const
|
|
116
|
+
const config = normalizeConfig({ ...raw, generatorVersion: packkitVersion() }, diagnostics);
|
|
117
|
+
return { config, diagnostics };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Generate a complete project in memory. No files are written, nothing is
|
|
122
|
+
* installed, no commands run. Returns a GeneratedProject with diagnostics.
|
|
123
|
+
* Throws PackkitValidationError if the config is fatally invalid.
|
|
124
|
+
*/
|
|
125
|
+
export function createProject(input = {}) {
|
|
126
|
+
const { config, diagnostics } = resolveProjectConfig(input);
|
|
127
|
+
return assembleProject(config, diagnostics);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Build a project from an already-resolved config (from resolveProjectConfig,
|
|
132
|
+
* possibly with a field like `repo` set since). For trusted callers only — the
|
|
133
|
+
* config is assumed valid; pass its resolution diagnostics through so they
|
|
134
|
+
* appear on the project. Re-normalizes idempotently.
|
|
135
|
+
*/
|
|
136
|
+
export function createProjectFromResolvedConfig(config, { diagnostics = [] } = {}) {
|
|
137
|
+
const resolved = normalizeConfig({ generatorVersion: packkitVersion(), ...config });
|
|
138
|
+
return assembleProject(resolved, [...diagnostics]);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Shared generation core: turn a resolved config into a GeneratedProject,
|
|
142
|
+
// appending collision and package-conflict diagnostics to whatever the caller
|
|
143
|
+
// already collected during resolution.
|
|
144
|
+
function assembleProject(config, diagnostics) {
|
|
145
|
+
const { files, summary, fileSources, fragments } = generateTracked(config);
|
|
110
146
|
|
|
111
147
|
for (const [path, sources] of Object.entries(fileSources)) {
|
|
112
148
|
if (sources.length > 1) {
|
|
@@ -236,8 +272,19 @@ export function exportProjectDefinition(project) {
|
|
|
236
272
|
};
|
|
237
273
|
}
|
|
238
274
|
|
|
239
|
-
/**
|
|
240
|
-
|
|
275
|
+
/**
|
|
276
|
+
* Rebuild a project from a stored definition, re-applying its extensions.
|
|
277
|
+
*
|
|
278
|
+
* If a file the host originally *added* now collides with one the current
|
|
279
|
+
* Packkit generates, that's surfaced as an error-severity diagnostic. With
|
|
280
|
+
* `driftPolicy: 'error'` that condition throws instead — for a caller that
|
|
281
|
+
* wants an unreconciled definition to fail loudly rather than reproduce
|
|
282
|
+
* silently. The default, `'report'`, returns the project with the diagnostic.
|
|
283
|
+
*/
|
|
284
|
+
export function createProjectFromDefinition(definition, { driftPolicy = 'report' } = {}) {
|
|
285
|
+
if (!['report', 'error'].includes(driftPolicy)) {
|
|
286
|
+
throw new TypeError(`Unknown driftPolicy "${driftPolicy}".`);
|
|
287
|
+
}
|
|
241
288
|
validateDefinition(definition);
|
|
242
289
|
const current = packkitVersion();
|
|
243
290
|
const base = createProject({ preset: definition.preset, config: definition.config });
|
|
@@ -278,6 +325,14 @@ export function createProjectFromDefinition(definition) {
|
|
|
278
325
|
}
|
|
279
326
|
}
|
|
280
327
|
|
|
328
|
+
// With driftPolicy: 'error', an unreconciled add-collision fails the rebuild
|
|
329
|
+
// rather than reproducing silently. Version drift alone (a warning) never
|
|
330
|
+
// throws — only the error-severity collision does.
|
|
331
|
+
const fatalDrift = drift.filter((d) => d.severity === 'error');
|
|
332
|
+
if (driftPolicy === 'error' && fatalDrift.length) {
|
|
333
|
+
throw new PackkitValidationError('The definition no longer reconciles with this Packkit version; see error.diagnostics.', fatalDrift);
|
|
334
|
+
}
|
|
335
|
+
|
|
281
336
|
const hasExt = Object.keys(plainFiles).length || Object.keys(ext.packageJson || {}).length;
|
|
282
337
|
// Carry the stored modes so re-exporting the replayed project preserves the
|
|
283
338
|
// original add/replace intent rather than re-deriving it against this base.
|
|
@@ -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
|
+
}
|
package/types/embedded.d.ts
CHANGED
|
@@ -85,8 +85,28 @@ export class PackkitValidationError extends Error {
|
|
|
85
85
|
export const SCHEMA_VERSION: number;
|
|
86
86
|
|
|
87
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;
|
|
88
90
|
export function extendProject(project: GeneratedProject, extension?: ProjectExtension): GeneratedProject;
|
|
89
91
|
export function exportProjectDefinition(project: GeneratedProject): PackkitProjectDefinition;
|
|
90
|
-
export function createProjectFromDefinition(
|
|
92
|
+
export function createProjectFromDefinition(
|
|
93
|
+
definition: PackkitProjectDefinition,
|
|
94
|
+
options?: { driftPolicy?: 'report' | 'error' },
|
|
95
|
+
): GeneratedProject;
|
|
91
96
|
export function calculateProjectDigest(project: GeneratedProject): string;
|
|
92
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>;
|