create-pkgbld 1.8.2 → 2.0.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 +85 -10
- package/extensions-schema.json +53 -0
- package/extensions.json +33 -0
- package/index.js +2 -2
- package/lock-schema-v1.json +26 -0
- package/package.json +23 -8
- package/src/conflicts.js +21 -0
- package/src/diff.js +83 -0
- package/src/engine.js +120 -0
- package/src/extension-cache.js +110 -0
- package/src/{get-git-root.ts → get-git-root.js} +1 -1
- package/src/index.js +257 -0
- package/src/install.js +59 -0
- package/src/inventory.js +161 -0
- package/src/package-names.js +25 -0
- package/src/package-operations.js +353 -0
- package/src/package-resolution.js +40 -0
- package/src/package-version.js +48 -0
- package/src/plugin-compatibility.js +79 -0
- package/src/project-changes.js +379 -0
- package/src/project-lock.js +74 -0
- package/src/registry.js +184 -0
- package/src/subcommands.js +345 -0
- package/src/tree.js +282 -0
- package/src/tui.js +167 -0
- package/src/types.js +28 -0
- package/src/update-engine.js +196 -0
- package/CHANGELOG.md +0 -206
- package/eslint.config.mjs +0 -42
- package/src/index.ts +0 -524
- package/src/reset.d.ts +0 -1
- package/src/types.ts +0 -33
- package/tsconfig.json +0 -13
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import semver from 'semver';
|
|
2
|
+
|
|
3
|
+
import { runRemove, runSetup } from './engine.js';
|
|
4
|
+
import { buildPackageInventory } from './inventory.js';
|
|
5
|
+
import { resolveInstalledPackage } from './package-resolution.js';
|
|
6
|
+
import { resolvePublishedVersion } from './package-version.js';
|
|
7
|
+
import { assertPluginCompatible } from './plugin-compatibility.js';
|
|
8
|
+
import { resolveExtension } from './registry.js';
|
|
9
|
+
import { Tree } from './tree.js';
|
|
10
|
+
import { runExtensionUpdate } from './update-engine.js';
|
|
11
|
+
|
|
12
|
+
/** @typedef {'managed' | 'updated' | 'absent'} PackageTarget */
|
|
13
|
+
/** @typedef {'setup' | 'restore' | 'adopt' | 'update' | 'remove' | 'none'} PackageEffect */
|
|
14
|
+
/** @typedef {'setup' | 'update' | 'remove' | 'adopt'} PackageIntent */
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {{
|
|
17
|
+
* id: string,
|
|
18
|
+
* name: string,
|
|
19
|
+
* description: string,
|
|
20
|
+
* tags: readonly string[],
|
|
21
|
+
* kind: 'plugin' | 'extension',
|
|
22
|
+
* state: import('./inventory.js').PackageState,
|
|
23
|
+
* error: string | null
|
|
24
|
+
* }} PackageView
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {{
|
|
28
|
+
* package: PackageView,
|
|
29
|
+
* target: PackageTarget,
|
|
30
|
+
* effect: PackageEffect,
|
|
31
|
+
* versionChange: { from: string, to: string } | null,
|
|
32
|
+
* requiresInstall: boolean,
|
|
33
|
+
* questions: readonly import('./types.js').Option[],
|
|
34
|
+
* stage(project: import('./project-changes.js').ProjectChanges, answers?: import('./types.js').OptionsValue): Promise<{
|
|
35
|
+
* package: PackageView,
|
|
36
|
+
* effect: PackageEffect
|
|
37
|
+
* }>
|
|
38
|
+
* }} PreparedPackageOperation
|
|
39
|
+
*/
|
|
40
|
+
/** @typedef {import('./inventory.js').PackageItem & { previousExtension?: import('./registry.js').Extension | null }} PreparedPackageItem */
|
|
41
|
+
|
|
42
|
+
export class PackageOperationError extends Error {
|
|
43
|
+
/** @param {'PACKAGE_NOT_FOUND' | 'PACKAGE_UNAVAILABLE' | 'VERSION_UNRESOLVED' | 'UPDATE_UNAVAILABLE' | 'PLUGIN_INCOMPATIBLE'} code @param {string} message */
|
|
44
|
+
constructor(code, message) {
|
|
45
|
+
super(message);
|
|
46
|
+
this.name = 'PackageOperationError';
|
|
47
|
+
this.code = code;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Open package operations for one project snapshot.
|
|
53
|
+
*
|
|
54
|
+
* @param {{ projectRoot: string, registry: import('./registry.js').ExtensionEntry[], resolveVersion?: (packageName: string, selector: string) => Promise<string> }} params
|
|
55
|
+
*/
|
|
56
|
+
export async function openPackageOperations({ projectRoot, registry, resolveVersion = resolvePublishedVersion }) {
|
|
57
|
+
const { items, warnings } = await buildPackageInventory(registry, projectRoot);
|
|
58
|
+
const inventory = Object.freeze(items.map(toPackageView));
|
|
59
|
+
|
|
60
|
+
return Object.freeze({
|
|
61
|
+
inventory,
|
|
62
|
+
warnings: Object.freeze([...warnings]),
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Prepare a package operation without staging project changes.
|
|
66
|
+
*
|
|
67
|
+
* @param {{ package: string, target: PackageTarget }} request
|
|
68
|
+
* @returns {Promise<PreparedPackageOperation>}
|
|
69
|
+
*/
|
|
70
|
+
async prepare(request) {
|
|
71
|
+
const item = items.find(candidate => candidate.entry.name === request.package || candidate.packageName === request.package);
|
|
72
|
+
if (!item) throw new PackageOperationError('PACKAGE_NOT_FOUND', `PKG BLD package "${request.package}" not found`);
|
|
73
|
+
|
|
74
|
+
const view = /** @type {PackageView} */ (inventory.find(candidate => candidate.id === item.packageName));
|
|
75
|
+
let effect = deriveEffect(item, request.target);
|
|
76
|
+
if (effect === 'none') {
|
|
77
|
+
return createPreparedOperation({ effect, item, questions: [], target: request.target, view });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** @type {import('./registry.js').Extension | null} */
|
|
81
|
+
let extension = item.ext;
|
|
82
|
+
let resolvedVersion = item.resolvedVersion;
|
|
83
|
+
let versionChange = null;
|
|
84
|
+
let previousExtension = null;
|
|
85
|
+
if (effect === 'adopt') {
|
|
86
|
+
resolvedVersion ??= resolveInstalledPackage(item.packageName, projectRoot)?.version ?? null;
|
|
87
|
+
if (!resolvedVersion) {
|
|
88
|
+
throw new PackageOperationError(
|
|
89
|
+
'VERSION_UNRESOLVED',
|
|
90
|
+
`Cannot adopt "${item.packageName}": install project dependencies first so its exact version can be resolved`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
if (item.kind === 'plugin')
|
|
94
|
+
assertCompatible(item.packageName, resolveInstalledPackage(item.packageName, projectRoot)?.manifest, projectRoot);
|
|
95
|
+
} else if (effect === 'update') {
|
|
96
|
+
if (!item.entry.official || !item.lockedVersion || !item.entry.version || !item.hasExtensionContract) {
|
|
97
|
+
throw new PackageOperationError(
|
|
98
|
+
'UPDATE_UNAVAILABLE',
|
|
99
|
+
`Cannot update "${item.packageName}": managed official extension metadata is required`
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
let targetVersion;
|
|
103
|
+
try {
|
|
104
|
+
targetVersion = await resolveVersion(item.packageName, item.entry.version);
|
|
105
|
+
} catch (/** @type {any} */ cause) {
|
|
106
|
+
throw new PackageOperationError(
|
|
107
|
+
'VERSION_UNRESOLVED',
|
|
108
|
+
`Cannot update "${item.packageName}": ${cause.message ?? String(cause)}`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
if (!semver.valid(targetVersion)) {
|
|
112
|
+
throw new PackageOperationError(
|
|
113
|
+
'VERSION_UNRESOLVED',
|
|
114
|
+
`Cannot update "${item.packageName}": target version is not exact`
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
if (semver.lt(targetVersion, item.lockedVersion)) {
|
|
118
|
+
throw new PackageOperationError(
|
|
119
|
+
'UPDATE_UNAVAILABLE',
|
|
120
|
+
`Cannot update "${item.packageName}": registry candidate ${targetVersion} is older than locked version ${item.lockedVersion}; downgrades are unsupported`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
if (targetVersion === item.lockedVersion) {
|
|
124
|
+
effect = 'none';
|
|
125
|
+
versionChange = { from: item.lockedVersion, to: targetVersion };
|
|
126
|
+
return createPreparedOperation({ effect, item, questions: [], target: request.target, versionChange, view });
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
previousExtension = await resolveExtension(
|
|
130
|
+
{ ...item.entry, version: item.lockedVersion, official: true },
|
|
131
|
+
projectRoot,
|
|
132
|
+
{ install: true, exactVersion: item.lockedVersion, resolveVersion }
|
|
133
|
+
);
|
|
134
|
+
extension = await resolveExtension({ ...item.entry, version: targetVersion, official: true }, projectRoot, {
|
|
135
|
+
install: true,
|
|
136
|
+
exactVersion: targetVersion,
|
|
137
|
+
resolveVersion,
|
|
138
|
+
});
|
|
139
|
+
} catch (/** @type {any} */ cause) {
|
|
140
|
+
throw new PackageOperationError(
|
|
141
|
+
'UPDATE_UNAVAILABLE',
|
|
142
|
+
`Cannot update "${item.packageName}": ${cause.message ?? String(cause)}`
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
if (item.kind === 'plugin') assertCompatible(item.packageName, extension.__packageManifest, projectRoot);
|
|
146
|
+
resolvedVersion = targetVersion;
|
|
147
|
+
versionChange = { from: item.lockedVersion, to: targetVersion };
|
|
148
|
+
} else if (requiresExtension(item, effect)) {
|
|
149
|
+
extension = await ensurePackageExtension(item, projectRoot, resolveVersion);
|
|
150
|
+
resolvedVersion = extension.__packageVersion ?? resolvedVersion;
|
|
151
|
+
if (item.kind === 'plugin') assertCompatible(item.packageName, extension.__packageManifest, projectRoot);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const questions =
|
|
155
|
+
(effect === 'setup' || effect === 'restore' || effect === 'update') && typeof extension?.prompts === 'function'
|
|
156
|
+
? (extension.prompts(
|
|
157
|
+
new Tree(projectRoot),
|
|
158
|
+
effect === 'update' ? { operation: 'update', ...versionChange } : { operation: effect }
|
|
159
|
+
) ?? [])
|
|
160
|
+
: [];
|
|
161
|
+
|
|
162
|
+
return createPreparedOperation({
|
|
163
|
+
effect,
|
|
164
|
+
item: { ...item, ext: extension, previousExtension, resolvedVersion },
|
|
165
|
+
questions,
|
|
166
|
+
target: request.target,
|
|
167
|
+
versionChange,
|
|
168
|
+
view,
|
|
169
|
+
});
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** @param {import('./inventory.js').PackageItem} item @param {PackageTarget} target @returns {PackageEffect} */
|
|
175
|
+
function deriveEffect(item, target) {
|
|
176
|
+
if (target === 'managed') {
|
|
177
|
+
if (item.state === 'installed-managed') return 'none';
|
|
178
|
+
if (item.state === 'installed-unmanaged') return 'adopt';
|
|
179
|
+
if (item.state === 'applied') return 'restore';
|
|
180
|
+
if (item.state === 'available') return 'setup';
|
|
181
|
+
throw new PackageOperationError(
|
|
182
|
+
'PACKAGE_UNAVAILABLE',
|
|
183
|
+
`PKG BLD package "${item.entry.name}" is unavailable${item.error ? `: ${item.error}` : ''}`
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
if (target === 'updated') {
|
|
187
|
+
if (!item.managed) {
|
|
188
|
+
throw new PackageOperationError('UPDATE_UNAVAILABLE', `Cannot update "${item.entry.name}": adopt or add it first`);
|
|
189
|
+
}
|
|
190
|
+
return 'update';
|
|
191
|
+
}
|
|
192
|
+
if (target === 'absent') return item.installed || item.managed ? 'remove' : 'none';
|
|
193
|
+
throw new TypeError(`Unknown package target: ${target}`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** @param {import('./inventory.js').PackageItem} item @param {PackageEffect} effect */
|
|
197
|
+
function requiresExtension(item, effect) {
|
|
198
|
+
if (!item.hasExtensionContract) return false;
|
|
199
|
+
return effect === 'setup' || effect === 'restore' || effect === 'remove';
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* @param {{
|
|
204
|
+
* effect: PackageEffect,
|
|
205
|
+
* item: PreparedPackageItem,
|
|
206
|
+
* questions: import('./types.js').Option[],
|
|
207
|
+
* target: PackageTarget,
|
|
208
|
+
* view: PackageView,
|
|
209
|
+
* versionChange?: { from: string, to: string } | null
|
|
210
|
+
* }} params
|
|
211
|
+
* @returns {PreparedPackageOperation}
|
|
212
|
+
*/
|
|
213
|
+
function createPreparedOperation({ effect, item, questions, target, view, versionChange = null }) {
|
|
214
|
+
let staged = false;
|
|
215
|
+
return Object.freeze({
|
|
216
|
+
package: view,
|
|
217
|
+
target,
|
|
218
|
+
effect,
|
|
219
|
+
versionChange: versionChange ? Object.freeze({ ...versionChange }) : null,
|
|
220
|
+
requiresInstall: effect === 'update' && item.kind === 'plugin',
|
|
221
|
+
questions: Object.freeze(
|
|
222
|
+
questions.map(question => Object.freeze('list' in question ? { ...question, list: [...question.list] } : { ...question }))
|
|
223
|
+
),
|
|
224
|
+
|
|
225
|
+
async stage(project, answers = {}) {
|
|
226
|
+
if (staged) throw new Error(`Package operation for "${view.name}" has already been staged`);
|
|
227
|
+
staged = true;
|
|
228
|
+
if (effect === 'none') return { package: view, effect };
|
|
229
|
+
|
|
230
|
+
const intent = /** @type {PackageIntent} */ (
|
|
231
|
+
effect === 'adopt' ? 'adopt' : effect === 'remove' ? 'remove' : effect === 'update' ? 'update' : 'setup'
|
|
232
|
+
);
|
|
233
|
+
const preparedItem = { ...item, intent, options: answers };
|
|
234
|
+
await project.stagePackageOperation(item.entry.name, ({ tree, projectLock, reportConflict }) =>
|
|
235
|
+
applyPackageIntent(preparedItem, tree, projectLock, reportConflict)
|
|
236
|
+
);
|
|
237
|
+
return { package: view, effect };
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Resolve extension behavior on demand. Locked entries always request their exact version.
|
|
244
|
+
* @param {import('./inventory.js').PackageItem} item
|
|
245
|
+
* @param {string} projectRoot
|
|
246
|
+
* @param {(packageName: string, selector: string) => Promise<string>} resolveVersion
|
|
247
|
+
*/
|
|
248
|
+
async function ensurePackageExtension(item, projectRoot, resolveVersion) {
|
|
249
|
+
if (!item.hasExtensionContract) throw new Error(`Package "${item.packageName}" does not provide create-pkgbld extension behavior`);
|
|
250
|
+
if (item.ext && (!item.lockedVersion || item.ext.__packageVersion === item.lockedVersion)) return item.ext;
|
|
251
|
+
const entry = item.lockedVersion ? { ...item.entry, version: item.lockedVersion, official: true } : item.entry;
|
|
252
|
+
const ext = await resolveExtension(entry, projectRoot, {
|
|
253
|
+
install: true,
|
|
254
|
+
exactVersion: item.lockedVersion ?? undefined,
|
|
255
|
+
resolveVersion,
|
|
256
|
+
});
|
|
257
|
+
item.ext = ext;
|
|
258
|
+
item.error = null;
|
|
259
|
+
item.resolvedVersion = ext.__packageVersion ?? item.resolvedVersion;
|
|
260
|
+
return ext;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* @param {PreparedPackageItem & { intent: PackageIntent, options: import('./types.js').OptionsValue }} item
|
|
265
|
+
* @param {import('./tree.js').Tree} tree
|
|
266
|
+
* @param {{ set(packageName: string, version: string): void, remove(packageName: string): void }} projectLock
|
|
267
|
+
* @param {(conflict: any) => void} reportConflict
|
|
268
|
+
*/
|
|
269
|
+
async function applyPackageIntent(item, tree, projectLock, reportConflict) {
|
|
270
|
+
if (item.intent === 'adopt') {
|
|
271
|
+
const version = item.resolvedVersion;
|
|
272
|
+
if (!version)
|
|
273
|
+
throw new PackageOperationError('VERSION_UNRESOLVED', `Cannot adopt "${item.packageName}": exact version is unavailable`);
|
|
274
|
+
projectLock.set(item.packageName, version);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (item.intent === 'setup') {
|
|
279
|
+
const ext = item.ext;
|
|
280
|
+
if (!ext) throw new Error(`Package "${item.packageName}" extension was not prepared`);
|
|
281
|
+
await runSetup(ext, tree, item.options);
|
|
282
|
+
const version = ext.__packageVersion;
|
|
283
|
+
if (!version) throw new Error(`Cannot lock "${item.packageName}": its exact package version could not be resolved`);
|
|
284
|
+
projectLock.set(item.packageName, version);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (item.intent === 'update') {
|
|
289
|
+
const previous = item.previousExtension;
|
|
290
|
+
const target = item.ext;
|
|
291
|
+
const fromVersion = item.lockedVersion;
|
|
292
|
+
const toVersion = item.resolvedVersion;
|
|
293
|
+
if (!previous || !target || !fromVersion || !toVersion) throw new Error(`Package "${item.packageName}" update was not prepared`);
|
|
294
|
+
const exclude =
|
|
295
|
+
item.kind === 'plugin'
|
|
296
|
+
? ['dependencies', 'devDependencies', 'peerDependencies'].map(field => `dependency:${field}:${item.packageName}`)
|
|
297
|
+
: [];
|
|
298
|
+
await runExtensionUpdate({
|
|
299
|
+
previous,
|
|
300
|
+
target,
|
|
301
|
+
tree,
|
|
302
|
+
fromVersion,
|
|
303
|
+
toVersion,
|
|
304
|
+
options: item.options,
|
|
305
|
+
exclude,
|
|
306
|
+
reportConflict,
|
|
307
|
+
});
|
|
308
|
+
if (item.kind === 'plugin') {
|
|
309
|
+
removePluginDependencies(tree, item.packageName);
|
|
310
|
+
tree.addDependency(item.packageName, toVersion, 'devDependencies');
|
|
311
|
+
}
|
|
312
|
+
projectLock.set(item.packageName, toVersion);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (item.ext) await runRemove(item.ext, tree, item.options);
|
|
317
|
+
if (item.kind === 'plugin') removePluginDependencies(tree, item.packageName);
|
|
318
|
+
projectLock.remove(item.packageName);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** @param {string} packageName @param {Record<string, any> | undefined} manifest @param {string} projectRoot */
|
|
322
|
+
function assertCompatible(packageName, manifest, projectRoot) {
|
|
323
|
+
try {
|
|
324
|
+
assertPluginCompatible(packageName, manifest, projectRoot);
|
|
325
|
+
} catch (/** @type {any} */ error) {
|
|
326
|
+
throw new PackageOperationError('PLUGIN_INCOMPATIBLE', error.message ?? String(error));
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/** @param {import('./tree.js').Tree} tree @param {string} packageName */
|
|
331
|
+
function removePluginDependencies(tree, packageName) {
|
|
332
|
+
tree.updateJson('package.json', pkg => {
|
|
333
|
+
for (const field of ['dependencies', 'devDependencies', 'peerDependencies']) {
|
|
334
|
+
if (!pkg[field] || !(packageName in pkg[field])) continue;
|
|
335
|
+
delete pkg[field][packageName];
|
|
336
|
+
if (Object.keys(pkg[field]).length === 0) delete pkg[field];
|
|
337
|
+
}
|
|
338
|
+
return pkg;
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** @param {import('./inventory.js').PackageItem} item @returns {PackageView} */
|
|
343
|
+
function toPackageView(item) {
|
|
344
|
+
return Object.freeze({
|
|
345
|
+
id: item.packageName,
|
|
346
|
+
name: item.entry.name,
|
|
347
|
+
description: item.entry.description ?? '',
|
|
348
|
+
tags: Object.freeze([...(item.entry.tags ?? [])]),
|
|
349
|
+
kind: item.kind,
|
|
350
|
+
state: item.state,
|
|
351
|
+
error: item.error,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Read package identity by walking upward from an already resolved module.
|
|
7
|
+
* @param {string} resolved
|
|
8
|
+
* @param {string} expectedName
|
|
9
|
+
*/
|
|
10
|
+
export function readResolvedPackage(resolved, expectedName) {
|
|
11
|
+
let dir = path.dirname(resolved);
|
|
12
|
+
for (;;) {
|
|
13
|
+
const manifestPath = path.join(dir, 'package.json');
|
|
14
|
+
try {
|
|
15
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
16
|
+
if (manifest.name === expectedName && typeof manifest.version === 'string') {
|
|
17
|
+
return { name: expectedName, version: manifest.version, dir, manifest };
|
|
18
|
+
}
|
|
19
|
+
} catch {
|
|
20
|
+
// Continue toward the filesystem root.
|
|
21
|
+
}
|
|
22
|
+
const parent = path.dirname(dir);
|
|
23
|
+
if (parent === dir) return null;
|
|
24
|
+
dir = parent;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Resolve the exact installed version from the target project without importing it.
|
|
30
|
+
* @param {string} packageName
|
|
31
|
+
* @param {string} projectRoot
|
|
32
|
+
*/
|
|
33
|
+
export function resolveInstalledPackage(packageName, projectRoot) {
|
|
34
|
+
try {
|
|
35
|
+
const resolved = createRequire(path.join(projectRoot, 'package.json')).resolve(packageName);
|
|
36
|
+
return readResolvedPackage(resolved, packageName);
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
import semver from 'semver';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Resolve the newest stable exact version published for a package selector.
|
|
7
|
+
* @param {string} packageName
|
|
8
|
+
* @param {string} selector
|
|
9
|
+
* @param {(command: string, args: string[]) => Promise<{ code: number, stdout: string, stderr: string }>} [runner]
|
|
10
|
+
*/
|
|
11
|
+
export async function resolvePublishedVersion(packageName, selector, runner = runCapture) {
|
|
12
|
+
const requested = `${packageName}@${selector || 'latest'}`;
|
|
13
|
+
const result = await runner('npm', ['view', requested, 'version', '--json']);
|
|
14
|
+
if (result.code !== 0) {
|
|
15
|
+
const detail = result.stderr.trim();
|
|
16
|
+
throw new Error(`Cannot resolve a published version for "${requested}"${detail ? `: ${detail}` : ''}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let value;
|
|
20
|
+
try {
|
|
21
|
+
value = JSON.parse(result.stdout);
|
|
22
|
+
} catch {
|
|
23
|
+
throw new Error(`npm returned invalid version metadata for "${requested}"`);
|
|
24
|
+
}
|
|
25
|
+
const versions = (Array.isArray(value) ? value : [value]).filter(
|
|
26
|
+
version => typeof version === 'string' && semver.valid(version) && semver.prerelease(version) === null
|
|
27
|
+
);
|
|
28
|
+
const version = semver.rsort(versions)[0];
|
|
29
|
+
if (!version) throw new Error(`No stable published version satisfies "${requested}"`);
|
|
30
|
+
return version;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** @param {string} command @param {string[]} args */
|
|
34
|
+
function runCapture(command, args) {
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
const child = spawn(command, args, { stdio: ['ignore', 'pipe', 'pipe'], shell: process.platform === 'win32' });
|
|
37
|
+
let stdout = '';
|
|
38
|
+
let stderr = '';
|
|
39
|
+
child.stdout.on('data', chunk => {
|
|
40
|
+
stdout += chunk;
|
|
41
|
+
});
|
|
42
|
+
child.stderr.on('data', chunk => {
|
|
43
|
+
stderr += chunk;
|
|
44
|
+
});
|
|
45
|
+
child.on('error', reject);
|
|
46
|
+
child.on('close', code => resolve({ code: code ?? 1, stdout, stderr }));
|
|
47
|
+
});
|
|
48
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import semver from 'semver';
|
|
5
|
+
|
|
6
|
+
import { resolveInstalledPackage } from './package-resolution.js';
|
|
7
|
+
|
|
8
|
+
/** @param {Record<string, any> | undefined} manifest */
|
|
9
|
+
export function getPkgbldPeerRange(manifest) {
|
|
10
|
+
const range = manifest?.peerDependencies?.pkgbld;
|
|
11
|
+
return typeof range === 'string' && range.trim() ? range.trim() : null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Inspect an installed plugin without importing it.
|
|
16
|
+
* @param {string} packageName
|
|
17
|
+
* @param {string} projectRoot
|
|
18
|
+
*/
|
|
19
|
+
export function inspectInstalledPlugin(packageName, projectRoot) {
|
|
20
|
+
const resolved = resolveInstalledPackage(packageName, projectRoot);
|
|
21
|
+
if (!resolved) {
|
|
22
|
+
return {
|
|
23
|
+
eligible: false,
|
|
24
|
+
resolved: null,
|
|
25
|
+
warning: `Ignoring ${packageName}: its installed package metadata cannot be resolved. Install project dependencies before managing it with create-pkgbld.`,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
if (!getPkgbldPeerRange(resolved.manifest)) {
|
|
29
|
+
return {
|
|
30
|
+
eligible: false,
|
|
31
|
+
resolved,
|
|
32
|
+
warning: `Ignoring ${packageName}: the installed package does not declare pkgbld in peerDependencies. Upgrade the plugin before managing it with create-pkgbld.`,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
return { eligible: true, resolved, warning: null };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Validate a candidate plugin manifest against the target project's PKG BLD.
|
|
40
|
+
* Workspace peer protocols are accepted for local monorepo development; they
|
|
41
|
+
* are converted to normal semver ranges when packages are published.
|
|
42
|
+
* @param {string} packageName
|
|
43
|
+
* @param {Record<string, any> | undefined} manifest
|
|
44
|
+
* @param {string} projectRoot
|
|
45
|
+
*/
|
|
46
|
+
export function assertPluginCompatible(packageName, manifest, projectRoot) {
|
|
47
|
+
const peerRange = getPkgbldPeerRange(manifest);
|
|
48
|
+
if (!peerRange) {
|
|
49
|
+
throw new Error(`Plugin "${packageName}" does not declare pkgbld in peerDependencies; upgrade it first`);
|
|
50
|
+
}
|
|
51
|
+
if (peerRange.startsWith('workspace:')) return;
|
|
52
|
+
|
|
53
|
+
const host = resolveInstalledPackage('pkgbld', projectRoot);
|
|
54
|
+
if (host) {
|
|
55
|
+
if (!semver.satisfies(host.version, peerRange)) {
|
|
56
|
+
throw new Error(`Plugin "${packageName}" requires pkgbld ${peerRange}, but the project resolves ${host.version}`);
|
|
57
|
+
}
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const declared = readDeclaredPkgbldRange(projectRoot);
|
|
62
|
+
if (!declared) throw new Error(`Plugin "${packageName}" requires pkgbld ${peerRange}, but the project does not declare pkgbld`);
|
|
63
|
+
if (!semver.validRange(declared) || !semver.intersects(declared, peerRange)) {
|
|
64
|
+
throw new Error(`Plugin "${packageName}" requires pkgbld ${peerRange}, but the project declares ${declared}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** @param {string} projectRoot */
|
|
69
|
+
function readDeclaredPkgbldRange(projectRoot) {
|
|
70
|
+
try {
|
|
71
|
+
const pkg = JSON.parse(readFileSync(path.join(projectRoot, 'package.json'), 'utf8'));
|
|
72
|
+
for (const field of ['dependencies', 'devDependencies', 'peerDependencies']) {
|
|
73
|
+
if (typeof pkg[field]?.pkgbld === 'string') return pkg[field].pkgbld;
|
|
74
|
+
}
|
|
75
|
+
} catch {
|
|
76
|
+
// The caller reports the missing host declaration.
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|