@jskit-ai/jskit-cli 0.2.152 → 0.2.154

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/jskit-cli",
3
- "version": "0.2.152",
3
+ "version": "0.2.154",
4
4
  "description": "Bundle and package orchestration CLI for JSKIT apps.",
5
5
  "type": "module",
6
6
  "files": [
@@ -21,10 +21,11 @@
21
21
  "test": "node --test"
22
22
  },
23
23
  "dependencies": {
24
- "@jskit-ai/jskit-catalog": "0.1.146",
25
- "@jskit-ai/kernel": "0.1.131",
26
- "@jskit-ai/shell-web": "0.1.132",
24
+ "@jskit-ai/jskit-catalog": "0.1.148",
25
+ "@jskit-ai/kernel": "0.1.133",
26
+ "@jskit-ai/shell-web": "0.1.134",
27
27
  "@vue/compiler-sfc": "^3.5.29",
28
+ "semver": "^7.7.4",
28
29
  "ts-morph": "^28.0.0",
29
30
  "yaml": "^2.8.3"
30
31
  },
@@ -1,5 +1,6 @@
1
1
  import { readFile, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
+ import semver from "semver";
3
4
 
4
5
  import {
5
6
  runExternalCommandAsync,
@@ -81,6 +82,144 @@ function resolveInstallSpecs(packageNames = [], latestVersions = new Map()) {
81
82
  return packageNames.map((packageName) => `${packageName}@${latestVersions.get(packageName)}`);
82
83
  }
83
84
 
85
+ function parseRegistryPackageManifest(rawValue, packageName, createCliError) {
86
+ let parsed;
87
+ try {
88
+ parsed = JSON.parse(String(rawValue || ""));
89
+ } catch (error) {
90
+ throw createCliError(
91
+ `npm returned invalid metadata for ${packageName}: ${error instanceof Error ? error.message : String(error)}.`,
92
+ { exitCode: 1 }
93
+ );
94
+ }
95
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
96
+ throw createCliError(`npm returned invalid metadata for ${packageName}: expected an object.`, {
97
+ exitCode: 1
98
+ });
99
+ }
100
+ return parsed;
101
+ }
102
+
103
+ async function resolveRegistryPackageManifests(packageNames = [], latestVersions = new Map(), {
104
+ appRoot,
105
+ createCliError,
106
+ registryArgs,
107
+ stderr,
108
+ stdout
109
+ }) {
110
+ const manifests = new Map();
111
+ for (const packageName of packageNames) {
112
+ const version = latestVersions.get(packageName);
113
+ const result = await runExternalCommandAsync(
114
+ "npm",
115
+ ["view", ...registryArgs, `${packageName}@${version}`, "--json"],
116
+ {
117
+ cwd: appRoot,
118
+ stdout,
119
+ stderr,
120
+ quiet: true,
121
+ createCliError
122
+ }
123
+ );
124
+ manifests.set(
125
+ packageName,
126
+ parseRegistryPackageManifest(result.stdout, packageName, createCliError)
127
+ );
128
+ }
129
+ return manifests;
130
+ }
131
+
132
+ function resolveDeclaredDependencySection(packageJson = {}, packageName = "") {
133
+ return DEPENDENCY_SECTIONS.find((section) =>
134
+ Object.prototype.hasOwnProperty.call(packageJson?.[section.name] || {}, packageName)
135
+ ) || null;
136
+ }
137
+
138
+ function findRangeIntersectionVersion(ranges = []) {
139
+ const normalizedRanges = ranges
140
+ .map((range) => semver.validRange(String(range || "").trim()))
141
+ .filter(Boolean);
142
+ if (normalizedRanges.length !== ranges.length) {
143
+ return null;
144
+ }
145
+
146
+ const candidates = normalizedRanges
147
+ .map((range) => semver.minVersion(range))
148
+ .filter(Boolean)
149
+ .sort(semver.compare);
150
+ return candidates.find((version) =>
151
+ normalizedRanges.every((range) => semver.satisfies(version, range))
152
+ ) || null;
153
+ }
154
+
155
+ function resolveRequiredDirectPeerUpdates({
156
+ createCliError,
157
+ packageJson = {},
158
+ packageManifests = new Map()
159
+ } = {}) {
160
+ const requirementsByPeer = new Map();
161
+ for (const [packageName, manifest] of packageManifests.entries()) {
162
+ const peerDependencies = ensureObject(manifest?.peerDependencies);
163
+ const peerDependenciesMeta = ensureObject(manifest?.peerDependenciesMeta);
164
+ for (const [peerName, rawRange] of Object.entries(peerDependencies)) {
165
+ if (ensureObject(peerDependenciesMeta[peerName]).optional === true) {
166
+ continue;
167
+ }
168
+ const section = resolveDeclaredDependencySection(packageJson, peerName);
169
+ if (!section) {
170
+ continue;
171
+ }
172
+ const range = String(rawRange || "").trim();
173
+ if (!semver.validRange(range)) {
174
+ throw createCliError(
175
+ `${packageName} declares an invalid required peer range for ${peerName}: ${range || "<empty>"}.`,
176
+ { exitCode: 1 }
177
+ );
178
+ }
179
+ const requirement = requirementsByPeer.get(peerName) || {
180
+ packageNames: [],
181
+ ranges: [],
182
+ section
183
+ };
184
+ requirement.packageNames.push(packageName);
185
+ requirement.ranges.push(range);
186
+ requirementsByPeer.set(peerName, requirement);
187
+ }
188
+ }
189
+
190
+ const updates = [];
191
+ for (const [peerName, requirement] of [...requirementsByPeer.entries()].sort(([left], [right]) =>
192
+ left.localeCompare(right)
193
+ )) {
194
+ const uniqueRanges = [...new Set(requirement.ranges)];
195
+ const targetVersion = findRangeIntersectionVersion(uniqueRanges);
196
+ if (!targetVersion) {
197
+ throw createCliError(
198
+ `Updated JSKIT packages require incompatible ${peerName} peers: ${uniqueRanges.join(", ")}.`,
199
+ { exitCode: 1 }
200
+ );
201
+ }
202
+
203
+ const currentRange = String(packageJson?.[requirement.section.name]?.[peerName] || "").trim();
204
+ if (findRangeIntersectionVersion([currentRange, ...uniqueRanges])) {
205
+ continue;
206
+ }
207
+
208
+ const subsetRange = uniqueRanges.find((candidateRange) =>
209
+ uniqueRanges.every((otherRange) => semver.subset(candidateRange, otherRange))
210
+ );
211
+ updates.push(Object.freeze({
212
+ name: peerName,
213
+ packageNames: Object.freeze([...new Set(requirement.packageNames)].sort()),
214
+ previousRange: currentRange,
215
+ section: requirement.section,
216
+ targetRange: subsetRange || targetVersion.version
217
+ }));
218
+ }
219
+
220
+ return Object.freeze(updates);
221
+ }
222
+
84
223
  function collectChangedInstalledPackageIds(lock = {}, latestVersions = new Map()) {
85
224
  const installedPackages = ensureObject(lock.installedPackages);
86
225
  return sortStrings(
@@ -474,13 +613,53 @@ async function updateRootPackages({
474
613
  stderr,
475
614
  stdout
476
615
  });
616
+ const packageManifests = await resolveRegistryPackageManifests(
617
+ rootPackageNames,
618
+ latestVersions,
619
+ {
620
+ appRoot,
621
+ createCliError,
622
+ registryArgs,
623
+ stderr,
624
+ stdout
625
+ }
626
+ );
627
+ const peerUpdates = resolveRequiredDirectPeerUpdates({
628
+ createCliError,
629
+ packageJson,
630
+ packageManifests
631
+ });
632
+ for (const peerUpdate of peerUpdates) {
633
+ stdout?.write(
634
+ `[jskit:update] reconciling required direct peer ${peerUpdate.name}: ` +
635
+ `${peerUpdate.previousRange} -> ${peerUpdate.targetRange} ` +
636
+ `(required by ${peerUpdate.packageNames.join(", ")}).\n`
637
+ );
638
+ packageJson[peerUpdate.section.name][peerUpdate.name] = peerUpdate.targetRange;
639
+ }
640
+ if (!dryRun && peerUpdates.length > 0) {
641
+ await writeFile(
642
+ path.join(appRoot, "package.json"),
643
+ `${JSON.stringify(packageJson, null, 2)}\n`,
644
+ "utf8"
645
+ );
646
+ }
647
+
477
648
  const dryRunArgs = dryRun ? ["--dry-run"] : [];
478
649
  for (const section of DEPENDENCY_SECTIONS) {
479
650
  const packageNames = collectJskitPackageNames(packageJson?.[section.name]);
480
- if (packageNames.length < 1) {
651
+ const dryRunPeerSpecs = dryRun
652
+ ? peerUpdates
653
+ .filter((peerUpdate) => peerUpdate.section.name === section.name)
654
+ .map((peerUpdate) => `${peerUpdate.name}@${peerUpdate.targetRange}`)
655
+ : [];
656
+ if (packageNames.length < 1 && dryRunPeerSpecs.length < 1) {
481
657
  continue;
482
658
  }
483
- const installSpecs = resolveInstallSpecs(packageNames, latestVersions);
659
+ const installSpecs = [
660
+ ...resolveInstallSpecs(packageNames, latestVersions),
661
+ ...dryRunPeerSpecs
662
+ ];
484
663
  stdout?.write(`[jskit:update] updating ${section.label} packages: ${installSpecs.join(" ")}\n`);
485
664
  await runExternalCommandAsync(
486
665
  "npm",
@@ -685,7 +864,9 @@ async function runAppUpdatePackagesCommand(ctx = {}, { appRoot = "", options = {
685
864
 
686
865
  export {
687
866
  collectChangedInstalledPackageIds,
867
+ findRangeIntersectionVersion,
688
868
  formatElapsedTime,
869
+ resolveRequiredDirectPeerUpdates,
689
870
  reapplyChangedInstalledPackages,
690
871
  runAppUpdatePackagesCommand,
691
872
  runWithProgress