@linxin666/dsh-client-ui-skin-center 0.1.16 → 0.1.17
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/index.js +156 -39
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -207,12 +207,66 @@ function wiredNames(registry) {
|
|
|
207
207
|
}
|
|
208
208
|
/**
|
|
209
209
|
* Drop legacy hand-written skin rows (insert rows with a name) and old touch
|
|
210
|
-
* comments.
|
|
211
|
-
*
|
|
210
|
+
* comments. Historical writers emitted a comment line above the row with
|
|
211
|
+
* either npm scope, but the row must go regardless of the comment line,
|
|
212
|
+
* indentation or scope — any leftover insert row for a ui-skin-* id plus the
|
|
213
|
+
* managed section's own row produces two insert rows for one loader id, and
|
|
214
|
+
* the boot fails with "duplicate loader entry id" (issue #267). Id-target
|
|
215
|
+
* rows (`- id: ui-skin-xp` + `disabled: true`) carry no `name:` line and
|
|
216
|
+
* must survive: they are the mutual-exclusion wiring, not inserts.
|
|
212
217
|
* @param patch - raw patch file text.
|
|
213
218
|
*/
|
|
214
219
|
function stripLegacySkinRows(patch) {
|
|
215
|
-
|
|
220
|
+
const lines = patch.split(/\r?\n/);
|
|
221
|
+
const kept = [];
|
|
222
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
223
|
+
const line = lines[i];
|
|
224
|
+
if (/^\s*- id:\s*(ui-skin-[a-z0-9-]+)\s*$/.exec(line) !== null) {
|
|
225
|
+
const next = lines[i + 1];
|
|
226
|
+
if ((next === void 0 ? null : /^\s*name:\s*['"]?@[a-z0-9][a-z0-9._-]*\/dsh-client-ui-skin-[^'"]*['"]?\s*$/.exec(next)) !== null) {
|
|
227
|
+
if (i > 0 && /^\s*#[^\n]*$/.test(lines[i - 1]) && kept[kept.length - 1] === lines[i - 1]) kept.pop();
|
|
228
|
+
i += 1;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
kept.push(line);
|
|
233
|
+
}
|
|
234
|
+
let text = kept.join("\n").replace(/^# \(touch\)[^\n]*\n?/gm, "");
|
|
235
|
+
text = dropEmptyInserts(text);
|
|
236
|
+
return text.replace(/\n{3,}/g, "\n\n");
|
|
237
|
+
}
|
|
238
|
+
/** Remove `- insert:` items left with no `- id:` rows after legacy cleanup,
|
|
239
|
+
* so an emptied block cannot perturb the loader or later renders. Blocks that
|
|
240
|
+
* still carry rows (any plugin id, skin or not) are kept byte-for-byte. */
|
|
241
|
+
function dropEmptyInserts(text) {
|
|
242
|
+
const lines = text.split("\n");
|
|
243
|
+
const out = [];
|
|
244
|
+
let i = 0;
|
|
245
|
+
while (i < lines.length) {
|
|
246
|
+
const line = lines[i];
|
|
247
|
+
const trimmed = line.trim();
|
|
248
|
+
if (/^-\s*insert:\s*$/.exec(trimmed) === null) {
|
|
249
|
+
out.push(line);
|
|
250
|
+
i += 1;
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
const indent = line.length - trimmed.length;
|
|
254
|
+
let j = i + 1;
|
|
255
|
+
let hasRow = false;
|
|
256
|
+
while (j < lines.length) {
|
|
257
|
+
const t = lines[j].trim();
|
|
258
|
+
if (t === "") {
|
|
259
|
+
j += 1;
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (lines[j].length - t.length <= indent) break;
|
|
263
|
+
if (!t.startsWith("#") && /^- id:/.test(t)) hasRow = true;
|
|
264
|
+
j += 1;
|
|
265
|
+
}
|
|
266
|
+
if (hasRow) for (let k = i; k < j; k += 1) out.push(lines[k]);
|
|
267
|
+
i = j;
|
|
268
|
+
}
|
|
269
|
+
return out.join("\n");
|
|
216
270
|
}
|
|
217
271
|
/**
|
|
218
272
|
* Remove the managed skin section. Throws on an unterminated section (a
|
|
@@ -269,14 +323,18 @@ function currentActive(patch, registry = loadRegistry()) {
|
|
|
269
323
|
return enabled.length ? enabled[enabled.length - 1].replace("ui-skin-", "") : null;
|
|
270
324
|
}
|
|
271
325
|
/**
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
* `disabled: true` id-target
|
|
275
|
-
* a YAML parser dependency is not worth the weight for this one
|
|
326
|
+
* Count the `insert:` list rows for a loader entry id in a patch text (the
|
|
327
|
+
* rows a skin bundle would contribute, as opposed to home-layer
|
|
328
|
+
* `disabled: true` id-target rows). The patch format is small and
|
|
329
|
+
* line-based; a YAML parser dependency is not worth the weight for this one
|
|
330
|
+
* probe. Two insert rows for one id fail the boot with "duplicate loader
|
|
331
|
+
* entry id" (issue #267), so the count is what the self-heal in useSkin
|
|
332
|
+
* keys on.
|
|
276
333
|
* @param patch - raw patch text.
|
|
277
334
|
* @param id - the loader entry id to look for.
|
|
278
335
|
*/
|
|
279
|
-
function
|
|
336
|
+
function countInsertId(patch, id) {
|
|
337
|
+
let count = 0;
|
|
280
338
|
let insertIndent = null;
|
|
281
339
|
for (const line of patch.split(/\r?\n/)) {
|
|
282
340
|
const trimmed = line.trimStart();
|
|
@@ -293,9 +351,13 @@ function patchHasInsertId(patch, id) {
|
|
|
293
351
|
continue;
|
|
294
352
|
}
|
|
295
353
|
const row = /^- id:\s*['"]?([^'"]+)['"]?\s*$/.exec(trimmed);
|
|
296
|
-
if (row !== null && row[1] === id)
|
|
354
|
+
if (row !== null && row[1] === id) count += 1;
|
|
297
355
|
}
|
|
298
|
-
return
|
|
356
|
+
return count;
|
|
357
|
+
}
|
|
358
|
+
/** Whether a patch contains at least one insert row for `id` (see countInsertId). */
|
|
359
|
+
function patchHasInsertId(patch, id) {
|
|
360
|
+
return countInsertId(patch, id) > 0;
|
|
299
361
|
}
|
|
300
362
|
/**
|
|
301
363
|
* Bundle entries from the active profile manifest's `dsh.profile.bundles` —
|
|
@@ -428,6 +490,51 @@ function registryWithProfileWiring(registry, profileModulesDir, profileManifestP
|
|
|
428
490
|
return out;
|
|
429
491
|
}
|
|
430
492
|
/**
|
|
493
|
+
* Derive the running harness home + profile from the skin-center package's
|
|
494
|
+
* own install location — the one authority that is true regardless of how
|
|
495
|
+
* the GUI was launched (issue #254: no DSH_PROFILE env var, cwd outside
|
|
496
|
+
* profiles/<name>, so every legacy fallback ends on the wrong profile).
|
|
497
|
+
* Both the literal module path and its realpath are scanned, because profile
|
|
498
|
+
* node_modules entries are commonly symlinks (per-skin links, pnpm store):
|
|
499
|
+
* the literal chain preserves the profiles/<name>/node_modules segment while
|
|
500
|
+
* the realpath chain covers store-resolved loads. The first ancestor matching
|
|
501
|
+
* <harnessHome>/profiles/<name>/node_modules wins; the inner node_modules
|
|
502
|
+
* under .pnpm/<pkg> never matches because its grandparent is .pnpm, not
|
|
503
|
+
* profiles.
|
|
504
|
+
* @param fromUrl - the module URL to resolve from (defaults to this module's
|
|
505
|
+
* own import.meta.url); injectable so tests can place the module inside a
|
|
506
|
+
* simulated install layout.
|
|
507
|
+
* @returns the harness home (already the .dsh dir — no suffix is appended)
|
|
508
|
+
* and the profile name, or null when the module is not installed under a
|
|
509
|
+
* profiles tree (monorepo dev checkout — callers keep their legacy
|
|
510
|
+
* fallbacks).
|
|
511
|
+
*/
|
|
512
|
+
function resolveInstallLayout(fromUrl = import.meta.url) {
|
|
513
|
+
const starts = [fileURLToPath(fromUrl)];
|
|
514
|
+
try {
|
|
515
|
+
const real = realpathSync(starts[0]);
|
|
516
|
+
if (real !== starts[0]) starts.push(real);
|
|
517
|
+
} catch {}
|
|
518
|
+
for (const start of starts) {
|
|
519
|
+
let current = dirname(start);
|
|
520
|
+
for (;;) {
|
|
521
|
+
if (basename(current) === "node_modules") {
|
|
522
|
+
const profileDir = dirname(current);
|
|
523
|
+
const profilesDir = dirname(profileDir);
|
|
524
|
+
const profile = basename(profileDir);
|
|
525
|
+
if (basename(profilesDir) === "profiles" && profile !== "" && profile !== "." && profile !== ".." && profile !== "node_modules") return {
|
|
526
|
+
harnessHome: dirname(profilesDir),
|
|
527
|
+
profile
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
const parent = dirname(current);
|
|
531
|
+
if (parent === current) break;
|
|
532
|
+
current = parent;
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return null;
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
431
538
|
* First non-blank string in a list of candidate values. Whitespace-only
|
|
432
539
|
* values (including environment variables set to spaces) count as unset.
|
|
433
540
|
*/
|
|
@@ -444,36 +551,25 @@ function firstNonBlank(...values) {
|
|
|
444
551
|
* - otherwise a trimmed non-empty `$DSH_HOME` is the harness home directly
|
|
445
552
|
* (dsh's `resolveDshHome()` contract — the env var already points at the
|
|
446
553
|
* `.dsh` directory, so no suffix is appended);
|
|
554
|
+
* - otherwise the harness home derived from this package's install layout
|
|
555
|
+
* (issue #254: the launcher may have configured the home without any env
|
|
556
|
+
* var reaching this process) — already the `.dsh` dir, no suffix;
|
|
447
557
|
* - otherwise `homedir()/.dsh`.
|
|
448
558
|
* @param optsHome - injectable HOME (tests); default resolves from env/homedir.
|
|
449
559
|
* @param env - environment map (defaults to process.env).
|
|
560
|
+
* @param installHome - harness home from resolveInstallLayout (no suffix).
|
|
450
561
|
*/
|
|
451
|
-
function resolveHarnessHome(optsHome, env = process.env) {
|
|
562
|
+
function resolveHarnessHome(optsHome, env = process.env, installHome) {
|
|
452
563
|
if (optsHome !== void 0) return join(optsHome, ".dsh");
|
|
453
|
-
return firstNonBlank(env.DSH_HOME) ?? join(homedir(), ".dsh");
|
|
564
|
+
return firstNonBlank(env.DSH_HOME, installHome) ?? join(homedir(), ".dsh");
|
|
454
565
|
}
|
|
455
566
|
/**
|
|
456
|
-
*
|
|
457
|
-
*
|
|
458
|
-
*
|
|
459
|
-
* 2. `$DSH_SKIN_PROFILE`;
|
|
460
|
-
* 3. `$DSH_PROFILE` (the generic dsh profile override);
|
|
461
|
-
* 4. `process.cwd()` when it is a directory directly under
|
|
462
|
-
* `<harnessHome>/profiles/<name>` — return that `<name>`;
|
|
463
|
-
* 5. `web`.
|
|
464
|
-
* Pure and injectable so tests can exercise every precedence level without
|
|
465
|
-
* mutating the process. `useSkin`/`currentSkin` call it with the same
|
|
466
|
-
* harness-home-derived profiles root the path resolver uses.
|
|
467
|
-
* @param optsProfile - explicit profile override.
|
|
468
|
-
* @param env - environment map (defaults to process.env).
|
|
469
|
-
* @param cwd - current working directory (defaults to process.cwd()).
|
|
470
|
-
* @param profilesRoot - `<harnessHome>/profiles` dir (defaults to the root
|
|
471
|
-
* derived from env/homedir).
|
|
567
|
+
* The profile name when cwd sits directly under `<harnessHome>/profiles/<name>`
|
|
568
|
+
* — else undefined. Pure so resolvePaths can reuse it with an install-derived
|
|
569
|
+
* profiles root while resolveProfile keeps its own signature for callers.
|
|
472
570
|
*/
|
|
473
|
-
function
|
|
474
|
-
const
|
|
475
|
-
if (explicit !== void 0) return explicit;
|
|
476
|
-
const root = resolve(profilesRoot ?? join(resolveHarnessHome(void 0, env), "profiles"));
|
|
571
|
+
function profileFromCwd(cwd, profilesRoot) {
|
|
572
|
+
const root = resolve(profilesRoot);
|
|
477
573
|
const normalizedCwd = resolve(cwd);
|
|
478
574
|
const canonicalDir = (p) => {
|
|
479
575
|
try {
|
|
@@ -488,17 +584,25 @@ function resolveProfile(optsProfile, env = process.env, cwd = process.cwd(), pro
|
|
|
488
584
|
if (name !== "" && statSync(normalizedCwd, { throwIfNoEntry: false })?.isDirectory() === true) return name;
|
|
489
585
|
} catch {}
|
|
490
586
|
}
|
|
491
|
-
return "web";
|
|
492
587
|
}
|
|
493
588
|
/**
|
|
494
589
|
* Resolve the DSH paths under a HOME. home/profile are injectable so tests
|
|
495
590
|
* can point at a throwaway HOME (mirrors scripts/dsh-skin.test.mjs).
|
|
591
|
+
* Precedence for the harness home: injected home > $DSH_HOME > install
|
|
592
|
+
* layout > homedir()/.dsh. For the profile: injected profile >
|
|
593
|
+
* $DSH_SKIN_PROFILE > $DSH_PROFILE > cwd under profiles/<name> > install
|
|
594
|
+
* layout profile > web (issue #254: the install layout is what makes a
|
|
595
|
+
* non-web profile resolve when no env var or cwd hint exists).
|
|
496
596
|
* @param home - home dir (defaults to $DSH_HOME or the process HOME).
|
|
497
|
-
* @param profile - profile name (defaults via
|
|
597
|
+
* @param profile - profile name (defaults via the precedence above).
|
|
598
|
+
* @param fromUrl - module URL the install layout is derived from (defaults
|
|
599
|
+
* to this module's import.meta.url); injectable for tests.
|
|
498
600
|
*/
|
|
499
|
-
function resolvePaths(home, profile) {
|
|
500
|
-
const
|
|
501
|
-
const
|
|
601
|
+
function resolvePaths(home, profile, fromUrl = import.meta.url) {
|
|
602
|
+
const install = resolveInstallLayout(fromUrl);
|
|
603
|
+
const harnessHome = resolveHarnessHome(home, process.env, install?.harnessHome);
|
|
604
|
+
const profilesRoot = join(harnessHome, "profiles");
|
|
605
|
+
const activeProfile = firstNonBlank(profile, process.env.DSH_SKIN_PROFILE, process.env.DSH_PROFILE) ?? profileFromCwd(process.cwd(), profilesRoot) ?? install?.profile ?? "web";
|
|
502
606
|
return {
|
|
503
607
|
patchPath: join(harnessHome, "cordis.patch.yml"),
|
|
504
608
|
profileModulesDir: join(harnessHome, "profiles", activeProfile, "node_modules"),
|
|
@@ -706,9 +810,22 @@ function useSkin(name, opts = {}) {
|
|
|
706
810
|
if (problem !== null) throw new Error(problem);
|
|
707
811
|
renderRegistry = registryWithProfileWiring(registry, paths.profileModulesDir, paths.profileManifestPath);
|
|
708
812
|
}
|
|
709
|
-
const
|
|
813
|
+
const patch = stripLegacySkinRows(stripManaged(readPatch(paths.patchPath)));
|
|
814
|
+
let next = `${patch.replace(/\s+$/, "")}\n\n${renderManaged(official ? null : name, renderRegistry)}\n`;
|
|
815
|
+
let skippedInsert = false;
|
|
816
|
+
if (!official && countInsertId(next, renderRegistry[name].id) > 1) {
|
|
817
|
+
const wired = {
|
|
818
|
+
...renderRegistry,
|
|
819
|
+
[name]: {
|
|
820
|
+
...renderRegistry[name],
|
|
821
|
+
bundleWired: true
|
|
822
|
+
}
|
|
823
|
+
};
|
|
824
|
+
next = `${patch.replace(/\s+$/, "")}\n\n${renderManaged(name, wired)}\n`;
|
|
825
|
+
skippedInsert = true;
|
|
826
|
+
}
|
|
710
827
|
writePatchAtomic(paths.patchPath, next);
|
|
711
|
-
return official ? "restored the official stock look — the config watcher applies it within seconds; refresh the page to see it." : `skin switched to "${name}" — the config watcher applies it within seconds; refresh the page (or the manifest re-fetches) to see it
|
|
828
|
+
return (official ? "restored the official stock look — the config watcher applies it within seconds; refresh the page to see it." : `skin switched to "${name}" — the config watcher applies it within seconds; refresh the page (or the manifest re-fetches) to see it.`) + (skippedInsert ? " (检测到补丁中已有该皮肤的 insert 行,已跳过本层 insert,避免 duplicate loader entry id。)" : "");
|
|
712
829
|
}
|
|
713
830
|
/**
|
|
714
831
|
* Read the active skin, mirroring `dsh-skin current` (prints the name or
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linxin666/dsh-client-ui-skin-center",
|
|
3
3
|
"description": "In-GUI skin center for the dsh web GUI: lists the official default plus every installed skin (qq98 / ths / xp / blue-fantasy / dragon-heir / minecraft), tries it on live inside the real GUI (executes the actual client bundle, light/dark preview), exits with a full restore, and applies in one click through the host /api/skin-center API (dsh-skin use, hot-reloaded, no restart)",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.17",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|