@spicetify/kit 0.4.3 → 0.4.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.
- package/dist/push.js +3 -2
- package/dist/stdlib-boundary.js +2 -1
- package/dist/vault-metadata.js +8 -0
- package/dist/version.js +1 -1
- package/package.json +2 -2
- package/vendor/stdlib/metadata.json +1 -1
- package/vendor/stdlib/src/util.ts +20 -5
- package/vendor/stdlib/src/webpack/ComponentLibrary.xpui.ts +12 -2
- package/vendor/stdlib/src/webpack/index.ts +37 -39
- package/vendor/stdlib/src/webpack/registry-quiet.ts +30 -0
- /package/vendor/classmaps/{1030001 → 1030003}/classmap.json +0 -0
package/dist/push.js
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
14
14
|
import path from "node:path";
|
|
15
|
+
import { KIND_OF_META_SOURCE } from "./vault-metadata.js";
|
|
15
16
|
// localStorage quota is ~5 MB for the xpui origin, shared with Spotify's own
|
|
16
17
|
// keys and other local modules, so thresholds are conservative. Counted in
|
|
17
18
|
// UTF-16 code units (JSON.stringify(...).length), the unit the quota counts.
|
|
@@ -257,8 +258,8 @@ export function pushExpression(rec, id, nonce, stamped) {
|
|
|
257
258
|
if (reenabled) await (M.reload ?? M.enable)(id);
|
|
258
259
|
// Re-enabling a theme the loader just unloaded would fight the
|
|
259
260
|
// single-active-theme invariant and knock the pushed theme back off.
|
|
260
|
-
|
|
261
|
-
const themed = (meta) => meta
|
|
261
|
+
const kindOf = ${KIND_OF_META_SOURCE};
|
|
262
|
+
const themed = (meta) => kindOf(meta) === "theme";
|
|
262
263
|
const pushedIsTheme = themed(rec.metadata);
|
|
263
264
|
const isTheme = (mid) => themed(M.manifest?.modules?.find((m) => m.identifier === mid));
|
|
264
265
|
for (const other of before) {
|
package/dist/stdlib-boundary.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
6
6
|
import path from "node:path";
|
|
7
|
+
import { kindOfMeta } from "./vault-metadata.js";
|
|
7
8
|
export const STDLIB_BOUNDARY_WARNING_RULES = ["ambient-client", "client-dom", "direct-map"];
|
|
8
9
|
const SOURCE_EXTENSIONS = new Set([".js", ".jsx", ".mjs", ".ts", ".tsx"]);
|
|
9
10
|
const STYLE_EXTENSIONS = new Set([".css", ".scss"]);
|
|
@@ -202,7 +203,7 @@ export function checkExternalStdlibBoundary(root, metadata) {
|
|
|
202
203
|
const entries = meta.entries;
|
|
203
204
|
if (typeof entries?.js !== "string" || meta.name === "stdlib")
|
|
204
205
|
return out;
|
|
205
|
-
const isTheme =
|
|
206
|
+
const isTheme = kindOfMeta(meta) === "theme";
|
|
206
207
|
const dependencies = meta.dependencies;
|
|
207
208
|
const missingDependency = !isTheme &&
|
|
208
209
|
typeof dependencies === "object" &&
|
package/dist/vault-metadata.js
CHANGED
|
@@ -12,6 +12,14 @@ export const kindOfMeta = (meta) => {
|
|
|
12
12
|
const tags = Array.isArray(meta.tags) ? meta.tags : [];
|
|
13
13
|
return KINDS.find((kind) => tags.includes(kind));
|
|
14
14
|
};
|
|
15
|
+
// kindOfMeta as JavaScript source, for expressions evaluated inside the
|
|
16
|
+
// client where kit code cannot be imported. It also accepts a missing meta.
|
|
17
|
+
export const KIND_OF_META_SOURCE = `((meta) => {
|
|
18
|
+
const kinds = ${JSON.stringify(KINDS)};
|
|
19
|
+
if (typeof meta?.kind === "string" && kinds.includes(meta.kind)) return meta.kind;
|
|
20
|
+
const tags = Array.isArray(meta?.tags) ? meta.tags : [];
|
|
21
|
+
return kinds.find((kind) => tags.includes(kind));
|
|
22
|
+
})`;
|
|
15
23
|
// metadata.json authors are plain names; author objects (with a github)
|
|
16
24
|
// pass through, so an artifact may declare either.
|
|
17
25
|
const normalizeAuthors = (authors) => authors.flatMap((a) => {
|
package/dist/version.js
CHANGED
|
@@ -4,5 +4,5 @@
|
|
|
4
4
|
*/
|
|
5
5
|
// Kept in lockstep with package.json by release-please. Scaffolds must depend
|
|
6
6
|
// on the kit version that generated them, especially across pre-1.0 minors.
|
|
7
|
-
export const KIT_VERSION = "0.4.
|
|
7
|
+
export const KIT_VERSION = "0.4.4"; // x-release-please-version
|
|
8
8
|
export const KIT_DEPENDENCY_RANGE = `^${KIT_VERSION}`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spicetify/kit",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.4",
|
|
4
4
|
"description": "Developer kit for spicetify v3 modules: scaffold, build, hot-push, and pack",
|
|
5
5
|
"license": "GPL-3.0-or-later",
|
|
6
6
|
"homepage": "https://github.com/spicetify/modules#readme",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"access": "public"
|
|
41
41
|
},
|
|
42
42
|
"spicetify": {
|
|
43
|
-
"stdlibVersion": "1.13.
|
|
43
|
+
"stdlibVersion": "1.13.2"
|
|
44
44
|
},
|
|
45
45
|
"scripts": {
|
|
46
46
|
"build": "tsc -p tsconfig.build.json",
|
|
@@ -14,9 +14,10 @@ type Predicate<A> = (value: A) => boolean;
|
|
|
14
14
|
|
|
15
15
|
// Some client exports are functions whose own toString is not callable;
|
|
16
16
|
// they can never match a needle, so they stringify to "".
|
|
17
|
-
const
|
|
17
|
+
const stringify = (x: any): string => {
|
|
18
|
+
if (x == null) return "";
|
|
18
19
|
try {
|
|
19
|
-
return x
|
|
20
|
+
return String(x);
|
|
20
21
|
} catch {
|
|
21
22
|
try {
|
|
22
23
|
return Function.prototype.toString.call(x);
|
|
@@ -26,18 +27,32 @@ const safeString = (x: any): string => {
|
|
|
26
27
|
}
|
|
27
28
|
};
|
|
28
29
|
|
|
30
|
+
const sources = new WeakMap<object, string>();
|
|
31
|
+
|
|
32
|
+
// sourceOf is the source text needles match against, computed once per
|
|
33
|
+
// function or object for the whole session.
|
|
34
|
+
export const sourceOf = (x: unknown): string => {
|
|
35
|
+
if ((typeof x !== "object" && typeof x !== "function") || x === null) return stringify(x);
|
|
36
|
+
let source = sources.get(x);
|
|
37
|
+
if (source === undefined) {
|
|
38
|
+
source = stringify(x);
|
|
39
|
+
sources.set(x, source);
|
|
40
|
+
}
|
|
41
|
+
return source;
|
|
42
|
+
};
|
|
43
|
+
|
|
29
44
|
export function findBy(...tests: Array<string | RegExp | Predicate<any>>) {
|
|
30
45
|
const testFns = tests.map((test): Predicate<any> => {
|
|
31
46
|
switch (typeof test) {
|
|
32
47
|
case "string":
|
|
33
|
-
return (x) =>
|
|
48
|
+
return (x) => sourceOf(x).includes(test);
|
|
34
49
|
case "function":
|
|
35
50
|
return (x) => test(x);
|
|
36
51
|
default: // assume regex
|
|
37
|
-
return (x) => test.test(
|
|
52
|
+
return (x) => test.test(sourceOf(x));
|
|
38
53
|
}
|
|
39
54
|
});
|
|
40
|
-
const testFn = (x: any) => testFns.
|
|
55
|
+
const testFn = (x: any) => testFns.every((t) => t(x));
|
|
41
56
|
return <A>(xs: A[]) => xs.find(testFn)!;
|
|
42
57
|
}
|
|
43
58
|
|
|
@@ -10,9 +10,19 @@ await CHUNKS.xpui.promise;
|
|
|
10
10
|
const componentNames = Object.keys(
|
|
11
11
|
exported.find((e) => e.BrowserDefaultFocusStyleProvider && Object.hasOwn(e, "BrowserDefaultFocusStyleProvider")),
|
|
12
12
|
);
|
|
13
|
-
const componentRegexes = componentNames.map(
|
|
13
|
+
const componentRegexes = componentNames.map(
|
|
14
|
+
(n) => new RegExp(String.raw`"data-encore-id":(?:[a-zA-Z_$][\w$]*\.){2}${n}\b`),
|
|
15
|
+
);
|
|
14
16
|
const componentPairs = [exportedFunctions.map((f) => [f, f]), exportedForwardRefs.map((f) => [(f as any).render, f])]
|
|
15
17
|
.flat()
|
|
16
|
-
.map(([s, f]) =>
|
|
18
|
+
.map(([s, f]) => {
|
|
19
|
+
const code = src(s);
|
|
20
|
+
return [
|
|
21
|
+
code.includes('"data-encore-id"')
|
|
22
|
+
? componentNames.find((_, i) => componentRegexes[i].test(code))
|
|
23
|
+
: undefined,
|
|
24
|
+
f,
|
|
25
|
+
];
|
|
26
|
+
});
|
|
17
27
|
|
|
18
28
|
export const UI: any = Object.fromEntries(componentPairs);
|
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { warn } from "../logger.ts";
|
|
7
|
+
import { sourceOf } from "../util.ts";
|
|
7
8
|
import { postWebpackRequireHooks, WebpackModule, WebpackRequire, webpackRequire } from "../wpunpk.mix.ts";
|
|
8
9
|
import { createCaptureReadiness } from "./capture-readiness.ts";
|
|
10
|
+
import { watchRegistryQuiet } from "./registry-quiet.ts";
|
|
9
11
|
|
|
10
12
|
export let modules: Array<[PropertyKey, WebpackModule]>;
|
|
11
13
|
export let exports: Array<Record<string, any>>;
|
|
@@ -18,19 +20,7 @@ export let exportedContexts: Array<React.Context<any>>;
|
|
|
18
20
|
export let exportedForwardRefs: Array<any>;
|
|
19
21
|
export let exportedMemos: React.NamedExoticComponent[];
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
// can never match a needle, so they stringify to "".
|
|
23
|
-
export const src = (f: unknown): string => {
|
|
24
|
-
try {
|
|
25
|
-
return String(f);
|
|
26
|
-
} catch {
|
|
27
|
-
try {
|
|
28
|
-
return Function.prototype.toString.call(f);
|
|
29
|
-
} catch {
|
|
30
|
-
return "";
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
};
|
|
23
|
+
export const src = sourceOf;
|
|
34
24
|
|
|
35
25
|
export const analyzeWebpackRequire = (webpackRequire: WebpackRequire) => {
|
|
36
26
|
const modules = Object.entries(webpackRequire.m) as Array<[keyof any, WebpackModule]>;
|
|
@@ -84,18 +74,31 @@ Object.assign(CHUNKS, {
|
|
|
84
74
|
// executed by capture time. Lazy route chunks go through the runtime's own
|
|
85
75
|
// script loader (wpr.l) from then on.
|
|
86
76
|
postWebpackRequireHooks.push((wpr: any) => {
|
|
77
|
+
let loading = 0;
|
|
87
78
|
if (typeof wpr?.l === "function") {
|
|
88
79
|
const load = wpr.l.bind(wpr);
|
|
89
|
-
wpr.l = (url: string, done: (event: unknown) => unknown, key?: string, chunkId?: unknown) =>
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
(
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
80
|
+
wpr.l = (url: string, done: (event: unknown) => unknown, key?: string, chunkId?: unknown) => {
|
|
81
|
+
loading++;
|
|
82
|
+
try {
|
|
83
|
+
return load(
|
|
84
|
+
url,
|
|
85
|
+
(event: unknown) => {
|
|
86
|
+
loading--;
|
|
87
|
+
try {
|
|
88
|
+
resolveChunk(new URL(url, location.href).pathname);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
console.error(`[stdlib] could not resolve chunk ${url}:`, error);
|
|
91
|
+
}
|
|
92
|
+
return done(event);
|
|
93
|
+
},
|
|
94
|
+
key,
|
|
95
|
+
chunkId,
|
|
96
|
+
);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
loading--;
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
99
102
|
}
|
|
100
103
|
// Chunk scripts that finished loading before capture. The xpui entry
|
|
101
104
|
// chunks are deliberately excluded: they are gated below on registry
|
|
@@ -108,23 +111,18 @@ postWebpackRequireHooks.push((wpr: any) => {
|
|
|
108
111
|
}
|
|
109
112
|
}
|
|
110
113
|
// Capture can fire while boot is still registering modules (xpui-modules
|
|
111
|
-
// and
|
|
112
|
-
// there). The analysis below
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const count = Object.keys(wpr?.m ?? {}).length;
|
|
120
|
-
stable = count === last ? stable + 1 : 0;
|
|
121
|
-
last = count;
|
|
122
|
-
if (stable >= 3 || ++ticks > 100) {
|
|
123
|
-
clearInterval(settle);
|
|
114
|
+
// and the lazy chunks the client requests right after it land later, and
|
|
115
|
+
// analysis needles such as GenericModal live there). The analysis below
|
|
116
|
+
// snapshots wpr.m once, so hold the xpui promises until the registry has
|
|
117
|
+
// been quiet, with no chunk in flight, for 300ms.
|
|
118
|
+
watchRegistryQuiet(
|
|
119
|
+
() => Object.keys(wpr?.m ?? {}).length,
|
|
120
|
+
() => loading,
|
|
121
|
+
() => {
|
|
124
122
|
resolveChunk("/vendor~xpui.js");
|
|
125
123
|
resolveChunk("/xpui.js");
|
|
126
|
-
}
|
|
127
|
-
|
|
124
|
+
},
|
|
125
|
+
);
|
|
128
126
|
});
|
|
129
127
|
|
|
130
128
|
// Capture subscribers run synchronously right after the analysis lands, so
|
|
@@ -135,7 +133,7 @@ postWebpackRequireHooks.push((wpr: any) => {
|
|
|
135
133
|
let captured = false;
|
|
136
134
|
const captureSubscribers: Array<() => void> = [];
|
|
137
135
|
const webpackCaptureReadiness = createCaptureReadiness({
|
|
138
|
-
// Registry quiescence caps at about
|
|
136
|
+
// Registry quiescence caps at about 10s. Leave room for its last tick,
|
|
139
137
|
// then release degraded so a changed runtime cannot deadlock all modules.
|
|
140
138
|
timeoutMs: 12000,
|
|
141
139
|
onTimeout: () => warn("[stdlib] capture health: webpack capture timed out; module surfaces will be degraded"),
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Afonso Jorge Ramos
|
|
3
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const QUIET_MS = 300;
|
|
7
|
+
const TICK_MS = 25;
|
|
8
|
+
const CAP_MS = 10_000;
|
|
9
|
+
|
|
10
|
+
// watchRegistryQuiet calls onQuiet once the module registry has not changed,
|
|
11
|
+
// and no chunk load has been in flight, for 300ms. A 10s cap bounds the wait
|
|
12
|
+
// so a chunk that never finishes, or a registry that never settles, still
|
|
13
|
+
// resolves.
|
|
14
|
+
export function watchRegistryQuiet(count: () => number, pending: () => number, onQuiet: () => void): void {
|
|
15
|
+
const started = Date.now();
|
|
16
|
+
let last = -1;
|
|
17
|
+
let quietSince = started;
|
|
18
|
+
const timer = setInterval(() => {
|
|
19
|
+
const current = count();
|
|
20
|
+
const at = Date.now();
|
|
21
|
+
if (current !== last || pending() > 0) {
|
|
22
|
+
last = current;
|
|
23
|
+
quietSince = at;
|
|
24
|
+
}
|
|
25
|
+
if (at - quietSince >= QUIET_MS || at - started >= CAP_MS) {
|
|
26
|
+
clearInterval(timer);
|
|
27
|
+
onQuiet();
|
|
28
|
+
}
|
|
29
|
+
}, TICK_MS);
|
|
30
|
+
}
|
|
File without changes
|