@spicemod/creator 0.0.1

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 (101) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +35 -0
  3. package/dist/bin.d.mts +1 -0
  4. package/dist/bin.mjs +1739 -0
  5. package/dist/client/index.d.mts +2175 -0
  6. package/dist/client/index.mjs +7 -0
  7. package/dist/templates/extension/js/react/eslint.config.js +29 -0
  8. package/dist/templates/extension/js/react/src/app.jsx +27 -0
  9. package/dist/templates/extension/js/react/src/components/Onboarding.jsx +72 -0
  10. package/dist/templates/extension/js/vanilla/components/Onboarding.js +71 -0
  11. package/dist/templates/extension/js/vanilla/eslint.config.js +16 -0
  12. package/dist/templates/extension/js/vanilla/src/app.js +12 -0
  13. package/dist/templates/extension/meta.json +4 -0
  14. package/dist/templates/extension/shared/.oxlintrc.json +36 -0
  15. package/dist/templates/extension/shared/README.template.md +53 -0
  16. package/dist/templates/extension/shared/app.css +163 -0
  17. package/dist/templates/extension/shared/biome.json +36 -0
  18. package/dist/templates/extension/shared/css.d.ts +44 -0
  19. package/dist/templates/extension/shared/jsconfig.json +32 -0
  20. package/dist/templates/extension/shared/spice.config.js +9 -0
  21. package/dist/templates/extension/shared/spice.config.ts +9 -0
  22. package/dist/templates/extension/shared/tsconfig.json +32 -0
  23. package/dist/templates/extension/ts/react/eslint.config.ts +29 -0
  24. package/dist/templates/extension/ts/react/src/app.tsx +27 -0
  25. package/dist/templates/extension/ts/react/src/components/Onboarding.tsx +83 -0
  26. package/dist/templates/extension/ts/vanilla/biome.json +36 -0
  27. package/dist/templates/extension/ts/vanilla/eslint.config.ts +16 -0
  28. package/dist/templates/extension/ts/vanilla/src/app.ts +12 -0
  29. package/dist/templates/extension/ts/vanilla/src/components/Onboarding.ts +79 -0
  30. package/dist/templates/liveReload.js +70 -0
  31. package/dist/templates/theme/js/react/eslint.config.js +29 -0
  32. package/dist/templates/theme/js/react/src/app.jsx +25 -0
  33. package/dist/templates/theme/js/react/src/components/Onboarding.jsx +72 -0
  34. package/dist/templates/theme/js/vanilla/eslint.config.js +16 -0
  35. package/dist/templates/theme/js/vanilla/src/app.js +11 -0
  36. package/dist/templates/theme/js/vanilla/src/components/Onboarding.js +71 -0
  37. package/dist/templates/theme/meta.json +4 -0
  38. package/dist/templates/theme/shared/.oxlintrc.json +36 -0
  39. package/dist/templates/theme/shared/README.template.md +53 -0
  40. package/dist/templates/theme/shared/app.css +163 -0
  41. package/dist/templates/theme/shared/biome.json +36 -0
  42. package/dist/templates/theme/shared/css.d.ts +44 -0
  43. package/dist/templates/theme/shared/jsconfig.json +31 -0
  44. package/dist/templates/theme/shared/spice.config.js +9 -0
  45. package/dist/templates/theme/shared/spice.config.ts +9 -0
  46. package/dist/templates/theme/shared/tsconfig.json +32 -0
  47. package/dist/templates/theme/ts/react/eslint.config.ts +29 -0
  48. package/dist/templates/theme/ts/react/src/app.tsx +26 -0
  49. package/dist/templates/theme/ts/react/src/components/Onboarding.tsx +83 -0
  50. package/dist/templates/theme/ts/vanilla/eslint.config.ts +16 -0
  51. package/dist/templates/theme/ts/vanilla/src/app.ts +11 -0
  52. package/dist/templates/theme/ts/vanilla/src/components/Onboarding.ts +79 -0
  53. package/dist/templates/wrapper.js +48 -0
  54. package/package.json +80 -0
  55. package/templates/extension/js/react/eslint.config.js +29 -0
  56. package/templates/extension/js/react/src/app.jsx +27 -0
  57. package/templates/extension/js/react/src/components/Onboarding.jsx +72 -0
  58. package/templates/extension/js/vanilla/components/Onboarding.js +71 -0
  59. package/templates/extension/js/vanilla/eslint.config.js +16 -0
  60. package/templates/extension/js/vanilla/src/app.js +12 -0
  61. package/templates/extension/meta.json +4 -0
  62. package/templates/extension/shared/.oxlintrc.json +36 -0
  63. package/templates/extension/shared/README.template.md +53 -0
  64. package/templates/extension/shared/app.css +163 -0
  65. package/templates/extension/shared/biome.json +36 -0
  66. package/templates/extension/shared/css.d.ts +44 -0
  67. package/templates/extension/shared/jsconfig.json +32 -0
  68. package/templates/extension/shared/spice.config.js +9 -0
  69. package/templates/extension/shared/spice.config.ts +9 -0
  70. package/templates/extension/shared/tsconfig.json +32 -0
  71. package/templates/extension/ts/react/eslint.config.ts +29 -0
  72. package/templates/extension/ts/react/src/app.tsx +27 -0
  73. package/templates/extension/ts/react/src/components/Onboarding.tsx +83 -0
  74. package/templates/extension/ts/vanilla/biome.json +36 -0
  75. package/templates/extension/ts/vanilla/eslint.config.ts +16 -0
  76. package/templates/extension/ts/vanilla/src/app.ts +12 -0
  77. package/templates/extension/ts/vanilla/src/components/Onboarding.ts +79 -0
  78. package/templates/liveReload.js +70 -0
  79. package/templates/theme/js/react/eslint.config.js +29 -0
  80. package/templates/theme/js/react/src/app.jsx +25 -0
  81. package/templates/theme/js/react/src/components/Onboarding.jsx +72 -0
  82. package/templates/theme/js/vanilla/eslint.config.js +16 -0
  83. package/templates/theme/js/vanilla/src/app.js +11 -0
  84. package/templates/theme/js/vanilla/src/components/Onboarding.js +71 -0
  85. package/templates/theme/meta.json +4 -0
  86. package/templates/theme/shared/.oxlintrc.json +36 -0
  87. package/templates/theme/shared/README.template.md +53 -0
  88. package/templates/theme/shared/app.css +163 -0
  89. package/templates/theme/shared/biome.json +36 -0
  90. package/templates/theme/shared/css.d.ts +44 -0
  91. package/templates/theme/shared/jsconfig.json +31 -0
  92. package/templates/theme/shared/spice.config.js +9 -0
  93. package/templates/theme/shared/spice.config.ts +9 -0
  94. package/templates/theme/shared/tsconfig.json +32 -0
  95. package/templates/theme/ts/react/eslint.config.ts +29 -0
  96. package/templates/theme/ts/react/src/app.tsx +26 -0
  97. package/templates/theme/ts/react/src/components/Onboarding.tsx +83 -0
  98. package/templates/theme/ts/vanilla/eslint.config.ts +16 -0
  99. package/templates/theme/ts/vanilla/src/app.ts +11 -0
  100. package/templates/theme/ts/vanilla/src/components/Onboarding.ts +79 -0
  101. package/templates/wrapper.js +48 -0
