@hominis/fireforge 0.48.0 → 0.49.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/dist/build-info.json +4 -4
- package/dist/src/cli.js +16 -28
- package/dist/src/commands/build.js +30 -4
- package/dist/src/commands/doctor-engine-eslint.d.ts +3 -0
- package/dist/src/commands/doctor-engine-eslint.js +12 -0
- package/dist/src/commands/doctor-macos-sdk.js +7 -5
- package/dist/src/commands/doctor-source-pin.js +9 -2
- package/dist/src/commands/doctor.js +2 -0
- package/dist/src/commands/furnace/apply.js +10 -4
- package/dist/src/commands/furnace/create-templates.js +0 -3
- package/dist/src/commands/lint.js +4 -0
- package/dist/src/commands/manifest.d.ts +1 -2
- package/dist/src/commands/patch/delete.js +18 -4
- package/dist/src/commands/re-export-options.js +7 -0
- package/dist/src/commands/re-export-register.js +1 -0
- package/dist/src/commands/re-export-removed.d.ts +4 -0
- package/dist/src/commands/re-export-removed.js +49 -0
- package/dist/src/commands/re-export-single.js +4 -1
- package/dist/src/commands/re-export.js +4 -0
- package/dist/src/commands/test-browser-preflight.d.ts +28 -0
- package/dist/src/commands/test-browser-preflight.js +181 -0
- package/dist/src/commands/test-build-phase.js +30 -7
- package/dist/src/commands/test-checkjs.d.ts +3 -0
- package/dist/src/commands/test-checkjs.js +27 -0
- package/dist/src/commands/test-diagnose.js +3 -0
- package/dist/src/commands/test-harness-env.d.ts +5 -6
- package/dist/src/commands/test-harness-env.js +27 -7
- package/dist/src/commands/test-modes.d.ts +2 -4
- package/dist/src/commands/test-register.js +11 -5
- package/dist/src/commands/test-run.d.ts +2 -0
- package/dist/src/commands/test-run.js +21 -1
- package/dist/src/commands/test-verdict.d.ts +2 -2
- package/dist/src/commands/test.js +83 -211
- package/dist/src/core/build-baseline-types.d.ts +12 -14
- package/dist/src/core/build-baseline-validate.d.ts +3 -0
- package/dist/src/core/build-baseline-validate.js +53 -0
- package/dist/src/core/build-baseline.d.ts +4 -3
- package/dist/src/core/build-baseline.js +23 -11
- package/dist/src/core/component-source-guard.d.ts +20 -0
- package/dist/src/core/component-source-guard.js +68 -0
- package/dist/src/core/coverage-extend.d.ts +5 -34
- package/dist/src/core/coverage-extend.js +26 -37
- package/dist/src/core/engine-write-boundary.d.ts +5 -0
- package/dist/src/core/engine-write-boundary.js +41 -0
- package/dist/src/core/furnace-apply-helpers.js +8 -2
- package/dist/src/core/furnace-apply.js +7 -0
- package/dist/src/core/furnace-validate-accessibility.js +9 -12
- package/dist/src/core/furnace-validate.js +0 -43
- package/dist/src/core/gecko-dom-types.d.ts +2 -0
- package/dist/src/core/gecko-dom-types.js +17 -0
- package/dist/src/core/lit-template-regions.d.ts +4 -0
- package/dist/src/core/lit-template-regions.js +50 -0
- package/dist/src/core/mach-build-artifacts.d.ts +6 -6
- package/dist/src/core/mach-build-artifacts.js +13 -7
- package/dist/src/core/mach-resource-shim.d.ts +1 -1
- package/dist/src/core/mach-resource-shim.js +2 -0
- package/dist/src/core/marionette-port.d.ts +2 -0
- package/dist/src/core/marionette-port.js +38 -53
- package/dist/src/core/mochitest-server-port.d.ts +1 -1
- package/dist/src/core/mochitest-server-port.js +20 -17
- package/dist/src/core/objdir-maintenance.d.ts +10 -0
- package/dist/src/core/objdir-maintenance.js +167 -0
- package/dist/src/core/patch-lint-checkjs.js +2 -1
- package/dist/src/core/patch-parse.js +8 -6
- package/dist/src/core/preflight-wait.d.ts +2 -0
- package/dist/src/core/preflight-wait.js +24 -0
- package/dist/src/core/process-owner.d.ts +9 -0
- package/dist/src/core/process-owner.js +43 -0
- package/dist/src/core/test-harness-crash.d.ts +1 -1
- package/dist/src/core/test-harness-crash.js +5 -5
- package/dist/src/core/test-host-state.d.ts +14 -0
- package/dist/src/core/test-host-state.js +80 -0
- package/dist/src/core/test-profile-files.d.ts +17 -0
- package/dist/src/core/test-profile-files.js +75 -0
- package/dist/src/core/test-stale-check.d.ts +6 -7
- package/dist/src/core/test-stale-check.js +9 -32
- package/dist/src/core/token-category.d.ts +7 -26
- package/dist/src/core/token-category.js +91 -117
- package/dist/src/core/token-inventory.js +12 -7
- package/dist/src/core/typecheck.js +6 -6
- package/dist/src/types/commands/options.d.ts +14 -3
- package/dist/src/utils/assert.js +2 -7
- package/dist/src/utils/child-env.d.ts +0 -3
- package/dist/src/utils/child-env.js +0 -3
- package/dist/src/utils/concurrency.d.ts +2 -2
- package/dist/src/utils/concurrency.js +15 -4
- package/dist/src/utils/fs.js +11 -1
- package/dist/src/utils/options.d.ts +16 -1
- package/dist/src/utils/options.js +5 -3
- package/dist/src/utils/paths.js +3 -2
- package/dist/src/utils/process-group.d.ts +1 -2
- package/dist/src/utils/process-group.js +1 -2
- package/dist/src/utils/process-input.js +0 -3
- package/dist/src/utils/process.d.ts +3 -2
- package/dist/src/utils/process.js +19 -7
- package/docs/configuration.md +12 -11
- package/docs/lifecycle-invariants.md +9 -5
- package/docs/machine-output.md +36 -19
- package/docs/patch-workflow.md +15 -0
- package/docs/testing.md +77 -12
- package/package.json +6 -5
|
@@ -14,12 +14,6 @@ import { join } from 'node:path';
|
|
|
14
14
|
import { GeneralError } from '../errors/base.js';
|
|
15
15
|
import { pathExists, readText } from '../utils/fs.js';
|
|
16
16
|
import { escapeRegex } from '../utils/regex.js';
|
|
17
|
-
function singleLineBannerPattern(category) {
|
|
18
|
-
return new RegExp(`\\/\\*\\s*=+\\s*${escapeRegex(category)}\\s*=+\\s*\\*\\/`);
|
|
19
|
-
}
|
|
20
|
-
function multiLineBlockNameMatches(blockLine, category) {
|
|
21
|
-
return blockLine.replace(/^\s*\*\s*/, '').trim() === category;
|
|
22
|
-
}
|
|
23
17
|
/**
|
|
24
18
|
* Body of the first `/* … *\/` comment on `line`, or `undefined` when the
|
|
25
19
|
* line carries no closed block comment.
|
|
@@ -85,37 +79,8 @@ function singleLineBannerName(line) {
|
|
|
85
79
|
* agree on what "exists" means.
|
|
86
80
|
*/
|
|
87
81
|
export function categoryHeaderExists(lines, category) {
|
|
88
|
-
|
|
89
|
-
for (const [i, line] of lines.entries()) {
|
|
90
|
-
if (singleLinePattern.test(line)) {
|
|
91
|
-
return true;
|
|
92
|
-
}
|
|
93
|
-
if (/^\s*\/\*\s*=+/.test(line) && !/\*\//.test(line)) {
|
|
94
|
-
for (let j = i + 1; j < Math.min(i + 6, lines.length); j++) {
|
|
95
|
-
const blockLine = lines[j] ?? '';
|
|
96
|
-
if (multiLineBlockNameMatches(blockLine, category)) {
|
|
97
|
-
return true;
|
|
98
|
-
}
|
|
99
|
-
if (/\*\//.test(blockLine))
|
|
100
|
-
break;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
return false;
|
|
82
|
+
return parseTokenCategorySections(lines).some((section) => section.name === category);
|
|
105
83
|
}
|
|
106
|
-
/**
|
|
107
|
-
* Scans a tokens CSS file for category header comments and returns the
|
|
108
|
-
* category names in document order. Used to enrich the "category not
|
|
109
|
-
* found" error body with concrete alternatives the operator can copy.
|
|
110
|
-
*
|
|
111
|
-
* Mirrors the shapes `findCategorySection` recognises:
|
|
112
|
-
* - Single-line: `/* = Foo = *\/`
|
|
113
|
-
* - Multi-line: `/* =====` opening, `Foo` on any of the next ~5 lines,
|
|
114
|
-
* closing `*\/`.
|
|
115
|
-
*
|
|
116
|
-
* This helper exists as a pure inspector. It never throws on malformed
|
|
117
|
-
* headers and silently skips shapes it cannot parse.
|
|
118
|
-
*/
|
|
119
84
|
/**
|
|
120
85
|
* Category name declared by the banner starting at `index`, or `undefined`
|
|
121
86
|
* when that line opens no banner.
|
|
@@ -128,7 +93,7 @@ export function categoryHeaderExists(lines, category) {
|
|
|
128
93
|
* @param index - 0-based line index to test
|
|
129
94
|
* @returns The declared category name, or undefined
|
|
130
95
|
*/
|
|
131
|
-
|
|
96
|
+
function categoryBannerNameAt(lines, index) {
|
|
132
97
|
const line = lines[index] ?? '';
|
|
133
98
|
const extracted = singleLineBannerName(line);
|
|
134
99
|
if (extracted !== undefined)
|
|
@@ -149,13 +114,40 @@ export function categoryBannerNameAt(lines, index) {
|
|
|
149
114
|
return undefined;
|
|
150
115
|
}
|
|
151
116
|
function discoverCategoryHeaders(lines) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
117
|
+
return [...new Set(parseTokenCategorySections(lines).map((section) => section.name))];
|
|
118
|
+
}
|
|
119
|
+
/** Parses category regions owned by the base root, including nested CSS rules. */
|
|
120
|
+
export function parseTokenCategorySections(lines) {
|
|
121
|
+
const bounds = findBaseRootBounds(lines);
|
|
122
|
+
if (bounds === undefined || bounds.close === -1)
|
|
123
|
+
return [];
|
|
124
|
+
const masked = maskCommentLines(lines);
|
|
125
|
+
const sections = [];
|
|
126
|
+
let current;
|
|
127
|
+
let bodyStarted = false;
|
|
128
|
+
for (let index = bounds.open + 1; index < bounds.close; index++) {
|
|
129
|
+
const line = lines[index] ?? '';
|
|
130
|
+
const name = categoryBannerNameAt(lines, index);
|
|
131
|
+
const boundary = name !== undefined || isSingleLineBannerLine(line);
|
|
132
|
+
if (!boundary) {
|
|
133
|
+
if ((masked[index] ?? '').trim())
|
|
134
|
+
bodyStarted = true;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
// A decorative rule immediately after a named header belongs to that
|
|
138
|
+
// header. A later rule, after declarations, ends its section.
|
|
139
|
+
if (name === undefined && current && !bodyStarted)
|
|
140
|
+
continue;
|
|
141
|
+
if (current)
|
|
142
|
+
current.sectionEnd = index;
|
|
143
|
+
current = undefined;
|
|
144
|
+
if (name !== undefined) {
|
|
145
|
+
current = { name, categoryLine: index, sectionEnd: bounds.close };
|
|
146
|
+
bodyStarted = false;
|
|
147
|
+
sections.push(current);
|
|
148
|
+
}
|
|
157
149
|
}
|
|
158
|
-
return
|
|
150
|
+
return sections;
|
|
159
151
|
}
|
|
160
152
|
/**
|
|
161
153
|
* Asserts the category banner exists (or that creation was requested).
|
|
@@ -204,30 +196,9 @@ export function declareCategoryBanner(lines, category, tokensCssPath) {
|
|
|
204
196
|
}
|
|
205
197
|
/** Locates and bounds the named category section. Throws when absent. */
|
|
206
198
|
export function findCategorySection(lines, category, tokensCssPath) {
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
// Check single-line format: /* = Category = */
|
|
211
|
-
if (singleLinePattern.test(line)) {
|
|
212
|
-
categoryLine = i;
|
|
213
|
-
break;
|
|
214
|
-
}
|
|
215
|
-
// Check multi-line format: line opens a block comment with === but does not close it
|
|
216
|
-
if (/^\s*\/\*\s*=+/.test(line) && !/\*\//.test(line)) {
|
|
217
|
-
for (let j = i + 1; j < Math.min(i + 6, lines.length); j++) {
|
|
218
|
-
const blockLine = lines[j] ?? '';
|
|
219
|
-
if (multiLineBlockNameMatches(blockLine, category)) {
|
|
220
|
-
categoryLine = i;
|
|
221
|
-
break;
|
|
222
|
-
}
|
|
223
|
-
if (/\*\//.test(blockLine))
|
|
224
|
-
break;
|
|
225
|
-
}
|
|
226
|
-
if (categoryLine !== -1)
|
|
227
|
-
break;
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
if (categoryLine === -1) {
|
|
199
|
+
const sections = parseTokenCategorySections(lines);
|
|
200
|
+
const section = sections.find((entry) => entry.name === category);
|
|
201
|
+
if (section === undefined) {
|
|
231
202
|
const discoveredCategories = discoverCategoryHeaders(lines);
|
|
232
203
|
const available = discoveredCategories.length > 0
|
|
233
204
|
? `Available categories in the file: ${discoveredCategories.map((name) => `"${name}"`).join(', ')}.`
|
|
@@ -236,45 +207,73 @@ export function findCategorySection(lines, category, tokensCssPath) {
|
|
|
236
207
|
`${available}\n\n` +
|
|
237
208
|
'Add a header by hand inside the :root block (format: "/* = My Category = */") or re-run "fireforge furnace init --force" to re-seed the default categories.');
|
|
238
209
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
210
|
+
return { categoryLine: section.categoryLine, sectionEnd: section.sectionEnd };
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Drops complete quoted strings from one CSS line in linear time. An
|
|
214
|
+
* unterminated quote is kept, and once one fails to close every later quote
|
|
215
|
+
* of that kind fails too, so no start position is ever rescanned.
|
|
216
|
+
*/
|
|
217
|
+
function stripCssStrings(line) {
|
|
218
|
+
let out = '';
|
|
219
|
+
const unterminated = new Set();
|
|
220
|
+
for (let i = 0; i < line.length; i++) {
|
|
221
|
+
const quote = line.charAt(i);
|
|
222
|
+
if ((quote !== '"' && quote !== "'") || unterminated.has(quote)) {
|
|
223
|
+
out += quote;
|
|
246
224
|
continue;
|
|
247
225
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
break;
|
|
226
|
+
let end = i + 1;
|
|
227
|
+
while (end < line.length && line.charAt(end) !== quote)
|
|
228
|
+
end += line.charAt(end) === '\\' ? 2 : 1;
|
|
229
|
+
if (end >= line.length) {
|
|
230
|
+
unterminated.add(quote);
|
|
231
|
+
out += quote;
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
i = end;
|
|
258
235
|
}
|
|
259
236
|
}
|
|
260
|
-
return
|
|
237
|
+
return out;
|
|
261
238
|
}
|
|
262
239
|
/** 0-based open/close line indices of the base `:root {` block. */
|
|
263
240
|
export function findBaseRootBounds(lines) {
|
|
264
241
|
const open = lines.findIndex((line) => /:root\s*\{/.test(line));
|
|
265
242
|
if (open === -1)
|
|
266
243
|
return undefined;
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
244
|
+
const masked = maskCommentLines(lines);
|
|
245
|
+
let depth = 0;
|
|
246
|
+
for (let i = open; i < lines.length; i++) {
|
|
247
|
+
// Ignore braces in CSS strings (including data URLs).
|
|
248
|
+
const code = stripCssStrings(masked[i] ?? '');
|
|
249
|
+
for (const char of code) {
|
|
250
|
+
if (char === '{')
|
|
251
|
+
depth++;
|
|
252
|
+
if (char === '}' && --depth === 0)
|
|
253
|
+
return { open, close: i };
|
|
270
254
|
}
|
|
271
255
|
}
|
|
272
256
|
return { open, close: -1 };
|
|
273
257
|
}
|
|
274
258
|
/** Masks block-comment content per line so declarations inside comments never match. */
|
|
275
259
|
export function maskCommentLines(lines) {
|
|
276
|
-
const
|
|
277
|
-
|
|
260
|
+
const source = lines.join('\n');
|
|
261
|
+
const parts = [];
|
|
262
|
+
let cursor = 0;
|
|
263
|
+
for (;;) {
|
|
264
|
+
const open = source.indexOf('/*', cursor);
|
|
265
|
+
if (open === -1)
|
|
266
|
+
break;
|
|
267
|
+
const close = source.indexOf('*/', open + 2);
|
|
268
|
+
// An unfinished edit has no complete comment to mask. Searching only
|
|
269
|
+
// once prevents restarting a regex at every repeated comment opener.
|
|
270
|
+
if (close === -1)
|
|
271
|
+
break;
|
|
272
|
+
parts.push(source.slice(cursor, open), source.slice(open, close + 2).replace(/[^\n]/g, ' '));
|
|
273
|
+
cursor = close + 2;
|
|
274
|
+
}
|
|
275
|
+
parts.push(source.slice(cursor));
|
|
276
|
+
return parts.join('').split('\n');
|
|
278
277
|
}
|
|
279
278
|
/**
|
|
280
279
|
* Finds an existing declaration of `tokenName` inside the base `:root`
|
|
@@ -288,38 +287,13 @@ export function findTokenDeclarationInRoot(lines, tokenName) {
|
|
|
288
287
|
if (bounds === undefined || bounds.close === -1)
|
|
289
288
|
return undefined;
|
|
290
289
|
const masked = maskCommentLines(lines);
|
|
290
|
+
const sections = parseTokenCategorySections(lines);
|
|
291
291
|
const declPattern = new RegExp(`^\\s*${escapeRegex(tokenName)}\\s*:`);
|
|
292
292
|
for (let i = bounds.open + 1; i < bounds.close; i++) {
|
|
293
293
|
if (!declPattern.test(masked[i] ?? ''))
|
|
294
294
|
continue;
|
|
295
|
-
const category =
|
|
295
|
+
const category = sections.find((section) => i > section.categoryLine && i < section.sectionEnd)?.name;
|
|
296
296
|
return category === undefined ? { line: i + 1 } : { line: i + 1, category };
|
|
297
297
|
}
|
|
298
298
|
return undefined;
|
|
299
299
|
}
|
|
300
|
-
/** Nearest category banner name above `lineIndex`, if any. */
|
|
301
|
-
function findSectionNameAbove(lines, lineIndex) {
|
|
302
|
-
const singleLinePattern = /\/\*\s*=+\s*(.+?)\s*=+\s*\*\//;
|
|
303
|
-
for (let i = lineIndex - 1; i >= 0; i--) {
|
|
304
|
-
const line = lines[i] ?? '';
|
|
305
|
-
const singleMatch = singleLinePattern.exec(line);
|
|
306
|
-
if (singleMatch?.[1]) {
|
|
307
|
-
const extracted = singleMatch[1].trim();
|
|
308
|
-
if (extracted.length > 0 && !/^=+$/.test(extracted))
|
|
309
|
-
return extracted;
|
|
310
|
-
continue;
|
|
311
|
-
}
|
|
312
|
-
if (/^\s*\/\*\s*=+/.test(line) && !/\*\//.test(line)) {
|
|
313
|
-
for (let j = i + 1; j < Math.min(i + 6, lines.length); j++) {
|
|
314
|
-
const blockLine = lines[j] ?? '';
|
|
315
|
-
if (/\*\//.test(blockLine))
|
|
316
|
-
break;
|
|
317
|
-
const trimmed = blockLine.replace(/^\s*\*\s*/, '').trim();
|
|
318
|
-
if (trimmed.length === 0 || /^=+$/.test(trimmed))
|
|
319
|
-
continue;
|
|
320
|
-
return trimmed;
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
return undefined;
|
|
325
|
-
}
|
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
* categories a project actually has, so the only way to name one correctly
|
|
9
9
|
* was to hand-parse a neighbouring `= Category =` banner out of the file.
|
|
10
10
|
* This module is that report. It reuses the banner and
|
|
11
|
-
*
|
|
11
|
+
* category-region model from `token-category.ts` rather than parsing
|
|
12
12
|
* tokens CSS a second way: two readers that disagree on what a banner is
|
|
13
13
|
* would put `token list` and `token add` in different sections of the same
|
|
14
14
|
* file.
|
|
15
15
|
*/
|
|
16
|
-
import {
|
|
16
|
+
import { findBaseRootBounds, maskCommentLines, parseTokenCategorySections, } from './token-category.js';
|
|
17
17
|
/** Matches a custom-property declaration and captures its name and value. */
|
|
18
18
|
const DECLARATION_PATTERN = /^\s*(--[A-Za-z0-9_-]+)\s*:\s*([^;]*);?/;
|
|
19
19
|
/**
|
|
@@ -32,14 +32,18 @@ export function collectTokenInventory(lines) {
|
|
|
32
32
|
if (bounds === undefined || bounds.close === -1)
|
|
33
33
|
return [];
|
|
34
34
|
const masked = maskCommentLines(lines);
|
|
35
|
+
const sections = parseTokenCategorySections(lines);
|
|
36
|
+
const headers = new Map(sections.map((section) => [section.categoryLine, section]));
|
|
35
37
|
const groups = [];
|
|
36
38
|
let current;
|
|
39
|
+
let sectionEnd = bounds.close;
|
|
37
40
|
for (let i = bounds.open + 1; i < bounds.close; i++) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
if (
|
|
42
|
-
current = { category:
|
|
41
|
+
if (i >= sectionEnd)
|
|
42
|
+
current = undefined;
|
|
43
|
+
const section = headers.get(i);
|
|
44
|
+
if (section !== undefined) {
|
|
45
|
+
current = { category: section.name, tokens: [] };
|
|
46
|
+
sectionEnd = section.sectionEnd;
|
|
43
47
|
groups.push(current);
|
|
44
48
|
continue;
|
|
45
49
|
}
|
|
@@ -48,6 +52,7 @@ export function collectTokenInventory(lines) {
|
|
|
48
52
|
continue;
|
|
49
53
|
if (current === undefined) {
|
|
50
54
|
current = { category: null, tokens: [] };
|
|
55
|
+
sectionEnd = bounds.close;
|
|
51
56
|
groups.push(current);
|
|
52
57
|
}
|
|
53
58
|
current.tokens.push({ name: match[1], line: i + 1, value: (match[2] ?? '').trim() });
|
|
@@ -26,6 +26,7 @@ import { sha256Hex } from '../utils/hash.js';
|
|
|
26
26
|
import { verbose } from '../utils/logger.js';
|
|
27
27
|
import { getPackageVersion } from '../utils/package-root.js';
|
|
28
28
|
import { normalizePathSlashes } from '../utils/paths.js';
|
|
29
|
+
import { withGeckoDomTypes } from './gecko-dom-types.js';
|
|
29
30
|
import { composeShimSource, SHIM_FILENAME, SUPPRESSED_DIAGNOSTIC_CODES, UNDEFINED_IDENTIFIER_CODES, UNDEFINED_IDENTIFIER_HINT, } from './typecheck-shim.js';
|
|
30
31
|
/**
|
|
31
32
|
* Sentinel string surfaced as a `TypecheckIssue.message` when the
|
|
@@ -252,7 +253,7 @@ async function runTypecheckForProject(ts, projectRoot, projectPath, shimSource,
|
|
|
252
253
|
// filtered out below, so the pass reports failures that do not exist.
|
|
253
254
|
const shimPath = normalizePathSlashes(resolve(projectDir, `.fireforge-${SHIM_FILENAME}`));
|
|
254
255
|
const rootFiles = [...parsed.fileNames, shimPath];
|
|
255
|
-
const defaultHost = ts.createCompilerHost(options);
|
|
256
|
+
const defaultHost = withGeckoDomTypes(ts, ts.createCompilerHost(options));
|
|
256
257
|
const host = {
|
|
257
258
|
...defaultHost,
|
|
258
259
|
getSourceFile(fileName, languageVersion, onError, shouldCreate) {
|
|
@@ -293,11 +294,10 @@ async function runTypecheckForProject(ts, projectRoot, projectPath, shimSource,
|
|
|
293
294
|
program = ts.createProgram(rootFiles, options, host);
|
|
294
295
|
}
|
|
295
296
|
else {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
});
|
|
297
|
+
// Always check the entire project in root order. Restoring TypeScript's
|
|
298
|
+
// per-file semantic cache makes checkJs inference depend on the first
|
|
299
|
+
// invalidated importer and can both invent and hide diagnostics.
|
|
300
|
+
const builder = ts.createEmitAndSemanticDiagnosticsBuilderProgram(rootFiles, { ...options, incremental: true, tsBuildInfoFile: buildInfoFile }, withSourceFileVersions(host));
|
|
301
301
|
program = builder;
|
|
302
302
|
// Under noEmit, emit() writes the build info and nothing else.
|
|
303
303
|
writeBuildInfo = () => {
|
|
@@ -109,6 +109,8 @@ export interface SourceSetOptions {
|
|
|
109
109
|
* Options for the build command.
|
|
110
110
|
*/
|
|
111
111
|
export interface BuildOptions extends EngineLockWaitable {
|
|
112
|
+
/** Wait for the objdir browser before deploying/building. */
|
|
113
|
+
waitBrowser?: number | boolean;
|
|
112
114
|
/** Fast UI-only rebuild */
|
|
113
115
|
ui?: boolean;
|
|
114
116
|
/** Number of parallel jobs */
|
|
@@ -368,6 +370,8 @@ export interface ReExportOptions extends EngineLockWaitable {
|
|
|
368
370
|
* rather than hidden.
|
|
369
371
|
*/
|
|
370
372
|
expectUnmanaged?: string[];
|
|
373
|
+
/** Explicitly retire named absent patch-created files in a scan-less export. */
|
|
374
|
+
expectRemoved?: string[];
|
|
371
375
|
}
|
|
372
376
|
/**
|
|
373
377
|
* Options for the `fireforge patch tier` subcommand. Sets or clears the
|
|
@@ -551,6 +555,12 @@ export interface RunOptions {
|
|
|
551
555
|
* Options for the test command.
|
|
552
556
|
*/
|
|
553
557
|
export interface TestOptions extends EngineLockWaitable {
|
|
558
|
+
/** Queue behind a browser using this objdir; bare flag means 60 seconds. */
|
|
559
|
+
waitBrowser?: number | boolean;
|
|
560
|
+
/** Queue behind the mochitest/Marionette port holder. */
|
|
561
|
+
waitPort?: number | boolean;
|
|
562
|
+
/** Copy a file into the profile at a relative destination (repeatable source=destination). */
|
|
563
|
+
profileFile?: string[];
|
|
554
564
|
/** Run tests in headless mode */
|
|
555
565
|
headless?: boolean;
|
|
556
566
|
/**
|
|
@@ -675,9 +685,10 @@ export interface TestOptions extends EngineLockWaitable {
|
|
|
675
685
|
*/
|
|
676
686
|
perfSamples?: string;
|
|
677
687
|
/**
|
|
678
|
-
* Seeded
|
|
679
|
-
* a number replays
|
|
680
|
-
*
|
|
688
|
+
* Seeded isolated path-argument order for mochitest runs. `true` draws a
|
|
689
|
+
* fresh seed; a number replays the shard permutation. Native mach
|
|
690
|
+
* `--shuffle` within a shard remains unseeded. Also exported as
|
|
691
|
+
* `FIREFORGE_SHUFFLE_SEED` for custom task code and recorded on the verdict.
|
|
681
692
|
*/
|
|
682
693
|
shuffle?: number | boolean;
|
|
683
694
|
}
|
package/dist/src/utils/assert.js
CHANGED
|
@@ -11,13 +11,8 @@
|
|
|
11
11
|
* says the opposite: nothing the operator could have done would have
|
|
12
12
|
* prevented it.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* permanently-uncovered branch to every function it appears in, and the
|
|
17
|
-
* per-module coverage floors in `scripts/check-coverage-thresholds.mjs`
|
|
18
|
-
* would pay for it in ~40 places. Routed through a call, the branch lives
|
|
19
|
-
* here once and is covered here once, and the `complexity` ceiling in
|
|
20
|
-
* `eslint.config.js` does not see it at all.
|
|
14
|
+
* Shared helpers provide consistent classification and TypeScript narrowing,
|
|
15
|
+
* and keep expensive failure descriptions lazy on successful paths.
|
|
21
16
|
*
|
|
22
17
|
* This module is a leaf: it imports the error class and nothing else, per
|
|
23
18
|
* the dependency-direction note in `src/utils/errors.ts`.
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Environment construction for spawned children.
|
|
3
|
-
*
|
|
4
|
-
* Its own module rather than a helper inside `process.ts` only because that
|
|
5
|
-
* file sits on a 500-line budget. The logic belongs to the exec layer.
|
|
6
3
|
*/
|
|
7
4
|
/** The `env` / `envUnset` slice of exec options this module needs. */
|
|
8
5
|
interface ChildEnvOptions {
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
/**
|
|
3
3
|
* Environment construction for spawned children.
|
|
4
|
-
*
|
|
5
|
-
* Its own module rather than a helper inside `process.ts` only because that
|
|
6
|
-
* file sits on a 500-line budget. The logic belongs to the exec layer.
|
|
7
4
|
*/
|
|
8
5
|
/**
|
|
9
6
|
* The child's environment: `process.env`, with `env` merged over it, then
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Bounded-concurrency helpers shared by commands that fan out per-file IO
|
|
3
3
|
* (status classification, import's unmanaged-dirty guard, re-export scans,
|
|
4
|
-
* the dry-run purity guard).
|
|
5
|
-
*
|
|
4
|
+
* the dry-run purity guard). On failure, stop scheduling new items and await
|
|
5
|
+
* every started operation before rejecting, so callers can safely clean up.
|
|
6
6
|
*/
|
|
7
7
|
/** Maps items with at most `limit` in-flight promises. Preserves order. */
|
|
8
8
|
export declare function mapWithConcurrency<T, R>(items: readonly T[], limit: number, fn: (item: T, index: number) => Promise<R>): Promise<R[]>;
|
|
@@ -1,21 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Bounded-concurrency helpers shared by commands that fan out per-file IO
|
|
3
3
|
* (status classification, import's unmanaged-dirty guard, re-export scans,
|
|
4
|
-
* the dry-run purity guard).
|
|
5
|
-
*
|
|
4
|
+
* the dry-run purity guard). On failure, stop scheduling new items and await
|
|
5
|
+
* every started operation before rejecting, so callers can safely clean up.
|
|
6
6
|
*/
|
|
7
7
|
/** Maps items with at most `limit` in-flight promises. Preserves order. */
|
|
8
8
|
export async function mapWithConcurrency(items, limit, fn) {
|
|
9
|
+
if (!Number.isSafeInteger(limit) || limit < 1) {
|
|
10
|
+
throw new RangeError('Concurrency limit must be a positive integer');
|
|
11
|
+
}
|
|
9
12
|
const results = new Array(items.length);
|
|
10
13
|
let next = 0;
|
|
14
|
+
const state = {};
|
|
11
15
|
const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
|
|
12
|
-
|
|
16
|
+
while (!state.failure) {
|
|
13
17
|
const index = next++;
|
|
14
18
|
if (index >= items.length)
|
|
15
19
|
return;
|
|
16
|
-
|
|
20
|
+
try {
|
|
21
|
+
results[index] = await fn(items[index], index);
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
state.failure ??= { error };
|
|
25
|
+
}
|
|
17
26
|
}
|
|
18
27
|
});
|
|
19
28
|
await Promise.all(workers);
|
|
29
|
+
if (state.failure)
|
|
30
|
+
throw state.failure.error;
|
|
20
31
|
return results;
|
|
21
32
|
}
|
package/dist/src/utils/fs.js
CHANGED
|
@@ -99,7 +99,17 @@ export async function isSymlink(path) {
|
|
|
99
99
|
*/
|
|
100
100
|
export async function copyFile(src, dest) {
|
|
101
101
|
await ensureParentDir(dest);
|
|
102
|
-
|
|
102
|
+
// Never follow a destination symlink or truncate a shared hardlink:
|
|
103
|
+
// build/deploy copies must not write through into components/ or a peer.
|
|
104
|
+
const tempPath = createAtomicTempPath(dest);
|
|
105
|
+
try {
|
|
106
|
+
await fsCopyFile(src, tempPath);
|
|
107
|
+
await renameWithRetries(tempPath, dest);
|
|
108
|
+
await syncParentDir(dest);
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
await rm(tempPath, { force: true });
|
|
112
|
+
}
|
|
103
113
|
}
|
|
104
114
|
/**
|
|
105
115
|
* Reads a JSON file and parses it.
|
|
@@ -89,7 +89,22 @@ export declare function resolveWaitLockSeconds(value: string | number | boolean
|
|
|
89
89
|
*/
|
|
90
90
|
export declare function sessionWaitLockMs(): number | undefined;
|
|
91
91
|
/**
|
|
92
|
-
*
|
|
92
|
+
* Registers `--wait-lock [seconds]` on a command that takes no lock, so a
|
|
93
|
+
* scripted sequence can blanket-append the flag without hitting a usage
|
|
94
|
+
* error. A command that rejects it with "unknown option" kills the whole
|
|
95
|
+
* sequence with a usage error instead of a lock message.
|
|
96
|
+
*
|
|
97
|
+
* The flag is accepted and ignored here, never silently repurposed, and
|
|
98
|
+
* the help text says so, so nobody reads its presence as evidence that this
|
|
99
|
+
* command waits for anything.
|
|
100
|
+
*
|
|
101
|
+
* The value is parsed and validated exactly as the honoring variant does:
|
|
102
|
+
* `--wait-lock nonsense` must still be a usage error everywhere, or the
|
|
103
|
+
* uniformity would be a lie.
|
|
104
|
+
*/
|
|
105
|
+
export declare function addAcceptedWaitLockOption(command: Command): Command;
|
|
106
|
+
/**
|
|
107
|
+
* Recursively gives every leaf command in the tree a `--wait-lock` flag: the
|
|
93
108
|
* honoring registration where one already exists, the accept-and-ignore
|
|
94
109
|
* one everywhere else.
|
|
95
110
|
*/
|
|
@@ -182,7 +182,7 @@ export function sessionWaitLockMs() {
|
|
|
182
182
|
* `--wait-lock nonsense` must still be a usage error everywhere, or the
|
|
183
183
|
* uniformity would be a lie.
|
|
184
184
|
*/
|
|
185
|
-
function addAcceptedWaitLockOption(command) {
|
|
185
|
+
export function addAcceptedWaitLockOption(command) {
|
|
186
186
|
return command.option('--wait-lock [seconds]', 'Accepted for scripting uniformity and ignored: this command takes no FireForge lock', commanderArgParser((raw) => resolveWaitLockSeconds(raw)));
|
|
187
187
|
}
|
|
188
188
|
/**
|
|
@@ -194,13 +194,15 @@ function hasWaitLockOption(command) {
|
|
|
194
194
|
return command.options.some((option) => option.long === '--wait-lock');
|
|
195
195
|
}
|
|
196
196
|
/**
|
|
197
|
-
* Recursively gives every command in the tree a `--wait-lock` flag: the
|
|
197
|
+
* Recursively gives every leaf command in the tree a `--wait-lock` flag: the
|
|
198
198
|
* honoring registration where one already exists, the accept-and-ignore
|
|
199
199
|
* one everywhere else.
|
|
200
200
|
*/
|
|
201
201
|
export function ensureWaitLockOptionEverywhere(command) {
|
|
202
202
|
for (const sub of command.commands) {
|
|
203
|
-
|
|
203
|
+
// Commander groups otherwise capture a trailing leaf option before the
|
|
204
|
+
// action can see it. Register the uniform flag only on actionable leaves.
|
|
205
|
+
if (sub.commands.length === 0 && !hasWaitLockOption(sub))
|
|
204
206
|
addAcceptedWaitLockOption(sub);
|
|
205
207
|
ensureWaitLockOptionEverywhere(sub);
|
|
206
208
|
}
|
package/dist/src/utils/paths.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
-
import { isAbsolute, relative, resolve } from 'node:path';
|
|
2
|
+
import { isAbsolute, relative, resolve, sep } from 'node:path';
|
|
3
3
|
const WINDOWS_ABSOLUTE_PATH = /^[a-zA-Z]:[\\/]/;
|
|
4
4
|
const RELATIVE_PATH_ROOT = resolve('/__fireforge_path_root__');
|
|
5
5
|
/**
|
|
@@ -55,7 +55,8 @@ export function isPathInsideRoot(root, candidate) {
|
|
|
55
55
|
: resolve(resolvedRoot, candidate);
|
|
56
56
|
const relativePath = relative(resolvedRoot, resolvedCandidate);
|
|
57
57
|
return (relativePath === '' ||
|
|
58
|
-
(
|
|
58
|
+
(relativePath !== '..' &&
|
|
59
|
+
!relativePath.startsWith(`..${sep}`) &&
|
|
59
60
|
!isAbsolute(relativePath) &&
|
|
60
61
|
!WINDOWS_ABSOLUTE_PATH.test(relativePath)));
|
|
61
62
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Process-group kill and post-run sweep helpers for the exec layer.
|
|
3
|
-
* out of `process.ts` to keep that file within the per-file line budget.
|
|
2
|
+
* Process-group kill and post-run sweep helpers for the exec layer.
|
|
4
3
|
* Spawn-based rather than `exec`-based so the two modules do not import
|
|
5
4
|
* each other cyclically.
|
|
6
5
|
*/
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
/**
|
|
3
|
-
* Process-group kill and post-run sweep helpers for the exec layer.
|
|
4
|
-
* out of `process.ts` to keep that file within the per-file line budget.
|
|
3
|
+
* Process-group kill and post-run sweep helpers for the exec layer.
|
|
5
4
|
* Spawn-based rather than `exec`-based so the two modules do not import
|
|
6
5
|
* each other cyclically.
|
|
7
6
|
*/
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
/**
|
|
3
3
|
* Stdin-fed command execution with byte-exact stdout.
|
|
4
|
-
*
|
|
5
|
-
* Its own module rather than a function in `process.ts` only because that
|
|
6
|
-
* file sits on a 500-line budget. The logic belongs to the exec layer.
|
|
7
4
|
*/
|
|
8
5
|
import { spawn } from 'node:child_process';
|
|
9
6
|
import { buildChildEnv } from './child-env.js';
|
|
@@ -150,8 +150,9 @@ export declare function execStream(command: string, args: string[], options?: St
|
|
|
150
150
|
* Registers a child whose shutdown {@link waitForActiveChildShutdown} must
|
|
151
151
|
* wait for, and returns the handle that deregisters it.
|
|
152
152
|
*
|
|
153
|
-
* @returns `settle()`. Call
|
|
154
|
-
*
|
|
153
|
+
* @returns `settle()`. Call after child closure and required teardown finish.
|
|
154
|
+
* An abort's `error` event does not prove closure; do not settle there.
|
|
155
|
+
* A failed start may settle when no live child or teardown remains.
|
|
155
156
|
*/
|
|
156
157
|
export declare function trackChildClosure(): {
|
|
157
158
|
settle: () => void;
|