@sparkelf/dsh-plus 0.1.0-rc.38 → 0.1.0-rc.40

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
@@ -363,11 +363,40 @@ function installProfilePackages(paths, consumerDirectory) {
363
363
  linkNestedBundles(paths, profileModules);
364
364
  return;
365
365
  }
366
- runPnpm(paths.profileDirectory, ["install", "--no-frozen-lockfile"], "pnpm install in the plus profile");
366
+ installWithRegistryFallback(paths);
367
367
  alignReplacedPackageNames(profileModules);
368
368
  linkNestedBundles(paths, profileModules);
369
369
  }
370
370
  /**
371
+ * Install the profile from the first registry that answers.
372
+ *
373
+ * The install fetches a full dependency closure, and a mainland consumer reaches the
374
+ * mirror far faster than the origin. Trying the preferred registry and falling back once
375
+ * keeps a first start quick without failing when the preferred one is unreachable.
376
+ *
377
+ * @param paths - resolved standalone paths.
378
+ */
379
+ function installWithRegistryFallback(paths) {
380
+ const registries = registryOrder();
381
+ for (const [index, registry] of registries.entries()) {
382
+ const result = spawnSync("pnpm", [
383
+ "install",
384
+ "--no-frozen-lockfile",
385
+ "--registry",
386
+ registry
387
+ ], {
388
+ cwd: paths.profileDirectory,
389
+ stdio: "inherit",
390
+ shell: process.platform === "win32"
391
+ });
392
+ if (result.error !== void 0) throw result.error;
393
+ if (result.status === 0) return;
394
+ const next = registries[index + 1];
395
+ if (next === void 0) throw new Error("pnpm install in the plus profile failed with exit code " + String(result.status));
396
+ console.log("Install from " + registry + " failed; trying " + next + ".");
397
+ }
398
+ }
399
+ /**
371
400
  * Make each replaced package declare the name of the location it occupies.
372
401
  *
373
402
  * \`overrides\` installs our build at the official path, but the manifest inside still
@@ -401,25 +430,63 @@ function alignReplacedPackageNames(profileModules) {
401
430
  renameSync(temporary, manifestPath);
402
431
  }
403
432
  }
433
+ /** The npm registry the distribution installs from by default. */
434
+ const OFFICIAL_REGISTRY = "https://registry.npmjs.org";
435
+ /** The mainland mirror, which serves the same packages and answers faster there. */
436
+ const MAINLAND_REGISTRY = "https://registry.npmmirror.com";
404
437
  /**
405
- * Run pnpm in one directory, inheriting its output.
438
+ * The registries to try, in order, for this machine.
406
439
  *
407
- * Windows resolves a command through its shell: \`spawnSync\` on a \`.cmd\` shim fails
408
- * with EINVAL, so the call goes through \`cmd.exe\` there. This matches the runner in
409
- * \`apply.ts\`, which the apply path has used on Windows since it shipped.
440
+ * A mainland locale reaches the mirror faster than the origin, which is why the Desktop
441
+ * installer already prefers it there. The same choice belongs to the profile install: it
442
+ * fetches a full dependency closure, so the delay is the first thing a consumer notices.
443
+ * `DSH_PLUS_INSTALL_REGISTRY` overrides the choice, and a failure falls back once.
410
444
  *
411
- * @param cwd - the directory pnpm runs in.
412
- * @param args - pnpm arguments, without the executable.
413
- * @param label - the failing step's name, for the error.
445
+ * @returns registries to try in order.
414
446
  */
