@spicetify/kit 0.3.1 → 0.4.0
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/README.md +82 -0
- package/dist/args.js +15 -0
- package/dist/cli.js +4 -1
- package/dist/create.js +9 -5
- package/dist/dev.js +86 -22
- package/dist/install.js +4 -6
- package/dist/launch.js +24 -24
- package/dist/push.js +187 -44
- package/dist/remove.js +37 -0
- package/dist/stdlib-boundary.js +2 -2
- package/dist/version.js +1 -1
- package/package.json +6 -4
- package/vendor/classmaps/1030000/classmap-19f856aefd5.json +109 -0
- package/vendor/stdlib/metadata.json +1 -1
- package/vendor/stdlib/query.ts +31 -0
- package/vendor/stdlib/src/client.ts +89 -1
- package/vendor/stdlib/src/events.mix.ts +9 -0
- package/vendor/stdlib/src/events.ts +1 -1
- package/vendor/stdlib/src/registers/menu-order.ts +17 -0
- package/vendor/stdlib/src/registers/menu.ts +8 -4
- package/vendor/stdlib/src/registers/mount.ts +35 -9
- package/vendor/stdlib/src/registers/root.ts +1 -1
- package/vendor/stdlib/src/storage.ts +1 -1
- package/vendor/stdlib/src/webpack/ReactComponents.xpui.ts +3 -9
- package/vendor/stdlib/src/webpack/URI.xpui.ts +1 -1
- package/vendor/stdlib/src/webpack/capture-readiness.ts +5 -3
- package/vendor/stdlib/src/webpack/index.ts +10 -4
- package/vendor/stdlib/src/webpack/playlist-menu.ts +54 -0
- package/vendor/stdlib/src/wpunpk.mix.ts +1 -1
- package/vendor/classmaps/1020099/classmap-19f856aefd5.json +0 -109
package/README.md
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# @spicetify/kit
|
|
2
|
+
|
|
3
|
+
The developer kit for [Spicetify](https://spicetify.app) v3 modules. It scaffolds a module, builds it, and hot-pushes every save into a running Spotify client.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- Node 22.6 or newer.
|
|
8
|
+
- Spotify with Spicetify v3 applied (`spicetify apply`). The kit pushes into the client the Spicetify CLI has patched, so a stock Spotify cannot load a pushed module.
|
|
9
|
+
- The standalone Spotify install. The Microsoft Store build cannot be started with a debug port.
|
|
10
|
+
|
|
11
|
+
## Start a module
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npm create spicetify-module my-module
|
|
15
|
+
cd my-module
|
|
16
|
+
npm install
|
|
17
|
+
npm run dev
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`npm run dev` starts Spotify with a remote debugging port, or reuses a client already running with one. It then rebuilds on every save and pushes the build into the client in about a second, with no re-apply and no restart. Stop it with ctrl-c, which removes the pushed copy so Spotify goes back to whatever version of the module is installed.
|
|
21
|
+
|
|
22
|
+
Pick a template with `--template`:
|
|
23
|
+
|
|
24
|
+
| Template | What you get |
|
|
25
|
+
| ----------------- | --------------------------------------- |
|
|
26
|
+
| `basic` (default) | a topbar button that opens a page |
|
|
27
|
+
| `extension` | behaviour only, with no UI |
|
|
28
|
+
| `app` | a sidebar entry and a full page |
|
|
29
|
+
| `theme` | CSS and `color.ini` only, no TypeScript |
|
|
30
|
+
|
|
31
|
+
## Scripts in a scaffolded project
|
|
32
|
+
|
|
33
|
+
| Script | What it does |
|
|
34
|
+
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
|
35
|
+
| `npm run dev` | start Spotify, then rebuild and hot-push on every save |
|
|
36
|
+
| `npm run build` | build once into `dist/` |
|
|
37
|
+
| `npm run check` | typecheck and audit the module against the [module standard](https://github.com/spicetify/modules/blob/main/docs/module-standard.md) |
|
|
38
|
+
| `npm test` | run the unit tests in `test/` |
|
|
39
|
+
| `npm run remove` | remove a pushed copy that is still installed in the client |
|
|
40
|
+
|
|
41
|
+
`dev` flags go after `--`, for example `npm run dev -- --keep`:
|
|
42
|
+
|
|
43
|
+
| Flag | Effect |
|
|
44
|
+
| ------------- | ------------------------------------------------------------------------- |
|
|
45
|
+
| `--keep` | leave the pushed copy installed when `dev` stops |
|
|
46
|
+
| `--once` | build and push once, then exit; the pushed copy stays |
|
|
47
|
+
| `--no-launch` | do not start Spotify; wait for one started with `--remote-debugging-port` |
|
|
48
|
+
| `--port <n>` | use another debug port (default 9229, or `SPICETIFY_CDP_PORT`) |
|
|
49
|
+
|
|
50
|
+
## Troubleshooting
|
|
51
|
+
|
|
52
|
+
**"port 9229 is already in use by ..., not Spotify"**: Node's inspector also defaults to 9229, so `node --inspect` or `wrangler dev` can hold it. Stop that process, or use another port with `npm run dev -- --port 9230`.
|
|
53
|
+
|
|
54
|
+
**"the v3 loader is not staged in this client"**: Spotify is running without Spicetify v3. Run `spicetify apply`, then start `dev` again.
|
|
55
|
+
|
|
56
|
+
**The UI still shows the old build**: a surface that was already on screen keeps the old code until it remounts. Navigate away from it and back.
|
|
57
|
+
|
|
58
|
+
## Commands
|
|
59
|
+
|
|
60
|
+
`spicetify-kit <command> --help` prints each command's flags.
|
|
61
|
+
|
|
62
|
+
| Command | What it does |
|
|
63
|
+
| ---------------------- | -------------------------------------------------------------- |
|
|
64
|
+
| `create <name>` | scaffold a module project |
|
|
65
|
+
| `dev <module>` | watch, rebuild, and hot-push into Spotify |
|
|
66
|
+
| `build [module...]` | bundle modules into `dist/` |
|
|
67
|
+
| `check [module]` | audit a module against the module standard |
|
|
68
|
+
| `remove <dir\|id>` | remove a pushed copy from the running client |
|
|
69
|
+
| `install <zip\|dir>` | push a packed or built module into the running client once |
|
|
70
|
+
| `pack <dist-dir>` | zip a built module and print its sha256 |
|
|
71
|
+
| `vault add <dist-dir>` | write the store registry entry for a release |
|
|
72
|
+
| `from-theme <dir>` | convert a classic `user.css` and `color.ini` theme to a module |
|
|
73
|
+
|
|
74
|
+
## Learn more
|
|
75
|
+
|
|
76
|
+
- [Building a module](https://github.com/spicetify/docs/blob/v3/src/content/docs/development/building-a-module.md)
|
|
77
|
+
- [Authoring guide](https://github.com/spicetify/modules/blob/main/docs/authoring-guide.md)
|
|
78
|
+
- [Publishing to the store](https://github.com/spicetify/modules/blob/main/docs/publishing.md)
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
GPL-3.0-or-later
|
package/dist/args.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Afonso Jorge Ramos
|
|
3
|
+
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
*/
|
|
5
|
+
// flagValue returns the operand of --<name>, undefined when the flag is
|
|
6
|
+
// absent, and throws when the flag is present without an operand.
|
|
7
|
+
export function flagValue(argv, name) {
|
|
8
|
+
const i = argv.indexOf(`--${name}`);
|
|
9
|
+
if (i < 0)
|
|
10
|
+
return undefined;
|
|
11
|
+
const value = argv[i + 1];
|
|
12
|
+
if (value === undefined || value.startsWith("--"))
|
|
13
|
+
throw new Error(`--${name} needs a value`);
|
|
14
|
+
return value;
|
|
15
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -10,10 +10,11 @@ usage: spicetify-kit <command> [args]
|
|
|
10
10
|
check [module] audit a module against the module standard (advisory)
|
|
11
11
|
from-theme <dir> migrate a classic theme (user.css + color.ini) to a module
|
|
12
12
|
build [module...] bundle modules (rolldown + scss) into dist/
|
|
13
|
-
dev <module> watch, rebuild, and hot-push into
|
|
13
|
+
dev <module> watch, rebuild, and hot-push into Spotify (starts it if needed)
|
|
14
14
|
pack <dist-dir> zip a built module and print its sha256
|
|
15
15
|
vault add <dir> record a built module into a vault file (--artifact <url>)
|
|
16
16
|
install <zip|dir> sideload a packed module into a running client
|
|
17
|
+
remove <dir|id> drop a hot-pushed override from a running client
|
|
17
18
|
|
|
18
19
|
run a command with --help for its flags`;
|
|
19
20
|
export async function main(argv) {
|
|
@@ -36,6 +37,8 @@ export async function main(argv) {
|
|
|
36
37
|
return await (await import("./vault.js")).runVault(rest);
|
|
37
38
|
case "install":
|
|
38
39
|
return await (await import("./install.js")).runInstall(rest);
|
|
40
|
+
case "remove":
|
|
41
|
+
return await (await import("./remove.js")).runRemove(rest);
|
|
39
42
|
case undefined:
|
|
40
43
|
case "--help":
|
|
41
44
|
case "-h":
|
package/dist/create.js
CHANGED
|
@@ -267,6 +267,7 @@ notification = 1C1C30
|
|
|
267
267
|
scripts: {
|
|
268
268
|
build: "spicetify-kit build .",
|
|
269
269
|
dev: "spicetify-kit dev .",
|
|
270
|
+
remove: "spicetify-kit remove .",
|
|
270
271
|
check: "spicetify-kit check .",
|
|
271
272
|
},
|
|
272
273
|
devDependencies: { "@spicetify/kit": KIT_DEPENDENCY_RANGE },
|
|
@@ -281,7 +282,7 @@ notification = 1C1C30
|
|
|
281
282
|
if (!bare) {
|
|
282
283
|
console.log("next steps:");
|
|
283
284
|
console.log(` cd ${rel} && npm install`);
|
|
284
|
-
console.log(" npm run dev # hot-push
|
|
285
|
+
console.log(" npm run dev # start Spotify, then rebuild and hot-push on every save");
|
|
285
286
|
}
|
|
286
287
|
}
|
|
287
288
|
export async function runCreate(argv, cwd = process.cwd()) {
|
|
@@ -369,6 +370,7 @@ export async function load(ctx: ModuleRuntimeContext) {
|
|
|
369
370
|
scripts: {
|
|
370
371
|
build: "spicetify-kit build .",
|
|
371
372
|
dev: "spicetify-kit dev .",
|
|
373
|
+
remove: "spicetify-kit remove .",
|
|
372
374
|
check: "tsc && spicetify-kit check .",
|
|
373
375
|
test: 'node --test "test/*.test.mts"',
|
|
374
376
|
},
|
|
@@ -392,7 +394,7 @@ export async function load(ctx: ModuleRuntimeContext) {
|
|
|
392
394
|
noEmit: true,
|
|
393
395
|
jsx: "react-jsx",
|
|
394
396
|
lib: ["ES2024", "ESNext.Disposable", "DOM", "DOM.Iterable"],
|
|
395
|
-
strict:
|
|
397
|
+
strict: true,
|
|
396
398
|
skipLibCheck: true,
|
|
397
399
|
resolveJsonModule: true,
|
|
398
400
|
types: [],
|
|
@@ -417,12 +419,14 @@ export async function load(ctx: ModuleRuntimeContext) {
|
|
|
417
419
|
console.log(`created ${rel}/`);
|
|
418
420
|
console.log("next steps:");
|
|
419
421
|
if (bare) {
|
|
420
|
-
console.log(`
|
|
421
|
-
console.log(`
|
|
422
|
+
console.log(` pnpm stitch modules/${name} # one-off build into dist/`);
|
|
423
|
+
console.log(` pnpm dev modules/${name} # start Spotify, then rebuild and hot-push on every save`);
|
|
422
424
|
}
|
|
423
425
|
else {
|
|
424
426
|
console.log(` cd ${rel} && npm install`);
|
|
425
|
-
console.log(" npm run dev #
|
|
427
|
+
console.log(" npm run dev # start Spotify, then rebuild and hot-push on every save");
|
|
426
428
|
console.log(" npm run build # one-off build into dist/");
|
|
429
|
+
console.log("stopping dev removes the pushed override; `npm run dev -- --keep` leaves it installed.");
|
|
430
|
+
console.log("Spotify needs Spicetify v3 applied (`spicetify apply`) for the push to land.");
|
|
427
431
|
}
|
|
428
432
|
}
|
package/dist/dev.js
CHANGED
|
@@ -9,32 +9,55 @@
|
|
|
9
9
|
* fresh bundle over the Chrome DevTools Protocol as a local install
|
|
10
10
|
* (Spicetify.Modules.installLocal), so nothing in the staged app bundle
|
|
11
11
|
* is touched and the loop is sub-second. Spotify must be running with
|
|
12
|
-
* --remote-debugging-port=<port
|
|
13
|
-
*
|
|
14
|
-
*
|
|
12
|
+
* --remote-debugging-port=<port>; dev starts (or reuses) such a client unless
|
|
13
|
+
* --no-launch is passed. Stopping dev removes the override so the client falls
|
|
14
|
+
* back to the staged copy; --keep (and --once) leave it installed, and
|
|
15
|
+
* `spicetify-kit remove` drops it later.
|
|
15
16
|
*/
|
|
16
17
|
import { watch } from "node:fs";
|
|
17
18
|
import path from "node:path";
|
|
19
|
+
import { flagValue } from "./args.js";
|
|
18
20
|
import { buildModule, readMetadata, resolveModuleDir } from "./build.js";
|
|
19
21
|
import { loadConfig, resolveClassmap } from "./classmap.js";
|
|
20
|
-
import { launchSpotify } from "./launch.js";
|
|
21
|
-
import { formatPushResult, push, record } from "./push.js";
|
|
22
|
+
import { launchSpotify, waitForTarget } from "./launch.js";
|
|
23
|
+
import { formatPushResult, formatRemoveOutcome, push, record, removeLocal, resolvePort } from "./push.js";
|
|
22
24
|
// Re-exported for callers that imported it from here before the push extraction.
|
|
23
25
|
export { formatPushResult } from "./push.js";
|
|
24
|
-
const USAGE =
|
|
25
|
-
|
|
26
|
+
const USAGE = `spicetify-kit dev <module> [--no-launch] [--keep] [--once] [--port 9229] [--classmap <key|path>] [--out <dir>]
|
|
27
|
+
--no-launch do not start Spotify; wait for one already running with --remote-debugging-port
|
|
28
|
+
--keep leave the pushed override installed when dev stops
|
|
29
|
+
--once build and push once, then exit (the override stays)
|
|
30
|
+
--port CDP port (default 9229, or SPICETIFY_CDP_PORT)`;
|
|
31
|
+
// isSourceChange filters watcher events down to module sources. A standalone
|
|
32
|
+
// project's module dir is the project root, so the build's own dist/ output,
|
|
33
|
+
// node_modules, and dot-dirs all sit inside the watched tree.
|
|
34
|
+
export function isSourceChange(file, moduleDir, outDir) {
|
|
35
|
+
const segments = file.split(/[\\/]/);
|
|
36
|
+
if (segments.some((s) => s.startsWith(".") || s === "node_modules"))
|
|
37
|
+
return false;
|
|
38
|
+
const rel = path.relative(outDir, path.resolve(moduleDir, file));
|
|
39
|
+
return rel.startsWith("..") || path.isAbsolute(rel);
|
|
40
|
+
}
|
|
26
41
|
export async function runDev(argv, cwd = process.cwd()) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
42
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
43
|
+
console.log(USAGE);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const valueFlags = new Set(["--port", "--classmap", "--out"]);
|
|
47
|
+
const moduleArg = argv.find((a, i) => !a.startsWith("--") && !valueFlags.has(argv[i - 1]));
|
|
48
|
+
const flag = (n) => flagValue(argv, n);
|
|
32
49
|
if (!moduleArg)
|
|
33
50
|
throw new Error(USAGE);
|
|
34
|
-
const port = flag("port")
|
|
51
|
+
const port = resolvePort(flag("port"));
|
|
35
52
|
const once = argv.includes("--once");
|
|
36
|
-
|
|
53
|
+
const keep = once || argv.includes("--keep");
|
|
54
|
+
if (argv.includes("--no-launch")) {
|
|
55
|
+
console.log(`[dev] waiting for Spotify on port ${port} (started with --remote-debugging-port=${port})`);
|
|
56
|
+
await waitForTarget(port, Number.POSITIVE_INFINITY);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
37
59
|
await launchSpotify(port);
|
|
60
|
+
}
|
|
38
61
|
const config = loadConfig(cwd);
|
|
39
62
|
const modulesDir = config.modulesDir ? path.resolve(cwd, config.modulesDir) : path.join(cwd, "modules");
|
|
40
63
|
const outDir = flag("out") ?? (config.outDir ? path.resolve(cwd, config.outDir) : path.join(cwd, "dist"));
|
|
@@ -48,7 +71,11 @@ export async function runDev(argv, cwd = process.cwd()) {
|
|
|
48
71
|
});
|
|
49
72
|
if (!resolved.path)
|
|
50
73
|
throw new Error("no classmap found (pass --classmap <key|path>)");
|
|
74
|
+
let pushed = false;
|
|
75
|
+
let stopping = false;
|
|
51
76
|
const cycle = async () => {
|
|
77
|
+
if (stopping)
|
|
78
|
+
return;
|
|
52
79
|
const started = Date.now();
|
|
53
80
|
let distDir;
|
|
54
81
|
try {
|
|
@@ -59,8 +86,13 @@ export async function runDev(argv, cwd = process.cwd()) {
|
|
|
59
86
|
console.error(`[dev] build failed: ${e.message}`);
|
|
60
87
|
return;
|
|
61
88
|
}
|
|
89
|
+
if (stopping)
|
|
90
|
+
return;
|
|
62
91
|
try {
|
|
63
|
-
const
|
|
92
|
+
const pending = push(record(distDir, id), id, port);
|
|
93
|
+
// Set before the await: a ctrl-c mid-push must still remove what lands.
|
|
94
|
+
pushed = true;
|
|
95
|
+
const raw = await pending;
|
|
64
96
|
const result = formatPushResult(raw);
|
|
65
97
|
const line = `[dev] ${id} ${result.message} (${Date.now() - started}ms)`;
|
|
66
98
|
if (result.ok)
|
|
@@ -72,13 +104,39 @@ export async function runDev(argv, cwd = process.cwd()) {
|
|
|
72
104
|
console.error(`[dev] push failed: ${e.message}`);
|
|
73
105
|
}
|
|
74
106
|
};
|
|
75
|
-
|
|
76
|
-
if (once)
|
|
77
|
-
return;
|
|
78
|
-
console.log(`[dev] watching ${moduleDir} (ctrl-c to stop; removeLocal("${id}") drops the override)`);
|
|
107
|
+
let current = Promise.resolve();
|
|
79
108
|
let timer;
|
|
109
|
+
const stop = async (code = 0) => {
|
|
110
|
+
if (stopping)
|
|
111
|
+
process.exit(130);
|
|
112
|
+
stopping = true;
|
|
113
|
+
clearTimeout(timer);
|
|
114
|
+
await current;
|
|
115
|
+
if (keep || !pushed)
|
|
116
|
+
process.exit(code);
|
|
117
|
+
try {
|
|
118
|
+
console.log(`[dev] ${formatRemoveOutcome(id, await removeLocal(id, port))}`);
|
|
119
|
+
}
|
|
120
|
+
catch (e) {
|
|
121
|
+
console.error(`[dev] could not remove the override: ${e.message}`);
|
|
122
|
+
console.error(`[dev] run \`spicetify-kit remove ${id}\` once Spotify is reachable`);
|
|
123
|
+
}
|
|
124
|
+
process.exit(code);
|
|
125
|
+
};
|
|
126
|
+
const onSignal = () => void stop();
|
|
127
|
+
process.on("SIGINT", onSignal);
|
|
128
|
+
process.on("SIGTERM", onSignal);
|
|
129
|
+
current = cycle();
|
|
130
|
+
await current;
|
|
131
|
+
if (once) {
|
|
132
|
+
process.off("SIGINT", onSignal);
|
|
133
|
+
process.off("SIGTERM", onSignal);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const onExit = keep ? "the override stays installed" : "the override is removed";
|
|
137
|
+
console.log(`[dev] watching ${moduleDir} (ctrl-c to stop; ${onExit})`);
|
|
80
138
|
let loggedDts = false;
|
|
81
|
-
watch(moduleDir, { recursive: true }, (_event, file) => {
|
|
139
|
+
const watcher = watch(moduleDir, { recursive: true }, (_event, file) => {
|
|
82
140
|
if (!file)
|
|
83
141
|
return;
|
|
84
142
|
// The build regenerates classmap.d.ts into the source dir on every run;
|
|
@@ -90,10 +148,16 @@ export async function runDev(argv, cwd = process.cwd()) {
|
|
|
90
148
|
}
|
|
91
149
|
return;
|
|
92
150
|
}
|
|
93
|
-
if (file
|
|
151
|
+
if (!isSourceChange(file, moduleDir, outDir))
|
|
94
152
|
return;
|
|
95
153
|
clearTimeout(timer);
|
|
96
|
-
timer = setTimeout(() =>
|
|
154
|
+
timer = setTimeout(() => {
|
|
155
|
+
current = current.then(cycle);
|
|
156
|
+
}, 200);
|
|
157
|
+
});
|
|
158
|
+
watcher.on("error", (e) => {
|
|
159
|
+
console.error(`[dev] watcher failed: ${e.message}`);
|
|
160
|
+
void stop(1);
|
|
97
161
|
});
|
|
98
162
|
// Keep the process alive while the watcher runs.
|
|
99
163
|
await new Promise(() => { });
|
package/dist/install.js
CHANGED
|
@@ -12,7 +12,8 @@ import { execFileSync } from "node:child_process";
|
|
|
12
12
|
import { existsSync, mkdtempSync, readFileSync, rmSync, statSync } from "node:fs";
|
|
13
13
|
import { tmpdir } from "node:os";
|
|
14
14
|
import path from "node:path";
|
|
15
|
-
import {
|
|
15
|
+
import { flagValue } from "./args.js";
|
|
16
|
+
import { formatPushResult, push, record, resolvePort } from "./push.js";
|
|
16
17
|
const USAGE = "spicetify-kit install <zip|dist-dir> [--port 9229]\n (requires `unzip` on PATH for a .zip)";
|
|
17
18
|
function unzipTo(zipPath, dest) {
|
|
18
19
|
try {
|
|
@@ -24,11 +25,8 @@ function unzipTo(zipPath, dest) {
|
|
|
24
25
|
}
|
|
25
26
|
export async function runInstall(argv, cwd = process.cwd()) {
|
|
26
27
|
const target = argv.find((a) => !a.startsWith("--"));
|
|
27
|
-
const flag = (n) =>
|
|
28
|
-
|
|
29
|
-
return i >= 0 ? argv[i + 1] : undefined;
|
|
30
|
-
};
|
|
31
|
-
const port = flag("port") ?? "9229";
|
|
28
|
+
const flag = (n) => flagValue(argv, n);
|
|
29
|
+
const port = resolvePort(flag("port"));
|
|
32
30
|
if (!target)
|
|
33
31
|
throw new Error(USAGE);
|
|
34
32
|
const abs = path.resolve(cwd, target);
|
package/dist/launch.js
CHANGED
|
@@ -16,9 +16,10 @@
|
|
|
16
16
|
import { execFileSync, spawn } from "node:child_process";
|
|
17
17
|
import { existsSync } from "node:fs";
|
|
18
18
|
import path from "node:path";
|
|
19
|
+
import { portInUseMessage, probePort } from "./push.js";
|
|
19
20
|
const APPX_MESSAGE = "Spotify is a Microsoft Store (AppX) install, which the kit cannot launch with a debug port: " +
|
|
20
21
|
"it needs the spicetify-staged --app-directory, which only the CLI knows. " +
|
|
21
|
-
"Start Spotify yourself with --remote-debugging-port,
|
|
22
|
+
"Start Spotify yourself with --remote-debugging-port, then run `spicetify-kit dev --no-launch`.";
|
|
22
23
|
// which returns the resolved path of a command on PATH, or null.
|
|
23
24
|
function defaultWhich(cmd) {
|
|
24
25
|
try {
|
|
@@ -38,7 +39,7 @@ export function discoverSpotify(platform = process.platform, env = process.env,
|
|
|
38
39
|
const app = "/Applications/Spotify.app";
|
|
39
40
|
if (exists(app))
|
|
40
41
|
return { kind: "macos", app };
|
|
41
|
-
throw new Error(`Spotify not found at ${app}. Install Spotify, or start it yourself with --remote-debugging-port and run dev
|
|
42
|
+
throw new Error(`Spotify not found at ${app}. Install Spotify, or start it yourself with --remote-debugging-port and run dev with --no-launch.`);
|
|
42
43
|
}
|
|
43
44
|
if (platform === "win32") {
|
|
44
45
|
// win32.join so paths use backslashes on any host (matters for tests).
|
|
@@ -53,30 +54,18 @@ export function discoverSpotify(platform = process.platform, env = process.env,
|
|
|
53
54
|
const onPath = which("spotify");
|
|
54
55
|
if (onPath)
|
|
55
56
|
return { kind: "linux", exe: onPath };
|
|
56
|
-
throw new Error("'spotify' not found on PATH. Install Spotify, or start it yourself with --remote-debugging-port and run dev
|
|
57
|
-
}
|
|
58
|
-
// hasXpuiTarget reports whether a debuggable xpui page is already listening
|
|
59
|
-
// on the port, so an already-good client is reused rather than killed.
|
|
60
|
-
export async function hasXpuiTarget(port) {
|
|
61
|
-
try {
|
|
62
|
-
const res = await fetch(`http://localhost:${port}/json/list`);
|
|
63
|
-
if (!res.ok)
|
|
64
|
-
return false;
|
|
65
|
-
const targets = (await res.json());
|
|
66
|
-
return targets.some((t) => t.url?.includes("xpui"));
|
|
67
|
-
}
|
|
68
|
-
catch {
|
|
69
|
-
return false;
|
|
70
|
-
}
|
|
57
|
+
throw new Error("'spotify' not found on PATH. Install Spotify, or start it yourself with --remote-debugging-port and run dev with --no-launch.");
|
|
71
58
|
}
|
|
72
59
|
// waitForTarget polls until an xpui debug target appears or the timeout
|
|
73
|
-
// fires
|
|
74
|
-
|
|
75
|
-
export async function waitForTarget(port, timeoutMs = 30_000) {
|
|
60
|
+
// fires. A foreign listener fails fast instead of burning the whole timeout.
|
|
61
|
+
export async function waitForTarget(port, timeoutMs = 30_000, probe = probePort) {
|
|
76
62
|
const deadline = Date.now() + timeoutMs;
|
|
77
63
|
while (Date.now() < deadline) {
|
|
78
|
-
|
|
64
|
+
const state = await probe(port);
|
|
65
|
+
if (state.state === "xpui")
|
|
79
66
|
return;
|
|
67
|
+
if (state.state === "other")
|
|
68
|
+
throw new Error(portInUseMessage(port, state.what));
|
|
80
69
|
await new Promise((r) => setTimeout(r, 500));
|
|
81
70
|
}
|
|
82
71
|
throw new Error(`timed out waiting for a Spotify xpui debug target on port ${port}. ` +
|
|
@@ -101,11 +90,22 @@ function spawnDetached(cmd, args) {
|
|
|
101
90
|
}
|
|
102
91
|
// launchSpotify reuses a good client, otherwise (re)starts one with the
|
|
103
92
|
// debug port and waits for its xpui target. Returns "reused" or "launched".
|
|
104
|
-
export async function launchSpotify(port, log = console.log, platform = process.platform) {
|
|
105
|
-
|
|
93
|
+
export async function launchSpotify(port, log = console.log, platform = process.platform, probe = probePort) {
|
|
94
|
+
const state = await probe(port);
|
|
95
|
+
if (state.state === "xpui") {
|
|
106
96
|
log(`[dev] reusing the Spotify client already debuggable on port ${port}`);
|
|
107
97
|
return "reused";
|
|
108
98
|
}
|
|
99
|
+
// Killing Spotify would not free the port, so the relaunch could never bind it.
|
|
100
|
+
if (state.state === "other")
|
|
101
|
+
throw new Error(portInUseMessage(port, state.what));
|
|
102
|
+
if (state.state === "unknown")
|
|
103
|
+
throw new Error(portInUseMessage(port, "a process that is not a DevTools endpoint"));
|
|
104
|
+
if (state.state === "spotify") {
|
|
105
|
+
log(`[dev] Spotify is starting on port ${port}, waiting for it`);
|
|
106
|
+
await waitForTarget(port, 30_000, probe);
|
|
107
|
+
return "reused";
|
|
108
|
+
}
|
|
109
109
|
const target = discoverSpotify(platform);
|
|
110
110
|
if (target.kind === "appx")
|
|
111
111
|
throw new Error(APPX_MESSAGE);
|
|
@@ -117,6 +117,6 @@ export async function launchSpotify(port, log = console.log, platform = process.
|
|
|
117
117
|
spawnDetached("open", ["-a", target.app, "--args", portFlag]);
|
|
118
118
|
else
|
|
119
119
|
spawnDetached(target.exe, [portFlag]);
|
|
120
|
-
await waitForTarget(port);
|
|
120
|
+
await waitForTarget(port, 30_000, probe);
|
|
121
121
|
return "launched";
|
|
122
122
|
}
|