@spicemod/creator 0.0.22 → 0.0.23

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.
Files changed (70) hide show
  1. package/client.d.ts +47 -0
  2. package/dist/bin.mjs +684 -245
  3. package/dist/index.d.mts +697 -0
  4. package/dist/{client/index.mjs → index.mjs} +1 -1
  5. package/dist/templates/custom-app/js/react/eslint.config.ts +29 -0
  6. package/dist/templates/custom-app/js/react/src/app.jsx +22 -0
  7. package/dist/templates/custom-app/js/react/src/components/Onboarding.jsx +82 -0
  8. package/dist/templates/custom-app/js/react/src/extension/index.jsx +23 -0
  9. package/dist/templates/custom-app/meta.json +4 -0
  10. package/dist/templates/custom-app/shared/DOT-gitignore +34 -0
  11. package/dist/templates/custom-app/shared/DOT-oxlintrc.json +36 -0
  12. package/dist/templates/custom-app/shared/README.template.md +53 -0
  13. package/dist/templates/custom-app/shared/app.css +163 -0
  14. package/dist/templates/custom-app/shared/biome.json +36 -0
  15. package/dist/templates/custom-app/shared/css/app.module.scss +58 -0
  16. package/dist/templates/custom-app/shared/icon-active.svg +7 -0
  17. package/dist/templates/custom-app/shared/icon.svg +7 -0
  18. package/dist/templates/custom-app/shared/jsconfig.json +32 -0
  19. package/dist/templates/custom-app/shared/spice.config.js +9 -0
  20. package/dist/templates/custom-app/shared/spice.config.ts +9 -0
  21. package/dist/templates/custom-app/shared/tsconfig.json +32 -0
  22. package/dist/templates/custom-app/ts/react/eslint.config.ts +29 -0
  23. package/dist/templates/custom-app/ts/react/src/app.tsx +23 -0
  24. package/dist/templates/custom-app/ts/react/src/components/Onboarding.tsx +105 -0
  25. package/dist/templates/custom-app/ts/react/src/extension/index.tsx +27 -0
  26. package/dist/templates/extension/js/vanilla/src/components/Onboarding.js +71 -0
  27. package/dist/templates/extension/shared/DOT-gitignore +34 -0
  28. package/dist/templates/extension/shared/DOT-oxlintrc.json +36 -0
  29. package/dist/templates/extension/shared/spice.config.js +1 -1
  30. package/dist/templates/extension/shared/spice.config.ts +1 -1
  31. package/dist/templates/liveReload.js +0 -1
  32. package/dist/templates/theme/shared/DOT-gitignore +34 -0
  33. package/dist/templates/theme/shared/DOT-oxlintrc.json +36 -0
  34. package/dist/templates/theme/shared/spice.config.js +1 -1
  35. package/dist/templates/theme/shared/spice.config.ts +1 -1
  36. package/dist/templates/wrapper.js +6 -9
  37. package/package.json +7 -3
  38. package/templates/custom-app/js/react/eslint.config.ts +29 -0
  39. package/templates/custom-app/js/react/src/app.jsx +22 -0
  40. package/templates/custom-app/js/react/src/components/Onboarding.jsx +82 -0
  41. package/templates/custom-app/js/react/src/extension/index.jsx +23 -0
  42. package/templates/custom-app/meta.json +4 -0
  43. package/templates/custom-app/shared/DOT-gitignore +34 -0
  44. package/templates/custom-app/shared/DOT-oxlintrc.json +36 -0
  45. package/templates/custom-app/shared/README.template.md +53 -0
  46. package/templates/custom-app/shared/app.css +163 -0
  47. package/templates/custom-app/shared/biome.json +36 -0
  48. package/templates/custom-app/shared/css/app.module.scss +58 -0
  49. package/templates/custom-app/shared/icon-active.svg +7 -0
  50. package/templates/custom-app/shared/icon.svg +7 -0
  51. package/templates/custom-app/shared/jsconfig.json +32 -0
  52. package/templates/custom-app/shared/spice.config.js +9 -0
  53. package/templates/custom-app/shared/spice.config.ts +9 -0
  54. package/templates/custom-app/shared/tsconfig.json +32 -0
  55. package/templates/custom-app/ts/react/eslint.config.ts +29 -0
  56. package/templates/custom-app/ts/react/src/app.tsx +23 -0
  57. package/templates/custom-app/ts/react/src/components/Onboarding.tsx +105 -0
  58. package/templates/custom-app/ts/react/src/extension/index.tsx +27 -0
  59. package/templates/extension/js/vanilla/src/components/Onboarding.js +71 -0
  60. package/templates/extension/shared/DOT-gitignore +34 -0
  61. package/templates/extension/shared/DOT-oxlintrc.json +36 -0
  62. package/templates/extension/shared/spice.config.js +1 -1
  63. package/templates/extension/shared/spice.config.ts +1 -1
  64. package/templates/liveReload.js +0 -1
  65. package/templates/theme/shared/DOT-gitignore +34 -0
  66. package/templates/theme/shared/DOT-oxlintrc.json +36 -0
  67. package/templates/theme/shared/spice.config.js +1 -1
  68. package/templates/theme/shared/spice.config.ts +1 -1
  69. package/templates/wrapper.js +6 -9
  70. 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,7 +10,7 @@ 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
16
  import { gzipSync } from "node:zlib";
@@ -22,8 +22,11 @@ import postcssImport from "postcss-import";
22
22
  import postcssPresetEnv from "postcss-preset-env";
23
23
  import { readFile, writeFile } from "node:fs/promises";
