@hominis/fireforge 0.16.5 → 0.18.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/CHANGELOG.md +56 -0
- package/README.md +46 -24
- package/dist/src/commands/build.js +33 -10
- package/dist/src/commands/config.js +32 -20
- package/dist/src/commands/doctor-furnace-manifest-sync.d.ts +18 -0
- package/dist/src/commands/doctor-furnace-manifest-sync.js +159 -0
- package/dist/src/commands/doctor-furnace.js +2 -0
- package/dist/src/commands/doctor-working-tree.d.ts +29 -0
- package/dist/src/commands/doctor-working-tree.js +93 -0
- package/dist/src/commands/doctor.js +23 -12
- package/dist/src/commands/export-all.js +11 -3
- package/dist/src/commands/export-shared.d.ts +7 -1
- package/dist/src/commands/export-shared.js +21 -3
- package/dist/src/commands/furnace/chrome-doc-tests.js +9 -2
- package/dist/src/commands/furnace/create-templates.d.ts +11 -0
- package/dist/src/commands/furnace/create-templates.js +11 -2
- package/dist/src/commands/furnace/init.js +97 -9
- package/dist/src/commands/furnace/override.js +23 -13
- package/dist/src/commands/furnace/remove.js +8 -0
- package/dist/src/commands/furnace/rename.js +133 -4
- package/dist/src/commands/lint.js +70 -6
- package/dist/src/commands/patch/delete.js +4 -1
- package/dist/src/commands/patch/reorder.js +4 -1
- package/dist/src/commands/re-export-files.js +3 -1
- package/dist/src/commands/re-export.js +4 -1
- package/dist/src/commands/register.js +11 -0
- package/dist/src/commands/resolve.d.ts +25 -1
- package/dist/src/commands/resolve.js +25 -15
- package/dist/src/commands/status.js +100 -122
- package/dist/src/commands/test.js +68 -14
- package/dist/src/commands/token-coverage.js +10 -3
- package/dist/src/commands/wire.js +50 -8
- package/dist/src/core/browser-wire.js +21 -4
- package/dist/src/core/build-audit.js +10 -0
- package/dist/src/core/config.d.ts +33 -0
- package/dist/src/core/config.js +43 -0
- package/dist/src/core/furnace-config.d.ts +23 -2
- package/dist/src/core/furnace-config.js +26 -3
- package/dist/src/core/git-diff.js +21 -2
- package/dist/src/core/mach.d.ts +43 -6
- package/dist/src/core/mach.js +57 -7
- package/dist/src/core/manifest-rules.js +10 -1
- package/dist/src/core/manifest-tokenizers.d.ts +6 -0
- package/dist/src/core/manifest-tokenizers.js +28 -0
- package/dist/src/core/marionette-port.d.ts +50 -0
- package/dist/src/core/marionette-port.js +215 -0
- package/dist/src/core/patch-lint.d.ts +47 -2
- package/dist/src/core/patch-lint.js +89 -14
- package/dist/src/core/patch-manifest-consistency.d.ts +21 -1
- package/dist/src/core/patch-manifest-consistency.js +31 -3
- package/dist/src/core/patch-manifest-io.js +10 -0
- package/dist/src/core/patch-manifest-resolve.d.ts +20 -1
- package/dist/src/core/patch-manifest-resolve.js +29 -2
- package/dist/src/core/patch-manifest-validate.js +25 -1
- package/dist/src/core/status-classify.d.ts +54 -0
- package/dist/src/core/status-classify.js +134 -0
- package/dist/src/core/token-coverage.js +24 -0
- package/dist/src/core/token-dark-mode.d.ts +49 -0
- package/dist/src/core/token-dark-mode.js +182 -0
- package/dist/src/core/token-manager.js +17 -33
- package/dist/src/core/wire-destroy.d.ts +7 -3
- package/dist/src/core/wire-destroy.js +11 -6
- package/dist/src/core/wire-dom-fragment.d.ts +17 -0
- package/dist/src/core/wire-dom-fragment.js +40 -0
- package/dist/src/core/wire-init.d.ts +9 -3
- package/dist/src/core/wire-init.js +18 -6
- package/dist/src/core/wire-subscript.d.ts +7 -3
- package/dist/src/core/wire-subscript.js +11 -4
- package/dist/src/types/commands/patches.d.ts +23 -0
- package/dist/src/types/furnace.d.ts +9 -0
- package/dist/src/utils/parse.d.ts +7 -0
- package/dist/src/utils/parse.js +15 -0
- package/package.json +1 -1
|
@@ -75,6 +75,46 @@ export function legacyAddDomFragment(content, includeDirective) {
|
|
|
75
75
|
lines.splice(insertIndex, 0, includeDirective);
|
|
76
76
|
return lines.join('\n');
|
|
77
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Dry-run precheck for `addDomFragment`. Reads the resolved chrome
|
|
80
|
+
* document and verifies it either already contains the `#include`
|
|
81
|
+
* directive (the idempotent-skip case) OR offers a locatable insertion
|
|
82
|
+
* point via {@link addDomFragmentTokenized} / {@link legacyAddDomFragment}.
|
|
83
|
+
* Throws the same `Could not find insertion point in chrome document`
|
|
84
|
+
* error the real run would throw when neither condition holds.
|
|
85
|
+
*
|
|
86
|
+
* Motivating case (2026-04-21 eval, Finding #12): `fireforge wire ...
|
|
87
|
+
* --dry-run` previewed a plausible mutation plan against
|
|
88
|
+
* `tokenHostDocuments[0]`, then `fireforge wire ...` without
|
|
89
|
+
* `--dry-run` threw `Could not find insertion point in chrome document`
|
|
90
|
+
* on the same arguments. The real run had always called the insertion
|
|
91
|
+
* helpers; dry-run did not. This helper runs the same check in the
|
|
92
|
+
* preview pass so plan and execution disagree less.
|
|
93
|
+
*/
|
|
94
|
+
export async function probeDomFragmentInsertionPoint(engineDir, domFilePath, targetPath = DEFAULT_DOM_TARGET) {
|
|
95
|
+
const targetAbsPath = join(engineDir, targetPath);
|
|
96
|
+
if (!(await pathExists(targetAbsPath))) {
|
|
97
|
+
// The callers in `wire.ts` run their own existence probe before
|
|
98
|
+
// invoking this helper, but a well-behaved probe is paranoid — if
|
|
99
|
+
// something changed between the two checks, fail with the same
|
|
100
|
+
// error the real run would surface.
|
|
101
|
+
throw new GeneralError(`${targetPath} not found in engine`);
|
|
102
|
+
}
|
|
103
|
+
const safeDomFilePath = toRootRelativePath(engineDir, domFilePath);
|
|
104
|
+
const targetDir = dirname(targetPath);
|
|
105
|
+
const includePath = relative(targetDir, safeDomFilePath).replace(/\\/g, '/');
|
|
106
|
+
const includeDirective = `#include ${includePath}`;
|
|
107
|
+
const content = await readText(targetAbsPath);
|
|
108
|
+
if (new RegExp(`^${escapeRegex(includeDirective)}$`, 'm').test(content)) {
|
|
109
|
+
// Already wired — the real run would idempotent-skip here, so
|
|
110
|
+
// dry-run is allowed to proceed too.
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
// Check the tokenised and legacy insertion paths symmetrically with
|
|
114
|
+
// the real run. Either helper returning without throwing is sufficient
|
|
115
|
+
// evidence that the real run can land the directive.
|
|
116
|
+
withParserFallback(() => addDomFragmentTokenized(content, includeDirective), () => legacyAddDomFragment(content, includeDirective), targetPath);
|
|
117
|
+
}
|
|
78
118
|
/**
|
|
79
119
|
* Inserts a `#include` directive for an `.inc.xhtml` file into the top-level
|
|
80
120
|
* chrome document (default: `browser/base/content/browser.xhtml`), before
|
|
@@ -5,12 +5,18 @@
|
|
|
5
5
|
* AST-based implementation: finds onLoad() method body, locates existing
|
|
6
6
|
* fireforge init blocks (TryStatements containing typeof guards), and inserts
|
|
7
7
|
* after the correct position.
|
|
8
|
+
*
|
|
9
|
+
* `marker` is prepended (uppercased) to the generated comment line so the
|
|
10
|
+
* emitted block carries the patch-lint `// <MARKER>:` signature that
|
|
11
|
+
* `lintModificationComments` looks for. Otherwise the first export after
|
|
12
|
+
* `wire` trips `missing-modification-comment` on wire-generated edits —
|
|
13
|
+
* exactly the eval 1 Finding #9 regression.
|
|
8
14
|
*/
|
|
9
|
-
export declare function addInitAST(content: string, expression: string, after?: string): string;
|
|
15
|
+
export declare function addInitAST(content: string, expression: string, after?: string, marker?: string): string;
|
|
10
16
|
/**
|
|
11
17
|
* Legacy regex/line-based implementation preserved as fallback.
|
|
12
18
|
*/
|
|
13
|
-
export declare function legacyAddInit(content: string, expression: string, after?: string): string;
|
|
19
|
+
export declare function legacyAddInit(content: string, expression: string, after?: string, marker?: string): string;
|
|
14
20
|
/**
|
|
15
21
|
* Adds an init expression as the first statement(s) in gBrowserInit.onLoad()
|
|
16
22
|
* in browser-init.js, after any previously-wired fireforge init blocks.
|
|
@@ -20,4 +26,4 @@ export declare function legacyAddInit(content: string, expression: string, after
|
|
|
20
26
|
* @param after - Optional name to insert after (e.g., "MyComponent" to insert after its block)
|
|
21
27
|
* @returns true if added, false if already present
|
|
22
28
|
*/
|
|
23
|
-
export declare function addInitToBrowserInit(engineDir: string, expression: string, after?: string): Promise<boolean>;
|
|
29
|
+
export declare function addInitToBrowserInit(engineDir: string, expression: string, after?: string, marker?: string): Promise<boolean>;
|
|
@@ -12,12 +12,24 @@ import { detectIndent, getNodeSource, parseScript } from './ast-utils.js';
|
|
|
12
12
|
import { withParserFallback } from './parser-fallback.js';
|
|
13
13
|
import { assertBraceBalancePreserved, coerceToCall, extractNameFromExpression, findInsertionAfterFireforgeBlocks, findMethodBody, findMethodBraceIndex, validateWireName, walkToTryBlockEnd, } from './wire-utils.js';
|
|
14
14
|
const BROWSER_INIT_JS = 'browser/base/content/browser-init.js';
|
|
15
|
+
/**
|
|
16
|
+
* Default patch-lint marker used when a caller does not supply a
|
|
17
|
+
* project-specific one. Kept as a constant so test fixtures and
|
|
18
|
+
* fallback code paths agree on the shape.
|
|
19
|
+
*/
|
|
20
|
+
const DEFAULT_MARKER = 'FIREFORGE:';
|
|
15
21
|
/**
|
|
16
22
|
* AST-based implementation: finds onLoad() method body, locates existing
|
|
17
23
|
* fireforge init blocks (TryStatements containing typeof guards), and inserts
|
|
18
24
|
* after the correct position.
|
|
25
|
+
*
|
|
26
|
+
* `marker` is prepended (uppercased) to the generated comment line so the
|
|
27
|
+
* emitted block carries the patch-lint `// <MARKER>:` signature that
|
|
28
|
+
* `lintModificationComments` looks for. Otherwise the first export after
|
|
29
|
+
* `wire` trips `missing-modification-comment` on wire-generated edits —
|
|
30
|
+
* exactly the eval 1 Finding #9 regression.
|
|
19
31
|
*/
|
|
20
|
-
export function addInitAST(content, expression, after) {
|
|
32
|
+
export function addInitAST(content, expression, after, marker = DEFAULT_MARKER) {
|
|
21
33
|
const name = extractNameFromExpression(expression);
|
|
22
34
|
// `validateWireName` accepts both `Foo.bar` and `Foo.bar()` shapes. The
|
|
23
35
|
// template below interpolates the value verbatim, so a bare property
|
|
@@ -99,7 +111,7 @@ export function addInitAST(content, expression, after) {
|
|
|
99
111
|
}
|
|
100
112
|
}
|
|
101
113
|
const block = [
|
|
102
|
-
`${indent}// ${
|
|
114
|
+
`${indent}// ${marker} wire-init ${name} — must be first, before Firefox subsystem`,
|
|
103
115
|
`${indent}// inits that reference native UI elements we hide.`,
|
|
104
116
|
`${indent}try {`,
|
|
105
117
|
`${indent} if (typeof ${name} !== "undefined") {`,
|
|
@@ -115,7 +127,7 @@ export function addInitAST(content, expression, after) {
|
|
|
115
127
|
/**
|
|
116
128
|
* Legacy regex/line-based implementation preserved as fallback.
|
|
117
129
|
*/
|
|
118
|
-
export function legacyAddInit(content, expression, after) {
|
|
130
|
+
export function legacyAddInit(content, expression, after, marker = DEFAULT_MARKER) {
|
|
119
131
|
const name = extractNameFromExpression(expression);
|
|
120
132
|
// See `addInitAST` for the rationale — the AST and fallback paths must
|
|
121
133
|
// agree on whether the emitted block is a function call, otherwise
|
|
@@ -174,7 +186,7 @@ export function legacyAddInit(content, expression, after) {
|
|
|
174
186
|
const inner = baseIndent + ' ';
|
|
175
187
|
const inner2 = inner + ' ';
|
|
176
188
|
const block = [
|
|
177
|
-
`${baseIndent}// ${
|
|
189
|
+
`${baseIndent}// ${marker} wire-init ${name} — must be first, before Firefox subsystem`,
|
|
178
190
|
`${baseIndent}// inits that reference native UI elements we hide.`,
|
|
179
191
|
`${baseIndent}try {`,
|
|
180
192
|
`${inner}if (typeof ${name} !== "undefined") {`,
|
|
@@ -196,7 +208,7 @@ export function legacyAddInit(content, expression, after) {
|
|
|
196
208
|
* @param after - Optional name to insert after (e.g., "MyComponent" to insert after its block)
|
|
197
209
|
* @returns true if added, false if already present
|
|
198
210
|
*/
|
|
199
|
-
export async function addInitToBrowserInit(engineDir, expression, after) {
|
|
211
|
+
export async function addInitToBrowserInit(engineDir, expression, after, marker = DEFAULT_MARKER) {
|
|
200
212
|
validateWireName(expression, 'init expression');
|
|
201
213
|
const filePath = join(engineDir, BROWSER_INIT_JS);
|
|
202
214
|
if (!(await pathExists(filePath))) {
|
|
@@ -212,7 +224,7 @@ export async function addInitToBrowserInit(engineDir, expression, after) {
|
|
|
212
224
|
if (initPattern.test(content)) {
|
|
213
225
|
return false;
|
|
214
226
|
}
|
|
215
|
-
const { value, usedFallback } = withParserFallback(() => addInitAST(content, expression, after), () => legacyAddInit(content, expression, after), BROWSER_INIT_JS);
|
|
227
|
+
const { value, usedFallback } = withParserFallback(() => addInitAST(content, expression, after, marker), () => legacyAddInit(content, expression, after, marker), BROWSER_INIT_JS);
|
|
216
228
|
if (usedFallback) {
|
|
217
229
|
assertBraceBalancePreserved(content, value, BROWSER_INIT_JS);
|
|
218
230
|
}
|
|
@@ -4,12 +4,16 @@
|
|
|
4
4
|
/**
|
|
5
5
|
* AST-based implementation: finds the last try/catch containing
|
|
6
6
|
* `loadSubScript` and inserts a new try/catch block after it.
|
|
7
|
+
*
|
|
8
|
+
* The inserted block carries a `// <MARKER>: wire-subscript ...` comment
|
|
9
|
+
* so the emitted edit satisfies `lintModificationComments` (eval 1
|
|
10
|
+
* Finding #9).
|
|
7
11
|
*/
|
|
8
|
-
export declare function addSubscriptAST(content: string, name: string): string;
|
|
12
|
+
export declare function addSubscriptAST(content: string, name: string, marker?: string): string;
|
|
9
13
|
/**
|
|
10
14
|
* Legacy regex/line-based implementation preserved as fallback.
|
|
11
15
|
*/
|
|
12
|
-
export declare function legacyAddSubscript(content: string, name: string): string;
|
|
16
|
+
export declare function legacyAddSubscript(content: string, name: string, marker?: string): string;
|
|
13
17
|
/**
|
|
14
18
|
* Adds a loadSubScript entry to browser-main.js with try/catch error handling.
|
|
15
19
|
*
|
|
@@ -17,4 +21,4 @@ export declare function legacyAddSubscript(content: string, name: string): strin
|
|
|
17
21
|
* @param name - Subscript name (without .js extension)
|
|
18
22
|
* @returns true if added, false if already present
|
|
19
23
|
*/
|
|
20
|
-
export declare function addSubscriptToBrowserMain(engineDir: string, name: string): Promise<boolean>;
|
|
24
|
+
export declare function addSubscriptToBrowserMain(engineDir: string, name: string, marker?: string): Promise<boolean>;
|
|
@@ -11,11 +11,16 @@ import { detectIndent, getNodeSource, parseScript, walkAST, } from './ast-utils.
|
|
|
11
11
|
import { withParserFallback } from './parser-fallback.js';
|
|
12
12
|
import { assertBraceBalancePreserved, findNearestTryLine, validateWireName, walkToTryBlockEnd, } from './wire-utils.js';
|
|
13
13
|
const BROWSER_MAIN_JS = 'browser/base/content/browser-main.js';
|
|
14
|
+
const DEFAULT_MARKER = 'FIREFORGE:';
|
|
14
15
|
/**
|
|
15
16
|
* AST-based implementation: finds the last try/catch containing
|
|
16
17
|
* `loadSubScript` and inserts a new try/catch block after it.
|
|
18
|
+
*
|
|
19
|
+
* The inserted block carries a `// <MARKER>: wire-subscript ...` comment
|
|
20
|
+
* so the emitted edit satisfies `lintModificationComments` (eval 1
|
|
21
|
+
* Finding #9).
|
|
17
22
|
*/
|
|
18
|
-
export function addSubscriptAST(content, name) {
|
|
23
|
+
export function addSubscriptAST(content, name, marker = DEFAULT_MARKER) {
|
|
19
24
|
const ast = parseScript(content);
|
|
20
25
|
const ms = new MagicString(content);
|
|
21
26
|
// Collect all TryStatements containing loadSubScript
|
|
@@ -58,6 +63,7 @@ export function addSubscriptAST(content, name) {
|
|
|
58
63
|
indent = detectIndent(content, lastBrace);
|
|
59
64
|
}
|
|
60
65
|
const block = [
|
|
66
|
+
`${indent}// ${marker} wire-subscript ${name}`,
|
|
61
67
|
`${indent}try {`,
|
|
62
68
|
`${indent} Services.scriptloader.loadSubScript("chrome://browser/content/${name}.js", this);`,
|
|
63
69
|
`${indent}} catch (e) {`,
|
|
@@ -70,7 +76,7 @@ export function addSubscriptAST(content, name) {
|
|
|
70
76
|
/**
|
|
71
77
|
* Legacy regex/line-based implementation preserved as fallback.
|
|
72
78
|
*/
|
|
73
|
-
export function legacyAddSubscript(content, name) {
|
|
79
|
+
export function legacyAddSubscript(content, name, marker = DEFAULT_MARKER) {
|
|
74
80
|
const lines = content.split('\n');
|
|
75
81
|
let lastSubScriptLine = -1;
|
|
76
82
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -103,6 +109,7 @@ export function legacyAddSubscript(content, name) {
|
|
|
103
109
|
const ind = refLine?.match(/^(\s*)/)?.[1] ?? ' ';
|
|
104
110
|
const inner = ind + ' ';
|
|
105
111
|
const block = [
|
|
112
|
+
`${ind}// ${marker} wire-subscript ${name}`,
|
|
106
113
|
`${ind}try {`,
|
|
107
114
|
`${inner}Services.scriptloader.loadSubScript("chrome://browser/content/${name}.js", this);`,
|
|
108
115
|
`${ind}} catch (e) {`,
|
|
@@ -119,7 +126,7 @@ export function legacyAddSubscript(content, name) {
|
|
|
119
126
|
* @param name - Subscript name (without .js extension)
|
|
120
127
|
* @returns true if added, false if already present
|
|
121
128
|
*/
|
|
122
|
-
export async function addSubscriptToBrowserMain(engineDir, name) {
|
|
129
|
+
export async function addSubscriptToBrowserMain(engineDir, name, marker = DEFAULT_MARKER) {
|
|
123
130
|
validateWireName(name, 'subscript name');
|
|
124
131
|
const filePath = join(engineDir, BROWSER_MAIN_JS);
|
|
125
132
|
if (!(await pathExists(filePath))) {
|
|
@@ -130,7 +137,7 @@ export async function addSubscriptToBrowserMain(engineDir, name) {
|
|
|
130
137
|
if (content.includes(`content/${name}.js"`)) {
|
|
131
138
|
return false;
|
|
132
139
|
}
|
|
133
|
-
const { value, usedFallback } = withParserFallback(() => addSubscriptAST(content, name), () => legacyAddSubscript(content, name), BROWSER_MAIN_JS);
|
|
140
|
+
const { value, usedFallback } = withParserFallback(() => addSubscriptAST(content, name, marker), () => legacyAddSubscript(content, name, marker), BROWSER_MAIN_JS);
|
|
134
141
|
if (usedFallback) {
|
|
135
142
|
assertBraceBalancePreserved(content, value, BROWSER_MAIN_JS);
|
|
136
143
|
}
|
|
@@ -73,6 +73,29 @@ export interface PatchMetadata {
|
|
|
73
73
|
* renamed or removed.
|
|
74
74
|
*/
|
|
75
75
|
lintIgnore?: string[];
|
|
76
|
+
/**
|
|
77
|
+
* Optional per-patch threshold-tier override for the `large-patch-lines`
|
|
78
|
+
* rule. Exists for branding patches that must touch a small number of
|
|
79
|
+
* cross-cutting registration files alongside `browser/branding/<name>/`
|
|
80
|
+
* (notably `browser/moz.configure` to register the new branding flavor
|
|
81
|
+
* with the top-level configure). The narrow auto-detect allowlist in
|
|
82
|
+
* `isBrandingOnlyPatch` covers the canonical shape, but a fork whose
|
|
83
|
+
* branding patch also touches an unlisted sibling (for example a
|
|
84
|
+
* `browser/themes/<name>/` override or a vendor-specific icon
|
|
85
|
+
* resource) falls through to the general tier and trips the hard
|
|
86
|
+
* limit on what is legitimately one branding diff.
|
|
87
|
+
*
|
|
88
|
+
* Declaring `tier: "branding"` here forces the branding thresholds
|
|
89
|
+
* (notice 3000 / warning 8000 / error 20000) regardless of
|
|
90
|
+
* `filesAffected`. The tier is the weaker claim than test — a patch
|
|
91
|
+
* of all-tests still lands in the test tier even if this field is
|
|
92
|
+
* set, because the test-tier thresholds are already more permissive
|
|
93
|
+
* and a test that is also branding-shaped is vanishingly rare.
|
|
94
|
+
*
|
|
95
|
+
* Only `"branding"` is currently recognised. Unknown values are
|
|
96
|
+
* rejected by the manifest validator, not silently stripped.
|
|
97
|
+
*/
|
|
98
|
+
tier?: 'branding';
|
|
76
99
|
}
|
|
77
100
|
/**
|
|
78
101
|
* Schema for patches/patches.json file.
|
|
@@ -102,6 +102,15 @@ export interface FurnaceConfig {
|
|
|
102
102
|
tokenPrefix?: string;
|
|
103
103
|
/** Custom properties allowed even though they don't match tokenPrefix (e.g. ["--background-color-box"]) */
|
|
104
104
|
tokenAllowlist?: string[];
|
|
105
|
+
/**
|
|
106
|
+
* CSS custom-property prefixes that identify upstream / platform
|
|
107
|
+
* variables the fork does not own. `token coverage` counts matches
|
|
108
|
+
* as `allowlisted` rather than `unknown` so a copied upstream
|
|
109
|
+
* baseline doesn't drag fork-owned coverage percentages down.
|
|
110
|
+
* Defaults to `['--moz-']` when unset. Pass an explicit empty array
|
|
111
|
+
* to restore the pre-0.18.0 strict contract.
|
|
112
|
+
*/
|
|
113
|
+
platformPrefixes?: string[];
|
|
105
114
|
/**
|
|
106
115
|
* Custom properties used as runtime state channels — written and read by the
|
|
107
116
|
* component itself (e.g. per-frame camera/tile positions) rather than
|
|
@@ -76,6 +76,13 @@ export declare class ParsedRecord {
|
|
|
76
76
|
* @throws Error if the field is missing or not an array of strings
|
|
77
77
|
*/
|
|
78
78
|
stringArray(key: string): string[];
|
|
79
|
+
/**
|
|
80
|
+
* Extracts an optional array-of-strings field.
|
|
81
|
+
* @param key - Field name
|
|
82
|
+
* @returns The string array (fresh copy) or undefined when absent
|
|
83
|
+
* @throws Error if the field is present but not an array of strings
|
|
84
|
+
*/
|
|
85
|
+
optionalStringArray(key: string): string[] | undefined;
|
|
79
86
|
/**
|
|
80
87
|
* Extracts a required nested object field.
|
|
81
88
|
* @param key - Field name
|
package/dist/src/utils/parse.js
CHANGED
|
@@ -142,6 +142,21 @@ export class ParsedRecord {
|
|
|
142
142
|
}
|
|
143
143
|
return [...value];
|
|
144
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* Extracts an optional array-of-strings field.
|
|
147
|
+
* @param key - Field name
|
|
148
|
+
* @returns The string array (fresh copy) or undefined when absent
|
|
149
|
+
* @throws Error if the field is present but not an array of strings
|
|
150
|
+
*/
|
|
151
|
+
optionalStringArray(key) {
|
|
152
|
+
const value = this.#data[key];
|
|
153
|
+
if (value === undefined)
|
|
154
|
+
return undefined;
|
|
155
|
+
if (!isArray(value) || !value.every(isString)) {
|
|
156
|
+
throw new Error(`${this.#label}.${key} must be an array of strings`);
|
|
157
|
+
}
|
|
158
|
+
return [...value];
|
|
159
|
+
}
|
|
145
160
|
/**
|
|
146
161
|
* Extracts a required nested object field.
|
|
147
162
|
* @param key - Field name
|