@spicemod/creator 0.0.22 → 0.0.24
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/client.d.ts +47 -0
- package/dist/bin.mjs +834 -342
- package/dist/index.d.mts +697 -0
- package/dist/{client/index.mjs → index.mjs} +1 -1
- package/dist/templates/custom-app/js/react/eslint.config.ts +29 -0
- package/dist/templates/custom-app/js/react/src/app.jsx +22 -0
- package/dist/templates/custom-app/js/react/src/components/Onboarding.jsx +82 -0
- package/dist/templates/custom-app/js/react/src/extension/index.jsx +22 -0
- package/dist/templates/custom-app/meta.json +4 -0
- package/dist/templates/custom-app/shared/DOT-gitignore +34 -0
- package/dist/templates/custom-app/shared/DOT-oxlintrc.json +36 -0
- package/dist/templates/custom-app/shared/README.template.md +53 -0
- package/dist/templates/custom-app/shared/app.css +163 -0
- package/dist/templates/custom-app/shared/biome.json +36 -0
- package/dist/templates/custom-app/shared/css/app.module.scss +58 -0
- package/dist/templates/custom-app/shared/icon-active.svg +7 -0
- package/dist/templates/custom-app/shared/icon.svg +7 -0
- package/dist/templates/custom-app/shared/jsconfig.json +32 -0
- package/dist/templates/custom-app/shared/spice.config.js +10 -0
- package/dist/templates/custom-app/shared/spice.config.ts +10 -0
- package/dist/templates/custom-app/shared/tsconfig.json +32 -0
- package/dist/templates/custom-app/ts/react/eslint.config.ts +29 -0
- package/dist/templates/custom-app/ts/react/src/app.tsx +22 -0
- package/dist/templates/custom-app/ts/react/src/components/Onboarding.tsx +92 -0
- package/dist/templates/custom-app/ts/react/src/extension/index.tsx +27 -0
- package/dist/templates/customAppEntry.js +6 -0
- package/dist/templates/extension/js/vanilla/src/components/Onboarding.js +71 -0
- package/dist/templates/extension/shared/DOT-gitignore +34 -0
- package/dist/templates/extension/shared/DOT-oxlintrc.json +36 -0
- package/dist/templates/extension/shared/spice.config.js +2 -1
- package/dist/templates/extension/shared/spice.config.ts +2 -1
- package/dist/templates/liveReload.js +0 -1
- package/dist/templates/theme/shared/DOT-gitignore +34 -0
- package/dist/templates/theme/shared/DOT-oxlintrc.json +36 -0
- package/dist/templates/theme/shared/spice.config.js +2 -1
- package/dist/templates/theme/shared/spice.config.ts +2 -1
- package/dist/templates/wrapper.js +5 -8
- package/package.json +7 -3
- package/templates/custom-app/js/react/eslint.config.ts +29 -0
- package/templates/custom-app/js/react/src/app.jsx +22 -0
- package/templates/custom-app/js/react/src/components/Onboarding.jsx +82 -0
- package/templates/custom-app/js/react/src/extension/index.jsx +22 -0
- package/templates/custom-app/meta.json +4 -0
- package/templates/custom-app/shared/DOT-gitignore +34 -0
- package/templates/custom-app/shared/DOT-oxlintrc.json +36 -0
- package/templates/custom-app/shared/README.template.md +53 -0
- package/templates/custom-app/shared/app.css +163 -0
- package/templates/custom-app/shared/biome.json +36 -0
- package/templates/custom-app/shared/css/app.module.scss +58 -0
- package/templates/custom-app/shared/icon-active.svg +7 -0
- package/templates/custom-app/shared/icon.svg +7 -0
- package/templates/custom-app/shared/jsconfig.json +32 -0
- package/templates/custom-app/shared/spice.config.js +10 -0
- package/templates/custom-app/shared/spice.config.ts +10 -0
- package/templates/custom-app/shared/tsconfig.json +32 -0
- package/templates/custom-app/ts/react/eslint.config.ts +29 -0
- package/templates/custom-app/ts/react/src/app.tsx +22 -0
- package/templates/custom-app/ts/react/src/components/Onboarding.tsx +92 -0
- package/templates/custom-app/ts/react/src/extension/index.tsx +27 -0
- package/templates/customAppEntry.js +6 -0
- package/templates/extension/js/vanilla/src/components/Onboarding.js +71 -0
- package/templates/extension/shared/DOT-gitignore +34 -0
- package/templates/extension/shared/DOT-oxlintrc.json +36 -0
- package/templates/extension/shared/spice.config.js +2 -1
- package/templates/extension/shared/spice.config.ts +2 -1
- package/templates/liveReload.js +0 -1
- package/templates/theme/shared/DOT-gitignore +34 -0
- package/templates/theme/shared/DOT-oxlintrc.json +36 -0
- package/templates/theme/shared/spice.config.js +2 -1
- package/templates/theme/shared/spice.config.ts +2 -1
- package/templates/wrapper.js +5 -8
- package/dist/client/index.d.mts +0 -2183
package/dist/bin.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { Command, Option } from "commander";
|
|
3
3
|
import * as v from "valibot";
|
|
4
4
|
import path, { basename, dirname, extname, join, relative, resolve } from "node:path";
|
|
5
|
-
import { context, transform } from "esbuild";
|
|
5
|
+
import { build, context, transform } from "esbuild";
|
|
6
6
|
import { createReadStream, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
7
7
|
import { watchConfig } from "c12";
|
|
8
8
|
import { globSync } from "tinyglobby";
|
|
@@ -10,9 +10,11 @@ import { URL as URL$1, fileURLToPath } from "node:url";
|
|
|
10
10
|
import { execSync, spawn, spawnSync } from "node:child_process";
|
|
11
11
|
import readline, { createInterface } from "node:readline";
|
|
12
12
|
import * as p from "@clack/prompts";
|
|
13
|
-
import { cancel, log } from "@clack/prompts";
|
|
13
|
+
import { cancel, log, spinner } from "@clack/prompts";
|
|
14
14
|
import pc from "picocolors";
|
|
15
15
|
import "dotenv/config";
|
|
16
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
17
|
+
import { parse } from "ini";
|
|
16
18
|
import { gzipSync } from "node:zlib";
|
|
17
19
|
import postcssMinify from "@csstools/postcss-minify";
|
|
18
20
|
import autoprefixer from "autoprefixer";
|
|
@@ -20,10 +22,11 @@ import { postcssModules, sassPlugin } from "esbuild-sass-plugin";
|
|
|
20
22
|
import postcss from "postcss";
|
|
21
23
|
import postcssImport from "postcss-import";
|
|
22
24
|
import postcssPresetEnv from "postcss-preset-env";
|
|
23
|
-
import {
|
|
24
|
-
import { parse } from "ini";
|
|
25
|
+
import { createHash } from "node:crypto";
|
|
25
26
|
import { chdir } from "node:process";
|
|
26
27
|
import { lookup } from "node:dns/promises";
|
|
28
|
+
import { mkdir, writeFile as writeFile$1 } from "fs/promises";
|
|
29
|
+
import { dirname as dirname$1, join as join$1 } from "path";
|
|
27
30
|
import { createServer } from "node:http";
|
|
28
31
|
import { WebSocket, WebSocketServer } from "ws";
|
|
29
32
|
|
|
@@ -57,7 +60,11 @@ const toOptions = (metadata) => metadata.map((m) => ({
|
|
|
57
60
|
label: m.title,
|
|
58
61
|
hint: m.description
|
|
59
62
|
}));
|
|
60
|
-
const templateTypes = [
|
|
63
|
+
const templateTypes = [
|
|
64
|
+
"extension",
|
|
65
|
+
"theme",
|
|
66
|
+
"custom-app"
|
|
67
|
+
];
|
|
61
68
|
const templates = templateTypes.map((dir) => {
|
|
62
69
|
const meta_file = dist(`templates/${dir}/meta.json`, import.meta.url);
|
|
63
70
|
const { title, description } = JSON.parse(readFileSync(meta_file, "utf8"));
|
|
@@ -130,12 +137,13 @@ const frameworks = frameworkTypes.map((name) => ({
|
|
|
130
137
|
}));
|
|
131
138
|
const frameworkOptions = toOptions(frameworks);
|
|
132
139
|
const liveReloadFilePath = dist(`templates/liveReload.js`, import.meta.url);
|
|
133
|
-
const
|
|
140
|
+
const templateWrapperFilePath = dist("templates/wrapper.js", import.meta.url);
|
|
141
|
+
const customAppEntryFilePath = dist("templates/customAppEntry.js", import.meta.url);
|
|
134
142
|
|
|
135
143
|
//#endregion
|
|
136
144
|
//#region package.json
|
|
137
145
|
var name = "@spicemod/creator";
|
|
138
|
-
var version = "0.0.
|
|
146
|
+
var version = "0.0.24";
|
|
139
147
|
|
|
140
148
|
//#endregion
|
|
141
149
|
//#region src/utils/common.ts
|
|
@@ -229,57 +237,6 @@ async function installPackages(packageManager, isOnline) {
|
|
|
229
237
|
});
|
|
230
238
|
}
|
|
231
239
|
|
|
232
|
-
//#endregion
|
|
233
|
-
//#region src/config/schema.ts
|
|
234
|
-
const ServerConfigSchema = v.object({
|
|
235
|
-
port: v.optional(v.number()),
|
|
236
|
-
serveDir: v.string(),
|
|
237
|
-
hmrPath: v.optional(v.string())
|
|
238
|
-
});
|
|
239
|
-
const EntryFileSchema = v.string();
|
|
240
|
-
const ThemeEntrySchema = v.object({
|
|
241
|
-
js: EntryFileSchema,
|
|
242
|
-
css: EntryFileSchema
|
|
243
|
-
});
|
|
244
|
-
const TemplateSpecificOptionalSchema = v.variant("template", [v.partial(v.object({
|
|
245
|
-
template: v.literal("extension"),
|
|
246
|
-
entry: EntryFileSchema
|
|
247
|
-
})), v.partial(v.object({
|
|
248
|
-
template: v.literal("theme"),
|
|
249
|
-
entry: ThemeEntrySchema
|
|
250
|
-
}))]);
|
|
251
|
-
const TemplateSpecificSchema = v.variant("template", [v.object({
|
|
252
|
-
template: v.literal("extension"),
|
|
253
|
-
entry: EntryFileSchema
|
|
254
|
-
}), v.object({
|
|
255
|
-
template: v.literal("theme"),
|
|
256
|
-
entry: ThemeEntrySchema
|
|
257
|
-
})]);
|
|
258
|
-
const CommonSchema = v.object({
|
|
259
|
-
name: v.string(),
|
|
260
|
-
outDir: v.string(),
|
|
261
|
-
linter: v.picklist(linterTypes),
|
|
262
|
-
framework: v.picklist(frameworkTypes),
|
|
263
|
-
packageManager: v.picklist(packageManagers),
|
|
264
|
-
esbuildOptions: v.record(v.string(), v.any()),
|
|
265
|
-
devModeVarName: v.optional(v.string()),
|
|
266
|
-
serverConfig: v.partial(ServerConfigSchema),
|
|
267
|
-
version: v.string()
|
|
268
|
-
});
|
|
269
|
-
const FileOptionsSchema = v.intersect([v.partial(CommonSchema), TemplateSpecificOptionalSchema]);
|
|
270
|
-
const OptionsSchema$1 = v.pipe(v.intersect([v.required(CommonSchema), TemplateSpecificSchema]), v.check((input) => !!input.name, "Name is required"));
|
|
271
|
-
|
|
272
|
-
//#endregion
|
|
273
|
-
//#region src/env.ts
|
|
274
|
-
const isInternal = process.env.SPICE_INTERNAL === "true";
|
|
275
|
-
const isDev = process.env.IS_DEV === "true";
|
|
276
|
-
const spicetifyBin = process.env.SPICETIFY_BIN || process.env.SPICE_BIN || "spicetify";
|
|
277
|
-
const env = {
|
|
278
|
-
isInternal,
|
|
279
|
-
isDev,
|
|
280
|
-
spicetifyBin
|
|
281
|
-
};
|
|
282
|
-
|
|
283
240
|
//#endregion
|
|
284
241
|
//#region src/constants.ts
|
|
285
242
|
const GITHUB_LINK = "https://github.com/sanoojes/spicetify-creator";
|
|
@@ -313,6 +270,150 @@ const VALID_PROJECT_FILES = new Set([
|
|
|
313
270
|
"yarnrc.yml",
|
|
314
271
|
".yarn"
|
|
315
272
|
]);
|
|
273
|
+
const CUSTOM_APP_NAME_LOCALES = [
|
|
274
|
+
"ms",
|
|
275
|
+
"gu",
|
|
276
|
+
"ko",
|
|
277
|
+
"pa-IN",
|
|
278
|
+
"az",
|
|
279
|
+
"ru",
|
|
280
|
+
"uk",
|
|
281
|
+
"nb",
|
|
282
|
+
"sv",
|
|
283
|
+
"sw",
|
|
284
|
+
"ur",
|
|
285
|
+
"bho",
|
|
286
|
+
"pa-PK",
|
|
287
|
+
"te",
|
|
288
|
+
"ro",
|
|
289
|
+
"vi",
|
|
290
|
+
"am",
|
|
291
|
+
"bn",
|
|
292
|
+
"en",
|
|
293
|
+
"id",
|
|
294
|
+
"bg",
|
|
295
|
+
"da",
|
|
296
|
+
"es-419",
|
|
297
|
+
"mr",
|
|
298
|
+
"ml",
|
|
299
|
+
"th",
|
|
300
|
+
"tr",
|
|
301
|
+
"is",
|
|
302
|
+
"fa",
|
|
303
|
+
"or",
|
|
304
|
+
"he",
|
|
305
|
+
"hi",
|
|
306
|
+
"zh-TW",
|
|
307
|
+
"sr",
|
|
308
|
+
"pt-BR",
|
|
309
|
+
"zu",
|
|
310
|
+
"nl",
|
|
311
|
+
"es",
|
|
312
|
+
"lt",
|
|
313
|
+
"ja",
|
|
314
|
+
"st",
|
|
315
|
+
"it",
|
|
316
|
+
"el",
|
|
317
|
+
"pt-PT",
|
|
318
|
+
"kn",
|
|
319
|
+
"de",
|
|
320
|
+
"fr",
|
|
321
|
+
"ne",
|
|
322
|
+
"ar",
|
|
323
|
+
"af",
|
|
324
|
+
"et",
|
|
325
|
+
"pl",
|
|
326
|
+
"ta",
|
|
327
|
+
"sl",
|
|
328
|
+
"pk",
|
|
329
|
+
"hr",
|
|
330
|
+
"sk",
|
|
331
|
+
"fi",
|
|
332
|
+
"lv",
|
|
333
|
+
"fil",
|
|
334
|
+
"fr-CA",
|
|
335
|
+
"cs",
|
|
336
|
+
"zh-CN",
|
|
337
|
+
"hu"
|
|
338
|
+
];
|
|
339
|
+
|
|
340
|
+
//#endregion
|
|
341
|
+
//#region src/config/schema.ts
|
|
342
|
+
const ServerConfigSchema = v.object({
|
|
343
|
+
port: v.optional(v.number()),
|
|
344
|
+
serveDir: v.string(),
|
|
345
|
+
hmrPath: v.optional(v.string())
|
|
346
|
+
});
|
|
347
|
+
const EntryFileSchema = v.string();
|
|
348
|
+
const AssetEntrySchema = v.object({
|
|
349
|
+
js: EntryFileSchema,
|
|
350
|
+
css: EntryFileSchema
|
|
351
|
+
});
|
|
352
|
+
const CustomAppEntrySchema = v.object({
|
|
353
|
+
extension: EntryFileSchema,
|
|
354
|
+
app: EntryFileSchema
|
|
355
|
+
});
|
|
356
|
+
const LocaleNameSchema = v.intersect([v.object({ en: v.string() }), v.record(v.picklist(CUSTOM_APP_NAME_LOCALES), v.string())]);
|
|
357
|
+
const ExtensionTemplateSchema = v.object({
|
|
358
|
+
name: v.string(),
|
|
359
|
+
template: v.literal("extension"),
|
|
360
|
+
entry: EntryFileSchema
|
|
361
|
+
});
|
|
362
|
+
const ThemeTemplateSchema = v.object({
|
|
363
|
+
name: v.string(),
|
|
364
|
+
template: v.literal("theme"),
|
|
365
|
+
entry: AssetEntrySchema
|
|
366
|
+
});
|
|
367
|
+
const CustomAppTemplateSchema = v.object({
|
|
368
|
+
name: v.union([v.string(), LocaleNameSchema], "Name must be a string or a translations object containing the required 'en' locale."),
|
|
369
|
+
icon: v.object({
|
|
370
|
+
default: v.string(),
|
|
371
|
+
active: v.optional(v.string())
|
|
372
|
+
}),
|
|
373
|
+
template: v.literal("custom-app"),
|
|
374
|
+
entry: CustomAppEntrySchema
|
|
375
|
+
});
|
|
376
|
+
const TemplateSpecificSchema = v.variant("template", [
|
|
377
|
+
ExtensionTemplateSchema,
|
|
378
|
+
ThemeTemplateSchema,
|
|
379
|
+
CustomAppTemplateSchema
|
|
380
|
+
]);
|
|
381
|
+
const TemplateSpecificOptionalSchema = v.variant("template", [
|
|
382
|
+
v.partial(ExtensionTemplateSchema),
|
|
383
|
+
v.partial(ThemeTemplateSchema),
|
|
384
|
+
v.partial(CustomAppTemplateSchema)
|
|
385
|
+
]);
|
|
386
|
+
const RequiredCommonSchema = v.object({
|
|
387
|
+
outDir: v.string(),
|
|
388
|
+
linter: v.picklist(linterTypes),
|
|
389
|
+
framework: v.picklist(frameworkTypes),
|
|
390
|
+
packageManager: v.picklist(packageManagers),
|
|
391
|
+
esbuildOptions: v.record(v.string(), v.any()),
|
|
392
|
+
serverConfig: v.partial(ServerConfigSchema),
|
|
393
|
+
version: v.string()
|
|
394
|
+
});
|
|
395
|
+
const OptionalCommonSchema = v.object({ devModeVarName: v.optional(v.string()) });
|
|
396
|
+
const FileOptionsSchema = v.intersect([
|
|
397
|
+
v.partial(RequiredCommonSchema),
|
|
398
|
+
OptionalCommonSchema,
|
|
399
|
+
TemplateSpecificOptionalSchema
|
|
400
|
+
]);
|
|
401
|
+
const OptionsSchema$1 = v.intersect([
|
|
402
|
+
v.required(RequiredCommonSchema),
|
|
403
|
+
OptionalCommonSchema,
|
|
404
|
+
TemplateSpecificSchema
|
|
405
|
+
]);
|
|
406
|
+
|
|
407
|
+
//#endregion
|
|
408
|
+
//#region src/env.ts
|
|
409
|
+
const isDev = process.env.IS_DEV === "true";
|
|
410
|
+
const spicetifyBin = process.env.SPICETIFY_BIN || process.env.SPICE_BIN || "spicetify";
|
|
411
|
+
const skipSpicetify = process.env.SPICETIFY_SKIP === "true" || process.env.CI === "true";
|
|
412
|
+
const env = {
|
|
413
|
+
isDev,
|
|
414
|
+
spicetifyBin,
|
|
415
|
+
skipSpicetify
|
|
416
|
+
};
|
|
316
417
|
|
|
317
418
|
//#endregion
|
|
318
419
|
//#region src/utils/logger.ts
|
|
@@ -350,8 +451,15 @@ var Logger = class {
|
|
|
350
451
|
this.add(console.warn, args);
|
|
351
452
|
}
|
|
352
453
|
error(...errors) {
|
|
353
|
-
|
|
354
|
-
for (const err of errors) if (err instanceof Error
|
|
454
|
+
const formatted = [];
|
|
455
|
+
for (const err of errors) if (err instanceof Error) {
|
|
456
|
+
formatted.push(pc.red(err.message));
|
|
457
|
+
if (this.isDev && err.stack) {
|
|
458
|
+
const stack = err.stack.split("\n").slice(1).join("\n");
|
|
459
|
+
formatted.push(pc.dim(stack));
|
|
460
|
+
}
|
|
461
|
+
} else formatted.push(err);
|
|
462
|
+
this.add(console.error, formatted);
|
|
355
463
|
}
|
|
356
464
|
log(...args) {
|
|
357
465
|
if (!this.isDev) return;
|
|
@@ -363,6 +471,10 @@ var Logger = class {
|
|
|
363
471
|
}
|
|
364
472
|
clear() {
|
|
365
473
|
if (!process.stdout.isTTY || process.env.CI) return;
|
|
474
|
+
if (this.isDev) {
|
|
475
|
+
this.log("clear skipped");
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
366
478
|
readline.cursorTo(process.stdout, 0, 0);
|
|
367
479
|
readline.clearScreenDown(process.stdout);
|
|
368
480
|
}
|
|
@@ -371,34 +483,90 @@ const createLogger = (prefix = "", mode) => new Logger(prefix, mode);
|
|
|
371
483
|
const logger$2 = createLogger("common");
|
|
372
484
|
|
|
373
485
|
//#endregion
|
|
374
|
-
//#region src/utils/schema.ts
|
|
375
|
-
|
|
376
|
-
|
|
486
|
+
//#region src/utils/spicetify/schema.ts
|
|
487
|
+
const SpicetifyConfigSchema = v.object({
|
|
488
|
+
Setting: v.object({
|
|
489
|
+
spotify_path: v.string(),
|
|
490
|
+
prefs_path: v.string(),
|
|
491
|
+
inject_theme_js: v.string(),
|
|
492
|
+
inject_css: v.string(),
|
|
493
|
+
current_theme: v.string(),
|
|
494
|
+
color_scheme: v.string(),
|
|
495
|
+
always_enable_devtools: v.string()
|
|
496
|
+
}),
|
|
497
|
+
AdditionalOptions: v.object({
|
|
498
|
+
experimental_features: v.string(),
|
|
499
|
+
extensions: v.pipe(v.string(), v.transform((input) => input.split("|").filter(Boolean))),
|
|
500
|
+
custom_apps: v.string()
|
|
501
|
+
}),
|
|
502
|
+
Backup: v.object({
|
|
503
|
+
version: v.string(),
|
|
504
|
+
with: v.string()
|
|
505
|
+
})
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
//#endregion
|
|
509
|
+
//#region src/utils/spicetify/index.ts
|
|
510
|
+
function runSpice(args) {
|
|
511
|
+
validateSpicetify(env.spicetifyBin);
|
|
512
|
+
return spawnSync(env.spicetifyBin, args, { encoding: "utf-8" });
|
|
513
|
+
}
|
|
514
|
+
const getCustomAppsDir = () => join(getSpiceDataPath(), "CustomApps");
|
|
515
|
+
const getExtensionDir = () => join(getSpiceDataPath(), "Extensions");
|
|
516
|
+
const getThemesDir = () => join(getSpiceDataPath(), "Themes");
|
|
517
|
+
async function getSpicetifyConfig() {
|
|
518
|
+
const { stdout, stderr, error } = runSpice(["path", "-c"]);
|
|
519
|
+
if (error || stderr) throw new Error(`Failed to locate Spicetify config: ${stderr || error?.message}`);
|
|
520
|
+
const rawConfig = parse(await readFile(stdout.trim(), "utf-8"));
|
|
521
|
+
const result = v.safeParse(SpicetifyConfigSchema, rawConfig);
|
|
377
522
|
if (result.success) return result.output;
|
|
378
|
-
|
|
379
|
-
result.issues.forEach((issue) => {
|
|
380
|
-
const path = issue.path?.map((p) => p.key).join(".") || "input";
|
|
381
|
-
logger$2.error(`${pc.dim(" └─")} ${pc.yellow(path)}: ${pc.white(issue.message)}`);
|
|
382
|
-
});
|
|
383
|
-
logger$2.error(`\n${pc.dim("Check your command flags and try again.")}\n`);
|
|
384
|
-
process.exit(1);
|
|
523
|
+
else throw new Error("Spicetify Config Validation Failed:", v.flatten(result.issues).nested);
|
|
385
524
|
}
|
|
525
|
+
function getSpiceDataPath() {
|
|
526
|
+
const { stdout, stderr, error } = runSpice(["path", "userdata"]);
|
|
527
|
+
if (error || stderr) throw new Error(`Failed to locate Spicetify config: ${stderr || error?.message}`);
|
|
528
|
+
return stdout.trim();
|
|
529
|
+
}
|
|
530
|
+
function validateSpicetify(bin) {
|
|
531
|
+
const result = spawnSync(bin, ["--version"], { encoding: "utf-8" });
|
|
532
|
+
if (result.error) throw result.error;
|
|
533
|
+
if (result.status !== 0) throw new Error(`Invalid spicetify binary "${bin}": ${result.stderr || "unknown error"}`);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
//#endregion
|
|
537
|
+
//#region src/config/globs.ts
|
|
538
|
+
const JS_EXTENSIONS = "{ts,tsx,js,jsx,mts,mjs,cts,cjs}";
|
|
539
|
+
const CSS_EXTENSIONS = "{css,scss,sass,less,styl,stylus,pcss,postcss}";
|
|
540
|
+
const withSrc = (dirs) => dirs.flatMap((dir) => [dir, `src/${dir}`]);
|
|
541
|
+
const createGlobs = (names, dirs, ext) => names.flatMap((name) => dirs.map((dir) => `${dir}${name}.${ext}`));
|
|
542
|
+
const JS_ENTRY_GLOBS = createGlobs(["app", "index"], withSrc([""]), JS_EXTENSIONS);
|
|
543
|
+
const JS_EXTENSION_ENTRY_GLOBS = createGlobs(["app", "index"], withSrc(["extension/"]), JS_EXTENSIONS);
|
|
544
|
+
const CSS_ENTRY_GLOBS = createGlobs(["app"], withSrc([
|
|
545
|
+
"",
|
|
546
|
+
"styles/",
|
|
547
|
+
"css/"
|
|
548
|
+
]), CSS_EXTENSIONS);
|
|
549
|
+
const ICON_GLOBS = createGlobs(["icon", "logo"], withSrc([
|
|
550
|
+
"",
|
|
551
|
+
"icons/",
|
|
552
|
+
"assets/"
|
|
553
|
+
]), "svg");
|
|
554
|
+
const ICON_ACTIVE_GLOBS = createGlobs(["icon-active", "logo-active"], withSrc([
|
|
555
|
+
"",
|
|
556
|
+
"icons/",
|
|
557
|
+
"assets/"
|
|
558
|
+
]), "svg");
|
|
559
|
+
const ENTRY_MAP = {
|
|
560
|
+
js: [...JS_ENTRY_GLOBS, ...JS_EXTENSION_ENTRY_GLOBS],
|
|
561
|
+
css: CSS_ENTRY_GLOBS,
|
|
562
|
+
"js-app-only": JS_ENTRY_GLOBS,
|
|
563
|
+
"js-extension-only": JS_EXTENSION_ENTRY_GLOBS
|
|
564
|
+
};
|
|
386
565
|
|
|
387
566
|
//#endregion
|
|
388
567
|
//#region src/config/index.ts
|
|
389
568
|
const logger$1 = createLogger("config");
|
|
390
|
-
|
|
391
|
-
"app.{ts,tsx,js,jsx,mts,mjs,cts,cjs}",
|
|
392
|
-
"extension/app.{ts,tsx,js,jsx,mts,mjs,cts,cjs}",
|
|
393
|
-
"src/app.{ts,tsx,js,jsx,mts,mjs,cts,cjs}",
|
|
394
|
-
"src/extension/app.{ts,tsx,js,jsx,mts,mjs,cts,cjs}"
|
|
395
|
-
];
|
|
396
|
-
const CSS_ENTRY_GLOBS = [
|
|
397
|
-
"app.{css,scss,sass,less,styl,stylus,pcss,postcss}",
|
|
398
|
-
"styles/app.{css,scss,sass,less,styl,stylus,pcss,postcss}",
|
|
399
|
-
"src/app.{css,scss,sass,less,styl,stylus,pcss,postcss}",
|
|
400
|
-
"src/styles/app.{css,scss,sass,less,styl,stylus,pcss,postcss}"
|
|
401
|
-
];
|
|
569
|
+
let previousConfig;
|
|
402
570
|
const CONFIG_DEFAULTS = {
|
|
403
571
|
outDir: "./dist",
|
|
404
572
|
linter: "biome",
|
|
@@ -409,71 +577,133 @@ const CONFIG_DEFAULTS = {
|
|
|
409
577
|
async function loadConfig(cb) {
|
|
410
578
|
let cleanup;
|
|
411
579
|
const runCb = async (config, isUpdate) => {
|
|
580
|
+
if (isUpdate && previousConfig) await cleanupSpicetifyConfig();
|
|
412
581
|
if (typeof cleanup === "function") await cleanup();
|
|
413
582
|
cleanup = await cb(config, isUpdate);
|
|
583
|
+
previousConfig = config;
|
|
414
584
|
};
|
|
415
585
|
const watcher = await watchConfig({
|
|
416
586
|
name: "spice",
|
|
417
587
|
defaults: CONFIG_DEFAULTS,
|
|
418
|
-
configFileRequired:
|
|
588
|
+
configFileRequired: true,
|
|
419
589
|
packageJson: true,
|
|
420
590
|
async onUpdate({ newConfig }) {
|
|
421
|
-
|
|
591
|
+
try {
|
|
592
|
+
await runCb(await getResolvedConfig(newConfig.config, { exitOnError: false }), true);
|
|
593
|
+
} catch {
|
|
594
|
+
logger$1.error(pc.red("Config validation failed, keeping previous configuration"));
|
|
595
|
+
}
|
|
422
596
|
}
|
|
423
597
|
});
|
|
424
598
|
await runCb(await getResolvedConfig(watcher.config), false);
|
|
425
599
|
return watcher;
|
|
426
600
|
}
|
|
427
|
-
async function getResolvedConfig(config) {
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
601
|
+
async function getResolvedConfig(config, { exitOnError = true } = {}) {
|
|
602
|
+
const resolvedContext = await resolveContext(config);
|
|
603
|
+
const result = v.safeParse(OptionsSchema$1, resolvedContext);
|
|
604
|
+
if (result.success) return result.output;
|
|
605
|
+
logger$1.error(pc.red("Failed to load configuration:"));
|
|
606
|
+
result.issues.forEach((issue) => {
|
|
607
|
+
const path = issue.path?.map((p) => p.key).join(".") || "input";
|
|
608
|
+
logger$1.error(`${pc.dim(" └─")} ${pc.yellow(path)}: ${pc.white(issue.message)}`);
|
|
609
|
+
});
|
|
610
|
+
if (exitOnError) process.exit(1);
|
|
611
|
+
throw new Error("Invalid configuration");
|
|
435
612
|
}
|
|
436
613
|
async function resolveContext(config) {
|
|
437
614
|
const cwd = process.cwd();
|
|
438
|
-
const
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
return {};
|
|
443
|
-
}
|
|
444
|
-
};
|
|
445
|
-
const pkg = config.name && config.version ? {} : getPkg();
|
|
446
|
-
if (!config.name) config.name = pkg.name || basename(cwd);
|
|
447
|
-
const DEFAULT_VERSION = "0.0.1";
|
|
448
|
-
if (!config.version) config.version = pkg.version ?? DEFAULT_VERSION;
|
|
449
|
-
if (!config.entry) if (config.template === "theme") {
|
|
450
|
-
config.entry = {
|
|
451
|
-
js: resolveDefaultEntries(cwd, "js"),
|
|
452
|
-
css: resolveDefaultEntries(cwd, "css")
|
|
453
|
-
};
|
|
454
|
-
if (!config.entry.js || config.entry.js.length === 0) config.entry.js = resolveDefaultEntries(cwd, "js");
|
|
455
|
-
if (!config.entry.css || config.entry.css.length === 0) config.entry.css = resolveDefaultEntries(cwd, "css");
|
|
456
|
-
} else config.entry = resolveDefaultEntries(cwd, "js");
|
|
457
|
-
if (!config.devModeVarName) config.devModeVarName = DEV_MODE_VAR_NAME;
|
|
615
|
+
const pkg = config.name && config.version ? {} : getPackageMeta(cwd);
|
|
616
|
+
config.name ||= pkg.name || basename(cwd);
|
|
617
|
+
config.version ||= pkg.version || "0.0.1";
|
|
618
|
+
config.entry ||= resolveConfigEntries(config.template, cwd);
|
|
458
619
|
config.outDir = resolve(cwd, config.outDir || "./dist");
|
|
459
620
|
config.esbuildOptions ??= {};
|
|
460
621
|
config.serverConfig ??= {};
|
|
622
|
+
if (config.template === "custom-app") config.icon ??= {
|
|
623
|
+
default: resolveDefaultIcon(cwd),
|
|
624
|
+
active: resolveActiveIcon(cwd)
|
|
625
|
+
};
|
|
461
626
|
return config;
|
|
462
627
|
}
|
|
463
|
-
function
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
628
|
+
function getPackageMeta(cwd) {
|
|
629
|
+
try {
|
|
630
|
+
return JSON.parse(readFileSync(resolve(cwd, "package.json"), "utf-8"));
|
|
631
|
+
} catch {
|
|
632
|
+
return {};
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
function resolveConfigEntries(template, cwd) {
|
|
636
|
+
if (template === "theme") return {
|
|
637
|
+
js: resolveDefaultEntry(cwd, "js"),
|
|
638
|
+
css: resolveDefaultEntry(cwd, "css")
|
|
639
|
+
};
|
|
640
|
+
if (template === "custom-app") return {
|
|
641
|
+
app: resolveDefaultEntry(cwd, "js-app-only"),
|
|
642
|
+
extension: resolveDefaultEntry(cwd, "js-extension-only")
|
|
473
643
|
};
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
644
|
+
return resolveDefaultEntry(cwd, "js");
|
|
645
|
+
}
|
|
646
|
+
function resolveDefaultEntry(cwd, type) {
|
|
647
|
+
const globs = ENTRY_MAP[type];
|
|
648
|
+
for (const glob of globs) {
|
|
649
|
+
const matches = globSync(glob, {
|
|
650
|
+
cwd,
|
|
651
|
+
absolute: true
|
|
652
|
+
});
|
|
653
|
+
if (matches.length > 0 && matches[0]) return matches[0];
|
|
654
|
+
}
|
|
655
|
+
const displayType = {
|
|
656
|
+
js: "JavaScript/TypeScript",
|
|
657
|
+
css: "CSS/Stylesheet",
|
|
658
|
+
"js-app-only": "JavaScript/TypeScript (Custom App)",
|
|
659
|
+
"js-extension-only": "JavaScript/TypeScript (Extension)"
|
|
660
|
+
}[type];
|
|
661
|
+
const expectedFiles = globs.map((g) => ` - ${g}`).join("\n");
|
|
662
|
+
throw new Error(`No ${displayType} entry file found in your project.\nPlease create one of the following files:\n${expectedFiles}`);
|
|
663
|
+
}
|
|
664
|
+
function resolveDefaultIcon(cwd) {
|
|
665
|
+
for (const glob of ICON_GLOBS) {
|
|
666
|
+
const matches = globSync(glob, {
|
|
667
|
+
cwd,
|
|
668
|
+
absolute: true
|
|
669
|
+
});
|
|
670
|
+
if (matches.length > 0 && matches[0]) return readFileSync(matches[0]).toString();
|
|
671
|
+
}
|
|
672
|
+
const expectedFiles = ICON_GLOBS.map((g) => ` - ${g}`).join("\n");
|
|
673
|
+
throw new Error(`No icon file found in your project.\nPlease create one of the following files:\n${expectedFiles}`);
|
|
674
|
+
}
|
|
675
|
+
function resolveActiveIcon(cwd) {
|
|
676
|
+
for (const glob of ICON_ACTIVE_GLOBS) {
|
|
677
|
+
const matches = globSync(glob, {
|
|
678
|
+
cwd,
|
|
679
|
+
absolute: true
|
|
680
|
+
});
|
|
681
|
+
if (matches.length > 0 && matches[0]) return readFileSync(matches[0]).toString();
|
|
682
|
+
}
|
|
683
|
+
return "";
|
|
684
|
+
}
|
|
685
|
+
const getEnName = (configName) => typeof configName === "string" ? configName : configName.en;
|
|
686
|
+
function getSpiceIdentifier(config) {
|
|
687
|
+
if (config.template === "extension") return `${urlSlugify(config.name)}.js`;
|
|
688
|
+
if (config.template === "custom-app") return urlSlugify(getEnName(config.name));
|
|
689
|
+
return urlSlugify(getEnName(config.name));
|
|
690
|
+
}
|
|
691
|
+
function getSpiceVarName(template) {
|
|
692
|
+
if (template === "extension") return "extensions";
|
|
693
|
+
if (template === "custom-app") return "custom_apps";
|
|
694
|
+
return "current_theme";
|
|
695
|
+
}
|
|
696
|
+
async function cleanupSpicetifyConfig() {
|
|
697
|
+
if (!previousConfig) return;
|
|
698
|
+
const prevIdentifier = getSpiceIdentifier(previousConfig);
|
|
699
|
+
const varName = getSpiceVarName(previousConfig.template);
|
|
700
|
+
logger$1.debug(`Cleaning up previous spicetify config: ${varName} ${prevIdentifier}-`);
|
|
701
|
+
runSpice([
|
|
702
|
+
"config",
|
|
703
|
+
varName,
|
|
704
|
+
`${prevIdentifier}-`
|
|
705
|
+
]);
|
|
706
|
+
runSpice(["apply"]);
|
|
477
707
|
}
|
|
478
708
|
|
|
479
709
|
//#endregion
|
|
@@ -577,14 +807,14 @@ function css({ minify = false, inline = false, logger = createLogger("plugin:css
|
|
|
577
807
|
type,
|
|
578
808
|
transform: postcssModules({
|
|
579
809
|
getJSON: () => {},
|
|
580
|
-
generateScopedName: "[name]__[local]___[hash:base64:5]"
|
|
581
|
-
localsConvention: "camelCaseOnly"
|
|
810
|
+
generateScopedName: "[name]__[local]___[hash:base64:5]"
|
|
582
811
|
}, postCssPlugins)
|
|
583
812
|
}), sassPlugin({
|
|
584
813
|
filter: /\.(s[ac]ss|css)$/,
|
|
585
814
|
type,
|
|
586
815
|
async transform(css, _resolveDir, filePath) {
|
|
587
816
|
const start = performance.now();
|
|
817
|
+
logger.log("processing:", filePath, "type:", type);
|
|
588
818
|
const result = await postcss(postCssPlugins).process(css, { from: filePath });
|
|
589
819
|
logger.debug("Global CSS processed", {
|
|
590
820
|
filePath,
|
|
@@ -615,68 +845,18 @@ const externalGlobal = (externals, namespace = "spicetify-global") => {
|
|
|
615
845
|
};
|
|
616
846
|
};
|
|
617
847
|
|
|
618
|
-
//#endregion
|
|
619
|
-
//#region src/utils/spicetify/schema.ts
|
|
620
|
-
const SpicetifyConfigSchema = v.object({
|
|
621
|
-
Setting: v.object({
|
|
622
|
-
spotify_path: v.string(),
|
|
623
|
-
prefs_path: v.string(),
|
|
624
|
-
inject_theme_js: v.string(),
|
|
625
|
-
inject_css: v.string(),
|
|
626
|
-
current_theme: v.string(),
|
|
627
|
-
color_scheme: v.string(),
|
|
628
|
-
always_enable_devtools: v.string()
|
|
629
|
-
}),
|
|
630
|
-
AdditionalOptions: v.object({
|
|
631
|
-
experimental_features: v.string(),
|
|
632
|
-
extensions: v.pipe(v.string(), v.transform((input) => input.split("|").filter(Boolean))),
|
|
633
|
-
custom_apps: v.string()
|
|
634
|
-
}),
|
|
635
|
-
Backup: v.object({
|
|
636
|
-
version: v.string(),
|
|
637
|
-
with: v.string()
|
|
638
|
-
})
|
|
639
|
-
});
|
|
640
|
-
|
|
641
|
-
//#endregion
|
|
642
|
-
//#region src/utils/spicetify/index.ts
|
|
643
|
-
function runSpice(args) {
|
|
644
|
-
validateSpicetify(env.spicetifyBin);
|
|
645
|
-
return spawnSync(env.spicetifyBin, args, { encoding: "utf-8" });
|
|
646
|
-
}
|
|
647
|
-
const getExtensionDir = () => join(getSpiceDataPath(), "Extensions");
|
|
648
|
-
const getThemesDir = () => join(getSpiceDataPath(), "Themes");
|
|
649
|
-
async function getSpicetifyConfig() {
|
|
650
|
-
const { stdout, stderr, error } = runSpice(["path", "-c"]);
|
|
651
|
-
if (error || stderr) throw new Error(`Failed to locate Spicetify config: ${stderr || error?.message}`);
|
|
652
|
-
const rawConfig = parse(await readFile(stdout.trim(), "utf-8"));
|
|
653
|
-
const result = v.safeParse(SpicetifyConfigSchema, rawConfig);
|
|
654
|
-
if (result.success) return result.output;
|
|
655
|
-
else throw new Error("Spicetify Config Validation Failed:", v.flatten(result.issues).nested);
|
|
656
|
-
}
|
|
657
|
-
function getSpiceDataPath() {
|
|
658
|
-
const { stdout, stderr, error } = runSpice(["path", "userdata"]);
|
|
659
|
-
if (error || stderr) throw new Error(`Failed to locate Spicetify config: ${stderr || error?.message}`);
|
|
660
|
-
return stdout.trim();
|
|
661
|
-
}
|
|
662
|
-
function validateSpicetify(bin) {
|
|
663
|
-
const result = spawnSync(bin, ["--version"], { encoding: "utf-8" });
|
|
664
|
-
if (result.error) throw result.error;
|
|
665
|
-
if (result.status !== 0) throw new Error(`Invalid spicetify binary "${bin}": ${result.stderr || "unknown error"}`);
|
|
666
|
-
}
|
|
667
|
-
|
|
668
848
|
//#endregion
|
|
669
849
|
//#region src/esbuild/plugins/spicetifyHandlers.ts
|
|
670
|
-
const
|
|
671
|
-
const spicetifyHandler = ({ config, options, cache, logger = createLogger("plugin:spicetifyHandler") }) => ({
|
|
850
|
+
const spicetifyHandler = ({ config, options, cache, logger = createLogger("plugin:spicetify-handler") }) => ({
|
|
672
851
|
name: "spice_internal__spicetify-build-handler",
|
|
673
852
|
async setup(build) {
|
|
674
853
|
const { apply = true, copy = true, applyOnce = true, remove, outDir = "./dist" } = options;
|
|
675
854
|
let hasAppliedOnce = false;
|
|
676
855
|
const isExtension = config.template === "extension";
|
|
677
|
-
const
|
|
678
|
-
|
|
679
|
-
|
|
856
|
+
const isCustomApp = config.template === "custom-app";
|
|
857
|
+
const identifier = isExtension ? `${urlSlugify(config.name)}.js` : urlSlugify(getEnName(config.name));
|
|
858
|
+
if (env.skipSpicetify) {
|
|
859
|
+
logger.info(pc.yellow("skipping spicetify operations"));
|
|
680
860
|
build.onEnd(async (result) => {
|
|
681
861
|
if (result.errors.length > 0) return;
|
|
682
862
|
if (!cache.hasChanges || cache.changed.size === 0) return;
|
|
@@ -702,38 +882,36 @@ const spicetifyHandler = ({ config, options, cache, logger = createLogger("plugi
|
|
|
702
882
|
const spiceConfig = await getSpicetifyConfig();
|
|
703
883
|
logger.debug(pc.green("Spicetify Config: "), spiceConfig);
|
|
704
884
|
if (apply) {
|
|
705
|
-
const defaultTheme = spiceConfig
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
]);
|
|
713
|
-
else runSpice([
|
|
714
|
-
"config",
|
|
715
|
-
"current_theme",
|
|
716
|
-
spiceIdentifier
|
|
717
|
-
]);
|
|
718
|
-
});
|
|
885
|
+
const defaultTheme = spiceConfig?.Setting?.current_theme || "SpicetifyDefault";
|
|
886
|
+
const spiceIdentifier = remove ? `${identifier}-` : identifier;
|
|
887
|
+
runSpice([
|
|
888
|
+
"config",
|
|
889
|
+
isExtension ? "extensions" : isCustomApp ? "custom_apps" : "current_theme",
|
|
890
|
+
spiceIdentifier
|
|
891
|
+
]);
|
|
719
892
|
if (!isExtension && !remove) {
|
|
720
|
-
const
|
|
721
|
-
runSpice([
|
|
893
|
+
const cleanup = () => {
|
|
894
|
+
if (isCustomApp) runSpice([
|
|
895
|
+
"config",
|
|
896
|
+
"custom_apps",
|
|
897
|
+
`${identifier}-`
|
|
898
|
+
]);
|
|
899
|
+
else runSpice([
|
|
722
900
|
"config",
|
|
723
901
|
"current_theme",
|
|
724
902
|
defaultTheme
|
|
725
903
|
]);
|
|
726
904
|
process.exit();
|
|
727
905
|
};
|
|
728
|
-
process.once("SIGINT",
|
|
729
|
-
process.once("SIGTERM",
|
|
906
|
+
process.once("SIGINT", cleanup);
|
|
907
|
+
process.once("SIGTERM", cleanup);
|
|
730
908
|
}
|
|
731
909
|
}
|
|
732
910
|
build.onEnd(async (result) => {
|
|
733
911
|
if (result.errors.length > 0) return;
|
|
734
912
|
if (!cache.hasChanges || cache.changed.size === 0) return;
|
|
735
913
|
const destDirs = [resolve(outDir)];
|
|
736
|
-
if (copy) destDirs.push(isExtension ? getExtensionDir() : resolve(getThemesDir(), identifier));
|
|
914
|
+
if (copy) destDirs.push(isExtension ? getExtensionDir() : isCustomApp ? resolve(getCustomAppsDir(), identifier) : resolve(getThemesDir(), identifier));
|
|
737
915
|
const tasks = [];
|
|
738
916
|
for (const filePath of cache.changed) {
|
|
739
917
|
const fileData = cache.files.get(filePath);
|
|
@@ -754,8 +932,8 @@ const spicetifyHandler = ({ config, options, cache, logger = createLogger("plugi
|
|
|
754
932
|
return;
|
|
755
933
|
}
|
|
756
934
|
if (apply && cache.hasChanges && (!applyOnce || !hasAppliedOnce)) {
|
|
757
|
-
const { stderr, status } = runSpice(["apply"]);
|
|
758
|
-
if (status !== 0) logger.error(pc.red(`${CROSS} Spicetify apply failed
|
|
935
|
+
const { stdout, stderr, status } = runSpice(["apply"]);
|
|
936
|
+
if (status !== 0) logger.error(pc.red(`${CROSS} Spicetify apply failed:`), stdout, stderr);
|
|
759
937
|
else hasAppliedOnce = true;
|
|
760
938
|
}
|
|
761
939
|
});
|
|
@@ -764,70 +942,161 @@ const spicetifyHandler = ({ config, options, cache, logger = createLogger("plugi
|
|
|
764
942
|
|
|
765
943
|
//#endregion
|
|
766
944
|
//#region src/esbuild/plugins/wrapWithLoader.ts
|
|
767
|
-
function wrapWithLoader({
|
|
945
|
+
function wrapWithLoader({ config, cache, outFiles, server, dev = false, logger = createLogger("plugin:wrapper") }) {
|
|
768
946
|
const namespace = "spice_internal__wrap-with-loader";
|
|
947
|
+
const name = typeof config.name === "string" ? config.name : config.name.en;
|
|
948
|
+
const { template: type, version } = config;
|
|
949
|
+
let previousManifestHash;
|
|
769
950
|
return {
|
|
770
951
|
name: namespace,
|
|
771
|
-
setup(build) {
|
|
772
|
-
if (build.initialOptions.write !== false) throw new Error(`[${namespace}] This plugin requires "write: false" in build options.`);
|
|
773
|
-
build.onEnd(async (res) => {
|
|
952
|
+
setup(build$3) {
|
|
953
|
+
if (build$3.initialOptions.write !== false) throw new Error(`[${namespace}] This plugin requires "write: false" in build options.`);
|
|
954
|
+
build$3.onEnd(async (res) => {
|
|
774
955
|
try {
|
|
775
956
|
if (res.errors.length > 0 || !res.outputFiles) return;
|
|
776
957
|
cache.changed.clear();
|
|
777
958
|
cache.hasChanges = false;
|
|
778
959
|
const filesChanged = [];
|
|
779
|
-
|
|
780
|
-
|
|
960
|
+
const outdir = resolve(build$3.initialOptions.outdir || "./dist");
|
|
961
|
+
const bundledCss = getBundledCss(res.outputFiles, outdir, type, dev);
|
|
962
|
+
const minify = build$3.initialOptions.minify;
|
|
963
|
+
const slug = varSlugify(`${name}_${version}`);
|
|
781
964
|
const transformPromises = res.outputFiles.map(async (file) => {
|
|
782
965
|
const isJs = file.path.endsWith(".js");
|
|
783
966
|
const isCss = file.path.endsWith(".css");
|
|
784
967
|
if (!dev && isCss && type === "extension") return;
|
|
785
|
-
const
|
|
786
|
-
const
|
|
968
|
+
const relPath = file.path.slice(outdir.length);
|
|
969
|
+
const isCustomAppExtension = type === "custom-app" && isExtensionDir(relPath);
|
|
970
|
+
let targetName;
|
|
971
|
+
if (isJs) targetName = isCustomAppExtension ? outFiles.jsExtension ?? "extension.js" : outFiles.js;
|
|
972
|
+
else if (isCss && !isCustomAppExtension) targetName = outFiles.css;
|
|
973
|
+
if (!targetName) {
|
|
974
|
+
logger.debug("Skipped file: ", file.path);
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
const renamedPath = join(build$3.initialOptions.outdir || "./dist/", targetName);
|
|
978
|
+
if (type === "custom-app" && targetName === outFiles.js && isJs) {
|
|
979
|
+
const globalName = varSlugify(getEnName(config.name));
|
|
980
|
+
const final = (await build({
|
|
981
|
+
bundle: true,
|
|
982
|
+
write: false,
|
|
983
|
+
minify,
|
|
984
|
+
platform: "browser",
|
|
985
|
+
format: "iife",
|
|
986
|
+
globalName,
|
|
987
|
+
stdin: {
|
|
988
|
+
contents: readFileSync(customAppEntryFilePath),
|
|
989
|
+
loader: "tsx",
|
|
990
|
+
sourcefile: "entry.tsx"
|
|
991
|
+
},
|
|
992
|
+
plugins: [plugins.externalGlobal({ react: "Spicetify.React" }), {
|
|
993
|
+
name: "virtual-modules",
|
|
994
|
+
setup(vBuild) {
|
|
995
|
+
vBuild.onResolve({ filter: /^virtual:app$/ }, () => ({
|
|
996
|
+
path: "app",
|
|
997
|
+
namespace: "virtual"
|
|
998
|
+
}));
|
|
999
|
+
vBuild.onLoad({
|
|
1000
|
+
filter: /.*/,
|
|
1001
|
+
namespace: "virtual"
|
|
1002
|
+
}, () => ({
|
|
1003
|
+
contents: file.text,
|
|
1004
|
+
loader: "js"
|
|
1005
|
+
}));
|
|
1006
|
+
}
|
|
1007
|
+
}]
|
|
1008
|
+
})).outputFiles?.[0];
|
|
1009
|
+
if (!final) return;
|
|
1010
|
+
let combinedCode = `${final.text}${dev ? `export default () => ${globalName}.default();` : `var render = () => ${globalName}.default();`}\n`;
|
|
1011
|
+
const nextBuffer = Buffer.from(combinedCode);
|
|
1012
|
+
const existingFile = cache.files.get(renamedPath);
|
|
1013
|
+
const nextHash = final.hash;
|
|
1014
|
+
if (!existingFile || existingFile.hash !== nextHash || config.template === "custom-app") {
|
|
1015
|
+
cache.files.set(renamedPath, {
|
|
1016
|
+
contents: nextBuffer,
|
|
1017
|
+
name: targetName,
|
|
1018
|
+
hash: final.hash
|
|
1019
|
+
});
|
|
1020
|
+
cache.changed.add(renamedPath);
|
|
1021
|
+
cache.hasChanges = true;
|
|
1022
|
+
filesChanged.push(renamedPath);
|
|
1023
|
+
}
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
787
1026
|
if (!isJs) {
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
1027
|
+
const nextBuffer = Buffer.from(file.contents);
|
|
1028
|
+
const existingFile = cache.files.get(renamedPath);
|
|
1029
|
+
const nextHash = file.hash;
|
|
1030
|
+
if (!existingFile || existingFile.hash !== nextHash || config.template === "custom-app") {
|
|
1031
|
+
cache.files.set(renamedPath, {
|
|
1032
|
+
name: targetName,
|
|
1033
|
+
contents: nextBuffer,
|
|
1034
|
+
hash: file.hash
|
|
1035
|
+
});
|
|
1036
|
+
cache.changed.add(renamedPath);
|
|
1037
|
+
cache.hasChanges = true;
|
|
1038
|
+
filesChanged.push(renamedPath);
|
|
1039
|
+
}
|
|
795
1040
|
return;
|
|
796
1041
|
}
|
|
797
|
-
const
|
|
798
|
-
const templateRaw = readFileSync(templateFilePath, "utf-8");
|
|
799
|
-
const minify = build.initialOptions.minify;
|
|
800
|
-
const { code: transformedTemp } = await transform(templateRaw, {
|
|
1042
|
+
const { code: transformedTemp } = await transform(readFileSync(templateWrapperFilePath, "utf-8"), {
|
|
801
1043
|
minify,
|
|
802
|
-
target: build.initialOptions.target || "es2020",
|
|
1044
|
+
target: build$3.initialOptions.target || "es2020",
|
|
803
1045
|
loader: "jsx",
|
|
804
1046
|
define: {
|
|
805
|
-
__ESBUILD__HAS_CSS: JSON.stringify(type
|
|
1047
|
+
__ESBUILD__HAS_CSS: JSON.stringify(type !== "theme"),
|
|
1048
|
+
__ESBUILD__INJECTED_CSS: JSON.stringify(bundledCss),
|
|
806
1049
|
__ESBUILD__APP_SLUG: JSON.stringify(slug),
|
|
807
1050
|
__ESBUILD__APP_TYPE: JSON.stringify(type),
|
|
808
1051
|
__ESBUILD__APP_ID: JSON.stringify(varSlugify(name)),
|
|
809
|
-
__ESBUILD__APP_VERSION: JSON.stringify(version)
|
|
810
|
-
__ESBUILD__APP_HASH: JSON.stringify("")
|
|
1052
|
+
__ESBUILD__APP_VERSION: JSON.stringify(version)
|
|
811
1053
|
}
|
|
812
1054
|
});
|
|
813
1055
|
const template = replace(transformedTemp, {
|
|
814
1056
|
"\"{{INJECT_START_COMMENT}}\"": minify ? "" : "/* --- START --- */",
|
|
815
1057
|
"\"{{INJECT_END_COMMENT}}\"": minify ? "" : "/* --- END --- */",
|
|
816
|
-
"{{INJECTED_CSS_HERE}}": bundledCss,
|
|
817
1058
|
"\"{{INJECTED_JS_HERE}}\"": file.text
|
|
818
1059
|
});
|
|
819
1060
|
const nextBuffer = Buffer.from(template);
|
|
820
|
-
const
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
1061
|
+
const existingFile = cache.files.get(renamedPath);
|
|
1062
|
+
const nextHash = file.hash;
|
|
1063
|
+
if (!existingFile || existingFile.hash !== nextHash || config.template === "custom-app") {
|
|
1064
|
+
cache.files.set(renamedPath, {
|
|
1065
|
+
name: targetName,
|
|
1066
|
+
contents: nextBuffer,
|
|
1067
|
+
hash: file.hash
|
|
1068
|
+
});
|
|
1069
|
+
cache.changed.add(renamedPath);
|
|
1070
|
+
cache.hasChanges = true;
|
|
1071
|
+
filesChanged.push(renamedPath);
|
|
1072
|
+
}
|
|
829
1073
|
});
|
|
830
1074
|
await Promise.all(transformPromises);
|
|
1075
|
+
if (type === "custom-app") {
|
|
1076
|
+
const icon = config.icon;
|
|
1077
|
+
const manifestPath = join(outdir, "manifest.json");
|
|
1078
|
+
const manifest = {
|
|
1079
|
+
name: config.name,
|
|
1080
|
+
subfiles: [],
|
|
1081
|
+
subfiles_extension: ["extension.js"],
|
|
1082
|
+
icon: icon.default,
|
|
1083
|
+
"active-icon": icon.active ?? ""
|
|
1084
|
+
};
|
|
1085
|
+
const manifestString = JSON.stringify(manifest, null, 2);
|
|
1086
|
+
const currentHash = createHash("md5").update(manifestString).digest("hex");
|
|
1087
|
+
if (currentHash !== previousManifestHash) {
|
|
1088
|
+
previousManifestHash = currentHash;
|
|
1089
|
+
const manifestBuffer = Buffer.from(manifestString);
|
|
1090
|
+
cache.files.set(manifestPath, {
|
|
1091
|
+
contents: manifestBuffer,
|
|
1092
|
+
name: "manifest.json",
|
|
1093
|
+
hash: currentHash
|
|
1094
|
+
});
|
|
1095
|
+
cache.changed.add(manifestPath);
|
|
1096
|
+
cache.hasChanges = true;
|
|
1097
|
+
filesChanged.push(manifestPath);
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
831
1100
|
if (filesChanged.length > 0) server?.broadcast(filesChanged);
|
|
832
1101
|
} catch (e) {
|
|
833
1102
|
logger.error(`Error: ${e instanceof Error ? e.message : String(e)}`);
|
|
@@ -836,6 +1105,15 @@ function wrapWithLoader({ name, type, version, cache, outFiles, server, dev = fa
|
|
|
836
1105
|
}
|
|
837
1106
|
};
|
|
838
1107
|
}
|
|
1108
|
+
function isExtensionDir(relPath) {
|
|
1109
|
+
return relPath.startsWith("/extension/") || relPath.startsWith("\\extension\\");
|
|
1110
|
+
}
|
|
1111
|
+
function getBundledCss(files, outdir, type, dev) {
|
|
1112
|
+
const cssFiles = files.filter((f) => f.path.endsWith(".css"));
|
|
1113
|
+
if (!dev && type === "extension") return cssFiles.map((f) => f.text).join("");
|
|
1114
|
+
if (type === "custom-app") return cssFiles.filter((f) => isExtensionDir(f.path.slice(outdir.length))).map((f) => f.text).join("");
|
|
1115
|
+
return "";
|
|
1116
|
+
}
|
|
839
1117
|
|
|
840
1118
|
//#endregion
|
|
841
1119
|
//#region src/esbuild/plugins/index.ts
|
|
@@ -861,11 +1139,12 @@ const defaultBuildOptions = {
|
|
|
861
1139
|
target: ["es2022", "chrome120"]
|
|
862
1140
|
};
|
|
863
1141
|
const getCommonPlugins = (opts) => {
|
|
864
|
-
const { template, minify, cache,
|
|
1142
|
+
const { template, minify, cache, buildOptions, outFiles, server, dev } = opts;
|
|
1143
|
+
const inline = !dev && template === "extension";
|
|
865
1144
|
return [
|
|
866
1145
|
...plugins.css({
|
|
867
1146
|
minify,
|
|
868
|
-
inline
|
|
1147
|
+
inline
|
|
869
1148
|
}),
|
|
870
1149
|
plugins.externalGlobal({
|
|
871
1150
|
react: "Spicetify.React",
|
|
@@ -875,9 +1154,7 @@ const getCommonPlugins = (opts) => {
|
|
|
875
1154
|
"react/jsx-runtime": "Spicetify.ReactJSX"
|
|
876
1155
|
}),
|
|
877
1156
|
plugins.wrapWithLoader({
|
|
878
|
-
|
|
879
|
-
version,
|
|
880
|
-
type: template,
|
|
1157
|
+
config: opts,
|
|
881
1158
|
cache,
|
|
882
1159
|
outFiles,
|
|
883
1160
|
server,
|
|
@@ -891,11 +1168,35 @@ const getCommonPlugins = (opts) => {
|
|
|
891
1168
|
plugins.buildLogger({ cache })
|
|
892
1169
|
];
|
|
893
1170
|
};
|
|
1171
|
+
function getEntryPoints(config) {
|
|
1172
|
+
if (config.template === "theme") return [config.entry.js, config.entry.css];
|
|
1173
|
+
if (config.template === "custom-app") return [config.entry.app, config.entry.extension];
|
|
1174
|
+
return [config.entry];
|
|
1175
|
+
}
|
|
1176
|
+
function getOutFiles(config, isDev = false) {
|
|
1177
|
+
switch (config.template) {
|
|
1178
|
+
case "custom-app": return {
|
|
1179
|
+
js: "index.js",
|
|
1180
|
+
css: "style.css",
|
|
1181
|
+
jsExtension: "extension.js",
|
|
1182
|
+
manifest: "manifest.json"
|
|
1183
|
+
};
|
|
1184
|
+
case "extension": return {
|
|
1185
|
+
js: `${urlSlugify(getEnName(config.name))}.js`,
|
|
1186
|
+
css: isDev ? "app.css" : void 0
|
|
1187
|
+
};
|
|
1188
|
+
case "theme": return {
|
|
1189
|
+
js: "theme.js",
|
|
1190
|
+
css: "user.css"
|
|
1191
|
+
};
|
|
1192
|
+
default: throw new Error("Unknown template");
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
894
1195
|
|
|
895
1196
|
//#endregion
|
|
896
1197
|
//#region src/build/index.ts
|
|
897
1198
|
const logger = createLogger("build");
|
|
898
|
-
async function build$
|
|
1199
|
+
async function build$2(options) {
|
|
899
1200
|
logger.clear();
|
|
900
1201
|
logger.greeting(pc.green("Building for production..."));
|
|
901
1202
|
let ctx;
|
|
@@ -922,10 +1223,7 @@ async function build$1(options) {
|
|
|
922
1223
|
});
|
|
923
1224
|
}
|
|
924
1225
|
function getJSBuildOptions(config, options) {
|
|
925
|
-
const entryPoints = (
|
|
926
|
-
if (config.template === "theme") return [config.entry.js, config.entry.css];
|
|
927
|
-
return [config.entry];
|
|
928
|
-
})();
|
|
1226
|
+
const entryPoints = getEntryPoints(config);
|
|
929
1227
|
const minify = options.watch ? false : options.minify;
|
|
930
1228
|
const outDir = resolve(config.outDir);
|
|
931
1229
|
const cache = {
|
|
@@ -933,14 +1231,13 @@ function getJSBuildOptions(config, options) {
|
|
|
933
1231
|
changed: /* @__PURE__ */ new Set(),
|
|
934
1232
|
hasChanges: true
|
|
935
1233
|
};
|
|
936
|
-
const outFiles =
|
|
937
|
-
js: config.template === "extension" ? `${urlSlugify(config.name)}.js` : "theme.js",
|
|
938
|
-
css: config.template === "theme" ? "user.css" : null
|
|
939
|
-
};
|
|
1234
|
+
const outFiles = getOutFiles(config);
|
|
940
1235
|
const overrides = {
|
|
941
1236
|
...defaultBuildOptions,
|
|
942
1237
|
outdir: outDir,
|
|
1238
|
+
format: "esm",
|
|
943
1239
|
minify,
|
|
1240
|
+
globalName: varSlugify(getEnName(config.name)),
|
|
944
1241
|
sourcemap: false,
|
|
945
1242
|
external: [
|
|
946
1243
|
...config.esbuildOptions?.external ? config.esbuildOptions.external : [],
|
|
@@ -949,7 +1246,7 @@ function getJSBuildOptions(config, options) {
|
|
|
949
1246
|
],
|
|
950
1247
|
define: {
|
|
951
1248
|
[DEV_MODE_VAR_NAME]: "false",
|
|
952
|
-
[config.devModeVarName]: "false",
|
|
1249
|
+
...config.devModeVarName ? { [config.devModeVarName]: "false" } : {},
|
|
953
1250
|
...config.esbuildOptions.define
|
|
954
1251
|
},
|
|
955
1252
|
plugins: [...config.esbuildOptions?.plugins ? config.esbuildOptions.plugins : [], ...getCommonPlugins({
|
|
@@ -973,6 +1270,20 @@ function getJSBuildOptions(config, options) {
|
|
|
973
1270
|
};
|
|
974
1271
|
}
|
|
975
1272
|
|
|
1273
|
+
//#endregion
|
|
1274
|
+
//#region src/utils/schema.ts
|
|
1275
|
+
function safeParse(schema, data, type = "CLI") {
|
|
1276
|
+
const result = v.safeParse(schema, data);
|
|
1277
|
+
if (result.success) return result.output;
|
|
1278
|
+
logger$2.error(`\n${pc.bgRed(pc.black(" ERROR "))} ${pc.red(`Invalid ${type} options:`)}`);
|
|
1279
|
+
result.issues.forEach((issue) => {
|
|
1280
|
+
const path = issue.path?.map((p) => p.key).join(".") || "input";
|
|
1281
|
+
logger$2.error(`${pc.dim(" └─")} ${pc.yellow(path)}: ${pc.white(issue.message)}`);
|
|
1282
|
+
});
|
|
1283
|
+
logger$2.error(`\n${pc.dim("Check your command flags and try again.")}\n`);
|
|
1284
|
+
process.exit(1);
|
|
1285
|
+
}
|
|
1286
|
+
|
|
976
1287
|
//#endregion
|
|
977
1288
|
//#region src/commands/build.ts
|
|
978
1289
|
const CLIOptionsSchema$1 = v.strictObject({
|
|
@@ -981,8 +1292,8 @@ const CLIOptionsSchema$1 = v.strictObject({
|
|
|
981
1292
|
apply: v.boolean(),
|
|
982
1293
|
copy: v.boolean()
|
|
983
1294
|
});
|
|
984
|
-
const build = new Command("build").description("Build your spicetify project").option("-a, --apply", "Apply to spicetify", false).option("-w, --watch", "Watch mode", false).option("--no-copy", "Do not copy files to spicetify").option("--no-minify", "Disable code minification").action(async (opts) => {
|
|
985
|
-
await build$
|
|
1295
|
+
const build$1 = new Command("build").description("Build your spicetify project").option("-a, --apply", "Apply to spicetify", false).option("-w, --watch", "Watch mode", false).option("--no-copy", "Do not copy files to spicetify").option("--no-minify", "Disable code minification").action(async (opts) => {
|
|
1296
|
+
await build$2(safeParse(CLIOptionsSchema$1, opts));
|
|
986
1297
|
});
|
|
987
1298
|
|
|
988
1299
|
//#endregion
|
|
@@ -1040,10 +1351,11 @@ function createPackageJSON(options) {
|
|
|
1040
1351
|
scripts: {
|
|
1041
1352
|
sc: "spicetify-creator",
|
|
1042
1353
|
dev: "spicetify-creator dev",
|
|
1043
|
-
build: "spicetify-creator build"
|
|
1354
|
+
build: "spicetify-creator build",
|
|
1355
|
+
"update-types": "spicetify-creator update-types"
|
|
1044
1356
|
},
|
|
1045
1357
|
dependencies: {},
|
|
1046
|
-
devDependencies: { "@spicetify/creator":
|
|
1358
|
+
devDependencies: { "@spicetify/creator": "latest" }
|
|
1047
1359
|
};
|
|
1048
1360
|
if (options.language === "ts") result.peerDependencies = {
|
|
1049
1361
|
...result.peerDependencies,
|
|
@@ -1085,9 +1397,10 @@ function validateProjectName(name) {
|
|
|
1085
1397
|
|
|
1086
1398
|
//#endregion
|
|
1087
1399
|
//#region src/create/template.ts
|
|
1088
|
-
const ext = (lang) => lang === "ts" ?
|
|
1400
|
+
const ext = (lang, jsx = false) => lang === "ts" ? `ts${jsx ? "x" : ""}` : `js${jsx ? "x" : ""}`;
|
|
1089
1401
|
const kv = ({ name, language, framework, linter, packageManager, template }) => ({
|
|
1090
1402
|
"{{project-name}}": name,
|
|
1403
|
+
"{{project-url}}": `/${urlSlugify(name)}`,
|
|
1091
1404
|
"{{framework}}": framework,
|
|
1092
1405
|
"{{linter}}": linter,
|
|
1093
1406
|
"{{package-manager}}": packageManager,
|
|
@@ -1104,36 +1417,60 @@ const kv = ({ name, language, framework, linter, packageManager, template }) =>
|
|
|
1104
1417
|
const action = { modify(c, opts) {
|
|
1105
1418
|
return replace(c, kv(opts));
|
|
1106
1419
|
} };
|
|
1107
|
-
const
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1420
|
+
const SHARED_FILES = (opts) => {
|
|
1421
|
+
const isShared = true;
|
|
1422
|
+
const files = [
|
|
1423
|
+
{
|
|
1424
|
+
from: "README.template.md",
|
|
1425
|
+
to: "README.md",
|
|
1426
|
+
action,
|
|
1427
|
+
isShared
|
|
1428
|
+
},
|
|
1429
|
+
{
|
|
1430
|
+
from: "DOT-gitignore",
|
|
1431
|
+
to: ".gitignore",
|
|
1432
|
+
isShared: true
|
|
1433
|
+
},
|
|
1434
|
+
{
|
|
1435
|
+
from: `spice.config.${ext(opts.language)}`,
|
|
1436
|
+
to: `spice.config.${ext(opts.language)}`,
|
|
1437
|
+
action,
|
|
1438
|
+
isShared
|
|
1439
|
+
}
|
|
1440
|
+
];
|
|
1441
|
+
if (opts.template === "custom-app") {
|
|
1442
|
+
files.push({
|
|
1443
|
+
from: `css/app.module.scss`,
|
|
1444
|
+
to: `src/css/app.module.scss`,
|
|
1445
|
+
action,
|
|
1446
|
+
isShared
|
|
1447
|
+
});
|
|
1448
|
+
files.push({
|
|
1449
|
+
from: `icon.svg`,
|
|
1450
|
+
to: `src/icon.svg`,
|
|
1451
|
+
action,
|
|
1452
|
+
isShared
|
|
1453
|
+
});
|
|
1454
|
+
files.push({
|
|
1455
|
+
from: `icon-active.svg`,
|
|
1456
|
+
to: `src/icon-active.svg`,
|
|
1457
|
+
action,
|
|
1458
|
+
isShared
|
|
1459
|
+
});
|
|
1460
|
+
files.push({
|
|
1461
|
+
from: "app.css",
|
|
1462
|
+
to: "src/extension/app.css",
|
|
1463
|
+
action,
|
|
1464
|
+
isShared
|
|
1465
|
+
});
|
|
1466
|
+
} else files.push({
|
|
1126
1467
|
from: "app.css",
|
|
1127
1468
|
to: "src/app.css",
|
|
1128
1469
|
action,
|
|
1129
|
-
isShared
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
to: "src/types/css.d.ts",
|
|
1134
|
-
isShared: true
|
|
1135
|
-
}] : []
|
|
1136
|
-
];
|
|
1470
|
+
isShared
|
|
1471
|
+
});
|
|
1472
|
+
return files;
|
|
1473
|
+
};
|
|
1137
1474
|
const LANGUAGE_FILES = {
|
|
1138
1475
|
js: [{
|
|
1139
1476
|
from: "jsconfig.json",
|
|
@@ -1147,24 +1484,36 @@ const LANGUAGE_FILES = {
|
|
|
1147
1484
|
}]
|
|
1148
1485
|
};
|
|
1149
1486
|
const FRAMEWORKS = {
|
|
1150
|
-
react: ({ language }) =>
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
}
|
|
1487
|
+
react: ({ language, template }) => {
|
|
1488
|
+
const react = [{
|
|
1489
|
+
from: `src/app.${ext(language, true)}`,
|
|
1490
|
+
to: `src/app.${ext(language, true)}`,
|
|
1491
|
+
action
|
|
1492
|
+
}, {
|
|
1493
|
+
from: `src/components/Onboarding.${ext(language, true)}`,
|
|
1494
|
+
to: `src/components/Onboarding.${ext(language, true)}`,
|
|
1495
|
+
action
|
|
1496
|
+
}];
|
|
1497
|
+
if (template === "custom-app") react.push({
|
|
1498
|
+
from: `src/extension/index.${ext(language, true)}`,
|
|
1499
|
+
to: `src/extension/index.${ext(language, true)}`,
|
|
1500
|
+
action
|
|
1501
|
+
});
|
|
1502
|
+
return react;
|
|
1503
|
+
},
|
|
1504
|
+
vanilla: ({ language, template }) => {
|
|
1505
|
+
const vanilla = [{
|
|
1506
|
+
from: `src/app.${ext(language)}`,
|
|
1507
|
+
to: `src/app.${ext(language)}`,
|
|
1508
|
+
action
|
|
1509
|
+
}, {
|
|
1510
|
+
from: `src/components/Onboarding.${ext(language)}`,
|
|
1511
|
+
to: `src/components/Onboarding.${ext(language)}`,
|
|
1512
|
+
action
|
|
1513
|
+
}];
|
|
1514
|
+
if (template === "custom-app") throw new Error("vanilla doesn't exist for custom-app");
|
|
1515
|
+
return vanilla;
|
|
1516
|
+
}
|
|
1168
1517
|
};
|
|
1169
1518
|
const LINTERS = {
|
|
1170
1519
|
biome: [{
|
|
@@ -1177,23 +1526,25 @@ const LINTERS = {
|
|
|
1177
1526
|
to: `eslint.config.${ext(language)}`
|
|
1178
1527
|
}],
|
|
1179
1528
|
oxlint: [{
|
|
1180
|
-
from: "
|
|
1529
|
+
from: "DOT-oxlintrc.json",
|
|
1181
1530
|
to: ".oxlintrc.json",
|
|
1182
1531
|
isShared: true
|
|
1183
1532
|
}]
|
|
1184
1533
|
};
|
|
1534
|
+
const getFiles = (options) => {
|
|
1535
|
+
const resolve = (slice) => typeof slice === "function" ? slice(options) : slice ?? [];
|
|
1536
|
+
return [
|
|
1537
|
+
...resolve(SHARED_FILES),
|
|
1538
|
+
...resolve(LANGUAGE_FILES[options.language]),
|
|
1539
|
+
...resolve(FRAMEWORKS[options.framework]),
|
|
1540
|
+
...resolve(LINTERS[options.linter])
|
|
1541
|
+
];
|
|
1542
|
+
};
|
|
1185
1543
|
function setupTemplateFiles(options, targetDir) {
|
|
1186
|
-
const { template, language, framework
|
|
1544
|
+
const { template, language, framework } = options;
|
|
1187
1545
|
const templateRoot = dist(`templates/${template}`, import.meta.url);
|
|
1188
1546
|
const fromDir = join(templateRoot, language, framework);
|
|
1189
|
-
|
|
1190
|
-
const files = [
|
|
1191
|
-
...resolve(COMMON_FILES),
|
|
1192
|
-
...resolve(LANGUAGE_FILES[language]),
|
|
1193
|
-
...resolve(FRAMEWORKS[framework]),
|
|
1194
|
-
...resolve(LINTERS[linter])
|
|
1195
|
-
];
|
|
1196
|
-
for (const file of files) {
|
|
1547
|
+
for (const file of getFiles(options)) {
|
|
1197
1548
|
const src = (() => {
|
|
1198
1549
|
if (file.isGlobal) return join(templateRoot, file.from);
|
|
1199
1550
|
if (file.isShared) return join(templateRoot, "shared", file.from);
|
|
@@ -1291,6 +1642,36 @@ function tryGitInit(root) {
|
|
|
1291
1642
|
}
|
|
1292
1643
|
}
|
|
1293
1644
|
|
|
1645
|
+
//#endregion
|
|
1646
|
+
//#region src/utils/update-types.ts
|
|
1647
|
+
const downloads = { spicetify: {
|
|
1648
|
+
from: "https://raw.githubusercontent.com/spicetify/cli/main/globals.d.ts",
|
|
1649
|
+
to: "./src/types/spicetify.d.ts",
|
|
1650
|
+
action: (content) => content.replace("const React: any;", "const React: typeof import(\"react\");").replace("const ReactDOM: any;", "const ReactDOM: typeof import(\"react-dom/client\");").replace("const ReactDOMServer: any;", "const ReactDOMServer: typeof import(\"react-dom/server\");")
|
|
1651
|
+
} };
|
|
1652
|
+
async function updateTypes(isUpdating = true, cwd = process.cwd()) {
|
|
1653
|
+
const s = spinner();
|
|
1654
|
+
s.start(`${isUpdating ? "Updating" : "Creating"} Types...`);
|
|
1655
|
+
await Promise.all(Object.entries(downloads).map(([name, download]) => downloadFile(name, download, isUpdating, cwd)));
|
|
1656
|
+
s.stop(`${isUpdating ? "Updated" : "Created"} Types!`);
|
|
1657
|
+
}
|
|
1658
|
+
async function downloadFile(name, { from, to, action }, isUpdating, cwd) {
|
|
1659
|
+
try {
|
|
1660
|
+
const res = await fetch(from);
|
|
1661
|
+
if (!res.ok) throw new Error(`HTTP Error: ${res.status} ${res.statusText}`);
|
|
1662
|
+
let text = await res.text();
|
|
1663
|
+
if (action) text = await action(text);
|
|
1664
|
+
const fullPath = join$1(cwd, to);
|
|
1665
|
+
await mkdir(dirname$1(fullPath), { recursive: true });
|
|
1666
|
+
await writeFile$1(fullPath, text, "utf8");
|
|
1667
|
+
const actionLog = isUpdating ? "updated" : "created";
|
|
1668
|
+
logger$2.log(`${name}.d.ts ${actionLog} (${from} -> ${fullPath})`);
|
|
1669
|
+
} catch (e) {
|
|
1670
|
+
log.error(`${name} failed`);
|
|
1671
|
+
console.error(e);
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1294
1675
|
//#endregion
|
|
1295
1676
|
//#region src/create/index.ts
|
|
1296
1677
|
const isOnline = await getOnline();
|
|
@@ -1337,7 +1718,8 @@ async function createProject(cwd, options) {
|
|
|
1337
1718
|
options: templateOptions
|
|
1338
1719
|
});
|
|
1339
1720
|
},
|
|
1340
|
-
framework: async () => {
|
|
1721
|
+
framework: async ({ results: { template } }) => {
|
|
1722
|
+
if (template === "custom-app") return "react";
|
|
1341
1723
|
if (options.framework) return options.framework;
|
|
1342
1724
|
return await p.select({
|
|
1343
1725
|
message: "Select which framework you want to chose",
|
|
@@ -1407,6 +1789,7 @@ async function create$1(cwd, options) {
|
|
|
1407
1789
|
try {
|
|
1408
1790
|
mkdirp(cwd);
|
|
1409
1791
|
setupTemplateFiles(options, cwd);
|
|
1792
|
+
await updateTypes(false, cwd);
|
|
1410
1793
|
const pkgJSON = createPackageJSON(options);
|
|
1411
1794
|
writePackageJSON(pkgJSON, cwd);
|
|
1412
1795
|
chdir(cwd);
|
|
@@ -1587,7 +1970,12 @@ async function createHmrServer(config, logger = createLogger("hmrServer")) {
|
|
|
1587
1970
|
resolve();
|
|
1588
1971
|
});
|
|
1589
1972
|
}),
|
|
1590
|
-
stop: () => new Promise((resolve, reject) => {
|
|
1973
|
+
stop: async () => new Promise((resolve, reject) => {
|
|
1974
|
+
if (!isRunning) return resolve();
|
|
1975
|
+
for (const client of clients) client.terminate();
|
|
1976
|
+
clients.clear();
|
|
1977
|
+
wss.close();
|
|
1978
|
+
if ("closeAllConnections" in httpServer) httpServer.closeAllConnections();
|
|
1591
1979
|
httpServer.close((err) => {
|
|
1592
1980
|
if (err) return reject(err);
|
|
1593
1981
|
isRunning = false;
|
|
@@ -1617,7 +2005,7 @@ const injectHMRExtension = async (rootLink, wsLink, outFiles) => {
|
|
|
1617
2005
|
const extName = `sc-live-reload-helper.js`;
|
|
1618
2006
|
const spiceConfig = await getSpicetifyConfig();
|
|
1619
2007
|
const cleanup = () => {
|
|
1620
|
-
|
|
2008
|
+
logger$2.info(`Removing Live reload extension...`);
|
|
1621
2009
|
try {
|
|
1622
2010
|
runSpice([
|
|
1623
2011
|
"config",
|
|
@@ -1625,18 +2013,16 @@ const injectHMRExtension = async (rootLink, wsLink, outFiles) => {
|
|
|
1625
2013
|
`${extName}-`
|
|
1626
2014
|
]);
|
|
1627
2015
|
runSpice(["apply"]);
|
|
1628
|
-
logger$2.
|
|
2016
|
+
logger$2.info(pc.green(`${CHECK} Cleanup successful.`));
|
|
1629
2017
|
} catch (e) {
|
|
1630
|
-
|
|
2018
|
+
logger$2.error(pc.red(`${CROSS} Cleanup failed: `), e);
|
|
1631
2019
|
}
|
|
1632
2020
|
process.exit();
|
|
1633
2021
|
};
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
process.on("SIGTERM", cleanup);
|
|
1637
|
-
}
|
|
2022
|
+
process.on("SIGINT", cleanup);
|
|
2023
|
+
process.on("SIGTERM", cleanup);
|
|
1638
2024
|
try {
|
|
1639
|
-
logger$2.debug(`
|
|
2025
|
+
logger$2.debug(`Preparing Live reload extension...`);
|
|
1640
2026
|
const destDir = getExtensionDir();
|
|
1641
2027
|
mkdirp(destDir);
|
|
1642
2028
|
const outDir = resolve(destDir, extName);
|
|
@@ -1663,6 +2049,114 @@ const injectHMRExtension = async (rootLink, wsLink, outFiles) => {
|
|
|
1663
2049
|
logger$2.error(pc.red(`${CROSS} Failed to inject HMR helper: ${err instanceof Error ? err.message : String(err)}`));
|
|
1664
2050
|
}
|
|
1665
2051
|
};
|
|
2052
|
+
const injectHMRCustomApp = async (rootLink, wsLink, outFiles, config) => {
|
|
2053
|
+
if (config.template !== "custom-app") throw new Error("only supports custom-app templates");
|
|
2054
|
+
const identifier = getEnName(config.name);
|
|
2055
|
+
const spiceConfig = await getSpicetifyConfig();
|
|
2056
|
+
const customAppId = urlSlugify(identifier);
|
|
2057
|
+
runSpice([
|
|
2058
|
+
"config",
|
|
2059
|
+
"extensions",
|
|
2060
|
+
"sc-live-reload-helper.js-"
|
|
2061
|
+
]);
|
|
2062
|
+
const cleanup = () => {
|
|
2063
|
+
logger$2.info(`Removing Live reload custom-app...`);
|
|
2064
|
+
try {
|
|
2065
|
+
runSpice([
|
|
2066
|
+
"config",
|
|
2067
|
+
"custom_apps",
|
|
2068
|
+
`${customAppId}-`
|
|
2069
|
+
]);
|
|
2070
|
+
runSpice(["apply"]);
|
|
2071
|
+
logger$2.info(pc.green(`${CHECK} Cleanup successful.`));
|
|
2072
|
+
} catch (e) {
|
|
2073
|
+
logger$2.error(pc.red(`${CROSS} Cleanup failed: `), e);
|
|
2074
|
+
}
|
|
2075
|
+
process.exit();
|
|
2076
|
+
};
|
|
2077
|
+
process.on("SIGINT", cleanup);
|
|
2078
|
+
process.on("SIGTERM", cleanup);
|
|
2079
|
+
try {
|
|
2080
|
+
logger$2.debug(`Preparing Live reload custom-app...`);
|
|
2081
|
+
const destDir = resolve(getCustomAppsDir(), customAppId);
|
|
2082
|
+
mkdirp(destDir);
|
|
2083
|
+
const indexJsPath = resolve(destDir, "index.js");
|
|
2084
|
+
const extensionJsPath = resolve(destDir, outFiles.jsExtension ?? "extension.js");
|
|
2085
|
+
writeFileSync(indexJsPath, `const React = Spicetify.React;
|
|
2086
|
+
const waitForImport = () => {
|
|
2087
|
+
return import("${rootLink}/files/${outFiles.js}")
|
|
2088
|
+
.then((mod) => mod.default || mod.render)
|
|
2089
|
+
.catch((err) => {
|
|
2090
|
+
console.error("Failed to import app:", err);
|
|
2091
|
+
return null;
|
|
2092
|
+
});
|
|
2093
|
+
};
|
|
2094
|
+
|
|
2095
|
+
const AppWrapper = ({ appPromise }) => {
|
|
2096
|
+
const [App, setApp] = React.useState(null);
|
|
2097
|
+
|
|
2098
|
+
React.useEffect(() => {
|
|
2099
|
+
let mounted = true;
|
|
2100
|
+
|
|
2101
|
+
appPromise.then((app) => {
|
|
2102
|
+
if (mounted && app) {
|
|
2103
|
+
setApp(() => app);
|
|
2104
|
+
}
|
|
2105
|
+
});
|
|
2106
|
+
|
|
2107
|
+
return () => {
|
|
2108
|
+
mounted = false;
|
|
2109
|
+
};
|
|
2110
|
+
}, [appPromise]);
|
|
2111
|
+
|
|
2112
|
+
if (!App) {
|
|
2113
|
+
return React.createElement(
|
|
2114
|
+
"div",
|
|
2115
|
+
{ className: "loading" },
|
|
2116
|
+
"Loading app..."
|
|
2117
|
+
);
|
|
2118
|
+
}
|
|
2119
|
+
|
|
2120
|
+
return React.createElement(App);
|
|
2121
|
+
};
|
|
2122
|
+
|
|
2123
|
+
const render = () => {
|
|
2124
|
+
const appPromise = waitForImport();
|
|
2125
|
+
return React.createElement(AppWrapper, { appPromise });
|
|
2126
|
+
};
|
|
2127
|
+
`);
|
|
2128
|
+
const { code: extensionCode } = await transform(readFileSync(liveReloadFilePath, "utf8"), {
|
|
2129
|
+
loader: "js",
|
|
2130
|
+
define: {
|
|
2131
|
+
_SERVER_URL: JSON.stringify(rootLink),
|
|
2132
|
+
_HOT_RELOAD_LINK: JSON.stringify(wsLink),
|
|
2133
|
+
_JS_PATH: JSON.stringify(`/files/${outFiles.jsExtension ?? "extension.js"}`),
|
|
2134
|
+
_CSS_PATH: JSON.stringify(outFiles.css ? `/files/${outFiles.css}` : `/files/app.css`)
|
|
2135
|
+
}
|
|
2136
|
+
});
|
|
2137
|
+
writeFileSync(extensionJsPath, extensionCode);
|
|
2138
|
+
const manifestPath = resolve(destDir, "manifest.json");
|
|
2139
|
+
const manifest = {
|
|
2140
|
+
name: config.name,
|
|
2141
|
+
subfiles: [],
|
|
2142
|
+
subfiles_extension: [outFiles.jsExtension ?? "extension.js"],
|
|
2143
|
+
icon: config.icon.default,
|
|
2144
|
+
"active-icon": config.icon.active ?? config.icon.default
|
|
2145
|
+
};
|
|
2146
|
+
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
2147
|
+
if (spiceConfig) {
|
|
2148
|
+
runSpice([
|
|
2149
|
+
"config",
|
|
2150
|
+
"custom_apps",
|
|
2151
|
+
customAppId
|
|
2152
|
+
]);
|
|
2153
|
+
runSpice(["apply"]);
|
|
2154
|
+
}
|
|
2155
|
+
logger$2.debug(pc.green(`${CHECK} Live reload custom-app injected successfully.`));
|
|
2156
|
+
} catch (err) {
|
|
2157
|
+
logger$2.error(pc.red(`${CROSS} Failed to inject HMR custom-app: ${err instanceof Error ? err.message : String(err)}`));
|
|
2158
|
+
}
|
|
2159
|
+
};
|
|
1666
2160
|
|
|
1667
2161
|
//#endregion
|
|
1668
2162
|
//#region src/dev/index.ts
|
|
@@ -1673,7 +2167,7 @@ async function dev$1(options) {
|
|
|
1673
2167
|
logger$2.greeting(pc.green("Starting development environment"));
|
|
1674
2168
|
let ctx;
|
|
1675
2169
|
let server = void 0;
|
|
1676
|
-
loadConfig(async (config, isNewUpdate) => {
|
|
2170
|
+
await loadConfig(async (config, isNewUpdate) => {
|
|
1677
2171
|
if (isNewUpdate) {
|
|
1678
2172
|
logger$2.clear();
|
|
1679
2173
|
logger$2.info(pc.green("Config updated, reloading..."));
|
|
@@ -1685,11 +2179,9 @@ async function dev$1(options) {
|
|
|
1685
2179
|
port: options.port ?? config.serverConfig.port
|
|
1686
2180
|
});
|
|
1687
2181
|
await server.start();
|
|
1688
|
-
const outFiles =
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
};
|
|
1692
|
-
await injectHMRExtension(server.link, server.wsLink, outFiles);
|
|
2182
|
+
const outFiles = getOutFiles(config, true);
|
|
2183
|
+
if (config.template === "custom-app") await injectHMRCustomApp(server.link, server.wsLink, outFiles, config);
|
|
2184
|
+
else await injectHMRExtension(server.link, server.wsLink, outFiles);
|
|
1693
2185
|
ctx = await context(getJSDevOptions(config, {
|
|
1694
2186
|
...options,
|
|
1695
2187
|
outFiles,
|
|
@@ -1701,21 +2193,14 @@ async function dev$1(options) {
|
|
|
1701
2193
|
logger$2.error("Failed to start dev server: ", err);
|
|
1702
2194
|
}
|
|
1703
2195
|
return async () => {
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
await server?.stop();
|
|
1708
|
-
} finally {
|
|
1709
|
-
process.exit();
|
|
1710
|
-
}
|
|
2196
|
+
await ctx?.dispose();
|
|
2197
|
+
ctx = void 0;
|
|
2198
|
+
await server?.stop();
|
|
1711
2199
|
};
|
|
1712
2200
|
});
|
|
1713
2201
|
}
|
|
1714
2202
|
function getJSDevOptions(config, options) {
|
|
1715
|
-
const entryPoints = (
|
|
1716
|
-
if (config.template === "theme") return [config.entry.js, config.entry.css];
|
|
1717
|
-
return [config.entry];
|
|
1718
|
-
})();
|
|
2203
|
+
const entryPoints = getEntryPoints(config);
|
|
1719
2204
|
const minify = false;
|
|
1720
2205
|
const cache = {
|
|
1721
2206
|
files: /* @__PURE__ */ new Map(),
|
|
@@ -1734,7 +2219,7 @@ function getJSDevOptions(config, options) {
|
|
|
1734
2219
|
],
|
|
1735
2220
|
define: {
|
|
1736
2221
|
[DEV_MODE_VAR_NAME]: "true",
|
|
1737
|
-
[config.devModeVarName]: "true",
|
|
2222
|
+
...config.devModeVarName ? { [config.devModeVarName]: "true" } : {},
|
|
1738
2223
|
...config.esbuildOptions.define
|
|
1739
2224
|
},
|
|
1740
2225
|
plugins: [...config.esbuildOptions?.plugins ? config.esbuildOptions.plugins : [], ...getCommonPlugins({
|
|
@@ -1743,7 +2228,9 @@ function getJSDevOptions(config, options) {
|
|
|
1743
2228
|
cache,
|
|
1744
2229
|
buildOptions: {
|
|
1745
2230
|
copy: true,
|
|
1746
|
-
|
|
2231
|
+
apply: false,
|
|
2232
|
+
applyOnce: false,
|
|
2233
|
+
remove: config.template !== "custom-app",
|
|
1747
2234
|
outDir
|
|
1748
2235
|
},
|
|
1749
2236
|
dev: true,
|
|
@@ -1765,12 +2252,17 @@ const dev = new Command("dev").description("Develop your spicetify project").opt
|
|
|
1765
2252
|
await dev$1(safeParse(CLIOptionsSchema, opts));
|
|
1766
2253
|
});
|
|
1767
2254
|
|
|
2255
|
+
//#endregion
|
|
2256
|
+
//#region src/commands/update-types.ts
|
|
2257
|
+
const update_types = new Command("update-types").description("Update Spicetify Types").action(async () => {
|
|
2258
|
+
await updateTypes();
|
|
2259
|
+
});
|
|
2260
|
+
|
|
1768
2261
|
//#endregion
|
|
1769
2262
|
//#region src/bin.ts
|
|
1770
2263
|
logger$2.debug(`Env: ${JSON.stringify(env, null, 2)}\n`);
|
|
1771
2264
|
const command = new Command();
|
|
1772
|
-
create.alias("init");
|
|
1773
|
-
command.addCommand(create).addCommand(build).addCommand(dev);
|
|
2265
|
+
command.version(version).addCommand(create.alias("init")).addCommand(build$1).addCommand(dev).addCommand(update_types);
|
|
1774
2266
|
command.parse();
|
|
1775
2267
|
|
|
1776
2268
|
//#endregion
|