24
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 } 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 = ["extension", "theme"];
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 templateFilePath = dist("templates/wrapper.js", import.meta.url);
140
+ const templateWrapperFilePath = dist("templates/wrapper.js", import.meta.url);
141
+ const templateCustomAppWrapperFilePath = dist("templates/customAppWrapper.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.22";
146
+ var version = "0.0.23";
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";
@@ -291,6 +248,7 @@ const DEV_MODE_VAR_NAME = `__SPICE_CREATOR_DEV__`;
291
248
  const CHECK = pc.bold(pc.green("✔"));
292
249
  const CROSS = pc.bold(pc.red("✖"));
293
250
  const WARN = pc.bold(pc.yellow("⚠"));
251
+ const SKIP_SPICETIFY = process.env.SPICETIFY_SKIP === "true" || process.env.CI === "true";
294
252
  const VALID_PROJECT_FILES = new Set([
295
253
  ".DS_Store",
296
254
  ".git",
@@ -313,6 +271,150 @@ const VALID_PROJECT_FILES = new Set([
313
271
  "yarnrc.yml",
314
272
  ".yarn"
315
273
  ]);
274
+ const CUSTOM_APP_NAME_LOCALES = [
275
+ "ms",
276
+ "gu",
277
+ "ko",
278
+ "pa-IN",
279
+ "az",
280
+ "ru",
281
+ "uk",
282
+ "nb",
283
+ "sv",
284
+ "sw",
285
+ "ur",
286
+ "bho",
287
+ "pa-PK",
288
+ "te",
289
+ "ro",
290
+ "vi",
291
+ "am",
292
+ "bn",
293
+ "en",
294
+ "id",
295
+ "bg",
296
+ "da",
297
+ "es-419",
298
+ "mr",
299
+ "ml",
300
+ "th",
301
+ "tr",
302
+ "is",
303
+ "fa",
304
+ "or",
305
+ "he",
306
+ "hi",
307
+ "zh-TW",
308
+ "sr",
309
+ "pt-BR",
310
+ "zu",
311
+ "nl",
312
+ "es",
313
+ "lt",
314
+ "ja",
315
+ "st",
316
+ "it",
317
+ "el",
318
+ "pt-PT",
319
+ "kn",
320
+ "de",
321
+ "fr",
322
+ "ne",
323
+ "ar",
324
+ "af",
325
+ "et",
326
+ "pl",
327
+ "ta",
328
+ "sl",
329
+ "pk",
330
+ "hr",
331
+ "sk",
332
+ "fi",
333
+ "lv",
334
+ "fil",
335
+ "fr-CA",
336
+ "cs",
337
+ "zh-CN",
338
+ "hu"
339
+ ];
340
+
341
+ //#endregion
342
+ //#region src/config/schema.ts
343
+ const ServerConfigSchema = v.object({
344
+ port: v.optional(v.number()),
345
+ serveDir: v.string(),
346
+ hmrPath: v.optional(v.string())
347
+ });
348
+ const EntryFileSchema = v.string();
349
+ const AssetEntrySchema = v.object({
350
+ js: EntryFileSchema,
351
+ css: EntryFileSchema
352
+ });
353
+ const CustomAppEntrySchema = v.object({
354
+ extension: EntryFileSchema,
355
+ app: EntryFileSchema
356
+ });
357
+ const LocaleNameSchema = v.intersect([v.object({ en: v.string() }), v.record(v.picklist(CUSTOM_APP_NAME_LOCALES), v.string())]);
358
+ const ExtensionTemplateSchema = v.object({
359
+ name: v.string(),
360
+ template: v.literal("extension"),
361
+ entry: EntryFileSchema
362
+ });
363
+ const ThemeTemplateSchema = v.object({
364
+ name: v.string(),
365
+ template: v.literal("theme"),
366
+ entry: AssetEntrySchema
367
+ });
368
+ const CustomAppTemplateSchema = v.object({
369
+ name: v.union([v.string(), LocaleNameSchema]),
370
+ icon: v.object({
371
+ default: v.string(),
372
+ active: v.optional(v.string())
373
+ }),
374
+ template: v.literal("custom-app"),
375
+ entry: CustomAppEntrySchema
376
+ });
377
+ const TemplateSpecificSchema = v.variant("template", [
378
+ ExtensionTemplateSchema,
379
+ ThemeTemplateSchema,
380
+ CustomAppTemplateSchema
381
+ ]);
382
+ const TemplateSpecificOptionalSchema = v.variant("template", [
383
+ v.partial(ExtensionTemplateSchema),
384
+ v.partial(ThemeTemplateSchema),
385
+ v.partial(CustomAppTemplateSchema)
386
+ ]);
387
+ const RequiredCommonSchema = v.object({
388
+ outDir: v.string(),
389
+ linter: v.picklist(linterTypes),
390
+ framework: v.picklist(frameworkTypes),
391
+ packageManager: v.picklist(packageManagers),
392
+ esbuildOptions: v.record(v.string(), v.any()),
393
+ serverConfig: v.partial(ServerConfigSchema),
394
+ version: v.string()
395
+ });
396
+ const OptionalCommonSchema = v.object({ devModeVarName: v.optional(v.string()) });
397
+ const FileOptionsSchema = v.intersect([
398
+ v.partial(RequiredCommonSchema),
399
+ OptionalCommonSchema,
400
+ TemplateSpecificOptionalSchema
401
+ ]);
402
+ const OptionsSchema$1 = v.intersect([
403
+ v.required(RequiredCommonSchema),
404
+ OptionalCommonSchema,
405
+ TemplateSpecificSchema
406
+ ]);
407
+
408
+ //#endregion
409
+ //#region src/env.ts
410
+ const isInternal = process.env.SPICE_INTERNAL === "true";
411
+ const isDev = process.env.IS_DEV === "true";
412
+ const spicetifyBin = process.env.SPICETIFY_BIN || process.env.SPICE_BIN || "spicetify";
413
+ const env = {
414
+ isInternal,
415
+ isDev,
416
+ spicetifyBin
417
+ };
316
418
 
317
419
  //#endregion
318
420
  //#region src/utils/logger.ts
@@ -350,8 +452,15 @@ var Logger = class {
350
452
  this.add(console.warn, args);
351
453
  }
352
454
  error(...errors) {
353
- this.add(console.error, errors);
354
- for (const err of errors) if (err instanceof Error && this.isDev && err.stack) console.error(pc.dim(err.stack.split("\n").slice(1).join("\n")));
455
+ const formatted = [];
456
+ for (const err of errors) if (err instanceof Error) {
457
+ formatted.push(pc.red(err.message));
458
+ if (this.isDev && err.stack) {
459
+ const stack = err.stack.split("\n").slice(1).join("\n");
460
+ formatted.push(pc.dim(stack));
461
+ }
462
+ } else formatted.push(err);
463
+ this.add(console.error, formatted);
355
464
  }
356
465
  log(...args) {
357
466
  if (!this.isDev) return;
@@ -384,21 +493,39 @@ function safeParse(schema, data, type = "CLI") {
384
493
  process.exit(1);
385
494
  }
386
495
 
496
+ //#endregion
497
+ //#region src/config/globs.ts
498
+ const JS_EXTENSIONS = "{ts,tsx,js,jsx,mts,mjs,cts,cjs}";
499
+ const CSS_EXTENSIONS = "{css,scss,sass,less,styl,stylus,pcss,postcss}";
500
+ const withSrc = (dirs) => dirs.flatMap((dir) => [dir, `src/${dir}`]);
501
+ const createGlobs = (names, dirs, ext) => names.flatMap((name) => dirs.map((dir) => `${dir}${name}.${ext}`));
502
+ const JS_ENTRY_GLOBS = createGlobs(["app", "index"], withSrc([""]), JS_EXTENSIONS);
503
+ const JS_EXTENSION_ENTRY_GLOBS = createGlobs(["app", "index"], withSrc(["extension/"]), JS_EXTENSIONS);
504
+ const CSS_ENTRY_GLOBS = createGlobs(["app"], withSrc([
505
+ "",
506
+ "styles/",
507
+ "css/"
508
+ ]), CSS_EXTENSIONS);
509
+ const ICON_GLOBS = createGlobs(["icon", "logo"], withSrc([
510
+ "",
511
+ "icons/",
512
+ "assets/"
513
+ ]), "svg");
514
+ const ICON_ACTIVE_GLOBS = createGlobs(["icon-active", "logo-active"], withSrc([
515
+ "",
516
+ "icons/",
517
+ "assets/"
518
+ ]), "svg");
519
+ const ENTRY_MAP = {
520
+ js: [...JS_ENTRY_GLOBS, ...JS_EXTENSION_ENTRY_GLOBS],
521
+ css: CSS_ENTRY_GLOBS,
522
+ "js-app-only": JS_ENTRY_GLOBS,
523
+ "js-extension-only": JS_EXTENSION_ENTRY_GLOBS
524
+ };
525
+
387
526
  //#endregion
388
527
  //#region src/config/index.ts
389
528
  const logger$1 = createLogger("config");
390
- const JS_ENTRY_GLOBS = [
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
- ];
402
529
  const CONFIG_DEFAULTS = {
403
530
  outDir: "./dist",
404
531
  linter: "biome",
@@ -435,46 +562,77 @@ async function getResolvedConfig(config) {
435
562
  }
436
563
  async function resolveContext(config) {
437
564
  const cwd = process.cwd();
438
- const getPkg = () => {
439
- try {
440
- return JSON.parse(readFileSync(resolve(cwd, "package.json"), "utf-8"));
441
- } catch {
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;
565
+ const pkg = config.name && config.version ? {} : getPackageMeta(cwd);
566
+ config.name ||= pkg.name || basename(cwd);
567
+ config.version ||= pkg.version || "0.0.1";
568
+ config.entry ||= resolveConfigEntries(config.template, cwd);
458
569
  config.outDir = resolve(cwd, config.outDir || "./dist");
459
570
  config.esbuildOptions ??= {};
460
571
  config.serverConfig ??= {};
572
+ if (config.template === "custom-app") config.icon ??= {
573
+ default: resolveDefaultIcon(cwd),
574
+ active: resolveActiveIcon(cwd)
575
+ };
461
576
  return config;
462
577
  }
463
- function resolveDefaultEntries(cwd, type) {
464
- const resolveFile = (globs) => {
465
- for (const glob of globs) {
466
- const matches = globSync(glob, {
467
- cwd,
468
- absolute: true
469
- });
470
- if (matches.length > 0) return matches;
471
- }
472
- return [];
578
+ function getPackageMeta(cwd) {
579
+ try {
580
+ return JSON.parse(readFileSync(resolve(cwd, "package.json"), "utf-8"));
581
+ } catch {
582
+ return {};
583
+ }
584
+ }
585
+ function resolveConfigEntries(template, cwd) {
586
+ if (template === "theme") return {
587
+ js: resolveDefaultEntry(cwd, "js"),
588
+ css: resolveDefaultEntry(cwd, "css")
589
+ };
590
+ if (template === "custom-app") return {
591
+ app: resolveDefaultEntry(cwd, "js-app-only"),
592
+ extension: resolveDefaultEntry(cwd, "js-extension-only")
473
593
  };
474
- const firstEntry = resolveFile(type === "js" ? JS_ENTRY_GLOBS : CSS_ENTRY_GLOBS)[0];
475
- if (!firstEntry) throw new Error(type === "js" ? "No JavaScript entry found (src/app or src/index)." : "No CSS entry found (src/app, src/index, or src/styles).");
476
- return firstEntry;
594
+ return resolveDefaultEntry(cwd, "js");
477
595
  }
596
+ function resolveDefaultEntry(cwd, type) {
597
+ const globs = ENTRY_MAP[type];
598
+ for (const glob of globs) {
599
+ const matches = globSync(glob, {
600
+ cwd,
601
+ absolute: true
602
+ });
603
+ if (matches.length > 0 && matches[0]) return matches[0];
604
+ }
605
+ const displayType = {
606
+ js: "JavaScript/TypeScript",
607
+ css: "CSS/Stylesheet",
608
+ "js-app-only": "JavaScript/TypeScript (Custom App)",
609
+ "js-extension-only": "JavaScript/TypeScript (Extension)"
610
+ }[type];
611
+ const expectedFiles = globs.map((g) => ` - ${g}`).join("\n");
612
+ throw new Error(`No ${displayType} entry file found in your project.\nPlease create one of the following files:\n${expectedFiles}`);
613
+ }
614
+ function resolveDefaultIcon(cwd) {
615
+ for (const glob of ICON_GLOBS) {
616
+ const matches = globSync(glob, {
617
+ cwd,
618
+ absolute: true
619
+ });
620
+ if (matches.length > 0 && matches[0]) return readFileSync(matches[0]).toString();
621
+ }
622
+ const expectedFiles = ICON_GLOBS.map((g) => ` - ${g}`).join("\n");
623
+ throw new Error(`No icon file found in your project.\nPlease create one of the following files:\n${expectedFiles}`);
624
+ }
625
+ function resolveActiveIcon(cwd) {
626
+ for (const glob of ICON_ACTIVE_GLOBS) {
627
+ const matches = globSync(glob, {
628
+ cwd,
629
+ absolute: true
630
+ });
631
+ if (matches.length > 0 && matches[0]) return readFileSync(matches[0]).toString();
632
+ }
633
+ return "";
634
+ }
635
+ const getEnName = (configName) => typeof configName === "string" ? configName : configName.en;
478
636
 
479
637
  //#endregion
480
638
  //#region src/esbuild/format.ts
@@ -577,14 +735,14 @@ function css({ minify = false, inline = false, logger = createLogger("plugin:css
577
735
  type,
578
736
  transform: postcssModules({
579
737
  getJSON: () => {},
580
- generateScopedName: "[name]__[local]___[hash:base64:5]",
581
- localsConvention: "camelCaseOnly"
738
+ generateScopedName: "[name]__[local]___[hash:base64:5]"
582
739
  }, postCssPlugins)
583
740
  }), sassPlugin({
584
741
  filter: /\.(s[ac]ss|css)$/,
585
742
  type,
586
743
  async transform(css, _resolveDir, filePath) {
587
744
  const start = performance.now();
745
+ logger.log("processing:", filePath, "type:", type);
588
746
  const result = await postcss(postCssPlugins).process(css, { from: filePath });
589
747
  logger.debug("Global CSS processed", {
590
748
  filePath,
@@ -644,6 +802,7 @@ function runSpice(args) {
644
802
  validateSpicetify(env.spicetifyBin);
645
803
  return spawnSync(env.spicetifyBin, args, { encoding: "utf-8" });
646
804
  }
805
+ const getCustomAppsDir = () => join(getSpiceDataPath(), "CustomApps");
647
806
  const getExtensionDir = () => join(getSpiceDataPath(), "Extensions");
648
807
  const getThemesDir = () => join(getSpiceDataPath(), "Themes");
649
808
  async function getSpicetifyConfig() {
@@ -667,16 +826,16 @@ function validateSpicetify(bin) {
667
826
 
668
827
  //#endregion
669
828
  //#region src/esbuild/plugins/spicetifyHandlers.ts
670
- const skipSpicetify = process.env.SPICETIFY_SKIP === "true" || process.env.CI === "true";
671
- const spicetifyHandler = ({ config, options, cache, logger = createLogger("plugin:spicetifyHandler") }) => ({
829
+ const spicetifyHandler = ({ config, options, cache, logger = createLogger("plugin:spicetify-handler") }) => ({
672
830
  name: "spice_internal__spicetify-build-handler",
673
831
  async setup(build) {
674
832
  const { apply = true, copy = true, applyOnce = true, remove, outDir = "./dist" } = options;
675
833
  let hasAppliedOnce = false;
676
834
  const isExtension = config.template === "extension";
677
- const identifier = isExtension ? `${urlSlugify(config.name)}.js` : urlSlugify(config.name);
678
- if (skipSpicetify) {
679
- logger.info(pc.yellow("SPICETIFY_SKIP=true, skipping spicetify operations"));
835
+ const isCustomApp = config.template === "custom-app";
836
+ const identifier = isExtension ? `${urlSlugify(config.name)}.js` : urlSlugify(getEnName(config.name));
837
+ if (SKIP_SPICETIFY) {
838
+ logger.info(pc.yellow("skipping spicetify operations"));
680
839
  build.onEnd(async (result) => {
681
840
  if (result.errors.length > 0) return;
682
841
  if (!cache.hasChanges || cache.changed.size === 0) return;
@@ -702,38 +861,36 @@ const spicetifyHandler = ({ config, options, cache, logger = createLogger("plugi
702
861
  const spiceConfig = await getSpicetifyConfig();
703
862
  logger.debug(pc.green("Spicetify Config: "), spiceConfig);
704
863
  if (apply) {
705
- const defaultTheme = spiceConfig.Setting.current_theme;
706
- build.onStart(() => {
707
- const spiceIdentifier = remove ? `${identifier}-` : identifier;
708
- if (isExtension) runSpice([
709
- "config",
710
- "extensions",
711
- spiceIdentifier
712
- ]);
713
- else runSpice([
714
- "config",
715
- "current_theme",
716
- spiceIdentifier
717
- ]);
718
- });
864
+ const defaultTheme = spiceConfig?.Setting?.current_theme || "SpicetifyDefault";
865
+ const spiceIdentifier = remove ? `${identifier}-` : identifier;
866
+ runSpice([
867
+ "config",
868
+ isExtension ? "extensions" : isCustomApp ? "custom_apps" : "current_theme",
869
+ spiceIdentifier
870
+ ]);
719
871
  if (!isExtension && !remove) {
720
- const resetTheme = () => {
721
- runSpice([
872
+ const cleanup = () => {
873
+ if (isCustomApp) runSpice([
874
+ "config",
875
+ "custom_apps",
876
+ `${identifier}-`
877
+ ]);
878
+ else runSpice([
722
879
  "config",
723
880
  "current_theme",
724
881
  defaultTheme
725
882
  ]);
726
883
  process.exit();
727
884
  };
728
- process.once("SIGINT", resetTheme);
729
- process.once("SIGTERM", resetTheme);
885
+ process.once("SIGINT", cleanup);
886
+ process.once("SIGTERM", cleanup);
730
887
  }
731
888
  }
732
889
  build.onEnd(async (result) => {
733
890
  if (result.errors.length > 0) return;
734
891
  if (!cache.hasChanges || cache.changed.size === 0) return;
735
892
  const destDirs = [resolve(outDir)];
736
- if (copy) destDirs.push(isExtension ? getExtensionDir() : resolve(getThemesDir(), identifier));
893
+ if (copy) destDirs.push(isExtension ? getExtensionDir() : isCustomApp ? resolve(getCustomAppsDir(), identifier) : resolve(getThemesDir(), identifier));
737
894
  const tasks = [];
738
895
  for (const filePath of cache.changed) {
739
896
  const fileData = cache.files.get(filePath);
@@ -754,8 +911,8 @@ const spicetifyHandler = ({ config, options, cache, logger = createLogger("plugi
754
911
  return;
755
912
  }
756
913
  if (apply && cache.hasChanges && (!applyOnce || !hasAppliedOnce)) {
757
- const { stderr, status } = runSpice(["apply"]);
758
- if (status !== 0) logger.error(pc.red(`${CROSS} Spicetify apply failed: ${stderr}`));
914
+ const { stdout, stderr, status } = runSpice(["apply"]);
915
+ if (status !== 0) logger.error(pc.red(`${CROSS} Spicetify apply failed:`), stdout, stderr);
759
916
  else hasAppliedOnce = true;
760
917
  }
761
918
  });
@@ -764,26 +921,88 @@ const spicetifyHandler = ({ config, options, cache, logger = createLogger("plugi
764
921
 
765
922
  //#endregion
766
923
  //#region src/esbuild/plugins/wrapWithLoader.ts
767
- function wrapWithLoader({ name, type, version, cache, outFiles, server, dev = false, logger = createLogger("plugin:wrapWithLoader") }) {
924
+ function wrapWithLoader({ config, cache, outFiles, server, dev = false, logger = createLogger("plugin:wrapper") }) {
768
925
  const namespace = "spice_internal__wrap-with-loader";
926
+ const name = typeof config.name === "string" ? config.name : config.name.en;
927
+ const { template: type, version } = config;
928
+ let previousManifestHash;
769
929
  return {
770
930
  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) => {
931
+ setup(build$3) {
932
+ if (build$3.initialOptions.write !== false) throw new Error(`[${namespace}] This plugin requires "write: false" in build options.`);
933
+ build$3.onEnd(async (res) => {
774
934
  try {
775
935
  if (res.errors.length > 0 || !res.outputFiles) return;
776
936
  cache.changed.clear();
777
937
  cache.hasChanges = false;
778
938
  const filesChanged = [];
779
- let bundledCss = "";
780
- if (!dev && type === "extension") bundledCss = res.outputFiles.filter((f) => f.path.endsWith(".css")).map((f) => f.text).join("");
939
+ const outdir = resolve(build$3.initialOptions.outdir || "./dist");
940
+ const bundledCss = getBundledCss(res.outputFiles, outdir, type, dev);
941
+ const minify = build$3.initialOptions.minify;
942
+ const slug = varSlugify(`${name}_${version}`);
781
943
  const transformPromises = res.outputFiles.map(async (file) => {
782
944
  const isJs = file.path.endsWith(".js");
783
945
  const isCss = file.path.endsWith(".css");
784
946
  if (!dev && isCss && type === "extension") return;
785
- const targetName = isJs ? outFiles.js : isCss ? outFiles.css ?? basename(file.path) : basename(file.path);
786
- const renamedPath = join(build.initialOptions.outdir || "./dist/", targetName);
947
+ const relPath = file.path.slice(outdir.length);
948
+ const isCustomAppExtension = type === "custom-app" && isExtensionDir(relPath);
949
+ let targetName;
950
+ if (isJs) targetName = isCustomAppExtension ? outFiles.jsExtension ?? "extension.js" : outFiles.js;
951
+ else if (isCss && !isCustomAppExtension) targetName = outFiles.css;
952
+ if (!targetName) {
953
+ logger.debug("Skipped file: ", file.path);
954
+ return;
955
+ }
956
+ const renamedPath = join(build$3.initialOptions.outdir || "./dist/", targetName);
957
+ if (type === "custom-app" && targetName === outFiles.js && isJs) {
958
+ const globalName = varSlugify(getEnName(config.name));
959
+ const final = (await build({
960
+ bundle: true,
961
+ write: false,
962
+ minify,
963
+ platform: "browser",
964
+ format: "iife",
965
+ globalName,
966
+ 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
+ `,
975
+ loader: "tsx",
976
+ sourcefile: "entry.tsx"
977
+ },
978
+ plugins: [plugins.externalGlobal({ react: "Spicetify.React" }), {
979
+ name: "virtual-modules",
980
+ setup(vBuild) {
981
+ vBuild.onResolve({ filter: /^virtual:app$/ }, () => ({
982
+ path: "app",
983
+ namespace: "virtual"
984
+ }));
985
+ vBuild.onLoad({
986
+ filter: /.*/,
987
+ namespace: "virtual"
988
+ }, () => ({
989
+ contents: file.text,
990
+ loader: "js"
991
+ }));
992
+ }
993
+ }]
994
+ })).outputFiles?.[0];
995
+ if (!final) return;
996
+ 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);
1004
+ return;
1005
+ }
787
1006
  if (!isJs) {
788
1007
  cache.files.set(renamedPath, {
789
1008
  name: targetName,
@@ -794,31 +1013,25 @@ function wrapWithLoader({ name, type, version, cache, outFiles, server, dev = fa
794
1013
  filesChanged.push(renamedPath);
795
1014
  return;
796
1015
  }
797
- const slug = varSlugify(`${name}_${version}`);
798
- const templateRaw = readFileSync(templateFilePath, "utf-8");
799
- const minify = build.initialOptions.minify;
800
- const { code: transformedTemp } = await transform(templateRaw, {
1016
+ const { code: transformedTemp } = await transform(readFileSync(templateWrapperFilePath, "utf-8"), {
801
1017
  minify,
802
- target: build.initialOptions.target || "es2020",
1018
+ target: build$3.initialOptions.target || "es2020",
803
1019
  loader: "jsx",
804
1020
  define: {
805
- __ESBUILD__HAS_CSS: JSON.stringify(type === "extension"),
1021
+ __ESBUILD__HAS_CSS: JSON.stringify(type !== "theme"),
1022
+ __ESBUILD__INJECTED_CSS: JSON.stringify(bundledCss),
806
1023
  __ESBUILD__APP_SLUG: JSON.stringify(slug),
807
1024
  __ESBUILD__APP_TYPE: JSON.stringify(type),
808
1025
  __ESBUILD__APP_ID: JSON.stringify(varSlugify(name)),
809
- __ESBUILD__APP_VERSION: JSON.stringify(version),
810
- __ESBUILD__APP_HASH: JSON.stringify("")
1026
+ __ESBUILD__APP_VERSION: JSON.stringify(version)
811
1027
  }
812
1028
  });
813
1029
  const template = replace(transformedTemp, {
814
1030
  "\"{{INJECT_START_COMMENT}}\"": minify ? "" : "/* --- START --- */",
815
1031
  "\"{{INJECT_END_COMMENT}}\"": minify ? "" : "/* --- END --- */",
816
- "{{INJECTED_CSS_HERE}}": bundledCss,
817
1032
  "\"{{INJECTED_JS_HERE}}\"": file.text
818
1033
  });
819
1034
  const nextBuffer = Buffer.from(template);
820
- const previous = cache.files.get(renamedPath);
821
- if (previous?.contents && Buffer.compare(previous.contents, nextBuffer) === 0) return;
822
1035
  cache.files.set(renamedPath, {
823
1036
  name: targetName,
824
1037
  contents: nextBuffer
@@ -828,6 +1041,29 @@ function wrapWithLoader({ name, type, version, cache, outFiles, server, dev = fa
828
1041
  filesChanged.push(renamedPath);
829
1042
  });
830
1043
  await Promise.all(transformPromises);
1044
+ if (type === "custom-app") {
1045
+ const icon = config.icon;
1046
+ const manifestPath = join(outdir, "manifest.json");
1047
+ const manifest = {
1048
+ name: config.name,
1049
+ subfiles: [],
1050
+ subfiles_extension: ["extension.js"],
1051
+ icon: icon.default,
1052
+ "active-icon": icon.active ?? ""
1053
+ };
1054
+ const manifestString = JSON.stringify(manifest, null, 2);
1055
+ const currentHash = createHash("md5").update(manifestString).digest("hex");
1056
+ if (currentHash !== previousManifestHash) {
1057
+ previousManifestHash = currentHash;
1058
+ cache.files.set(manifestPath, {
1059
+ contents: Buffer.from(manifestString),
1060
+ name: "manifest.json"
1061
+ });
1062
+ cache.changed.add(manifestPath);
1063
+ cache.hasChanges = true;
1064
+ filesChanged.push(manifestPath);
1065
+ }
1066
+ }
831
1067
  if (filesChanged.length > 0) server?.broadcast(filesChanged);
832
1068
  } catch (e) {
833
1069
  logger.error(`Error: ${e instanceof Error ? e.message : String(e)}`);
@@ -836,6 +1072,15 @@ function wrapWithLoader({ name, type, version, cache, outFiles, server, dev = fa
836
1072
  }
837
1073
  };
838
1074
  }
1075
+ function isExtensionDir(relPath) {
1076
+ return relPath.startsWith("/extension/") || relPath.startsWith("\\extension\\");
1077
+ }
1078
+ function getBundledCss(files, outdir, type, dev) {
1079
+ const cssFiles = files.filter((f) => f.path.endsWith(".css"));
1080
+ if (!dev && type === "extension") return cssFiles.map((f) => f.text).join("");
1081
+ if (type === "custom-app") return cssFiles.filter((f) => isExtensionDir(f.path.slice(outdir.length))).map((f) => f.text).join("");
1082
+ return "";
1083
+ }
839
1084
 
840
1085
  //#endregion
841
1086
  //#region src/esbuild/plugins/index.ts
@@ -861,7 +1106,7 @@ const defaultBuildOptions = {
861
1106
  target: ["es2022", "chrome120"]
862
1107
  };
863
1108
  const getCommonPlugins = (opts) => {
864
- const { template, minify, cache, name, version, buildOptions, outFiles, server, dev } = opts;
1109
+ const { template, minify, cache, buildOptions, outFiles, server, dev } = opts;
865
1110
  return [
866
1111
  ...plugins.css({
867
1112
  minify,
@@ -875,9 +1120,7 @@ const getCommonPlugins = (opts) => {
875
1120
  "react/jsx-runtime": "Spicetify.ReactJSX"
876
1121
  }),
877
1122
  plugins.wrapWithLoader({
878
- name,
879
- version,
880
- type: template,
1123
+ config: opts,
881
1124
  cache,
882
1125
  outFiles,
883
1126
  server,
@@ -891,11 +1134,32 @@ const getCommonPlugins = (opts) => {
891
1134
  plugins.buildLogger({ cache })
892
1135
  ];
893
1136
  };
1137
+ function getEntryPoints(config) {
1138
+ if (config.template === "theme") return [config.entry.js, config.entry.css];
1139
+ if (config.template === "custom-app") return [config.entry.app, config.entry.extension];
1140
+ return [config.entry];
1141
+ }
1142
+ function getOutFiles(config) {
1143
+ switch (config.template) {
1144
+ case "custom-app": return {
1145
+ js: "index.js",
1146
+ css: "style.css",
1147
+ jsExtension: "extension.js",
1148
+ manifest: "manifest.json"
1149
+ };
1150
+ case "extension": return { js: `${urlSlugify(getEnName(config.name))}.js` };
1151
+ case "theme": return {
1152
+ js: "theme.js",
1153
+ css: "user.css"
1154
+ };
1155
+ default: throw new Error("Unknown template");
1156
+ }
1157
+ }
894
1158
 
895
1159
  //#endregion
896
1160
  //#region src/build/index.ts
897
1161
  const logger = createLogger("build");
898
- async function build$1(options) {
1162
+ async function build$2(options) {
899
1163
  logger.clear();
900
1164
  logger.greeting(pc.green("Building for production..."));
901
1165
  let ctx;
@@ -922,10 +1186,7 @@ async function build$1(options) {
922
1186
  });
923
1187
  }
924
1188
  function getJSBuildOptions(config, options) {
925
- const entryPoints = (() => {
926
- if (config.template === "theme") return [config.entry.js, config.entry.css];
927
- return [config.entry];
928
- })();
1189
+ const entryPoints = getEntryPoints(config);
929
1190
  const minify = options.watch ? false : options.minify;
930
1191
  const outDir = resolve(config.outDir);
931
1192
  const cache = {
@@ -933,14 +1194,13 @@ function getJSBuildOptions(config, options) {
933
1194
  changed: /* @__PURE__ */ new Set(),
934
1195
  hasChanges: true
935
1196
  };
936
- const outFiles = {
937
- js: config.template === "extension" ? `${urlSlugify(config.name)}.js` : "theme.js",
938
- css: config.template === "theme" ? "user.css" : null
939
- };
1197
+ const outFiles = getOutFiles(config);
940
1198
  const overrides = {
941
1199
  ...defaultBuildOptions,
942
1200
  outdir: outDir,
1201
+ format: "esm",
943
1202
  minify,
1203
+ globalName: varSlugify(getEnName(config.name)),
944
1204
  sourcemap: false,
945
1205
  external: [
946
1206
  ...config.esbuildOptions?.external ? config.esbuildOptions.external : [],
@@ -949,7 +1209,7 @@ function getJSBuildOptions(config, options) {
949
1209
  ],
950
1210
  define: {
951
1211
  [DEV_MODE_VAR_NAME]: "false",
952
- [config.devModeVarName]: "false",
1212
+ ...config.devModeVarName ? { [config.devModeVarName]: "false" } : {},
953
1213
  ...config.esbuildOptions.define
954
1214
  },
955
1215
  plugins: [...config.esbuildOptions?.plugins ? config.esbuildOptions.plugins : [], ...getCommonPlugins({
@@ -981,8 +1241,8 @@ const CLIOptionsSchema$1 = v.strictObject({
981
1241
  apply: v.boolean(),
982
1242
  copy: v.boolean()
983
1243
  });
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$1(safeParse(CLIOptionsSchema$1, opts));
1244
+ 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) => {
1245
+ await build$2(safeParse(CLIOptionsSchema$1, opts));
986
1246
  });
987
1247
 
988
1248
  //#endregion
@@ -1040,7 +1300,8 @@ function createPackageJSON(options) {
1040
1300
  scripts: {
1041
1301
  sc: "spicetify-creator",
1042
1302
  dev: "spicetify-creator dev",
1043
- build: "spicetify-creator build"
1303
+ build: "spicetify-creator build",
1304
+ "update-types": "spicetify-creator update-types"
1044
1305
  },
1045
1306
  dependencies: {},
1046
1307
  devDependencies: { "@spicetify/creator": env.isInternal ? "link:@spicetify/creator" : "latest" }
@@ -1085,9 +1346,10 @@ function validateProjectName(name) {
1085
1346
 
1086
1347
  //#endregion
1087
1348
  //#region src/create/template.ts
1088
- const ext = (lang) => lang === "ts" ? "ts" : "js";
1349
+ const ext = (lang, jsx = false) => lang === "ts" ? `ts${jsx ? "x" : ""}` : `js${jsx ? "x" : ""}`;
1089
1350
  const kv = ({ name, language, framework, linter, packageManager, template }) => ({
1090
1351
  "{{project-name}}": name,
1352
+ "{{project-url}}": `/${urlSlugify(name)}`,
1091
1353
  "{{framework}}": framework,
1092
1354
  "{{linter}}": linter,
1093
1355
  "{{package-manager}}": packageManager,
@@ -1104,36 +1366,60 @@ const kv = ({ name, language, framework, linter, packageManager, template }) =>
1104
1366
  const action = { modify(c, opts) {
1105
1367
  return replace(c, kv(opts));
1106
1368
  } };
1107
- const COMMON_FILES = (opts) => [
1108
- {
1109
- from: "README.template.md",
1110
- to: "README.md",
1111
- action,
1112
- isShared: true
1113
- },
1114
- {
1115
- from: ".gitignore",
1116
- to: ".gitignore",
1117
- isShared: true
1118
- },
1119
- {
1120
- from: `spice.config.${ext(opts.language)}`,
1121
- to: `spice.config.${ext(opts.language)}`,
1122
- action,
1123
- isShared: true
1124
- },
1125
- {
1369
+ const SHARED_FILES = (opts) => {
1370
+ const isShared = true;
1371
+ const files = [
1372
+ {
1373
+ from: "README.template.md",
1374
+ to: "README.md",
1375
+ action,
1376
+ isShared
1377
+ },
1378
+ {
1379
+ from: "DOT-gitignore",
1380
+ to: ".gitignore",
1381
+ isShared: true
1382
+ },
1383
+ {
1384
+ from: `spice.config.${ext(opts.language)}`,
1385
+ to: `spice.config.${ext(opts.language)}`,
1386
+ action,
1387
+ isShared
1388
+ }
1389
+ ];
1390
+ if (opts.template === "custom-app") {
1391
+ files.push({
1392
+ from: `css/app.module.scss`,
1393
+ to: `src/css/app.module.scss`,
1394
+ action,
1395
+ isShared
1396
+ });
1397
+ files.push({
1398
+ from: `icon.svg`,
1399
+ to: `src/icon.svg`,
1400
+ action,
1401
+ isShared
1402
+ });
1403
+ files.push({
1404
+ from: `icon-active.svg`,
1405
+ to: `src/icon-active.svg`,
1406
+ action,
1407
+ isShared
1408
+ });
1409
+ files.push({
1410
+ from: "app.css",
1411
+ to: "src/extension/app.css",
1412
+ action,
1413
+ isShared
1414
+ });
1415
+ } else files.push({
1126
1416
  from: "app.css",
1127
1417
  to: "src/app.css",
1128
1418
  action,
1129
- isShared: true
1130
- },
1131
- ...opts.language === "ts" ? [{
1132
- from: "css.d.ts",
1133
- to: "src/types/css.d.ts",
1134
- isShared: true
1135
- }] : []
1136
- ];
1419
+ isShared
1420
+ });
1421
+ return files;
1422
+ };
1137
1423
  const LANGUAGE_FILES = {
1138
1424
  js: [{
1139
1425
  from: "jsconfig.json",
@@ -1147,24 +1433,36 @@ const LANGUAGE_FILES = {
1147
1433
  }]
1148
1434
  };
1149
1435
  const FRAMEWORKS = {
1150
- react: ({ language }) => [{
1151
- from: `src/app.${ext(language)}x`,
1152
- to: `src/app.${ext(language)}x`,
1153
- action
1154
- }, {
1155
- from: `src/components/Onboarding.${ext(language)}x`,
1156
- to: `src/components/Onboarding.${ext(language)}x`,
1157
- action
1158
- }],
1159
- vanilla: ({ language }) => [{
1160
- from: `src/app.${ext(language)}`,
1161
- to: `src/app.${ext(language)}`,
1162
- action
1163
- }, {
1164
- from: `src/components/Onboarding.${ext(language)}`,
1165
- to: `src/components/Onboarding.${ext(language)}`,
1166
- action
1167
- }]
1436
+ react: ({ language, template }) => {
1437
+ const react = [{
1438
+ from: `src/app.${ext(language, true)}`,
1439
+ to: `src/app.${ext(language, true)}`,
1440
+ action
1441
+ }, {
1442
+ from: `src/components/Onboarding.${ext(language, true)}`,
1443
+ to: `src/components/Onboarding.${ext(language, true)}`,
1444
+ action
1445
+ }];
1446
+ if (template === "custom-app") react.push({
1447
+ from: `src/extension/index.${ext(language, true)}`,
1448
+ to: `src/extension/index.${ext(language, true)}`,
1449
+ action
1450
+ });
1451
+ return react;
1452
+ },
1453
+ vanilla: ({ language, template }) => {
1454
+ const vanilla = [{
1455
+ from: `src/app.${ext(language)}`,
1456
+ to: `src/app.${ext(language)}`,
1457
+ action
1458
+ }, {
1459
+ from: `src/components/Onboarding.${ext(language)}`,
1460
+ to: `src/components/Onboarding.${ext(language)}`,
1461
+ action
1462
+ }];
1463
+ if (template === "custom-app") throw new Error("vanilla doesn't exist for custom-app");
1464
+ return vanilla;
1465
+ }
1168
1466
  };
1169
1467
  const LINTERS = {
1170
1468
  biome: [{
@@ -1177,23 +1475,25 @@ const LINTERS = {
1177
1475
  to: `eslint.config.${ext(language)}`
1178
1476
  }],
1179
1477
  oxlint: [{
1180
- from: ".oxlintrc.json",
1478
+ from: "DOT-oxlintrc.json",
1181
1479
  to: ".oxlintrc.json",
1182
1480
  isShared: true
1183
1481
  }]
1184
1482
  };
1483
+ const getFiles = (options) => {
1484
+ const resolve = (slice) => typeof slice === "function" ? slice(options) : slice ?? [];
1485
+ return [
1486
+ ...resolve(SHARED_FILES),
1487
+ ...resolve(LANGUAGE_FILES[options.language]),
1488
+ ...resolve(FRAMEWORKS[options.framework]),
1489
+ ...resolve(LINTERS[options.linter])
1490
+ ];
1491
+ };
1185
1492
  function setupTemplateFiles(options, targetDir) {
1186
- const { template, language, framework, linter } = options;
1493
+ const { template, language, framework } = options;
1187
1494
  const templateRoot = dist(`templates/${template}`, import.meta.url);
1188
1495
  const fromDir = join(templateRoot, language, framework);
1189
- const resolve = (slice) => typeof slice === "function" ? slice(options) : slice ?? [];
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) {
1496
+ for (const file of getFiles(options)) {
1197
1497
  const src = (() => {
1198
1498
  if (file.isGlobal) return join(templateRoot, file.from);
1199
1499
  if (file.isShared) return join(templateRoot, "shared", file.from);
@@ -1291,6 +1591,35 @@ function tryGitInit(root) {
1291
1591
  }
1292
1592
  }
1293
1593
 
1594
+ //#endregion
1595
+ //#region src/utils/update-types.ts
1596
+ const downloads = { spicetify: {
1597
+ from: "https://raw.githubusercontent.com/spicetify/cli/main/globals.d.ts",
1598
+ to: "./src/types/spicetify.d.ts",
1599
+ 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
+ } };
1601
+ async function updateTypes(isUpdating = true) {
1602
+ const s = spinner();
1603
+ s.start(`${isUpdating ? "Updating" : "Creating"} Types...`);
1604
+ await Promise.all(Object.entries(downloads).map(([name, download]) => downloadFile(name, download, isUpdating)));
1605
+ s.stop(`${isUpdating ? "Updated" : "Created"} Types!`);
1606
+ }
1607
+ async function downloadFile(name, { from, to, action }, isUpdating) {
1608
+ try {
1609
+ const res = await fetch(from);
1610
+ if (!res.ok) throw new Error(`HTTP Error: ${res.status} ${res.statusText}`);
1611
+ let text = await res.text();
1612
+ if (action) text = await action(text);
1613
+ await mkdir(dirname$1(to), { recursive: true });
1614
+ await writeFile$1(to, text, "utf8");
1615
+ const actionLog = isUpdating ? "updated" : "created";
1616
+ logger$2.log(`${name}.d.ts ${actionLog} (${from} -> ${to})`);
1617
+ } catch (e) {
1618
+ log.error(`${name} failed`);
1619
+ console.error(e);
1620
+ }
1621
+ }
1622
+
1294
1623
  //#endregion
1295
1624
  //#region src/create/index.ts
1296
1625
  const isOnline = await getOnline();
@@ -1337,7 +1666,8 @@ async function createProject(cwd, options) {
1337
1666
  options: templateOptions
1338
1667
  });
1339
1668
  },
1340
- framework: async () => {
1669
+ framework: async ({ results: { template } }) => {
1670
+ if (template === "custom-app") return "react";
1341
1671
  if (options.framework) return options.framework;
1342
1672
  return await p.select({
1343
1673
  message: "Select which framework you want to chose",
@@ -1407,6 +1737,7 @@ async function create$1(cwd, options) {
1407
1737
  try {
1408
1738
  mkdirp(cwd);
1409
1739
  setupTemplateFiles(options, cwd);
1740
+ await updateTypes(false);
1410
1741
  const pkgJSON = createPackageJSON(options);
1411
1742
  writePackageJSON(pkgJSON, cwd);
1412
1743
  chdir(cwd);
@@ -1617,7 +1948,7 @@ const injectHMRExtension = async (rootLink, wsLink, outFiles) => {
1617
1948
  const extName = `sc-live-reload-helper.js`;
1618
1949
  const spiceConfig = await getSpicetifyConfig();
1619
1950
  const cleanup = () => {
1620
- if (env.isDev) logger$2.log(`[Spicetify] Removing Live reload extension...`);
1951
+ logger$2.info(`Removing Live reload extension...`);
1621
1952
  try {
1622
1953
  runSpice([
1623
1954
  "config",
@@ -1625,18 +1956,16 @@ const injectHMRExtension = async (rootLink, wsLink, outFiles) => {
1625
1956
  `${extName}-`
1626
1957
  ]);
1627
1958
  runSpice(["apply"]);
1628
- logger$2.debug(pc.green(`${CHECK} Cleanup successful.`));
1959
+ logger$2.info(pc.green(`${CHECK} Cleanup successful.`));
1629
1960
  } catch (e) {
1630
- if (env.isDev) logger$2.error(pc.red(`${CROSS} Cleanup failed: `), e);
1961
+ logger$2.error(pc.red(`${CROSS} Cleanup failed: `), e);
1631
1962
  }
1632
1963
  process.exit();
1633
1964
  };
1634
- if (env.isDev) {
1635
- process.on("SIGINT", cleanup);
1636
- process.on("SIGTERM", cleanup);
1637
- }
1965
+ process.on("SIGINT", cleanup);
1966
+ process.on("SIGTERM", cleanup);
1638
1967
  try {
1639
- logger$2.debug(`[Spicetify] Preparing Live reload extension...`);
1968
+ logger$2.debug(`Preparing Live reload extension...`);
1640
1969
  const destDir = getExtensionDir();
1641
1970
  mkdirp(destDir);
1642
1971
  const outDir = resolve(destDir, extName);
@@ -1663,6 +1992,114 @@ const injectHMRExtension = async (rootLink, wsLink, outFiles) => {
1663
1992
  logger$2.error(pc.red(`${CROSS} Failed to inject HMR helper: ${err instanceof Error ? err.message : String(err)}`));
1664
1993
  }
1665
1994
  };
1995
+ const injectHMRCustomApp = async (rootLink, wsLink, outFiles, config) => {
1996
+ if (config.template !== "custom-app") throw new Error("only supports custom-app templates");
1997
+ const identifier = getEnName(config.name);
1998
+ const spiceConfig = await getSpicetifyConfig();
1999
+ const customAppId = urlSlugify(identifier);
2000
+ runSpice([
2001
+ "config",
2002
+ "extensions",
2003
+ "sc-live-reload-helper.js-"
2004
+ ]);
2005
+ const cleanup = () => {
2006
+ logger$2.info(`Removing Live reload custom-app...`);
2007
+ try {
2008
+ runSpice([
2009
+ "config",
2010
+ "custom_apps",
2011
+ `${customAppId}-`
2012
+ ]);
2013
+ runSpice(["apply"]);
2014
+ logger$2.info(pc.green(`${CHECK} Cleanup successful.`));
2015
+ } catch (e) {
2016
+ logger$2.error(pc.red(`${CROSS} Cleanup failed: `), e);
2017
+ }
2018
+ process.exit();
2019
+ };
2020
+ process.on("SIGINT", cleanup);
2021
+ process.on("SIGTERM", cleanup);
2022
+ try {
2023
+ logger$2.debug(`Preparing Live reload custom-app...`);
2024
+ const destDir = resolve(getCustomAppsDir(), customAppId);
2025
+ mkdirp(destDir);
2026
+ const indexJsPath = resolve(destDir, "index.js");
2027
+ const extensionJsPath = resolve(destDir, outFiles.jsExtension ?? "extension.js");
2028
+ writeFileSync(indexJsPath, `const React = Spicetify.React;
2029
+ const waitForImport = () => {
2030
+ return import("${rootLink}/files/${outFiles.js}")
2031
+ .then((mod) => mod.default || mod.render)
2032
+ .catch((err) => {
2033
+ console.error("Failed to import app:", err);
2034
+ return null;
2035
+ });
2036
+ };
2037
+
2038
+ const AppWrapper = ({ appPromise }) => {
2039
+ const [App, setApp] = React.useState(null);
2040
+
2041
+ React.useEffect(() => {
2042
+ let mounted = true;
2043
+
2044
+ appPromise.then((app) => {
2045
+ if (mounted && app) {
2046
+ setApp(() => app);
2047
+ }
2048
+ });
2049
+
2050
+ return () => {
2051
+ mounted = false;
2052
+ };
2053
+ }, [appPromise]);
2054
+
2055
+ if (!App) {
2056
+ return React.createElement(
2057
+ "div",
2058
+ { className: "loading" },
2059
+ "Loading app..."
2060
+ );
2061
+ }
2062
+
2063
+ return React.createElement(App);
2064
+ };
2065
+
2066
+ const render = () => {
2067
+ const appPromise = waitForImport();
2068
+ return React.createElement(AppWrapper, { appPromise });
2069
+ };
2070
+ `);
2071
+ const { code: extensionCode } = await transform(readFileSync(liveReloadFilePath, "utf8"), {
2072
+ loader: "js",
2073
+ define: {
2074
+ _SERVER_URL: JSON.stringify(rootLink),
2075
+ _HOT_RELOAD_LINK: JSON.stringify(wsLink),
2076
+ _JS_PATH: JSON.stringify(`/files/${outFiles.jsExtension ?? "extension.js"}`),
2077
+ _CSS_PATH: JSON.stringify(outFiles.css ? `/files/${outFiles.css}` : `/files/app.css`)
2078
+ }
2079
+ });
2080
+ writeFileSync(extensionJsPath, extensionCode);
2081
+ const manifestPath = resolve(destDir, "manifest.json");
2082
+ const manifest = {
2083
+ name: identifier,
2084
+ subfiles: [],
2085
+ subfiles_extension: [outFiles.jsExtension ?? "extension.js"],
2086
+ icon: config.icon.default,
2087
+ "active-icon": config.icon.active ?? config.icon.default
2088
+ };
2089
+ writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
2090
+ if (spiceConfig) {
2091
+ runSpice([
2092
+ "config",
2093
+ "custom_apps",
2094
+ customAppId
2095
+ ]);
2096
+ runSpice(["apply"]);
2097
+ }
2098
+ logger$2.debug(pc.green(`${CHECK} Live reload custom-app injected successfully.`));
2099
+ } catch (err) {
2100
+ logger$2.error(pc.red(`${CROSS} Failed to inject HMR custom-app: ${err instanceof Error ? err.message : String(err)}`));
2101
+ }
2102
+ };
1666
2103
 
1667
2104
  //#endregion
1668
2105
  //#region src/dev/index.ts
@@ -1685,11 +2122,9 @@ async function dev$1(options) {
1685
2122
  port: options.port ?? config.serverConfig.port
1686
2123
  });
1687
2124
  await server.start();
1688
- const outFiles = {
1689
- js: config.template === "extension" ? `${urlSlugify(config.name)}.js` : "theme.js",
1690
- css: config.template === "theme" ? "user.css" : null
1691
- };
1692
- await injectHMRExtension(server.link, server.wsLink, outFiles);
2125
+ const outFiles = getOutFiles(config);
2126
+ if (config.template === "custom-app") await injectHMRCustomApp(server.link, server.wsLink, outFiles, config);
2127
+ else await injectHMRExtension(server.link, server.wsLink, outFiles);
1693
2128
  ctx = await context(getJSDevOptions(config, {
1694
2129
  ...options,
1695
2130
  outFiles,
@@ -1712,10 +2147,7 @@ async function dev$1(options) {
1712
2147
  });
1713
2148
  }
1714
2149
  function getJSDevOptions(config, options) {
1715
- const entryPoints = (() => {
1716
- if (config.template === "theme") return [config.entry.js, config.entry.css];
1717
- return [config.entry];
1718
- })();
2150
+ const entryPoints = getEntryPoints(config);
1719
2151
  const minify = false;
1720
2152
  const cache = {
1721
2153
  files: /* @__PURE__ */ new Map(),
@@ -1734,7 +2166,7 @@ function getJSDevOptions(config, options) {
1734
2166
  ],
1735
2167
  define: {
1736
2168
  [DEV_MODE_VAR_NAME]: "true",
1737
- [config.devModeVarName]: "true",
2169
+ ...config.devModeVarName ? { [config.devModeVarName]: "true" } : {},
1738
2170
  ...config.esbuildOptions.define
1739
2171
  },
1740
2172
  plugins: [...config.esbuildOptions?.plugins ? config.esbuildOptions.plugins : [], ...getCommonPlugins({
@@ -1743,7 +2175,9 @@ function getJSDevOptions(config, options) {
1743
2175
  cache,
1744
2176
  buildOptions: {
1745
2177
  copy: true,
1746
- remove: true,
2178
+ apply: false,
2179
+ applyOnce: false,
2180
+ remove: config.template !== "custom-app",
1747
2181
  outDir
1748
2182
  },
1749
2183
  dev: true,
@@ -1765,12 +2199,17 @@ const dev = new Command("dev").description("Develop your spicetify project").opt
1765
2199
  await dev$1(safeParse(CLIOptionsSchema, opts));
1766
2200
  });
1767
2201
 
2202
+ //#endregion
2203
+ //#region src/commands/update-types.ts
2204
+ const update_types = new Command("update-types").description("Update Spicetify Types").action(async () => {
2205
+ await updateTypes();
2206
+ });
2207
+
1768
2208
  //#endregion
1769
2209
  //#region src/bin.ts
1770
2210
  logger$2.debug(`Env: ${JSON.stringify(env, null, 2)}\n`);
1771
2211
  const command = new Command();
1772
- create.alias("init");
1773
- command.addCommand(create).addCommand(build).addCommand(dev);
2212
+ command.addCommand(create.alias("init")).addCommand(build$1).addCommand(dev).addCommand(update_types);
1774
2213
  command.parse();
1775
2214
 
1776
2215
  //#endregion