@linxin666/dsh-client-ui-skin-center 0.1.3 → 0.1.4

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 (2) hide show
  1. package/lib/index.js +96 -34
  2. package/package.json +1 -1
package/lib/index.js CHANGED
@@ -23,22 +23,52 @@ import { fileURLToPath } from "node:url";
23
23
  * @module @linxin666/dsh-client-ui-skin-center/skin-switch
24
24
  */
25
25
  /**
26
+ * Walk up from a file location to the nearest directory whose @linxin666/
27
+ * scoped subdir actually holds skin packages (dsh-skins carrier or
28
+ * dsh-client-ui-skin-* packages). pnpm's virtual store realpaths packages
29
+ * into node_modules/.pnpm/<pkg>@<ver>/node_modules/<name>, so a plain
30
+ * '../../' from the skin-center package can never see its siblings there —
31
+ * this anchor finds the node_modules root that owns the scoped dir.
32
+ * @param fromDir - the realpathed package dir to walk up from.
33
+ * @returns the anchor dir, or null when no scoped skin dir is found.
34
+ */
35
+ function findScopedAnchor(fromDir) {
36
+ let current = fromDir;
37
+ for (;;) {
38
+ const scoped = join(current, "@linxin666");
39
+ try {
40
+ for (const entry of readdirSync(scoped)) {
41
+ if (entry === "dsh-skins") return current;
42
+ if (entry.startsWith("dsh-client-ui-skin-") && entry !== "dsh-client-ui-skin-center") return current;
43
+ }
44
+ } catch {}
45
+ const parent = dirname(current);
46
+ if (parent === current) return null;
47
+ current = parent;
48
+ }
49
+ }
50
+ /**
26
51
  * Resolve the directory that holds the skin packages (each a dir carrying a
27
- * skin.json). Both install layouts resolve with new URL('../../', ...):
28
- * - monorepo: packages/skins/skin-center/lib/../../ -> packages/skins/
29
- * - npm install: node_modules/@linxin666/dsh-client-ui-skin-center/lib/../..
30
- * -> node_modules/@linxin666/ (the scoped dir holding dsh-client-ui-skin-*)
31
- * The legacy '../../../skins/' spelling (which pointed at node_modules/skins/
32
- * under npm — the ENOENT of zhu1090093659/dsh-web-ui#21/#33/#34) is kept as a
33
- * fallback candidate, and DSH_SKINS_DIR overrides everything (tests use it).
52
+ * skin.json). Candidates, in order:
53
+ * - monorepo / flat npm layout: new URL('../../', import.meta.url)
54
+ * (packages/skins/ or node_modules/@linxin666/);
55
+ * - pnpm virtual-store layout: the nearest @linxin666/ scoped dir found by
56
+ * walking up from this package's realpathed location;
57
+ * - the legacy '../../../skins/' spelling (which pointed at
58
+ * node_modules/skins/ under npm the ENOENT of
59
+ * zhu1090093659/dsh-web-ui#21/#33/#34), kept as a fallback.
60
+ * DSH_SKINS_DIR overrides everything (tests use it).
34
61
  */
35
62
  function resolveSkinsDir() {
36
63
  const fromEnv = process.env.DSH_SKINS_DIR;
37
64
  if (fromEnv !== void 0 && fromEnv !== "") return fromEnv;
38
- const candidates = [fileURLToPath(new URL("../../", import.meta.url)), fileURLToPath(new URL("../../../skins/", import.meta.url))];
39
- for (const candidate of candidates) try {
40
- for (const dir of readdirSync(candidate)) if (statSync(join(candidate, dir, "skin.json"), { throwIfNoEntry: false })) return candidate;
41
- } catch {}
65
+ const here = fileURLToPath(import.meta.url);
66
+ const candidates = [
67
+ fileURLToPath(new URL("../../", import.meta.url)),
68
+ findScopedAnchor(dirname(here)),
69
+ fileURLToPath(new URL("../../../skins/", import.meta.url))
70
+ ].filter((candidate) => candidate !== null);
71
+ for (const candidate of candidates) if (listSkinDirCandidates(candidate).length > 0) return candidate;
42
72
  return candidates[0];
43
73
  }
44
74
  /** The skin-package root for this install (see resolveSkinsDir). */
@@ -52,12 +82,11 @@ const DEFAULT_PROFILE = process.env.DSH_SKIN_PROFILE ?? "web";
52
82
  * Parse the switch-relevant fields of one skin.json. Returns null for
53
83
  * anything that is not a valid skin so it is simply skipped — never walking
54
84
  * outside the skins tree (the id is validated before any path use).
55
- * @param dir - directory name under the skins root.
56
- * @param skinsDir - the skins root (defaults to the resolved install layout).
85
+ * @param absDir - absolute path of the candidate skin directory.
57
86
  */
