@spicemod/creator 0.0.23 → 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/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 templateCustomAppWrapperFilePath = dist("templates/customAppWrapper.js", import.meta.url);
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.23";
146
+ var version = "0.0.24";
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
- function safeParse(schema, data, type = "CLI") {
485
- const result = v.safeParse(schema, data);
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
- logger$2.error(`\n${pc.bgRed(pc.black(" ERROR "))} ${pc.red(`Invalid ${type} options:`)}`);
488
- result.issues.forEach((issue) => {
489
- const path = issue.path?.map((p) => p.key).join(".") || "input";
490
- logger$2.error(`${pc.dim(" └─")} ${pc.yellow(path)}: ${pc.white(issue.message)}`);
491
- });
492
- logger$2.error(`\n${pc.dim("Check your command flags and try again.")}\n`);
493
- process.exit(1);
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: false,
588
+ configFileRequired: true,
546
589
  packageJson: true,
547
590
  async onUpdate({ newConfig }) {
548
- await runCb(await getResolvedConfig(newConfig.config), true);
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
- try {
556
- return safeParse(OptionsSchema$1, await resolveContext(config), "Config");
557
- } catch (e) {
558
- const message = e instanceof Error ? e.message : String(e);
559
- logger$1.error(pc.red(`Failed to load configuration: ${message}`));
560
- process.exit(1);
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();
@@ -633,6 +683,28 @@ function resolveActiveIcon(cwd) {
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 (SKIP_SPICETIFY) {
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
- cache.files.set(renamedPath, {
998
- contents: Buffer.from(combinedCode),
999
- name: targetName
1000
- });
1001
- cache.changed.add(renamedPath);
1002
- cache.hasChanges = true;
1003
- filesChanged.push(renamedPath);
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
- cache.files.set(renamedPath, {
1008
- name: targetName,
1009
- contents: file.contents
1010
- });
1011
- cache.changed.add(renamedPath);
1012
- cache.hasChanges = true;
1013
- filesChanged.push(renamedPath);
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.set(renamedPath, {
1036
- name: targetName,
1037
- contents: nextBuffer
1038
- });
1039
- cache.changed.add(renamedPath);
1040
- cache.hasChanges = true;
1041
- filesChanged.push(renamedPath);
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: Buffer.from(manifestString),
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: !dev && template === "extension"
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 { js: `${urlSlugify(getEnName(config.name))}.js` };
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": env.isInternal ? "link:@spicetify/creator" : "latest" }
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
- await mkdir(dirname$1(to), { recursive: true });
1614
- await writeFile$1(to, text, "utf8");
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} -> ${to})`);
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;
@@ -2080,7 +2137,7 @@ const render = () => {
2080
2137
  writeFileSync(extensionJsPath, extensionCode);
2081
2138
  const manifestPath = resolve(destDir, "manifest.json");
2082
2139
  const manifest = {
2083
- name: identifier,
2140
+ name: config.name,
2084
2141
  subfiles: [],
2085
2142
  subfiles_extension: [outFiles.jsExtension ?? "extension.js"],
2086
2143
  icon: config.icon.default,
@@ -2110,7 +2167,7 @@ async function dev$1(options) {
2110
2167
  logger$2.greeting(pc.green("Starting development environment"));
2111
2168
  let ctx;
2112
2169
  let server = void 0;
2113
- loadConfig(async (config, isNewUpdate) => {
2170
+ await loadConfig(async (config, isNewUpdate) => {
2114
2171
  if (isNewUpdate) {
2115
2172
  logger$2.clear();
2116
2173
  logger$2.info(pc.green("Config updated, reloading..."));
@@ -2122,7 +2179,7 @@ async function dev$1(options) {
2122
2179
  port: options.port ?? config.serverConfig.port
2123
2180
  });
2124
2181
  await server.start();
2125
- const outFiles = getOutFiles(config);
2182
+ const outFiles = getOutFiles(config, true);
2126
2183
  if (config.template === "custom-app") await injectHMRCustomApp(server.link, server.wsLink, outFiles, config);
2127
2184
  else await injectHMRExtension(server.link, server.wsLink, outFiles);
2128
2185
  ctx = await context(getJSDevOptions(config, {
@@ -2136,13 +2193,9 @@ async function dev$1(options) {
2136
2193
  logger$2.error("Failed to start dev server: ", err);
2137
2194
  }
2138
2195
  return async () => {
2139
- try {
2140
- await ctx?.dispose();
2141
- ctx = void 0;
2142
- await server?.stop();
2143
- } finally {
2144
- process.exit();
2145
- }
2196
+ await ctx?.dispose();
2197
+ ctx = void 0;
2198
+ await server?.stop();
2146
2199
  };
2147
2200
  });
2148
2201
  }
@@ -2209,7 +2262,7 @@ const update_types = new Command("update-types").description("Update Spicetify T
2209
2262
  //#region src/bin.ts
2210
2263
  logger$2.debug(`Env: ${JSON.stringify(env, null, 2)}\n`);
2211
2264
  const command = new Command();
2212
- command.addCommand(create.alias("init")).addCommand(build$1).addCommand(dev).addCommand(update_types);
2265
+ command.version(version).addCommand(create.alias("init")).addCommand(build$1).addCommand(dev).addCommand(update_types);
2213
2266
  command.parse();
2214
2267
 
2215
2268
  //#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>], 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>], 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 = () => {
6
5
  const [count, setCount] = useState(0);
@@ -2,6 +2,7 @@ import { defineConfig } from "@spicetify/creator";
2
2
 
3
3
  // Learn more: {{config-reference-link}}
4
4
  export default defineConfig({
5
+ name: "{{project-name}}",
5
6
  framework: "{{framework}}",
6
7
  linter: "{{linter}}",
7
8
  template: "{{template}}",
@@ -2,6 +2,7 @@ import { defineConfig } from "@spicetify/creator";
2
2
 
3
3
  // Learn more: {{config-reference-link}}
4
4
  export default defineConfig({
5
+ name: "{{project-name}}",
5
6
  framework: "{{framework}}",
6
7
  linter: "{{linter}}",
7
8
  template: "{{template}}",
@@ -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}>
@@ -0,0 +1,6 @@
1
+ import React from "react";
2
+ import App from "virtual:app";
3
+
4
+ export default function render() {
5
+ return <App />;
6
+ }
@@ -2,6 +2,7 @@ import { defineConfig } from "@spicetify/creator";
2
2
 
3
3
  // Learn more: {{config-reference-link}}
4
4
  export default defineConfig({
5
+ name: "{{project-name}}",
5
6
  framework: "{{framework}}",
6
7
  linter: "{{linter}}",
7
8
  template: "{{template}}",
@@ -2,6 +2,7 @@ import { defineConfig } from "@spicetify/creator";
2
2
 
3
3
  // Learn more: {{config-reference-link}}
4
4
  export default defineConfig({
5
+ name: "{{project-name}}",
5
6
  framework: "{{framework}}",
6
7
  linter: "{{linter}}",
7
8
  template: "{{template}}",
@@ -2,6 +2,7 @@ import { defineConfig } from "@spicetify/creator";
2
2
 
3
3
  // Learn more: {{config-reference-link}}
4
4
  export default defineConfig({
5
+ name: "{{project-name}}",
5
6
  framework: "{{framework}}",
6
7
  linter: "{{linter}}",
7
8
  template: "{{template}}",
@@ -2,6 +2,7 @@ import { defineConfig } from "@spicetify/creator";
2
2
 
3
3
  // Learn more: {{config-reference-link}}
4
4
  export default defineConfig({
5
+ name: "{{project-name}}",
5
6
  framework: "{{framework}}",
6
7
  linter: "{{linter}}",
7
8
  template: "{{template}}",
@@ -6,7 +6,7 @@ __ESBUILD__HAS_CSS &&
6
6
  style.setAttribute("data-app", __ESBUILD__APP_ID);
7
7
  style.textContent = css;
8
8
  document.head.appendChild(style);
9
- } catch { }
9
+ } catch {}
10
10
  })();
11
11
  (async () => {
12
12
  const _ID = `${__ESBUILD__APP_SLUG}-${__ESBUILD__APP_TYPE}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spicemod/creator",
3
- "version": "0.0.23",
3
+ "version": "0.0.24",
4
4
  "description": "Easily make Spicetify extensions and themes",
5
5
  "keywords": [
6
6
  "cli",
@@ -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 = () => {
6
5
  const [count, setCount] = useState(0);
@@ -2,6 +2,7 @@ import { defineConfig } from "@spicetify/creator";
2
2
 
3
3
  // Learn more: {{config-reference-link}}
4
4
  export default defineConfig({
5
+ name: "{{project-name}}",
5
6
  framework: "{{framework}}",
6
7
  linter: "{{linter}}",
7
8
  template: "{{template}}",
@@ -2,6 +2,7 @@ import { defineConfig } from "@spicetify/creator";
2
2
 
3
3
  // Learn more: {{config-reference-link}}
4
4
  export default defineConfig({
5
+ name: "{{project-name}}",
5
6
  framework: "{{framework}}",
6
7
  linter: "{{linter}}",
7
8
  template: "{{template}}",
@@ -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}>
@@ -0,0 +1,6 @@
1
+ import React from "react";
2
+ import App from "virtual:app";
3
+
4
+ export default function render() {
5
+ return <App />;
6
+ }
@@ -2,6 +2,7 @@ import { defineConfig } from "@spicetify/creator";
2
2
 
3
3
  // Learn more: {{config-reference-link}}
4
4
  export default defineConfig({
5
+ name: "{{project-name}}",
5
6
  framework: "{{framework}}",
6
7
  linter: "{{linter}}",
7
8
  template: "{{template}}",
@@ -2,6 +2,7 @@ import { defineConfig } from "@spicetify/creator";
2
2
 
3
3
  // Learn more: {{config-reference-link}}
4
4
  export default defineConfig({
5
+ name: "{{project-name}}",
5
6
  framework: "{{framework}}",
6
7
  linter: "{{linter}}",
7
8
  template: "{{template}}",
@@ -2,6 +2,7 @@ import { defineConfig } from "@spicetify/creator";
2
2
 
3
3
  // Learn more: {{config-reference-link}}
4
4
  export default defineConfig({
5
+ name: "{{project-name}}",
5
6
  framework: "{{framework}}",
6
7
  linter: "{{linter}}",
7
8
  template: "{{template}}",
@@ -2,6 +2,7 @@ import { defineConfig } from "@spicetify/creator";
2
2
 
3
3
  // Learn more: {{config-reference-link}}
4
4
  export default defineConfig({
5
+ name: "{{project-name}}",
5
6
  framework: "{{framework}}",
6
7
  linter: "{{linter}}",
7
8
  template: "{{template}}",
@@ -6,7 +6,7 @@ __ESBUILD__HAS_CSS &&
6
6
  style.setAttribute("data-app", __ESBUILD__APP_ID);
7
7
  style.textContent = css;
8
8
  document.head.appendChild(style);
9
- } catch { }
9
+ } catch {}
10
10
  })();
11
11
  (async () => {
12
12
  const _ID = `${__ESBUILD__APP_SLUG}-${__ESBUILD__APP_TYPE}`;