@hominis/fireforge 0.16.2 → 0.16.5
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 +15 -2
- package/dist/bin/fireforge.js +11 -2
- package/dist/src/commands/doctor-furnace.js +83 -1
- package/dist/src/commands/doctor.js +18 -0
- package/dist/src/commands/download.js +58 -12
- package/dist/src/commands/export-all.js +19 -2
- package/dist/src/commands/export-shared.d.ts +36 -0
- package/dist/src/commands/export-shared.js +76 -0
- package/dist/src/commands/export.js +23 -2
- package/dist/src/commands/furnace/chrome-doc-templates.d.ts +21 -3
- package/dist/src/commands/furnace/chrome-doc-templates.js +23 -5
- package/dist/src/commands/furnace/chrome-doc-tests.js +42 -17
- package/dist/src/commands/furnace/create-readback.d.ts +23 -0
- package/dist/src/commands/furnace/create-readback.js +34 -0
- package/dist/src/commands/furnace/create-templates.d.ts +17 -7
- package/dist/src/commands/furnace/create-templates.js +85 -31
- package/dist/src/commands/furnace/create-xpcshell.d.ts +1 -1
- package/dist/src/commands/furnace/create-xpcshell.js +1 -1
- package/dist/src/commands/furnace/create.js +2 -0
- package/dist/src/commands/furnace/preview.d.ts +12 -0
- package/dist/src/commands/furnace/preview.js +34 -2
- package/dist/src/commands/furnace/status.js +1 -1
- package/dist/src/commands/import.js +63 -11
- package/dist/src/commands/patch/delete.js +10 -1
- package/dist/src/commands/patch/index.js +10 -1
- package/dist/src/commands/re-export.js +79 -6
- package/dist/src/commands/resolve.js +15 -1
- package/dist/src/commands/run.js +27 -5
- package/dist/src/commands/setup-support.js +60 -7
- package/dist/src/commands/status.js +28 -1
- package/dist/src/commands/test.js +28 -5
- package/dist/src/commands/token-coverage.js +55 -1
- package/dist/src/commands/token.js +19 -2
- package/dist/src/commands/wire.js +22 -2
- package/dist/src/core/branding.d.ts +10 -0
- package/dist/src/core/branding.js +7 -9
- package/dist/src/core/build-prepare.js +8 -1
- package/dist/src/core/file-lock.js +49 -15
- package/dist/src/core/furnace-operation.d.ts +17 -0
- package/dist/src/core/furnace-operation.js +30 -1
- package/dist/src/core/furnace-validate-helpers.d.ts +33 -1
- package/dist/src/core/furnace-validate-helpers.js +53 -2
- package/dist/src/core/git.js +39 -10
- package/dist/src/core/mach-error-hints.js +16 -0
- package/dist/src/core/mach.js +15 -6
- package/dist/src/core/manifest-rules.js +16 -0
- package/dist/src/core/marionette-preflight.js +43 -12
- package/dist/src/core/patch-files.d.ts +12 -1
- package/dist/src/core/patch-files.js +14 -11
- package/dist/src/core/patch-lint.js +62 -11
- package/dist/src/core/wire-destroy.js +18 -5
- package/dist/src/core/wire-init.js +20 -5
- package/dist/src/core/wire-utils.d.ts +15 -0
- package/dist/src/core/wire-utils.js +17 -0
- package/dist/src/types/commands/options.d.ts +7 -0
- package/package.json +1 -1
|
@@ -110,6 +110,16 @@ export async function runMarionettePreflight(engineDir, options = {}) {
|
|
|
110
110
|
cwd: engineDir,
|
|
111
111
|
env: { ...process.env, MOZ_HEADLESS: '1' },
|
|
112
112
|
stdio: ['ignore', 'ignore', 'pipe'],
|
|
113
|
+
// `detached: true` puts the child in a new process group so we can
|
|
114
|
+
// signal it and every descendant (Firefox, its helpers) via
|
|
115
|
+
// `process.kill(-pid, …)` in the finally block. Without this, the
|
|
116
|
+
// child is Python running mach; a SIGTERM kills Python but the
|
|
117
|
+
// Firefox grandchild inherits the stderr pipe FD and keeps Node's
|
|
118
|
+
// event loop alive even after the preflight PASS log. The symptom
|
|
119
|
+
// is `fireforge test --doctor` printing `Marionette preflight:
|
|
120
|
+
// PASS` and then hanging indefinitely in `uv__io_poll` — see the
|
|
121
|
+
// eval finding.
|
|
122
|
+
detached: true,
|
|
113
123
|
});
|
|
114
124
|
}
|
|
115
125
|
catch (error) {
|
|
@@ -154,24 +164,21 @@ export async function runMarionettePreflight(engineDir, options = {}) {
|
|
|
154
164
|
}
|
|
155
165
|
finally {
|
|
156
166
|
if (child && !hasChildExited(child)) {
|
|
157
|
-
|
|
158
|
-
child.kill('SIGTERM');
|
|
159
|
-
}
|
|
160
|
-
catch {
|
|
161
|
-
// Already exited — nothing to do.
|
|
162
|
-
}
|
|
167
|
+
killProcessGroup(child, 'SIGTERM');
|
|
163
168
|
// Small escalation: if the child doesn't honour SIGTERM quickly, SIGKILL
|
|
164
169
|
// so we don't leave a ghost mach process around after a failed probe.
|
|
165
170
|
await delay(500);
|
|
166
171
|
if (!hasChildExited(child)) {
|
|
167
|
-
|
|
168
|
-
child.kill('SIGKILL');
|
|
169
|
-
}
|
|
170
|
-
catch {
|
|
171
|
-
// Already gone.
|
|
172
|
-
}
|
|
172
|
+
killProcessGroup(child, 'SIGKILL');
|
|
173
173
|
}
|
|
174
174
|
}
|
|
175
|
+
// Destroy the stderr pipe explicitly. Firefox (a grandchild of the Python
|
|
176
|
+
// mach wrapper we spawned) can inherit and hold the stderr FD even after
|
|
177
|
+
// its direct parent exits — until the pipe closes, Node's readable
|
|
178
|
+
// stream stays attached and `uv__io_poll` keeps the event loop alive.
|
|
179
|
+
// `destroy()` closes the local end regardless, so `fireforge test
|
|
180
|
+
// --doctor` exits cleanly after a passing preflight.
|
|
181
|
+
child?.stderr?.destroy();
|
|
175
182
|
try {
|
|
176
183
|
await rm(profileDir, { recursive: true, force: true });
|
|
177
184
|
}
|
|
@@ -180,6 +187,30 @@ export async function runMarionettePreflight(engineDir, options = {}) {
|
|
|
180
187
|
}
|
|
181
188
|
}
|
|
182
189
|
}
|
|
190
|
+
/**
|
|
191
|
+
* Sends `signal` to the child's whole process group when possible, falling
|
|
192
|
+
* back to a direct `child.kill` for environments that don't support
|
|
193
|
+
* `detached: true` (Windows in particular: Node still returns a pid, but
|
|
194
|
+
* `kill(-pid, …)` is not a supported kernel primitive).
|
|
195
|
+
*/
|
|
196
|
+
function killProcessGroup(child, signal) {
|
|
197
|
+
if (child.pid !== undefined && process.platform !== 'win32') {
|
|
198
|
+
try {
|
|
199
|
+
process.kill(-child.pid, signal);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
// ESRCH / EPERM — fall through to the narrow kill below so we at
|
|
204
|
+
// least signal the direct child.
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
try {
|
|
208
|
+
child.kill(signal);
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
// Already exited — nothing to do.
|
|
212
|
+
}
|
|
213
|
+
}
|
|
183
214
|
function fail(layer, message, durationMs) {
|
|
184
215
|
return {
|
|
185
216
|
ok: false,
|
|
@@ -7,5 +7,16 @@ export declare function countPatches(patchesDir: string): Promise<number>;
|
|
|
7
7
|
export declare function isNewFilePatch(patchPath: string): Promise<boolean>;
|
|
8
8
|
/** Returns the first target file path referenced by a patch, if any. */
|
|
9
9
|
export declare function getTargetFileFromPatch(patchPath: string): Promise<string | null>;
|
|
10
|
-
/**
|
|
10
|
+
/**
|
|
11
|
+
* Returns all target file paths referenced by a multi-file patch.
|
|
12
|
+
*
|
|
13
|
+
* Delegates to {@link extractAffectedFiles} so `GIT binary patch` sections
|
|
14
|
+
* (which have no `+++ b/…` line, only a `diff --git a/… b/…` header) are
|
|
15
|
+
* included alongside text hunks. Before this delegation the custom regex
|
|
16
|
+
* only matched `+++ b/…` lines, dropping every binary file from
|
|
17
|
+
* `filesAffected` — verify reported `files-affected-mismatch` against
|
|
18
|
+
* branding patches and `doctor --repair-patches-manifest` "repaired" the
|
|
19
|
+
* manifest by rewriting it to the text-only subset, hiding the true scope
|
|
20
|
+
* of the patch.
|
|
21
|
+
*/
|
|
11
22
|
export declare function getAllTargetFilesFromPatch(patchPath: string): Promise<string[]>;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { readdir } from 'node:fs/promises';
|
|
3
3
|
import { extname, join } from 'node:path';
|
|
4
4
|
import { pathExists, readText } from '../utils/fs.js';
|
|
5
|
-
import { extractOrder } from './patch-parse.js';
|
|
5
|
+
import { extractAffectedFiles, extractOrder } from './patch-parse.js';
|
|
6
6
|
/** Discovers patch files in a directory and returns them in apply order. */
|
|
7
7
|
export async function discoverPatches(patchesDir) {
|
|
8
8
|
if (!(await pathExists(patchesDir))) {
|
|
@@ -35,17 +35,20 @@ export async function getTargetFileFromPatch(patchPath) {
|
|
|
35
35
|
const match = /^\+\+\+ b\/(.+)$/m.exec(content);
|
|
36
36
|
return match?.[1] ?? null;
|
|
37
37
|
}
|
|
38
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* Returns all target file paths referenced by a multi-file patch.
|
|
40
|
+
*
|
|
41
|
+
* Delegates to {@link extractAffectedFiles} so `GIT binary patch` sections
|
|
42
|
+
* (which have no `+++ b/…` line, only a `diff --git a/… b/…` header) are
|
|
43
|
+
* included alongside text hunks. Before this delegation the custom regex
|
|
44
|
+
* only matched `+++ b/…` lines, dropping every binary file from
|
|
45
|
+
* `filesAffected` — verify reported `files-affected-mismatch` against
|
|
46
|
+
* branding patches and `doctor --repair-patches-manifest` "repaired" the
|
|
47
|
+
* manifest by rewriting it to the text-only subset, hiding the true scope
|
|
48
|
+
* of the patch.
|
|
49
|
+
*/
|
|
39
50
|
export async function getAllTargetFilesFromPatch(patchPath) {
|
|
40
51
|
const content = await readText(patchPath);
|
|
41
|
-
|
|
42
|
-
const regex = /^\+\+\+ b\/(.+)$/gm;
|
|
43
|
-
let match;
|
|
44
|
-
while ((match = regex.exec(content)) !== null) {
|
|
45
|
-
if (match[1]) {
|
|
46
|
-
files.push(match[1]);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
return files;
|
|
52
|
+
return extractAffectedFiles(content);
|
|
50
53
|
}
|
|
51
54
|
//# sourceMappingURL=patch-files.js.map
|
|
@@ -60,7 +60,28 @@ export function countNonBinaryDiffLines(diffContent) {
|
|
|
60
60
|
const PATCH_LINE_THRESHOLDS = {
|
|
61
61
|
general: { notice: 800, warning: 1500, error: 3000 },
|
|
62
62
|
test: { notice: 1500, warning: 3000, error: 6000 },
|
|
63
|
+
/**
|
|
64
|
+
* Branding patches have a legitimate reason to be large: they include
|
|
65
|
+
* every locale's `brand.ftl`, copied upstream CSS/PNG assets, and the
|
|
66
|
+
* fork-specific `configure.sh` / `brand.properties` under a single
|
|
67
|
+
* `browser/branding/<name>/` subtree. The general hard limit of 3000
|
|
68
|
+
* lines fires on even a first-export branding patch (the eval saw 15904
|
|
69
|
+
* lines for a freshly-setup fork), which is loud but not actionable —
|
|
70
|
+
* the patch already is the minimum branding diff. A dedicated tier keeps
|
|
71
|
+
* the size guidance while moving the hard limit to a threshold that
|
|
72
|
+
* corresponds to "something other than branding is bundled in here too".
|
|
73
|
+
*/
|
|
74
|
+
branding: { notice: 3000, warning: 8000, error: 20000 },
|
|
63
75
|
};
|
|
76
|
+
/**
|
|
77
|
+
* Returns true when every file in a patch lives under `browser/branding/`.
|
|
78
|
+
* Used by `lintPatchSize` to pick the branding threshold tier.
|
|
79
|
+
*/
|
|
80
|
+
function isBrandingOnlyPatch(files) {
|
|
81
|
+
if (files.length === 0)
|
|
82
|
+
return false;
|
|
83
|
+
return files.every((file) => file.startsWith('browser/branding/'));
|
|
84
|
+
}
|
|
64
85
|
/**
|
|
65
86
|
* Returns true if the filename looks like a JS/MJS/JSM file.
|
|
66
87
|
* Handles `.sys.mjs` as well.
|
|
@@ -133,10 +154,18 @@ export async function lintPatchedCss(repoDir, affectedFiles, diffContent, config
|
|
|
133
154
|
// Strip block comments before scanning
|
|
134
155
|
const cssContent = rawCss.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
135
156
|
// Check only introduced raw color values when diff context is available.
|
|
136
|
-
// Skip files on the raw-color allowlist (exact path or basename match)
|
|
157
|
+
// Skip files on the raw-color allowlist (exact path or basename match) and
|
|
158
|
+
// auto-exempt files under `browser/branding/` — those are the fork's
|
|
159
|
+
// visual identity assets (app-about dialogs, installer pages, branded
|
|
160
|
+
// CSS copied from Firefox's `unofficial` template) and belong to the
|
|
161
|
+
// design-decision layer the design-token system does not govern.
|
|
162
|
+
// Without this auto-exemption, every first-time setup's copied CSS
|
|
163
|
+
// failed `raw-color-value` with no actionable fix other than manually
|
|
164
|
+
// listing each path in `rawColorAllowlist`.
|
|
137
165
|
const allowlist = config?.patchLint?.rawColorAllowlist;
|
|
138
166
|
const isAllowlisted = allowlist?.some((entry) => file === entry || file.endsWith('/' + entry));
|
|
139
|
-
|
|
167
|
+
const isBranding = file.startsWith('browser/branding/');
|
|
168
|
+
if (!isAllowlisted && !isBranding) {
|
|
140
169
|
// Strip lines with inline fireforge-ignore: raw-color-value suppression.
|
|
141
170
|
// Check against rawCss (before comment stripping) so the CSS comment marker is still present.
|
|
142
171
|
const sourceForSuppression = addedLinesByFile
|
|
@@ -239,14 +268,25 @@ export async function lintNewFileHeaders(repoDir, newFiles, config) {
|
|
|
239
268
|
continue;
|
|
240
269
|
const content = await readText(filePath);
|
|
241
270
|
const expectedHeader = getLicenseHeader(license, style);
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
271
|
+
// Auto-exempt `browser/branding/` when the file carries ANY recognised
|
|
272
|
+
// license header in the matching comment style. The setup-generated
|
|
273
|
+
// branding directory is copied from Firefox's `unofficial` template,
|
|
274
|
+
// which arrives with Mozilla MPL-2.0 headers — those are legitimate
|
|
275
|
+
// for copyright purposes (the assets are Mozilla's) even when the
|
|
276
|
+
// fork's own code is 0BSD / EUPL-1.2 / GPL-2.0-or-later. The narrower
|
|
277
|
+
// license-match rule would force operators to either rewrite the
|
|
278
|
+
// copied headers (misrepresenting authorship) or suppress the lint
|
|
279
|
+
// with `--skip-lint` (hiding real issues elsewhere).
|
|
280
|
+
if (content.startsWith(expectedHeader))
|
|
281
|
+
continue;
|
|
282
|
+
if (file.startsWith('browser/branding/') && hasAnyLicenseHeader(content, style))
|
|
283
|
+
continue;
|
|
284
|
+
issues.push({
|
|
285
|
+
file,
|
|
286
|
+
check: 'missing-license-header',
|
|
287
|
+
message: `New file is missing the required ${license} license header.`,
|
|
288
|
+
severity: 'error',
|
|
289
|
+
});
|
|
250
290
|
}
|
|
251
291
|
return issues;
|
|
252
292
|
}
|
|
@@ -402,8 +442,19 @@ export function lintPatchSize(filesAffected, lineCount) {
|
|
|
402
442
|
severity: 'warning',
|
|
403
443
|
});
|
|
404
444
|
}
|
|
445
|
+
// Tier selection: test > branding > general. Tests keep their elevated
|
|
446
|
+
// thresholds because a big regression test is legitimate (table-driven
|
|
447
|
+
// harnesses run into the thousands of lines). Branding patches get their
|
|
448
|
+
// own tier so a first-export of setup-generated branding doesn't fire
|
|
449
|
+
// the general hard limit — see `PATCH_LINE_THRESHOLDS.branding` above
|
|
450
|
+
// for the eval data motivating this tier.
|
|
405
451
|
const allTests = filesAffected.length > 0 && filesAffected.every(isTestFile);
|
|
406
|
-
const
|
|
452
|
+
const branding = !allTests && isBrandingOnlyPatch(filesAffected);
|
|
453
|
+
const thresholds = allTests
|
|
454
|
+
? PATCH_LINE_THRESHOLDS.test
|
|
455
|
+
: branding
|
|
456
|
+
? PATCH_LINE_THRESHOLDS.branding
|
|
457
|
+
: PATCH_LINE_THRESHOLDS.general;
|
|
407
458
|
if (lineCount >= thresholds.error) {
|
|
408
459
|
issues.push({
|
|
409
460
|
file: '(patch)',
|
|
@@ -10,7 +10,7 @@ import { pathExists, readText, writeText } from '../utils/fs.js';
|
|
|
10
10
|
import { escapeRegex } from '../utils/regex.js';
|
|
11
11
|
import { detectIndent, parseScript } from './ast-utils.js';
|
|
12
12
|
import { withParserFallback } from './parser-fallback.js';
|
|
13
|
-
import { assertBraceBalancePreserved, extractNameFromExpression, findMethodBody, findMethodBraceIndex, validateWireName, } from './wire-utils.js';
|
|
13
|
+
import { assertBraceBalancePreserved, coerceToCall, extractNameFromExpression, findMethodBody, findMethodBraceIndex, validateWireName, } from './wire-utils.js';
|
|
14
14
|
const BROWSER_INIT_JS = 'browser/base/content/browser-init.js';
|
|
15
15
|
/**
|
|
16
16
|
* AST-based implementation: finds onUnload()/uninit() method body and
|
|
@@ -18,6 +18,12 @@ const BROWSER_INIT_JS = 'browser/base/content/browser-init.js';
|
|
|
18
18
|
*/
|
|
19
19
|
export function addDestroyAST(content, expression) {
|
|
20
20
|
const name = extractNameFromExpression(expression);
|
|
21
|
+
// See wire-init.ts for the rationale: the template interpolates the
|
|
22
|
+
// expression verbatim, so a bare `Foo.bar` compiled to `Foo.bar;`
|
|
23
|
+
// (a property reference) instead of `Foo.bar();`. `coerceToCall`
|
|
24
|
+
// appends `()` when absent so the emitted block always invokes the
|
|
25
|
+
// teardown hook the operator asked for.
|
|
26
|
+
const callExpression = coerceToCall(expression);
|
|
21
27
|
const ast = parseScript(content);
|
|
22
28
|
const ms = new MagicString(content);
|
|
23
29
|
const body = findMethodBody(ast, ['onUnload', 'uninit']);
|
|
@@ -41,7 +47,7 @@ export function addDestroyAST(content, expression) {
|
|
|
41
47
|
`${indent}// ${name} destroy`,
|
|
42
48
|
`${indent}try {`,
|
|
43
49
|
`${indent} if (typeof ${name} !== "undefined") {`,
|
|
44
|
-
`${indent} ${
|
|
50
|
+
`${indent} ${callExpression};`,
|
|
45
51
|
`${indent} }`,
|
|
46
52
|
`${indent}} catch (e) {`,
|
|
47
53
|
`${indent} console.error("${name} destroy failed:", e);`,
|
|
@@ -55,6 +61,9 @@ export function addDestroyAST(content, expression) {
|
|
|
55
61
|
*/
|
|
56
62
|
export function legacyAddDestroy(content, expression) {
|
|
57
63
|
const name = extractNameFromExpression(expression);
|
|
64
|
+
// Match the AST path on the call-coercion contract so fallback vs AST
|
|
65
|
+
// emits identical blocks (see wire-init.ts).
|
|
66
|
+
const callExpression = coerceToCall(expression);
|
|
58
67
|
const lines = content.split('\n');
|
|
59
68
|
const destroyRegex = /\b(?:async\s+)?(onUnload|uninit)\s*[(:]/;
|
|
60
69
|
const found = findMethodBraceIndex(lines, destroyRegex, { requireBrace: true });
|
|
@@ -67,7 +76,7 @@ export function legacyAddDestroy(content, expression) {
|
|
|
67
76
|
` // ${name} destroy`,
|
|
68
77
|
` try {`,
|
|
69
78
|
` if (typeof ${name} !== "undefined") {`,
|
|
70
|
-
` ${
|
|
79
|
+
` ${callExpression};`,
|
|
71
80
|
` }`,
|
|
72
81
|
` } catch (e) {`,
|
|
73
82
|
` console.error("${name} destroy failed:", e);`,
|
|
@@ -91,8 +100,12 @@ export async function addDestroyToBrowserInit(engineDir, expression) {
|
|
|
91
100
|
throw new GeneralError(`${BROWSER_INIT_JS} not found in engine`);
|
|
92
101
|
}
|
|
93
102
|
const content = await readText(filePath);
|
|
94
|
-
// Idempotency check —
|
|
95
|
-
|
|
103
|
+
// Idempotency check — look for the coerced (call) form because that is
|
|
104
|
+
// what the emitter writes. Matching against the raw input would miss a
|
|
105
|
+
// previous `EvalStartup.destroy` invocation that the 0.16.0 coercion
|
|
106
|
+
// already persisted as `EvalStartup.destroy()`.
|
|
107
|
+
const callExpression = coerceToCall(expression);
|
|
108
|
+
const destroyPattern = new RegExp(`(?:^|\\W)${escapeRegex(callExpression)}\\s*;?\\s*$`, 'm');
|
|
96
109
|
if (destroyPattern.test(content)) {
|
|
97
110
|
return false;
|
|
98
111
|
}
|
|
@@ -10,7 +10,7 @@ import { pathExists, readText, writeText } from '../utils/fs.js';
|
|
|
10
10
|
import { escapeRegex } from '../utils/regex.js';
|
|
11
11
|
import { detectIndent, getNodeSource, parseScript } from './ast-utils.js';
|
|
12
12
|
import { withParserFallback } from './parser-fallback.js';
|
|
13
|
-
import { assertBraceBalancePreserved, extractNameFromExpression, findInsertionAfterFireforgeBlocks, findMethodBody, findMethodBraceIndex, validateWireName, walkToTryBlockEnd, } from './wire-utils.js';
|
|
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
15
|
/**
|
|
16
16
|
* AST-based implementation: finds onLoad() method body, locates existing
|
|
@@ -19,6 +19,12 @@ const BROWSER_INIT_JS = 'browser/base/content/browser-init.js';
|
|
|
19
19
|
*/
|
|
20
20
|
export function addInitAST(content, expression, after) {
|
|
21
21
|
const name = extractNameFromExpression(expression);
|
|
22
|
+
// `validateWireName` accepts both `Foo.bar` and `Foo.bar()` shapes. The
|
|
23
|
+
// template below interpolates the value verbatim, so a bare property
|
|
24
|
+
// path compiles to `Foo.bar;` — a silent no-op, not a lifecycle
|
|
25
|
+
// invocation. `coerceToCall` normalises to the function-call form so
|
|
26
|
+
// the emitted block always invokes the hook the operator asked for.
|
|
27
|
+
const callExpression = coerceToCall(expression);
|
|
22
28
|
const ast = parseScript(content);
|
|
23
29
|
const ms = new MagicString(content);
|
|
24
30
|
const body = findMethodBody(ast, 'onLoad');
|
|
@@ -97,7 +103,7 @@ export function addInitAST(content, expression, after) {
|
|
|
97
103
|
`${indent}// inits that reference native UI elements we hide.`,
|
|
98
104
|
`${indent}try {`,
|
|
99
105
|
`${indent} if (typeof ${name} !== "undefined") {`,
|
|
100
|
-
`${indent} ${
|
|
106
|
+
`${indent} ${callExpression};`,
|
|
101
107
|
`${indent} }`,
|
|
102
108
|
`${indent}} catch (e) {`,
|
|
103
109
|
`${indent} console.error("${name} init failed:", e);`,
|
|
@@ -111,6 +117,11 @@ export function addInitAST(content, expression, after) {
|
|
|
111
117
|
*/
|
|
112
118
|
export function legacyAddInit(content, expression, after) {
|
|
113
119
|
const name = extractNameFromExpression(expression);
|
|
120
|
+
// See `addInitAST` for the rationale — the AST and fallback paths must
|
|
121
|
+
// agree on whether the emitted block is a function call, otherwise
|
|
122
|
+
// operators would see different behaviour depending on which parser
|
|
123
|
+
// happened to handle their browser-init.js layout.
|
|
124
|
+
const callExpression = coerceToCall(expression);
|
|
114
125
|
const lines = content.split('\n');
|
|
115
126
|
const onLoadRegex = /\b(?:async\s+)?onLoad\s*[(:]/;
|
|
116
127
|
const found = findMethodBraceIndex(lines, onLoadRegex, { requireBrace: true });
|
|
@@ -167,7 +178,7 @@ export function legacyAddInit(content, expression, after) {
|
|
|
167
178
|
`${baseIndent}// inits that reference native UI elements we hide.`,
|
|
168
179
|
`${baseIndent}try {`,
|
|
169
180
|
`${inner}if (typeof ${name} !== "undefined") {`,
|
|
170
|
-
`${inner2}${
|
|
181
|
+
`${inner2}${callExpression};`,
|
|
171
182
|
`${inner}}`,
|
|
172
183
|
`${baseIndent}} catch (e) {`,
|
|
173
184
|
`${inner}console.error("${name} init failed:", e);`,
|
|
@@ -192,8 +203,12 @@ export async function addInitToBrowserInit(engineDir, expression, after) {
|
|
|
192
203
|
throw new GeneralError(`${BROWSER_INIT_JS} not found in engine`);
|
|
193
204
|
}
|
|
194
205
|
const content = await readText(filePath);
|
|
195
|
-
// Idempotency check —
|
|
196
|
-
|
|
206
|
+
// Idempotency check — look for the coerced (call) form because that is
|
|
207
|
+
// what the emitter writes. Matching against the raw input would miss a
|
|
208
|
+
// previous `EvalStartup.init` invocation that the 0.16.0 coercion
|
|
209
|
+
// already persisted as `EvalStartup.init()`.
|
|
210
|
+
const callExpression = coerceToCall(expression);
|
|
211
|
+
const initPattern = new RegExp(`(?:^|\\W)${escapeRegex(callExpression)}\\s*;?\\s*$`, 'm');
|
|
197
212
|
if (initPattern.test(content)) {
|
|
198
213
|
return false;
|
|
199
214
|
}
|
|
@@ -5,6 +5,21 @@ import { type AcornESTreeNode } from './ast-utils.js';
|
|
|
5
5
|
* Rejects strings containing characters that could break out of JS strings or inject code.
|
|
6
6
|
*/
|
|
7
7
|
export declare function validateWireName(value: string, label: string): void;
|
|
8
|
+
/**
|
|
9
|
+
* Coerces an init/destroy expression into a function call by appending `()`
|
|
10
|
+
* when the caller passed a bare property chain. Idempotent: an expression
|
|
11
|
+
* already ending in `()` is returned unchanged, so operators can pass either
|
|
12
|
+
* `EvalStartup.init` or `EvalStartup.init()` and get the same wired output.
|
|
13
|
+
*
|
|
14
|
+
* Motivation (eval finding 8): `validateWireName` accepts both shapes, but
|
|
15
|
+
* the generated block interpolated the expression verbatim inside
|
|
16
|
+
* `${expression};`. When a caller passed `EvalStartup.init`, the emitted
|
|
17
|
+
* code was `EvalStartup.init;` — a plain property reference that never
|
|
18
|
+
* invoked the lifecycle hook. The symptom was silent: `wire` reported
|
|
19
|
+
* success and the browser-init block looked plausible, but the hook
|
|
20
|
+
* never fired at runtime. Coercion at the template site closes that gap.
|
|
21
|
+
*/
|
|
22
|
+
export declare function coerceToCall(expression: string): string;
|
|
8
23
|
/**
|
|
9
24
|
* Counts net brace depth change in a single line, ignoring braces inside
|
|
10
25
|
* string literals (single, double, template), line comments (`//`), and
|
|
@@ -17,6 +17,23 @@ export function validateWireName(value, label) {
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Coerces an init/destroy expression into a function call by appending `()`
|
|
22
|
+
* when the caller passed a bare property chain. Idempotent: an expression
|
|
23
|
+
* already ending in `()` is returned unchanged, so operators can pass either
|
|
24
|
+
* `EvalStartup.init` or `EvalStartup.init()` and get the same wired output.
|
|
25
|
+
*
|
|
26
|
+
* Motivation (eval finding 8): `validateWireName` accepts both shapes, but
|
|
27
|
+
* the generated block interpolated the expression verbatim inside
|
|
28
|
+
* `${expression};`. When a caller passed `EvalStartup.init`, the emitted
|
|
29
|
+
* code was `EvalStartup.init;` — a plain property reference that never
|
|
30
|
+
* invoked the lifecycle hook. The symptom was silent: `wire` reported
|
|
31
|
+
* success and the browser-init block looked plausible, but the hook
|
|
32
|
+
* never fired at runtime. Coercion at the template site closes that gap.
|
|
33
|
+
*/
|
|
34
|
+
export function coerceToCall(expression) {
|
|
35
|
+
return expression.endsWith('()') ? expression : `${expression}()`;
|
|
36
|
+
}
|
|
20
37
|
/**
|
|
21
38
|
* Counts net brace depth change in a single line, ignoring braces inside
|
|
22
39
|
* string literals (single, double, template), line comments (`//`), and
|
|
@@ -86,6 +86,13 @@ export interface ExportOptions {
|
|
|
86
86
|
forceUnsafe?: boolean;
|
|
87
87
|
/** Exclude furnace-managed file paths from the export. */
|
|
88
88
|
excludeFurnace?: boolean;
|
|
89
|
+
/**
|
|
90
|
+
* Acknowledge that the export will create cross-patch ownership overlap
|
|
91
|
+
* with existing non-superseded patches. Without this flag, `export`
|
|
92
|
+
* refuses when one or more `filesAffected` are already claimed by
|
|
93
|
+
* another patch, because the resulting queue fails `verify` immediately.
|
|
94
|
+
*/
|
|
95
|
+
allowOverlap?: boolean;
|
|
89
96
|
}
|
|
90
97
|
/**
|
|
91
98
|
* Options for the reset command.
|