58
- function readSkinMeta(dir, skinsDir = SKINS_DIR) {
87
+ function readSkinMeta(absDir) {
59
88
  try {
60
- const meta = JSON.parse(readFileSync(join(skinsDir, dir, "skin.json"), "utf8"));
89
+ const meta = JSON.parse(readFileSync(join(absDir, "skin.json"), "utf8"));
61
90
  if (typeof meta !== "object" || meta === null) return null;
62
91
  const record = meta;
63
92
  if (typeof record.id !== "string" || !/^[a-z0-9-]+$/.test(record.id)) return null;
@@ -78,18 +107,17 @@ function readSkinMeta(dir, skinsDir = SKINS_DIR) {
78
107
  }
79
108
  }
80
109
  /**
81
- * Derive the skin registry from each skin dir's skin.json the single
82
- * source of truth (skin.json already carries package/wiring.id/bundleWired).
83
- * Replaces the CLI's hand-maintained SKINS dictionary, so adding a skin
84
- * needs no code change here. Directories without a readable skin.json
85
- * (the skin-center manager itself, non-skin packages in the npm scoped dir)
86
- * are skipped. The root is injectable so tests can point at either install
87
- * layout.
88
- * @param skinsDir - the skins root (defaults to the resolved install layout).
89
- * @returns skin id -> switch metadata.
110
+ * Enumerate every candidate skin directory under a skins root. Two shapes:
111
+ * - direct subdirectories carrying a skin.json (monorepo packages/skins/<id>,
112
+ * and per-skin npm packages @linxin666/dsh-client-ui-skin-<id>);
113
+ * - the bundled-skins carrier: @linxin666/dsh-skins/skins/<id> (skin assets
114
+ * shipped inside the dsh-skins aggregate so npm needs no per-skin
115
+ * package names). Directories without a skin.json are skipped.
116
+ * @param skinsDir - the skins root.
117
+ * @returns absolute candidate dirs (possibly empty).
90
118
  */
91
- function loadRegistry(skinsDir = SKINS_DIR) {
92
- const out = {};
119
+ function listSkinDirCandidates(skinsDir) {
120
+ const out = [];
93
121
  let entries;
94
122
  try {
95
123
  entries = readdirSync(skinsDir);
@@ -97,12 +125,46 @@ function loadRegistry(skinsDir = SKINS_DIR) {
97
125
  return out;
98
126
  }
99
127
  for (const dir of entries) {
100
- const meta = readSkinMeta(dir, skinsDir);
128
+ const candidate = join(skinsDir, dir);
129
+ if (statSync(join(candidate, "skin.json"), { throwIfNoEntry: false })) {
130
+ out.push(candidate);
131
+ continue;
132
+ }
133
+ if (dir === "dsh-skins") {
134
+ const bundled = join(candidate, "skins");
135
+ let subdirs;
136
+ try {
137
+ subdirs = readdirSync(bundled);
138
+ } catch {
139
+ continue;
140
+ }
141
+ for (const sub of subdirs) {
142
+ const subDir = join(bundled, sub);
143
+ if (statSync(join(subDir, "skin.json"), { throwIfNoEntry: false })) out.push(subDir);
144
+ }
145
+ }
146
+ }
147
+ return out;
148
+ }
149
+ /**
150
+ * Derive the skin registry from each skin dir's skin.json — the single
151
+ * source of truth (skin.json already carries package/wiring.id/bundleWired).
152
+ * Replaces the CLI's hand-maintained SKINS dictionary, so adding a skin
153
+ * needs no code change here. Candidate dirs come from
154
+ * listSkinDirCandidates (direct skin dirs + the dsh-skins bundled carrier).
155
+ * The root is injectable so tests can point at either install layout.
156
+ * @param skinsDir - the skins root (defaults to the resolved install layout).
157
+ * @returns skin id -> switch metadata.
158
+ */
159
+ function loadRegistry(skinsDir = SKINS_DIR) {
160
+ const out = {};
161
+ for (const dir of listSkinDirCandidates(skinsDir)) {
162
+ const meta = readSkinMeta(dir);
101
163
  if (meta === null || meta.wiring === void 0 || meta.package === void 0) continue;
102
164
  out[meta.id] = {
103
165
  pkg: meta.package,
104
166
  id: meta.wiring.id,
105
- dir: join(skinsDir, dir),
167
+ dir,
106
168
  bundleWired: meta.wiring.bundleWired === true
107
169
  };
108
170
  }
@@ -486,17 +548,17 @@ function postRoute(path, run) {
486
548
  * skin.json. The id is validated against this map (never used as a raw
487
549
  * path) so the bundle route cannot be walked off the skins tree. The root
488
550
  * resolves per install layout (monorepo packages/skins/, npm
489
- * node_modules/@linxin666/) see skin-switch resolveSkinsDir.
551
+ * node_modules/@linxin666/) and candidates include the bundled dsh-skins
552
+ * carrier (npm layout) — see skin-switch resolveSkinsDir /
553
+ * listSkinDirCandidates.
490
554
  * @returns skin id -> directory name.
491
555
  */
492
556
  function skinDirectories() {
493
557
  const out = /* @__PURE__ */ new Map();
494
- for (const dir of readdirSync(SKINS_DIR)) {
495
- const metaFile = join(SKINS_DIR, dir, "skin.json");
496
- if (!statSync(metaFile, { throwIfNoEntry: false })) continue;
558
+ for (const dir of listSkinDirCandidates(SKINS_DIR)) {
497
559
  let meta;
498
560
  try {
499
- meta = JSON.parse(readFileSync(metaFile, "utf8"));
561
+ meta = JSON.parse(readFileSync(join(dir, "skin.json"), "utf8"));
500
562
  } catch {
501
563
  continue;
502
564
  }
@@ -545,7 +607,7 @@ function bundleRoute() {
545
607
  });
546
608
  return;
547
609
  }
548
- const bundle = join(SKINS_DIR, dir, "lib", "client.js");
610
+ const bundle = join(dir, "lib", "client.js");
549
611
  if (!statSync(bundle, { throwIfNoEntry: false })) {
550
612
  json(res, 404, {
551
613
  ok: false,
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.3",
4
+ "version": "0.1.4",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {