@unbound-app/cli 0.11.0 → 1.1.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.
@@ -0,0 +1,2585 @@
1
+ import { createRequire } from "node:module";
2
+ import { Type } from "typebox";
3
+ import { Value } from "typebox/value";
4
+ import { execFile } from "node:child_process";
5
+ import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
6
+ import { tmpdir } from "node:os";
7
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
8
+ import { fetchManifest, resolveHermesTargets } from "@unbound-app/rollup-plugin-hermes/manifest";
9
+ import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
10
+ import { promisify } from "node:util";
11
+ import { createHash } from "node:crypto";
12
+ import { parseSync } from "rolldown/utils";
13
+ import { gzipSync } from "node:zlib";
14
+ import { fileURLToPath } from "node:url";
15
+ import { rolldown } from "rolldown";
16
+ // \0rolldown/runtime.js
17
+ var __create = Object.create;
18
+ var __defProp = Object.defineProperty;
19
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
20
+ var __getOwnPropNames = Object.getOwnPropertyNames;
21
+ var __getProtoOf = Object.getPrototypeOf;
22
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
23
+ var __esmMin = (fn, res, err) => () => {
24
+ if (err) throw err[0];
25
+ try {
26
+ return fn && (res = fn(fn = 0)), res;
27
+ } catch (e) {
28
+ throw err = [e], e;
29
+ }
30
+ };
31
+ var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod, mod.exports), cb = null), mod.exports);
32
+ var __exportAll = (all, no_symbols) => {
33
+ let target = {};
34
+ for (var name in all) __defProp(target, name, {
35
+ get: all[name],
36
+ enumerable: true
37
+ });
38
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
39
+ return target;
40
+ };
41
+ var __copyProps = (to, from, except, desc) => {
42
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
43
+ key = keys[i];
44
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
45
+ get: ((k) => from[k]).bind(null, key),
46
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
47
+ });
48
+ }
49
+ return to;
50
+ };
51
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
52
+ value: mod,
53
+ enumerable: true
54
+ }) : target, mod));
55
+ var __toCommonJS = (mod) => __hasOwnProp.call(mod, "module.exports") ? mod["module.exports"] : __copyProps(__defProp({}, "__esModule", { value: true }), mod);
56
+ var __require = /* #__PURE__ */ (() => createRequire(import.meta.url))();
57
+ // ../types/typings/addons.ts
58
+ /** The kinds of addon, as a manifest's `type` declares them. One manager governs each, in this start order. */
59
+ const ADDON_KINDS = [
60
+ "plugin",
61
+ "theme",
62
+ "icon-pack",
63
+ "font"
64
+ ];
65
+ /**
66
+ * The globals a plugin bundle is evaluated with, in parameter order. Bytecode compiled against an earlier
67
+ * list receives them by position, so new globals are only ever appended.
68
+ */
69
+ const ADDON_SCOPE_GLOBALS = [
70
+ "React",
71
+ "unbound",
72
+ "addon",
73
+ "patcher",
74
+ "logger",
75
+ "setTimeout",
76
+ "setInterval",
77
+ "clearTimeout",
78
+ "clearInterval",
79
+ "commands"
80
+ ];
81
+ // ../contracts/src/addon-assets.ts
82
+ /** The folder, at the plugin root and beside the built bundle, that holds a plugin's images. */
83
+ const ASSETS_DIRECTORY = "assets";
84
+ /** The image types a plugin ships: the formats React Native's `Image` decodes. An SVG is a component, not an asset. */
85
+ const ASSET_TYPES = [
86
+ "png",
87
+ "jpg",
88
+ "webp",
89
+ "gif"
90
+ ];
91
+ /** The pixel densities a plugin image can ship at, as its `@2x` and `@3x` file suffixes name them. */
92
+ const ASSET_SCALES = [
93
+ "1",
94
+ "2",
95
+ "3"
96
+ ];
97
+ /** Path segments joined by `/`, each starting with a letter, digit, `_` or `-`, so no name can leave its folder. */
98
+ const ASSET_NAME_PATTERN = "^[\\w-][\\w.-]*(?:/[\\w-][\\w.-]*)*$";
99
+ const ASSET_NAME = new RegExp(ASSET_NAME_PATTERN);
100
+ /**
101
+ * @description Tells whether `value` can name a plugin asset: path segments without `..`, at most 200 characters.
102
+ * @param value The untrusted value.
103
+ * @returns Whether it is a safe asset name.
104
+ */
105
+ function isAssetName(value) {
106
+ return typeof value === "string" && value.length <= 200 && ASSET_NAME.test(value);
107
+ }
108
+ /**
109
+ * @description Names an image file beside an addon's bundle. Files are named by content, so equal images share one.
110
+ * @param main The manifest's `main`, such as `index.js`.
111
+ * @param sha256 The file's SHA-256.
112
+ * @param type The file's type.
113
+ * @returns The file's path relative to the manifest, such as `assets/<sha256>.png`.
114
+ */
115
+ function getAssetPath(main, sha256, type) {
116
+ return `${main.slice(0, main.lastIndexOf("/") + 1)}${ASSETS_DIRECTORY}/${sha256}.${type}`;
117
+ }
118
+ // ../contracts/src/addon-locales.ts
119
+ /** The Discord locale every plugin ships in its bundle, and the source of truth for its message keys. */
120
+ const SOURCE_LOCALE = "en-US";
121
+ /** The folder, beside a plugin's `src/` and beside its built bundle, that holds one JSON file per locale. */
122
+ const LOCALES_DIRECTORY = "i18n";
123
+ /** A Discord locale code, such as `de`, `en-GB` or `es-419`. It names a file, so it holds no path characters. */
124
+ const LOCALE_CODE_PATTERN = "^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})*$";
125
+ const LOCALE_CODE = new RegExp(LOCALE_CODE_PATTERN);
126
+ /**
127
+ * @description Tells whether `value` is a locale code that can name a locale file.
128
+ * @param value The untrusted value.
129
+ * @returns Whether it is a path-safe locale code.
130
+ */
131
+ function isLocaleCode(value) {
132
+ return typeof value === "string" && value.length <= 35 && LOCALE_CODE.test(value);
133
+ }
134
+ /**
135
+ * @description Names the folder of locale files beside an addon's bundle.
136
+ * @param main The manifest's `main`, such as `index.js`.
137
+ * @returns The folder's path relative to the manifest, such as `i18n`.
138
+ */
139
+ function getLocalesDirectory(main) {
140
+ return `${main.slice(0, main.lastIndexOf("/") + 1)}${LOCALES_DIRECTORY}`;
141
+ }
142
+ /**
143
+ * @description Names a locale file beside an addon's bundle, as the build emits it and the client downloads it.
144
+ * @param main The manifest's `main`, such as `index.js`.
145
+ * @param code The locale code.
146
+ * @returns The file's path relative to the manifest, such as `i18n/de.json`.
147
+ */
148
+ function getLocalePath(main, code) {
149
+ return `${getLocalesDirectory(main)}/${code}.json`;
150
+ }
151
+ // ../contracts/src/addon-files.ts
152
+ /**
153
+ * @description Lists every extra file a manifest names beside its bundle, once per path; publishing copies them and installing downloads them.
154
+ * @param manifest The manifest.
155
+ * @returns The files, each with its SHA-256: the images, then the locales.
156
+ */
157
+ function listAddonFiles(manifest) {
158
+ const files = /* @__PURE__ */ new Map();
159
+ for (const [name, asset] of Object.entries(manifest.assets ?? {})) for (const [scale, sha256] of Object.entries(asset.scales)) {
160
+ if (!sha256) continue;
161
+ const path = getAssetPath(manifest.main, sha256, asset.type);
162
+ const label = `asset ${name}${scale === "1" ? "" : `@${scale}x`}`;
163
+ if (!files.has(path)) files.set(path, {
164
+ kind: "asset",
165
+ path,
166
+ sha256,
167
+ label
168
+ });
169
+ }
170
+ for (const [code, sha256] of Object.entries(manifest.locales ?? {})) {
171
+ const path = getLocalePath(manifest.main, code);
172
+ files.set(path, {
173
+ kind: "locale",
174
+ path,
175
+ sha256,
176
+ label: `locale ${code}`
177
+ });
178
+ }
179
+ return [...files.values()];
180
+ }
181
+ // src/rollup/hermes-bytecode.ts
182
+ const run = promisify(execFile);
183
+ const FALL_BACK_TO_GLOBAL = /* @__PURE__ */ new Set([
184
+ "unbound",
185
+ "setTimeout",
186
+ "setInterval",
187
+ "clearTimeout",
188
+ "clearInterval"
189
+ ]);
190
+ const parameters = ADDON_SCOPE_GLOBALS.map((name) => FALL_BACK_TO_GLOBAL.has(name) ? `${name} = globalThis.${name}` : name).join(", ");
191
+ /**
192
+ * @description Wraps a plugin bundle the way the client evaluates its source, so the compiled program's completion value is the same factory.
193
+ * @param code The plugin's final bundle expression.
194
+ * @returns The wrapped source.
195
+ */
196
+ function bytecodeSource(code) {
197
+ return `((${parameters}) => { return ${code} })`;
198
+ }
199
+ /**
200
+ * @description Names the compiled bundle beside a source bundle, matching the loader's lookup.
201
+ * @param fileName The source bundle's file name, such as `index.js`.
202
+ * @param version The Hermes bytecode version.
203
+ * @returns The bytecode file name, such as `index.98.hbc`.
204
+ */
205
+ function bytecodeFileName(fileName, version) {
206
+ return `${fileName.replace(/\.[^.]+$/, "")}.${version}.hbc`;
207
+ }
208
+ function firstError(error) {
209
+ const output = (error instanceof Error && "stderr" in error && typeof error.stderr === "string" ? error.stderr : void 0) ?? (error instanceof Error ? error.message : String(error));
210
+ return output.split("\n").find((line) => line.includes("error:"))?.trim() ?? output.trim();
211
+ }
212
+ async function compile(hermesc, directory, source, output) {
213
+ await run(hermesc, [
214
+ "--emit-binary",
215
+ "-O",
216
+ "-g1",
217
+ "-Wno-direct-eval",
218
+ "-Wno-undefined-variable",
219
+ "--out",
220
+ output,
221
+ source
222
+ ], { cwd: directory });
223
+ return readFile(output);
224
+ }
225
+ /**
226
+ * @description Counts how many of hermesc's newest versions to resolve so every wanted version is among them.
227
+ * @param latest hermesc's versions, newest first.
228
+ * @param wanted The bytecode versions to compile for.
229
+ * @returns The number of newest versions to resolve.
230
+ */
231
+ function hermescCount(latest, wanted) {
232
+ const missing = wanted.filter((version) => !latest.includes(version));
233
+ if (missing.length) throw new Error(`hermesc does not publish bytecode ${missing.join(", ")}.`);
234
+ return Math.max(...wanted.map((version) => latest.indexOf(version)), 0) + 1;
235
+ }
236
+ /**
237
+ * @description Compiles each plugin bundle to Hermes bytecode for exactly the bytecode versions the client ships.
238
+ * @param options The bytecode versions to compile for, and the name the bytecode's source locations use.
239
+ * @returns The rollup plugin.
240
+ */
241
+ function hermesBytecode({ versions, sourceName }) {
242
+ let targets;
243
+ return {
244
+ name: "hermes-bytecode",
245
+ async generateBundle(_outputOptions, bundle) {
246
+ targets ??= (await resolveHermesTargets(this, { versions: hermescCount((await fetchManifest(this)).latest, versions) })).filter((target) => versions.includes(target.version));
247
+ const executable = process.platform === "win32" ? "hermesc.exe" : "hermesc";
248
+ const directory = await mkdtemp(join(tmpdir(), "plugin-bytecode-"));
249
+ try {
250
+ for (const chunk of Object.values(bundle)) {
251
+ if (chunk.type !== "chunk") continue;
252
+ const source = `unbound-plugin/${sourceName}.js`;
253
+ await mkdir(join(directory, "unbound-plugin"), { recursive: true });
254
+ await writeFile(join(directory, source), bytecodeSource(chunk.code));
255
+ for (const { version, dir } of targets) {
256
+ if (!dir) {
257
+ this.warn(`hermesc for bytecode version ${version} is unavailable, skipping it.`);
258
+ continue;
259
+ }
260
+ const compiled = await compile(resolve(dir, process.platform, executable), directory, source, join(directory, `${version}.hbc`)).catch((error) => {
261
+ this.warn(`hermesc ${version} could not compile ${chunk.fileName}, so that runtime loads its source: ${firstError(error)}`);
262
+ });
263
+ if (compiled) this.emitFile({
264
+ type: "asset",
265
+ fileName: bytecodeFileName(chunk.fileName, version),
266
+ source: compiled
267
+ });
268
+ }
269
+ }
270
+ } finally {
271
+ await rm(directory, {
272
+ recursive: true,
273
+ force: true
274
+ });
275
+ }
276
+ }
277
+ };
278
+ }
279
+ // src/rollup/diagnostic.ts
280
+ const SEVERITIES = ["error", "warning"];
281
+ /**
282
+ * @description Builds a diagnostic, placing it on the whole file when it has no line.
283
+ * @param init The diagnostic's fields.
284
+ * @returns The diagnostic.
285
+ */
286
+ function diagnostic({ line = 0, column = 0, ...rest }) {
287
+ return {
288
+ ...rest,
289
+ line,
290
+ column
291
+ };
292
+ }
293
+ /**
294
+ * @description Tells whether a value is a diagnostic, such as one a rollup plugin carried in a log's `meta`.
295
+ * @param value The value to test.
296
+ * @returns Whether it has every field a diagnostic needs.
297
+ */
298
+ function isDiagnostic(value) {
299
+ if (!value || typeof value !== "object") return false;
300
+ return "file" in value && typeof value.file === "string" && "line" in value && typeof value.line === "number" && "column" in value && typeof value.column === "number" && "message" in value && typeof value.message === "string" && "severity" in value && typeof value.severity === "string" && SEVERITIES.includes(value.severity);
301
+ }
302
+ /**
303
+ * @description Drops a message's closing full stop, so a suffix such as a rule name can follow it.
304
+ * @param text The message.
305
+ * @returns The message without its closing full stop.
306
+ */
307
+ function withoutFullStop(text) {
308
+ const trimmed = text.trim();
309
+ return trimmed.endsWith(".") ? trimmed.slice(0, -1) : trimmed;
310
+ }
311
+ function sentence(text) {
312
+ return `${withoutFullStop(text)}.`;
313
+ }
314
+ /**
315
+ * @description Formats a diagnostic as `file:L:C <path> <problem>. Fix: <fix>.`, the line every
316
+ * check, `ubd build` and `ubd check` print.
317
+ * @param diagnostic The diagnostic.
318
+ * @param file The file as the reader should see it; defaults to the diagnostic's own.
319
+ * @returns The line.
320
+ */
321
+ function formatDiagnostic(diagnostic, file = diagnostic.file) {
322
+ const parts = [
323
+ diagnostic.line ? `${file}:${diagnostic.line}:${diagnostic.column}` : file,
324
+ diagnostic.path,
325
+ sentence(diagnostic.message)
326
+ ];
327
+ if (diagnostic.fix) parts.push(`Fix: ${sentence(diagnostic.fix)}`);
328
+ return parts.filter(Boolean).join(" ");
329
+ }
330
+ /**
331
+ * @description Orders diagnostics by file, then errors before warnings, then by position.
332
+ * @param diagnostics The diagnostics.
333
+ * @returns A sorted copy.
334
+ */
335
+ function sortDiagnostics(diagnostics) {
336
+ return diagnostics.toSorted((a, b) => a.file.localeCompare(b.file) || SEVERITIES.indexOf(a.severity) - SEVERITIES.indexOf(b.severity) || a.line - b.line || a.column - b.column || a.message.localeCompare(b.message));
337
+ }
338
+ /**
339
+ * @description Reports a diagnostic from a rollup plugin hook: an error stops the build, a warning
340
+ * does not. Rollup prints the formatted line, and `ubd check` reads the diagnostic back from the
341
+ * log's `meta`, so both show the same problem the same way.
342
+ * @param context The hook's `this`.
343
+ * @param found The diagnostic, with its file relative to the plugin's directory.
344
+ */
345
+ function reportDiagnostic(context, found) {
346
+ const log = {
347
+ message: formatDiagnostic(found),
348
+ meta: { diagnostic: found }
349
+ };
350
+ if (found.severity === "error") context.error(log);
351
+ context.warn(log);
352
+ }
353
+ /**
354
+ * @description Builds a preset plugin's diagnostic under its `ubd check` id, with the file and any absolute
355
+ * path in the message made relative to the plugin, so `--only <check>` selects it.
356
+ * @param check The check id, one of `BUNDLE_REPORTED_CHECKS`.
357
+ * @param pluginRoot The plugin's directory.
358
+ * @param problem The problem.
359
+ * @returns The diagnostic.
360
+ */
361
+ function pluginDiagnostic(check, pluginRoot, problem) {
362
+ const file = isAbsolute(problem.file) ? relative(pluginRoot, problem.file) : problem.file;
363
+ return diagnostic({
364
+ ...problem,
365
+ file: file.split(sep).join("/"),
366
+ message: problem.message.replaceAll(pluginRoot + sep, ""),
367
+ check
368
+ });
369
+ }
370
+ /**
371
+ * @description Reports a preset plugin's problem from a rollup hook, as {@link pluginDiagnostic} builds it.
372
+ * @param context The hook's `this`.
373
+ * @param check The check id, one of `BUNDLE_REPORTED_CHECKS`.
374
+ * @param pluginRoot The plugin's directory.
375
+ * @param problem The problem.
376
+ */
377
+ function reportProblem(context, check, pluginRoot, problem) {
378
+ reportDiagnostic(context, pluginDiagnostic(check, pluginRoot, problem));
379
+ }
380
+ /**
381
+ * @description Reports several diagnostics at once: each warning on its own, then every error in one failure,
382
+ * whose `meta.diagnostics` `ubd check` reads back, so a build names all its errors rather than the first.
383
+ * @param context The hook's `this`.
384
+ * @param found The diagnostics.
385
+ */
386
+ function reportDiagnostics(context, found) {
387
+ const errors = found.filter((item) => item.severity === "error");
388
+ for (const item of found) if (item.severity === "warning") reportDiagnostic(context, item);
389
+ if (errors.length === 1 && errors[0]) reportDiagnostic(context, errors[0]);
390
+ if (errors.length > 1) context.error({
391
+ message: errors.map((item) => formatDiagnostic(item)).join("\n"),
392
+ meta: { diagnostics: errors }
393
+ });
394
+ }
395
+ // src/rollup/source-alias.ts
396
+ const extensions = [
397
+ ".tsx",
398
+ ".ts",
399
+ ".jsx",
400
+ ".js",
401
+ ".mjs",
402
+ ".json"
403
+ ];
404
+ const indexFiles = [
405
+ "index.tsx",
406
+ "index.ts",
407
+ "index.jsx",
408
+ "index.js"
409
+ ];
410
+ function isFile(path) {
411
+ return existsSync(path) && statSync(path).isFile();
412
+ }
413
+ /**
414
+ * @description Resolves a module path the way a bundler would, trying source extensions and then index files.
415
+ * @param candidate An absolute path, with or without its extension.
416
+ * @returns The file it resolves to, or `null` when none exists.
417
+ */
418
+ function findFile(candidate) {
419
+ if (isFile(candidate)) return candidate;
420
+ for (const extension of extensions) if (isFile(`${candidate}${extension}`)) return `${candidate}${extension}`;
421
+ for (const indexFile of indexFiles) if (isFile(resolve(candidate, indexFile))) return resolve(candidate, indexFile);
422
+ return null;
423
+ }
424
+ /**
425
+ * @description Resolves `~/` imports to a plugin's `src` directory, mirroring the `paths` in its tsconfig.
426
+ * @param sourceRoot Absolute path of the plugin's `src` directory.
427
+ * @returns The rollup plugin.
428
+ */
429
+ function sourceAlias(sourceRoot) {
430
+ return {
431
+ name: "plugin-source-alias",
432
+ resolveId(source) {
433
+ if (!source.startsWith("~/")) return null;
434
+ return findFile(resolve(sourceRoot, source.slice(2)));
435
+ }
436
+ };
437
+ }
438
+ // ../api/testing/icu-parse.ts
439
+ /** A syntax error in an ICU message; `position` is the 1-based character it stopped at. */
440
+ var IcuSyntaxError = class extends Error {
441
+ position;
442
+ constructor(reason, position) {
443
+ super(`${reason} at character ${position}`);
444
+ this.position = position;
445
+ }
446
+ };
447
+ function isSpace(char) {
448
+ return char !== "" && char.trim() === "";
449
+ }
450
+ function fail(reader, reason) {
451
+ throw new IcuSyntaxError(reason, reader.index + 1);
452
+ }
453
+ function peek(reader) {
454
+ return reader.text[reader.index] ?? "";
455
+ }
456
+ function skipSpace(reader) {
457
+ while (isSpace(peek(reader))) reader.index++;
458
+ }
459
+ function readWord(reader) {
460
+ skipSpace(reader);
461
+ const start = reader.index;
462
+ while (reader.index < reader.text.length) {
463
+ const char = peek(reader);
464
+ if (isSpace(char) || char === "," || char === "{" || char === "}") break;
465
+ reader.index++;
466
+ }
467
+ return reader.text.slice(start, reader.index);
468
+ }
469
+ function expect(reader, char) {
470
+ skipSpace(reader);
471
+ if (peek(reader) !== char) fail(reader, `Expected "${char}"`);
472
+ reader.index++;
473
+ }
474
+ function readQuoted(reader, inPlural) {
475
+ const next = reader.text[reader.index + 1] ?? "";
476
+ if (next === "'") {
477
+ reader.index += 2;
478
+ return "'";
479
+ }
480
+ if (next !== "{" && next !== "}" && !(inPlural && next === "#")) {
481
+ reader.index++;
482
+ return "'";
483
+ }
484
+ let quoted = "";
485
+ reader.index++;
486
+ while (reader.index < reader.text.length) {
487
+ const char = peek(reader);
488
+ if (char === "'" && reader.text[reader.index + 1] === "'") {
489
+ quoted += "'";
490
+ reader.index += 2;
491
+ } else if (char === "'") {
492
+ reader.index++;
493
+ return quoted;
494
+ } else {
495
+ quoted += char;
496
+ reader.index++;
497
+ }
498
+ }
499
+ return quoted;
500
+ }
501
+ function readMessage(reader, nested, inPlural) {
502
+ const nodes = [];
503
+ let text = "";
504
+ function flush() {
505
+ if (text) nodes.push({
506
+ kind: "text",
507
+ text
508
+ });
509
+ text = "";
510
+ }
511
+ while (reader.index < reader.text.length) {
512
+ const char = peek(reader);
513
+ if (char === "'") text += readQuoted(reader, inPlural);
514
+ else if (char === "{") {
515
+ flush();
516
+ reader.index++;
517
+ nodes.push(readArgument(reader));
518
+ } else if (char === "}") {
519
+ if (!nested) fail(reader, "Unmatched \"}\"");
520
+ flush();
521
+ return nodes;
522
+ } else if (char === "#" && inPlural) {
523
+ flush();
524
+ nodes.push({ kind: "pound" });
525
+ reader.index++;
526
+ } else {
527
+ text += char;
528
+ reader.index++;
529
+ }
530
+ }
531
+ if (nested) fail(reader, "Unclosed \"{\"");
532
+ flush();
533
+ return nodes;
534
+ }
535
+ function readBranches(reader, inPlural) {
536
+ const branches = /* @__PURE__ */ new Map();
537
+ let offset = 0;
538
+ skipSpace(reader);
539
+ while (reader.index < reader.text.length && peek(reader) !== "}") {
540
+ const selector = readWord(reader);
541
+ if (!selector) fail(reader, "Expected a selector");
542
+ if (selector.startsWith("offset:")) offset = Number(selector.slice(7));
543
+ else {
544
+ expect(reader, "{");
545
+ branches.set(selector, readMessage(reader, true, inPlural));
546
+ expect(reader, "}");
547
+ }
548
+ skipSpace(reader);
549
+ }
550
+ return {
551
+ branches,
552
+ offset
553
+ };
554
+ }
555
+ function readStyle(reader) {
556
+ const start = reader.index;
557
+ let depth = 0;
558
+ while (reader.index < reader.text.length) {
559
+ const char = peek(reader);
560
+ if (char === "}" && depth === 0) return reader.text.slice(start, reader.index).trim();
561
+ if (char === "{") depth++;
562
+ if (char === "}") depth--;
563
+ reader.index++;
564
+ }
565
+ return fail(reader, "Unclosed argument");
566
+ }
567
+ function readArgument(reader) {
568
+ const name = readWord(reader);
569
+ if (!name) fail(reader, "Expected an argument name");
570
+ skipSpace(reader);
571
+ if (peek(reader) === "}") {
572
+ reader.index++;
573
+ return {
574
+ kind: "argument",
575
+ name,
576
+ type: "",
577
+ style: ""
578
+ };
579
+ }
580
+ expect(reader, ",");
581
+ const type = readWord(reader);
582
+ let node;
583
+ skipSpace(reader);
584
+ if (type === "plural" || type === "selectordinal") {
585
+ expect(reader, ",");
586
+ const { branches, offset } = readBranches(reader, true);
587
+ node = {
588
+ kind: "plural",
589
+ name,
590
+ ordinal: type === "selectordinal",
591
+ offset,
592
+ branches
593
+ };
594
+ } else if (type === "select") {
595
+ expect(reader, ",");
596
+ node = {
597
+ kind: "select",
598
+ name,
599
+ branches: readBranches(reader, false).branches
600
+ };
601
+ } else {
602
+ if (peek(reader) === ",") reader.index++;
603
+ node = {
604
+ kind: "argument",
605
+ name,
606
+ type,
607
+ style: readStyle(reader)
608
+ };
609
+ }
610
+ expect(reader, "}");
611
+ return node;
612
+ }
613
+ /**
614
+ * @description Parses an ICU MessageFormat string: arguments, `number`/`date`/`time`, `plural`, `selectordinal`,
615
+ * `select`, `#`, `offset:` and apostrophe quoting. The CLI's i18n typings and checks and the testing kit share it.
616
+ * @param message The message, such as `{count, plural, one {# file} other {# files}}`.
617
+ * @returns The message's nodes; throws an `IcuSyntaxError` naming the position of a syntax error.
618
+ */
619
+ function parseIcu(message) {
620
+ return readMessage({
621
+ text: message,
622
+ index: 0
623
+ }, false, false);
624
+ }
625
+ // src/rollup/icu-arguments.ts
626
+ const NUMERIC_TYPES = /* @__PURE__ */ new Set([
627
+ "number",
628
+ "spellout",
629
+ "ordinal",
630
+ "duration"
631
+ ]);
632
+ const DATE_TYPES = /* @__PURE__ */ new Set(["date", "time"]);
633
+ function merge(left, right) {
634
+ if (!left || left.kind === "any") return right;
635
+ if (right.kind === "any") return left;
636
+ if (left.kind === "select" && right.kind === "select") return {
637
+ kind: "select",
638
+ options: [.../* @__PURE__ */ new Set([...left.options, ...right.options])],
639
+ other: left.other || right.other
640
+ };
641
+ return left.kind === right.kind ? left : { kind: "any" };
642
+ }
643
+ function record(found, name, argument) {
644
+ found.set(name, merge(found.get(name), argument));
645
+ }
646
+ function argumentOf(node) {
647
+ if (NUMERIC_TYPES.has(node.type)) return { kind: "number" };
648
+ return DATE_TYPES.has(node.type) ? { kind: "date" } : { kind: "any" };
649
+ }
650
+ function collect(nodes, found, withoutOther) {
651
+ for (const node of nodes) {
652
+ if (node.kind === "text" || node.kind === "pound") continue;
653
+ if (node.kind === "argument") {
654
+ record(found, node.name, argumentOf(node));
655
+ continue;
656
+ }
657
+ const selectors = [...node.branches.keys()];
658
+ for (const branch of node.branches.values()) collect(branch, found, withoutOther);
659
+ if (!node.branches.has("other")) withoutOther.push(node.name);
660
+ record(found, node.name, node.kind === "select" ? {
661
+ kind: "select",
662
+ options: selectors.filter((selector) => selector !== "other"),
663
+ other: node.branches.has("other")
664
+ } : { kind: "number" });
665
+ }
666
+ }
667
+ /**
668
+ * @description Lists the arguments an ICU MessageFormat string reads, nested branches included.
669
+ * @param message The message, such as `{count, plural, one {# file} other {# files}}`.
670
+ * @returns Each argument by name; throws naming the position of a syntax error.
671
+ */
672
+ function readIcuArguments(message) {
673
+ return readIcuMessage(message).arguments;
674
+ }
675
+ /**
676
+ * @description Reads an ICU MessageFormat string: the arguments it reads, and the branching ones that lack `other`.
677
+ * @param message The message, such as `{count, plural, one {# file} other {# files}}`.
678
+ * @returns Each argument by name, and the names of plural, selectordinal and select arguments without `other`.
679
+ */
680
+ function readIcuMessage(message) {
681
+ const found = /* @__PURE__ */ new Map();
682
+ const withoutOther = [];
683
+ collect(parseIcu(message), found, withoutOther);
684
+ return {
685
+ arguments: found,
686
+ withoutOther
687
+ };
688
+ }
689
+ /**
690
+ * @description Spells a string as a single-quoted TypeScript literal, as the repo's formatter writes it.
691
+ * @param value The string.
692
+ * @returns The literal, such as `'male'`.
693
+ */
694
+ function typeLiteral(value) {
695
+ return `'${value.replaceAll("\\", "\\\\").replaceAll("'", "\\'")}'`;
696
+ }
697
+ /**
698
+ * @description Spells a property key the way the formatter leaves it: bare when it is an identifier, quoted otherwise.
699
+ * @param key The key.
700
+ * @returns The key as written in a type literal.
701
+ */
702
+ function typeKey(key) {
703
+ return /^[A-Za-z_$][\w$]*$/.test(key) ? key : typeLiteral(key);
704
+ }
705
+ /**
706
+ * @description Spells the TypeScript type `format` accepts for one argument.
707
+ * @param argument The argument.
708
+ * @returns The type, such as `number` or `'male' | 'female'`.
709
+ */
710
+ function icuArgumentType(argument) {
711
+ if (argument.kind === "number") return "number";
712
+ if (argument.kind === "date") return "Date | number";
713
+ if (argument.kind === "any" || argument.options.length === 0) return "string | number";
714
+ return [...argument.options.map((option) => typeLiteral(option)), ...argument.other ? ["(string & {})"] : []].join(" | ");
715
+ }
716
+ // src/rollup/build-report-schema.ts
717
+ const count = Type.Integer({ minimum: 0 });
718
+ const counts = Type.Record(Type.String(), count, { additionalProperties: false });
719
+ const reportSectionSchema = Type.Union([
720
+ Type.Literal("inlined"),
721
+ Type.Literal("shipped"),
722
+ Type.Literal("metro")
723
+ ]);
724
+ /** What one feature plugin adds to the build report. Entries with the same section and label merge into one line. */
725
+ const reportEntrySchema = Type.Object({
726
+ section: reportSectionSchema,
727
+ label: Type.String(),
728
+ /** Show how many entries merged, as `(3 <unit>s)`, or `(3)` for `''`. */
729
+ unit: Type.Optional(Type.String()),
730
+ /** Bytes this entry adds; merged entries sum them. */
731
+ bytes: Type.Optional(count),
732
+ /** Counts this entry under a kind, such as `keyed` or `uncached`. */
733
+ tally: Type.Optional(Type.String()),
734
+ /** Where the entry comes from, such as `src/index.tsx:12`. */
735
+ location: Type.Optional(Type.String()),
736
+ /** Names this entry in the `ubd dev` summary, such as `uncached filter`. */
737
+ issue: Type.Optional(Type.String())
738
+ });
739
+ /** One line of a report section: every contribution with the same section and label, merged. */
740
+ const reportGroupSchema = Type.Object({
741
+ label: Type.String(),
742
+ /** When set, the line shows how many contributions it merged, as `(3 <unit>s)`, or `(3)` for `''`. */
743
+ unit: Type.Optional(Type.String()),
744
+ items: count,
745
+ bytes: count,
746
+ /** Contributions counted by kind, such as `{ keyed: 9, uncached: 1 }`. */
747
+ tallies: counts,
748
+ /** Where each located contribution came from, such as `src/index.tsx:12`. */
749
+ locations: Type.Array(Type.String()),
750
+ /** Contributions `ubd dev` names in its one-line summary, counted by noun, such as `uncached filter`. */
751
+ issues: counts
752
+ });
753
+ const budgetNameSchema = Type.Union([Type.Literal("bundle"), Type.Literal("assets")]);
754
+ const reportWarningSchema = Type.Union([Type.Object({
755
+ kind: Type.Literal("budget"),
756
+ budget: budgetNameSchema,
757
+ bytes: count,
758
+ limit: count
759
+ }), Type.Object({
760
+ kind: Type.Literal("shim"),
761
+ library: Type.String(),
762
+ modules: Type.Array(Type.String())
763
+ })]);
764
+ /** What `ubd build` reports for one plugin, written by the preset's `build-report` plugin. */
765
+ const buildReportSchema = Type.Object({
766
+ name: Type.String(),
767
+ version: Type.String(),
768
+ bundle: Type.Object({
769
+ file: Type.String(),
770
+ bytes: count,
771
+ gzip: count,
772
+ bytecode: Type.Array(Type.Object({
773
+ version: Type.Integer(),
774
+ bytes: count
775
+ }))
776
+ }),
777
+ inlined: Type.Array(reportGroupSchema),
778
+ shipped: Type.Array(reportGroupSchema),
779
+ metro: Type.Array(reportGroupSchema),
780
+ /** The largest source modules by rendered size, before minification. */
781
+ modules: Type.Array(Type.Object({
782
+ id: Type.String(),
783
+ bytes: count
784
+ })),
785
+ /** Each budget's limit in bytes. */
786
+ budgets: Type.Object({
787
+ bundle: count,
788
+ assets: count
789
+ }),
790
+ warnings: Type.Array(reportWarningSchema)
791
+ });
792
+ // src/rollup/report-line.ts
793
+ /** Report entries ride the bundle as assets under this folder; the `build-report` plugin removes them before the write. */
794
+ const REPORT_ENTRY_FOLDER = ".unbound-report/";
795
+ let emitted = 0;
796
+ /**
797
+ * @description Wraps a report entry as an asset to pass to `this.emitFile`, from any hook up to `generateBundle`.
798
+ * @param entry The line to add.
799
+ * @returns The asset carrying the entry.
800
+ * @example this.emitFile(reportLine({ section: 'inlined', label: 'svg', unit: '', bytes: xml.length }));
801
+ */
802
+ function reportLine(entry) {
803
+ emitted += 1;
804
+ return {
805
+ type: "asset",
806
+ fileName: `${REPORT_ENTRY_FOLDER}${emitted}.json`,
807
+ source: JSON.stringify(entry)
808
+ };
809
+ }
810
+ /**
811
+ * @description Removes every report entry from the bundle, so none is written to `dist`.
812
+ * @param bundle The output bundle, in `generateBundle`.
813
+ * @param warn Reports an entry that is not a valid {@link ReportEntry}, which is dropped.
814
+ * @returns The entries, in emit order.
815
+ */
816
+ function takeReportEntries(bundle, warn) {
817
+ const entries = [];
818
+ for (const [fileName, file] of Object.entries(bundle)) {
819
+ if (!fileName.startsWith(".unbound-report/") || file.type !== "asset") continue;
820
+ const entry = JSON.parse(String(file.source));
821
+ delete bundle[fileName];
822
+ if (Value.Check(reportEntrySchema, entry)) entries.push(entry);
823
+ else warn(`Failed to read the build report entry ${fileName}: it is not a reportLine entry.`);
824
+ }
825
+ return entries;
826
+ }
827
+ /**
828
+ * @description Merges entries with the same section and label into one report line each, in first-seen order.
829
+ * @param entries The entries.
830
+ * @param section The section to merge.
831
+ * @returns The section's lines.
832
+ */
833
+ function mergeEntries(entries, section) {
834
+ const groups = /* @__PURE__ */ new Map();
835
+ for (const entry of entries) {
836
+ if (entry.section !== section) continue;
837
+ const group = groups.get(entry.label) ?? {
838
+ label: entry.label,
839
+ items: 0,
840
+ bytes: 0,
841
+ tallies: {},
842
+ locations: [],
843
+ issues: {}
844
+ };
845
+ group.items += 1;
846
+ group.bytes += entry.bytes ?? 0;
847
+ group.unit ??= entry.unit;
848
+ if (entry.tally) group.tallies[entry.tally] = (group.tallies[entry.tally] ?? 0) + 1;
849
+ if (entry.location) group.locations.push(entry.location);
850
+ if (entry.issue) group.issues[entry.issue] = (group.issues[entry.issue] ?? 0) + 1;
851
+ groups.set(entry.label, group);
852
+ }
853
+ return [...groups.values()];
854
+ }
855
+ // src/rollup/locales.ts
856
+ /** The bare specifier a plugin imports its own messages from; the build resolves it to a generated module. */
857
+ const LOCALE_MODULE = "@unbound-app/i18n";
858
+ /**
859
+ * Where `ubd build` and `ubd dev` write the ambient typings of {@link LOCALE_MODULE}, relative to the plugin.
860
+ * It sits in `src/`, which every plugin tsconfig includes, so the editor finds it with no setup.
861
+ */
862
+ const LOCALE_TYPINGS = "src/unbound-i18n.d.ts";
863
+ const VIRTUAL_ID = "\0unbound-locales";
864
+ const CHECK = "locales";
865
+ /**
866
+ * @description Names the en-US file whose presence turns a plugin's locale support on.
867
+ * @param pluginRoot The plugin's directory.
868
+ * @returns The absolute path of `i18n/en-US.json`.
869
+ */
870
+ function sourceLocalePath(pluginRoot) {
871
+ return join(pluginRoot, LOCALES_DIRECTORY, `${SOURCE_LOCALE}.json`);
872
+ }
873
+ /**
874
+ * @description Reads one locale file as a table of message keys to strings.
875
+ * @param path The file's absolute path.
876
+ * @returns The table; throws naming the file when it is not valid JSON or not an object of strings.
877
+ */
878
+ function readTable(path) {
879
+ let value;
880
+ try {
881
+ value = JSON.parse(readFileSync(path, "utf8"));
882
+ } catch (error) {
883
+ throw new Error(`${path} is not valid JSON: ${error.message}`);
884
+ }
885
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${path} must be a JSON object of message keys to strings.`);
886
+ const table = {};
887
+ for (const [key, message] of Object.entries(value)) {
888
+ if (typeof message !== "string") throw new Error(`${path}: ${key} must be a string, not ${typeof message}.`);
889
+ table[key] = message;
890
+ }
891
+ return table;
892
+ }
893
+ function argumentsOf(path, key, message) {
894
+ try {
895
+ return readIcuArguments(message);
896
+ } catch (error) {
897
+ throw new Error(`${path}: ${key} is not valid ICU MessageFormat: ${error.message}.`);
898
+ }
899
+ }
900
+ function sameNames(left, right) {
901
+ return left.size === right.size && [...left.keys()].every((name) => right.has(name));
902
+ }
903
+ function names(found) {
904
+ return found.size ? [...found.keys()].map((name) => `{${name}}`).join(", ") : "none";
905
+ }
906
+ function readTranslation(path, code, source, warnings) {
907
+ const table = readTable(path);
908
+ const kept = {};
909
+ for (const [key, message] of Object.entries(table)) {
910
+ const expected = source[key];
911
+ if (expected === void 0) {
912
+ warnings.push({
913
+ file: `${LOCALES_DIRECTORY}/${code}.json`,
914
+ path: key,
915
+ message: `is not in ${SOURCE_LOCALE}.json, so it is left out`,
916
+ fix: `add it to ${SOURCE_LOCALE}.json, or remove it (ubd i18n --prune)`
917
+ });
918
+ continue;
919
+ }
920
+ const found = argumentsOf(path, key, message);
921
+ const wanted = argumentsOf(path, key, expected);
922
+ if (!sameNames(found, wanted)) warnings.push({
923
+ file: `${LOCALES_DIRECTORY}/${code}.json`,
924
+ path: key,
925
+ message: `has placeholders ${names(found)}, but ${SOURCE_LOCALE}.json has ${names(wanted)}`,
926
+ fix: `use the placeholders ${SOURCE_LOCALE}.json uses`
927
+ });
928
+ kept[key] = message;
929
+ }
930
+ const text = JSON.stringify(kept);
931
+ return {
932
+ text,
933
+ digest: createHash("sha256").update(text).digest("hex")
934
+ };
935
+ }
936
+ /**
937
+ * @description Reads and validates a plugin's `i18n/` folder: every file must be a JSON object of strings in
938
+ * valid ICU syntax, and en-US.json is required. Keys en-US lacks, and placeholders it disagrees on, are warnings.
939
+ * @param pluginRoot The plugin's directory.
940
+ * @returns The folder, or `undefined` when the plugin has none; throws naming the file that is invalid.
941
+ */
942
+ function readLocaleFolder(pluginRoot) {
943
+ const directory = join(pluginRoot, LOCALES_DIRECTORY);
944
+ if (!existsSync(directory)) return;
945
+ const files = readdirSync(directory).filter((name) => name.endsWith(".json"));
946
+ if (files.length === 0) return;
947
+ const sourcePath = sourceLocalePath(pluginRoot);
948
+ if (!existsSync(sourcePath)) throw new Error(`${directory} has translations but no ${SOURCE_LOCALE}.json, which every plugin must ship.`);
949
+ const source = readTable(sourcePath);
950
+ const translations = /* @__PURE__ */ new Map();
951
+ const warnings = [];
952
+ for (const [key, message] of Object.entries(source)) argumentsOf(sourcePath, key, message);
953
+ for (const file of files.sort()) {
954
+ const code = file.slice(0, -5);
955
+ if (code === "en-US") continue;
956
+ if (!isLocaleCode(code)) throw new Error(`${join(directory, file)} is not named after a Discord locale code, such as de.json or pt-BR.json.`);
957
+ translations.set(code, readTranslation(join(directory, file), code, source, warnings));
958
+ }
959
+ return {
960
+ source,
961
+ translations,
962
+ warnings
963
+ };
964
+ }
965
+ function comment$1(message) {
966
+ return message.replaceAll("*/", "*\\/").replaceAll("\n", " ");
967
+ }
968
+ /**
969
+ * @description Writes the typings of {@link LOCALE_MODULE} from en-US, as an ambient module declaration:
970
+ * `messages` has its keys, and `format` takes each key's placeholders, typed from their ICU argument types.
971
+ * @param source The en-US messages.
972
+ * @returns The `.d.ts` source.
973
+ */
974
+ function localeTypings(source) {
975
+ const messages = [];
976
+ const vars = [];
977
+ for (const [key, message] of Object.entries(source)) {
978
+ const name = typeKey(key);
979
+ const found = readIcuArguments(message);
980
+ messages.push(`\t\t/** ${comment$1(message)} */\n\t\treadonly ${name}: string;`);
981
+ if (found.size) {
982
+ const fields = [...found].map(([argument, type]) => `${typeKey(argument)}: ${icuArgumentType(type)}`);
983
+ vars.push(`\t\t${name}: { ${fields.join("; ")} };`);
984
+ }
985
+ }
986
+ return [
987
+ "// Generated by ubd from i18n/en-US.json. Do not edit: `ubd build` and `ubd dev` rewrite it.",
988
+ "",
989
+ `declare module '${LOCALE_MODULE}' {`,
990
+ " /** Every en-US message, read in the user's locale. */",
991
+ `\texport type Messages = {\n${messages.join("\n")}\n\t};`,
992
+ "",
993
+ " /** The placeholder values each message that has placeholders takes. */",
994
+ vars.length ? `\texport type FormatVars = {\n${vars.join("\n")}\n\t};` : " export type FormatVars = Record<never, never>;",
995
+ "",
996
+ " /** Each message in the user's locale, falling back to en-US. */",
997
+ " export const messages: Messages;",
998
+ " /** Formats a message that has no placeholders. */",
999
+ " export function format(key: Exclude<keyof Messages, keyof FormatVars>): string;",
1000
+ " /** Formats a message, filling its placeholders from `vars`. */",
1001
+ " export function format<K extends keyof FormatVars>(key: K, vars: FormatVars[K]): string;",
1002
+ " /**",
1003
+ " * Re-renders the component when the user's locale changes or its translation loads, and returns the locale code.",
1004
+ " * The build adds it to components and hooks that read `messages` or `format`.",
1005
+ " */",
1006
+ " export function useMessages(): string;",
1007
+ "",
1008
+ " /** Every key of the plugin's en-US.json. */",
1009
+ " export type MessageKey = keyof Messages;",
1010
+ "",
1011
+ " global {",
1012
+ " /** Gives the plugin's commands, toasts and dialogs its message keys. */",
1013
+ " // oxlint-disable-next-line typescript/consistent-type-definitions -- merging needs an interface.",
1014
+ " interface UnboundMessageKeys extends Record<MessageKey, true> {}",
1015
+ " }",
1016
+ "}",
1017
+ ""
1018
+ ].join("\n");
1019
+ }
1020
+ /**
1021
+ * @description Rewrites `src/unbound-i18n.d.ts` from the plugin's en-US file, leaving the file alone when it is current.
1022
+ * @param pluginRoot The plugin's directory.
1023
+ * @returns Whether the file changed; `false` when the plugin has no en-US file. Throws when en-US is invalid.
1024
+ */
1025
+ function writeLocaleTypings(pluginRoot) {
1026
+ const sourcePath = sourceLocalePath(pluginRoot);
1027
+ if (!existsSync(sourcePath)) return false;
1028
+ const typings = localeTypings(readTable(sourcePath));
1029
+ const path = join(pluginRoot, LOCALE_TYPINGS);
1030
+ if (existsSync(path) && readFileSync(path, "utf8") === typings) return false;
1031
+ mkdirSync(dirname(path), { recursive: true });
1032
+ writeFileSync(path, typings);
1033
+ return true;
1034
+ }
1035
+ /**
1036
+ * @description The source of {@link LOCALE_MODULE}: en-US inlined as the synchronous fallback, and the other
1037
+ * locales read from beside the installed bundle by the plugin's scoped `i18n.createLocales`.
1038
+ * @param source The en-US messages.
1039
+ * @returns The module's source.
1040
+ */
1041
+ function localeModuleSource(source) {
1042
+ return [
1043
+ "import { i18n } from '@unbound-app/api';",
1044
+ `const locales = i18n.createLocales(${JSON.stringify(source)});`,
1045
+ "export const messages = locales.messages;",
1046
+ "export const format = locales.format;",
1047
+ "export const useMessages = locales.useMessages;"
1048
+ ].join("\n");
1049
+ }
1050
+ function importTarget(source, importer, pluginRoot) {
1051
+ if (source.startsWith("~/")) return resolve(pluginRoot, "src", source.slice(2));
1052
+ if (source.startsWith(".")) return resolve(dirname(importer), source);
1053
+ }
1054
+ function isInsideFolder(target, folder) {
1055
+ return target === folder || dirname(target) === folder;
1056
+ }
1057
+ /**
1058
+ * @description Resolves {@link LOCALE_MODULE} to one generated module for the plugin being built, and refuses
1059
+ * an import of the `i18n/` folder or its files by path: a JSON file would put that language in the bundle.
1060
+ * @param pluginRoot The plugin's directory.
1061
+ * @returns The rollup plugin.
1062
+ */
1063
+ function localeModule(pluginRoot) {
1064
+ const folder = join(pluginRoot, LOCALES_DIRECTORY);
1065
+ return {
1066
+ name: "unbound-locale-module",
1067
+ resolveId(source, importer) {
1068
+ if (source === "@unbound-app/i18n") {
1069
+ if (!existsSync(sourceLocalePath(pluginRoot))) reportProblem(this, CHECK, pluginRoot, {
1070
+ file: importer ?? "i18n",
1071
+ severity: "error",
1072
+ message: `imports ${LOCALE_MODULE}, but the plugin has no ${LOCALES_DIRECTORY}/${SOURCE_LOCALE}.json`,
1073
+ fix: `add ${LOCALES_DIRECTORY}/${SOURCE_LOCALE}.json beside src/, with every message key`
1074
+ });
1075
+ return VIRTUAL_ID;
1076
+ }
1077
+ const target = importer && importTarget(source, importer, pluginRoot);
1078
+ if (!target || !isInsideFolder(target, folder)) return null;
1079
+ reportProblem(this, CHECK, pluginRoot, {
1080
+ file: importer ?? "",
1081
+ severity: "error",
1082
+ message: `imports ${source} by path, which would bundle its languages`,
1083
+ fix: `import from '${LOCALE_MODULE}' instead: it inlines en-US and loads the rest on demand`
1084
+ });
1085
+ return null;
1086
+ },
1087
+ load(id) {
1088
+ if (id !== VIRTUAL_ID) return null;
1089
+ const sourcePath = sourceLocalePath(pluginRoot);
1090
+ this.addWatchFile(sourcePath);
1091
+ return localeModuleSource(readTable(sourcePath));
1092
+ },
1093
+ transform(code, id) {
1094
+ if (id !== VIRTUAL_ID) return null;
1095
+ this.emitFile(reportLine({
1096
+ section: "inlined",
1097
+ label: `${SOURCE_LOCALE} strings`,
1098
+ bytes: Buffer.byteLength(code)
1099
+ }));
1100
+ return null;
1101
+ }
1102
+ };
1103
+ }
1104
+ /**
1105
+ * @description Validates the plugin's `i18n/` folder, writes the typings of {@link LOCALE_MODULE}, emits each
1106
+ * locale but en-US beside the bundle as `i18n/<code>.json`, and lists their SHA-256 under `locales` in the dist manifest.
1107
+ * @param pluginRoot The plugin's directory.
1108
+ * @returns The rollup plugin.
1109
+ */
1110
+ function locales(pluginRoot) {
1111
+ let folder;
1112
+ return {
1113
+ name: "unbound-locales",
1114
+ buildStart() {
1115
+ try {
1116
+ folder = readLocaleFolder(pluginRoot);
1117
+ } catch (error) {
1118
+ reportProblem(this, CHECK, pluginRoot, {
1119
+ file: LOCALES_DIRECTORY,
1120
+ severity: "error",
1121
+ message: error.message
1122
+ });
1123
+ }
1124
+ if (!folder) return;
1125
+ for (const warning of folder.warnings) reportProblem(this, CHECK, pluginRoot, {
1126
+ ...warning,
1127
+ severity: "warning"
1128
+ });
1129
+ for (const file of readdirSync(join(pluginRoot, LOCALES_DIRECTORY))) {
1130
+ if (!file.endsWith(".json")) continue;
1131
+ this.addWatchFile(join(pluginRoot, LOCALES_DIRECTORY, file));
1132
+ }
1133
+ writeLocaleTypings(pluginRoot);
1134
+ },
1135
+ generateBundle(_options, bundle) {
1136
+ const manifest = bundle["manifest.json"];
1137
+ if (!folder?.translations.size || manifest?.type !== "asset") return;
1138
+ const parsed = JSON.parse(String(manifest.source));
1139
+ const digests = {};
1140
+ for (const [code, { text, digest }] of folder.translations) {
1141
+ digests[code] = digest;
1142
+ this.emitFile({
1143
+ type: "asset",
1144
+ fileName: getLocalePath(parsed.main, code),
1145
+ source: text
1146
+ });
1147
+ }
1148
+ manifest.source = `${JSON.stringify({
1149
+ ...parsed,
1150
+ locales: digests
1151
+ }, null, " ")}\n`;
1152
+ }
1153
+ };
1154
+ }
1155
+ // src/rollup/byte-size.ts
1156
+ const UNITS = {
1157
+ b: 1,
1158
+ kb: 1024,
1159
+ mb: 1048576
1160
+ };
1161
+ const SIZE = /^(\d+(?:\.\d+)?)\s*(b|kb|mb)?$/i;
1162
+ /**
1163
+ * @description Reads a budget size such as `50kb` as bytes.
1164
+ * @param size The size, in bytes or with a `b`, `kb` or `mb` unit.
1165
+ * @returns The size in bytes.
1166
+ */
1167
+ function parseByteSize(size) {
1168
+ if (typeof size === "number") return size;
1169
+ const match = SIZE.exec(size.trim());
1170
+ if (!match) throw new Error(`"${size}" is not a size. Write bytes, or a number with b, kb or mb.`);
1171
+ const unit = match[2]?.toLowerCase() ?? "b";
1172
+ return Math.round(Number(match[1]) * UNITS[unit]);
1173
+ }
1174
+ /**
1175
+ * @description Renders a byte count for the build report, such as `18.4 KB`.
1176
+ * @param bytes The size in bytes.
1177
+ * @returns The size with one decimal and its unit.
1178
+ */
1179
+ function formatBytes(bytes) {
1180
+ if (bytes < UNITS.kb) return `${bytes} B`;
1181
+ if (bytes < UNITS.mb) return `${(bytes / UNITS.kb).toFixed(1)} KB`;
1182
+ return `${(bytes / UNITS.mb).toFixed(1)} MB`;
1183
+ }
1184
+ // ../client/scripts/build/shim-libraries.ts
1185
+ /** Each library in Discord's bundle the client shims, by package name, with the props that find its entry module. */
1186
+ var shim_libraries_default = {
1187
+ "@gorhom/bottom-sheet": [
1188
+ "BottomSheetModal",
1189
+ "BottomSheetView",
1190
+ "useBottomSheet"
1191
+ ],
1192
+ "@protobuf-ts/runtime": [
1193
+ "BinaryReader",
1194
+ "BinaryWriter",
1195
+ "MessageType"
1196
+ ],
1197
+ "@react-native-clipboard/clipboard": ["useClipboard", "default"],
1198
+ "@react-native-community/netinfo": [
1199
+ "NetInfoStateType",
1200
+ "useNetInfo",
1201
+ "configure"
1202
+ ],
1203
+ "@react-navigation/elements": [
1204
+ "HeaderBackButton",
1205
+ "useHeaderHeight",
1206
+ "getHeaderTitle"
1207
+ ],
1208
+ "@react-navigation/native": [
1209
+ "NavigationContainer",
1210
+ "useNavigation",
1211
+ "CommonActions"
1212
+ ],
1213
+ "@react-navigation/native-stack": ["createNativeStackNavigator", "NativeStackView"],
1214
+ "@react-navigation/stack": ["createStackNavigator", "CardStyleInterpolators"],
1215
+ "@sentry/react-native": [
1216
+ "captureException",
1217
+ "withScope",
1218
+ "wrap",
1219
+ "init"
1220
+ ],
1221
+ "@shopify/flash-list": ["FlashList", "MasonryFlashList"],
1222
+ "base64-js": [
1223
+ "toByteArray",
1224
+ "fromByteArray",
1225
+ "byteLength"
1226
+ ],
1227
+ "big-integer": ["randBetween", "isInstance"],
1228
+ buffer: [
1229
+ "Buffer",
1230
+ "SlowBuffer",
1231
+ "kMaxLength"
1232
+ ],
1233
+ "chroma-js": [
1234
+ "scale",
1235
+ "mix",
1236
+ "contrast",
1237
+ "temperature"
1238
+ ],
1239
+ "date-fns": [
1240
+ "format",
1241
+ "addDays",
1242
+ "differenceInDays",
1243
+ "parseISO"
1244
+ ],
1245
+ events: [
1246
+ "EventEmitter",
1247
+ "once",
1248
+ "listenerCount"
1249
+ ],
1250
+ lodash: [
1251
+ "debounce",
1252
+ "throttle",
1253
+ "cloneDeep",
1254
+ "chunk"
1255
+ ],
1256
+ moment: [
1257
+ "isMoment",
1258
+ "duration",
1259
+ "utc",
1260
+ "locale"
1261
+ ],
1262
+ pako: [
1263
+ "inflate",
1264
+ "deflate",
1265
+ "Inflate",
1266
+ "gzip"
1267
+ ],
1268
+ punycode: [
1269
+ "toASCII",
1270
+ "toUnicode",
1271
+ "ucs2"
1272
+ ],
1273
+ qs: [
1274
+ "parse",
1275
+ "stringify",
1276
+ "formats"
1277
+ ],
1278
+ "react-native-gesture-handler": [
1279
+ "GestureDetector",
1280
+ "Gesture",
1281
+ "GestureHandlerRootView"
1282
+ ],
1283
+ "react-native-image-picker": ["launchImageLibrary", "launchCamera"],
1284
+ "react-native-keyboard-controller": [
1285
+ "KeyboardProvider",
1286
+ "KeyboardController",
1287
+ "useKeyboardHandler"
1288
+ ],
1289
+ "react-native-nitro-modules": ["NitroModules", "getHybridObjectConstructor"],
1290
+ "react-native-reanimated": [
1291
+ "useSharedValue",
1292
+ "useAnimatedStyle",
1293
+ "ReduceMotion"
1294
+ ],
1295
+ "react-native-safe-area-context": [
1296
+ "SafeAreaProvider",
1297
+ "useSafeAreaInsets",
1298
+ "initialWindowMetrics"
1299
+ ],
1300
+ "react-native-screens": [
1301
+ "enableScreens",
1302
+ "ScreenStack",
1303
+ "enableFreeze"
1304
+ ],
1305
+ "react-native-svg": [
1306
+ "Svg",
1307
+ "Path",
1308
+ "SvgXml"
1309
+ ],
1310
+ "react-native-url-polyfill": ["setupURLPolyfill"],
1311
+ scheduler: [
1312
+ "unstable_scheduleCallback",
1313
+ "unstable_now",
1314
+ "unstable_NormalPriority"
1315
+ ],
1316
+ "simple-markdown": [
1317
+ "parserFor",
1318
+ "outputFor",
1319
+ "defaultRules"
1320
+ ],
1321
+ tinycolor2: [
1322
+ "mostReadable",
1323
+ "readability",
1324
+ "fromRatio"
1325
+ ],
1326
+ "ts-pattern": [
1327
+ "match",
1328
+ "P",
1329
+ "isMatching"
1330
+ ],
1331
+ uuid: [
1332
+ "v4",
1333
+ "v1",
1334
+ "validate",
1335
+ "NIL"
1336
+ ]
1337
+ };
1338
+ // src/rollup/build-report.ts
1339
+ /** The variable through which `ubd build` and `ubd dev` tell the preset where to write the report. */
1340
+ const BUILD_REPORT_ENV = "UNBOUND_BUILD_REPORT";
1341
+ const DEFAULT_BUDGET = {
1342
+ bundle: "50kb",
1343
+ assets: "500kb"
1344
+ };
1345
+ const TOP_MODULES = 5;
1346
+ const SHIPPED_UNITS = { i18n: "locale" };
1347
+ const manifestSchema = Type.Object({
1348
+ main: Type.String(),
1349
+ version: Type.Optional(Type.String()),
1350
+ assets: Type.Optional(Type.Record(Type.String(), Type.Object({
1351
+ type: Type.Union(ASSET_TYPES.map((type) => Type.Literal(type))),
1352
+ scales: Type.Record(Type.String(), Type.String())
1353
+ }))),
1354
+ locales: Type.Optional(Type.Record(Type.String(), Type.String()))
1355
+ });
1356
+ const SHIPPED_LABELS = {
1357
+ asset: ASSETS_DIRECTORY,
1358
+ locale: LOCALES_DIRECTORY
1359
+ };
1360
+ const BYTECODE_FILE = /^(.+)\.(\d+)\.hbc$/;
1361
+ function sizeOf(file) {
1362
+ if (file.type === "chunk") return Buffer.byteLength(file.code);
1363
+ return typeof file.source === "string" ? Buffer.byteLength(file.source) : file.source.byteLength;
1364
+ }
1365
+ function stem(fileName) {
1366
+ return fileName.replace(/\.[^.]+$/, "");
1367
+ }
1368
+ function cleanId(id) {
1369
+ return id.replace(/^\0/, "").replace(/\?.*$/, "");
1370
+ }
1371
+ function displayId(id, pluginRoot) {
1372
+ const path = cleanId(id);
1373
+ const nodeModules = path.lastIndexOf("/node_modules/");
1374
+ if (nodeModules !== -1) return path.slice(nodeModules + 1);
1375
+ return isAbsolute(path) ? relative(pluginRoot, path) : path;
1376
+ }
1377
+ /**
1378
+ * @description Reads the package a bundled module belongs to from its path.
1379
+ * @param id The module id.
1380
+ * @returns The package name, such as `lodash` or `@scope/name`, when the module is in `node_modules`.
1381
+ */
1382
+ function packageOf(id) {
1383
+ const path = cleanId(id);
1384
+ const nodeModules = path.lastIndexOf("/node_modules/");
1385
+ if (nodeModules === -1) return;
1386
+ const [first, second] = path.slice(nodeModules + 14).split("/");
1387
+ return first?.startsWith("@") && second ? `${first}/${second}` : first;
1388
+ }
1389
+ function readManifest(bundle) {
1390
+ const manifest = bundle["manifest.json"];
1391
+ if (manifest?.type !== "asset") return;
1392
+ const parsed = JSON.parse(String(manifest.source));
1393
+ return Value.Check(manifestSchema, parsed) ? parsed : void 0;
1394
+ }
1395
+ function shippedEntries(bundle, entry, manifest) {
1396
+ const entries = [];
1397
+ const listed = /* @__PURE__ */ new Set();
1398
+ for (const file of manifest ? listAddonFiles(manifest) : []) {
1399
+ const output = bundle[file.path];
1400
+ listed.add(file.path);
1401
+ if (!output) continue;
1402
+ const label = SHIPPED_LABELS[file.kind];
1403
+ entries.push({
1404
+ section: "shipped",
1405
+ label,
1406
+ unit: SHIPPED_UNITS[label] ?? "",
1407
+ bytes: sizeOf(output)
1408
+ });
1409
+ }
1410
+ for (const file of Object.values(bundle)) {
1411
+ const bytecode = BYTECODE_FILE.exec(file.fileName);
1412
+ if (file === entry || listed.has(file.fileName) || file.fileName === "manifest.json" || bytecode?.[1] === stem(entry.fileName)) continue;
1413
+ entries.push({
1414
+ section: "shipped",
1415
+ label: "files",
1416
+ unit: "",
1417
+ bytes: sizeOf(file)
1418
+ });
1419
+ }
1420
+ return entries;
1421
+ }
1422
+ function bytecodeSizes(bundle, entry) {
1423
+ return Object.values(bundle).map((file) => ({
1424
+ match: BYTECODE_FILE.exec(file.fileName),
1425
+ bytes: sizeOf(file)
1426
+ })).filter(({ match }) => match?.[1] === stem(entry.fileName)).map(({ match, bytes }) => ({
1427
+ version: Number(match?.[2]),
1428
+ bytes
1429
+ })).toSorted((a, b) => a.version - b.version);
1430
+ }
1431
+ function shimWarnings(entry, pluginRoot) {
1432
+ const bundled = /* @__PURE__ */ new Map();
1433
+ for (const id of Object.keys(entry.modules)) {
1434
+ const library = packageOf(id);
1435
+ if (!library || !Object.hasOwn(shim_libraries_default, library)) continue;
1436
+ bundled.set(library, [...bundled.get(library) ?? [], displayId(id, pluginRoot)]);
1437
+ }
1438
+ return [...bundled].map(([library, modules]) => ({
1439
+ kind: "shim",
1440
+ library,
1441
+ modules
1442
+ }));
1443
+ }
1444
+ function budgetWarnings(sizes, budgets) {
1445
+ return Object.keys(budgets).filter((budget) => sizes[budget] > budgets[budget]).map((budget) => ({
1446
+ kind: "budget",
1447
+ budget,
1448
+ bytes: sizes[budget],
1449
+ limit: budgets[budget]
1450
+ }));
1451
+ }
1452
+ /**
1453
+ * @description Measures a written plugin bundle: its size, what features inlined into it, the files shipped beside it, and what breaks a budget.
1454
+ * @param bundle The output bundle, with the report entries already taken out.
1455
+ * @param entries The entries feature plugins contributed with `reportLine`.
1456
+ * @param options The plugin's directory and its budgets in bytes.
1457
+ * @returns The report, or `undefined` when the bundle has no entry chunk.
1458
+ */
1459
+ function createReport(bundle, entries, { pluginRoot, budgets }) {
1460
+ const entry = Object.values(bundle).find((file) => file.type === "chunk" && file.isEntry);
1461
+ if (!entry) return;
1462
+ const bytes = sizeOf(entry);
1463
+ const manifest = readManifest(bundle);
1464
+ const shipped = mergeEntries([...shippedEntries(bundle, entry, manifest), ...entries], "shipped");
1465
+ const assets = shipped.find((group) => group.label === "assets")?.bytes ?? 0;
1466
+ const modules = Object.entries(entry.modules).map(([id, module]) => ({
1467
+ id: displayId(id, pluginRoot),
1468
+ bytes: module.renderedLength
1469
+ })).toSorted((a, b) => b.bytes - a.bytes).slice(0, TOP_MODULES);
1470
+ return {
1471
+ name: basename(pluginRoot),
1472
+ version: manifest?.version ?? "",
1473
+ bundle: {
1474
+ file: entry.fileName,
1475
+ bytes,
1476
+ gzip: gzipSync(entry.code).byteLength,
1477
+ bytecode: bytecodeSizes(bundle, entry)
1478
+ },
1479
+ inlined: mergeEntries(entries, "inlined"),
1480
+ shipped,
1481
+ metro: mergeEntries(entries, "metro"),
1482
+ modules,
1483
+ budgets,
1484
+ warnings: [...budgetWarnings({
1485
+ bundle: bytes,
1486
+ assets
1487
+ }, budgets), ...shimWarnings(entry, pluginRoot)]
1488
+ };
1489
+ }
1490
+ /**
1491
+ * @description Collects the entries feature plugins add with `reportLine`, keeps them out of `dist`, and writes the build report after the bundle is written.
1492
+ * @param options The plugin's directory, its budgets, and where to write the report.
1493
+ * @returns The rollup plugin. Its `generateBundle` runs last, so it sees the bytecode and every emitted file.
1494
+ */
1495
+ function buildReport({ pluginRoot, budget = {}, reportFile }) {
1496
+ const budgets = {
1497
+ bundle: parseByteSize(budget.bundle ?? DEFAULT_BUDGET.bundle),
1498
+ assets: parseByteSize(budget.assets ?? DEFAULT_BUDGET.assets)
1499
+ };
1500
+ let entries = [];
1501
+ return {
1502
+ name: "build-report",
1503
+ generateBundle: {
1504
+ order: "post",
1505
+ handler(_options, bundle) {
1506
+ entries = takeReportEntries(bundle, (message) => this.warn(message));
1507
+ }
1508
+ },
1509
+ async writeBundle(_options, bundle) {
1510
+ if (!reportFile) return;
1511
+ const report = createReport(bundle, entries, {
1512
+ pluginRoot,
1513
+ budgets
1514
+ });
1515
+ if (report) await writeFile(reportFile, JSON.stringify(report));
1516
+ }
1517
+ };
1518
+ }
1519
+ // src/rollup/output-only.ts
1520
+ /** The `api` flag on a plugin that only wraps, minifies or compiles the chunk, so `ubd check` renders without it. */
1521
+ const OUTPUT_ONLY = "unboundOutputOnly";
1522
+ /**
1523
+ * @description Marks a plugin as output-only: `ubd check` leaves it out when it renders the chunk for the plugins that check it.
1524
+ * @param plugin The plugin.
1525
+ * @returns A copy carrying the flag in its `api`.
1526
+ */
1527
+ function outputOnly(plugin) {
1528
+ return {
1529
+ ...plugin,
1530
+ api: {
1531
+ ...plugin.api,
1532
+ [OUTPUT_ONLY]: true
1533
+ }
1534
+ };
1535
+ }
1536
+ // ../contracts/src/settings-schema.ts
1537
+ const BUILDERS$1 = "setting.boolean/number/integer/string/choice/array/section/button/notice from '@unbound-app/settings/schema'";
1538
+ const ENTITIES = [
1539
+ "user",
1540
+ "channel",
1541
+ "guild",
1542
+ "role"
1543
+ ];
1544
+ const COMMON = [
1545
+ "kind",
1546
+ "displayAs",
1547
+ "title",
1548
+ "subtitle",
1549
+ "hidden",
1550
+ "requiresRestart",
1551
+ "visibleWhen",
1552
+ "disabledWhen",
1553
+ "default"
1554
+ ];
1555
+ const TYPEBOX = /* @__PURE__ */ new Set([
1556
+ "type",
1557
+ "anyOf",
1558
+ "items",
1559
+ "required",
1560
+ "properties",
1561
+ "additionalProperties",
1562
+ "$schema",
1563
+ "$id"
1564
+ ]);
1565
+ const SPEC = {
1566
+ boolean: {
1567
+ displays: ["switch", "checkbox"],
1568
+ keys: []
1569
+ },
1570
+ number: {
1571
+ displays: [
1572
+ "input",
1573
+ "slider",
1574
+ "stepper",
1575
+ "duration"
1576
+ ],
1577
+ keys: [
1578
+ "minimum",
1579
+ "maximum",
1580
+ "multipleOf",
1581
+ "units"
1582
+ ]
1583
+ },
1584
+ integer: {
1585
+ displays: [
1586
+ "input",
1587
+ "slider",
1588
+ "stepper",
1589
+ "duration"
1590
+ ],
1591
+ keys: [
1592
+ "minimum",
1593
+ "maximum",
1594
+ "multipleOf",
1595
+ "units"
1596
+ ]
1597
+ },
1598
+ string: {
1599
+ displays: [
1600
+ "input",
1601
+ "multiline",
1602
+ "secret",
1603
+ "url",
1604
+ "color",
1605
+ "image",
1606
+ "font",
1607
+ "emoji",
1608
+ ...ENTITIES
1609
+ ],
1610
+ keys: [
1611
+ "minLength",
1612
+ "maxLength",
1613
+ "placeholder",
1614
+ "alpha",
1615
+ "presets",
1616
+ "palette",
1617
+ "maxSize"
1618
+ ]
1619
+ },
1620
+ choice: {
1621
+ displays: [
1622
+ "radio",
1623
+ "picker",
1624
+ "segmented"
1625
+ ],
1626
+ keys: ["options"]
1627
+ },
1628
+ array: {
1629
+ displays: [
1630
+ "list",
1631
+ "checkboxes",
1632
+ ...ENTITIES
1633
+ ],
1634
+ keys: [
1635
+ "of",
1636
+ "minItems",
1637
+ "maxItems"
1638
+ ]
1639
+ },
1640
+ button: {
1641
+ displays: ["button", "danger"],
1642
+ keys: ["action", "confirm"]
1643
+ },
1644
+ notice: {
1645
+ displays: ["notice"],
1646
+ keys: ["tone"]
1647
+ }
1648
+ };
1649
+ const DISPLAY_KEYS = {
1650
+ minLength: ["input", "multiline"],
1651
+ maxLength: ["input", "multiline"],
1652
+ placeholder: [
1653
+ "input",
1654
+ "multiline",
1655
+ "secret",
1656
+ "url"
1657
+ ],
1658
+ alpha: ["color"],
1659
+ presets: ["color"],
1660
+ palette: ["color"],
1661
+ maxSize: ["image"],
1662
+ units: ["duration"]
1663
+ };
1664
+ const SNOWFLAKE = /^\d{17,20}$/;
1665
+ const URL_PATTERN = /^https?:\/\/[^\s/$.?#][^\s]*$/i;
1666
+ const EMOJI = /^[^\s:]+(?::\d{17,20})?$/;
1667
+ function textOf(value) {
1668
+ return typeof value === "string" ? value : JSON.stringify(value) ?? "nothing";
1669
+ }
1670
+ function isObject(value) {
1671
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1672
+ }
1673
+ /**
1674
+ * @description Tells a nested settings section from a setting or action.
1675
+ * @param node A property of a settings schema.
1676
+ * @returns Whether it is a section.
1677
+ */
1678
+ function isSettingsGroup(node) {
1679
+ return node.kind === "section";
1680
+ }
1681
+ /**
1682
+ * @description Tells an action from a stored setting.
1683
+ * @param node A property of a settings schema or section.
1684
+ * @returns Whether it is an action.
1685
+ */
1686
+ function isButton(node) {
1687
+ return node.kind === "button";
1688
+ }
1689
+ /**
1690
+ * @description Tells a notice from a stored setting or an action.
1691
+ * @param node A property of a settings schema or section.
1692
+ * @returns Whether it is a notice.
1693
+ */
1694
+ function isNotice(node) {
1695
+ return node.kind === "notice";
1696
+ }
1697
+ /**
1698
+ * @description Tells a stored setting from a row that stores nothing, a button or a notice.
1699
+ * @param node A property of a settings schema or section.
1700
+ * @returns Whether it is a stored setting.
1701
+ */
1702
+ function isStored(node) {
1703
+ return !isButton(node) && !isNotice(node);
1704
+ }
1705
+ /**
1706
+ * @description Tells an http or https URL, as a `displayAs: 'url'` setting holds.
1707
+ * @param value The text.
1708
+ * @returns Whether it is one.
1709
+ */
1710
+ function isHttpUrl(value) {
1711
+ return URL_PATTERN.test(value);
1712
+ }
1713
+ /**
1714
+ * @description Tells a Discord id, as an entity setting holds.
1715
+ * @param value The text.
1716
+ * @returns Whether it is one.
1717
+ */
1718
+ function isSnowflake(value) {
1719
+ return SNOWFLAKE.test(value);
1720
+ }
1721
+ function isEntityDisplay(display) {
1722
+ return ENTITIES.includes(display);
1723
+ }
1724
+ function isMultiple(value, step) {
1725
+ const ratio = value / step;
1726
+ return Math.abs(ratio - Math.round(ratio)) < 1e-9;
1727
+ }
1728
+ function isStringValue(field, value) {
1729
+ const within = value.length >= (field.minLength ?? 0) && value.length <= (field.maxLength ?? Infinity);
1730
+ switch (field.displayAs) {
1731
+ case "color": return (field.alpha ? /^#[0-9a-f]{6}(?:[0-9a-f]{2})?$/i : /^#[0-9a-f]{6}$/i).test(value);
1732
+ case "url": return within && (value === "" || isHttpUrl(value));
1733
+ case "emoji": return value === "" || EMOJI.test(value);
1734
+ case "user":
1735
+ case "channel":
1736
+ case "guild":
1737
+ case "role": return value === "" || isSnowflake(value);
1738
+ default: return within;
1739
+ }
1740
+ }
1741
+ function isItemValue(field, value) {
1742
+ switch (field.kind) {
1743
+ case "boolean": return typeof value === "boolean";
1744
+ case "string": return typeof value === "string" && isStringValue(field, value);
1745
+ case "choice": return field.options.some((choice) => choice.const === value);
1746
+ case "array": {
1747
+ const display = field.displayAs;
1748
+ const item = field.of.kind === "string" && isEntityDisplay(display) ? {
1749
+ ...field.of,
1750
+ displayAs: display
1751
+ } : field.of;
1752
+ return Array.isArray(value) && value.length >= (field.minItems ?? 0) && value.length <= (field.maxItems ?? Infinity) && new Set(value).size === value.length && value.every((each) => isItemValue(item, each));
1753
+ }
1754
+ default: return typeof value === "number" && Number.isFinite(value) && (field.kind !== "integer" || Number.isInteger(value)) && value >= (field.minimum ?? -Infinity) && value <= (field.maximum ?? Infinity) && (field.multipleOf === void 0 || isMultiple(value, field.multipleOf));
1755
+ }
1756
+ }
1757
+ /**
1758
+ * @description Checks a value against one setting, by its primitive and display: the same check at build time (each default) and on device (each stored value and write).
1759
+ * @param field The setting.
1760
+ * @param value The value.
1761
+ * @returns Whether the setting can hold it.
1762
+ */
1763
+ function isSettingValue(field, value) {
1764
+ return isItemValue(field, value);
1765
+ }
1766
+ function issue(reader, path, problem, fix) {
1767
+ reader.issues.push({
1768
+ path,
1769
+ problem,
1770
+ fix
1771
+ });
1772
+ }
1773
+ function readLabels(node, path, reader) {
1774
+ for (const key of [
1775
+ "title",
1776
+ "subtitle",
1777
+ "confirm",
1778
+ "placeholder"
1779
+ ]) if (node[key] !== void 0 && typeof node[key] !== "string") issue(reader, path, `has a non-string \`${key}\``, "set it to a message key");
1780
+ if (node.hidden !== true && typeof node.title !== "string") issue(reader, path, "has no title", "add `title: <message key>`, or `hidden: true`");
1781
+ for (const key of ["visibleWhen", "disabledWhen"]) {
1782
+ const condition = node[key];
1783
+ if (condition === void 0) continue;
1784
+ if (typeof condition === "string") reader.conditions.push([
1785
+ path,
1786
+ key,
1787
+ condition
1788
+ ]);
1789
+ else if (!isObject(condition) || condition.predicate !== true) issue(reader, `${path}.${key}`, "is not a predicate or a key", "write `(settings) => boolean` or a boolean setting key");
1790
+ }
1791
+ }
1792
+ function readChoices(node, path, reader) {
1793
+ const options = Array.isArray(node.options) ? node.options : [];
1794
+ if (options.length === 0) issue(reader, path, "has no options", "add at least one entry to `options`");
1795
+ if (!options.every((choice) => isObject(choice) && typeof choice.const === "string" && typeof choice.title === "string")) issue(reader, path, "has an option without a message key", "map each option to a key or { title, subtitle }");
1796
+ if (node.displayAs === "segmented" && options.length > 4) issue(reader, path, `shows ${options.length} options as segmented, which fits at most 4`, "use displayAs: 'radio' or 'picker'");
1797
+ }
1798
+ function readDisplay(node, path, reader) {
1799
+ const display = textOf(node.displayAs);
1800
+ for (const [key, displays] of Object.entries(DISPLAY_KEYS)) if (node[key] !== void 0 && !displays.includes(display)) issue(reader, path, `sets \`${key}\`, which only displayAs ${displays.map((name) => `'${name}'`).join("/")} takes`, `remove \`${key}\` or change displayAs`);
1801
+ if (display === "slider" && (node.minimum === void 0 || node.maximum === void 0)) issue(reader, path, "shows as a slider without both `min` and `max`", "set both, or use displayAs: 'input'");
1802
+ if (node.palette === true && !Array.isArray(node.presets)) issue(reader, path, "sets `palette` without `presets`", "add `presets`");
1803
+ if (node.kind === "choice") readChoices(node, path, reader);
1804
+ if (node.kind === "array") {
1805
+ const of = isObject(node.of) ? node.of : {};
1806
+ const choice = of.kind === "choice";
1807
+ if (of.kind !== "string" && !choice) issue(reader, path, "holds items that are not strings or choices", "set `of: setting.string()` or `of: setting.choice(...)`");
1808
+ else if (choice !== (display === "checkboxes")) issue(reader, path, `shows ${choice ? "choices" : "strings"} as '${display}'`, choice ? "use displayAs: 'checkboxes'" : "use displayAs: 'list' or an entity picker");
1809
+ if (choice) readChoices(of, path, reader);
1810
+ }
1811
+ if (node.kind === "notice" && ![
1812
+ "info",
1813
+ "warning",
1814
+ "danger"
1815
+ ].includes(textOf(node.tone))) issue(reader, path, `has the tone ${textOf(node.tone)}`, "set `tone` to 'info', 'warning' or 'danger'");
1816
+ if (node.kind === "button" && (typeof node.action !== "string" || !node.action)) issue(reader, path, "has no `action`", "set `action: '<name of an export of src/actions.ts>'`");
1817
+ }
1818
+ function copy(node, keys) {
1819
+ return Object.fromEntries(Object.entries(node).filter(([key]) => keys.includes(key)));
1820
+ }
1821
+ function readField(node, path, reader) {
1822
+ const spec = isObject(node) && typeof node.kind === "string" ? SPEC[node.kind] : void 0;
1823
+ if (!isObject(node) || !spec) {
1824
+ issue(reader, path, "is not built with a setting builder", `use ${BUILDERS$1}`);
1825
+ return;
1826
+ }
1827
+ for (const key of Object.keys(node)) if (!COMMON.includes(key) && !spec.keys.includes(key) && !TYPEBOX.has(key)) issue(reader, path, `has the unsupported \`${key}\` option`, "remove it");
1828
+ if (!spec.displays.includes(textOf(node.displayAs))) {
1829
+ issue(reader, path, `cannot show as '${textOf(node.displayAs)}'`, `use displayAs ${spec.displays.map((name) => `'${name}'`).join(", ")}`);
1830
+ return;
1831
+ }
1832
+ readLabels(node, path, reader);
1833
+ readDisplay(node, path, reader);
1834
+ const kept = copy(node, [...COMMON, ...spec.keys]);
1835
+ if (isObject(kept.of)) kept.of = copy(kept.of, [
1836
+ "kind",
1837
+ "displayAs",
1838
+ "options",
1839
+ ...SPEC.string?.keys ?? []
1840
+ ]);
1841
+ const field = kept;
1842
+ if (!isStored(field)) return field;
1843
+ if (field.kind === "boolean") reader.booleans.add(path);
1844
+ if (!isSettingValue(field, node.default)) issue(reader, path, `has the default ${JSON.stringify(node.default)}, which its own display or constraints reject`, "change the default or the constraints");
1845
+ return field;
1846
+ }
1847
+ function readProperties(node, path, reader, visit) {
1848
+ for (const [key, child] of Object.entries(isObject(node.properties) ? node.properties : {})) {
1849
+ const at = path ? `${path}.${key}` : key;
1850
+ if (key.includes(".")) {
1851
+ issue(reader, at, "has a dot in its key", "rename it");
1852
+ continue;
1853
+ }
1854
+ visit(key, child, at);
1855
+ }
1856
+ }
1857
+ function readGroup(node, path, reader) {
1858
+ const properties = {};
1859
+ readLabels(node, path, reader);
1860
+ if (!["group", "collapsible"].includes(textOf(node.displayAs))) issue(reader, path, `cannot show as '${textOf(node.displayAs)}'`, "use displayAs 'group' or 'collapsible'");
1861
+ readProperties(node, path, reader, (key, child, at) => {
1862
+ if (isObject(child) && child.kind === "section") {
1863
+ issue(reader, at, "nests a section inside a section", "move its settings into the outer section");
1864
+ return;
1865
+ }
1866
+ const field = readField(child, at, reader);
1867
+ if (field) properties[key] = field;
1868
+ });
1869
+ return {
1870
+ ...copy(node, COMMON),
1871
+ properties
1872
+ };
1873
+ }
1874
+ /**
1875
+ * @description Validates the JSON a plugin's `defineSettings(...)` evaluates to and copies the supported options out of it.
1876
+ * @param value The evaluated schema.
1877
+ * @returns The supported part of the schema, and every problem found, each naming its property.
1878
+ */
1879
+ function readSettingsSchema(value) {
1880
+ const reader = {
1881
+ issues: [],
1882
+ booleans: /* @__PURE__ */ new Set(),
1883
+ conditions: []
1884
+ };
1885
+ const properties = {};
1886
+ if (!isObject(value) || value.kind !== "settings") {
1887
+ issue(reader, "", "does not default-export defineSettings(...)", "write `export default defineSettings({ ... })` with defineSettings from '@unbound-app/settings/schema'");
1888
+ return {
1889
+ schema: {
1890
+ kind: "settings",
1891
+ properties
1892
+ },
1893
+ issues: reader.issues
1894
+ };
1895
+ }
1896
+ readProperties(value, "", reader, (key, child, at) => {
1897
+ const node = isObject(child) && child.kind === "section" ? readGroup(child, at, reader) : readField(child, at, reader);
1898
+ if (node) properties[key] = node;
1899
+ });
1900
+ for (const [path, key, condition] of reader.conditions) if (!reader.booleans.has(condition)) issue(reader, `${path}.${key}`, `names "${condition}", which is not a boolean setting`, "name a boolean setting by its dot-separated key, or write a predicate");
1901
+ return {
1902
+ schema: {
1903
+ kind: "settings",
1904
+ ...typeof value.title === "string" ? { title: value.title } : {},
1905
+ properties
1906
+ },
1907
+ issues: reader.issues
1908
+ };
1909
+ }
1910
+ // src/rollup/predicate-scope.ts
1911
+ /** Globals a predicate may read: pure built-ins. */
1912
+ const SAFE_GLOBALS = /* @__PURE__ */ new Set([
1913
+ "undefined",
1914
+ "NaN",
1915
+ "Infinity",
1916
+ "Boolean",
1917
+ "Number",
1918
+ "String",
1919
+ "Math",
1920
+ "Array",
1921
+ "Object",
1922
+ "JSON",
1923
+ "isNaN",
1924
+ "isFinite",
1925
+ "parseInt",
1926
+ "parseFloat"
1927
+ ]);
1928
+ const TYPE_FIELDS = /* @__PURE__ */ new Set([
1929
+ "typeAnnotation",
1930
+ "returnType",
1931
+ "typeParameters",
1932
+ "typeArguments"
1933
+ ]);
1934
+ const IMPURE = {
1935
+ AssignmentExpression: "assigns a variable",
1936
+ UpdateExpression: "assigns a variable",
1937
+ ThisExpression: "reads `this`",
1938
+ NewExpression: "constructs an object",
1939
+ ImportExpression: "imports a module",
1940
+ AwaitExpression: "awaits",
1941
+ YieldExpression: "yields"
1942
+ };
1943
+ function isNode(value) {
1944
+ return typeof value === "object" && value !== null && typeof Reflect.get(value, "type") === "string";
1945
+ }
1946
+ function children(node) {
1947
+ const found = [];
1948
+ for (const [field, value] of Object.entries(node)) {
1949
+ if (TYPE_FIELDS.has(field)) continue;
1950
+ for (const item of Array.isArray(value) ? value : [value]) if (isNode(item)) found.push([field, item]);
1951
+ }
1952
+ return found;
1953
+ }
1954
+ function walk(node, visit, parent, field = "") {
1955
+ visit(node, parent, field);
1956
+ for (const [childField, child] of children(node)) walk(child, visit, node, childField);
1957
+ }
1958
+ function declaredNames(fn) {
1959
+ const names = /* @__PURE__ */ new Set();
1960
+ walk(fn, (node) => {
1961
+ if (node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression") {
1962
+ for (const param of node.params) if (param.type === "Identifier") names.add(param.name);
1963
+ }
1964
+ if (node.type === "VariableDeclarator" && node.id.type === "Identifier") names.add(node.id.name);
1965
+ });
1966
+ return names;
1967
+ }
1968
+ function isReference(parent, field) {
1969
+ if (parent?.type === "MemberExpression") return field !== "property" || parent.computed;
1970
+ if (parent?.type === "Property") return field !== "key" || parent.computed;
1971
+ return !(parent?.type === "VariableDeclarator" && field === "id" || (parent?.type === "ArrowFunctionExpression" || parent?.type === "FunctionExpression") && field === "params");
1972
+ }
1973
+ /**
1974
+ * @description Checks that a `visibleWhen` or `disabledWhen` predicate can be lifted into the plugin bundle on its own:
1975
+ * it reads only its parameter, its own locals and pure built-ins, and assigns, constructs and imports nothing.
1976
+ * @param fn The predicate's function node.
1977
+ * @returns Every problem, each at its offset in the schema file.
1978
+ */
1979
+ function checkPredicate(fn) {
1980
+ if (fn.type !== "ArrowFunctionExpression" && fn.type !== "FunctionExpression") return [{
1981
+ offset: fn.start,
1982
+ problem: "is not a function",
1983
+ fix: "write `(settings) => boolean` inline"
1984
+ }];
1985
+ const names = declaredNames(fn);
1986
+ const problems = [];
1987
+ walk(fn, (node, parent, field) => {
1988
+ const impure = IMPURE[node.type];
1989
+ if (impure) {
1990
+ problems.push({
1991
+ offset: node.start,
1992
+ problem: impure,
1993
+ fix: "keep the predicate pure: read `settings` and return a boolean"
1994
+ });
1995
+ return;
1996
+ }
1997
+ if (node.type !== "Identifier" || !isReference(parent, field)) return;
1998
+ if (!names.has(node.name) && !SAFE_GLOBALS.has(node.name)) problems.push({
1999
+ offset: node.start,
2000
+ problem: `reads outer variable "${node.name}"`,
2001
+ fix: "read only the predicate's `settings` parameter; the predicate runs in the plugin bundle without settings.ts"
2002
+ });
2003
+ });
2004
+ return problems;
2005
+ }
2006
+ // src/rollup/settings-source.ts
2007
+ const CONDITIONS = /* @__PURE__ */ new Set(["visibleWhen", "disabledWhen"]);
2008
+ /**
2009
+ * @description Lists the offset each line of a source starts at, for {@link toLocation}.
2010
+ * @param source The source.
2011
+ * @returns The offsets.
2012
+ */
2013
+ function lineStarts(source) {
2014
+ const starts = [0];
2015
+ for (let i = 0, len = source.length; i < len; i++) if (source[i] === "\n") starts.push(i + 1);
2016
+ return starts;
2017
+ }
2018
+ /**
2019
+ * @description Turns an offset into a line and column.
2020
+ * @param starts The source's {@link lineStarts}.
2021
+ * @param offset The offset.
2022
+ * @returns The 1-based line and column.
2023
+ */
2024
+ function toLocation(starts, offset) {
2025
+ const line = Math.max(0, starts.findLastIndex((start) => start <= offset));
2026
+ return {
2027
+ line: line + 1,
2028
+ column: offset - (starts[line] ?? 0) + 1
2029
+ };
2030
+ }
2031
+ function keyName(property) {
2032
+ if (property.type !== "Property") return;
2033
+ if (property.key.type === "Identifier") return property.key.name;
2034
+ return property.key.type === "Literal" && typeof property.key.value === "string" ? property.key.value : void 0;
2035
+ }
2036
+ function unwrap(node) {
2037
+ let current = node;
2038
+ while (current.type === "TSAsExpression" || current.type === "TSSatisfiesExpression" || current.type === "TSNonNullExpression" || current.type === "ParenthesizedExpression") current = current.expression;
2039
+ return current;
2040
+ }
2041
+ function objectArgument(node, index) {
2042
+ const argument = node?.type === "CallExpression" ? node.arguments[index] : void 0;
2043
+ return argument?.type === "ObjectExpression" ? argument : void 0;
2044
+ }
2045
+ function rootObject(program) {
2046
+ const exported = program.body.find((node) => node.type === "ExportDefaultDeclaration");
2047
+ if (!exported) return;
2048
+ let call = exported.declaration;
2049
+ if (call.type === "Identifier") {
2050
+ const name = call.name;
2051
+ call = program.body.flatMap((node) => node.type === "VariableDeclaration" ? node.declarations : []).find((node) => node.id.type === "Identifier" && node.id.name === name)?.init;
2052
+ }
2053
+ return {
2054
+ start: exported.start,
2055
+ object: objectArgument(call, 0)
2056
+ };
2057
+ }
2058
+ /**
2059
+ * @description Reads settings.ts's syntax: where each property is written, and the source of each predicate, checked to be liftable.
2060
+ * @param fileName The schema file's name, for the parser.
2061
+ * @param source The schema file's source.
2062
+ * @returns The offsets, predicates and problems.
2063
+ */
2064
+ function readSettingsSource(fileName, source) {
2065
+ const { program } = parseSync(fileName, source, { lang: "ts" });
2066
+ const result = {
2067
+ offsets: /* @__PURE__ */ new Map(),
2068
+ predicates: [],
2069
+ problems: [],
2070
+ starts: lineStarts(source)
2071
+ };
2072
+ const root = rootObject(program);
2073
+ if (!root) return result;
2074
+ result.offsets.set("", root.start);
2075
+ function visitOptions(options, path) {
2076
+ for (const property of options?.properties ?? []) {
2077
+ const option = keyName(property);
2078
+ if (property.type !== "Property" || !option || !CONDITIONS.has(option)) continue;
2079
+ const fn = unwrap(property.value);
2080
+ if (fn.type === "Literal" || fn.type === "TemplateLiteral") continue;
2081
+ const problems = checkPredicate(fn);
2082
+ result.offsets.set(`${path}.${option}`, property.start);
2083
+ result.problems.push(...problems.map((problem) => ({
2084
+ ...problem,
2085
+ path: `${path}.${option}`
2086
+ })));
2087
+ if (!problems.length) result.predicates.push({
2088
+ path,
2089
+ option: option === "visibleWhen" ? "visibleWhen" : "disabledWhen",
2090
+ source: source.slice(fn.start, fn.end)
2091
+ });
2092
+ }
2093
+ }
2094
+ function visit(object, prefix) {
2095
+ for (const property of object?.properties ?? []) {
2096
+ const key = keyName(property);
2097
+ if (!key || property.type !== "Property") continue;
2098
+ const path = prefix ? `${prefix}.${key}` : key;
2099
+ result.offsets.set(path, property.start);
2100
+ visitOptions(objectArgument(property.value, 0), path);
2101
+ if (!prefix) visit(objectArgument(property.value, 1), path);
2102
+ }
2103
+ }
2104
+ visit(root.object, "");
2105
+ return result;
2106
+ }
2107
+ /**
2108
+ * @description Reads the named exports of a plugin's `src/actions.ts`, and which of them are functions: a function declaration, or a const bound to a function.
2109
+ * @param fileName The file's name, for the parser.
2110
+ * @param source The file's source.
2111
+ * @returns Each export's name, and whether it is a function.
2112
+ */
2113
+ function readActionExports(fileName, source) {
2114
+ const { program } = parseSync(fileName, source);
2115
+ const exports = /* @__PURE__ */ new Map();
2116
+ for (const node of program.body) {
2117
+ if (node.type !== "ExportNamedDeclaration" || node.exportKind === "type") continue;
2118
+ const { declaration } = node;
2119
+ if (declaration?.type === "FunctionDeclaration" && declaration.id) exports.set(declaration.id.name, true);
2120
+ if (declaration?.type === "VariableDeclaration") for (const declarator of declaration.declarations) {
2121
+ const init = declarator.init ? unwrap(declarator.init) : void 0;
2122
+ if (declarator.id.type === "Identifier") exports.set(declarator.id.name, init?.type === "ArrowFunctionExpression" || init?.type === "FunctionExpression");
2123
+ }
2124
+ for (const spec of node.specifiers) {
2125
+ const name = spec.exported.type === "Identifier" ? spec.exported.name : String(spec.exported.value);
2126
+ if (!exports.has(name)) exports.set(name, false);
2127
+ }
2128
+ }
2129
+ return exports;
2130
+ }
2131
+ // src/rollup/settings-read.ts
2132
+ const SOURCE_MESSAGES = "i18n/en-US.json";
2133
+ /** A plugin's settings schema file, beside `src/`. */
2134
+ const SETTINGS_FILE = "settings.ts";
2135
+ /** The runtime module plugin source reads its settings from. */
2136
+ const SETTINGS_MODULE = "@unbound-app/settings";
2137
+ /** The build-time module `settings.ts` imports its builders from; it exists only while the build evaluates the file. */
2138
+ const SETTINGS_SCHEMA_MODULE = "@unbound-app/settings/schema";
2139
+ const BUILDERS = fileURLToPath(import.meta.resolve("@unbound-app/cli/settings/schema"));
2140
+ /**
2141
+ * @description Names a plugin's settings schema file.
2142
+ * @param pluginRoot The plugin's directory.
2143
+ * @returns The absolute path of `settings.ts`.
2144
+ */
2145
+ function settingsPath(pluginRoot) {
2146
+ return join(pluginRoot, SETTINGS_FILE);
2147
+ }
2148
+ /**
2149
+ * @description Bundles `settings.ts` in memory, resolving the builders module to the CLI's implementation, and imports it.
2150
+ * @param path The schema file.
2151
+ * @returns Its default export as JSON, and the files it was bundled from.
2152
+ */
2153
+ async function evaluateSettings(path) {
2154
+ const bundle = await rolldown({
2155
+ input: path,
2156
+ platform: "node",
2157
+ logLevel: "silent",
2158
+ plugins: [{
2159
+ name: "unbound-settings-schema",
2160
+ resolveId: (source) => source === "@unbound-app/settings/schema" ? BUILDERS : null
2161
+ }]
2162
+ });
2163
+ try {
2164
+ const { output } = await bundle.generate({ format: "esm" });
2165
+ const module = await import(`data:text/javascript;base64,${Buffer.from(output[0].code).toString("base64")}`);
2166
+ const watchFiles = (await bundle.watchFiles).filter((file) => file !== BUILDERS);
2167
+ return {
2168
+ value: JSON.parse(JSON.stringify(module.default ?? null)),
2169
+ watchFiles
2170
+ };
2171
+ } finally {
2172
+ await bundle.close();
2173
+ }
2174
+ }
2175
+ function readMessages(pluginRoot, issues) {
2176
+ if (!existsSync(sourceLocalePath(pluginRoot))) {
2177
+ issues.push({
2178
+ path: "",
2179
+ problem: `labels are message keys, but the plugin has no ${SOURCE_MESSAGES}`,
2180
+ fix: `create ${SOURCE_MESSAGES} with a key for each title, subtitle and option`
2181
+ });
2182
+ return {};
2183
+ }
2184
+ try {
2185
+ return readTable(sourceLocalePath(pluginRoot));
2186
+ } catch (error) {
2187
+ issues.push({
2188
+ path: "",
2189
+ problem: error.message.replace(pluginRoot + sep, ""),
2190
+ fix: "fix its JSON"
2191
+ });
2192
+ return {};
2193
+ }
2194
+ }
2195
+ function screamingSnake(path) {
2196
+ return path.replaceAll(/([a-z0-9])([A-Z])/g, "$1_$2").replaceAll(/[^A-Za-z0-9]+/g, "_").toUpperCase();
2197
+ }
2198
+ function labelsOf(node) {
2199
+ const labels = [
2200
+ ["title", node.title],
2201
+ ["subtitle", node.subtitle],
2202
+ ["placeholder", node.placeholder],
2203
+ ["confirm", node.confirm]
2204
+ ];
2205
+ for (const options of [node.options, node.of?.options]) for (const option of options ?? []) labels.push([`option ${option.const}`, option.title], [`option ${option.const} subtitle`, option.subtitle]);
2206
+ return labels;
2207
+ }
2208
+ function checkKeys(schema, messages, issues) {
2209
+ const nodes = schema.title ? [["", { title: schema.title }]] : [];
2210
+ for (const [key, node] of Object.entries(schema.properties)) {
2211
+ nodes.push([key, node]);
2212
+ if (isSettingsGroup(node)) for (const [child, field] of Object.entries(node.properties)) nodes.push([`${key}.${child}`, field]);
2213
+ }
2214
+ for (const [path, node] of nodes) for (const [label, key] of labelsOf(node)) {
2215
+ if (typeof key !== "string" || Object.hasOwn(messages, key)) continue;
2216
+ const suggested = screamingSnake([path || "settings", label === "title" ? "" : label].join("_"));
2217
+ issues.push({
2218
+ path,
2219
+ problem: `${label} ${JSON.stringify(key)} is not a key of ${SOURCE_MESSAGES}`,
2220
+ fix: `add "${key}" (or "${suggested.replace(/_$/, "")}") to ${SOURCE_MESSAGES}`
2221
+ });
2222
+ }
2223
+ }
2224
+ function schemaNodes(schema) {
2225
+ return Object.entries(schema.properties).flatMap(([key, node]) => [[key, node], ...isSettingsGroup(node) ? Object.entries(node.properties).map(([child, field]) => [`${key}.${child}`, field]) : []]);
2226
+ }
2227
+ function checkPredicates(schema, source, issues) {
2228
+ const lifted = new Set(source.predicates.map(({ path, option }) => `${path}.${option}`));
2229
+ const failed = new Set(source.problems.map(({ path }) => path));
2230
+ for (const [path, node] of schemaNodes(schema)) for (const option of ["visibleWhen", "disabledWhen"]) {
2231
+ const at = `${path}.${option}`;
2232
+ if (typeof node[option] === "object" && !lifted.has(at) && !failed.has(at)) issues.push({
2233
+ path: at,
2234
+ problem: "is a predicate the build cannot find",
2235
+ fix: "write the predicate inline in the builder options"
2236
+ });
2237
+ }
2238
+ }
2239
+ function checkActions(schema, actionsPath, issues) {
2240
+ const buttons = schemaNodes(schema).flatMap(([path, node]) => !isSettingsGroup(node) && isButton(node) ? [[path, node.action]] : []);
2241
+ if (!buttons.length) return;
2242
+ if (!actionsPath) {
2243
+ issues.push({
2244
+ path: buttons[0]?.[0] ?? "",
2245
+ problem: "is a button, but the plugin has no src/actions.ts",
2246
+ fix: `create src/actions.ts and export function ${buttons[0]?.[1] ?? "action"}() from it`
2247
+ });
2248
+ return;
2249
+ }
2250
+ const exports = readActionExports(actionsPath, readFileSync(actionsPath, "utf8"));
2251
+ for (const [path, action] of buttons) {
2252
+ if (exports.get(action)) continue;
2253
+ issues.push({
2254
+ path,
2255
+ problem: exports.has(action) ? `runs "${action}", which src/actions.ts exports but not as a function` : `runs "${action}", which src/actions.ts does not export`,
2256
+ fix: `write \`export function ${action}() {}\` or \`export const ${action} = () => {}\` in src/actions.ts`
2257
+ });
2258
+ }
2259
+ }
2260
+ function formatIssue({ path, problem, fix, offset }, source) {
2261
+ let at = path;
2262
+ while (at && !source.offsets.has(at)) at = at.slice(0, Math.max(0, at.lastIndexOf(".")));
2263
+ const found = offset ?? source.offsets.get(at);
2264
+ const { line, column } = found === void 0 ? {
2265
+ line: 1,
2266
+ column: 1
2267
+ } : toLocation(source.starts, found);
2268
+ return diagnostic({
2269
+ file: SETTINGS_FILE,
2270
+ line,
2271
+ column,
2272
+ path: path || "settings",
2273
+ severity: "error",
2274
+ message: problem,
2275
+ fix,
2276
+ check: "settings"
2277
+ });
2278
+ }
2279
+ /**
2280
+ * @description Reads a plugin's `settings.ts`: evaluates it, validates the schema, lifts its predicates and checks every label against en-US.
2281
+ * @param pluginRoot The plugin's directory.
2282
+ * @returns The settings with every problem formatted, or `undefined` when the plugin has no settings.ts.
2283
+ */
2284
+ async function readPluginSettings(pluginRoot) {
2285
+ const path = settingsPath(pluginRoot);
2286
+ if (!existsSync(path)) return;
2287
+ const source = readSettingsSource(path, readFileSync(path, "utf8"));
2288
+ const issues = source.problems.map(({ path: at, offset, problem, fix }) => ({
2289
+ path: at,
2290
+ offset,
2291
+ problem,
2292
+ fix
2293
+ }));
2294
+ const messages = readMessages(pluginRoot, issues);
2295
+ let schema = {
2296
+ kind: "settings",
2297
+ properties: {}
2298
+ };
2299
+ let watchFiles = [path];
2300
+ try {
2301
+ const evaluated = await evaluateSettings(path);
2302
+ const result = readSettingsSchema(evaluated.value);
2303
+ ({schema} = result);
2304
+ watchFiles = evaluated.watchFiles;
2305
+ issues.push(...result.issues);
2306
+ } catch (error) {
2307
+ issues.push({
2308
+ path: "",
2309
+ problem: `failed to evaluate: ${error instanceof Error ? error.message : String(error)}`,
2310
+ fix: "fix that error"
2311
+ });
2312
+ }
2313
+ if (Object.keys(messages).length) checkKeys(schema, messages, issues);
2314
+ const actionsPath = findFile(join(pluginRoot, "src", "actions")) ?? void 0;
2315
+ checkPredicates(schema, source, issues);
2316
+ checkActions(schema, actionsPath, issues);
2317
+ return {
2318
+ schema,
2319
+ messages,
2320
+ predicates: source.predicates,
2321
+ actionsPath,
2322
+ issues: issues.map((issue) => formatIssue(issue, source)),
2323
+ watchFiles
2324
+ };
2325
+ }
2326
+ // src/rollup/settings-typings.ts
2327
+ /**
2328
+ * Where `ubd build` and `ubd dev` write the ambient typings of {@link SETTINGS_MODULE} and {@link SETTINGS_SCHEMA_MODULE}.
2329
+ * It sits in `src/`, which every plugin tsconfig includes, beside the `@unbound-app/i18n` typings.
2330
+ */
2331
+ const SETTINGS_TYPINGS = "src/unbound-settings.d.ts";
2332
+ const HEADER = "// Generated by ubd from settings.ts and i18n/en-US.json. Do not edit: `ubd build` and `ubd dev` rewrite it.";
2333
+ const REFINED_TYPES = [
2334
+ " /** A `color` setting: `#RRGGBB`, or `#RRGGBBAA` with `alpha`. */",
2335
+ " export type HexColor = import('@unbound-app/api/types').HexColor;",
2336
+ " /** A `url` setting: an http or https URL. */",
2337
+ " export type HttpUrl = import('@unbound-app/api/types').HttpUrl;",
2338
+ " /** A Discord id, as an entity setting holds it. */",
2339
+ " export type Snowflake = import('@unbound-app/api/types').Snowflake;",
2340
+ " /** An `emoji` setting: a Unicode emoji or a custom emoji's name, then `:` and its id when custom. */",
2341
+ " export type EmojiString = import('@unbound-app/api/types').EmojiString;"
2342
+ ].join("\n");
2343
+ const UTILS = `import('${SETTINGS_MODULE}')`;
2344
+ const SCHEMA_TYPINGS = `declare module '${SETTINGS_SCHEMA_MODULE}' {
2345
+ /** A message key of this plugin's i18n/en-US.json. */
2346
+ export type MessageKey = import('@unbound-app/i18n').MessageKey;
2347
+ type HexColor = ${UTILS}.HexColor;
2348
+ type HttpUrl = ${UTILS}.HttpUrl;
2349
+ type Snowflake = ${UTILS}.Snowflake;
2350
+ type EmojiString = ${UTILS}.EmojiString;
2351
+ type Settings = import('${SETTINGS_MODULE}').Settings;
2352
+ type ActionName = import('${SETTINGS_MODULE}').ActionName;
2353
+ type Never<K extends string> = { [P in K]?: never };
2354
+
2355
+ const stored: unique symbol;
2356
+ const item: unique symbol;
2357
+ const section: unique symbol;
2358
+ const button: unique symbol;
2359
+ const notice: unique symbol;
2360
+ const definition: unique symbol;
2361
+
2362
+ /** A stored setting, from one of the \`setting\` builders. */
2363
+ export type Setting = { readonly [stored]: true };
2364
+ /** An \`array\` item, from \`setting.string()\` or \`setting.choice({ options })\`; \`T\` is its value. */
2365
+ export type Item<T> = { readonly [item]: T };
2366
+ /** A section from \`setting.section\`. */
2367
+ export type Section = { readonly [section]: true };
2368
+ /** A button row from \`setting.button\`; it stores nothing. */
2369
+ export type Button = { readonly [button]: true };
2370
+ /** A notice row from \`setting.notice\`; it stores nothing. */
2371
+ export type Notice = { readonly [notice]: true };
2372
+ /** What \`defineSettings\` returns: the default export of settings.ts. */
2373
+ export type SettingsDefinition = { readonly [definition]: true };
2374
+
2375
+ /** Shows the row only while it holds: a predicate of every setting, defaults applied, or a boolean setting's key. */
2376
+ export type Condition = ((settings: Settings) => boolean) | import('${SETTINGS_MODULE}').BooleanSettingKey;
2377
+
2378
+ /** What every setting, section and button takes. A hidden setting needs no title. */
2379
+ export type Common = {
2380
+ /** The row's sub-label. */
2381
+ subtitle?: MessageKey;
2382
+ visibleWhen?: Condition;
2383
+ disabledWhen?: Condition;
2384
+ /** Changing it shows a hint to restart Discord. */
2385
+ requiresRestart?: boolean;
2386
+ } & (
2387
+ | { /** Stored and typed, but not drawn. */ hidden: true; title?: MessageKey }
2388
+ | { hidden?: false; /** The row's label or the section's title. */ title: MessageKey }
2389
+ );
2390
+
2391
+ type Range = { min?: number; max?: number };
2392
+ type NumberDisplay<Step> =
2393
+ | ({ displayAs?: 'input' } & Range & Never<'step' | 'units'>)
2394
+ | ({ /** The default when \`min\` and \`max\` are set. */ displayAs?: 'slider'; min: number; max: number; step?: number } & Never<'units'>)
2395
+ | ({ displayAs: 'stepper' } & Range & Step & Never<'units'>)
2396
+ | ({ displayAs: 'duration'; /** The units offered, smallest first. */ units?: ('ms' | 's' | 'm' | 'h' | 'd')[] } & Range & Never<'step'>);
2397
+
2398
+ type TextOptions = { minLength?: number; maxLength?: number; placeholder?: MessageKey };
2399
+ type ColorOptions = { alpha?: boolean; default: HexColor } & (
2400
+ | { palette?: false; presets?: HexColor[] }
2401
+ | { /** Draws \`presets\` inline. */ palette: true; presets: [HexColor, ...HexColor[]] }
2402
+ );
2403
+ type StringDisplay =
2404
+ | ({ displayAs?: 'input' | 'multiline'; default: string } & TextOptions & Never<'alpha' | 'presets' | 'palette' | 'maxSize'>)
2405
+ | ({ displayAs: 'secret'; default: string; placeholder?: MessageKey } & Never<'minLength' | 'maxLength' | 'alpha' | 'presets' | 'palette' | 'maxSize'>)
2406
+ | ({ displayAs: 'url'; default: HttpUrl | ''; placeholder?: MessageKey } & Never<'minLength' | 'maxLength' | 'alpha' | 'presets' | 'palette' | 'maxSize'>)
2407
+ | ({ displayAs: 'color' } & ColorOptions & Never<'minLength' | 'maxLength' | 'placeholder' | 'maxSize'>)
2408
+ | ({ displayAs: 'image'; default?: string; /** The largest file, in bytes. */ maxSize?: number } & Never<'minLength' | 'maxLength' | 'placeholder' | 'alpha' | 'presets' | 'palette'>)
2409
+ | ({ displayAs: 'font'; default?: string } & Never<'minLength' | 'maxLength' | 'placeholder' | 'alpha' | 'presets' | 'palette' | 'maxSize'>)
2410
+ | ({ /** \`name\`, or \`name:id\` for a custom emoji. */ displayAs: 'emoji'; default?: EmojiString } & Never<'minLength' | 'maxLength' | 'placeholder' | 'alpha' | 'presets' | 'palette' | 'maxSize'>)
2411
+ | ({ displayAs: 'user' | 'channel' | 'guild' | 'role'; default?: Snowflake | '' } & Never<'minLength' | 'maxLength' | 'placeholder' | 'alpha' | 'presets' | 'palette' | 'maxSize'>);
2412
+
2413
+ /** An option's label: a message key, or a title and a subtitle a radio group shows under it. */
2414
+ export type OptionLabel = MessageKey | { title: MessageKey; subtitle?: MessageKey };
2415
+ type Options = Record<string, OptionLabel>;
2416
+
2417
+ /** The builders, one per stored primitive; \`displayAs\` picks the widget. */
2418
+ export const setting: {
2419
+ /** A \`boolean\`: a switch, or a checkbox. */
2420
+ boolean(options: Common & { default: boolean; displayAs?: 'switch' | 'checkbox' }): Setting;
2421
+ /** A \`number\`: an input; a slider with \`min\` and \`max\`; a stepper, which needs \`step\`; or a duration in milliseconds. */
2422
+ number(options: Common & { default: number } & NumberDisplay<{ step: number }>): Setting;
2423
+ /** A whole \`number\`: an input; a slider with \`min\` and \`max\`; a stepper; or a duration in milliseconds. */
2424
+ integer(options: Common & { default: number } & NumberDisplay<{ step?: number }>): Setting;
2425
+ /** A \`string\` item of an \`array\`. */
2426
+ string(): Item<string>;
2427
+ /** A \`string\`: an input, a multiline input, a secret, a URL, a color, an image, a font, an emoji or a Discord entity. */
2428
+ string(options: Common & StringDisplay): Setting;
2429
+ /** A choice item of an \`array\`. */
2430
+ choice<const O extends Options>(options: { options: O }): Item<keyof O & string>;
2431
+ /** One of \`options\`' keys: a radio group (up to 5 options), a picker sheet (more), or a segmented control (up to 4). */
2432
+ choice<const O extends Options>(
2433
+ options: Common & { options: O; default: NoInfer<keyof O & string>; displayAs?: 'radio' | 'picker' | 'segmented' },
2434
+ ): Setting;
2435
+ /** A list of \`of\`: strings as an editable list or a Discord entity picker, choices as checkboxes. */
2436
+ array<const T>(
2437
+ options: Common & { of: Item<T>; default?: NoInfer<T>[]; minItems?: number; maxItems?: number } & (string extends T
2438
+ ? { displayAs?: 'list' | 'user' | 'channel' | 'guild' | 'role' }
2439
+ : { displayAs?: 'checkboxes' }),
2440
+ ): Setting;
2441
+ /** A section of settings, titled by \`title\`. */
2442
+ section(
2443
+ options: Common & { displayAs?: 'group' | 'collapsible' },
2444
+ properties: Record<string, Setting | Button | Notice>,
2445
+ ): Section;
2446
+ /** A button row that calls \`action\`, a function src/actions.ts exports, asking \`confirm\` first when set. */
2447
+ button(
2448
+ options: Common & { action: ActionName; confirm?: MessageKey; displayAs?: 'button' | 'danger' },
2449
+ ): Button;
2450
+ /** A message row, shown while \`visibleWhen\` holds; it stores nothing. \`tone\` is \`info\` unless set. */
2451
+ notice(options: {
2452
+ title: MessageKey;
2453
+ subtitle?: MessageKey;
2454
+ tone?: 'info' | 'warning' | 'danger';
2455
+ visibleWhen?: Condition;
2456
+ }): Notice;
2457
+ };
2458
+
2459
+ /** Declares the plugin's settings; \`title\` names the section holding the top-level ones. */
2460
+ export function defineSettings(
2461
+ properties: Record<string, Setting | Section | Button | Notice>,
2462
+ options?: { title?: MessageKey },
2463
+ ): SettingsDefinition;
2464
+ }`;
2465
+ function literalUnion(values) {
2466
+ return values.length ? values.map((value) => JSON.stringify(value)).join(" | ") : "never";
2467
+ }
2468
+ function stringType(displayAs, empty) {
2469
+ const unset = empty ? " | ''" : "";
2470
+ switch (displayAs) {
2471
+ case "color": return `${UTILS}.HexColor`;
2472
+ case "url": return `${UTILS}.HttpUrl${unset}`;
2473
+ case "emoji": return `${UTILS}.EmojiString`;
2474
+ case "user":
2475
+ case "channel":
2476
+ case "guild":
2477
+ case "role": return `${UTILS}.Snowflake${unset}`;
2478
+ default: return "string";
2479
+ }
2480
+ }
2481
+ /**
2482
+ * @description Writes the TypeScript type a setting's value has, narrowed by its display.
2483
+ * @param field The setting.
2484
+ * @returns The type's source.
2485
+ */
2486
+ function settingType(field) {
2487
+ switch (field.kind) {
2488
+ case "boolean": return "boolean";
2489
+ case "string": return stringType(field.displayAs, true);
2490
+ case "choice": return literalUnion(field.options.map((choice) => choice.const));
2491
+ case "array": return `(${field.of.kind === "choice" ? literalUnion(field.of.options.map((choice) => choice.const)) : stringType(field.displayAs, false)})[]`;
2492
+ default: return "number";
2493
+ }
2494
+ }
2495
+ function comment(labels, messages, indent) {
2496
+ const lines = [labels.title, labels.subtitle].filter((key) => key !== void 0).map((key) => (messages[key] ?? key).replaceAll("*/", "*\\/").replaceAll("\n", " "));
2497
+ if (!lines.length) return [];
2498
+ return [
2499
+ `${indent}/**`,
2500
+ ...lines.flatMap((line, index) => index ? [`${indent} *`, `${indent} * ${line}`] : [`${indent} * ${line}`]),
2501
+ `${indent} */`
2502
+ ];
2503
+ }
2504
+ function key(name) {
2505
+ return /^[A-Za-z_$][\w$]*$/.test(name) ? name : JSON.stringify(name);
2506
+ }
2507
+ const ACTION_NAME = "Extract<{ [K in keyof Actions]: Actions[K] extends () => void | Promise<void> ? K : never }[keyof Actions], string>";
2508
+ /**
2509
+ * @description Writes the ambient typings of the builders and of a plugin's settings, each property documented with its en-US title and subtitle.
2510
+ * @param schema The plugin's validated settings schema.
2511
+ * @param messages The plugin's en-US messages.
2512
+ * @param hasActions Whether the plugin has a `src/actions.ts`, whose exports name `ActionName`.
2513
+ * @returns The `.d.ts` source.
2514
+ */
2515
+ function settingsTypings(schema, messages, hasActions = false) {
2516
+ const shape = [];
2517
+ const paths = [];
2518
+ const booleans = [];
2519
+ function addField(path, name, field, indent) {
2520
+ const type = settingType(field);
2521
+ shape.push(...comment(field, messages, indent), `${indent}${key(name)}: ${type};`);
2522
+ paths.push(...comment(field, messages, " "), `\t\t${JSON.stringify(path)}: ${type};`);
2523
+ if (field.kind === "boolean") booleans.push(JSON.stringify(path));
2524
+ }
2525
+ for (const [name, node] of Object.entries(schema.properties)) {
2526
+ if (!isSettingsGroup(node)) {
2527
+ if (isStored(node)) addField(name, name, node, " ");
2528
+ continue;
2529
+ }
2530
+ shape.push(...comment(node, messages, " "), `\t\treadonly ${key(name)}: {`);
2531
+ paths.push(...comment(node, messages, " "), `\t\t${JSON.stringify(name)}: Settings[${JSON.stringify(name)}];`);
2532
+ for (const [child, field] of Object.entries(node.properties)) if (isStored(field)) addField(`${name}.${child}`, child, field, " ");
2533
+ shape.push(" };");
2534
+ }
2535
+ return [
2536
+ HEADER,
2537
+ "",
2538
+ SCHEMA_TYPINGS,
2539
+ "",
2540
+ `declare module '${SETTINGS_MODULE}' {`,
2541
+ REFINED_TYPES,
2542
+ "",
2543
+ " /** The plugin's settings: reads apply defaults, and writes are validated and saved to its Bucket. */",
2544
+ `\texport type Settings = {\n${shape.join("\n")}\n\t};`,
2545
+ "",
2546
+ " /** Each setting and section by its dot-separated key. */",
2547
+ `\ttype SettingValues = {\n${paths.join("\n")}\n\t};`,
2548
+ "",
2549
+ " /** Every setting and section by its dot-separated key; buttons and notices are not settings. */",
2550
+ " export type SettingKey = keyof SettingValues;",
2551
+ " /** The value of the setting or section at `K`. */",
2552
+ " export type SettingsEntry<K extends SettingKey> = SettingValues[K];",
2553
+ "",
2554
+ " /** The functions src/actions.ts exports, which a `setting.button` can run. */",
2555
+ hasActions ? `\ttype Actions = typeof import('./actions');\n\texport type ActionName = ${ACTION_NAME};` : " export type ActionName = never;",
2556
+ "",
2557
+ " /** The keys of the boolean settings, which `visibleWhen` and `disabledWhen` can name. */",
2558
+ `\texport type BooleanSettingKey = ${booleans.length ? booleans.join(" | ") : "never"};`,
2559
+ "",
2560
+ " /** The plugin's settings. Assign a property, a section's too, to save it. */",
2561
+ " export const settings: Settings;",
2562
+ " /** Reads a setting or a whole section, and re-renders the component when it changes. */",
2563
+ " export function useSetting<K extends SettingKey>(key: K): SettingsEntry<K>;",
2564
+ " /** Puts every setting back to its default. */",
2565
+ " export function resetSettings(): void;",
2566
+ " /** The settings panel generated from settings.ts; a custom `getSettingsPanel` can render it too. */",
2567
+ " export function SettingsPanel(): import('react').ReactElement;",
2568
+ "}",
2569
+ ""
2570
+ ].join("\n");
2571
+ }
2572
+ /**
2573
+ * @description Rewrites `src/unbound-settings.d.ts`, leaving the file alone when it is current.
2574
+ * @param pluginRoot The plugin's directory.
2575
+ * @param typings The `.d.ts` source from {@link settingsTypings}.
2576
+ * @returns Whether the file changed.
2577
+ */
2578
+ function writeSettingsTypings(pluginRoot, typings) {
2579
+ const path = join(pluginRoot, SETTINGS_TYPINGS);
2580
+ if (existsSync(path) && readFileSync(path, "utf8") === typings) return false;
2581
+ mkdirSync(dirname(path), { recursive: true });
2582
+ writeFileSync(path, typings);
2583
+ return true;
2584
+ }
2585
+ export { ASSET_NAME_PATTERN as $, readIcuMessage as A, reportProblem as B, readLocaleFolder as C, reportLine as D, writeLocaleTypings as E, formatDiagnostic as F, hermesBytecode as G, withoutFullStop as H, isDiagnostic as I, LOCALES_DIRECTORY as J, hermescCount as K, pluginDiagnostic as L, findFile as M, sourceAlias as N, buildReportSchema as O, diagnostic as P, ASSETS_DIRECTORY as Q, reportDiagnostic as R, locales as S, sourceLocalePath as T, bytecodeFileName as U, sortDiagnostics as V, bytecodeSource as W, SOURCE_LOCALE as X, LOCALE_CODE_PATTERN as Y, isLocaleCode as Z, formatBytes as _, SETTINGS_MODULE as a, __commonJSMin as at, LOCALE_TYPINGS as b, settingsPath as c, __require as ct, OUTPUT_ONLY as d, ASSET_SCALES as et, outputOnly as f, shim_libraries_default as g, buildReport as h, SETTINGS_FILE as i, ADDON_KINDS as it, IcuSyntaxError as j, readIcuArguments as k, lineStarts as l, __toCommonJS as lt, DEFAULT_BUDGET as m, settingsTypings as n, getAssetPath as nt, SETTINGS_SCHEMA_MODULE as o, __esmMin as ot, BUILD_REPORT_ENV as p, listAddonFiles as q, writeSettingsTypings as r, isAssetName as rt, readPluginSettings as s, __exportAll as st, SETTINGS_TYPINGS as t, ASSET_TYPES as tt, toLocation as u, __toESM as ut, parseByteSize as v, readTable as w, localeModule as x, LOCALE_MODULE as y, reportDiagnostics as z };