@hominis/fireforge 0.10.1 → 0.11.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 +93 -1
- package/README.md +125 -238
- package/dist/bin/fireforge.js +26 -0
- package/dist/src/cli.d.ts +1 -1
- package/dist/src/cli.js +131 -52
- package/dist/src/commands/bootstrap.js +6 -2
- package/dist/src/commands/build.js +4 -2
- package/dist/src/commands/discard.js +16 -4
- package/dist/src/commands/doctor-furnace.d.ts +8 -0
- package/dist/src/commands/doctor-furnace.js +422 -0
- package/dist/src/commands/doctor.d.ts +115 -0
- package/dist/src/commands/doctor.js +327 -258
- package/dist/src/commands/download.js +16 -1
- package/dist/src/commands/export-all.js +15 -0
- package/dist/src/commands/export-flow.d.ts +91 -0
- package/dist/src/commands/export-flow.js +344 -0
- package/dist/src/commands/export.js +151 -5
- package/dist/src/commands/furnace/apply.d.ts +3 -2
- package/dist/src/commands/furnace/apply.js +169 -36
- package/dist/src/commands/furnace/create.js +162 -52
- package/dist/src/commands/furnace/deploy.js +156 -144
- package/dist/src/commands/furnace/diff.d.ts +8 -4
- package/dist/src/commands/furnace/diff.js +142 -73
- package/dist/src/commands/furnace/index.d.ts +6 -2
- package/dist/src/commands/furnace/index.js +76 -25
- package/dist/src/commands/furnace/init.d.ts +11 -0
- package/dist/src/commands/furnace/init.js +76 -0
- package/dist/src/commands/furnace/list.d.ts +4 -1
- package/dist/src/commands/furnace/list.js +35 -3
- package/dist/src/commands/furnace/override.d.ts +8 -0
- package/dist/src/commands/furnace/override.js +216 -26
- package/dist/src/commands/furnace/preview.js +184 -30
- package/dist/src/commands/furnace/refresh.d.ts +10 -0
- package/dist/src/commands/furnace/refresh.js +268 -0
- package/dist/src/commands/furnace/remove.js +285 -89
- package/dist/src/commands/furnace/rename.d.ts +5 -0
- package/dist/src/commands/furnace/rename.js +308 -0
- package/dist/src/commands/furnace/scan.d.ts +4 -1
- package/dist/src/commands/furnace/scan.js +72 -11
- package/dist/src/commands/furnace/status.js +85 -20
- package/dist/src/commands/furnace/sync.d.ts +12 -0
- package/dist/src/commands/furnace/sync.js +77 -0
- package/dist/src/commands/furnace/validate.d.ts +4 -1
- package/dist/src/commands/furnace/validate.js +99 -3
- package/dist/src/commands/furnace/validation-output.d.ts +24 -1
- package/dist/src/commands/furnace/validation-output.js +93 -1
- package/dist/src/commands/import.js +37 -4
- package/dist/src/commands/lint.js +11 -2
- package/dist/src/commands/manifest.d.ts +39 -0
- package/dist/src/commands/manifest.js +59 -0
- package/dist/src/commands/patch/delete.d.ts +28 -0
- package/dist/src/commands/patch/delete.js +209 -0
- package/dist/src/commands/patch/index.d.ts +17 -0
- package/dist/src/commands/patch/index.js +25 -0
- package/dist/src/commands/patch/reorder.d.ts +30 -0
- package/dist/src/commands/patch/reorder.js +377 -0
- package/dist/src/commands/re-export-files.d.ts +17 -0
- package/dist/src/commands/re-export-files.js +177 -0
- package/dist/src/commands/re-export.js +44 -0
- package/dist/src/commands/rebase/abort.d.ts +1 -1
- package/dist/src/commands/rebase/abort.js +12 -3
- package/dist/src/commands/rebase/confirm.d.ts +3 -3
- package/dist/src/commands/rebase/confirm.js +4 -4
- package/dist/src/commands/rebase/index.js +13 -4
- package/dist/src/commands/reset.js +20 -4
- package/dist/src/commands/run.js +46 -1
- package/dist/src/commands/setup-support.js +5 -5
- package/dist/src/commands/status.js +97 -6
- package/dist/src/commands/test.js +5 -37
- package/dist/src/commands/verify.d.ts +31 -0
- package/dist/src/commands/verify.js +126 -0
- package/dist/src/core/build-prepare.js +40 -16
- package/dist/src/core/destructive.d.ts +96 -0
- package/dist/src/core/destructive.js +137 -0
- package/dist/src/core/diff-hunks.d.ts +73 -0
- package/dist/src/core/diff-hunks.js +268 -0
- package/dist/src/core/firefox.d.ts +1 -1
- package/dist/src/core/firefox.js +1 -1
- package/dist/src/core/furnace-apply-helpers.d.ts +89 -6
- package/dist/src/core/furnace-apply-helpers.js +302 -57
- package/dist/src/core/furnace-apply-output.d.ts +16 -0
- package/dist/src/core/furnace-apply-output.js +57 -0
- package/dist/src/core/furnace-apply.d.ts +21 -3
- package/dist/src/core/furnace-apply.js +260 -29
- package/dist/src/core/furnace-checksum-utils.d.ts +4 -0
- package/dist/src/core/furnace-checksum-utils.js +24 -0
- package/dist/src/core/furnace-config.d.ts +28 -1
- package/dist/src/core/furnace-config.js +180 -17
- package/dist/src/core/furnace-constants.d.ts +22 -0
- package/dist/src/core/furnace-constants.js +36 -0
- package/dist/src/core/furnace-graph-utils.d.ts +11 -0
- package/dist/src/core/furnace-graph-utils.js +94 -0
- package/dist/src/core/furnace-operation.d.ts +108 -0
- package/dist/src/core/furnace-operation.js +220 -0
- package/dist/src/core/furnace-refresh.d.ts +20 -0
- package/dist/src/core/furnace-refresh.js +118 -0
- package/dist/src/core/furnace-registration-ast.d.ts +5 -0
- package/dist/src/core/furnace-registration-ast.js +134 -4
- package/dist/src/core/furnace-registration-remove.d.ts +25 -3
- package/dist/src/core/furnace-registration-remove.js +196 -62
- package/dist/src/core/furnace-registration-validate.d.ts +13 -1
- package/dist/src/core/furnace-registration-validate.js +15 -3
- package/dist/src/core/furnace-registration.d.ts +27 -4
- package/dist/src/core/furnace-registration.js +93 -11
- package/dist/src/core/furnace-rollback.d.ts +11 -0
- package/dist/src/core/furnace-rollback.js +78 -7
- package/dist/src/core/furnace-scanner.d.ts +8 -2
- package/dist/src/core/furnace-scanner.js +152 -55
- package/dist/src/core/furnace-stories.js +7 -5
- package/dist/src/core/furnace-validate-accessibility.js +7 -1
- package/dist/src/core/furnace-validate-compatibility.d.ts +1 -1
- package/dist/src/core/furnace-validate-compatibility.js +85 -1
- package/dist/src/core/furnace-validate-helpers.d.ts +4 -0
- package/dist/src/core/furnace-validate-helpers.js +31 -0
- package/dist/src/core/furnace-validate-registration.d.ts +17 -2
- package/dist/src/core/furnace-validate-registration.js +73 -3
- package/dist/src/core/furnace-validate-structure.d.ts +10 -2
- package/dist/src/core/furnace-validate-structure.js +45 -3
- package/dist/src/core/furnace-validate.d.ts +10 -1
- package/dist/src/core/furnace-validate.js +80 -6
- package/dist/src/core/furnace-version-drift.d.ts +55 -0
- package/dist/src/core/furnace-version-drift.js +101 -0
- package/dist/src/core/git-file-ops.d.ts +8 -0
- package/dist/src/core/git-file-ops.js +19 -6
- package/dist/src/core/lint-projection.d.ts +25 -0
- package/dist/src/core/lint-projection.js +44 -0
- package/dist/src/core/mach.d.ts +4 -2
- package/dist/src/core/mach.js +17 -2
- package/dist/src/core/markdown-table.d.ts +104 -0
- package/dist/src/core/markdown-table.js +266 -0
- package/dist/src/core/ownership-table.d.ts +53 -0
- package/dist/src/core/ownership-table.js +144 -0
- package/dist/src/core/patch-apply.d.ts +17 -3
- package/dist/src/core/patch-apply.js +86 -8
- package/dist/src/core/patch-export.d.ts +119 -5
- package/dist/src/core/patch-export.js +183 -25
- package/dist/src/core/patch-lint-cross.d.ts +195 -0
- package/dist/src/core/patch-lint-cross.js +428 -0
- package/dist/src/core/patch-lint-diff.d.ts +33 -0
- package/dist/src/core/patch-lint-diff.js +84 -0
- package/dist/src/core/patch-lint.d.ts +2 -4
- package/dist/src/core/patch-lint.js +12 -50
- package/dist/src/core/patch-lock.js +2 -1
- package/dist/src/core/patch-manifest-io.d.ts +102 -1
- package/dist/src/core/patch-manifest-io.js +270 -2
- package/dist/src/core/patch-manifest-query.d.ts +1 -1
- package/dist/src/core/patch-manifest-query.js +1 -1
- package/dist/src/core/patch-manifest.d.ts +1 -1
- package/dist/src/core/patch-manifest.js +1 -1
- package/dist/src/core/patch-transform.d.ts +12 -0
- package/dist/src/core/patch-transform.js +21 -7
- package/dist/src/core/token-manager.js +67 -69
- package/dist/src/core/wire-destroy.js +6 -3
- package/dist/src/core/wire-init.js +10 -4
- package/dist/src/core/wire-subscript.js +9 -3
- package/dist/src/core/wire-utils.d.ts +52 -5
- package/dist/src/core/wire-utils.js +69 -6
- package/dist/src/errors/base.d.ts +20 -0
- package/dist/src/errors/base.js +24 -0
- package/dist/src/errors/furnace.js +7 -1
- package/dist/src/errors/rebase.js +6 -1
- package/dist/src/types/commands/index.d.ts +1 -1
- package/dist/src/types/commands/options.d.ts +125 -4
- package/dist/src/types/commands/patches.d.ts +11 -1
- package/dist/src/types/config.d.ts +1 -1
- package/dist/src/types/furnace.d.ts +55 -1
- package/dist/src/utils/fs.d.ts +12 -0
- package/dist/src/utils/fs.js +30 -1
- package/dist/src/utils/package-root.d.ts +5 -0
- package/dist/src/utils/package-root.js +12 -0
- package/dist/src/utils/process.js +9 -4
- package/dist/src/utils/validation.d.ts +20 -2
- package/dist/src/utils/validation.js +26 -3
- package/package.json +1 -1
|
@@ -1,16 +1,187 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
/**
|
|
3
3
|
* Removal of custom element registrations from customElements.js.
|
|
4
|
-
*
|
|
4
|
+
*
|
|
5
|
+
* Uses the same AST parser as the add path (`furnace-registration-ast.ts`) to
|
|
6
|
+
* locate and delete registration entries. The earlier implementation walked
|
|
7
|
+
* the file line-by-line with a 20-line scan bound for bracket matching, which
|
|
8
|
+
* only worked against Firefox's stock formatting and silently failed on any
|
|
9
|
+
* hand-reformatted customElements.js. AST-based bracket matching is format-
|
|
10
|
+
* agnostic by construction.
|
|
11
|
+
*
|
|
12
|
+
* Contract:
|
|
13
|
+
* - Idempotent: if the tag is not registered, the file is left unchanged.
|
|
14
|
+
* - Non-destructive on parse failure: if customElements.js cannot be parsed,
|
|
15
|
+
* the file is left untouched rather than fall through to a line-based
|
|
16
|
+
* heuristic that could delete the wrong range.
|
|
17
|
+
* - Two registration shapes are recognised:
|
|
18
|
+
* (A) Standalone statement:
|
|
19
|
+
* customElements.setElementCreationCallback("tag", ...);
|
|
20
|
+
* (B) Entry inside a `for (... of [ ... ])` registration array:
|
|
21
|
+
* ["tag", "chrome://..."]
|
|
22
|
+
* Both are deleted together with any trailing comma and newline so the
|
|
23
|
+
* resulting file is still valid JavaScript.
|
|
5
24
|
*/
|
|
6
25
|
import { join } from 'node:path';
|
|
26
|
+
import MagicString from 'magic-string';
|
|
7
27
|
import { pathExists, readText, writeText } from '../utils/fs.js';
|
|
28
|
+
import { parseScript, walkAST } from './ast-utils.js';
|
|
8
29
|
import { CUSTOM_ELEMENTS_JS } from './furnace-constants.js';
|
|
30
|
+
/**
|
|
31
|
+
* Expands `[start, end)` to consume a trailing comma and any whitespace up
|
|
32
|
+
* to (and including) the next newline. This keeps the surrounding file
|
|
33
|
+
* layout stable: deleting a list entry should not leave a dangling comma
|
|
34
|
+
* behind, and deleting an expression statement should not leave a blank
|
|
35
|
+
* line where the statement used to be.
|
|
36
|
+
*/
|
|
37
|
+
function expandRemovalRange(content, start, end) {
|
|
38
|
+
let expandedEnd = end;
|
|
39
|
+
// Walk past horizontal whitespace so a trailing comma is reachable even
|
|
40
|
+
// when the source has `[...] ,` or similar.
|
|
41
|
+
while (expandedEnd < content.length &&
|
|
42
|
+
(content[expandedEnd] === ' ' || content[expandedEnd] === '\t')) {
|
|
43
|
+
expandedEnd++;
|
|
44
|
+
}
|
|
45
|
+
if (content[expandedEnd] === ',') {
|
|
46
|
+
expandedEnd++;
|
|
47
|
+
}
|
|
48
|
+
else if (content[expandedEnd] === ';') {
|
|
49
|
+
expandedEnd++;
|
|
50
|
+
}
|
|
51
|
+
// Consume a trailing inline comment up to end-of-line so we do not leave
|
|
52
|
+
// the comment marooned on its own line. A leading inline comment is left
|
|
53
|
+
// alone — it may belong to the next entry.
|
|
54
|
+
while (expandedEnd < content.length &&
|
|
55
|
+
(content[expandedEnd] === ' ' || content[expandedEnd] === '\t')) {
|
|
56
|
+
expandedEnd++;
|
|
57
|
+
}
|
|
58
|
+
if (content[expandedEnd] === '/' && content[expandedEnd + 1] === '/') {
|
|
59
|
+
while (expandedEnd < content.length && content[expandedEnd] !== '\n') {
|
|
60
|
+
expandedEnd++;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (content[expandedEnd] === '\n') {
|
|
64
|
+
expandedEnd++;
|
|
65
|
+
}
|
|
66
|
+
// Also consume leading whitespace on the line the removal starts on, so
|
|
67
|
+
// indentation does not survive as a blank-looking line.
|
|
68
|
+
let expandedStart = start;
|
|
69
|
+
while (expandedStart > 0) {
|
|
70
|
+
const prev = content[expandedStart - 1];
|
|
71
|
+
if (prev === ' ' || prev === '\t') {
|
|
72
|
+
expandedStart--;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
// If the removal now starts at the beginning of a line and the preceding
|
|
78
|
+
// line is blank, consume that blank line as well. This matches the older
|
|
79
|
+
// line-based implementation's "eat one leading blank" behaviour so that
|
|
80
|
+
// removing a callback block from between two blank-line-separated
|
|
81
|
+
// sections does not leave a doubled-up gap behind.
|
|
82
|
+
if (expandedStart > 0 &&
|
|
83
|
+
content[expandedStart - 1] === '\n' &&
|
|
84
|
+
expandedStart >= 2 &&
|
|
85
|
+
content[expandedStart - 2] === '\n') {
|
|
86
|
+
expandedStart--;
|
|
87
|
+
}
|
|
88
|
+
return { start: expandedStart, end: expandedEnd };
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Returns true if `node` represents a `[tag, module]` entry inside a
|
|
92
|
+
* registration array — i.e. an `ArrayExpression` whose first element is a
|
|
93
|
+
* string literal equal to `tagName`. Callers still have to verify the
|
|
94
|
+
* parent is the outer registration array so we do not accidentally delete
|
|
95
|
+
* an arbitrary user-owned `["moz-card", ...]` literal elsewhere in the file.
|
|
96
|
+
*/
|
|
97
|
+
function isEntryArrayFor(node, tagName) {
|
|
98
|
+
if (!node || node.type !== 'ArrayExpression')
|
|
99
|
+
return false;
|
|
100
|
+
const [first] = node.elements;
|
|
101
|
+
if (!first || first.type !== 'Literal')
|
|
102
|
+
return false;
|
|
103
|
+
const literal = first;
|
|
104
|
+
return literal.value === tagName;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Returns true if `call` is `customElements.setElementCreationCallback(tagName, ...)`
|
|
108
|
+
* (optionally prefixed with `lazy.` or similar member chain). We only match
|
|
109
|
+
* the property name rather than the full callee chain so unusual-but-valid
|
|
110
|
+
* receivers (`lazy.customElements…`, `this.customElements…`) still count.
|
|
111
|
+
*/
|
|
112
|
+
function isStandaloneCallbackCallFor(call, tagName) {
|
|
113
|
+
if (call.callee.type !== 'MemberExpression')
|
|
114
|
+
return false;
|
|
115
|
+
const prop = call.callee.property;
|
|
116
|
+
if (prop.type !== 'Identifier' || prop.name !== 'setElementCreationCallback')
|
|
117
|
+
return false;
|
|
118
|
+
const [tagArg] = call.arguments;
|
|
119
|
+
if (!tagArg || tagArg.type !== 'Literal')
|
|
120
|
+
return false;
|
|
121
|
+
return tagArg.value === tagName;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Walks the AST and collects every removal range for `tagName`. Ancestor
|
|
125
|
+
* tracking lets us tell an "entry" array from a random `["moz-foo", ...]`
|
|
126
|
+
* literal (only direct children of the outer registration `ArrayExpression`
|
|
127
|
+
* are entries) and lets us lift a `setElementCreationCallback` call up to
|
|
128
|
+
* its enclosing statement.
|
|
129
|
+
*/
|
|
130
|
+
function collectRemovalRanges(ast, content, tagName) {
|
|
131
|
+
const ranges = [];
|
|
132
|
+
const ancestors = [];
|
|
133
|
+
walkAST(ast, {
|
|
134
|
+
enter(node) {
|
|
135
|
+
// Entry array: [tag, module] inside the outer for-of array literal.
|
|
136
|
+
// The immediate parent must be an ArrayExpression (the registration
|
|
137
|
+
// list) whose own grandparent is the `for (...) of <array>` loop.
|
|
138
|
+
if (node.type === 'ArrayExpression' && isEntryArrayFor(node, tagName)) {
|
|
139
|
+
const parent = ancestors[ancestors.length - 1];
|
|
140
|
+
const grandparent = ancestors[ancestors.length - 2];
|
|
141
|
+
if (parent &&
|
|
142
|
+
parent.type === 'ArrayExpression' &&
|
|
143
|
+
grandparent &&
|
|
144
|
+
grandparent.type === 'ForOfStatement') {
|
|
145
|
+
ranges.push(expandRemovalRange(content, node.start, node.end));
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// Standalone call: customElements.setElementCreationCallback(tag, …)
|
|
149
|
+
if (node.type === 'CallExpression') {
|
|
150
|
+
const call = node;
|
|
151
|
+
if (isStandaloneCallbackCallFor(call, tagName)) {
|
|
152
|
+
// Find the enclosing statement so we can delete `call(...);` as
|
|
153
|
+
// a unit, not just the call expression body.
|
|
154
|
+
let enclosing = null;
|
|
155
|
+
for (let i = ancestors.length - 1; i >= 0; i--) {
|
|
156
|
+
const ancestor = ancestors[i];
|
|
157
|
+
if (!ancestor)
|
|
158
|
+
continue;
|
|
159
|
+
if (ancestor.type === 'ExpressionStatement' ||
|
|
160
|
+
ancestor.type === 'VariableDeclaration') {
|
|
161
|
+
enclosing = ancestor;
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const target = enclosing ?? call;
|
|
166
|
+
ranges.push(expandRemovalRange(content, target.start, target.end));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
ancestors.push(node);
|
|
170
|
+
},
|
|
171
|
+
leave() {
|
|
172
|
+
ancestors.pop();
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
return ranges;
|
|
176
|
+
}
|
|
9
177
|
/**
|
|
10
178
|
* Removes a custom element registration from customElements.js.
|
|
11
179
|
*
|
|
12
|
-
* This operation is idempotent — if the tag is not registered or the file
|
|
13
|
-
* not exist, nothing happens.
|
|
180
|
+
* This operation is idempotent — if the tag is not registered or the file
|
|
181
|
+
* does not exist, nothing happens. If the file exists but cannot be parsed,
|
|
182
|
+
* the file is left unchanged rather than fall back to a line-based
|
|
183
|
+
* heuristic; a corrupted customElements.js is a doctor problem, not
|
|
184
|
+
* something `furnace remove` should "helpfully" edit around.
|
|
14
185
|
*
|
|
15
186
|
* @param engineDir - Path to the Firefox engine source root
|
|
16
187
|
* @param tagName - Custom element tag name to remove
|
|
@@ -20,70 +191,33 @@ export async function removeCustomElementRegistration(engineDir, tagName) {
|
|
|
20
191
|
if (!(await pathExists(filePath))) {
|
|
21
192
|
return;
|
|
22
193
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
if (
|
|
28
|
-
let endLine = callbackLine;
|
|
29
|
-
for (let i = callbackLine + 1; i < lines.length; i++) {
|
|
30
|
-
const line = lines[i];
|
|
31
|
-
if (line === undefined)
|
|
32
|
-
continue;
|
|
33
|
-
if (/^\s*\}\);/.test(line)) {
|
|
34
|
-
endLine = i;
|
|
35
|
-
break;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
let startLine = callbackLine;
|
|
39
|
-
const precedingLine = lines[startLine - 1];
|
|
40
|
-
if (startLine > 0 && precedingLine !== undefined && precedingLine.trim() === '') {
|
|
41
|
-
startLine--;
|
|
42
|
-
}
|
|
43
|
-
lines.splice(startLine, endLine - startLine + 1);
|
|
44
|
-
content = lines.join('\n');
|
|
45
|
-
await writeText(filePath, content);
|
|
194
|
+
const content = await readText(filePath);
|
|
195
|
+
// Cheap pre-check: if the tag literal never appears in the file there is
|
|
196
|
+
// nothing to remove and we avoid the cost of parsing a large file on the
|
|
197
|
+
// hot path of a no-op remove.
|
|
198
|
+
if (!content.includes(`"${tagName}"`) && !content.includes(`'${tagName}'`)) {
|
|
46
199
|
return;
|
|
47
200
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
await writeText(filePath, content);
|
|
201
|
+
let ast;
|
|
202
|
+
try {
|
|
203
|
+
ast = parseScript(content);
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
54
206
|
return;
|
|
55
207
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (multiLineTagIdx !== -1) {
|
|
59
|
-
// Scan backwards from the tag line to find the opening [ (bounded to 20 lines)
|
|
60
|
-
let startLine = multiLineTagIdx;
|
|
61
|
-
const scanLimit = Math.max(0, multiLineTagIdx - 20);
|
|
62
|
-
for (let i = multiLineTagIdx - 1; i >= scanLimit; i--) {
|
|
63
|
-
const line = lines[i];
|
|
64
|
-
if (line !== undefined && /^\s*\[$/.test(line)) {
|
|
65
|
-
startLine = i;
|
|
66
|
-
break;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
const openIndent = (lines[startLine] ?? '').match(/^(\s*)/)?.[1]?.length ?? 0;
|
|
70
|
-
// Scan forwards from the tag line to find the closing ],
|
|
71
|
-
let endLine = multiLineTagIdx;
|
|
72
|
-
for (let i = multiLineTagIdx + 1; i < lines.length; i++) {
|
|
73
|
-
const line = lines[i];
|
|
74
|
-
if (line !== undefined && /^\s*\],?\s*$/.test(line)) {
|
|
75
|
-
const closeIndent = line.match(/^(\s*)/)?.[1]?.length ?? 0;
|
|
76
|
-
if (closeIndent === openIndent) {
|
|
77
|
-
endLine = i;
|
|
78
|
-
break;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
lines.splice(startLine, endLine - startLine + 1);
|
|
83
|
-
content = lines.join('\n');
|
|
84
|
-
await writeText(filePath, content);
|
|
208
|
+
const ranges = collectRemovalRanges(ast, content, tagName);
|
|
209
|
+
if (ranges.length === 0) {
|
|
85
210
|
return;
|
|
86
211
|
}
|
|
87
|
-
//
|
|
212
|
+
// Apply all removals in a single MagicString pass. MagicString tracks
|
|
213
|
+
// original offsets so the ranges do not need to be sorted or reversed.
|
|
214
|
+
const ms = new MagicString(content);
|
|
215
|
+
for (const range of ranges) {
|
|
216
|
+
ms.remove(range.start, range.end);
|
|
217
|
+
}
|
|
218
|
+
const next = ms.toString();
|
|
219
|
+
if (next !== content) {
|
|
220
|
+
await writeText(filePath, next);
|
|
221
|
+
}
|
|
88
222
|
}
|
|
89
223
|
//# sourceMappingURL=furnace-registration-remove.js.map
|
|
@@ -2,8 +2,20 @@
|
|
|
2
2
|
* Shared validation for furnace custom element registration placement.
|
|
3
3
|
* Used after both AST and legacy code paths to avoid duplicating logic.
|
|
4
4
|
*/
|
|
5
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* Regex for valid custom element tag names. A valid name is lowercase, starts
|
|
7
|
+
* with a letter, and contains one or more hyphen-separated groups where each
|
|
8
|
+
* group is a non-empty alphanumeric run. Consecutive hyphens and trailing
|
|
9
|
+
* hyphens are both rejected. Kept in sync with the HTML custom element spec
|
|
10
|
+
* requirement that a name contain at least one hyphen.
|
|
11
|
+
*
|
|
12
|
+
* A single shared constant is used by every furnace authoring path
|
|
13
|
+
* (`furnace create`, `furnace override`, and the AST registration helper) so
|
|
14
|
+
* that a name accepted by one command cannot be rejected by another.
|
|
15
|
+
*/
|
|
6
16
|
export declare const CUSTOM_ELEMENT_TAG_PATTERN: RegExp;
|
|
17
|
+
/** Human-readable description of the tag-name rules, for CLI error messages. */
|
|
18
|
+
export declare const CUSTOM_ELEMENT_TAG_RULES = "must be lowercase, start with a letter, and use hyphens to separate non-empty alphanumeric groups (e.g., \"my-widget\")";
|
|
7
19
|
/**
|
|
8
20
|
* Validates that a tag name conforms to custom element naming requirements.
|
|
9
21
|
* @throws FurnaceError if the tag name is invalid
|
|
@@ -4,15 +4,27 @@
|
|
|
4
4
|
* Used after both AST and legacy code paths to avoid duplicating logic.
|
|
5
5
|
*/
|
|
6
6
|
import { FurnaceError } from '../errors/furnace.js';
|
|
7
|
-
/**
|
|
8
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Regex for valid custom element tag names. A valid name is lowercase, starts
|
|
9
|
+
* with a letter, and contains one or more hyphen-separated groups where each
|
|
10
|
+
* group is a non-empty alphanumeric run. Consecutive hyphens and trailing
|
|
11
|
+
* hyphens are both rejected. Kept in sync with the HTML custom element spec
|
|
12
|
+
* requirement that a name contain at least one hyphen.
|
|
13
|
+
*
|
|
14
|
+
* A single shared constant is used by every furnace authoring path
|
|
15
|
+
* (`furnace create`, `furnace override`, and the AST registration helper) so
|
|
16
|
+
* that a name accepted by one command cannot be rejected by another.
|
|
17
|
+
*/
|
|
18
|
+
export const CUSTOM_ELEMENT_TAG_PATTERN = /^[a-z][a-z0-9]*(-[a-z0-9]+)+$/;
|
|
19
|
+
/** Human-readable description of the tag-name rules, for CLI error messages. */
|
|
20
|
+
export const CUSTOM_ELEMENT_TAG_RULES = 'must be lowercase, start with a letter, and use hyphens to separate non-empty alphanumeric groups (e.g., "my-widget")';
|
|
9
21
|
/**
|
|
10
22
|
* Validates that a tag name conforms to custom element naming requirements.
|
|
11
23
|
* @throws FurnaceError if the tag name is invalid
|
|
12
24
|
*/
|
|
13
25
|
export function validateTagName(tagName) {
|
|
14
26
|
if (!CUSTOM_ELEMENT_TAG_PATTERN.test(tagName)) {
|
|
15
|
-
throw new FurnaceError(`Invalid tag name "${tagName}":
|
|
27
|
+
throw new FurnaceError(`Invalid tag name "${tagName}": ${CUSTOM_ELEMENT_TAG_RULES}`, tagName);
|
|
16
28
|
}
|
|
17
29
|
}
|
|
18
30
|
/**
|
|
@@ -1,16 +1,34 @@
|
|
|
1
1
|
export { CUSTOM_ELEMENTS_JS, JAR_MN } from './furnace-constants.js';
|
|
2
|
-
export { addCustomElementRegistration, removeCustomElementRegistration, } from './furnace-registration-ast.js';
|
|
2
|
+
export { addCustomElementRegistration, removeCustomElementRegistration, validateCustomElementRegistration, } from './furnace-registration-ast.js';
|
|
3
3
|
/**
|
|
4
4
|
* Adds jar.mn entries that map chrome:// URIs to on-disk paths for a
|
|
5
5
|
* component's files.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
7
|
+
* ## Assumed jar.mn format
|
|
8
|
+
*
|
|
9
|
+
* Firefox's `toolkit/content/jar.mn` uses a stable format that has not
|
|
10
|
+
* changed in the custom-element era (Firefox 90+). The entry format is:
|
|
11
|
+
*
|
|
8
12
|
* ```
|
|
9
13
|
* content/global/elements/{file} (widgets/{tagName}/{file})
|
|
10
14
|
* ```
|
|
11
15
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
16
|
+
* - **Indent**: detected dynamically from the nearest existing
|
|
17
|
+
* `content/global/elements/` line. Falls back to 3 spaces when no
|
|
18
|
+
* reference line exists.
|
|
19
|
+
* - **Insertion point**: identified by existing lines matching the regex
|
|
20
|
+
* `^\s+content\/global\/elements\/([^.]+)\.` — new entries are inserted
|
|
21
|
+
* in alphabetical order relative to these.
|
|
22
|
+
* - **Fallback**: if no `content/global/elements/` line exists (empty
|
|
23
|
+
* project), looks for any `content/global/` line and inserts after it.
|
|
24
|
+
* - **Idempotency**: entries already present (checked by exact path match
|
|
25
|
+
* on `content/global/elements/{file}` with a trailing whitespace or
|
|
26
|
+
* end-of-line boundary) are skipped.
|
|
27
|
+
*
|
|
28
|
+
* If Firefox upstream changes the jar.mn section ordering or switches to a
|
|
29
|
+
* different resource registration mechanism, the preflight validation in
|
|
30
|
+
* `validateJarMnEntries` will catch the format mismatch before any writes
|
|
31
|
+
* occur.
|
|
14
32
|
*
|
|
15
33
|
* @param engineDir - Path to the Firefox engine source root
|
|
16
34
|
* @param tagName - Custom element tag name
|
|
@@ -27,3 +45,8 @@ export declare function addJarMnEntries(engineDir: string, tagName: string, file
|
|
|
27
45
|
* @param tagName - Custom element tag name whose entries should be removed
|
|
28
46
|
*/
|
|
29
47
|
export declare function removeJarMnEntries(engineDir: string, tagName: string): Promise<void>;
|
|
48
|
+
/**
|
|
49
|
+
* Validates that jar.mn entries *could* be added without writing anything.
|
|
50
|
+
* Used by dry-run to surface structural problems early.
|
|
51
|
+
*/
|
|
52
|
+
export declare function validateJarMnEntries(engineDir: string, tagName: string, files: string[]): Promise<void>;
|
|
@@ -2,21 +2,57 @@
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { FurnaceError } from '../errors/furnace.js';
|
|
4
4
|
import { pathExists, readText, writeText } from '../utils/fs.js';
|
|
5
|
+
/** Escapes special regex characters in a literal string. */
|
|
6
|
+
function escapeForRegex(str) {
|
|
7
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Detects the indentation used by existing `content/global/elements/` lines
|
|
11
|
+
* in jar.mn. Falls back to 3 spaces (the historical Firefox convention) when
|
|
12
|
+
* no reference line is found.
|
|
13
|
+
*/
|
|
14
|
+
function detectJarMnIndent(lines) {
|
|
15
|
+
for (const line of lines) {
|
|
16
|
+
const match = /^(\s+)content\/global\/elements\//.exec(line);
|
|
17
|
+
if (match?.[1]) {
|
|
18
|
+
return match[1];
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return ' ';
|
|
22
|
+
}
|
|
5
23
|
// Re-export everything from the AST module so existing imports keep working
|
|
6
24
|
export { CUSTOM_ELEMENTS_JS, JAR_MN } from './furnace-constants.js';
|
|
7
|
-
export { addCustomElementRegistration, removeCustomElementRegistration, } from './furnace-registration-ast.js';
|
|
25
|
+
export { addCustomElementRegistration, removeCustomElementRegistration, validateCustomElementRegistration, } from './furnace-registration-ast.js';
|
|
8
26
|
import { JAR_MN } from './furnace-constants.js';
|
|
9
27
|
/**
|
|
10
28
|
* Adds jar.mn entries that map chrome:// URIs to on-disk paths for a
|
|
11
29
|
* component's files.
|
|
12
30
|
*
|
|
13
|
-
*
|
|
31
|
+
* ## Assumed jar.mn format
|
|
32
|
+
*
|
|
33
|
+
* Firefox's `toolkit/content/jar.mn` uses a stable format that has not
|
|
34
|
+
* changed in the custom-element era (Firefox 90+). The entry format is:
|
|
35
|
+
*
|
|
14
36
|
* ```
|
|
15
37
|
* content/global/elements/{file} (widgets/{tagName}/{file})
|
|
16
38
|
* ```
|
|
17
39
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
40
|
+
* - **Indent**: detected dynamically from the nearest existing
|
|
41
|
+
* `content/global/elements/` line. Falls back to 3 spaces when no
|
|
42
|
+
* reference line exists.
|
|
43
|
+
* - **Insertion point**: identified by existing lines matching the regex
|
|
44
|
+
* `^\s+content\/global\/elements\/([^.]+)\.` — new entries are inserted
|
|
45
|
+
* in alphabetical order relative to these.
|
|
46
|
+
* - **Fallback**: if no `content/global/elements/` line exists (empty
|
|
47
|
+
* project), looks for any `content/global/` line and inserts after it.
|
|
48
|
+
* - **Idempotency**: entries already present (checked by exact path match
|
|
49
|
+
* on `content/global/elements/{file}` with a trailing whitespace or
|
|
50
|
+
* end-of-line boundary) are skipped.
|
|
51
|
+
*
|
|
52
|
+
* If Firefox upstream changes the jar.mn section ordering or switches to a
|
|
53
|
+
* different resource registration mechanism, the preflight validation in
|
|
54
|
+
* `validateJarMnEntries` will catch the format mismatch before any writes
|
|
55
|
+
* occur.
|
|
20
56
|
*
|
|
21
57
|
* @param engineDir - Path to the Firefox engine source root
|
|
22
58
|
* @param tagName - Custom element tag name
|
|
@@ -29,12 +65,14 @@ export async function addJarMnEntries(engineDir, tagName, files) {
|
|
|
29
65
|
}
|
|
30
66
|
let content = await readText(filePath);
|
|
31
67
|
const lines = content.split('\n');
|
|
32
|
-
// Filter to files not already registered
|
|
33
|
-
|
|
68
|
+
// Filter to files not already registered. Use a word-boundary-aware
|
|
69
|
+
// check so that "moz-card.css" does not match "moz-card-group.css".
|
|
70
|
+
const newFiles = files.filter((f) => !new RegExp(`content/global/elements/${escapeForRegex(f)}(?:\\s|$)`, 'm').test(content));
|
|
34
71
|
if (newFiles.length === 0)
|
|
35
72
|
return;
|
|
36
|
-
// Build new entry lines
|
|
37
|
-
const
|
|
73
|
+
// Build new entry lines using the indent detected from existing entries.
|
|
74
|
+
const indent = detectJarMnIndent(lines);
|
|
75
|
+
const newEntries = newFiles.map((f) => `${indent}content/global/elements/${f} (widgets/${tagName}/${f})`);
|
|
38
76
|
// Find insertion point among existing content/global/elements/ lines
|
|
39
77
|
const elementLinePattern = /^\s+content\/global\/elements\/([^.]+)\./;
|
|
40
78
|
let insertIndex = -1;
|
|
@@ -64,7 +102,11 @@ export async function addJarMnEntries(engineDir, tagName, files) {
|
|
|
64
102
|
}
|
|
65
103
|
}
|
|
66
104
|
if (insertIndex === -1) {
|
|
67
|
-
|
|
105
|
+
const nonEmpty = lines.some((line) => line.trim().length > 0);
|
|
106
|
+
if (!nonEmpty) {
|
|
107
|
+
throw new FurnaceError('jar.mn is empty or contains only whitespace. It may be malformed — verify the engine was downloaded correctly.', tagName);
|
|
108
|
+
}
|
|
109
|
+
throw new FurnaceError('Could not find a content/global/ section in jar.mn for element entries. The file may be malformed.', tagName);
|
|
68
110
|
}
|
|
69
111
|
lines.splice(insertIndex, 0, ...newEntries);
|
|
70
112
|
content = lines.join('\n');
|
|
@@ -86,11 +128,51 @@ export async function removeJarMnEntries(engineDir, tagName) {
|
|
|
86
128
|
}
|
|
87
129
|
let content = await readText(filePath);
|
|
88
130
|
const lines = content.split('\n');
|
|
89
|
-
|
|
90
|
-
const
|
|
131
|
+
// Use a regex with word boundary so "moz-card" does not match "moz-card-group".
|
|
132
|
+
const pattern = new RegExp(`content/global/elements/${escapeForRegex(tagName)}\\.`);
|
|
133
|
+
const filtered = lines.filter((line) => !pattern.test(line));
|
|
91
134
|
if (filtered.length === lines.length)
|
|
92
135
|
return;
|
|
93
136
|
content = filtered.join('\n');
|
|
94
137
|
await writeText(filePath, content);
|
|
95
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* Validates that jar.mn entries *could* be added without writing anything.
|
|
141
|
+
* Used by dry-run to surface structural problems early.
|
|
142
|
+
*/
|
|
143
|
+
export async function validateJarMnEntries(engineDir, tagName, files) {
|
|
144
|
+
const filePath = join(engineDir, JAR_MN);
|
|
145
|
+
if (!(await pathExists(filePath))) {
|
|
146
|
+
throw new FurnaceError('jar.mn not found in engine', tagName);
|
|
147
|
+
}
|
|
148
|
+
const content = await readText(filePath);
|
|
149
|
+
const lines = content.split('\n');
|
|
150
|
+
const newFiles = files.filter((f) => !new RegExp(`content/global/elements/${escapeForRegex(f)}(?:\\s|$)`, 'm').test(content));
|
|
151
|
+
if (newFiles.length === 0)
|
|
152
|
+
return;
|
|
153
|
+
const elementLinePattern = /^\s+content\/global\/elements\/([^.]+)\./;
|
|
154
|
+
let hasInsertionPoint = false;
|
|
155
|
+
for (const line of lines) {
|
|
156
|
+
if (elementLinePattern.test(line)) {
|
|
157
|
+
hasInsertionPoint = true;
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (!hasInsertionPoint) {
|
|
162
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
163
|
+
const line = lines[i];
|
|
164
|
+
if (line !== undefined && /^\s+content\/global\//.test(line)) {
|
|
165
|
+
hasInsertionPoint = true;
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (!hasInsertionPoint) {
|
|
171
|
+
const nonEmpty = lines.some((line) => line.trim().length > 0);
|
|
172
|
+
if (!nonEmpty) {
|
|
173
|
+
throw new FurnaceError('jar.mn is empty or contains only whitespace. It may be malformed — verify the engine was downloaded correctly.', tagName);
|
|
174
|
+
}
|
|
175
|
+
throw new FurnaceError('Could not find a content/global/ section in jar.mn for element entries. The file may be malformed.', tagName);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
96
178
|
//# sourceMappingURL=furnace-registration.js.map
|
|
@@ -6,11 +6,22 @@ interface FileSnapshot {
|
|
|
6
6
|
export interface RollbackJournal {
|
|
7
7
|
files: Map<string, FileSnapshot>;
|
|
8
8
|
createdDirs: Set<string>;
|
|
9
|
+
/** Paths that were skipped during snapshotDir because they are symlinks. */
|
|
10
|
+
skippedSymlinks: Set<string>;
|
|
9
11
|
}
|
|
10
12
|
/** Creates an empty rollback journal for tracking touched files and created directories. */
|
|
11
13
|
export declare function createRollbackJournal(): RollbackJournal;
|
|
12
14
|
/** Records a directory that should be removed if the operation later rolls back. */
|
|
13
15
|
export declare function recordCreatedDir(journal: RollbackJournal, dirPath: string): void;
|
|
16
|
+
/**
|
|
17
|
+
* Recursively snapshots every file under a directory tree so a later rollback
|
|
18
|
+
* can restore deleted files. Skips symlinks to avoid following them out of the
|
|
19
|
+
* tree. The directory itself is not recorded as "created" — callers that
|
|
20
|
+
* intend to delete and restore the directory should record it explicitly.
|
|
21
|
+
*
|
|
22
|
+
* Safe to call on a missing path: it returns without recording anything.
|
|
23
|
+
*/
|
|
24
|
+
export declare function snapshotDir(journal: RollbackJournal, dirPath: string): Promise<void>;
|
|
14
25
|
/** Snapshots a file once so rollback can restore its previous contents or absence. */
|
|
15
26
|
export declare function snapshotFile(journal: RollbackJournal, filePath: string): Promise<void>;
|
|
16
27
|
/** Restores all snapshotted files and removes directories created during the operation. */
|