@sparkelf/dsh-plus 0.1.0-rc.33 → 0.1.0-rc.34

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/lib/bin.js CHANGED
@@ -426,6 +426,78 @@ function ensureProfile(paths, consumerDirectory) {
426
426
  writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
427
427
  return true;
428
428
  }
429
+ /** Run git in one directory, returning undefined instead of throwing when asked to. */
430
+ function git(root, args, acceptFailure = false) {
431
+ const result = spawnSync("git", args, {
432
+ cwd: root,
433
+ encoding: "utf8"
434
+ });
435
+ if (result.status === 0) return result.stdout.trim();
436
+ if (acceptFailure) return void 0;
437
+ const detail = result.stderr.trim();
438
+ throw new Error("git " + args.join(" ") + " failed" + (detail === "" ? "" : ": " + detail));
439
+ }
440
+ /**
441
+ * Apply the reviewed npm-target patches to the profile's installed packages.
442
+ *
443
+ * A standalone installation runs no `apply` step: it installs the distribution from
444
+ * the registry, links the consumer's packages, and starts. The npm patches a
445
+ * distribution declares therefore need an owner that does not require an official
446
+ * source checkout — the source half of the apply step needs one, this does not.
447
+ *
448
+ * The work is idempotent: a patch whose reverse already applies is left alone, so a
449
+ * second start neither re-applies nor fails. A reinstall restores the published bytes,
450
+ * which is why this runs on every start rather than once.
451
+ *
452
+ * @param distributionDirectory - the installed `@sparkelf/dsh-plus` directory.
453
+ * @param profileDirectory - the standalone profile directory.
454
+ * @returns the labels of the patches that were applied.
455
+ */
456
+ function applyProfileNpmPatches(distributionDirectory, profileDirectory) {
457
+ const names = requireRecord(requireRecord(JSON.parse(readFileSync(join(distributionDirectory, "package.json"), "utf8")), "Plus distribution manifest").dshPlus, "dshPlus").patchPackages;
458
+ if (!Array.isArray(names)) throw new Error("dshPlus.patchPackages must be an array");
459
+ const applied = [];
460
+ for (const value of names) {
461
+ if (typeof value !== "string" || value === "") throw new Error("dshPlus.patchPackages entries must be non-empty strings");
462
+ const patchPackage = resolveInstalledPackage(distributionDirectory, value);
463
+ const variants = requireRecord(patchPackage.manifest.dshPatch, value + " dshPatch").variants;
464
+ if (!Array.isArray(variants)) throw new Error(value + " dshPatch.variants must be an array");
465
+ for (const entry of variants) {
466
+ const variant = requireRecord(entry, value + " variant");
467
+ const target = requireRecord(variant.target, value + " variant target");
468
+ if (target.kind !== "npm") continue;
469
+ const targetName = String(target.name);
470
+ const patched = resolveInstalledPackage(profileDirectory, targetName);
471
+ const file = resolve(patchPackage.directory, String(variant.file));
472
+ if (git(patched.directory, [
473
+ "apply",
474
+ "--reverse",
475
+ "--check",
476
+ file
477
+ ], true) !== void 0) continue;
478
+ git(patched.directory, ["apply", file]);
479
+ applied.push(value + " -> " + targetName);
480
+ }
481
+ }
482
+ return applied;
483
+ }
484
+ /** Resolve one installed package's manifest from a requiring directory. */
485
+ function resolveInstalledPackage(from, packageName) {
486
+ const requireFrom = createRequire(join(from, "package.json"));
487
+ let manifestPath;
488
+ try {
489
+ manifestPath = requireFrom.resolve(packageName + "/package.json");
490
+ } catch {
491
+ throw new Error(packageName + " is not installed under " + from);
492
+ }
493
+ const manifest = requireRecord(JSON.parse(readFileSync(manifestPath, "utf8")), packageName + " manifest");
494
+ return {
495
+ name: packageName,
496
+ version: String(manifest.version),
497
+ directory: dirname(manifestPath),
498
+ manifest
499
+ };
500
+ }
429
501
  //#endregion
430
502
  //#region lib/types/standalone-cli.js