415
- function runPnpm(cwd, args, label) {
416
- const result = spawnSync("pnpm", [...args], {
417
- cwd,
418
- stdio: "inherit",
419
- shell: process.platform === "win32"
420
- });
421
- if (result.error !== void 0) throw result.error;
422
- if (result.status !== 0) throw new Error(label + " failed with exit code " + String(result.status));
447
+ function registryOrder() {
448
+ const configured = process.env.DSH_PLUS_INSTALL_REGISTRY;
449
+ if (configured !== void 0 && configured !== "") return [configured, MAINLAND_REGISTRY];
450
+ const locale = (process.env.LANG ?? process.env.LC_ALL ?? "").toLowerCase();
451
+ return locale.includes("zh") || locale.includes("cn") ? [MAINLAND_REGISTRY, OFFICIAL_REGISTRY] : [OFFICIAL_REGISTRY, MAINLAND_REGISTRY];
452
+ }
453
+ /**
454
+ * Report whether pnpm can run.
455
+ *
456
+ * The profile installs its own tree so its \`overrides\` apply, and only pnpm reads those
457
+ * from a workspace, so pnpm is a prerequisite the distribution cannot supply. Probing
458
+ * first turns a cryptic failure from the install into a message naming what is missing.
459
+ *
460
+ * @returns \`true\` when pnpm answers with a version.
461
+ */
462
+ function pnpmAvailable() {
463
+ return spawnSync("pnpm", ["--version"], {
464
+ stdio: "pipe",
465
+ shell: process.platform === "win32",
466
+ encoding: "utf8"
467
+ }).status === 0;
468
+ }
469
+ /**
470
+ * The commands that install pnpm, in the order worth trying.
471
+ *
472
+ * Corepack ships with Node and needs no download, so it comes first; npm is the fallback
473
+ * for an installation whose Corepack is absent or disabled. Both are the consumer's own
474
+ * toolchain, which is what lets the first start offer to install rather than only report.
475
+ *
476
+ * @returns commands to try in order, stopping at the first that works.
477
+ */
478
+ function pnpmInstallCommands() {
479
+ const registry = registryOrder()[0];
480
+ if (registry === void 0) return ["corepack enable pnpm", "npm install -g pnpm"];
481
+ return ["corepack enable pnpm", "npm install -g pnpm --registry " + registry];
482
+ }
483
+ /**
484
+ * The command to show a consumer who declines the automatic install.
485
+ *
486
+ * @returns the preferred command, which is the one the offer runs first.
487
+ */
488
+ function pnpmInstallCommand() {
489
+ return pnpmInstallCommands()[0] ?? "npm install -g pnpm";
423
490
  }
424
491
  /**
425
492
  * Expose the distribution's nested packages at the top level the profile searches.
@@ -698,6 +765,35 @@ async function start(argv) {
698
765
  const options = parseStartOptions(argv);
699
766
  const anchor = installationAnchor();
700
767
  const paths = resolvePaths(anchor);
768
+ if (!pnpmAvailable()) {
769
+ const command = pnpmInstallCommand();
770
+ console.log("pnpm is required to install the plus profile, and was not found.");
771
+ console.log("Install it with: " + command);
772
+ if (!await confirm("Install pnpm now?")) {
773
+ console.log("Install pnpm and run dsh-plus start again.");
774
+ return 1;
775
+ }
776
+ let installed = false;
777
+ const preferred = registryOrder()[0];
778
+ for (const candidate of pnpmInstallCommands()) if (spawnSync(candidate, {
779
+ stdio: "inherit",
780
+ shell: true,
781
+ env: preferred === void 0 ? process.env : {
782
+ ...process.env,
783
+ COREPACK_NPM_REGISTRY: preferred
784
+ }
785
+ }).status === 0 && pnpmAvailable()) {
786
+ installed = true;
787
+ break;
788
+ }
789
+ if (!installed) {
790
+ console.log("Could not install pnpm automatically.");
791
+ console.log("Run one of these, then run dsh-plus start again:");
792
+ for (const candidate of pnpmInstallCommands()) console.log(" " + candidate);
793
+ return 1;
794
+ }
795
+ console.log("pnpm installed.");
796
+ }
701
797
  const created = ensureProfile(paths, installationRoot());
702
798
  console.log(created ? "Created the plus profile at " + paths.profileDirectory : "Using the existing plus profile");
703
799
  for (const label of applyProfileNpmPatches(paths.distributionDirectory, paths.profileDirectory)) console.log("Applied the reviewed patch " + label);
@@ -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, applyProfileNpmPatches, ensureProfile, readDistributionProfile, resolvePaths, } from "./standalone-profile.js";
17
+ import { STANDALONE_PROFILE, applyProfileNpmPatches, ensureProfile, pnpmAvailable, pnpmInstallCommand, pnpmInstallCommands, registryOrder, 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) {
@@ -100,6 +100,45 @@ async function start(argv) {
100
100
  const options = parseStartOptions(argv);
101
101
  const anchor = installationAnchor();
102
102
  const paths = resolvePaths(anchor);
103
+ // The profile installs its own dependency tree, which needs pnpm. Asking here rather
104
+ // than failing inside the install turns a missing prerequisite into a decision the
105
+ // consumer makes, and the install it can run is the one command that provides it.
106
+ if (!pnpmAvailable()) {
107
+ const command = pnpmInstallCommand();
108
+ console.log('pnpm is required to install the ' + STANDALONE_PROFILE + ' profile, and was not found.');
109
+ console.log('Install it with: ' + command);
110
+ if (!await confirm('Install pnpm now?')) {
111
+ console.log('Install pnpm and run dsh-plus start again.');
112
+ return 1;
113
+ }
114
+ // Try each installer in turn: Corepack is absent on an installation that disabled it,
115
+ // and npm succeeds there. A failure reports the commands rather than a stack trace.
116
+ let installed = false;
117
+ const preferred = registryOrder()[0];
118
+ for (const candidate of pnpmInstallCommands()) {
119
+ // Corepack reads its registry from the environment; npm takes a flag, which the
120
+ // command already carries. Setting it for both keeps the download on the mirror.
121
+ const attempt = spawnSync(candidate, {
122
+ stdio: 'inherit',
123
+ shell: true,
124
+ env: preferred === undefined
125
+ ? process.env
126
+ : { ...process.env, COREPACK_NPM_REGISTRY: preferred },
127
+ });
128
+ if (attempt.status === 0 && pnpmAvailable()) {
129
+ installed = true;
130
+ break;
131
+ }
132
+ }
133
+ if (!installed) {
134
+ console.log('Could not install pnpm automatically.');
135
+ console.log('Run one of these, then run dsh-plus start again:');
136
+ for (const candidate of pnpmInstallCommands())
137
+ console.log(' ' + candidate);
138
+ return 1;
139
+ }
140
+ console.log('pnpm installed.');
141
+ }
103
142
  const created = ensureProfile(paths, installationRoot());
104
143
  console.log(created
105
144
  ? 'Created the ' + STANDALONE_PROFILE + ' profile at ' + paths.profileDirectory
@@ -94,6 +94,43 @@ export declare function installProfilePackages(paths: StandalonePaths, consumerD
94
94
  * @param profileModules - the profile's \`node_modules\` directory.
95
95
  */
96
96
  export declare function alignReplacedPackageNames(profileModules: string): void;
97
+ /**
98
+ * The registries to try, in order, for this machine.
99
+ *
100
+ * A mainland locale reaches the mirror faster than the origin, which is why the Desktop
101
+ * installer already prefers it there. The same choice belongs to the profile install: it
102
+ * fetches a full dependency closure, so the delay is the first thing a consumer notices.
103
+ * `DSH_PLUS_INSTALL_REGISTRY` overrides the choice, and a failure falls back once.
104
+ *
105
+ * @returns registries to try in order.
106
+ */
107
+ export declare function registryOrder(): readonly string[];
108
+ /**
109
+ * Report whether pnpm can run.
110
+ *
111
+ * The profile installs its own tree so its \`overrides\` apply, and only pnpm reads those
112
+ * from a workspace, so pnpm is a prerequisite the distribution cannot supply. Probing
113
+ * first turns a cryptic failure from the install into a message naming what is missing.
114
+ *
115
+ * @returns \`true\` when pnpm answers with a version.
116
+ */
117
+ export declare function pnpmAvailable(): boolean;
118
+ /**
119
+ * The commands that install pnpm, in the order worth trying.
120
+ *
121
+ * Corepack ships with Node and needs no download, so it comes first; npm is the fallback
122
+ * for an installation whose Corepack is absent or disabled. Both are the consumer's own
123
+ * toolchain, which is what lets the first start offer to install rather than only report.
124
+ *
125
+ * @returns commands to try in order, stopping at the first that works.
126
+ */
127
+ export declare function pnpmInstallCommands(): readonly string[];
128
+ /**
129
+ * The command to show a consumer who declines the automatic install.
130
+ *
131
+ * @returns the preferred command, which is the one the offer runs first.
132
+ */
133
+ export declare function pnpmInstallCommand(): string;
97
134
  /** Resolve every path a command needs, without creating anything. */
98
135
  export declare function resolvePaths(anchor: string, env?: NodeJS.ProcessEnv): StandalonePaths;
99
136
  /**
@@ -159,10 +159,38 @@ export function installProfilePackages(paths, consumerDirectory) {
159
159
  linkNestedBundles(paths, profileModules);
160
160
  return;
161
161
  }
162
- runPnpm(paths.profileDirectory, ['install', '--no-frozen-lockfile'], 'pnpm install in the plus profile');
162
+ installWithRegistryFallback(paths);
163
163
  alignReplacedPackageNames(profileModules);
164
164
  linkNestedBundles(paths, profileModules);
165
165
  }
166
+ /**
167
+ * Install the profile from the first registry that answers.
168
+ *
169
+ * The install fetches a full dependency closure, and a mainland consumer reaches the
170
+ * mirror far faster than the origin. Trying the preferred registry and falling back once
171
+ * keeps a first start quick without failing when the preferred one is unreachable.
172
+ *
173
+ * @param paths - resolved standalone paths.
174
+ */
175
+ function installWithRegistryFallback(paths) {
176
+ const registries = registryOrder();
177
+ for (const [index, registry] of registries.entries()) {
178
+ const result = spawnSync('pnpm', ['install', '--no-frozen-lockfile', '--registry', registry], {
179
+ cwd: paths.profileDirectory,
180
+ stdio: 'inherit',
181
+ shell: process.platform === 'win32',
182
+ });
183
+ if (result.error !== undefined)
184
+ throw result.error;
185
+ if (result.status === 0)
186
+ return;
187
+ const next = registries[index + 1];
188
+ if (next === undefined) {
189
+ throw new Error('pnpm install in the plus profile failed with exit code ' + String(result.status));
190
+ }
191
+ console.log('Install from ' + registry + ' failed; trying ' + next + '.');
192
+ }
193
+ }
166
194
  /**
167
195
  * Make each replaced package declare the name of the location it occupies.
168
196
  *
@@ -197,27 +225,70 @@ export function alignReplacedPackageNames(profileModules) {
197
225
  renameSync(temporary, manifestPath);
198
226
  }
199
227
  }
228
+ /** The npm registry the distribution installs from by default. */
229
+ const OFFICIAL_REGISTRY = 'https://registry.npmjs.org';
230
+ /** The mainland mirror, which serves the same packages and answers faster there. */
231
+ const MAINLAND_REGISTRY = 'https://registry.npmmirror.com';
232
+ /**
233
+ * The registries to try, in order, for this machine.
234
+ *
235
+ * A mainland locale reaches the mirror faster than the origin, which is why the Desktop
236
+ * installer already prefers it there. The same choice belongs to the profile install: it
237
+ * fetches a full dependency closure, so the delay is the first thing a consumer notices.
238
+ * `DSH_PLUS_INSTALL_REGISTRY` overrides the choice, and a failure falls back once.
239
+ *
240
+ * @returns registries to try in order.
241
+ */
242
+ export function registryOrder() {
243
+ const configured = process.env.DSH_PLUS_INSTALL_REGISTRY;
244
+ if (configured !== undefined && configured !== '')
245
+ return [configured, MAINLAND_REGISTRY];
246
+ const locale = (process.env.LANG ?? process.env.LC_ALL ?? '').toLowerCase();
247
+ const mainlandFirst = locale.includes('zh') || locale.includes('cn');
248
+ return mainlandFirst ? [MAINLAND_REGISTRY, OFFICIAL_REGISTRY] : [OFFICIAL_REGISTRY, MAINLAND_REGISTRY];
249
+ }
200
250
  /**
201
- * Run pnpm in one directory, inheriting its output.
251
+ * Report whether pnpm can run.
202
252
  *
203
- * Windows resolves a command through its shell: \`spawnSync\` on a \`.cmd\` shim fails
204
- * with EINVAL, so the call goes through \`cmd.exe\` there. This matches the runner in
205
- * \`apply.ts\`, which the apply path has used on Windows since it shipped.
253
+ * The profile installs its own tree so its \`overrides\` apply, and only pnpm reads those
254
+ * from a workspace, so pnpm is a prerequisite the distribution cannot supply. Probing
255
+ * first turns a cryptic failure from the install into a message naming what is missing.
206
256
  *
207
- * @param cwd - the directory pnpm runs in.
208
- * @param args - pnpm arguments, without the executable.
209
- * @param label - the failing step's name, for the error.
257
+ * @returns \`true\` when pnpm answers with a version.
210
258
  */
211
- function runPnpm(cwd, args, label) {
212
- const result = spawnSync('pnpm', [...args], {
213
- cwd,
214
- stdio: 'inherit',
259
+ export function pnpmAvailable() {
260
+ const probe = spawnSync('pnpm', ['--version'], {
261
+ stdio: 'pipe',
215
262
  shell: process.platform === 'win32',
263
+ encoding: 'utf8',
216
264
  });
217
- if (result.error !== undefined)
218
- throw result.error;
219
- if (result.status !== 0)
220
- throw new Error(label + ' failed with exit code ' + String(result.status));
265
+ return probe.status === 0;
266
+ }
267
+ /**
268
+ * The commands that install pnpm, in the order worth trying.
269
+ *
270
+ * Corepack ships with Node and needs no download, so it comes first; npm is the fallback
271
+ * for an installation whose Corepack is absent or disabled. Both are the consumer's own
272
+ * toolchain, which is what lets the first start offer to install rather than only report.
273
+ *
274
+ * @returns commands to try in order, stopping at the first that works.
275
+ */
276
+ export function pnpmInstallCommands() {
277
+ const registry = registryOrder()[0];
278
+ if (registry === undefined)
279
+ return ['corepack enable pnpm', 'npm install -g pnpm'];
280
+ // Both installers take the registry explicitly. npm does so with a flag; corepack reads
281
+ // COREPACK_NPM_REGISTRY, which the caller sets. A mainland consumer therefore downloads
282
+ // the package from the mirror, for the same reason the profile install prefers it.
283
+ return ['corepack enable pnpm', 'npm install -g pnpm --registry ' + registry];
284
+ }
285
+ /**
286
+ * The command to show a consumer who declines the automatic install.
287
+ *
288
+ * @returns the preferred command, which is the one the offer runs first.
289
+ */
290
+ export function pnpmInstallCommand() {
291
+ return pnpmInstallCommands()[0] ?? 'npm install -g pnpm';
221
292
  }
222
293
  /**
223
294
  * Expose the distribution's nested packages at the top level the profile searches.
package/package.json CHANGED
@@ -166,5 +166,5 @@
166
166
  },
167
167
  "type": "module",
168
168
  "types": "lib/types/index.d.ts",
169
- "version": "0.1.0-rc.38"
169
+ "version": "0.1.0-rc.40"
170
170
  }