@spicemod/creator 0.0.23 → 0.0.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.mjs +195 -140
- package/dist/index.d.mts +2 -2
- package/dist/templates/custom-app/js/react/src/extension/index.jsx +0 -1
- package/dist/templates/custom-app/shared/spice.config.js +1 -0
- package/dist/templates/custom-app/shared/spice.config.ts +1 -0
- package/dist/templates/custom-app/ts/react/src/app.tsx +1 -2
- package/dist/templates/custom-app/ts/react/src/components/Onboarding.tsx +4 -17
- package/dist/templates/customAppEntry.js +6 -0
- package/dist/templates/extension/shared/spice.config.js +1 -0
- package/dist/templates/extension/shared/spice.config.ts +1 -0
- package/dist/templates/theme/shared/spice.config.js +1 -0
- package/dist/templates/theme/shared/spice.config.ts +1 -0
- package/dist/templates/wrapper.js +1 -1
- package/package.json +1 -1
- package/templates/custom-app/js/react/src/extension/index.jsx +0 -1
- package/templates/custom-app/shared/spice.config.js +1 -0
- package/templates/custom-app/shared/spice.config.ts +1 -0
- package/templates/custom-app/ts/react/src/app.tsx +1 -2
- package/templates/custom-app/ts/react/src/components/Onboarding.tsx +4 -17
- package/templates/customAppEntry.js +6 -0
- package/templates/extension/shared/spice.config.js +1 -0
- package/templates/extension/shared/spice.config.ts +1 -0
- package/templates/theme/shared/spice.config.js +1 -0
- package/templates/theme/shared/spice.config.ts +1 -0
- package/templates/wrapper.js +1 -1
package/dist/bin.mjs
CHANGED
|
@@ -13,6 +13,8 @@ import * as p from "@clack/prompts";
|
|
|
13
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,13 +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 { readFile, writeFile } from "node:fs/promises";
|
|
24
|
-
import { parse } from "ini";
|
|
25
25
|
import { createHash } from "node:crypto";
|
|
26
26
|
import { chdir } from "node:process";
|
|
27
27
|
import { lookup } from "node:dns/promises";
|
|
28
28
|
import { mkdir, writeFile as writeFile$1 } from "fs/promises";
|
|
29
|
-
import { dirname as dirname$1 } from "path";
|
|
29
|
+
import { dirname as dirname$1, join as join$1 } from "path";
|
|
30
30
|
import { createServer } from "node:http";
|
|
31
31
|
import { WebSocket, WebSocketServer } from "ws";
|
|
32
32
|
|
|
@@ -138,12 +138,12 @@ const frameworks = frameworkTypes.map((name) => ({
|
|
|
138
138
|
const frameworkOptions = toOptions(frameworks);
|
|
139
139
|
const liveReloadFilePath = dist(`templates/liveReload.js`, import.meta.url);
|
|
140
140
|
const templateWrapperFilePath = dist("templates/wrapper.js", import.meta.url);
|
|
141
|
-
const
|
|
141
|
+
const customAppEntryFilePath = dist("templates/customAppEntry.js", import.meta.url);
|
|
142
142
|
|
|
143
143
|
//#endregion
|
|
144
144
|
//#region package.json
|
|
145
145
|
var name = "@spicemod/creator";
|
|
146
|
-
var version = "0.0.
|
|
146
|
+
var version = "0.0.25";
|
|
147
147
|
|
|
148
148
|
//#endregion
|
|
149
149
|
//#region src/utils/common.ts
|
|
@@ -248,7 +248,6 @@ const DEV_MODE_VAR_NAME = `__SPICE_CREATOR_DEV__`;
|
|
|
248
248
|
const CHECK = pc.bold(pc.green("✔"));
|
|
249
249
|
const CROSS = pc.bold(pc.red("✖"));
|
|
250
250
|
const WARN = pc.bold(pc.yellow("⚠"));
|
|
251
|
-
const SKIP_SPICETIFY = process.env.SPICETIFY_SKIP === "true" || process.env.CI === "true";
|
|
252
251
|
const VALID_PROJECT_FILES = new Set([
|
|
253
252
|
".DS_Store",
|
|
254
253
|
".git",
|
|
@@ -366,7 +365,7 @@ const ThemeTemplateSchema = v.object({
|
|
|
366
365
|
entry: AssetEntrySchema
|
|
367
366
|
});
|
|
368
367
|
const CustomAppTemplateSchema = v.object({
|
|
369
|
-
name: v.union([v.string(), LocaleNameSchema]),
|
|
368
|
+
name: v.union([v.string(), LocaleNameSchema], "Name must be a string or a translations object containing the required 'en' locale."),
|
|
370
369
|
icon: v.object({
|
|
371
370
|
default: v.string(),
|
|
372
371
|
active: v.optional(v.string())
|
|
@@ -407,13 +406,13 @@ const OptionsSchema$1 = v.intersect([
|
|
|
407
406
|
|
|
408
407
|
//#endregion
|
|
409
408
|
//#region src/env.ts
|
|
410
|
-
const isInternal = process.env.SPICE_INTERNAL === "true";
|
|
411
409
|
const isDev = process.env.IS_DEV === "true";
|
|
412
410
|
const spicetifyBin = process.env.SPICETIFY_BIN || process.env.SPICE_BIN || "spicetify";
|
|
411
|
+
const skipSpicetify = process.env.SPICETIFY_SKIP === "true" || process.env.CI === "true";
|
|
413
412
|
const env = {
|
|
414
|
-
isInternal,
|
|
415
413
|
isDev,
|
|
416
|
-
spicetifyBin
|
|
414
|
+
spicetifyBin,
|
|
415
|
+
skipSpicetify
|
|
417
416
|
};
|
|
418
417
|
|
|
419
418
|
//#endregion
|
|
@@ -472,6 +471,10 @@ var Logger = class {
|
|
|
472
471
|
}
|
|
473
472
|
clear() {
|
|
474
473
|
if (!process.stdout.isTTY || process.env.CI) return;
|
|
474
|
+
if (this.isDev) {
|
|
475
|
+
this.log("clear skipped");
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
475
478
|
readline.cursorTo(process.stdout, 0, 0);
|
|
476
479
|
readline.clearScreenDown(process.stdout);
|
|
477
480
|
}
|
|
@@ -480,17 +483,54 @@ const createLogger = (prefix = "", mode) => new Logger(prefix, mode);
|
|
|
480
483
|
const logger$2 = createLogger("common");
|
|
481
484
|
|
|
482
485
|
//#endregion
|
|
483
|
-
//#region src/utils/schema.ts
|
|
484
|
-
|
|
485
|
-
|
|
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);
|
|
486
522
|
if (result.success) return result.output;
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
});
|
|
492
|
-
|
|
493
|
-
|
|
523
|
+
else throw new Error("Spicetify Config Validation Failed:", v.flatten(result.issues).nested);
|
|
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"}`);
|
|
494
534
|
}
|
|
495
535
|
|
|
496
536
|
//#endregion
|
|
@@ -526,6 +566,7 @@ const ENTRY_MAP = {
|
|
|
526
566
|
//#endregion
|
|
527
567
|
//#region src/config/index.ts
|
|
528
568
|
const logger$1 = createLogger("config");
|
|
569
|
+
let previousConfig;
|
|
529
570
|
const CONFIG_DEFAULTS = {
|
|
530
571
|
outDir: "./dist",
|
|
531
572
|
linter: "biome",
|
|
@@ -536,29 +577,38 @@ const CONFIG_DEFAULTS = {
|
|
|
536
577
|
async function loadConfig(cb) {
|
|
537
578
|
let cleanup;
|
|
538
579
|
const runCb = async (config, isUpdate) => {
|
|
580
|
+
if (isUpdate && previousConfig) await cleanupSpicetifyConfig();
|
|
539
581
|
if (typeof cleanup === "function") await cleanup();
|
|
540
582
|
cleanup = await cb(config, isUpdate);
|
|
583
|
+
previousConfig = config;
|
|
541
584
|
};
|
|
542
585
|
const watcher = await watchConfig({
|
|
543
586
|
name: "spice",
|
|
544
587
|
defaults: CONFIG_DEFAULTS,
|
|
545
|
-
configFileRequired:
|
|
588
|
+
configFileRequired: true,
|
|
546
589
|
packageJson: true,
|
|
547
590
|
async onUpdate({ newConfig }) {
|
|
548
|
-
|
|
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
|
+
}
|
|
549
596
|
}
|
|
550
597
|
});
|
|
551
598
|
await runCb(await getResolvedConfig(watcher.config), false);
|
|
552
599
|
return watcher;
|
|
553
600
|
}
|
|
554
|
-
async function getResolvedConfig(config) {
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
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");
|
|
562
612
|
}
|
|
563
613
|
async function resolveContext(config) {
|
|
564
614
|
const cwd = process.cwd();
|
|
@@ -617,7 +667,7 @@ function resolveDefaultIcon(cwd) {
|
|
|
617
667
|
cwd,
|
|
618
668
|
absolute: true
|
|
619
669
|
});
|
|
620
|
-
if (matches.length > 0 && matches[0]) return
|
|
670
|
+
if (matches.length > 0 && matches[0]) return matches[0];
|
|
621
671
|
}
|
|
622
672
|
const expectedFiles = ICON_GLOBS.map((g) => ` - ${g}`).join("\n");
|
|
623
673
|
throw new Error(`No icon file found in your project.\nPlease create one of the following files:\n${expectedFiles}`);
|
|
@@ -628,11 +678,33 @@ function resolveActiveIcon(cwd) {
|
|
|
628
678
|
cwd,
|
|
629
679
|
absolute: true
|
|
630
680
|
});
|
|
631
|
-
if (matches.length > 0 && matches[0]) return
|
|
681
|
+
if (matches.length > 0 && matches[0]) return matches[0];
|
|
632
682
|
}
|
|
633
683
|
return "";
|
|
634
684
|
}
|
|
635
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"]);
|
|
707
|
+
}
|
|
636
708
|
|
|
637
709
|
//#endregion
|
|
638
710
|
//#region src/esbuild/format.ts
|
|
@@ -773,57 +845,6 @@ const externalGlobal = (externals, namespace = "spicetify-global") => {
|
|
|
773
845
|
};
|
|
774
846
|
};
|
|
775
847
|
|
|
776
|
-
//#endregion
|
|
777
|
-
//#region src/utils/spicetify/schema.ts
|
|
778
|
-
const SpicetifyConfigSchema = v.object({
|
|
779
|
-
Setting: v.object({
|
|
780
|
-
spotify_path: v.string(),
|
|
781
|
-
prefs_path: v.string(),
|
|
782
|
-
inject_theme_js: v.string(),
|
|
783
|
-
inject_css: v.string(),
|
|
784
|
-
current_theme: v.string(),
|
|
785
|
-
color_scheme: v.string(),
|
|
786
|
-
always_enable_devtools: v.string()
|
|
787
|
-
}),
|
|
788
|
-
AdditionalOptions: v.object({
|
|
789
|
-
experimental_features: v.string(),
|
|
790
|
-
extensions: v.pipe(v.string(), v.transform((input) => input.split("|").filter(Boolean))),
|
|
791
|
-
custom_apps: v.string()
|
|
792
|
-
}),
|
|
793
|
-
Backup: v.object({
|
|
794
|
-
version: v.string(),
|
|
795
|
-
with: v.string()
|
|
796
|
-
})
|
|
797
|
-
});
|
|
798
|
-
|
|
799
|
-
//#endregion
|
|
800
|
-
//#region src/utils/spicetify/index.ts
|
|
801
|
-
function runSpice(args) {
|
|
802
|
-
validateSpicetify(env.spicetifyBin);
|
|
803
|
-
return spawnSync(env.spicetifyBin, args, { encoding: "utf-8" });
|
|
804
|
-
}
|
|
805
|
-
const getCustomAppsDir = () => join(getSpiceDataPath(), "CustomApps");
|
|
806
|
-
const getExtensionDir = () => join(getSpiceDataPath(), "Extensions");
|
|
807
|
-
const getThemesDir = () => join(getSpiceDataPath(), "Themes");
|
|
808
|
-
async function getSpicetifyConfig() {
|
|
809
|
-
const { stdout, stderr, error } = runSpice(["path", "-c"]);
|
|
810
|
-
if (error || stderr) throw new Error(`Failed to locate Spicetify config: ${stderr || error?.message}`);
|
|
811
|
-
const rawConfig = parse(await readFile(stdout.trim(), "utf-8"));
|
|
812
|
-
const result = v.safeParse(SpicetifyConfigSchema, rawConfig);
|
|
813
|
-
if (result.success) return result.output;
|
|
814
|
-
else throw new Error("Spicetify Config Validation Failed:", v.flatten(result.issues).nested);
|
|
815
|
-
}
|
|
816
|
-
function getSpiceDataPath() {
|
|
817
|
-
const { stdout, stderr, error } = runSpice(["path", "userdata"]);
|
|
818
|
-
if (error || stderr) throw new Error(`Failed to locate Spicetify config: ${stderr || error?.message}`);
|
|
819
|
-
return stdout.trim();
|
|
820
|
-
}
|
|
821
|
-
function validateSpicetify(bin) {
|
|
822
|
-
const result = spawnSync(bin, ["--version"], { encoding: "utf-8" });
|
|
823
|
-
if (result.error) throw result.error;
|
|
824
|
-
if (result.status !== 0) throw new Error(`Invalid spicetify binary "${bin}": ${result.stderr || "unknown error"}`);
|
|
825
|
-
}
|
|
826
|
-
|
|
827
848
|
//#endregion
|
|
828
849
|
//#region src/esbuild/plugins/spicetifyHandlers.ts
|
|
829
850
|
const spicetifyHandler = ({ config, options, cache, logger = createLogger("plugin:spicetify-handler") }) => ({
|
|
@@ -834,7 +855,7 @@ const spicetifyHandler = ({ config, options, cache, logger = createLogger("plugi
|
|
|
834
855
|
const isExtension = config.template === "extension";
|
|
835
856
|
const isCustomApp = config.template === "custom-app";
|
|
836
857
|
const identifier = isExtension ? `${urlSlugify(config.name)}.js` : urlSlugify(getEnName(config.name));
|
|
837
|
-
if (
|
|
858
|
+
if (env.skipSpicetify) {
|
|
838
859
|
logger.info(pc.yellow("skipping spicetify operations"));
|
|
839
860
|
build.onEnd(async (result) => {
|
|
840
861
|
if (result.errors.length > 0) return;
|
|
@@ -964,14 +985,7 @@ function wrapWithLoader({ config, cache, outFiles, server, dev = false, logger =
|
|
|
964
985
|
format: "iife",
|
|
965
986
|
globalName,
|
|
966
987
|
stdin: {
|
|
967
|
-
contents:
|
|
968
|
-
import App from "virtual:app";
|
|
969
|
-
import React from "react";
|
|
970
|
-
|
|
971
|
-
export default function render() {
|
|
972
|
-
return <App />;
|
|
973
|
-
}
|
|
974
|
-
`,
|
|
988
|
+
contents: readFileSync(customAppEntryFilePath),
|
|
975
989
|
loader: "tsx",
|
|
976
990
|
sourcefile: "entry.tsx"
|
|
977
991
|
},
|
|
@@ -994,23 +1008,35 @@ export default function render() {
|
|
|
994
1008
|
})).outputFiles?.[0];
|
|
995
1009
|
if (!final) return;
|
|
996
1010
|
let combinedCode = `${final.text}${dev ? `export default () => ${globalName}.default();` : `var render = () => ${globalName}.default();`}\n`;
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
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
|
+
}
|
|
1004
1024
|
return;
|
|
1005
1025
|
}
|
|
1006
1026
|
if (!isJs) {
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
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
|
+
}
|
|
1014
1040
|
return;
|
|
1015
1041
|
}
|
|
1016
1042
|
const { code: transformedTemp } = await transform(readFileSync(templateWrapperFilePath, "utf-8"), {
|
|
@@ -1032,13 +1058,18 @@ export default function render() {
|
|
|
1032
1058
|
"\"{{INJECTED_JS_HERE}}\"": file.text
|
|
1033
1059
|
});
|
|
1034
1060
|
const nextBuffer = Buffer.from(template);
|
|
1035
|
-
cache.files.
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
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
|
+
}
|
|
1042
1073
|
});
|
|
1043
1074
|
await Promise.all(transformPromises);
|
|
1044
1075
|
if (type === "custom-app") {
|
|
@@ -1055,9 +1086,11 @@ export default function render() {
|
|
|
1055
1086
|
const currentHash = createHash("md5").update(manifestString).digest("hex");
|
|
1056
1087
|
if (currentHash !== previousManifestHash) {
|
|
1057
1088
|
previousManifestHash = currentHash;
|
|
1089
|
+
const manifestBuffer = Buffer.from(manifestString);
|
|
1058
1090
|
cache.files.set(manifestPath, {
|
|
1059
|
-
contents:
|
|
1060
|
-
name: "manifest.json"
|
|
1091
|
+
contents: manifestBuffer,
|
|
1092
|
+
name: "manifest.json",
|
|
1093
|
+
hash: currentHash
|
|
1061
1094
|
});
|
|
1062
1095
|
cache.changed.add(manifestPath);
|
|
1063
1096
|
cache.hasChanges = true;
|
|
@@ -1107,10 +1140,11 @@ const defaultBuildOptions = {
|
|
|
1107
1140
|
};
|
|
1108
1141
|
const getCommonPlugins = (opts) => {
|
|
1109
1142
|
const { template, minify, cache, buildOptions, outFiles, server, dev } = opts;
|
|
1143
|
+
const inline = !dev && template === "extension";
|
|
1110
1144
|
return [
|
|
1111
1145
|
...plugins.css({
|
|
1112
1146
|
minify,
|
|
1113
|
-
inline
|
|
1147
|
+
inline
|
|
1114
1148
|
}),
|
|
1115
1149
|
plugins.externalGlobal({
|
|
1116
1150
|
react: "Spicetify.React",
|
|
@@ -1139,7 +1173,7 @@ function getEntryPoints(config) {
|
|
|
1139
1173
|
if (config.template === "custom-app") return [config.entry.app, config.entry.extension];
|
|
1140
1174
|
return [config.entry];
|
|
1141
1175
|
}
|
|
1142
|
-
function getOutFiles(config) {
|
|
1176
|
+
function getOutFiles(config, isDev = false) {
|
|
1143
1177
|
switch (config.template) {
|
|
1144
1178
|
case "custom-app": return {
|
|
1145
1179
|
js: "index.js",
|
|
@@ -1147,7 +1181,10 @@ function getOutFiles(config) {
|
|
|
1147
1181
|
jsExtension: "extension.js",
|
|
1148
1182
|
manifest: "manifest.json"
|
|
1149
1183
|
};
|
|
1150
|
-
case "extension": return {
|
|
1184
|
+
case "extension": return {
|
|
1185
|
+
js: `${urlSlugify(getEnName(config.name))}.js`,
|
|
1186
|
+
css: isDev ? "app.css" : void 0
|
|
1187
|
+
};
|
|
1151
1188
|
case "theme": return {
|
|
1152
1189
|
js: "theme.js",
|
|
1153
1190
|
css: "user.css"
|
|
@@ -1233,6 +1270,20 @@ function getJSBuildOptions(config, options) {
|
|
|
1233
1270
|
};
|
|
1234
1271
|
}
|
|
1235
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
|
+
|
|
1236
1287
|
//#endregion
|
|
1237
1288
|
//#region src/commands/build.ts
|
|
1238
1289
|
const CLIOptionsSchema$1 = v.strictObject({
|
|
@@ -1304,7 +1355,7 @@ function createPackageJSON(options) {
|
|
|
1304
1355
|
"update-types": "spicetify-creator update-types"
|
|
1305
1356
|
},
|
|
1306
1357
|
dependencies: {},
|
|
1307
|
-
devDependencies: { "@spicetify/creator":
|
|
1358
|
+
devDependencies: { "@spicetify/creator": "latest" }
|
|
1308
1359
|
};
|
|
1309
1360
|
if (options.language === "ts") result.peerDependencies = {
|
|
1310
1361
|
...result.peerDependencies,
|
|
@@ -1598,22 +1649,23 @@ const downloads = { spicetify: {
|
|
|
1598
1649
|
to: "./src/types/spicetify.d.ts",
|
|
1599
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\");")
|
|
1600
1651
|
} };
|
|
1601
|
-
async function updateTypes(isUpdating = true) {
|
|
1652
|
+
async function updateTypes(isUpdating = true, cwd = process.cwd()) {
|
|
1602
1653
|
const s = spinner();
|
|
1603
1654
|
s.start(`${isUpdating ? "Updating" : "Creating"} Types...`);
|
|
1604
|
-
await Promise.all(Object.entries(downloads).map(([name, download]) => downloadFile(name, download, isUpdating)));
|
|
1655
|
+
await Promise.all(Object.entries(downloads).map(([name, download]) => downloadFile(name, download, isUpdating, cwd)));
|
|
1605
1656
|
s.stop(`${isUpdating ? "Updated" : "Created"} Types!`);
|
|
1606
1657
|
}
|
|
1607
|
-
async function downloadFile(name, { from, to, action }, isUpdating) {
|
|
1658
|
+
async function downloadFile(name, { from, to, action }, isUpdating, cwd) {
|
|
1608
1659
|
try {
|
|
1609
1660
|
const res = await fetch(from);
|
|
1610
1661
|
if (!res.ok) throw new Error(`HTTP Error: ${res.status} ${res.statusText}`);
|
|
1611
1662
|
let text = await res.text();
|
|
1612
1663
|
if (action) text = await action(text);
|
|
1613
|
-
|
|
1614
|
-
await
|
|
1664
|
+
const fullPath = join$1(cwd, to);
|
|
1665
|
+
await mkdir(dirname$1(fullPath), { recursive: true });
|
|
1666
|
+
await writeFile$1(fullPath, text, "utf8");
|
|
1615
1667
|
const actionLog = isUpdating ? "updated" : "created";
|
|
1616
|
-
logger$2.log(`${name}.d.ts ${actionLog} (${from} -> ${
|
|
1668
|
+
logger$2.log(`${name}.d.ts ${actionLog} (${from} -> ${fullPath})`);
|
|
1617
1669
|
} catch (e) {
|
|
1618
1670
|
log.error(`${name} failed`);
|
|
1619
1671
|
console.error(e);
|
|
@@ -1737,7 +1789,7 @@ async function create$1(cwd, options) {
|
|
|
1737
1789
|
try {
|
|
1738
1790
|
mkdirp(cwd);
|
|
1739
1791
|
setupTemplateFiles(options, cwd);
|
|
1740
|
-
await updateTypes(false);
|
|
1792
|
+
await updateTypes(false, cwd);
|
|
1741
1793
|
const pkgJSON = createPackageJSON(options);
|
|
1742
1794
|
writePackageJSON(pkgJSON, cwd);
|
|
1743
1795
|
chdir(cwd);
|
|
@@ -1918,7 +1970,12 @@ async function createHmrServer(config, logger = createLogger("hmrServer")) {
|
|
|
1918
1970
|
resolve();
|
|
1919
1971
|
});
|
|
1920
1972
|
}),
|
|
1921
|
-
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();
|
|
1922
1979
|
httpServer.close((err) => {
|
|
1923
1980
|
if (err) return reject(err);
|
|
1924
1981
|
isRunning = false;
|
|
@@ -2078,13 +2135,15 @@ const render = () => {
|
|
|
2078
2135
|
}
|
|
2079
2136
|
});
|
|
2080
2137
|
writeFileSync(extensionJsPath, extensionCode);
|
|
2138
|
+
const icon = readFileSync(config.icon.default).toString();
|
|
2139
|
+
const activeIcon = config.icon.active ? readFileSync(config.icon.active).toString() : icon;
|
|
2081
2140
|
const manifestPath = resolve(destDir, "manifest.json");
|
|
2082
2141
|
const manifest = {
|
|
2083
|
-
name:
|
|
2142
|
+
name: config.name,
|
|
2084
2143
|
subfiles: [],
|
|
2085
2144
|
subfiles_extension: [outFiles.jsExtension ?? "extension.js"],
|
|
2086
|
-
icon
|
|
2087
|
-
|
|
2145
|
+
icon,
|
|
2146
|
+
activeIcon
|
|
2088
2147
|
};
|
|
2089
2148
|
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
2090
2149
|
if (spiceConfig) {
|
|
@@ -2110,7 +2169,7 @@ async function dev$1(options) {
|
|
|
2110
2169
|
logger$2.greeting(pc.green("Starting development environment"));
|
|
2111
2170
|
let ctx;
|
|
2112
2171
|
let server = void 0;
|
|
2113
|
-
loadConfig(async (config, isNewUpdate) => {
|
|
2172
|
+
await loadConfig(async (config, isNewUpdate) => {
|
|
2114
2173
|
if (isNewUpdate) {
|
|
2115
2174
|
logger$2.clear();
|
|
2116
2175
|
logger$2.info(pc.green("Config updated, reloading..."));
|
|
@@ -2122,7 +2181,7 @@ async function dev$1(options) {
|
|
|
2122
2181
|
port: options.port ?? config.serverConfig.port
|
|
2123
2182
|
});
|
|
2124
2183
|
await server.start();
|
|
2125
|
-
const outFiles = getOutFiles(config);
|
|
2184
|
+
const outFiles = getOutFiles(config, true);
|
|
2126
2185
|
if (config.template === "custom-app") await injectHMRCustomApp(server.link, server.wsLink, outFiles, config);
|
|
2127
2186
|
else await injectHMRExtension(server.link, server.wsLink, outFiles);
|
|
2128
2187
|
ctx = await context(getJSDevOptions(config, {
|
|
@@ -2136,13 +2195,9 @@ async function dev$1(options) {
|
|
|
2136
2195
|
logger$2.error("Failed to start dev server: ", err);
|
|
2137
2196
|
}
|
|
2138
2197
|
return async () => {
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
await server?.stop();
|
|
2143
|
-
} finally {
|
|
2144
|
-
process.exit();
|
|
2145
|
-
}
|
|
2198
|
+
await ctx?.dispose();
|
|
2199
|
+
ctx = void 0;
|
|
2200
|
+
await server?.stop();
|
|
2146
2201
|
};
|
|
2147
2202
|
});
|
|
2148
2203
|
}
|
|
@@ -2209,7 +2264,7 @@ const update_types = new Command("update-types").description("Update Spicetify T
|
|
|
2209
2264
|
//#region src/bin.ts
|
|
2210
2265
|
logger$2.debug(`Env: ${JSON.stringify(env, null, 2)}\n`);
|
|
2211
2266
|
const command = new Command();
|
|
2212
|
-
command.addCommand(create.alias("init")).addCommand(build$1).addCommand(dev).addCommand(update_types);
|
|
2267
|
+
command.version(version).addCommand(create.alias("init")).addCommand(build$1).addCommand(dev).addCommand(update_types);
|
|
2213
2268
|
command.parse();
|
|
2214
2269
|
|
|
2215
2270
|
//#endregion
|
package/dist/index.d.mts
CHANGED
|
@@ -266,7 +266,7 @@ declare const FileOptionsSchema: v.IntersectSchema<[Omit<v.ObjectSchema<{
|
|
|
266
266
|
}, Omit<v.ObjectSchema<{
|
|
267
267
|
readonly name: v.UnionSchema<[v.StringSchema<undefined>, v.IntersectSchema<[v.ObjectSchema<{
|
|
268
268
|
readonly en: v.StringSchema<undefined>;
|
|
269
|
-
}, undefined>, v.RecordSchema<v.PicklistSchema<readonly ["ms", "gu", "ko", "pa-IN", "az", "ru", "uk", "nb", "sv", "sw", "ur", "bho", "pa-PK", "te", "ro", "vi", "am", "bn", "en", "id", "bg", "da", "es-419", "mr", "ml", "th", "tr", "is", "fa", "or", "he", "hi", "zh-TW", "sr", "pt-BR", "zu", "nl", "es", "lt", "ja", "st", "it", "el", "pt-PT", "kn", "de", "fr", "ne", "ar", "af", "et", "pl", "ta", "sl", "pk", "hr", "sk", "fi", "lv", "fil", "fr-CA", "cs", "zh-CN", "hu"], undefined>, v.StringSchema<undefined>, undefined>], undefined>],
|
|
269
|
+
}, undefined>, v.RecordSchema<v.PicklistSchema<readonly ["ms", "gu", "ko", "pa-IN", "az", "ru", "uk", "nb", "sv", "sw", "ur", "bho", "pa-PK", "te", "ro", "vi", "am", "bn", "en", "id", "bg", "da", "es-419", "mr", "ml", "th", "tr", "is", "fa", "or", "he", "hi", "zh-TW", "sr", "pt-BR", "zu", "nl", "es", "lt", "ja", "st", "it", "el", "pt-PT", "kn", "de", "fr", "ne", "ar", "af", "et", "pl", "ta", "sl", "pk", "hr", "sk", "fi", "lv", "fil", "fr-CA", "cs", "zh-CN", "hu"], undefined>, v.StringSchema<undefined>, undefined>], undefined>], "Name must be a string or a translations object containing the required 'en' locale.">;
|
|
270
270
|
readonly icon: v.ObjectSchema<{
|
|
271
271
|
readonly default: v.StringSchema<undefined>;
|
|
272
272
|
readonly active: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
@@ -280,7 +280,7 @@ declare const FileOptionsSchema: v.IntersectSchema<[Omit<v.ObjectSchema<{
|
|
|
280
280
|
readonly entries: {
|
|
281
281
|
readonly name: v.OptionalSchema<v.UnionSchema<[v.StringSchema<undefined>, v.IntersectSchema<[v.ObjectSchema<{
|
|
282
282
|
readonly en: v.StringSchema<undefined>;
|
|
283
|
-
}, undefined>, v.RecordSchema<v.PicklistSchema<readonly ["ms", "gu", "ko", "pa-IN", "az", "ru", "uk", "nb", "sv", "sw", "ur", "bho", "pa-PK", "te", "ro", "vi", "am", "bn", "en", "id", "bg", "da", "es-419", "mr", "ml", "th", "tr", "is", "fa", "or", "he", "hi", "zh-TW", "sr", "pt-BR", "zu", "nl", "es", "lt", "ja", "st", "it", "el", "pt-PT", "kn", "de", "fr", "ne", "ar", "af", "et", "pl", "ta", "sl", "pk", "hr", "sk", "fi", "lv", "fil", "fr-CA", "cs", "zh-CN", "hu"], undefined>, v.StringSchema<undefined>, undefined>], undefined>],
|
|
283
|
+
}, undefined>, v.RecordSchema<v.PicklistSchema<readonly ["ms", "gu", "ko", "pa-IN", "az", "ru", "uk", "nb", "sv", "sw", "ur", "bho", "pa-PK", "te", "ro", "vi", "am", "bn", "en", "id", "bg", "da", "es-419", "mr", "ml", "th", "tr", "is", "fa", "or", "he", "hi", "zh-TW", "sr", "pt-BR", "zu", "nl", "es", "lt", "ja", "st", "it", "el", "pt-PT", "kn", "de", "fr", "ne", "ar", "af", "et", "pl", "ta", "sl", "pk", "hr", "sk", "fi", "lv", "fil", "fr-CA", "cs", "zh-CN", "hu"], undefined>, v.StringSchema<undefined>, undefined>], undefined>], "Name must be a string or a translations object containing the required 'en' locale.">, undefined>;
|
|
284
284
|
readonly icon: v.OptionalSchema<v.ObjectSchema<{
|
|
285
285
|
readonly default: v.StringSchema<undefined>;
|
|
286
286
|
readonly active: v.OptionalSchema<v.StringSchema<undefined>, undefined>;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import styles from "@/css/app.module.scss";
|
|
2
2
|
import { useState } from "react";
|
|
3
|
-
import "@/app.css";
|
|
4
3
|
|
|
5
4
|
const App: React.FC = () => {
|
|
6
5
|
const [count, setCount] = useState(0);
|
|
@@ -20,4 +19,4 @@ const App: React.FC = () => {
|
|
|
20
19
|
);
|
|
21
20
|
};
|
|
22
21
|
|
|
23
|
-
export default App;
|
|
22
|
+
export default App;
|
|
@@ -22,16 +22,9 @@ const Onboarding: React.FC<OnboardingProps> = ({ config }) => {
|
|
|
22
22
|
if (!isVisible) return null;
|
|
23
23
|
|
|
24
24
|
return (
|
|
25
|
-
<div
|
|
26
|
-
className={`onboarding-overlay ${isFading ? "fade-out" : ""}`}
|
|
27
|
-
onClick={handleDismiss}
|
|
28
|
-
>
|
|
25
|
+
<div className={`onboarding-overlay ${isFading ? "fade-out" : ""}`} onClick={handleDismiss}>
|
|
29
26
|
<div className="onboarding-card" onClick={(e) => e.stopPropagation()}>
|
|
30
|
-
<button
|
|
31
|
-
className="close-icon-btn"
|
|
32
|
-
onClick={handleDismiss}
|
|
33
|
-
aria-label="Close"
|
|
34
|
-
>
|
|
27
|
+
<button className="close-icon-btn" onClick={handleDismiss} aria-label="Close">
|
|
35
28
|
<svg width="12" height="12" viewBox="0 0 12 12" fill="none">
|
|
36
29
|
<path
|
|
37
30
|
d="M1 1L11 11M1 11L11 1"
|
|
@@ -63,10 +56,7 @@ const Onboarding: React.FC<OnboardingProps> = ({ config }) => {
|
|
|
63
56
|
</div>
|
|
64
57
|
|
|
65
58
|
<div className="onboarding-actions">
|
|
66
|
-
<button
|
|
67
|
-
className="get-started-btn"
|
|
68
|
-
onClick={() => openLink("{{get-started-link}}")}
|
|
69
|
-
>
|
|
59
|
+
<button className="get-started-btn" onClick={() => openLink("{{get-started-link}}")}>
|
|
70
60
|
Get Started
|
|
71
61
|
</button>
|
|
72
62
|
<button
|
|
@@ -78,10 +68,7 @@ const Onboarding: React.FC<OnboardingProps> = ({ config }) => {
|
|
|
78
68
|
>
|
|
79
69
|
Go to Custom App
|
|
80
70
|
</button>
|
|
81
|
-
<button
|
|
82
|
-
className="discord-btn"
|
|
83
|
-
onClick={() => openLink("{{discord-link}}")}
|
|
84
|
-
>
|
|
71
|
+
<button className="discord-btn" onClick={() => openLink("{{discord-link}}")}>
|
|
85
72
|
Join Discord
|
|
86
73
|
</button>
|
|
87
74
|
<button className="dismiss-btn" onClick={handleDismiss}>
|
package/package.json
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import styles from "@/css/app.module.scss";
|
|
2
2
|
import { useState } from "react";
|
|
3
|
-
import "@/app.css";
|
|
4
3
|
|
|
5
4
|
const App: React.FC = () => {
|
|
6
5
|
const [count, setCount] = useState(0);
|
|
@@ -20,4 +19,4 @@ const App: React.FC = () => {
|
|
|
20
19
|
);
|
|
21
20
|
};
|
|
22
21
|
|
|
23
|
-
export default App;
|
|
22
|
+
export default App;
|
|
@@ -22,16 +22,9 @@ const Onboarding: React.FC<OnboardingProps> = ({ config }) => {
|
|
|
22
22
|
if (!isVisible) return null;
|
|
23
23
|
|
|
24
24
|
return (
|
|
25
|
-
<div
|
|
26
|
-
className={`onboarding-overlay ${isFading ? "fade-out" : ""}`}
|
|
27
|
-
onClick={handleDismiss}
|
|
28
|
-
>
|
|
25
|
+
<div className={`onboarding-overlay ${isFading ? "fade-out" : ""}`} onClick={handleDismiss}>
|
|
29
26
|
<div className="onboarding-card" onClick={(e) => e.stopPropagation()}>
|
|
30
|
-
<button
|
|
31
|
-
className="close-icon-btn"
|
|
32
|
-
onClick={handleDismiss}
|
|
33
|
-
aria-label="Close"
|
|
34
|
-
>
|
|
27
|
+
<button className="close-icon-btn" onClick={handleDismiss} aria-label="Close">
|
|
35
28
|
<svg width="12" height="12" viewBox="0 0 12 12" fill="none">
|
|
36
29
|
<path
|
|
37
30
|
d="M1 1L11 11M1 11L11 1"
|
|
@@ -63,10 +56,7 @@ const Onboarding: React.FC<OnboardingProps> = ({ config }) => {
|
|
|
63
56
|
</div>
|
|
64
57
|
|
|
65
58
|
<div className="onboarding-actions">
|
|
66
|
-
<button
|
|
67
|
-
className="get-started-btn"
|
|
68
|
-
onClick={() => openLink("{{get-started-link}}")}
|
|
69
|
-
>
|
|
59
|
+
<button className="get-started-btn" onClick={() => openLink("{{get-started-link}}")}>
|
|
70
60
|
Get Started
|
|
71
61
|
</button>
|
|
72
62
|
<button
|
|
@@ -78,10 +68,7 @@ const Onboarding: React.FC<OnboardingProps> = ({ config }) => {
|
|
|
78
68
|
>
|
|
79
69
|
Go to Custom App
|
|
80
70
|
</button>
|
|
81
|
-
<button
|
|
82
|
-
className="discord-btn"
|
|
83
|
-
onClick={() => openLink("{{discord-link}}")}
|
|
84
|
-
>
|
|
71
|
+
<button className="discord-btn" onClick={() => openLink("{{discord-link}}")}>
|
|
85
72
|
Join Discord
|
|
86
73
|
</button>
|
|
87
74
|
<button className="dismiss-btn" onClick={handleDismiss}>
|
package/templates/wrapper.js
CHANGED