431
503
  /**
@@ -529,6 +601,7 @@ async function start(argv) {
529
601
  const paths = resolvePaths(anchor);
530
602
  const created = ensureProfile(paths, installationRoot());
531
603
  console.log(created ? "Created the plus profile at " + paths.profileDirectory : "Using the existing plus profile");
604
+ for (const label of applyProfileNpmPatches(paths.distributionDirectory, paths.profileDirectory)) console.log("Applied the reviewed patch " + label);
532
605
  const entry = launcherEntry(anchor);
533
606
  if (options.foreground) return runForeground(entry, options.port, options.host, options.open);
534
607
  const existing = readState(paths.home);
@@ -14,7 +14,7 @@ import { dirname, join } from 'node:path';
14
14
  import { fileURLToPath } from 'node:url';
15
15
  import { newerVersion } from "./registry-versions.js";
16
16
  import { DEFAULT_PORT, STOP_GRACE_MILLISECONDS, choosePort, clearState, isRunning, portAvailable, readState, spawnServer, stateDirectory, waitForAuthenticatedUrl, waitForServer, writeState, } from "./standalone-server.js";
17
- import { STANDALONE_PROFILE, ensureProfile, readDistributionProfile, resolvePaths, } from "./standalone-profile.js";
17
+ import { STANDALONE_PROFILE, applyProfileNpmPatches, ensureProfile, readDistributionProfile, resolvePaths, } from "./standalone-profile.js";
18
18
  /** Milliseconds a start waits for the server to answer before reporting failure. */
19
19
  const READY_TIMEOUT_MILLISECONDS = 90_000;
