@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.
- package/LICENSE +674 -0
- package/README.md +35 -0
- package/dist/bin.d.mts +1 -0
- package/dist/bin.mjs +1739 -0
- package/dist/client/index.d.mts +2175 -0
- package/dist/client/index.mjs +7 -0
- package/dist/templates/extension/js/react/eslint.config.js +29 -0
- package/dist/templates/extension/js/react/src/app.jsx +27 -0
- package/dist/templates/extension/js/react/src/components/Onboarding.jsx +72 -0
- package/dist/templates/extension/js/vanilla/components/Onboarding.js +71 -0
- package/dist/templates/extension/js/vanilla/eslint.config.js +16 -0
- package/dist/templates/extension/js/vanilla/src/app.js +12 -0
- package/dist/templates/extension/meta.json +4 -0
- package/dist/templates/extension/shared/.oxlintrc.json +36 -0
- package/dist/templates/extension/shared/README.template.md +53 -0
- package/dist/templates/extension/shared/app.css +163 -0
- package/dist/templates/extension/shared/biome.json +36 -0
- package/dist/templates/extension/shared/css.d.ts +44 -0
- package/dist/templates/extension/shared/jsconfig.json +32 -0
- package/dist/templates/extension/shared/spice.config.js +9 -0
- package/dist/templates/extension/shared/spice.config.ts +9 -0
- package/dist/templates/extension/shared/tsconfig.json +32 -0
- package/dist/templates/extension/ts/react/eslint.config.ts +29 -0
- package/dist/templates/extension/ts/react/src/app.tsx +27 -0
- package/dist/templates/extension/ts/react/src/components/Onboarding.tsx +83 -0
- package/dist/templates/extension/ts/vanilla/biome.json +36 -0
- package/dist/templates/extension/ts/vanilla/eslint.config.ts +16 -0
- package/dist/templates/extension/ts/vanilla/src/app.ts +12 -0
- package/dist/templates/extension/ts/vanilla/src/components/Onboarding.ts +79 -0
- package/dist/templates/liveReload.js +70 -0
- package/dist/templates/theme/js/react/eslint.config.js +29 -0
- package/dist/templates/theme/js/react/src/app.jsx +25 -0
- package/dist/templates/theme/js/react/src/components/Onboarding.jsx +72 -0
- package/dist/templates/theme/js/vanilla/eslint.config.js +16 -0
- package/dist/templates/theme/js/vanilla/src/app.js +11 -0
- package/dist/templates/theme/js/vanilla/src/components/Onboarding.js +71 -0
- package/dist/templates/theme/meta.json +4 -0
- package/dist/templates/theme/shared/.oxlintrc.json +36 -0
- package/dist/templates/theme/shared/README.template.md +53 -0
- package/dist/templates/theme/shared/app.css +163 -0
- package/dist/templates/theme/shared/biome.json +36 -0
- package/dist/templates/theme/shared/css.d.ts +44 -0
- package/dist/templates/theme/shared/jsconfig.json +31 -0
- package/dist/templates/theme/shared/spice.config.js +9 -0
- package/dist/templates/theme/shared/spice.config.ts +9 -0
- package/dist/templates/theme/shared/tsconfig.json +32 -0
- package/dist/templates/theme/ts/react/eslint.config.ts +29 -0
- package/dist/templates/theme/ts/react/src/app.tsx +26 -0
- package/dist/templates/theme/ts/react/src/components/Onboarding.tsx +83 -0
- package/dist/templates/theme/ts/vanilla/eslint.config.ts +16 -0
- package/dist/templates/theme/ts/vanilla/src/app.ts +11 -0
- package/dist/templates/theme/ts/vanilla/src/components/Onboarding.ts +79 -0
- package/dist/templates/wrapper.js +48 -0
- package/package.json +80 -0
- package/templates/extension/js/react/eslint.config.js +29 -0
- package/templates/extension/js/react/src/app.jsx +27 -0
- package/templates/extension/js/react/src/components/Onboarding.jsx +72 -0
- package/templates/extension/js/vanilla/components/Onboarding.js +71 -0
- package/templates/extension/js/vanilla/eslint.config.js +16 -0
- package/templates/extension/js/vanilla/src/app.js +12 -0
- package/templates/extension/meta.json +4 -0
- package/templates/extension/shared/.oxlintrc.json +36 -0
- package/templates/extension/shared/README.template.md +53 -0
- package/templates/extension/shared/app.css +163 -0
- package/templates/extension/shared/biome.json +36 -0
- package/templates/extension/shared/css.d.ts +44 -0
- package/templates/extension/shared/jsconfig.json +32 -0
- package/templates/extension/shared/spice.config.js +9 -0
- package/templates/extension/shared/spice.config.ts +9 -0
- package/templates/extension/shared/tsconfig.json +32 -0
- package/templates/extension/ts/react/eslint.config.ts +29 -0
- package/templates/extension/ts/react/src/app.tsx +27 -0
- package/templates/extension/ts/react/src/components/Onboarding.tsx +83 -0
- package/templates/extension/ts/vanilla/biome.json +36 -0
- package/templates/extension/ts/vanilla/eslint.config.ts +16 -0
- package/templates/extension/ts/vanilla/src/app.ts +12 -0
- package/templates/extension/ts/vanilla/src/components/Onboarding.ts +79 -0
- package/templates/liveReload.js +70 -0
- package/templates/theme/js/react/eslint.config.js +29 -0
- package/templates/theme/js/react/src/app.jsx +25 -0
- package/templates/theme/js/react/src/components/Onboarding.jsx +72 -0
- package/templates/theme/js/vanilla/eslint.config.js +16 -0
- package/templates/theme/js/vanilla/src/app.js +11 -0
- package/templates/theme/js/vanilla/src/components/Onboarding.js +71 -0
- package/templates/theme/meta.json +4 -0
- package/templates/theme/shared/.oxlintrc.json +36 -0
- package/templates/theme/shared/README.template.md +53 -0
- package/templates/theme/shared/app.css +163 -0
- package/templates/theme/shared/biome.json +36 -0
- package/templates/theme/shared/css.d.ts +44 -0
- package/templates/theme/shared/jsconfig.json +31 -0
- package/templates/theme/shared/spice.config.js +9 -0
- package/templates/theme/shared/spice.config.ts +9 -0
- package/templates/theme/shared/tsconfig.json +32 -0
- package/templates/theme/ts/react/eslint.config.ts +29 -0
- package/templates/theme/ts/react/src/app.tsx +26 -0
- package/templates/theme/ts/react/src/components/Onboarding.tsx +83 -0
- package/templates/theme/ts/vanilla/eslint.config.ts +16 -0
- package/templates/theme/ts/vanilla/src/app.ts +11 -0
- package/templates/theme/ts/vanilla/src/components/Onboarding.ts +79 -0
- 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 { };
|