@sparkelf/dsh-plus 0.2.1-rc.2.4 → 0.2.2-alpha.2.1

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.
Files changed (37) hide show
  1. package/README.md +7 -5
  2. package/README.zh.md +7 -5
  3. package/lib/apply-08huHfLG.js +665 -0
  4. package/lib/apply-DmICvIGh.js +653 -0
  5. package/lib/apply.js +1 -1
  6. package/lib/bin.js +133 -37
  7. package/lib/types/apply.d.ts +1 -1
  8. package/lib/types/apply.js +56 -117
  9. package/lib/types/patch-set.d.ts +59 -0
  10. package/lib/types/patch-set.js +132 -0
  11. package/lib/types/standalone-cli.js +56 -2
  12. package/lib/types/standalone-profile.d.ts +8 -1
  13. package/lib/types/standalone-profile.js +84 -38
  14. package/package.json +625 -175
  15. package/patches/approval-allow-policy.patch +108 -0
  16. package/patches/atomic-write-zombie-lock-holder.patch +53 -0
  17. package/patches/bootstrap-exempt-names.patch +32 -0
  18. package/patches/browser-auth-mode.patch +127 -0
  19. package/patches/composer-popover-boundaries.patch +337 -0
  20. package/patches/diagram-preview.patch +562 -0
  21. package/patches/image-offload-hoist-replay.patch +533 -0
  22. package/patches/legacy-code-preset.patch +148 -0
  23. package/patches/llm-pi-ai-transport-truncation.patch +33 -0
  24. package/patches/mobile-journal-generation.patch +76 -0
  25. package/patches/office-preview-stable-load.patch +34 -0
  26. package/patches/officecli-deliverables.patch +42 -0
  27. package/patches/ptc-mcp-schema-types.patch +156 -0
  28. package/patches/responses-reasoning-status.patch +289 -0
  29. package/patches/session-envelope-ignorable.patch +63 -0
  30. package/patches/session-export-chinese.patch +85 -0
  31. package/patches/session-format-legacy-restart.patch +35 -0
  32. package/patches/session-log-trajectory-toolbar.patch +352 -0
  33. package/patches/state-dot-brand-blue.patch +185 -0
  34. package/patches/ui-settings-compact-phone.patch +76 -0
  35. package/patches/web-runtime-compat.patch +83 -0
  36. package/patches/workspace-storage-restore.patch +186 -0
  37. package/patches/wsl-native-open.patch +265 -0
package/lib/bin.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { r as runApply } from "./apply-e0aMC4B4.js";
2
+ import { i as readPatchSet, r as runApply } from "./apply-08huHfLG.js";
3
3
  import { createRequire } from "node:module";
4
4
  import { execFileSync, spawn, spawnSync } from "node:child_process";
5
5
  import { createWriteStream, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
@@ -858,6 +858,11 @@ function requireStringArray(value, label) {
858
858
  if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) throw new Error(label + " must be a string array");
859
859
  return value;
860
860
  }
861
+ /** Read one non-empty string field. */
862
+ function requireString(value, label) {
863
+ if (typeof value !== "string" || value.trim() === "") throw new Error(label + " must be a non-empty string");
864
+ return value;
865
+ }
861
866
  /** Resolve the DSH home, honouring the environment override the launcher itself reads. */
862
867
  function resolveHome(env = process.env) {
863
868
  const configured = env.DSH_HOME;
@@ -910,18 +915,12 @@ function resolveDistributionDirectory(anchor) {
910
915
  }
911
916
  throw new Error("@sparkelf/dsh-plus resolved outside " + resolve(dirname(anchor)) + " (found " + resolved + "); run this command from the directory that installed it");
912
917
  }