package/dist/bin.mjs ADDED
@@ -0,0 +1,1739 @@
1
+ #!/usr/bin/env node
2
+ import { Command, Option } from "commander";
3
+ import * as v from "valibot";
4
+ import path, { basename, dirname, extname, join, relative, resolve } from "node:path";
5
+ import { context, transform } from "esbuild";
6
+ import { createReadStream, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
7
+ import { watchConfig } from "c12";
8
+ import { globSync } from "tinyglobby";
9
+ import { URL as URL$1, fileURLToPath } from "node:url";
10
+ import { execSync, spawn, spawnSync } from "node:child_process";
11
+ import readline, { createInterface } from "node:readline";
12
+ import * as p from "@clack/prompts";
13
+ import { cancel, log } from "@clack/prompts";
14
+ import pc from "picocolors";
15
+ import "dotenv/config";
16
+ import { gzipSync } from "node:zlib";
17
+ import postcssMinify from "@csstools/postcss-minify";
18
+ import autoprefixer from "autoprefixer";
19
+ import { postcssModules, sassPlugin } from "esbuild-sass-plugin";
20
+ import postcss from "postcss";
21
+ import postcssImport from "postcss-import";
22
+ import postcssPresetEnv from "postcss-preset-env";
23
+ import { readFile, writeFile } from "node:fs/promises";
24
+ import { parse } from "ini";
25
+ import { chdir } from "node:process";
26
+ import { lookup } from "node:dns/promises";
27
+ import { createServer } from "node:http";
28
+ import { WebSocket, WebSocketServer } from "ws";
29
+
30
+ //#region src/utils/replace.ts
31
+ function replace(contents, kv) {
32
+ const keys = Object.keys(kv);
33
+ if (keys.length === 0) return contents;
34
+ const pattern = new RegExp(keys.map((k) => k.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|"), "g");
35
+ return contents.replace(pattern, (matched) => kv[matched] ?? matched);
36
+ }
37
+
38
+ //#endregion
39
+ //#region src/utils/fs.ts
40
+ function mkdirp(dir) {
41
+ try {
42
+ mkdirSync(dir, { recursive: true });
43
+ } catch (e) {
44
+ if (e?.code === "EEXIST") return;
45
+ throw e;
46
+ }
47
+ }
48
+ function dist(path, url = import.meta.url) {
49
+ const insideDistFolder = url.includes("dist");
50
+ return fileURLToPath(new URL(`./${!insideDistFolder ? "dist/" : ""}${path}`, url).href);
51
+ }
52
+
53
+ //#endregion
54
+ //#region src/metadata.ts
55
+ const toOptions = (metadata) => metadata.map((m) => ({
56
+ value: m.name,
57
+ label: m.title,
58
+ hint: m.description
59
+ }));
60
+ const templateTypes = ["extension", "theme"];
61
+ const templates = templateTypes.map((dir) => {
62
+ const meta_file = dist(`templates/${dir}/meta.json`, import.meta.url);
63
+ const { title, description } = JSON.parse(readFileSync(meta_file, "utf8"));
64
+ return {
65
+ name: dir,
66
+ title,
67
+ description
68
+ };
69
+ });
70
+ const templateOptions = toOptions(templates);
71
+ const linterTypes = [
72
+ "biome",
73
+ "eslint",
74
+ "oxlint",
75
+ "none"
76
+ ];
77
+ const linterMeta = {
78
+ oxlint: {
79
+ title: "Oxlint",
80
+ description: "high-performance linter - http://oxc.rs"
81
+ },
82
+ biome: {
83
+ title: "Biome",
84
+ description: "Fast formatter and linter - https://biomejs.dev"
85
+ },
86
+ eslint: {
87
+ title: "ESLint",
88
+ description: "Industry standard, highly extensible - https://eslint.org"
89
+ },
90
+ none: {
91
+ title: "None",
92
+ description: "Skip linting for this project"
93
+ }
94
+ };
95
+ const linters = linterTypes.map((name) => ({
96
+ name,
97
+ ...linterMeta[name]
98
+ }));
99
+ const linterOptions = toOptions(linters);
100
+ const languageTypes = ["ts", "js"];
101
+ const languageMeta = {
102
+ ts: {
103
+ title: "Typescript",
104
+ description: "with types (Recommended)"
105
+ },
106
+ js: {
107
+ title: "Javascript",
108
+ description: "with JSDOC"
109
+ }
110
+ };
111
+ const languages = languageTypes.map((name) => ({
112
+ name,
113
+ ...languageMeta[name]
114
+ }));
115
+ const languageOptions = toOptions(languages);
116
+ const frameworkTypes = ["react", "vanilla"];
117
+ const frameworkMeta = {
118
+ react: {
119
+ title: "ReactJS",
120
+ description: "js framework - https://react.dev"
121
+ },
122
+ vanilla: {
123
+ title: "Vanilla",
124
+ description: "ya know it"
125
+ }
126
+ };
127
+ const frameworks = frameworkTypes.map((name) => ({
128
+ name,
129
+ ...frameworkMeta[name]
130
+ }));
131
+ const frameworkOptions = toOptions(frameworks);
132
+ const liveReloadFilePath = dist(`templates/liveReload.js`, import.meta.url);
133
+ const templateFilePath = dist("templates/wrapper.js", import.meta.url);
134
+
135
+ //#endregion
136
+ //#region package.json
137
+ var name = "@spicemod/creator";
138
+ var version = "0.0.1";
139
+
140
+ //#endregion
141
+ //#region src/utils/common.ts
142
+ const runCommand = async (action, options = {
143
+ outro: false,
144
+ message: void 0
145
+ }) => {
146
+ try {
147
+ p.intro(options.message === void 0 || typeof options.message !== "function" ? `${name} ${pc.gray(`(v${version})`)}` : options.message({
148
+ name,
149
+ version
150
+ }));
151
+ await action();
152
+ if (options.outro) p.outro("You're all set!");
153
+ } catch (e) {
154
+ if (e instanceof Error) {
155
+ p.log.error(e.stack ?? String(e));
156
+ p.log.message();
157
+ }
158
+ p.cancel("Operation failed.");
159
+ }
160
+ };
161
+ const normalizePosix = (dir) => path.posix.normalize(dir.replace(/\\/g, "/"));
162
+ function urlSlugify(text) {
163
+ return text.toString().normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().trim().replace(/\s+/g, "-").replace(/[^\w-]+/g, "").replace(/--+/g, "-");
164
+ }
165
+ function varSlugify(text) {
166
+ return text.toString().normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().trim().replace(/\s+/g, "_").replace(/[^\w]/g, "").replace(/__+/g, "_").replace(/^[0-9]/, (val) => `_${val}`);
167
+ }
168
+
169
+ //#endregion
170
+ //#region src/utils/package-manager.ts
171
+ const packageManagers = [
172
+ "npm",
173
+ "pnpm",
174
+ "yarn",
175
+ "bun"
176
+ ];
177
+ function getPackageManager() {
178
+ const userAgent = process.env.npm_config_user_agent ?? "";
179
+ if (userAgent.startsWith("yarn")) return "yarn";
180
+ if (userAgent.startsWith("pnpm")) return "pnpm";
181
+ if (userAgent.startsWith("bun")) return "bun";
182
+ return "npm";
183
+ }
184
+ /**
185
+ * Spawn a package manager installation based on user preference.
186
+ *
187
+ * @returns A Promise that resolves once the installation is finished.
188
+ */
189
+ async function installPackages(packageManager, isOnline) {
190
+ const args = ["install"];
191
+ if (!isOnline) {
192
+ log.warn(pc.yellow("You appear to be offline.\nFalling back to the local cache."));
193
+ args.push("--offline");
194
+ }
195
+ /**
196
+ * Return a Promise that resolves once the installation is finished.
197
+ */
198
+ return new Promise((resolve, reject) => {
199
+ /**
200
+ * Spawn the installation process.
201
+ */
202
+ const child = spawn(packageManager, args, {
203
+ stdio: [
204
+ "inherit",
205
+ "pipe",
206
+ "pipe"
207
+ ],
208
+ env: {
209
+ ...process.env,
210
+ ADBLOCK: "1",
211
+ NODE_ENV: "development",
212
+ DISABLE_OPENCOLLECTIVE: "1"
213
+ }
214
+ });
215
+ createInterface({ input: child.stdout }).on("line", (line) => {
216
+ log.message(line);
217
+ });
218
+ createInterface({ input: child.stderr }).on("line", (line) => {
219
+ log.error(line);
220
+ });
221
+ child.on("close", (code) => {
222
+ log.message();
223
+ if (code !== 0) {
224
+ reject({ command: `${packageManager} ${args.join(" ")}` });
225
+ return;
226
+ }
227
+ resolve();
228
+ });
229
+ });
230
+ }
231
+
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
+ serverConfig: v.partial(ServerConfigSchema),
266
+ version: v.string()
267
+ });
268
+ const FileOptionsSchema = v.intersect([v.partial(CommonSchema), TemplateSpecificOptionalSchema]);
269
+ const OptionsSchema$1 = v.pipe(v.intersect([v.required(CommonSchema), TemplateSpecificSchema]), v.check((input) => !!input.name, "Name is required"));
270
+
271
+ //#endregion
272
+ //#region src/env.ts
273
+ const isInternal = process.env.SPICE_INTERNAL === "true";
274
+ const isDev = process.env.IS_DEV === "true";
275
+ const spicetifyBin = process.env.SPICETIFY_BIN || process.env.SPICE_BIN || "spicetify";
276
+ const env = {
277
+ isInternal,
278
+ isDev,
279
+ spicetifyBin
280
+ };
281
+
282
+ //#endregion
283
+ //#region src/constants.ts
284
+ const GITHUB_LINK = "https://github.com/sanoojes/spicetify-creator";
285
+ const DISCORD_LINK = "https://discord.gg/YGkktjdYV8";
286
+ const DOCS_LINK = "https://github.com/sanoojes/spicetify-creator";
287
+ const CONFIG_REF_LINK = "https://github.com/sanoojes/spicetify-creator";
288
+ const SPICETIFY_LINK = "https://spicetify.app/docs/getting-started";
289
+ const CHECK = pc.bold(pc.green("✔"));
290
+ const CROSS = pc.bold(pc.red("✖"));
291
+ const WARN = pc.bold(pc.yellow("⚠"));
292
+ const VALID_PROJECT_FILES = new Set([
293
+ ".DS_Store",
294
+ ".git",
295
+ ".gitattributes",
296
+ ".gitignore",
297
+ ".gitlab-ci.yml",
298
+ ".hg",
299
+ ".hgcheck",
300
+ ".hgignore",
301
+ ".idea",
302
+ ".npmignore",
303
+ ".travis.yml",
304
+ "LICENSE",
305
+ "Thumbs.db",
306
+ "docs",
307
+ "mkdocs.yml",
308
+ "npm-debug.log",
309
+ "yarn-debug.log",
310
+ "yarn-error.log",
311
+ "yarnrc.yml",
312
+ ".yarn"
313
+ ]);
314
+
315
+ //#endregion
316
+ //#region src/utils/logger.ts
317
+ var Logger = class {
318
+ constructor(prefix = "", mode = env.isDev ? "dev" : "prod") {
319
+ this.prefix = prefix;
320
+ this.mode = mode;
321
+ }
322
+ get isDev() {
323
+ return this.mode === "dev";
324
+ }
325
+ getPrefix() {
326
+ if (!this.prefix || !env.isDev) return null;
327
+ return pc.dim(`[${this.prefix.toLowerCase()}]`);
328
+ }
329
+ add(fn, args) {
330
+ const p = this.getPrefix();
331
+ if (p) fn(p, ...args);
332
+ else fn(...args);
333
+ }
334
+ greeting(msg = "") {
335
+ const label = pc.bgBlue(pc.black(` ${name.toUpperCase()} `));
336
+ console.log(`${label} ${pc.dim(`v${version}`)} ${msg}`);
337
+ }
338
+ info(...args) {
339
+ this.add(console.info, args);
340
+ }
341
+ success(m) {
342
+ console.log(`${CHECK} ${pc.green(m)}`);
343
+ }
344
+ warn(m) {
345
+ console.log(`${WARN} ${pc.yellow(m)}`);
346
+ }
347
+ cwarn(...args) {
348
+ this.add(console.warn, args);
349
+ }
350
+ error(...errors) {
351
+ this.add(console.error, errors);
352
+ 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")));
353
+ }
354
+ log(...args) {
355
+ if (!this.isDev) return;
356
+ this.add(console.log, args);
357
+ }
358
+ debug(...args) {
359
+ if (!this.isDev) return;
360
+ this.add((...a) => console.log(pc.magenta("[debug]"), ...a), args);
361
+ }
362
+ clear() {
363
+ if (!process.stdout.isTTY || process.env.CI) return;
364
+ readline.cursorTo(process.stdout, 0, 0);
365
+ readline.clearScreenDown(process.stdout);
366
+ }
367
+ };
368
+ const createLogger = (prefix = "", mode) => new Logger(prefix, mode);
369
+ const logger$2 = createLogger("common");
370
+
371
+ //#endregion
372
+ //#region src/utils/schema.ts
373
+ function safeParse(schema, data, type = "CLI") {
374
+ const result = v.safeParse(schema, data);
375
+ if (result.success) return result.output;
376
+ logger$2.error(`\n${pc.bgRed(pc.black(" ERROR "))} ${pc.red(`Invalid ${type} options:`)}`);
377
+ result.issues.forEach((issue) => {
378
+ const path = issue.path?.map((p) => p.key).join(".") || "input";
379
+ logger$2.error(`${pc.dim(" └─")} ${pc.yellow(path)}: ${pc.white(issue.message)}`);
380
+ });
381
+ logger$2.error(`\n${pc.dim("Check your command flags and try again.")}\n`);
382
+ process.exit(1);
383
+ }
384
+
385
+ //#endregion
386
+ //#region src/config/index.ts
387
+ const logger$1 = createLogger("config");
388
+ const JS_ENTRY_GLOBS = [
389
+ "app.{ts,tsx,js,jsx,mts,mjs,cts,cjs}",
390
+ "extension/app.{ts,tsx,js,jsx,mts,mjs,cts,cjs}",
391
+ "src/app.{ts,tsx,js,jsx,mts,mjs,cts,cjs}",
392
+ "src/extension/app.{ts,tsx,js,jsx,mts,mjs,cts,cjs}"
393
+ ];
394
+ const CSS_ENTRY_GLOBS = [
395
+ "app.{css,scss,sass,less,styl,stylus,pcss,postcss}",
396
+ "styles/app.{css,scss,sass,less,styl,stylus,pcss,postcss}",
397
+ "src/app.{css,scss,sass,less,styl,stylus,pcss,postcss}",
398
+ "src/styles/app.{css,scss,sass,less,styl,stylus,pcss,postcss}"
399
+ ];
400
+ const CONFIG_DEFAULTS = {
401
+ outDir: "./dist",
402
+ linter: "biome",
403
+ framework: "react",
404
+ template: "extension",
405
+ packageManager: getPackageManager()
406
+ };
407
+ async function loadConfig(cb) {
408
+ let cleanup;
409
+ const runCb = async (config, isUpdate) => {
410
+ if (typeof cleanup === "function") await cleanup();
411
+ cleanup = await cb(config, isUpdate);
412
+ };
413
+ const watcher = await watchConfig({
414
+ name: "spice",
415
+ defaults: CONFIG_DEFAULTS,
416
+ configFileRequired: false,
417
+ packageJson: true,
418
+ async onUpdate({ newConfig }) {
419
+ await runCb(await getResolvedConfig(newConfig.config), true);
420
+ }
421
+ });
422
+ await runCb(await getResolvedConfig(watcher.config), false);
423
+ return watcher;
424
+ }
425
+ async function getResolvedConfig(config) {
426
+ try {
427
+ return safeParse(OptionsSchema$1, await resolveContext(config), "Config");
428
+ } catch (e) {
429
+ const message = e instanceof Error ? e.message : String(e);
430
+ logger$1.error(pc.red(`Failed to load configuration: ${message}`));
431
+ process.exit(1);
432
+ }
433
+ }
434
+ async function resolveContext(config) {
435
+ const cwd = process.cwd();
436
+ const getPkg = () => {
437
+ try {
438
+ return JSON.parse(readFileSync(resolve(cwd, "package.json"), "utf-8"));
439
+ } catch {
440
+ return {};
441
+ }
442
+ };
443
+ const pkg = config.name && config.version ? {} : getPkg();
444
+ if (!config.name) config.name = pkg.name || basename(cwd);
445
+ const DEFAULT_VERSION = "0.0.1";
446
+ if (!config.version) config.version = pkg.version ?? DEFAULT_VERSION;
447
+ if (!config.entry) if (config.template === "theme") {
448
+ config.entry = {
449
+ js: resolveDefaultEntries(cwd, "js"),
450
+ css: resolveDefaultEntries(cwd, "css")
451
+ };
452
+ if (!config.entry.js || config.entry.js.length === 0) config.entry.js = resolveDefaultEntries(cwd, "js");
453
+ if (!config.entry.css || config.entry.css.length === 0) config.entry.css = resolveDefaultEntries(cwd, "css");
454
+ } else config.entry = resolveDefaultEntries(cwd, "js");
455
+ config.outDir = resolve(cwd, config.outDir || "./dist");
456
+ config.esbuildOptions ??= {};
457
+ config.serverConfig ??= {};
458
+ return config;
459
+ }
460
+ function resolveDefaultEntries(cwd, type) {
461
+ const resolveFile = (globs) => {
462
+ for (const glob of globs) {
463
+ const matches = globSync(glob, {
464
+ cwd,
465
+ absolute: true
466
+ });
467
+ if (matches.length > 0) return matches;
468
+ }
469
+ return [];
470
+ };
471
+ const firstEntry = resolveFile(type === "js" ? JS_ENTRY_GLOBS : CSS_ENTRY_GLOBS)[0];
472
+ 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).");
473
+ return firstEntry;
474
+ }
475
+
476
+ //#endregion
477
+ //#region src/esbuild/format.ts
478
+ function formatBuildSummary(files) {
479
+ try {
480
+ const tableData = [];
481
+ let totalSize = 0;
482
+ for (const [filePath, file] of files) {
483
+ let gzipKB = 0;
484
+ if (file.contents) gzipKB = gzipSync(file.contents).length / 1024;
485
+ const sizeKB = file.contents.length / 1024;
486
+ totalSize += file.contents.length;
487
+ tableData.push({
488
+ fullPath: relative(process.cwd(), filePath),
489
+ sizeKB,
490
+ gzipKB
491
+ });
492
+ }
493
+ const maxPathWidth = Math.max(...tableData.map((d) => d.fullPath.length), 10);
494
+ const lines = [];
495
+ for (const file of tableData) {
496
+ const sizeColor = file.sizeKB > 500 ? pc.magenta : file.sizeKB > 100 ? pc.yellow : pc.cyan;
497
+ const pathPart = pc.dim(file.fullPath);
498
+ const padding = " ".repeat(maxPathWidth - file.fullPath.length + 2);
499
+ const sizeStr = `${file.sizeKB.toFixed(2)} kB`.padStart(9);
500
+ const gzipStr = pc.dim(`│ gzip: ${file.gzipKB.toFixed(2).padStart(5)} kB`);
501
+ lines.push(`${pc.blue("i")} ${pathPart}${padding}${sizeColor(sizeStr)} ${gzipStr}`);
502
+ }
503
+ const totalSizeKB = totalSize / 1024;
504
+ const fileCount = tableData.length;
505
+ lines.push(`${pc.blue("ℹ")} ${pc.bold(pc.gray(`${fileCount} files, total: ${totalSizeKB.toFixed(2)} kB`))}`);
506
+ return lines.join("\n");
507
+ } catch (e) {
508
+ logger$2.debug(e);
509
+ return pc.red("Failed to generate build summary");
510
+ }
511
+ }
512
+
513
+ //#endregion
514
+ //#region src/esbuild/plugins/buildLogger.ts
515
+ const buildLogger = ({ cache }) => ({
516
+ name: "spice_internal__build-logger",
517
+ setup(build) {
518
+ let isFirstBuild = true;
519
+ let buildStartTime;
520
+ build.onStart(() => {
521
+ buildStartTime = performance.now();
522
+ if (!isFirstBuild) {
523
+ logger$2.clear();
524
+ logger$2.info(pc.dim("Rebuilding..."));
525
+ } else logger$2.info(pc.dim("Build started..."));
526
+ });
527
+ build.onEnd((result) => {
528
+ if (result.errors.length > 0) {
529
+ logger$2.info(pc.red("build failed."));
530
+ return;
531
+ }
532
+ const moduleCount = result.metafile ? Object.keys(result.metafile.inputs).length : 0;
533
+ logger$2.info(`${CHECK} ${moduleCount} modules transformed.`);
534
+ if (result.metafile) {
535
+ const details = formatBuildSummary(cache.files);
536
+ logger$2.info(details);
537
+ }
538
+ logger$2.info(pc.green(`${CHECK} built in ${getTime(buildStartTime)}.`));
539
+ isFirstBuild = false;
540
+ });
541
+ }
542
+ });
543
+ function getTime(start) {
544
+ const ms = performance.now() - start;
545
+ return ms > 1e3 ? `${(ms / 1e3).toFixed(2)}s` : `${Math.round(ms)}ms`;
546
+ }
547
+
548
+ //#endregion
549
+ //#region src/esbuild/plugins/cleanDist.ts
550
+ const cleanDist = (outDir) => ({
551
+ name: "spice_internal__clean-dist",
552
+ setup(build) {
553
+ build.onStart(() => {
554
+ if (existsSync(outDir)) rmSync(outDir, {
555
+ recursive: true,
556
+ force: true
557
+ });
558
+ });
559
+ }
560
+ });
561
+
562
+ //#endregion
563
+ //#region src/esbuild/plugins/css.ts
564
+ function css({ minify = false, inline = false, logger = createLogger("plugin:css") } = {}) {
565
+ const postCssPlugins = [
566
+ postcssImport({ path: [resolve(process.cwd(), "src")] }),
567
+ autoprefixer,
568
+ postcssPresetEnv({ stage: 0 }),
569
+ ...minify ? [postcssMinify()] : []
570
+ ];
571
+ const type = inline ? "style" : "css";
572
+ return [sassPlugin({
573
+ filter: /\.module\.(s[ac]ss|css)$/,
574
+ type,
575
+ transform: postcssModules({
576
+ getJSON: () => {},
577
+ generateScopedName: "[name]__[local]___[hash:base64:5]",
578
+ localsConvention: "camelCaseOnly"
579
+ }, postCssPlugins)
580
+ }), sassPlugin({
581
+ filter: /\.(s[ac]ss|css)$/,
582
+ type,
583
+ async transform(css, _resolveDir, filePath) {
584
+ const start = performance.now();
585
+ const result = await postcss(postCssPlugins).process(css, { from: filePath });
586
+ logger.debug("Global CSS processed", {
587
+ filePath,
588
+ ms: Math.round(performance.now() - start)
589
+ });
590
+ return result.css;
591
+ }
592
+ })];
593
+ }
594
+
595
+ //#endregion
596
+ //#region src/esbuild/plugins/externalGlobal.ts
597
+ const externalGlobal = (externals, namespace = "spicetify-global") => {
598
+ return {
599
+ name: "spice_internal__external-global",
600
+ setup(build) {
601
+ build.onResolve({ filter: new RegExp(`^(${Object.keys(externals).join("|")})$`) }, (args) => ({
602
+ path: args.path,
603
+ namespace
604
+ }));
605
+ build.onLoad({
606
+ filter: /.*/,
607
+ namespace
608
+ }, (args) => {
609
+ return { contents: `module.exports = ${externals[args.path]}` };
610
+ });
611
+ }
612
+ };
613
+ };
614
+
615
+ //#endregion
616
+ //#region src/utils/spicetify/schema.ts
617
+ const SpicetifyConfigSchema = v.object({
618
+ Setting: v.object({
619
+ spotify_path: v.string(),
620
+ prefs_path: v.string(),
621
+ inject_theme_js: v.string(),
622
+ inject_css: v.string(),
623
+ current_theme: v.string(),
624
+ color_scheme: v.string(),
625
+ always_enable_devtools: v.string()
626
+ }),
627
+ AdditionalOptions: v.object({
628
+ experimental_features: v.string(),
629
+ extensions: v.pipe(v.string(), v.transform((input) => input.split("|").filter(Boolean))),
630
+ custom_apps: v.string()
631
+ }),
632
+ Backup: v.object({
633
+ version: v.string(),
634
+ with: v.string()
635
+ })
636
+ });
637
+
638
+ //#endregion
639
+ //#region src/utils/spicetify/index.ts
640
+ function runSpice(args) {
641
+ validateSpicetify(env.spicetifyBin);
642
+ return spawnSync(env.spicetifyBin, args, { encoding: "utf-8" });
643
+ }
644
+ const getExtensionDir = () => join(getSpiceDataPath(), "Extensions");
645
+ const getThemesDir = () => join(getSpiceDataPath(), "Themes");
646
+ async function getSpicetifyConfig() {
647
+ const { stdout, stderr, error } = runSpice(["path", "-c"]);
648
+ if (error || stderr) throw new Error(`Failed to locate Spicetify config: ${stderr || error?.message}`);
649
+ const rawConfig = parse(await readFile(stdout.trim(), "utf-8"));
650
+ const result = v.safeParse(SpicetifyConfigSchema, rawConfig);
651
+ if (result.success) return result.output;
652
+ else throw new Error("Spicetify Config Validation Failed:", v.flatten(result.issues).nested);
653
+ }
654
+ function getSpiceDataPath() {
655
+ const { stdout, stderr, error } = runSpice(["path", "userdata"]);
656
+ if (error || stderr) throw new Error(`Failed to locate Spicetify config: ${stderr || error?.message}`);
657
+ return stdout.trim();
658
+ }
659
+ function validateSpicetify(bin) {
660
+ const result = spawnSync(bin, ["--version"], { encoding: "utf-8" });
661
+ if (result.error) throw result.error;
662
+ if (result.status !== 0) throw new Error(`Invalid spicetify binary "${bin}": ${result.stderr || "unknown error"}`);
663
+ }
664
+
665
+ //#endregion
666
+ //#region src/esbuild/plugins/spicetifyHandlers.ts
667
+ const spicetifyHandler = ({ config, options, cache, logger = createLogger("plugin:spicetifyHandler") }) => ({
668
+ name: "spice_internal__spicetify-build-handler",
669
+ async setup(build) {
670
+ const { apply = true, copy = true, applyOnce = true, remove, outDir = "./dist" } = options;
671
+ let hasAppliedOnce = false;
672
+ const isExtension = config.template === "extension";
673
+ const identifier = isExtension ? `${urlSlugify(config.name)}.js` : urlSlugify(config.name);
674
+ const spiceConfig = await getSpicetifyConfig();
675
+ logger.debug(pc.green("Spicetify Config: "), spiceConfig);
676
+ if (apply) {
677
+ const defaultTheme = spiceConfig.Setting.current_theme;
678
+ build.onStart(() => {
679
+ const spiceIdentifier = remove ? `${identifier}-` : identifier;
680
+ if (isExtension) runSpice([
681
+ "config",
682
+ "extensions",
683
+ spiceIdentifier
684
+ ]);
685
+ else runSpice([
686
+ "config",
687
+ "current_theme",
688
+ spiceIdentifier
689
+ ]);
690
+ });
691
+ if (!isExtension && !remove) {
692
+ const resetTheme = () => {
693
+ runSpice([
694
+ "config",
695
+ "current_theme",
696
+ defaultTheme
697
+ ]);
698
+ process.exit();
699
+ };
700
+ process.once("SIGINT", resetTheme);
701
+ process.once("SIGTERM", resetTheme);
702
+ }
703
+ }
704
+ build.onEnd(async (result) => {
705
+ if (result.errors.length > 0) return;
706
+ if (!cache.hasChanges || cache.changed.size === 0) return;
707
+ const destDirs = [resolve(outDir)];
708
+ if (copy) destDirs.push(isExtension ? getExtensionDir() : resolve(getThemesDir(), identifier));
709
+ const tasks = [];
710
+ for (const filePath of cache.changed) {
711
+ const fileData = cache.files.get(filePath);
712
+ if (!fileData) continue;
713
+ for (const destDir of destDirs) {
714
+ const targetPath = resolve(destDir, basename(filePath));
715
+ tasks.push((async () => {
716
+ await mkdirp(destDir);
717
+ await writeFile(targetPath, fileData.contents);
718
+ })());
719
+ }
720
+ }
721
+ try {
722
+ await Promise.all(tasks);
723
+ logger.debug(pc.green(`${CHECK} Changed files copied.`));
724
+ } catch (err) {
725
+ logger.error(pc.red(`${CROSS} Failed to copy files: ${err instanceof Error ? err.message : String(err)}`));
726
+ return;
727
+ }
728
+ if (apply && cache.hasChanges && (!applyOnce || !hasAppliedOnce)) {
729
+ const { stderr, status } = runSpice(["apply"]);
730
+ if (status !== 0) logger.error(pc.red(`${CROSS} Spicetify apply failed: ${stderr}`));
731
+ else hasAppliedOnce = true;
732
+ }
733
+ });
734
+ }
735
+ });
736
+
737
+ //#endregion
738
+ //#region src/esbuild/plugins/wrapWithLoader.ts
739
+ function wrapWithLoader({ name, type, version, cache, outFiles, server, dev = false, logger = createLogger("plugin:wrapWithLoader") }) {
740
+ const namespace = "spice_internal__wrap-with-loader";
741
+ return {
742
+ name: namespace,
743
+ setup(build) {
744
+ if (build.initialOptions.write !== false) throw new Error(`[${namespace}] This plugin requires "write: false" in build options.`);
745
+ build.onEnd(async (res) => {
746
+ try {
747
+ if (res.errors.length > 0 || !res.outputFiles) return;
748
+ cache.changed.clear();
749
+ cache.hasChanges = false;
750
+ const filesChanged = [];
751
+ let bundledCss = "";
752
+ if (!dev && type === "extension") bundledCss = res.outputFiles.filter((f) => f.path.endsWith(".css")).map((f) => f.text).join("");
753
+ const transformPromises = res.outputFiles.map(async (file) => {
754
+ const isJs = file.path.endsWith(".js");
755
+ const isCss = file.path.endsWith(".css");
756
+ if (!dev && isCss && type === "extension") return;
757
+ const targetName = isJs ? outFiles.js : isCss ? outFiles.css ?? basename(file.path) : basename(file.path);
758
+ const renamedPath = join(build.initialOptions.outdir || "./dist/", targetName);
759
+ if (!isJs) {
760
+ cache.files.set(renamedPath, {
761
+ name: targetName,
762
+ contents: file.contents
763
+ });
764
+ cache.changed.add(renamedPath);
765
+ cache.hasChanges = true;
766
+ filesChanged.push(renamedPath);
767
+ return;
768
+ }
769
+ const slug = varSlugify(`${name}_${version}`);
770
+ const templateRaw = readFileSync(templateFilePath, "utf-8");
771
+ const minify = build.initialOptions.minify;
772
+ const { code: transformedTemp } = await transform(templateRaw, {
773
+ minify,
774
+ target: build.initialOptions.target || "es2020",
775
+ loader: "jsx",
776
+ define: {
777
+ __ESBUILD__HAS_CSS: JSON.stringify(type === "extension"),
778
+ __ESBUILD__APP_SLUG: JSON.stringify(slug),
779
+ __ESBUILD__APP_TYPE: JSON.stringify(type),
780
+ __ESBUILD__APP_ID: JSON.stringify(varSlugify(name)),
781
+ __ESBUILD__APP_VERSION: JSON.stringify(version),
782
+ __ESBUILD__APP_HASH: JSON.stringify("")
783
+ }
784
+ });
785
+ const template = replace(transformedTemp, {
786
+ "\"{{INJECT_START_COMMENT}}\"": minify ? "" : "/* --- START --- */",
787
+ "\"{{INJECT_END_COMMENT}}\"": minify ? "" : "/* --- END --- */",
788
+ "{{INJECTED_CSS_HERE}}": bundledCss,
789
+ "\"{{INJECTED_JS_HERE}}\"": file.text
790
+ });
791
+ const nextBuffer = Buffer.from(template);
792
+ const previous = cache.files.get(renamedPath);
793
+ if (previous?.contents && Buffer.compare(previous.contents, nextBuffer) === 0) return;
794
+ cache.files.set(renamedPath, {
795
+ name: targetName,
796
+ contents: nextBuffer
797
+ });
798
+ cache.changed.add(renamedPath);
799
+ cache.hasChanges = true;
800
+ filesChanged.push(renamedPath);
801
+ });
802
+ await Promise.all(transformPromises);
803
+ if (filesChanged.length > 0) server?.broadcast(filesChanged);
804
+ } catch (e) {
805
+ logger.error(`Error: ${e instanceof Error ? e.message : String(e)}`);
806
+ }
807
+ });
808
+ }
809
+ };
810
+ }
811
+
812
+ //#endregion
813
+ //#region src/esbuild/plugins/index.ts
814
+ const plugins = {
815
+ css,
816
+ cleanDist,
817
+ buildLogger,
818
+ externalGlobal,
819
+ wrapWithLoader,
820
+ spicetifyHandler
821
+ };
822
+
823
+ //#endregion
824
+ //#region src/esbuild/index.ts
825
+ const defaultBuildOptions = {
826
+ bundle: true,
827
+ write: false,
828
+ metafile: true,
829
+ treeShaking: true,
830
+ jsx: "transform",
831
+ format: "esm",
832
+ platform: "browser",
833
+ target: ["es2022", "chrome120"]
834
+ };
835
+ const getCommonPlugins = (opts) => {
836
+ const { template, minify, cache, name, version, buildOptions, outFiles, server, dev } = opts;
837
+ return [
838
+ ...plugins.css({
839
+ minify,
840
+ inline: !dev && template === "extension"
841
+ }),
842
+ plugins.externalGlobal({
843
+ react: "Spicetify.React",
844
+ "react-dom": "Spicetify.ReactDOM",
845
+ "react-dom/client": "Spicetify.ReactDOM",
846
+ "react-dom/server": "Spicetify.ReactDOMServer",
847
+ "react/jsx-runtime": "Spicetify.ReactJSX"
848
+ }),
849
+ plugins.wrapWithLoader({
850
+ name,
851
+ version,
852
+ type: template,
853
+ cache,
854
+ outFiles,
855
+ server,
856
+ dev
857
+ }),
858
+ plugins.spicetifyHandler({
859
+ config: opts,
860
+ cache,
861
+ options: buildOptions
862
+ }),
863
+ plugins.buildLogger({ cache })
864
+ ];
865
+ };
866
+
867
+ //#endregion
868
+ //#region src/build/index.ts
869
+ const logger = createLogger("build");
870
+ async function build$1(options) {
871
+ logger.clear();
872
+ logger.greeting(pc.green("Building for production..."));
873
+ let ctx;
874
+ await loadConfig(async (config, isNewUpdate) => {
875
+ if (isNewUpdate) {
876
+ logger.clear();
877
+ logger.info(pc.green("Config updated, reloading..."));
878
+ }
879
+ if (ctx) await ctx.dispose();
880
+ ctx = await context(getJSBuildOptions(config, options));
881
+ if (options.watch) {
882
+ logger.info(pc.blue("Watching for changes..."));
883
+ await ctx.watch();
884
+ } else try {
885
+ await ctx.rebuild();
886
+ } catch {} finally {
887
+ await ctx.dispose();
888
+ process.exit(0);
889
+ }
890
+ return async () => {
891
+ await ctx?.dispose();
892
+ ctx = void 0;
893
+ };
894
+ });
895
+ }
896
+ function getJSBuildOptions(config, options) {
897
+ const entryPoints = (() => {
898
+ if (config.template === "theme") return [config.entry.js, config.entry.css];
899
+ return [config.entry];
900
+ })();
901
+ const minify = options.watch ? false : options.minify;
902
+ const outDir = resolve(config.outDir);
903
+ const cache = {
904
+ files: /* @__PURE__ */ new Map(),
905
+ changed: /* @__PURE__ */ new Set(),
906
+ hasChanges: true
907
+ };
908
+ const outFiles = {
909
+ js: config.template === "extension" ? `${urlSlugify(config.name)}.js` : "theme.js",
910
+ css: config.template === "theme" ? "user.css" : null
911
+ };
912
+ const overrides = {
913
+ ...defaultBuildOptions,
914
+ outdir: outDir,
915
+ minify,
916
+ sourcemap: false,
917
+ external: [
918
+ ...config.esbuildOptions?.external ? config.esbuildOptions.external : [],
919
+ "react",
920
+ "react-dom"
921
+ ],
922
+ plugins: [...config.esbuildOptions?.plugins ? config.esbuildOptions.plugins : [], ...getCommonPlugins({
923
+ ...config,
924
+ minify,
925
+ cache,
926
+ buildOptions: {
927
+ apply: options.apply,
928
+ copy: options.copy,
929
+ outDir,
930
+ applyOnce: false,
931
+ remove: false
932
+ },
933
+ outFiles
934
+ })]
935
+ };
936
+ return {
937
+ entryPoints,
938
+ ...config.esbuildOptions,
939
+ ...overrides
940
+ };
941
+ }
942
+
943
+ //#endregion
944
+ //#region src/commands/build.ts
945
+ const CLIOptionsSchema$1 = v.strictObject({
946
+ watch: v.boolean(),
947
+ minify: v.boolean(),
948
+ apply: v.boolean(),
949
+ copy: v.boolean()
950
+ });
951
+ 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) => {
952
+ await build$1(safeParse(CLIOptionsSchema$1, opts));
953
+ });
954
+
955
+ //#endregion
956
+ //#region src/create/package.ts
957
+ const FRAMEWORKS$1 = {
958
+ react: {
959
+ dependencies: {
960
+ react: "^18.3.1",
961
+ "react-dom": "^18.3.1"
962
+ },
963
+ devDependencies: {
964
+ "@types/react": "^18.3.27",
965
+ "@types/react-dom": "^18.3.7"
966
+ }
967
+ },
968
+ vanilla: {}
969
+ };
970
+ const LINTERS$1 = {
971
+ eslint: {
972
+ scripts: { lint: "eslint ." },
973
+ devDependencies: {
974
+ eslint: "^9.39.2",
975
+ "@eslint/js": "^9.39.2",
976
+ "@eslint/css": "^0.14.1",
977
+ globals: "^17.2.0",
978
+ jiti: "^2.6.1"
979
+ }
980
+ },
981
+ biome: {
982
+ scripts: { lint: "biome check --apply ." },
983
+ devDependencies: { "@biomejs/biome": "latest" }
984
+ },
985
+ oxlint: {
986
+ scripts: {
987
+ lint: "oxlint",
988
+ "lint:fix": "oxlint --fix"
989
+ },
990
+ devDependencies: { oxlint: "^1.43.0" }
991
+ }
992
+ };
993
+ const INTERSECTIONS = { reactEslint: {
994
+ condition: (options) => options.framework === "react" && options.linter === "eslint",
995
+ devDependencies: {
996
+ "eslint-plugin-react": "^7.37.5",
997
+ "typescript-eslint": "^8.54.0"
998
+ }
999
+ } };
1000
+ function createPackageJSON(options) {
1001
+ const result = {
1002
+ name: options.name,
1003
+ description: `A Spicetify ${options.template} created with @spicetify/creator`,
1004
+ version: "0.0.1",
1005
+ type: "module",
1006
+ private: true,
1007
+ scripts: {
1008
+ sc: "spicetify-creator",
1009
+ dev: "spicetify-creator dev",
1010
+ build: "spicetify-creator build"
1011
+ },
1012
+ dependencies: {},
1013
+ devDependencies: { "@spicetify/creator": env.isInternal ? "link:@spicetify/creator" : "latest" }
1014
+ };
1015
+ if (options.language === "ts") result.peerDependencies = {
1016
+ ...result.peerDependencies,
1017
+ typescript: "^5"
1018
+ };
1019
+ const slices = [FRAMEWORKS$1[options.framework], LINTERS$1[options.linter]];
1020
+ Object.values(INTERSECTIONS).forEach((intersection) => {
1021
+ if (intersection.condition(options)) slices.push(intersection);
1022
+ });
1023
+ slices.forEach((slice) => {
1024
+ if (!slice) return;
1025
+ for (const key of [
1026
+ "scripts",
1027
+ "dependencies",
1028
+ "devDependencies"
1029
+ ]) if (slice[key]) result[key] = {
1030
+ ...result[key],
1031
+ ...slice[key]
1032
+ };
1033
+ });
1034
+ return result;
1035
+ }
1036
+ function writePackageJSON(packageJSON, targetDir) {
1037
+ try {
1038
+ mkdirp(targetDir);
1039
+ const data = `${JSON.stringify(packageJSON, null, 2)}\n`;
1040
+ writeFileSync(join(targetDir, "package.json"), data, "utf8");
1041
+ } catch (e) {
1042
+ const message = e instanceof Error ? e.message : "Unknown error";
1043
+ log.error(`Failed to write package.json: ${message}`);
1044
+ cancel("Exiting...");
1045
+ process.exit(1);
1046
+ }
1047
+ }
1048
+ function validateProjectName(name) {
1049
+ if (!name.length) return "Project name is required";
1050
+ if (!/^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(name)) return "Invalid project name (must be lowercase and URL-friendly)";
1051
+ }
1052
+
1053
+ //#endregion
1054
+ //#region src/create/template.ts
1055
+ const ext = (lang) => lang === "ts" ? "ts" : "js";
1056
+ const kv = ({ name, language, framework, linter, packageManager, template }) => ({
1057
+ "{{project-name}}": name,
1058
+ "{{framework}}": framework,
1059
+ "{{linter}}": linter,
1060
+ "{{package-manager}}": packageManager,
1061
+ "{{template}}": template,
1062
+ "{{language}}": ext(language),
1063
+ "{{entry-ext}}": `${ext(language)}${framework === "react" ? "x" : ""}`,
1064
+ "{{docs-link}}": DOCS_LINK,
1065
+ "{{get-started-link}}": DOCS_LINK,
1066
+ "{{discord-link}}": DISCORD_LINK,
1067
+ "{{github-link}}": GITHUB_LINK,
1068
+ "{{spicetify-link}}": SPICETIFY_LINK,
1069
+ "{{config-reference-link}}": CONFIG_REF_LINK
1070
+ });
1071
+ const action = { modify(c, opts) {
1072
+ return replace(c, kv(opts));
1073
+ } };
1074
+ const COMMON_FILES = (opts) => [
1075
+ {
1076
+ from: "README.template.md",
1077
+ to: "README.md",
1078
+ action,
1079
+ isShared: true
1080
+ },
1081
+ {
1082
+ from: ".gitignore",
1083
+ to: ".gitignore",
1084
+ isShared: true
1085
+ },
1086
+ {
1087
+ from: `spice.config.${ext(opts.language)}`,
1088
+ to: `spice.config.${ext(opts.language)}`,
1089
+ action,
1090
+ isShared: true
1091
+ },
1092
+ {
1093
+ from: "app.css",
1094
+ to: "src/app.css",
1095
+ action,
1096
+ isShared: true
1097
+ },
1098
+ ...opts.language === "ts" ? [{
1099
+ from: "css.d.ts",
1100
+ to: "src/types/css.d.ts",
1101
+ isShared: true
1102
+ }] : []
1103
+ ];
1104
+ const LANGUAGE_FILES = {
1105
+ js: [{
1106
+ from: "jsconfig.json",
1107
+ to: "jsconfig.json",
1108
+ isShared: true
1109
+ }],
1110
+ ts: [{
1111
+ from: "tsconfig.json",
1112
+ to: "tsconfig.json",
1113
+ isShared: true
1114
+ }]
1115
+ };
1116
+ const FRAMEWORKS = {
1117
+ react: ({ language }) => [{
1118
+ from: `src/app.${ext(language)}x`,
1119
+ to: `src/app.${ext(language)}x`,
1120
+ action
1121
+ }, {
1122
+ from: `src/components/Onboarding.${ext(language)}x`,
1123
+ to: `src/components/Onboarding.${ext(language)}x`,
1124
+ action
1125
+ }],
1126
+ vanilla: ({ language }) => [{
1127
+ from: `src/app.${ext(language)}`,
1128
+ to: `src/app.${ext(language)}`,
1129
+ action
1130
+ }, {
1131
+ from: `src/components/Onboarding.${ext(language)}`,
1132
+ to: `src/components/Onboarding.${ext(language)}`,
1133
+ action
1134
+ }]
1135
+ };
1136
+ const LINTERS = {
1137
+ biome: [{
1138
+ from: "biome.json",
1139
+ to: "biome.json",
1140
+ isShared: true
1141
+ }],
1142
+ eslint: ({ language }) => [{
1143
+ from: `eslint.config.${ext(language)}`,
1144
+ to: `eslint.config.${ext(language)}`
1145
+ }],
1146
+ oxlint: [{
1147
+ from: ".oxlintrc.json",
1148
+ to: ".oxlintrc.json",
1149
+ isShared: true
1150
+ }]
1151
+ };
1152
+ function setupTemplateFiles(options, targetDir) {
1153
+ const { template, language, framework, linter } = options;
1154
+ const templateRoot = dist(`templates/${template}`, import.meta.url);
1155
+ const fromDir = join(templateRoot, language, framework);
1156
+ const resolve = (slice) => typeof slice === "function" ? slice(options) : slice ?? [];
1157
+ const files = [
1158
+ ...resolve(COMMON_FILES),
1159
+ ...resolve(LANGUAGE_FILES[language]),
1160
+ ...resolve(FRAMEWORKS[framework]),
1161
+ ...resolve(LINTERS[linter])
1162
+ ];
1163
+ for (const file of files) {
1164
+ const src = (() => {
1165
+ if (file.isGlobal) return join(templateRoot, file.from);
1166
+ if (file.isShared) return join(templateRoot, "shared", file.from);
1167
+ return join(fromDir, file.from);
1168
+ })();
1169
+ const dest = join(targetDir, file.to);
1170
+ if (!existsSync(src)) {
1171
+ log.warn(`[Template] Source missing: ${src}`);
1172
+ continue;
1173
+ }
1174
+ mkdirp(dirname(dest));
1175
+ let content = readFileSync(src, "utf8");
1176
+ if (file.action?.modify) content = file.action.modify(content, options);
1177
+ writeFileSync(dest, content, "utf8");
1178
+ }
1179
+ }
1180
+
1181
+ //#endregion
1182
+ //#region src/utils/is-online.ts
1183
+ function getProxy() {
1184
+ if (process.env.https_proxy) return process.env.https_proxy;
1185
+ try {
1186
+ const httpsProxy = execSync("npm config get https-proxy").toString().trim();
1187
+ return httpsProxy !== "null" ? httpsProxy : void 0;
1188
+ } catch {
1189
+ return;
1190
+ }
1191
+ }
1192
+ async function getOnline() {
1193
+ try {
1194
+ await lookup("registry.yarnpkg.com");
1195
+ return true;
1196
+ } catch {
1197
+ const proxy = getProxy();
1198
+ if (!proxy) return false;
1199
+ let url;
1200
+ try {
1201
+ url = new URL$1(proxy);
1202
+ } catch {
1203
+ return false;
1204
+ }
1205
+ const { hostname } = url;
1206
+ if (!hostname) return false;
1207
+ try {
1208
+ await lookup(hostname);
1209
+ return true;
1210
+ } catch {
1211
+ return false;
1212
+ }
1213
+ }
1214
+ }
1215
+
1216
+ //#endregion
1217
+ //#region src/utils/git.ts
1218
+ function isInGitRepository() {
1219
+ try {
1220
+ execSync("git rev-parse --is-inside-work-tree", { stdio: "ignore" });
1221
+ return true;
1222
+ } catch {}
1223
+ return false;
1224
+ }
1225
+ function isInMercurialRepository() {
1226
+ try {
1227
+ execSync("hg --cwd . root", { stdio: "ignore" });
1228
+ return true;
1229
+ } catch {}
1230
+ return false;
1231
+ }
1232
+ function isDefaultBranchSet() {
1233
+ try {
1234
+ execSync("git config init.defaultBranch", { stdio: "ignore" });
1235
+ return true;
1236
+ } catch {}
1237
+ return false;
1238
+ }
1239
+ function tryGitInit(root) {
1240
+ let didInit = false;
1241
+ try {
1242
+ execSync("git --version", { stdio: "ignore" });
1243
+ if (isInGitRepository() || isInMercurialRepository()) return false;
1244
+ execSync("git init", { stdio: "ignore" });
1245
+ didInit = true;
1246
+ if (!isDefaultBranchSet()) execSync("git checkout -b main", { stdio: "ignore" });
1247
+ execSync("git add -A", { stdio: "ignore" });
1248
+ execSync("git commit -m \"Initial commit from @spicetify/creator create\"", { stdio: "ignore" });
1249
+ return true;
1250
+ } catch {
1251
+ if (didInit) try {
1252
+ rmSync(join(root, ".git"), {
1253
+ recursive: true,
1254
+ force: true
1255
+ });
1256
+ } catch {}
1257
+ return false;
1258
+ }
1259
+ }
1260
+
1261
+ //#endregion
1262
+ //#region src/create/index.ts
1263
+ const isOnline = await getOnline();
1264
+ async function createProject(cwd, options) {
1265
+ const { directory, template, language, linter, framework, packageManager, disableGit } = await p.group({
1266
+ directory: async () => {
1267
+ if (cwd) return normalizePosix(cwd);
1268
+ const defaultPath = "./";
1269
+ return await p.text({
1270
+ message: "Where would you like your project to be created?",
1271
+ placeholder: ` (hit Enter to use '${defaultPath}')`,
1272
+ defaultValue: defaultPath
1273
+ });
1274
+ },
1275
+ force: async ({ results: { directory } }) => {
1276
+ if (!options.dirCheck || !directory) return;
1277
+ if (!existsSync(directory)) return;
1278
+ const root = resolve(directory);
1279
+ const name = basename(root);
1280
+ const conflicts = readdirSync(root).filter((file) => !VALID_PROJECT_FILES.has(file) && !file.endsWith(".iml"));
1281
+ if (conflicts.length === 0) return;
1282
+ p.log.warn(`The directory ${pc.green(name)} contains files that could conflict:`);
1283
+ const conflictList = conflicts.map((file) => {
1284
+ try {
1285
+ return lstatSync(join(root, file)).isDirectory() ? ` ${pc.blue(file)}/` : ` ${file}`;
1286
+ } catch {
1287
+ return `${file}`;
1288
+ }
1289
+ }).join("\n");
1290
+ p.note(conflictList, "Potential Conflicts");
1291
+ const force = await p.confirm({
1292
+ message: `Directory is not empty. ${pc.yellow("Overwrite and continue?")}`,
1293
+ initialValue: false
1294
+ });
1295
+ if (p.isCancel(force) || !force) {
1296
+ p.cancel("Operation cancelled.");
1297
+ process.exit(0);
1298
+ }
1299
+ },
1300
+ template: async () => {
1301
+ if (options.template) return options.template;
1302
+ return await p.select({
1303
+ message: "Select which template you want to chose",
1304
+ options: templateOptions
1305
+ });
1306
+ },
1307
+ framework: async () => {
1308
+ if (options.framework) return options.framework;
1309
+ return await p.select({
1310
+ message: "Select which framework you want to chose",
1311
+ options: frameworkOptions
1312
+ });
1313
+ },
1314
+ language: async () => {
1315
+ if (options.language) return options.language;
1316
+ return await p.select({
1317
+ message: "Select which language you want to chose",
1318
+ initialValue: "ts",
1319
+ options: languageOptions
1320
+ });
1321
+ },
1322
+ linter: async () => {
1323
+ if (options.linter) return options.linter;
1324
+ return await p.select({
1325
+ message: "Select which linter you want to chose",
1326
+ options: linterOptions
1327
+ });
1328
+ },
1329
+ packageManager: async () => {
1330
+ if (options.packageManager) return options.packageManager;
1331
+ return await p.select({
1332
+ message: "Select which package manager you want to choose to install packages",
1333
+ options: packageManagers.map((value) => ({
1334
+ value,
1335
+ title: value
1336
+ })),
1337
+ initialValue: getPackageManager()
1338
+ });
1339
+ },
1340
+ disableGit: async () => {
1341
+ if (!options.git) return options.git;
1342
+ return !await p.confirm({
1343
+ message: "Initialize a Git repository?",
1344
+ initialValue: true
1345
+ });
1346
+ }
1347
+ }, { onCancel: () => {
1348
+ p.cancel("Operation cancelled.");
1349
+ process.exit(0);
1350
+ } });
1351
+ const projectPath = resolve(directory);
1352
+ const projectName = await p.text({
1353
+ message: "Confirm or enter the package name:",
1354
+ initialValue: basename(projectPath).toLowerCase(),
1355
+ validate: validateProjectName
1356
+ });
1357
+ if (p.isCancel(projectName)) {
1358
+ p.cancel("Operation cancelled.");
1359
+ process.exit(0);
1360
+ }
1361
+ await create$1(projectPath, {
1362
+ name: projectName,
1363
+ template,
1364
+ language,
1365
+ linter,
1366
+ framework,
1367
+ packageManager,
1368
+ disableGit,
1369
+ install: options.install
1370
+ });
1371
+ p.log.success("Project created");
1372
+ }
1373
+ async function create$1(cwd, options) {
1374
+ try {
1375
+ mkdirp(cwd);
1376
+ setupTemplateFiles(options, cwd);
1377
+ const pkgJSON = createPackageJSON(options);
1378
+ writePackageJSON(pkgJSON, cwd);
1379
+ chdir(cwd);
1380
+ await promptPackageInstallation(options, pkgJSON);
1381
+ if (!options.disableGit) if (tryGitInit(cwd)) p.log.success("Initialized a git repository");
1382
+ else p.log.error("Failed to initialize git repository");
1383
+ else p.log.info("Skipping git initialization");
1384
+ } catch (e) {
1385
+ const message = e instanceof Error ? e.message : "Unexpected Error";
1386
+ p.log.error(`Failed to scaffold ${options.template}: ${message}\n`);
1387
+ p.cancel("Exiting...");
1388
+ process.exit(1);
1389
+ }
1390
+ }
1391
+ async function promptPackageInstallation(options, pkg) {
1392
+ if (!options.install) return;
1393
+ const shouldInstall = await p.confirm({
1394
+ message: `Install dependencies using ${options.packageManager}?`,
1395
+ initialValue: true
1396
+ });
1397
+ if (p.isCancel(shouldInstall)) {
1398
+ p.cancel("Operation cancelled.");
1399
+ process.exit(0);
1400
+ }
1401
+ try {
1402
+ if (shouldInstall) {
1403
+ p.log.info(`Installing dependencies with ${options.packageManager}...`);
1404
+ await installPackages(options.packageManager, isOnline);
1405
+ const formatDeps = (deps) => {
1406
+ return Object.entries(deps).map(([name, version]) => `${pc.bold(name)}@${pc.blue(version)}`).join("\n");
1407
+ };
1408
+ if (pkg.dependencies) p.note(formatDeps(pkg.dependencies), "Dependencies");
1409
+ if (pkg.devDependencies) p.note(formatDeps(pkg.devDependencies), "Dev Dependencies");
1410
+ p.log.info("Dependencies installed successfully.");
1411
+ } else p.log.info(`Skipping install. You can run '${options.packageManager} install' later.`);
1412
+ } catch {
1413
+ p.log.info(`Failed to install dependencies, check the errors above.`);
1414
+ }
1415
+ }
1416
+
1417
+ //#endregion
1418
+ //#region src/commands/create.ts
1419
+ const PathSchema = v.optional(v.string());
1420
+ const OptionsSchema = v.strictObject({
1421
+ install: v.boolean(),
1422
+ git: v.boolean(),
1423
+ dirCheck: v.boolean(),
1424
+ language: v.optional(v.picklist(languageTypes)),
1425
+ template: v.optional(v.picklist(templateTypes)),
1426
+ framework: v.optional(v.picklist(frameworkTypes)),
1427
+ linter: v.optional(v.picklist(linterTypes)),
1428
+ packageManager: v.optional(v.picklist(packageManagers))
1429
+ });
1430
+ const templateOption = new Option("-t, --template <name>", "Template to use").choices(templateTypes);
1431
+ const linterOption = new Option("--linter <name>", "Linter to use").choices(linterTypes);
1432
+ const langOption = new Option("--language <lang>", "Language to use").choices(languageTypes);
1433
+ const frameworkOption = new Option("--framework <name>", "Framework to use").choices(frameworkTypes);
1434
+ const packageManagerOption = new Option("--pm, --package-manager <name>", "Package manager to use").choices(packageManagers);
1435
+ const create = new Command("create").description("Scaffolds a new spicetify project").argument("[path]", "Where the project will be created").addOption(templateOption).addOption(langOption).addOption(frameworkOption).addOption(linterOption).addOption(packageManagerOption).option("--disable-git, --no-git", "Skip initializing a git repository.").option("--skip-install, --no-install", "Skip installing packages").option("--skip-dir-check, --no-dir-check", "Even if the folder is not empty, no prompt will be shown").action((path, opts) => {
1436
+ try {
1437
+ const cwd = safeParse(PathSchema, path);
1438
+ const options = safeParse(OptionsSchema, opts);
1439
+ runCommand(async () => {
1440
+ await createProject(cwd, options);
1441
+ }, {
1442
+ outro: true,
1443
+ message: (opts) => `Welcome to ${opts.name} CLI ${pc.gray(`(v${opts.version})`)}`
1444
+ });
1445
+ } catch (error) {
1446
+ if (v.isValiError(error)) {
1447
+ logger$2.error("\nInvalid configuration:");
1448
+ error.issues.forEach((issue) => {
1449
+ const path = issue.path?.map((p) => p.key).join(".") || "input";
1450
+ logger$2.error(`${pc.yellow(` - ${path}:`)} ${issue.message}`);
1451
+ });
1452
+ } else logger$2.error(`\nAn unexpected error occurred: ${error instanceof Error ? error.message : String(error)}`);
1453
+ process.exit(1);
1454
+ }
1455
+ });
1456
+
1457
+ //#endregion
1458
+ //#region src/dev/server/templates/root.ts
1459
+ const root = () => `<!DOCTYPE html>
1460
+ <html lang="en">
1461
+ <head>
1462
+ <meta charset="UTF-8">
1463
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
1464
+ <title>Spicetify Creator | Dev Server</title>
1465
+ </head>
1466
+ <body>
1467
+ <h1>Spicetify Creator Server up and running.</h1>
1468
+ </body>
1469
+ </html>
1470
+ `;
1471
+
1472
+ //#endregion
1473
+ //#region src/dev/server/index.ts
1474
+ const WS_PATH = "/spicetify-creator";
1475
+ async function createHmrServer(config, logger = createLogger("hmrServer")) {
1476
+ const { port = DEFAULT_PORT, serveDir = outDir } = config;
1477
+ let isRunning = false;
1478
+ const mimeTypes = {
1479
+ ".html": "text/html",
1480
+ ".js": "text/javascript",
1481
+ ".css": "text/css",
1482
+ ".json": "application/json",
1483
+ ".png": "image/png",
1484
+ ".jpg": "image/jpeg",
1485
+ ".jpeg": "image/jpeg",
1486
+ ".gif": "image/gif",
1487
+ ".svg": "image/svg+xml",
1488
+ ".ico": "image/x-icon"
1489
+ };
1490
+ const httpServer = createServer((req, res) => {
1491
+ const corsHeaders = {
1492
+ "Access-Control-Allow-Origin": "*",
1493
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS, PUT, PATCH, DELETE",
1494
+ "Access-Control-Allow-Headers": "X-Requested-With, Content-Type, Authorization",
1495
+ "Access-Control-Max-Age": "86400"
1496
+ };
1497
+ if (req.method === "OPTIONS") {
1498
+ res.writeHead(204, corsHeaders);
1499
+ res.end();
1500
+ return;
1501
+ }
1502
+ const wrapResponse = (statusCode, headers) => {
1503
+ res.writeHead(statusCode, {
1504
+ ...headers,
1505
+ ...corsHeaders
1506
+ });
1507
+ };
1508
+ const cleanUrl = (req.url ?? "/").split("?")[0] ?? "/";
1509
+ if (cleanUrl === "/" || cleanUrl === "/index.html") {
1510
+ wrapResponse(200, { "Content-Type": "text/html" });
1511
+ res.end(root());
1512
+ return;
1513
+ }
1514
+ const filePath = resolve(join(serveDir, cleanUrl.startsWith("/files/") ? cleanUrl.slice(6) : cleanUrl));
1515
+ try {
1516
+ if (existsSync(filePath) && statSync(filePath).isFile()) {
1517
+ const contentType = mimeTypes[extname(filePath).toLowerCase()] || "application/octet-stream";
1518
+ wrapResponse(200, { "Content-Type": contentType });
1519
+ createReadStream(filePath).pipe(res);
1520
+ return;
1521
+ }
1522
+ } catch {}
1523
+ wrapResponse(404, { "Content-Type": "text/html" });
1524
+ res.end("<h1>404 - Not Found</h1>");
1525
+ });
1526
+ const wss = new WebSocketServer({ noServer: true });
1527
+ const clients = /* @__PURE__ */ new Set();
1528
+ httpServer.on("upgrade", (req, socket, head) => {
1529
+ const { url } = req;
1530
+ if (!url?.startsWith(WS_PATH)) {
1531
+ socket.destroy();
1532
+ return;
1533
+ }
1534
+ wss.handleUpgrade(req, socket, head, (ws) => {
1535
+ clients.add(ws);
1536
+ ws.on("close", () => {
1537
+ clients.delete(ws);
1538
+ });
1539
+ ws.on("error", () => {
1540
+ clients.delete(ws);
1541
+ });
1542
+ wss.emit("connection", ws, req);
1543
+ });
1544
+ });
1545
+ function broadcast(data) {
1546
+ const message = typeof data === "string" ? data : JSON.stringify(data);
1547
+ for (const client of clients) if (client.readyState === WebSocket.OPEN) client.send(message);
1548
+ }
1549
+ return {
1550
+ start: async () => new Promise((resolve) => {
1551
+ httpServer.listen(port, () => {
1552
+ logger.debug(`${pc.bold("HTTP Server Started at")}: ${pc.cyan(`http://localhost:${port}/`)}`);
1553
+ isRunning = true;
1554
+ resolve();
1555
+ });
1556
+ }),
1557
+ stop: () => new Promise((resolve, reject) => {
1558
+ httpServer.close((err) => {
1559
+ if (err) return reject(err);
1560
+ isRunning = false;
1561
+ logger.debug(`${pc.yellow("! ")} ${pc.gray("HTTP server stopped")}`);
1562
+ resolve();
1563
+ });
1564
+ }),
1565
+ broadcast,
1566
+ get port() {
1567
+ return port;
1568
+ },
1569
+ get isRunning() {
1570
+ return isRunning;
1571
+ },
1572
+ get link() {
1573
+ return `http://localhost:${port}`;
1574
+ },
1575
+ get wsLink() {
1576
+ return `ws://localhost:${port}${WS_PATH}`;
1577
+ }
1578
+ };
1579
+ }
1580
+
1581
+ //#endregion
1582
+ //#region src/utils/hmr.ts
1583
+ const injectHMRExtension = async (rootLink, wsLink, outFiles) => {
1584
+ const extName = `sc-live-reload-helper.js`;
1585
+ const spiceConfig = await getSpicetifyConfig();
1586
+ const cleanup = () => {
1587
+ if (env.isDev) logger$2.log(`[Spicetify] Removing Live reload extension...`);
1588
+ try {
1589
+ runSpice([
1590
+ "config",
1591
+ "extensions",
1592
+ `${extName}-`
1593
+ ]);
1594
+ runSpice(["apply"]);
1595
+ logger$2.debug(pc.green(`${CHECK} Cleanup successful.`));
1596
+ } catch (e) {
1597
+ if (env.isDev) logger$2.error(pc.red(`${CROSS} Cleanup failed: `), e);
1598
+ }
1599
+ process.exit();
1600
+ };
1601
+ if (env.isDev) {
1602
+ process.on("SIGINT", cleanup);
1603
+ process.on("SIGTERM", cleanup);
1604
+ }
1605
+ try {
1606
+ logger$2.debug(`[Spicetify] Preparing Live reload extension...`);
1607
+ const destDir = getExtensionDir();
1608
+ mkdirp(destDir);
1609
+ const outDir = resolve(destDir, extName);
1610
+ const { code } = await transform(readFileSync(liveReloadFilePath, "utf8"), {
1611
+ loader: "js",
1612
+ define: {
1613
+ _SERVER_URL: JSON.stringify(rootLink),
1614
+ _HOT_RELOAD_LINK: JSON.stringify(wsLink),
1615
+ _JS_PATH: JSON.stringify(`/files/${outFiles.js}`),
1616
+ _CSS_PATH: JSON.stringify(outFiles.css ? `/files/${outFiles.css}` : `/files/app.css`)
1617
+ }
1618
+ });
1619
+ writeFileSync(outDir, code);
1620
+ if (spiceConfig) {
1621
+ runSpice([
1622
+ "config",
1623
+ "extensions",
1624
+ extName
1625
+ ]);
1626
+ runSpice(["apply"]);
1627
+ }
1628
+ logger$2.debug(pc.green(`${CHECK} Live reload extension injected successfully.`));
1629
+ } catch (err) {
1630
+ logger$2.error(pc.red(`${CROSS} Failed to inject HMR helper: ${err instanceof Error ? err.message : String(err)}`));
1631
+ }
1632
+ };
1633
+
1634
+ //#endregion
1635
+ //#region src/dev/index.ts
1636
+ const DEFAULT_PORT = 54321;
1637
+ const outDir = resolve(`./.spicetify/build/`);
1638
+ async function dev$1(options) {
1639
+ logger$2.clear();
1640
+ logger$2.greeting(pc.green("Starting development environment"));
1641
+ let ctx;
1642
+ let server = void 0;
1643
+ loadConfig(async (config, isNewUpdate) => {
1644
+ if (isNewUpdate) {
1645
+ logger$2.clear();
1646
+ logger$2.info(pc.green("Config updated, reloading..."));
1647
+ }
1648
+ try {
1649
+ server = await createHmrServer({
1650
+ ...config.serverConfig,
1651
+ serveDir: config.serverConfig.serveDir ?? outDir,
1652
+ port: options.port ?? config.serverConfig.port
1653
+ });
1654
+ await server.start();
1655
+ const outFiles = {
1656
+ js: config.template === "extension" ? `${urlSlugify(config.name)}.js` : "theme.js",
1657
+ css: config.template === "theme" ? "user.css" : null
1658
+ };
1659
+ await injectHMRExtension(server.link, server.wsLink, outFiles);
1660
+ ctx = await context(getJSDevOptions(config, {
1661
+ ...options,
1662
+ outFiles,
1663
+ server
1664
+ }));
1665
+ await ctx.watch();
1666
+ logger$2.info(pc.blue("Watching for changes..."));
1667
+ } catch (err) {
1668
+ logger$2.error("Failed to start dev server: ", err);
1669
+ }
1670
+ return async () => {
1671
+ try {
1672
+ await ctx?.dispose();
1673
+ ctx = void 0;
1674
+ await server?.stop();
1675
+ } finally {
1676
+ process.exit();
1677
+ }
1678
+ };
1679
+ });
1680
+ }
1681
+ function getJSDevOptions(config, options) {
1682
+ const entryPoints = (() => {
1683
+ if (config.template === "theme") return [config.entry.js, config.entry.css];
1684
+ return [config.entry];
1685
+ })();
1686
+ const minify = false;
1687
+ const cache = {
1688
+ files: /* @__PURE__ */ new Map(),
1689
+ changed: /* @__PURE__ */ new Set(),
1690
+ hasChanges: true
1691
+ };
1692
+ const overrides = {
1693
+ ...defaultBuildOptions,
1694
+ outdir: outDir,
1695
+ minify,
1696
+ sourcemap: "inline",
1697
+ external: [
1698
+ ...config.esbuildOptions?.external ? config.esbuildOptions.external : [],
1699
+ "react",
1700
+ "react-dom"
1701
+ ],
1702
+ plugins: [...config.esbuildOptions?.plugins ? config.esbuildOptions.plugins : [], ...getCommonPlugins({
1703
+ ...config,
1704
+ minify,
1705
+ cache,
1706
+ buildOptions: {
1707
+ copy: true,
1708
+ remove: true,
1709
+ outDir
1710
+ },
1711
+ dev: true,
1712
+ server: options.server,
1713
+ outFiles: options.outFiles
1714
+ })]
1715
+ };
1716
+ return {
1717
+ entryPoints,
1718
+ ...config.esbuildOptions,
1719
+ ...overrides
1720
+ };
1721
+ }
1722
+
1723
+ //#endregion
1724
+ //#region src/commands/dev.ts
1725
+ const CLIOptionsSchema = v.strictObject({ port: v.optional(v.pipe(v.union([v.string(), v.number()]), v.transform((val) => Number(val)), v.number("Port must be a valid number"), v.minValue(1, "Port must be greater than 0"), v.maxValue(65535, "Port must be less than 65536"))) });
1726
+ const dev = new Command("dev").description("Develop your spicetify project").option("-p, --port <number>", "Port for the development server").action(async (opts) => {
1727
+ await dev$1(safeParse(CLIOptionsSchema, opts));
1728
+ });
1729
+
1730
+ //#endregion
1731
+ //#region src/bin.ts
1732
+ logger$2.debug(`Env: ${JSON.stringify(env, null, 2)}\n`);
1733
+ const command = new Command();
1734
+ create.alias("init");
1735
+ command.addCommand(create).addCommand(build).addCommand(dev);
1736
+ command.parse();
1737
+
1738
+ //#endregion
1739
+ export { };