@triflare/fx 1.0.3 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -0
- package/bin/fx.js +4 -1162
- package/package.json +5 -3
- package/product.yml +1 -1
- package/src/brand.js +9 -3
- package/src/cli/commands/build.js +98 -0
- package/src/cli/commands/catalog.js +42 -0
- package/src/cli/commands/check.js +44 -0
- package/src/cli/commands/completion.js +54 -0
- package/src/cli/commands/decompile.js +266 -0
- package/src/cli/commands/dev.js +179 -0
- package/src/cli/commands/extension.js +110 -0
- package/src/cli/commands/index.js +39 -0
- package/src/cli/commands/init.js +169 -0
- package/src/cli/commands/package.js +137 -0
- package/src/cli/commands/sprite/add.js +85 -0
- package/src/cli/commands/sprite/build.js +62 -0
- package/src/cli/commands/sprite/helpers.js +66 -0
- package/src/cli/commands/sprite/index.js +29 -0
- package/src/cli/commands/sprite/list.js +31 -0
- package/src/cli/commands/sprite/remove.js +51 -0
- package/src/cli/commands/vendor/add.js +98 -0
- package/src/cli/commands/vendor/helpers.js +50 -0
- package/src/cli/commands/vendor/index.js +39 -0
- package/src/cli/commands/vendor/install.js +64 -0
- package/src/cli/commands/vendor/list.js +45 -0
- package/src/cli/commands/vendor/migrate.js +47 -0
- package/src/cli/commands/vendor/outdated.js +51 -0
- package/src/cli/commands/vendor/remove.js +34 -0
- package/src/cli/commands/vendor/update.js +49 -0
- package/src/cli/commands/vendor/verify.js +44 -0
- package/src/cli/completion/bash.js +10 -0
- package/src/cli/completion/fish.js +9 -0
- package/src/cli/completion/index.js +97 -0
- package/src/cli/completion/zsh.js +11 -0
- package/src/cli/context.js +257 -0
- package/src/cli/define-command.js +140 -0
- package/src/cli/errors.js +111 -0
- package/src/cli/format.js +31 -0
- package/src/cli/prompts.js +183 -0
- package/src/cli/reporter.js +194 -0
- package/src/cli/run.js +248 -0
- package/src/cli/templates/index.js +112 -0
- package/src/cli/theme.js +84 -0
- package/src/config-edit.js +280 -1
- package/src/config.js +137 -3
- package/src/extensions.js +72 -19
- package/src/index.js +21 -0
- package/src/vendor.js +1323 -0
- package/vendor/pm-packager-dist/.fx-build.json +1 -1
package/src/config-edit.js
CHANGED
|
@@ -54,7 +54,8 @@ function childKey(lines, start, end, key) {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
function childIndent(lines, start, end) {
|
|
57
|
-
|
|
57
|
+
const limit = Math.min(end, lines.length);
|
|
58
|
+
for (let index = start + 1; index < limit; index++) {
|
|
58
59
|
const match = /^([ \t]+)\S/.exec(content(lines[index]));
|
|
59
60
|
if (match) return match[1];
|
|
60
61
|
}
|
|
@@ -336,3 +337,281 @@ export function removeProjectExtension(configPath, target) {
|
|
|
336
337
|
unsandboxedRemoved: unsandboxedRemoved.length > 0,
|
|
337
338
|
};
|
|
338
339
|
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Remove `project.extensions` entirely (used by migration).
|
|
343
|
+
*
|
|
344
|
+
* Unlike {@link removeProjectExtension}, this drops the key itself instead of
|
|
345
|
+
* leaving an empty list, so a fully migrated file has no legacy section left.
|
|
346
|
+
*/
|
|
347
|
+
export function removeProjectExtensions(configPath) {
|
|
348
|
+
const text = fs.readFileSync(configPath, "utf8");
|
|
349
|
+
const lines = splitLines(text);
|
|
350
|
+
const project = topLevel(lines, "project");
|
|
351
|
+
if (!project) return [];
|
|
352
|
+
const { value: rest, comment } = splitComment(project.rest);
|
|
353
|
+
|
|
354
|
+
if (rest.startsWith("{")) {
|
|
355
|
+
if (!rest.endsWith("}"))
|
|
356
|
+
throw new Error("project must be a mapping to edit it");
|
|
357
|
+
const mapping = parseValue(rest);
|
|
358
|
+
if (!mapping || typeof mapping !== "object" || Array.isArray(mapping))
|
|
359
|
+
throw new Error("project must be a mapping to edit it");
|
|
360
|
+
if (!Object.hasOwn(mapping, "extensions")) return [];
|
|
361
|
+
|
|
362
|
+
const removed = stringList(mapping.extensions);
|
|
363
|
+
delete mapping.extensions;
|
|
364
|
+
const line = lines[project.index];
|
|
365
|
+
lines[project.index] =
|
|
366
|
+
`project: ${JSON.stringify(mapping)}` +
|
|
367
|
+
`${comment ? ` ${comment}` : ""}${ending(line) || eolOf(text)}`;
|
|
368
|
+
writeDocument(configPath, lines.join(""));
|
|
369
|
+
return removed;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const bounds = projectBounds(lines);
|
|
373
|
+
if (!bounds) return [];
|
|
374
|
+
const list = readList(lines, "extensions", bounds.index, bounds.end);
|
|
375
|
+
if (!list) return [];
|
|
376
|
+
|
|
377
|
+
const removed = [...list.values];
|
|
378
|
+
if (list.style === "block") {
|
|
379
|
+
const end = Math.max(list.keyLine.index, list.lastLine) + 1;
|
|
380
|
+
lines.splice(list.keyLine.index, end - list.keyLine.index);
|
|
381
|
+
} else {
|
|
382
|
+
lines.splice(list.keyLine.index, 1);
|
|
383
|
+
}
|
|
384
|
+
writeDocument(configPath, lines.join(""));
|
|
385
|
+
return removed;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// --------------------------------------------------------------- vendor edits
|
|
389
|
+
|
|
390
|
+
const SAFE_KEY = /^[A-Za-z0-9_][A-Za-z0-9_.-]*$/;
|
|
391
|
+
|
|
392
|
+
/** Quote a mapping key when YAML would otherwise parse it as non-text. */
|
|
393
|
+
function quoteKey(key) {
|
|
394
|
+
// YAML 1.1 treats bare yes/no/on/off/null as non-strings.
|
|
395
|
+
return SAFE_KEY.test(key) &&
|
|
396
|
+
!/^(?:y|yes|n|no|on|off|null|true|false)$/i.test(key)
|
|
397
|
+
? key
|
|
398
|
+
: JSON.stringify(String(key));
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** Serialize one vendor entry, expanding options only when needed. */
|
|
402
|
+
function vendorEntryLines(name, source, options, indent, eol) {
|
|
403
|
+
const extra = [];
|
|
404
|
+
if (options.unsandboxed) extra.push(`${indent} unsandboxed: true${eol}`);
|
|
405
|
+
if (options.volatile !== undefined)
|
|
406
|
+
extra.push(`${indent} volatile: ${Boolean(options.volatile)}${eol}`);
|
|
407
|
+
if (extra.length === 0)
|
|
408
|
+
return [`${indent}${quoteKey(name)}: ${quote(source)}${eol}`];
|
|
409
|
+
return [
|
|
410
|
+
`${indent}${quoteKey(name)}:${eol}`,
|
|
411
|
+
`${indent} url: ${quote(source)}${eol}`,
|
|
412
|
+
...extra,
|
|
413
|
+
];
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** Locate a top-level mapping, accepting an inline flow mapping. */
|
|
417
|
+
function topLevelBounds(lines, key) {
|
|
418
|
+
const found = topLevel(lines, key);
|
|
419
|
+
if (!found) return null;
|
|
420
|
+
const { value: rest, comment } = splitComment(found.rest);
|
|
421
|
+
if (rest !== "") {
|
|
422
|
+
// A flow mapping can be expanded losslessly (comments on the line aside).
|
|
423
|
+
if (rest.startsWith("{") && rest.endsWith("}")) {
|
|
424
|
+
const mapping = parseValue(rest);
|
|
425
|
+
if (mapping && typeof mapping === "object" && !Array.isArray(mapping))
|
|
426
|
+
return {
|
|
427
|
+
index: found.index,
|
|
428
|
+
end: found.index + 1,
|
|
429
|
+
inline: rest,
|
|
430
|
+
comment,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
throw new Error(`${key} must be a block mapping to edit it`);
|
|
434
|
+
}
|
|
435
|
+
return {
|
|
436
|
+
index: found.index,
|
|
437
|
+
end: sectionEnd(lines, found.index),
|
|
438
|
+
inline: null,
|
|
439
|
+
comment,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** Find direct child entries and their line ranges in a mapping. */
|
|
444
|
+
function mappingEntrySpans(lines, start, end) {
|
|
445
|
+
const indent = childIndent(lines, start, end);
|
|
446
|
+
const spans = [];
|
|
447
|
+
const limit = Math.min(end, lines.length);
|
|
448
|
+
for (let index = start + 1; index < limit; index++) {
|
|
449
|
+
const text = content(lines[index]);
|
|
450
|
+
if (text.trim() === "" || /^[ \t]*#/.test(text)) continue;
|
|
451
|
+
const outer = /^([ \t]+)(.*)$/.exec(text);
|
|
452
|
+
if (!outer || outer[1] !== indent) continue;
|
|
453
|
+
const match = /^(.+?)[ \t]*:[ \t]*(.*)$/.exec(outer[2]);
|
|
454
|
+
if (!match) continue;
|
|
455
|
+
let key;
|
|
456
|
+
try {
|
|
457
|
+
const parsed = yaml.load(`${match[1]}: null`);
|
|
458
|
+
key =
|
|
459
|
+
parsed && typeof parsed === "object" ? Object.keys(parsed)[0] : null;
|
|
460
|
+
} catch {
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
if (key == null) continue;
|
|
464
|
+
spans.push({ index, indent, key: String(key), rest: match[2] });
|
|
465
|
+
}
|
|
466
|
+
for (let index = 0; index < spans.length; index++)
|
|
467
|
+
spans[index].end = index + 1 < spans.length ? spans[index + 1].index : end;
|
|
468
|
+
return spans;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/** Replace a half-open line range in place. */
|
|
472
|
+
function replaceRange(lines, start, end, added) {
|
|
473
|
+
lines.splice(start, end - start, ...added);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Add or replace a `vendor:` entry without reformatting the rest of fx.yml.
|
|
478
|
+
*
|
|
479
|
+
* @param {string} configPath
|
|
480
|
+
* @param {string} name
|
|
481
|
+
* @param {string} source
|
|
482
|
+
* @param {{unsandboxed?: boolean, volatile?: boolean}} [options]
|
|
483
|
+
* @returns {{added: boolean, updated: boolean}}
|
|
484
|
+
*/
|
|
485
|
+
export function addVendorEntry(configPath, name, source, options = {}) {
|
|
486
|
+
const text = fs.readFileSync(configPath, "utf8");
|
|
487
|
+
const lines = splitLines(text);
|
|
488
|
+
const eol = eolOf(text);
|
|
489
|
+
const key = String(name);
|
|
490
|
+
let added = false;
|
|
491
|
+
let updated = false;
|
|
492
|
+
|
|
493
|
+
const bounds = topLevelBounds(lines, "vendor");
|
|
494
|
+
if (!bounds) {
|
|
495
|
+
if (lines.length > 0 && !lines[lines.length - 1].endsWith("\n"))
|
|
496
|
+
lines[lines.length - 1] += eol;
|
|
497
|
+
lines.push(
|
|
498
|
+
`vendor:${eol}`,
|
|
499
|
+
...vendorEntryLines(key, source, options, " ", eol),
|
|
500
|
+
);
|
|
501
|
+
added = true;
|
|
502
|
+
} else if (bounds.inline) {
|
|
503
|
+
const mapping = parseValue(bounds.inline) || {};
|
|
504
|
+
updated = Object.hasOwn(mapping, key);
|
|
505
|
+
added = !updated;
|
|
506
|
+
mapping[key] =
|
|
507
|
+
options.unsandboxed || options.volatile !== undefined
|
|
508
|
+
? {
|
|
509
|
+
url: source,
|
|
510
|
+
...(options.unsandboxed ? { unsandboxed: true } : {}),
|
|
511
|
+
...(options.volatile !== undefined
|
|
512
|
+
? { volatile: Boolean(options.volatile) }
|
|
513
|
+
: {}),
|
|
514
|
+
}
|
|
515
|
+
: source;
|
|
516
|
+
const dumped = yaml
|
|
517
|
+
.dump({ vendor: mapping }, { lineWidth: -1, noRefs: true })
|
|
518
|
+
.split("\n")
|
|
519
|
+
.filter((line, index, all) => !(index === all.length - 1 && line === ""))
|
|
520
|
+
.map((line) => `${line}${eol}`);
|
|
521
|
+
if (bounds.comment) dumped[0] = `vendor: ${bounds.comment}${eol}`;
|
|
522
|
+
replaceRange(lines, bounds.index, bounds.index + 1, dumped);
|
|
523
|
+
} else {
|
|
524
|
+
const spans = mappingEntrySpans(lines, bounds.index, bounds.end);
|
|
525
|
+
const existing = spans.find((span) => span.key === key);
|
|
526
|
+
if (existing) {
|
|
527
|
+
replaceRange(
|
|
528
|
+
lines,
|
|
529
|
+
existing.index,
|
|
530
|
+
existing.end,
|
|
531
|
+
vendorEntryLines(key, source, options, existing.indent, eol),
|
|
532
|
+
);
|
|
533
|
+
updated = true;
|
|
534
|
+
} else {
|
|
535
|
+
const indent = childIndent(lines, bounds.index, bounds.end);
|
|
536
|
+
insertAt(
|
|
537
|
+
lines,
|
|
538
|
+
bounds.end,
|
|
539
|
+
vendorEntryLines(key, source, options, indent, eol),
|
|
540
|
+
eol,
|
|
541
|
+
);
|
|
542
|
+
added = true;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (added || updated) writeDocument(configPath, lines.join(""));
|
|
547
|
+
return { added, updated };
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Remove a `vendor:` entry.
|
|
552
|
+
* @param {string} configPath
|
|
553
|
+
* @param {string} name
|
|
554
|
+
* @returns {{removed: string|null}}
|
|
555
|
+
*/
|
|
556
|
+
export function removeVendorEntry(configPath, name) {
|
|
557
|
+
const text = fs.readFileSync(configPath, "utf8");
|
|
558
|
+
const lines = splitLines(text);
|
|
559
|
+
const eol = eolOf(text);
|
|
560
|
+
const key = String(name);
|
|
561
|
+
const bounds = topLevelBounds(lines, "vendor");
|
|
562
|
+
if (!bounds) return { removed: null };
|
|
563
|
+
|
|
564
|
+
if (bounds.inline) {
|
|
565
|
+
const mapping = parseValue(bounds.inline);
|
|
566
|
+
if (!mapping || typeof mapping !== "object" || Array.isArray(mapping))
|
|
567
|
+
return { removed: null };
|
|
568
|
+
if (!Object.hasOwn(mapping, key)) return { removed: null };
|
|
569
|
+
const source =
|
|
570
|
+
typeof mapping[key] === "string"
|
|
571
|
+
? mapping[key]
|
|
572
|
+
: (mapping[key]?.url ?? mapping[key]?.source ?? null);
|
|
573
|
+
delete mapping[key];
|
|
574
|
+
const rest = Object.keys(mapping).length
|
|
575
|
+
? ` ${JSON.stringify(mapping)}`
|
|
576
|
+
: " {}";
|
|
577
|
+
const comment = bounds.comment ? ` ${bounds.comment}` : "";
|
|
578
|
+
const line = lines[bounds.index];
|
|
579
|
+
lines[bounds.index] = `vendor:${rest}${comment}${
|
|
580
|
+
/\r?\n$/.test(line) ? ending(line) : eol
|
|
581
|
+
}`;
|
|
582
|
+
writeDocument(configPath, lines.join(""));
|
|
583
|
+
return { removed: source };
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const spans = mappingEntrySpans(lines, bounds.index, bounds.end);
|
|
587
|
+
const existing = spans.find((span) => span.key === key);
|
|
588
|
+
if (!existing) return { removed: null };
|
|
589
|
+
|
|
590
|
+
let source = null;
|
|
591
|
+
try {
|
|
592
|
+
const parsed = yaml.load(
|
|
593
|
+
[
|
|
594
|
+
content(lines[bounds.index]),
|
|
595
|
+
...lines.slice(existing.index, existing.end),
|
|
596
|
+
]
|
|
597
|
+
.join("\n")
|
|
598
|
+
.replace(/^vendor:/, "vendor:"),
|
|
599
|
+
);
|
|
600
|
+
const entry = parsed?.vendor?.[key];
|
|
601
|
+
source =
|
|
602
|
+
typeof entry === "string"
|
|
603
|
+
? entry
|
|
604
|
+
: (entry?.url ?? entry?.source ?? entry?.path ?? null);
|
|
605
|
+
} catch {
|
|
606
|
+
// The entry is removed either way.
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
replaceRange(lines, existing.index, existing.end, []);
|
|
610
|
+
const end = bounds.end - (existing.end - existing.index);
|
|
611
|
+
const remaining = mappingEntrySpans(lines, bounds.index, end);
|
|
612
|
+
if (remaining.length === 0) {
|
|
613
|
+
replaceRange(lines, bounds.index, end, [`vendor: {}${eol}`]);
|
|
614
|
+
}
|
|
615
|
+
writeDocument(configPath, lines.join(""));
|
|
616
|
+
return { removed: source ?? key };
|
|
617
|
+
}
|
package/src/config.js
CHANGED
|
@@ -31,8 +31,112 @@ export const DEFAULT_PROJECT = {
|
|
|
31
31
|
*/
|
|
32
32
|
export const DEFAULT_FX = {
|
|
33
33
|
partialSprites: false,
|
|
34
|
+
vendorDir: "extensions",
|
|
34
35
|
};
|
|
35
36
|
|
|
37
|
+
/** Default directory that receives vendored extension sources. */
|
|
38
|
+
export const DEFAULT_VENDOR_DIR = "extensions";
|
|
39
|
+
|
|
40
|
+
/** Source schemes that identify a remote extension. */
|
|
41
|
+
const REMOTE_SOURCE = /^https?:\/\//i;
|
|
42
|
+
const DATA_SOURCE = /^data:/i;
|
|
43
|
+
|
|
44
|
+
/** True for http(s) extension sources. */
|
|
45
|
+
function isRemoteSource(source) {
|
|
46
|
+
return REMOTE_SOURCE.test(String(source));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** True for embedded data-URI extension sources. */
|
|
50
|
+
function isDataSource(source) {
|
|
51
|
+
return DATA_SOURCE.test(String(source));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Localhost/loopback URLs can point at a development server that changes. */
|
|
55
|
+
export function isLocalhostSource(source) {
|
|
56
|
+
if (!isRemoteSource(source)) return false;
|
|
57
|
+
try {
|
|
58
|
+
const url = new URL(String(source));
|
|
59
|
+
const host = url.hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
60
|
+
return (
|
|
61
|
+
host === "localhost" ||
|
|
62
|
+
host.endsWith(".localhost") ||
|
|
63
|
+
host === "0.0.0.0" ||
|
|
64
|
+
host === "::1" ||
|
|
65
|
+
/^127\./.test(host)
|
|
66
|
+
);
|
|
67
|
+
} catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Normalize one `vendor:` entry. A string is shorthand for `{ url }`; a
|
|
74
|
+
* mapping may add `unsandboxed`, `volatile` and any future fields.
|
|
75
|
+
*
|
|
76
|
+
* @param {string} name
|
|
77
|
+
* @param {unknown} raw
|
|
78
|
+
* @returns {object}
|
|
79
|
+
*/
|
|
80
|
+
export function normalizeVendorEntry(name, raw) {
|
|
81
|
+
const key = String(name);
|
|
82
|
+
let source;
|
|
83
|
+
let options = {};
|
|
84
|
+
if (typeof raw === "string" || typeof raw === "number") {
|
|
85
|
+
source = String(raw);
|
|
86
|
+
} else if (isPlainObject(raw)) {
|
|
87
|
+
source = raw.url ?? raw.source ?? raw.path ?? raw.file;
|
|
88
|
+
options = { ...raw };
|
|
89
|
+
} else {
|
|
90
|
+
throw new Error(
|
|
91
|
+
`${configFilename()}: vendor.${key} must be a URL, path, or mapping`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
if (typeof source !== "string" || source.trim() === "")
|
|
95
|
+
throw new Error(
|
|
96
|
+
`${configFilename()}: vendor.${key} needs a "url" (or "source"/"path")`,
|
|
97
|
+
);
|
|
98
|
+
source = source.trim();
|
|
99
|
+
|
|
100
|
+
const remote = isRemoteSource(source);
|
|
101
|
+
const local = !remote && !isDataSource(source);
|
|
102
|
+
const unsandboxed =
|
|
103
|
+
options.unsandboxed === true ||
|
|
104
|
+
(typeof options.unsandboxed === "string" &&
|
|
105
|
+
options.unsandboxed.trim() !== "");
|
|
106
|
+
const volatile =
|
|
107
|
+
options.volatile == null
|
|
108
|
+
? remote && isLocalhostSource(source)
|
|
109
|
+
: Boolean(options.volatile);
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
...options,
|
|
113
|
+
name: key,
|
|
114
|
+
source,
|
|
115
|
+
url: source,
|
|
116
|
+
remote,
|
|
117
|
+
local,
|
|
118
|
+
data: isDataSource(source),
|
|
119
|
+
volatile,
|
|
120
|
+
unsandboxed,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Normalize the top-level `vendor:` mapping. Order is load order, so this is an
|
|
126
|
+
* array rather than a plain object.
|
|
127
|
+
*
|
|
128
|
+
* @param {unknown} raw
|
|
129
|
+
* @returns {object[]}
|
|
130
|
+
*/
|
|
131
|
+
export function normalizeVendor(raw = {}) {
|
|
132
|
+
if (raw == null) return [];
|
|
133
|
+
if (!isPlainObject(raw))
|
|
134
|
+
throw new Error(`${configFilename()}: "vendor" must be a mapping`);
|
|
135
|
+
return Object.entries(raw).map(([name, entry]) =>
|
|
136
|
+
normalizeVendorEntry(name, entry),
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
36
140
|
/**
|
|
37
141
|
* Packaging defaults. These never reach PenguinMod itself; they are handed to
|
|
38
142
|
* the PenguinMod Packager when `fx package` or `fx dev` runs.
|
|
@@ -281,6 +385,11 @@ export function normalizeFx(raw = {}) {
|
|
|
281
385
|
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
282
386
|
if (raw.partialSprites != null)
|
|
283
387
|
fx.partialSprites = Boolean(raw.partialSprites);
|
|
388
|
+
if (raw.vendorDir != null) {
|
|
389
|
+
if (typeof raw.vendorDir !== "string" || raw.vendorDir.trim() === "")
|
|
390
|
+
throw new Error(`${configFilename()}: fx.vendorDir must be a path`);
|
|
391
|
+
fx.vendorDir = raw.vendorDir.trim();
|
|
392
|
+
}
|
|
284
393
|
}
|
|
285
394
|
return fx;
|
|
286
395
|
}
|
|
@@ -288,7 +397,7 @@ export function normalizeFx(raw = {}) {
|
|
|
288
397
|
/**
|
|
289
398
|
* Parse a YAML config string.
|
|
290
399
|
* @param {string} text
|
|
291
|
-
* @returns {{project: typeof DEFAULT_PROJECT, fx: typeof DEFAULT_FX, raw: object}}
|
|
400
|
+
* @returns {{project: typeof DEFAULT_PROJECT, fx: typeof DEFAULT_FX, package: typeof DEFAULT_PACKAGE, vendor: object[], vendorDir: string, raw: object}}
|
|
292
401
|
*/
|
|
293
402
|
export function parseConfig(text) {
|
|
294
403
|
const raw = yaml.load(text) || {};
|
|
@@ -298,10 +407,20 @@ export function parseConfig(text) {
|
|
|
298
407
|
);
|
|
299
408
|
if (raw.fx != null && (typeof raw.fx !== "object" || Array.isArray(raw.fx)))
|
|
300
409
|
throw new Error(`${configFilename()} must contain an "fx" mapping`);
|
|
410
|
+
const fx = normalizeFx(raw.fx || {});
|
|
411
|
+
// `vendorDir` is documented at the top level, but accepting it under `fx:`
|
|
412
|
+
// keeps older drafts and long configs working.
|
|
413
|
+
if (raw.vendorDir != null) {
|
|
414
|
+
if (typeof raw.vendorDir !== "string" || raw.vendorDir.trim() === "")
|
|
415
|
+
throw new Error(`${configFilename()}: vendorDir must be a path`);
|
|
416
|
+
fx.vendorDir = raw.vendorDir.trim();
|
|
417
|
+
}
|
|
301
418
|
return {
|
|
302
419
|
project: normalizeProject(raw.project || {}),
|
|
303
|
-
fx
|
|
420
|
+
fx,
|
|
304
421
|
package: normalizePackage(raw.package || {}),
|
|
422
|
+
vendor: normalizeVendor(raw.vendor || {}),
|
|
423
|
+
vendorDir: fx.vendorDir,
|
|
305
424
|
raw,
|
|
306
425
|
};
|
|
307
426
|
}
|
|
@@ -339,14 +458,26 @@ export function loadConfigFrom(dir) {
|
|
|
339
458
|
project,
|
|
340
459
|
fx,
|
|
341
460
|
package: pkg,
|
|
461
|
+
vendor,
|
|
462
|
+
vendorDir,
|
|
342
463
|
raw,
|
|
343
464
|
} = parseConfig(fs.readFileSync(found, "utf8"));
|
|
344
|
-
return {
|
|
465
|
+
return {
|
|
466
|
+
project,
|
|
467
|
+
fx,
|
|
468
|
+
package: pkg,
|
|
469
|
+
vendor,
|
|
470
|
+
vendorDir,
|
|
471
|
+
raw,
|
|
472
|
+
path: found,
|
|
473
|
+
};
|
|
345
474
|
}
|
|
346
475
|
return {
|
|
347
476
|
project: { ...DEFAULT_PROJECT },
|
|
348
477
|
fx: { ...DEFAULT_FX },
|
|
349
478
|
package: normalizePackage({}),
|
|
479
|
+
vendor: [],
|
|
480
|
+
vendorDir: DEFAULT_VENDOR_DIR,
|
|
350
481
|
raw: {},
|
|
351
482
|
path: null,
|
|
352
483
|
};
|
|
@@ -394,7 +525,10 @@ export function configComment(project) {
|
|
|
394
525
|
|
|
395
526
|
export {
|
|
396
527
|
addProjectExtension,
|
|
528
|
+
addVendorEntry,
|
|
397
529
|
extensionKey,
|
|
398
530
|
readProjectExtensions,
|
|
399
531
|
removeProjectExtension,
|
|
532
|
+
removeProjectExtensions,
|
|
533
|
+
removeVendorEntry,
|
|
400
534
|
} from "./config-edit.js";
|
package/src/extensions.js
CHANGED
|
@@ -757,6 +757,7 @@ const BASE64_PAYLOAD = /^[A-Za-z0-9+/=\s]*$/;
|
|
|
757
757
|
* @param {string} source
|
|
758
758
|
* @returns {string}
|
|
759
759
|
*/
|
|
760
|
+
/** Encode extension source as a self-contained JavaScript data URI. */
|
|
760
761
|
export function extensionDataUri(source) {
|
|
761
762
|
const base64 = Buffer.from(String(source), "utf8").toString("base64");
|
|
762
763
|
return `data:text/javascript;base64,${base64}`;
|
|
@@ -782,8 +783,13 @@ export function readExtensionDataUri(uri) {
|
|
|
782
783
|
}
|
|
783
784
|
}
|
|
784
785
|
|
|
785
|
-
/**
|
|
786
|
-
|
|
786
|
+
/**
|
|
787
|
+
* Resolve a config entry to source text and the original bytes.
|
|
788
|
+
*
|
|
789
|
+
* The raw buffer is retained so vendoring can lock exactly what the server
|
|
790
|
+
* returned, even when the text decoder strips a byte-order mark.
|
|
791
|
+
*/
|
|
792
|
+
export async function resolveSource(entry, baseDir, fetchImpl) {
|
|
787
793
|
const value = String(entry);
|
|
788
794
|
if (/^data:/i.test(value)) {
|
|
789
795
|
const source = readExtensionDataUri(value);
|
|
@@ -791,7 +797,13 @@ async function resolveSource(entry, baseDir, fetchImpl) {
|
|
|
791
797
|
throw new Error(
|
|
792
798
|
"unsupported data URI; expected base64 or percent-encoded JavaScript",
|
|
793
799
|
);
|
|
794
|
-
return {
|
|
800
|
+
return {
|
|
801
|
+
source,
|
|
802
|
+
buffer: Buffer.from(source, "utf8"),
|
|
803
|
+
remote: false,
|
|
804
|
+
dataUri: true,
|
|
805
|
+
url: value,
|
|
806
|
+
};
|
|
795
807
|
}
|
|
796
808
|
if (/^https?:\/\//i.test(value)) {
|
|
797
809
|
const fetchFn = fetchImpl || globalThis.fetch;
|
|
@@ -800,13 +812,24 @@ async function resolveSource(entry, baseDir, fetchImpl) {
|
|
|
800
812
|
const response = await fetchFn(value);
|
|
801
813
|
if (!response.ok)
|
|
802
814
|
throw new Error(`HTTP ${response.status} ${response.statusText}`.trim());
|
|
803
|
-
|
|
815
|
+
const buffer =
|
|
816
|
+
typeof response.arrayBuffer === "function"
|
|
817
|
+
? Buffer.from(await response.arrayBuffer())
|
|
818
|
+
: Buffer.from(await response.text(), "utf8");
|
|
819
|
+
return {
|
|
820
|
+
source: buffer.toString("utf8").replace(/^\uFEFF/, ""),
|
|
821
|
+
buffer,
|
|
822
|
+
remote: true,
|
|
823
|
+
url: value,
|
|
824
|
+
};
|
|
804
825
|
}
|
|
805
826
|
const resolved = path.isAbsolute(value)
|
|
806
827
|
? value
|
|
807
828
|
: path.resolve(baseDir, value);
|
|
829
|
+
const buffer = fs.readFileSync(resolved);
|
|
808
830
|
return {
|
|
809
|
-
source:
|
|
831
|
+
source: buffer.toString("utf8"),
|
|
832
|
+
buffer,
|
|
810
833
|
remote: false,
|
|
811
834
|
url: value,
|
|
812
835
|
file: resolved,
|
|
@@ -850,19 +873,39 @@ async function evaluateExtensionAny(source, filename) {
|
|
|
850
873
|
}
|
|
851
874
|
}
|
|
852
875
|
|
|
876
|
+
/**
|
|
877
|
+
* Inspect extension source without running it in the Node.js process.
|
|
878
|
+
* Useful to propose an extension id while vendoring or migrating.
|
|
879
|
+
* @param {string} source
|
|
880
|
+
* @param {string} [filename]
|
|
881
|
+
* @returns {Promise<{infos: object[], unsandboxed: boolean}>}
|
|
882
|
+
*/
|
|
883
|
+
export async function inspectExtension(source, filename = "extension") {
|
|
884
|
+
return evaluateExtensionAny(source, filename);
|
|
885
|
+
}
|
|
886
|
+
|
|
853
887
|
/**
|
|
854
888
|
* Load a list of extension references.
|
|
855
889
|
*
|
|
856
890
|
* @param {Array<string>} list Paths, `http(s)://` URLs or `data:` URIs.
|
|
857
|
-
* @param {{baseDir?: string, fetchImpl?: typeof fetch, expectUnsandboxed?: string|string[]}} [options]
|
|
891
|
+
* @param {{baseDir?: string, fetchImpl?: typeof fetch, expectUnsandboxed?: string|string[], reportUnmatchedUnsandboxed?: boolean}} [options]
|
|
858
892
|
* `expectUnsandboxed` acknowledges extensions that are known to require
|
|
859
|
-
* unsandboxed mode, silencing the warning for them.
|
|
860
|
-
*
|
|
893
|
+
* unsandboxed mode, silencing the warning for them. Set
|
|
894
|
+
* `reportUnmatchedUnsandboxed` to false when a caller combines declarations
|
|
895
|
+
* from several loaders and reports unmatched names itself.
|
|
896
|
+
* @returns {Promise<{catalog: Record<string, object>, extensionUrls: Record<string, string>, warnings: string[], ids: string[], unsandboxed: string[], matchedUnsandboxed: string[], unnecessaryUnsandboxed: string[]}>}
|
|
861
897
|
* `unsandboxed` holds the ids of extensions that only load unsandboxed.
|
|
898
|
+
* `matchedUnsandboxed` and `unnecessaryUnsandboxed` let callers combine
|
|
899
|
+
* declarations across multiple loaders without double-reporting them.
|
|
862
900
|
*/
|
|
863
901
|
export async function loadExtensions(
|
|
864
902
|
list,
|
|
865
|
-
{
|
|
903
|
+
{
|
|
904
|
+
baseDir = process.cwd(),
|
|
905
|
+
fetchImpl,
|
|
906
|
+
expectUnsandboxed = [],
|
|
907
|
+
reportUnmatchedUnsandboxed = true,
|
|
908
|
+
} = {},
|
|
866
909
|
) {
|
|
867
910
|
const catalog = {};
|
|
868
911
|
const extensionUrls = {};
|
|
@@ -941,17 +984,27 @@ export async function loadExtensions(
|
|
|
941
984
|
"project.unsandboxed in fx.yml once you trust it)",
|
|
942
985
|
);
|
|
943
986
|
}
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
987
|
+
if (reportUnmatchedUnsandboxed) {
|
|
988
|
+
for (const name of declared) {
|
|
989
|
+
if (declaredNotNeeded.has(name)) {
|
|
990
|
+
warnings.push(
|
|
991
|
+
`project.unsandboxed entry "${name}" loads in the sandbox; remove it from fx.yml`,
|
|
992
|
+
);
|
|
993
|
+
} else if (!declaredSeen.has(name)) {
|
|
994
|
+
warnings.push(
|
|
995
|
+
`project.unsandboxed entry "${name}" does not match any loaded extension`,
|
|
996
|
+
);
|
|
997
|
+
}
|
|
953
998
|
}
|
|
954
999
|
}
|
|
955
1000
|
|
|
956
|
-
return {
|
|
1001
|
+
return {
|
|
1002
|
+
catalog,
|
|
1003
|
+
extensionUrls,
|
|
1004
|
+
warnings,
|
|
1005
|
+
ids,
|
|
1006
|
+
unsandboxed,
|
|
1007
|
+
matchedUnsandboxed: [...declaredSeen],
|
|
1008
|
+
unnecessaryUnsandboxed: [...declaredNotNeeded],
|
|
1009
|
+
};
|
|
957
1010
|
}
|
package/src/index.js
CHANGED
|
@@ -29,6 +29,27 @@ export {
|
|
|
29
29
|
resolvePackageConfig,
|
|
30
30
|
slugify,
|
|
31
31
|
} from "./package.js";
|
|
32
|
+
export {
|
|
33
|
+
LOCK_FILENAME,
|
|
34
|
+
VendorError,
|
|
35
|
+
defaultVendorName,
|
|
36
|
+
fetchVendorSource,
|
|
37
|
+
formatDiff,
|
|
38
|
+
installVendor,
|
|
39
|
+
installVendorEntry,
|
|
40
|
+
loadVendorExtensions,
|
|
41
|
+
migrateProjectExtensions,
|
|
42
|
+
outdatedVendor,
|
|
43
|
+
readLock,
|
|
44
|
+
removeVendoredExtension,
|
|
45
|
+
resolveVendorFiles,
|
|
46
|
+
sha256,
|
|
47
|
+
uniqueVendorName,
|
|
48
|
+
updateVendor,
|
|
49
|
+
vendorDirPath,
|
|
50
|
+
verifyVendor,
|
|
51
|
+
writeLock,
|
|
52
|
+
} from "./vendor.js";
|
|
32
53
|
export { formatDevUrls, injectDevClient, startDevServer } from "./dev.js";
|
|
33
54
|
export * from "./catalog.js";
|
|
34
55
|
export * from "./config.js";
|