913
- /** The reviewed bundle order and pins the installed distribution declares. */
914
918
  function readDistributionProfile(distributionDirectory) {
915
919
  const manifest = requireRecord(JSON.parse(readFileSync(join(distributionDirectory, "package.json"), "utf8")), "Plus distribution manifest");
916
920
  const plus = requireRecord(manifest.dshPlus, "dshPlus");
917
921
  const profile = requireRecord(plus.profile, "dshPlus.profile");
918
- const rawDependencies = requireRecord(profile.dependencies, "dshPlus.profile.dependencies");
922
+ const dependencies = requireDependencyMap(requireRecord(profile.dependencies, "dshPlus.profile.dependencies"), "dshPlus.profile.dependencies");
919
923
  const rawAllowBuilds = requireRecord(profile.allowBuilds, "dshPlus.profile.allowBuilds");
920
- const dependencies = {};
921
- for (const [name, spec] of Object.entries(rawDependencies)) {
922
- if (typeof spec !== "string" || spec === "") throw new Error("dshPlus.profile.dependencies." + name + " must be a non-empty string");
923
- dependencies[name] = spec;
924
- }
925
924
  const allowBuilds = {};
926
925
  for (const [name, allowed] of Object.entries(rawAllowBuilds)) {
927
926
  if (typeof allowed !== "boolean") throw new Error("dshPlus.profile.allowBuilds." + name + " must be a boolean");
@@ -933,6 +932,16 @@ function readDistributionProfile(distributionDirectory) {
933
932
  if (typeof spec !== "string" || spec === "") throw new Error("dshPlus.profile.overrides." + name + " must be a non-empty string");
934
933
  overrides[name] = spec;
935
934
  }
935
+ const editions = {};
936
+ const rawEditions = plus.editions === void 0 ? {} : requireRecord(plus.editions, "dshPlus.editions");
937
+ for (const [id, value] of Object.entries(rawEditions)) {
938
+ const edition = requireRecord(value, "dshPlus.editions." + id);
939
+ editions[id] = {
940
+ label: requireString(edition.label, "dshPlus.editions." + id + ".label"),
941
+ bundles: requireStringArray(edition.bundles, "dshPlus.editions." + id + ".bundles"),
942
+ packages: requireDependencyMap(requireRecord(edition.packages, "dshPlus.editions." + id + ".packages"), "dshPlus.editions." + id + ".packages")
943
+ };
944
+ }
936
945
  const compatibility = requireRecord(plus.compatibility, "dshPlus.compatibility");
937
946
  return {
938
947
  name: String(manifest.name),
@@ -940,10 +949,20 @@ function readDistributionProfile(distributionDirectory) {
940
949
  dependencies,
941
950
  allowBuilds,
942
951
  overrides,
952
+ editions,
943
953
  dshRange: String(compatibility.dsh),
944
954
  version: String(manifest.version)
945
955
  };
946
956
  }
957
+ /** Read one package-name-to-spec map, rejecting empty entries. */
958
+ function requireDependencyMap(value, label) {
959
+ const dependencies = {};
960
+ for (const [name, spec] of Object.entries(value)) {
961
+ if (typeof spec !== "string" || spec === "") throw new Error(label + "." + name + " must be a non-empty string");
962
+ dependencies[name] = spec;
963
+ }
964
+ return dependencies;
965
+ }
947
966
  /**
948
967
  * Give the profile its own installed tree, built from the distribution's declarations.
949
968
  *
@@ -993,7 +1012,8 @@ function installWithRegistryFallback(paths) {
993
1012
  ], {
994
1013
  cwd: paths.profileDirectory,
995
1014
  stdio: "inherit",
996
- shell: process.platform === "win32"
1015
+ shell: process.platform === "win32",
1016
+ timeout: 5 * 6e4
997
1017
  });
998
1018
  if (result.error !== void 0) throw result.error;
999
1019
  if (result.status === 0) return;
@@ -1153,12 +1173,35 @@ function resolvePaths(anchor, env = process.env) {
1153
1173
  *
1154
1174
  * @param paths - resolved standalone paths.
1155
1175
  * @param consumerDirectory - directory whose `node_modules` holds the installed packages.
1176
+ * @param edition - edition id whose package list to install, or undefined for the default.
1156
1177
  * @returns whether this call created the manifest.
1157
1178
  */
1158
- function ensureProfile(paths, consumerDirectory) {
1179
+ function ensureProfile(paths, consumerDirectory, edition) {
1159
1180
  const manifestPath = join(paths.profileDirectory, "package.json");
1160
1181
  const distribution = readDistributionProfile(paths.distributionDirectory);
1182
+ const composition = edition === void 0 ? void 0 : distribution.editions[edition];
1183
+ if (edition !== void 0 && composition === void 0) {
1184
+ const offered = Object.keys(distribution.editions);
1185
+ throw new Error("unknown edition \"" + edition + "\"; this release offers " + (offered.length === 0 ? "(none)" : offered.join(", ")));
1186
+ }
1187
+ const editionPackages = composition?.packages ?? {};
1188
+ const editionBundles = composition?.bundles ?? distribution.bundles;
1161
1189
  if (existsSync(manifestPath)) {
1190
+ if (composition !== void 0) {
1191
+ const manifest = requireRecord(JSON.parse(readFileSync(manifestPath, "utf8")), "Plus profile manifest");
1192
+ manifest.dependencies = {
1193
+ ...manifest.dependencies,
1194
+ ...editionPackages
1195
+ };
1196
+ manifest.dsh = {
1197
+ ...manifest.dsh,
1198
+ profile: {
1199
+ ...manifest.dsh?.profile,
1200
+ bundles: [...editionBundles]
1201
+ }
1202
+ };
1203
+ writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
1204
+ }
1162
1205
  writeProfileOverrides(paths.profileDirectory, distribution.overrides, distribution.allowBuilds, paths.omittedPackages);
1163
1206
  installProfilePackages(paths, consumerDirectory);
1164
1207
  return false;
@@ -1171,10 +1214,11 @@ function ensureProfile(paths, consumerDirectory) {
1171
1214
  dependencies: {
1172
1215
  "@sparkelf/dsh-plus": distribution.version,
1173
1216
  "@deepseek-ai/dsh": distribution.dshRange,
1174
- ...distribution.dependencies
1217
+ ...distribution.dependencies,
1218
+ ...editionPackages
1175
1219
  },
1176
1220
  dsh: { profile: {
1177
- bundles: distribution.bundles,
1221
+ bundles: [...editionBundles],
1178
1222
  patchReload: "live"
1179
1223
  } }
1180
1224
  };
@@ -1234,30 +1278,21 @@ function git(root, args, acceptFailure = false) {
1234
1278
  * @returns the labels of the patches that were applied.
1235
1279
  */
1236
1280
  function applyProfileNpmPatches(distributionDirectory, profileDirectory) {
1237
- const names = requireRecord(requireRecord(JSON.parse(readFileSync(join(distributionDirectory, "package.json"), "utf8")), "Plus distribution manifest").dshPlus, "dshPlus").patchPackages;
1238
- if (!Array.isArray(names)) throw new Error("dshPlus.patchPackages must be an array");
1281
+ const patchSet = readPatchSet(distributionDirectory, requireRecord(JSON.parse(readFileSync(join(distributionDirectory, "package.json"), "utf8")), "Plus distribution manifest"));
1239
1282
  const applied = [];
1240
- for (const value of names) {
1241
- if (typeof value !== "string" || value === "") throw new Error("dshPlus.patchPackages entries must be non-empty strings");
1242
- const patchPackage = resolveInstalledPackage(distributionDirectory, value);
1243
- const variants = requireRecord(patchPackage.manifest.dshPatch, value + " dshPatch").variants;
1244
- if (!Array.isArray(variants)) throw new Error(value + " dshPatch.variants must be an array");
1245
- for (const entry of variants) {
1246
- const variant = requireRecord(entry, value + " variant");
1247
- const target = requireRecord(variant.target, value + " variant target");
1248
- if (target.kind !== "npm") continue;
1249
- const targetName = String(target.name);
1250
- const patched = resolveInstalledPackage(profileDirectory, targetName);
1251
- const file = resolve(patchPackage.directory, String(variant.file));
1252
- if (git(patched.directory, [
1253
- "apply",
1254
- "--reverse",
1255
- "--check",
1256
- file
1257
- ], true) !== void 0) continue;
1258
- git(patched.directory, ["apply", file]);
1259
- applied.push(value + " -> " + targetName);
1260
- }
1283
+ for (const entry of [...patchSet.entries, ...patchSet.thirdPartyPatches]) {
1284
+ if (entry.target.kind !== "npm") continue;
1285
+ const targetName = entry.target.name;
1286
+ const patched = resolveInstalledPackage(profileDirectory, targetName);
1287
+ const file = resolve(distributionDirectory, entry.file);
1288
+ if (git(patched.directory, [
1289
+ "apply",
1290
+ "--reverse",
1291
+ "--check",
1292
+ file
1293
+ ], true) !== void 0) continue;
1294
+ git(patched.directory, ["apply", file]);
1295
+ applied.push(entry.id + " -> " + targetName);
1261
1296
  }
1262
1297
  return applied;
1263
1298
  }
@@ -1268,7 +1303,18 @@ function resolveInstalledPackage(from, packageName) {
1268
1303
  try {
1269
1304
  manifestPath = requireFrom.resolve(packageName + "/package.json");
1270
1305
  } catch {
1271
- throw new Error(packageName + " is not installed under " + from);
1306
+ let directory;
1307
+ try {
1308
+ directory = dirname(requireFrom.resolve(packageName));
1309
+ } catch {
1310
+ throw new Error(packageName + " is not installed under " + from);
1311
+ }
1312
+ while (!existsSync(resolve(directory, "package.json"))) {
1313
+ const parent = dirname(directory);
1314
+ if (parent === directory) throw new Error(packageName + " has no resolvable package manifest under " + from);
1315
+ directory = parent;
1316
+ }
1317
+ manifestPath = resolve(directory, "package.json");
1272
1318
  }
1273
1319
  const manifest = requireRecord(JSON.parse(readFileSync(manifestPath, "utf8")), packageName + " manifest");
1274
1320
  return {
@@ -1298,6 +1344,7 @@ function parseStartOptions(argv) {
1298
1344
  let host = "127.0.0.1";
1299
1345
  let open = true;
1300
1346
  let foreground = false;
1347
+ let edition;
1301
1348
  let capabilities;
1302
1349
  for (let index = 0; index < argv.length; index += 1) {
1303
1350
  const token = argv[index];
@@ -1323,6 +1370,13 @@ function parseStartOptions(argv) {
1323
1370
  foreground = true;
1324
1371
  continue;
1325
1372
  }
1373
+ if (token === "--edition") {
1374
+ const value = argv[index + 1];
1375
+ if (value === void 0 || value === "") throw new Error("--edition requires a value");
1376
+ edition = value;
1377
+ index += 1;
1378
+ continue;
1379
+ }
1326
1380
  if (token === "--capabilities") {
1327
1381
  const value = argv[index + 1];
1328
1382
  if (value === void 0) throw new Error("--capabilities requires a comma-separated list, or an empty string for none");
@@ -1340,6 +1394,7 @@ function parseStartOptions(argv) {
1340
1394
  host,
1341
1395
  open,
1342
1396
  foreground,
1397
+ edition,
1343
1398
  capabilities
1344
1399
  };
1345
1400
  }
@@ -1425,6 +1480,40 @@ function runForeground(entry, profileName, port, host, open) {
1425
1480
  if (!open) args.push("--no-open");
1426
1481
  return spawnSync(process.execPath, args, { stdio: "inherit" }).status ?? 1;
1427
1482
  }
1483
+ /** Parse the install command's edition selection. */
1484
+ function parseInstallOptions(argv) {
1485
+ let edition;
1486
+ for (let index = 0; index < argv.length; index += 1) {
1487
+ const token = argv[index];
1488
+ if (token === "--edition") {
1489
+ const value = argv[index + 1];
1490
+ if (value === void 0 || value === "") throw new Error("--edition requires a value");
1491
+ edition = value;
1492
+ index += 1;
1493
+ continue;
1494
+ }
1495
+ throw new Error("unknown option: " + String(token));
1496
+ }
1497
+ if (edition === void 0) throw new Error("install requires --edition personal or --edition team");
1498
+ return { edition };
1499
+ }
1500
+ /**
1501
+ * Install one edition's package manifest into the profile.
1502
+ *
1503
+ * The profile's `package.json` is the install artifact; there is no separate standalone
1504
+ * package. A later `start` uses the same manifest, so changing the edition is a new
1505
+ * install rather than a second product.
1506
+ *
1507
+ * @param argv - arguments after `install`.
1508
+ * @returns the process exit code.
1509
+ */
1510
+ function install(argv) {
1511
+ const { edition } = parseInstallOptions(argv);
1512
+ const paths = resolvePaths(declarationAnchor());
1513
+ ensureProfile(paths, installationRoot(), edition);
1514
+ console.log("Installed the " + edition + " edition into " + paths.profileDirectory);
1515
+ return 0;
1516
+ }
1428
1517
  async function start(argv) {
1429
1518
  const options = parseStartOptions(argv);
1430
1519
  const anchor = installationAnchor();
@@ -1458,8 +1547,9 @@ async function start(argv) {
1458
1547
  }
1459
1548
  console.log("pnpm installed.");
1460
1549
  }
1461
- const created = ensureProfile(paths, installationRoot());
1550
+ const created = ensureProfile(paths, installationRoot(), options.edition);
1462
1551
  console.log(created ? "Created the " + paths.profileName + " profile at " + paths.profileDirectory : "Using the existing " + paths.profileName + " profile");
1552
+ if (options.edition !== void 0) console.log("Edition: " + options.edition);
1463
1553
  for (const label of applyProfileNpmPatches(paths.distributionDirectory, paths.profileDirectory)) console.log("Applied the reviewed patch " + label);
1464
1554
  const answers = created || !existsSync(join(paths.home, "capabilities.json")) ? options.capabilities === void 0 ? await interviewCapabilities(true) : selectCapabilities(options.capabilities) : void 0;
1465
1555
  if (answers !== void 0) {
@@ -1809,6 +1899,7 @@ const USAGE = [
1809
1899
  "Usage: dsh-plus <command> [options]",
1810
1900
  "",
1811
1901
  "Commands:",
1902
+ " install install an edition package manifest into the profile",
1812
1903
  " start start the server (first run creates the profile)",
1813
1904
  " stop stop the server",
1814
1905
  " status report whether the server is running",
@@ -1821,6 +1912,10 @@ const USAGE = [
1821
1912
  " --host <h> interface to bind (default 127.0.0.1)",
1822
1913
  " --no-open do not open a browser",
1823
1914
  " --foreground run in this terminal instead of in the background",
1915
+ " --edition <id> install the named edition manifest (personal or team)",
1916
+ "",
1917
+ "install options:",
1918
+ " --edition <id> package manifest to install (personal or team)",
1824
1919
  "",
1825
1920
  "capabilities options:",
1826
1921
  " --enable <ids> enable the comma-separated capability ids",
@@ -1843,6 +1938,7 @@ async function runStandaloneCli(argv) {
1843
1938
  console.log(USAGE);
1844
1939
  return 0;
1845
1940
  }
1941
+ if (command === "install") return install(rest);
1846
1942
  if (command === "start") return await start(rest);
1847
1943
  if (command === "stop") return stop();
1848
1944
  if (command === "status") return status();
@@ -1,4 +1,4 @@
1
- /** Materialize the Plus distribution's independent patch packages without Desktop. */
1
+ /** Materialize the Plus distribution's embedded patch set without Desktop. */
2
2
  /** One official workspace package available as a profile peer dependency. */
3
3
  export interface OfficialWorkspacePackage {
4
4
  name: string;
@@ -1,11 +1,12 @@
1
- /** Materialize the Plus distribution's independent patch packages without Desktop. */
1
+ /** Materialize the Plus distribution's embedded patch set without Desktop. */
2
2
  import { spawnSync } from 'node:child_process';
3
3
  import { chmodSync, copyFileSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, symlinkSync, writeFileSync, } from 'node:fs';
4
4
  import { createRequire } from 'node:module';
5
- import { dirname, isAbsolute, relative, resolve } from 'node:path';
5
+ import { basename, dirname, isAbsolute, relative, resolve } from 'node:path';
6
6
  import { tmpdir } from 'node:os';
7
7
  import { satisfies, valid, validRange } from 'semver';
8
8
  import { parseDocument } from 'yaml';
9
+ import { readPatchSet } from "./patch-set.js";
9
10
  const FORMAT_VERSION = 1;
10
11
  function isSelectedNpmPatch(patch) {
11
12
  return patch.target.kind === 'npm';
@@ -50,7 +51,22 @@ function requireStringArray(value, label) {
50
51
  }
51
52
  function resolveInstalledPackage(distributionDirectory, packageName) {
52
53
  const requireFromDistribution = createRequire(resolve(distributionDirectory, 'package.json'));
53
- const manifestPath = requireFromDistribution.resolve(packageName + '/package.json');
54
+ let manifestPath;
55
+ try {
56
+ manifestPath = requireFromDistribution.resolve(packageName + '/package.json');
57
+ }
58
+ catch {
59
+ // Some packages do not export `./package.json`; resolve the entry module instead and walk
60
+ // up to the directory that holds the manifest.
61
+ let directory = dirname(requireFromDistribution.resolve(packageName));
62
+ while (!existsSync(resolve(directory, 'package.json'))) {
63
+ const parent = dirname(directory);
64
+ if (parent === directory)
65
+ throw new Error(packageName + ' has no resolvable package manifest');
66
+ directory = parent;
67
+ }
68
+ manifestPath = resolve(directory, 'package.json');
69
+ }
54
70
  const manifest = readJsonObject(manifestPath, packageName + ' manifest');
55
71
  return {
56
72
  name: packageName,
@@ -59,48 +75,6 @@ function resolveInstalledPackage(distributionDirectory, packageName) {
59
75
  manifest,
60
76
  };
61
77
  }
62
- function resolvePatchFile(packageDirectory, declared) {
63
- const absoluteFile = resolve(packageDirectory, declared);
64
- const local = relative(packageDirectory, absoluteFile);
65
- if (isAbsolute(local) || local === '..' || local.startsWith('../')) {
66
- throw new Error('patch file must stay inside its patch package: ' + declared);
67
- }
68
- if (!existsSync(absoluteFile))
69
- throw new Error('patch file does not exist: ' + absoluteFile);
70
- return { relativeFile: local.replaceAll('\\', '/'), absoluteFile };
71
- }
72
- function parseVariant(value, index, patchPackage) {
73
- const label = patchPackage.name + ' dshPatch.variants[' + String(index) + ']';
74
- const variant = requireObject(value, label);
75
- const target = requireObject(variant.target, label + '.target');
76
- const kind = requireString(target.kind, label + '.target.kind');
77
- const common = {
78
- id: requireString(variant.id, label + '.id'),
79
- dsh: requireRange(variant.dsh, label + '.dsh'),
80
- file: requireString(variant.file, label + '.file'),
81
- patchPackage,
82
- };
83
- if (kind === 'npm') {
84
- return {
85
- ...common,
86
- target: {
87
- kind,
88
- name: requireString(target.name, label + '.target.name'),
89
- range: requireRange(target.range, label + '.target.range'),
90
- },
91
- };
92
- }
93
- if (kind === 'dsh-source') {
94
- return {
95
- ...common,
96
- target: {
97
- kind,
98
- baseRevision: requireString(target.baseRevision, label + '.target.baseRevision'),
99
- },
100
- };
101
- }
102
- throw new Error('unsupported patch target kind: ' + kind);
103
- }
104
78
  function git(root, args, acceptFailure = false) {
105
79
  const result = spawnSync('git', args, { cwd: root, encoding: 'utf8' });
106
80
  if (result.status === 0)
@@ -148,23 +122,6 @@ function requireSourceBase(root, baseRevision) {
148
122
  }
149
123
  return checkoutRevision;
150
124
  }
151
- function patchPackageVariants(distributionDirectory, names) {
152
- const variants = [];
153
- for (const name of names) {
154
- const patchPackage = resolveInstalledPackage(distributionDirectory, name);
155
- const declaration = requireObject(patchPackage.manifest.dshPatch, name + ' dshPatch');
156
- if (declaration.formatVersion !== FORMAT_VERSION) {
157
- throw new Error('unsupported dshPatch formatVersion in ' + name + ': ' + String(declaration.formatVersion));
158
- }
159
- if (!Array.isArray(declaration.variants) || declaration.variants.length === 0) {
160
- throw new Error(name + ' dshPatch.variants must be a non-empty array');
161
- }
162
- for (const [index, value] of declaration.variants.entries()) {
163
- variants.push(parseVariant(value, index, patchPackage));
164
- }
165
- }
166
- return variants;
167
- }
168
125
  // 先验证official source,再物化Plus声明的profile依赖,避免source不匹配时改动用户profile。
169
126
  function prepareSelection(distributionDirectory, profileDirectory, dshRoot) {
170
127
  const distributionManifest = readJsonObject(resolve(distributionDirectory, 'package.json'), 'Plus distribution manifest');
@@ -192,31 +149,24 @@ function prepareSelection(distributionDirectory, profileDirectory, dshRoot) {
192
149
  throw new Error('dshPlus.profile.allowBuilds.' + name + ' must be a boolean');
193
150
  allowBuilds[name] = allowed;
194
151
  }
195
- const patchPackageNames = requireStringArray(plus.patchPackages, 'dshPlus.patchPackages');
152
+ const patchSet = readPatchSet(distributionDirectory, distributionManifest);
196
153
  const dependencies = requireObject(distributionManifest.dependencies, 'Plus distribution dependencies');
197
154
  const dshManifest = readJsonObject(resolve(dshRoot, 'package.json'), 'DSH source manifest');
198
155
  const dshVersion = requireVersion(dshManifest.version, 'DSH source version');
199
156
  if (!satisfies(dshVersion, dshRange, { includePrerelease: true })) {
200
157
  throw new Error(distributionName + '@' + distributionVersion + ' requires DSH ' + dshRange + ', found ' + dshVersion);
201
158
  }
202
- const checkoutRevision = requireSourceBase(dshRoot, baseRevision);
203
- const groups = new Map();
204
- for (const variant of patchPackageVariants(distributionDirectory, patchPackageNames)) {
205
- const entries = groups.get(variant.id) ?? [];
206
- entries.push(variant);
207
- groups.set(variant.id, entries);
208
- }
159
+ const checkoutRevision = requireSourceBase(dshRoot, patchSet.sourceBaseRevision);
160
+ const patchEntries = [...patchSet.entries, ...patchSet.thirdPartyPatches];
209
161
  const sourceStates = new Map();
210
- const orderedGroups = [...groups].sort(([left], [right]) => left.localeCompare(right));
211
- for (const [, variants] of orderedGroups) {
212
- for (const variant of variants) {
213
- if (variant.target.kind !== 'dsh-source'
214
- || variant.target.baseRevision !== baseRevision
215
- || !satisfies(dshVersion, variant.dsh, { includePrerelease: true }))
216
- continue;
217
- const file = resolvePatchFile(variant.patchPackage.directory, variant.file);
218
- sourceStates.set(file.absoluteFile, sourcePatchState(dshRoot, file.absoluteFile));
162
+ for (const item of patchEntries) {
163
+ if (!satisfies(dshVersion, item.dsh, { includePrerelease: true })) {
164
+ throw new Error('patch ' + item.id + ' declares DSH ' + item.dsh + ', found ' + dshVersion);
219
165
  }
166
+ if (item.target.kind !== 'dsh-source')
167
+ continue;
168
+ const absoluteFile = resolve(distributionDirectory, item.file);
169
+ sourceStates.set(absoluteFile, sourcePatchState(dshRoot, absoluteFile));
220
170
  }
221
171
  const pendingSourceFiles = [...sourceStates].flatMap(([file, state]) => state === 'pending' ? [file] : []);
222
172
  preflightSourcePatches(dshRoot, pendingSourceFiles);
@@ -227,49 +177,38 @@ function prepareSelection(distributionDirectory, profileDirectory, dshRoot) {
227
177
  return { name, version: installed.version };
228
178
  });
229
179
  const runtimePackages = [
230
- ...Object.keys(dependencies).filter(name => !patchPackageNames.includes(name)).sort().map((name) => {
180
+ ...Object.keys(dependencies).sort().map((name) => {
231
181
  const installed = resolveInstalledPackage(distributionDirectory, name);
232
182
  return { name, version: installed.version };
233
183
  }),
234
184
  ...profileDependencies,
235
185
  ].sort((left, right) => left.name.localeCompare(right.name));
236
186
  const selected = [];
237
- // 每个patch id必须在当前DSH、official base与installed npm graph上唯一命中,不能fallback到第二variant。
238
- for (const [id, variants] of orderedGroups) {
239
- const candidates = variants.flatMap((variant) => {
240
- if (!satisfies(dshVersion, variant.dsh, { includePrerelease: true }))
241
- return [];
242
- const file = resolvePatchFile(variant.patchPackage.directory, variant.file);
243
- if (variant.target.kind === 'dsh-source') {
244
- if (variant.target.baseRevision !== baseRevision)
245
- return [];
246
- return [{
247
- id,
248
- patchPackage: { name: variant.patchPackage.name, version: variant.patchPackage.version },
249
- file: file.relativeFile,
250
- absoluteFile: file.absoluteFile,
251
- target: { kind: 'dsh-source', baseRevision },
252
- alreadyApplied: sourceStates.get(file.absoluteFile) === 'applied',
253
- }];
254
- }
255
- const targetDirectory = variant.target.name in profileDependencySpecs ? profileDirectory : distributionDirectory;
256
- const targetVersion = resolveInstalledPackage(targetDirectory, variant.target.name).version;
257
- if (!satisfies(targetVersion, variant.target.range, { includePrerelease: true }))
258
- return [];
259
- return [{
260
- id,
261
- patchPackage: { name: variant.patchPackage.name, version: variant.patchPackage.version },
262
- file: file.relativeFile,
263
- absoluteFile: file.absoluteFile,
264
- target: { kind: 'npm', name: variant.target.name, version: targetVersion },
265
- }];
187
+ for (const item of patchEntries) {
188
+ const absoluteFile = resolve(distributionDirectory, item.file);
189
+ if (item.target.kind === 'dsh-source') {
190
+ selected.push({
191
+ id: item.id,
192
+ scope: item.scope,
193
+ file: item.file,
194
+ absoluteFile,
195
+ target: { kind: 'dsh-source', baseRevision: patchSet.sourceBaseRevision },
196
+ alreadyApplied: sourceStates.get(absoluteFile) === 'applied',
197
+ });
198
+ continue;
199
+ }
200
+ const targetDirectory = item.target.name in profileDependencySpecs ? profileDirectory : distributionDirectory;
201
+ const targetVersion = resolveInstalledPackage(targetDirectory, item.target.name).version;
202
+ if (!satisfies(targetVersion, item.target.range, { includePrerelease: true })) {
203
+ throw new Error('patch ' + item.id + ' targets ' + item.target.name + ' ' + item.target.range + ', found ' + targetVersion);
204
+ }
205
+ selected.push({
206
+ id: item.id,
207
+ scope: item.scope,
208
+ file: item.file,
209
+ absoluteFile,
210
+ target: { kind: 'npm', name: item.target.name, version: targetVersion },
266
211
  });
267
- const [candidate] = candidates;
268
- if (candidate === undefined)
269
- throw new Error('no compatible patch variant for ' + id + ' on DSH ' + dshVersion);
270
- if (candidates.length > 1)
271
- throw new Error('multiple compatible patch variants for ' + id + ' on DSH ' + dshVersion);
272
- selected.push(candidate);
273
212
  }
274
213
  const lockPatches = selected.map((patch) => {
275
214
  if ('alreadyApplied' in patch) {
@@ -299,7 +238,7 @@ function materializePatchFiles(profileDirectory, patches) {
299
238
  const root = resolve(profileDirectory, '.dsh-plus', 'patches');
300
239
  rmSync(root, { recursive: true, force: true });
301
240
  const stablePatches = patches.map((patch) => {
302
- const destination = resolve(root, patch.patchPackage.name, patch.patchPackage.version, patch.file);
241
+ const destination = resolve(root, patch.scope, patch.id, basename(patch.file));
303
242
  mkdirSync(dirname(destination), { recursive: true });
304
243
  copyFileSync(patch.absoluteFile, destination);
305
244
  return { ...patch, absoluteFile: destination };
@@ -323,7 +262,7 @@ function materializePatchFiles(profileDirectory, patches) {
323
262
  continue;
324
263
  }
325
264
  // 成员或版本变化必须改变workspace路径,使pnpm重新应用新的组合payload。
326
- const members = group.map(patch => encodeURIComponent(patch.id + '@' + patch.patchPackage.version)).join('+');
265
+ const members = group.map(patch => encodeURIComponent(patch.id)).join('+');
327
266
  const destination = resolve(root, 'combined', encodeURIComponent(key), members + '.patch');
328
267
  mkdirSync(dirname(destination), { recursive: true });
329
268
  const contents = group.map((patch) => {
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Read the Plus distribution's embedded patch set.
3
+ *
4
+ * The distribution owns one patch set for official DSH and, separately, the patches it
5
+ * carries for third-party packages. Both live inside the distribution package: the
6
+ * manifest declares entries, and each entry's payload is a file under `patches/`. A
7
+ * consumer resolves no independently published patch package.
8
+ *
9
+ * The two lists are governed differently. A Plus entry must target an official package
10
+ * that the profile republishes and overrides; a third-party entry must name its upstream
11
+ * and a retirement condition, and never becomes part of the official patch set.
12
+ */
13
+ /** An npm package the distribution patches in place. */
14
+ export interface NpmPatchTarget {
15
+ readonly kind: 'npm';
16
+ readonly name: string;
17
+ readonly range: string;
18
+ }
19
+ /** One official source path set, applied to the exact `dshPlus.sourceBase` revision. */
20
+ export interface SourcePatchTarget {
21
+ readonly kind: 'dsh-source';
22
+ readonly baseRevision: string;
23
+ /** Official packages the patch modifies; each must be republished and overridden. */
24
+ readonly packages: readonly string[];
25
+ readonly paths: readonly string[];
26
+ }
27
+ /** Where one patch payload lands. */
28
+ export type PatchTarget = NpmPatchTarget | SourcePatchTarget;
29
+ /** Who owns the entry: the Plus official delta or a carried third-party patch. */
30
+ export type PatchScope = 'plus' | 'third-party';
31
+ /** One entry of the embedded patch set. */
32
+ export interface PatchSetEntry {
33
+ /** Stable identity used by the lock and by retirement records. */
34
+ readonly id: string;
35
+ readonly scope: PatchScope;
36
+ /** DSH range the entry applies to. */
37
+ readonly dsh: string;
38
+ /** Payload path, relative to the distribution package. */
39
+ readonly file: string;
40
+ /** The behavior or upstream release that retires this entry. */
41
+ readonly retireWhen: string;
42
+ /** Third-party entries name the upstream they track. */
43
+ readonly upstreamUrl?: string;
44
+ readonly target: PatchTarget;
45
+ }
46
+ /** The distribution's complete embedded patch declaration. */
47
+ export interface PatchSet {
48
+ readonly entries: readonly PatchSetEntry[];
49
+ readonly thirdPartyPatches: readonly PatchSetEntry[];
50
+ readonly sourceBaseRevision: string;
51
+ }
52
+ /**
53
+ * Read and validate the embedded patch set from an installed distribution.
54
+ * @param distributionDirectory - installed `@sparkelf/dsh-plus` directory.
55
+ * @param manifest - its parsed package manifest.
56
+ * @returns the ordered Plus entries, third-party entries, and the exact source base.
57
+ */
58
+ export declare function readPatchSet(distributionDirectory: string, manifest: Record<string, unknown>): PatchSet;
59
+ //# sourceMappingURL=patch-set.d.ts.map