spotifyplus 0.1.1 → 0.1.3
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/dev.cjs +336 -0
- package/package.json +8 -1
package/dev.cjs
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// tools/dev-cli.mjs
|
|
27
|
+
var import_node_child_process = require("node:child_process");
|
|
28
|
+
var import_node_fs = __toESM(require("node:fs"), 1);
|
|
29
|
+
var import_node_http = __toESM(require("node:http"), 1);
|
|
30
|
+
var import_node_path = __toESM(require("node:path"), 1);
|
|
31
|
+
var import_node_util = require("node:util");
|
|
32
|
+
var import_esbuild = require("esbuild");
|
|
33
|
+
var execFile = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
34
|
+
var DEFAULT_PORT = 37846;
|
|
35
|
+
var DEFAULT_MODULE_PACKAGE = "com.lenerd.spotifyplus";
|
|
36
|
+
var START_BRIDGE_ACTION = "com.lenerd.spotifyplus.action.START_BRIDGE";
|
|
37
|
+
var HOT_RELOAD_ACTION = "com.lenerd.spotifyplus.action.HOT_RELOAD";
|
|
38
|
+
var WATCH_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx", ".json"]);
|
|
39
|
+
var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".gradle"]);
|
|
40
|
+
function parseArgs(argv) {
|
|
41
|
+
const options = {
|
|
42
|
+
scriptDir: process.cwd(),
|
|
43
|
+
adb: process.env.ADB || "adb",
|
|
44
|
+
device: "",
|
|
45
|
+
port: DEFAULT_PORT,
|
|
46
|
+
modulePackage: DEFAULT_MODULE_PACKAGE,
|
|
47
|
+
spotifyPackage: "com.spotify.music",
|
|
48
|
+
debounceMs: 150,
|
|
49
|
+
help: false
|
|
50
|
+
};
|
|
51
|
+
const positional = [];
|
|
52
|
+
for (let i = 0; i < argv.length; i++) {
|
|
53
|
+
const arg = argv[i];
|
|
54
|
+
if (arg === "--help" || arg === "-h") {
|
|
55
|
+
options.help = true;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (!arg.startsWith("--")) {
|
|
59
|
+
positional.push(arg);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const [rawName, inlineValue] = arg.slice(2).split("=", 2);
|
|
63
|
+
const value = inlineValue ?? argv[++i];
|
|
64
|
+
if (value == null) throw new Error(`Missing value for --${rawName}`);
|
|
65
|
+
switch (rawName) {
|
|
66
|
+
case "adb":
|
|
67
|
+
options.adb = value;
|
|
68
|
+
break;
|
|
69
|
+
case "device":
|
|
70
|
+
options.device = value;
|
|
71
|
+
break;
|
|
72
|
+
case "port":
|
|
73
|
+
options.port = parseInteger(value, "--port");
|
|
74
|
+
break;
|
|
75
|
+
case "module-package":
|
|
76
|
+
options.modulePackage = value;
|
|
77
|
+
break;
|
|
78
|
+
case "spotify-package":
|
|
79
|
+
options.spotifyPackage = value;
|
|
80
|
+
break;
|
|
81
|
+
case "debounce-ms":
|
|
82
|
+
options.debounceMs = parseInteger(value, "--debounce-ms");
|
|
83
|
+
break;
|
|
84
|
+
default:
|
|
85
|
+
throw new Error(`Unknown option --${rawName}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (positional.length > 1) throw new Error("Expected at most one script directory");
|
|
89
|
+
if (positional.length === 1) options.scriptDir = positional[0];
|
|
90
|
+
options.scriptDir = import_node_path.default.resolve(options.scriptDir);
|
|
91
|
+
return options;
|
|
92
|
+
}
|
|
93
|
+
function parseInteger(value, name) {
|
|
94
|
+
const parsed = Number(value);
|
|
95
|
+
if (!Number.isInteger(parsed) || parsed <= 0) throw new Error(`${name} must be a positive integer`);
|
|
96
|
+
return parsed;
|
|
97
|
+
}
|
|
98
|
+
function printHelp() {
|
|
99
|
+
console.log(`Usage: spotifyplus dev [scriptDir] [options]
|
|
100
|
+
|
|
101
|
+
Options:
|
|
102
|
+
--adb <path> adb executable to use (default: adb)
|
|
103
|
+
--device <serial> adb device serial
|
|
104
|
+
--port <port> local HTTP/reverse port (default: ${DEFAULT_PORT})
|
|
105
|
+
--module-package <name> SpotifyPlus manager app package (default: ${DEFAULT_MODULE_PACKAGE})
|
|
106
|
+
--spotify-package <name> Spotify app package, reserved for future checks (default: com.spotify.music)
|
|
107
|
+
--debounce-ms <ms> file change debounce (default: 150)
|
|
108
|
+
--help, -h show this help
|
|
109
|
+
`);
|
|
110
|
+
}
|
|
111
|
+
async function readManifest(scriptDir) {
|
|
112
|
+
const manifestPath = import_node_path.default.join(scriptDir, "manifest.json");
|
|
113
|
+
const text = await import_node_fs.default.promises.readFile(manifestPath, "utf8");
|
|
114
|
+
const manifest = JSON.parse(text);
|
|
115
|
+
if (!manifest || typeof manifest !== "object") throw new Error("manifest.json must contain an object");
|
|
116
|
+
if (typeof manifest.id !== "string" || manifest.id.length === 0) throw new Error("manifest.id must be a non-empty string");
|
|
117
|
+
if (typeof manifest.main !== "string" || manifest.main.length === 0) throw new Error("manifest.main must be a non-empty string");
|
|
118
|
+
return manifest;
|
|
119
|
+
}
|
|
120
|
+
async function bundleScript(scriptDir) {
|
|
121
|
+
const manifest = await readManifest(scriptDir);
|
|
122
|
+
const entryPath = await resolveEntryPath(scriptDir, manifest);
|
|
123
|
+
const result = await (0, import_esbuild.build)({
|
|
124
|
+
entryPoints: [entryPath],
|
|
125
|
+
bundle: true,
|
|
126
|
+
write: false,
|
|
127
|
+
platform: "node",
|
|
128
|
+
format: "cjs",
|
|
129
|
+
target: "es2020",
|
|
130
|
+
jsx: "automatic",
|
|
131
|
+
external: ["spotifyplus", "spotifyplus/*", "react"],
|
|
132
|
+
logLevel: "silent"
|
|
133
|
+
});
|
|
134
|
+
const source = result.outputFiles?.[0]?.text;
|
|
135
|
+
if (!source) throw new Error("esbuild did not produce an output bundle");
|
|
136
|
+
return {
|
|
137
|
+
buildId: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
138
|
+
manifest,
|
|
139
|
+
source
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
async function resolveEntryPath(scriptDir, manifest) {
|
|
143
|
+
const manifestEntry = import_node_path.default.resolve(scriptDir, manifest.main);
|
|
144
|
+
if (import_node_fs.default.existsSync(manifestEntry)) return manifestEntry;
|
|
145
|
+
const candidates = [
|
|
146
|
+
"src/index.tsx",
|
|
147
|
+
"src/index.ts",
|
|
148
|
+
"src/index.jsx",
|
|
149
|
+
"src/index.js",
|
|
150
|
+
"index.tsx",
|
|
151
|
+
"index.ts",
|
|
152
|
+
"index.jsx"
|
|
153
|
+
];
|
|
154
|
+
for (const candidate of candidates) {
|
|
155
|
+
const candidatePath = import_node_path.default.resolve(scriptDir, candidate);
|
|
156
|
+
if (import_node_fs.default.existsSync(candidatePath)) {
|
|
157
|
+
console.warn(`[spotifyplus] manifest.main points to missing ${manifest.main}; using ${candidate} as the dev entry`);
|
|
158
|
+
return candidatePath;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
throw new Error(`Could not resolve ${manifestEntry}. Set manifest.main to a real source file, or add src/index.tsx.`);
|
|
162
|
+
}
|
|
163
|
+
function createReloadServer(state, port) {
|
|
164
|
+
const server = import_node_http.default.createServer((request, response) => {
|
|
165
|
+
const url = new URL(request.url ?? "/", `http://127.0.0.1:${port}`);
|
|
166
|
+
const match = /^\/reload\/([^/]+)\/([^/]+)\.json$/.exec(url.pathname);
|
|
167
|
+
if (!match) {
|
|
168
|
+
response.writeHead(404, { "content-type": "application/json" });
|
|
169
|
+
response.end(JSON.stringify({ error: "not found" }));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const [, buildId, scriptId] = match;
|
|
173
|
+
const current = state.currentBuild;
|
|
174
|
+
if (!current || current.buildId !== buildId || current.manifest.id !== decodeURIComponent(scriptId)) {
|
|
175
|
+
response.writeHead(404, { "content-type": "application/json" });
|
|
176
|
+
response.end(JSON.stringify({ error: "build not found" }));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
response.writeHead(200, {
|
|
180
|
+
"content-type": "application/json; charset=utf-8",
|
|
181
|
+
"cache-control": "no-store"
|
|
182
|
+
});
|
|
183
|
+
response.end(JSON.stringify(current));
|
|
184
|
+
});
|
|
185
|
+
return new Promise((resolve, reject) => {
|
|
186
|
+
server.once("error", reject);
|
|
187
|
+
server.listen(port, "127.0.0.1", () => {
|
|
188
|
+
server.off("error", reject);
|
|
189
|
+
resolve(server);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
async function adb(options, args) {
|
|
194
|
+
const fullArgs = [];
|
|
195
|
+
if (options.device) fullArgs.push("-s", options.device);
|
|
196
|
+
fullArgs.push(...args);
|
|
197
|
+
try {
|
|
198
|
+
return await execFile(options.adb, fullArgs, { windowsHide: true });
|
|
199
|
+
} catch (error) {
|
|
200
|
+
const stderr = error?.stderr ? String(error.stderr).trim() : "";
|
|
201
|
+
const stdout = error?.stdout ? String(error.stdout).trim() : "";
|
|
202
|
+
const details = stderr || stdout || error?.message || "adb command failed";
|
|
203
|
+
throw new Error(`${options.adb} ${fullArgs.join(" ")} failed: ${details}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
async function notifyDevice(options, buildInfo) {
|
|
207
|
+
const encodedScriptId = encodeURIComponent(buildInfo.manifest.id);
|
|
208
|
+
const url = `http://127.0.0.1:${options.port}/reload/${buildInfo.buildId}/${encodedScriptId}.json`;
|
|
209
|
+
await adb(options, ["reverse", `tcp:${options.port}`, `tcp:${options.port}`]);
|
|
210
|
+
await adb(options, [
|
|
211
|
+
"shell",
|
|
212
|
+
"am",
|
|
213
|
+
"broadcast",
|
|
214
|
+
"-a",
|
|
215
|
+
START_BRIDGE_ACTION,
|
|
216
|
+
"-n",
|
|
217
|
+
`${options.modulePackage}/.manager.bridge.BridgeStartReceiver`
|
|
218
|
+
]);
|
|
219
|
+
await adb(options, [
|
|
220
|
+
"shell",
|
|
221
|
+
"am",
|
|
222
|
+
"broadcast",
|
|
223
|
+
"-a",
|
|
224
|
+
HOT_RELOAD_ACTION,
|
|
225
|
+
"-n",
|
|
226
|
+
`${options.modulePackage}/.manager.bridge.HotReloadReceiver`,
|
|
227
|
+
"--es",
|
|
228
|
+
"scriptId",
|
|
229
|
+
buildInfo.manifest.id,
|
|
230
|
+
"--es",
|
|
231
|
+
"buildId",
|
|
232
|
+
buildInfo.buildId,
|
|
233
|
+
"--es",
|
|
234
|
+
"url",
|
|
235
|
+
url
|
|
236
|
+
]);
|
|
237
|
+
return url;
|
|
238
|
+
}
|
|
239
|
+
function shouldWatchFile(filePath) {
|
|
240
|
+
return WATCH_EXTENSIONS.has(import_node_path.default.extname(filePath).toLowerCase());
|
|
241
|
+
}
|
|
242
|
+
function shouldIgnoreDirectory(name) {
|
|
243
|
+
return IGNORED_DIRECTORIES.has(name);
|
|
244
|
+
}
|
|
245
|
+
function watchRecursively(root, onChange) {
|
|
246
|
+
const watchers = [];
|
|
247
|
+
const watchDirectory = (directory) => {
|
|
248
|
+
const watcher = import_node_fs.default.watch(directory, (eventType, fileName) => {
|
|
249
|
+
const changedPath = fileName ? import_node_path.default.join(directory, fileName.toString()) : directory;
|
|
250
|
+
if (shouldWatchFile(changedPath)) onChange(eventType, changedPath);
|
|
251
|
+
import_node_fs.default.promises.stat(changedPath).then((stats) => {
|
|
252
|
+
if (stats.isDirectory() && !shouldIgnoreDirectory(import_node_path.default.basename(changedPath))) {
|
|
253
|
+
watchDirectory(changedPath);
|
|
254
|
+
}
|
|
255
|
+
}).catch(() => {
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
watchers.push(watcher);
|
|
259
|
+
};
|
|
260
|
+
const walk = (directory) => {
|
|
261
|
+
watchDirectory(directory);
|
|
262
|
+
for (const entry of import_node_fs.default.readdirSync(directory, { withFileTypes: true })) {
|
|
263
|
+
if (!entry.isDirectory() || shouldIgnoreDirectory(entry.name)) continue;
|
|
264
|
+
walk(import_node_path.default.join(directory, entry.name));
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
try {
|
|
268
|
+
const watcher = import_node_fs.default.watch(root, { recursive: true }, (eventType, fileName) => {
|
|
269
|
+
const changedPath = fileName ? import_node_path.default.join(root, fileName.toString()) : root;
|
|
270
|
+
if (shouldWatchFile(changedPath)) onChange(eventType, changedPath);
|
|
271
|
+
});
|
|
272
|
+
watchers.push(watcher);
|
|
273
|
+
} catch {
|
|
274
|
+
walk(root);
|
|
275
|
+
}
|
|
276
|
+
return () => {
|
|
277
|
+
for (const watcher of watchers) watcher.close();
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
async function run() {
|
|
281
|
+
const options = parseArgs(process.argv.slice(2));
|
|
282
|
+
if (options.help) {
|
|
283
|
+
printHelp();
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
const state = { currentBuild: null };
|
|
287
|
+
const server = await createReloadServer(state, options.port);
|
|
288
|
+
console.log(`[spotifyplus] dev server listening on http://127.0.0.1:${options.port}`);
|
|
289
|
+
console.log(`[spotifyplus] watching ${options.scriptDir}`);
|
|
290
|
+
let buildInFlight = false;
|
|
291
|
+
let pendingBuild = false;
|
|
292
|
+
const rebuild = async (reason) => {
|
|
293
|
+
if (buildInFlight) {
|
|
294
|
+
pendingBuild = true;
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
buildInFlight = true;
|
|
298
|
+
try {
|
|
299
|
+
const buildInfo = await bundleScript(options.scriptDir);
|
|
300
|
+
state.currentBuild = buildInfo;
|
|
301
|
+
const url = await notifyDevice(options, buildInfo);
|
|
302
|
+
console.log(`[spotifyplus] reloaded ${buildInfo.manifest.id} (${buildInfo.buildId})`);
|
|
303
|
+
console.log(`[spotifyplus] served ${url}`);
|
|
304
|
+
} catch (error) {
|
|
305
|
+
console.error(`[spotifyplus] ${reason} failed`);
|
|
306
|
+
console.error(error?.message ?? error);
|
|
307
|
+
if (Array.isArray(error?.errors)) {
|
|
308
|
+
for (const item of error.errors) console.error(item.text ?? item);
|
|
309
|
+
}
|
|
310
|
+
} finally {
|
|
311
|
+
buildInFlight = false;
|
|
312
|
+
if (pendingBuild) {
|
|
313
|
+
pendingBuild = false;
|
|
314
|
+
await rebuild("queued rebuild");
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
let debounceHandle = null;
|
|
319
|
+
const scheduleRebuild = (_eventType, filePath) => {
|
|
320
|
+
clearTimeout(debounceHandle);
|
|
321
|
+
debounceHandle = setTimeout(() => rebuild(`rebuild after ${import_node_path.default.relative(options.scriptDir, filePath)}`), options.debounceMs);
|
|
322
|
+
};
|
|
323
|
+
const closeWatchers = watchRecursively(options.scriptDir, scheduleRebuild);
|
|
324
|
+
await rebuild("initial build");
|
|
325
|
+
const close = () => {
|
|
326
|
+
closeWatchers();
|
|
327
|
+
server.close();
|
|
328
|
+
process.exit(0);
|
|
329
|
+
};
|
|
330
|
+
process.on("SIGINT", close);
|
|
331
|
+
process.on("SIGTERM", close);
|
|
332
|
+
}
|
|
333
|
+
run().catch((error) => {
|
|
334
|
+
console.error(error?.stack ?? error);
|
|
335
|
+
process.exit(1);
|
|
336
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spotifyplus",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"type": "commonjs",
|
|
5
5
|
"main": "./index.cjs",
|
|
6
6
|
"types": "./index.d.ts",
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
},
|
|
57
57
|
"files": [
|
|
58
58
|
"index.cjs",
|
|
59
|
+
"dev.cjs",
|
|
59
60
|
"index.d.ts",
|
|
60
61
|
"components.js",
|
|
61
62
|
"components.d.ts",
|
|
@@ -66,6 +67,12 @@
|
|
|
66
67
|
"package.json",
|
|
67
68
|
"internal/*.d.ts"
|
|
68
69
|
],
|
|
70
|
+
"bin": {
|
|
71
|
+
"spotifyplus": "./dev.cjs"
|
|
72
|
+
},
|
|
73
|
+
"dependencies": {
|
|
74
|
+
"esbuild": "^0.27.5"
|
|
75
|
+
},
|
|
69
76
|
"typesVersions": {
|
|
70
77
|
"*": {
|
|
71
78
|
"react": [
|