20
20
  function parseStartOptions(argv) {
@@ -104,6 +104,12 @@ async function start(argv) {
104
104
  console.log(created
105
105
  ? 'Created the ' + STANDALONE_PROFILE + ' profile at ' + paths.profileDirectory
106
106
  : 'Using the existing ' + STANDALONE_PROFILE + ' profile');
107
+ // The profile symlinks the consumer's packages, so a patch lands on the installed
108
+ // copy the launcher loads. A reinstall restores the published bytes, which is why
109
+ // this runs on every start rather than only when the profile was created.
110
+ for (const label of applyProfileNpmPatches(paths.distributionDirectory, paths.profileDirectory)) {
111
+ console.log('Applied the reviewed patch ' + label);
112
+ }
107
113
  const entry = launcherEntry(anchor);
108
114
  if (options.foreground)
109
115
  return runForeground(entry, options.port, options.host, options.open);
@@ -91,4 +91,21 @@ export declare function resolvePaths(anchor: string, env?: NodeJS.ProcessEnv): S
91
91
  * @returns whether this call created the manifest.
92
92
  */
93
93
  export declare function ensureProfile(paths: StandalonePaths, consumerDirectory: string): boolean;
94
+ /**
95
+ * Apply the reviewed npm-target patches to the profile's installed packages.
96
+ *
97
+ * A standalone installation runs no `apply` step: it installs the distribution from
98
+ * the registry, links the consumer's packages, and starts. The npm patches a
99
+ * distribution declares therefore need an owner that does not require an official
100
+ * source checkout — the source half of the apply step needs one, this does not.
101
+ *
102
+ * The work is idempotent: a patch whose reverse already applies is left alone, so a
103
+ * second start neither re-applies nor fails. A reinstall restores the published bytes,
104
+ * which is why this runs on every start rather than once.
105
+ *
106
+ * @param distributionDirectory - the installed `@sparkelf/dsh-plus` directory.
107
+ * @param profileDirectory - the standalone profile directory.
108
+ * @returns the labels of the patches that were applied.
109
+ */
110
+ export declare function applyProfileNpmPatches(distributionDirectory: string, profileDirectory: string): string[];
94
111
  //# sourceMappingURL=standalone-profile.d.ts.map
@@ -8,6 +8,7 @@
8
8
  * the launcher mounts exactly the bundles the profile names and nothing expands a
9
9
  * bundle's own list.
10
10
  */
11
+ import { spawnSync } from 'node:child_process';
11
12
  import { existsSync, mkdirSync, readdirSync, readFileSync, symlinkSync, writeFileSync } from 'node:fs';
12
13
  import { createRequire } from 'node:module';
13
14
  import { homedir } from 'node:os';
@@ -223,4 +224,76 @@ export function ensureProfile(paths, consumerDirectory) {
223
224
  writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
224
225
  return true;
225
226
  }
227
+ /** Run git in one directory, returning undefined instead of throwing when asked to. */
228
+ function git(root, args, acceptFailure = false) {
229
+ const result = spawnSync('git', args, { cwd: root, encoding: 'utf8' });
230
+ if (result.status === 0)
231
+ return result.stdout.trim();
232
+ if (acceptFailure)
233
+ return undefined;
234
+ const detail = result.stderr.trim();
235
+ throw new Error('git ' + args.join(' ') + ' failed' + (detail === '' ? '' : ': ' + detail));
236
+ }
237
+ /**
238
+ * Apply the reviewed npm-target patches to the profile's installed packages.
239
+ *
240
+ * A standalone installation runs no `apply` step: it installs the distribution from
241
+ * the registry, links the consumer's packages, and starts. The npm patches a
242
+ * distribution declares therefore need an owner that does not require an official
243
+ * source checkout — the source half of the apply step needs one, this does not.
244
+ *
245
+ * The work is idempotent: a patch whose reverse already applies is left alone, so a
246
+ * second start neither re-applies nor fails. A reinstall restores the published bytes,
247
+ * which is why this runs on every start rather than once.
248
+ *
249
+ * @param distributionDirectory - the installed `@sparkelf/dsh-plus` directory.
250
+ * @param profileDirectory - the standalone profile directory.
251
+ * @returns the labels of the patches that were applied.
252
+ */
253
+ export function applyProfileNpmPatches(distributionDirectory, profileDirectory) {
254
+ const manifest = requireRecord(JSON.parse(readFileSync(join(distributionDirectory, 'package.json'), 'utf8')), 'Plus distribution manifest');
255
+ const plus = requireRecord(manifest.dshPlus, 'dshPlus');
256
+ const names = plus.patchPackages;
257
+ if (!Array.isArray(names))
258
+ throw new Error('dshPlus.patchPackages must be an array');
259
+ const applied = [];
260
+ for (const value of names) {
261
+ if (typeof value !== 'string' || value === '')
262
+ throw new Error('dshPlus.patchPackages entries must be non-empty strings');
263
+ const patchPackage = resolveInstalledPackage(distributionDirectory, value);
264
+ const declaration = requireRecord(patchPackage.manifest.dshPatch, value + ' dshPatch');
265
+ const variants = declaration.variants;
266
+ if (!Array.isArray(variants))
267
+ throw new Error(value + ' dshPatch.variants must be an array');
268
+ for (const entry of variants) {
269
+ const variant = requireRecord(entry, value + ' variant');
270
+ const target = requireRecord(variant.target, value + ' variant target');
271
+ // Only npm targets reach an installed package; a source target needs the
272
+ // official checkout, which a standalone installation does not have.
273
+ if (target.kind !== 'npm')
274
+ continue;
275
+ const targetName = String(target.name);
276
+ const patched = resolveInstalledPackage(profileDirectory, targetName);
277
+ const file = resolve(patchPackage.directory, String(variant.file));
278
+ if (git(patched.directory, ['apply', '--reverse', '--check', file], true) !== undefined)
279
+ continue;
280
+ git(patched.directory, ['apply', file]);
281
+ applied.push(value + ' -> ' + targetName);
282
+ }
283
+ }
284
+ return applied;
285
+ }
286
+ /** Resolve one installed package's manifest from a requiring directory. */
287
+ function resolveInstalledPackage(from, packageName) {
288
+ const requireFrom = createRequire(join(from, 'package.json'));
289
+ let manifestPath;
290
+ try {
291
+ manifestPath = requireFrom.resolve(packageName + '/package.json');
292
+ }
293
+ catch {
294
+ throw new Error(packageName + ' is not installed under ' + from);
295
+ }
296
+ const manifest = requireRecord(JSON.parse(readFileSync(manifestPath, 'utf8')), packageName + ' manifest');
297
+ return { name: packageName, version: String(manifest.version), directory: dirname(manifestPath), manifest };
298
+ }
226
299
  //# sourceMappingURL=standalone-profile.js.map
package/package.json CHANGED
@@ -140,5 +140,5 @@
140
140
  },
141
141
  "type": "module",
142
142
  "types": "lib/types/index.d.ts",
143
- "version": "0.1.0-rc.33"
143
+ "version": "0.1.0-rc.34"
144
144
  }