@hominis/fireforge 0.47.1 → 0.49.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 +83 -0
- package/dist/bin/fireforge.js +5 -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.d.ts +8 -0
- package/dist/src/commands/doctor-macos-sdk.js +158 -0
- package/dist/src/commands/doctor-source-pin.js +9 -2
- package/dist/src/commands/doctor.js +4 -0
- package/dist/src/commands/download.js +64 -3
- package/dist/src/commands/furnace/apply.js +10 -4
- package/dist/src/commands/furnace/create-templates.js +0 -3
- package/dist/src/commands/import.js +5 -2
- package/dist/src/commands/lint-per-patch.js +44 -6
- package/dist/src/commands/lint-per-run-checkjs.d.ts +3 -0
- package/dist/src/commands/lint-per-run-checkjs.js +1 -0
- package/dist/src/commands/lint.js +14 -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-lint.d.ts +17 -2
- package/dist/src/commands/re-export-lint.js +33 -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 +8 -2
- package/dist/src/commands/rebase/confirm.d.ts +12 -1
- package/dist/src/commands/rebase/confirm.js +6 -3
- package/dist/src/commands/rebase/index.js +77 -13
- package/dist/src/commands/run.js +43 -7
- package/dist/src/commands/status.js +5 -3
- 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/commands/typecheck.d.ts +5 -0
- package/dist/src/commands/typecheck.js +7 -1
- 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/build-prepare.d.ts +2 -0
- package/dist/src/core/build-prepare.js +67 -35
- 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/config-paths.d.ts +1 -1
- package/dist/src/core/config-paths.js +1 -0
- package/dist/src/core/config-validate.js +7 -0
- package/dist/src/core/coverage-extend.d.ts +5 -34
- package/dist/src/core/coverage-extend.js +26 -37
- package/dist/src/core/discard-baseline.js +2 -1
- 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/forward-registrations.d.ts +33 -0
- package/dist/src/core/forward-registrations.js +16 -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/git-file-ops.d.ts +21 -0
- package/dist/src/core/git-file-ops.js +75 -0
- package/dist/src/core/git-status.d.ts +14 -0
- package/dist/src/core/git-status.js +41 -2
- 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-mozconfig.d.ts +2 -1
- package/dist/src/core/mach-mozconfig.js +2 -0
- 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 +151 -0
- package/dist/src/core/patch-apply-fuzz.d.ts +12 -0
- package/dist/src/core/patch-apply-fuzz.js +27 -6
- package/dist/src/core/patch-apply.d.ts +13 -0
- package/dist/src/core/patch-apply.js +27 -2
- package/dist/src/core/patch-lint-checkjs-cache.d.ts +18 -0
- package/dist/src/core/patch-lint-checkjs-cache.js +45 -0
- package/dist/src/core/patch-lint-checkjs.d.ts +6 -0
- package/dist/src/core/patch-lint-checkjs.js +10 -2
- package/dist/src/core/patch-lint-cross.d.ts +7 -1
- package/dist/src/core/patch-lint-cross.js +13 -12
- package/dist/src/core/patch-lint-forward-registration-extended.d.ts +38 -0
- package/dist/src/core/patch-lint-forward-registration-extended.js +240 -0
- package/dist/src/core/patch-lint-forward-registration.d.ts +18 -5
- package/dist/src/core/patch-lint-forward-registration.js +83 -37
- package/dist/src/core/patch-lint.d.ts +11 -0
- package/dist/src/core/patch-lint.js +20 -2
- package/dist/src/core/patch-transform.d.ts +14 -13
- package/dist/src/core/patch-transform.js +29 -17
- 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/rebase-dry-run.d.ts +48 -0
- package/dist/src/core/rebase-dry-run.js +105 -0
- package/dist/src/core/run-profile.d.ts +50 -0
- package/dist/src/core/run-profile.js +131 -0
- package/dist/src/core/status-binary.d.ts +1 -1
- package/dist/src/core/status-binary.js +3 -1
- package/dist/src/core/status-classify.js +29 -2
- 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 +68 -121
- package/dist/src/core/token-inventory.js +12 -7
- package/dist/src/core/toolchain-preflight.d.ts +6 -0
- package/dist/src/core/toolchain-preflight.js +1 -1
- package/dist/src/core/typecheck.d.ts +11 -1
- package/dist/src/core/typecheck.js +86 -12
- package/dist/src/errors/rebase.d.ts +9 -0
- package/dist/src/errors/rebase.js +20 -0
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.js +2 -1
- package/dist/src/types/commands/options.d.ts +29 -3
- package/dist/src/types/config.d.ts +10 -0
- 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.d.ts +27 -0
- package/dist/src/utils/process-input.js +46 -0
- package/dist/src/utils/process.d.ts +5 -2
- package/dist/src/utils/process.js +21 -8
- package/dist/src/utils/validation.d.ts +11 -0
- package/dist/src/utils/validation.js +43 -0
- package/docs/configuration.md +82 -0
- package/docs/exit-codes.md +1 -1
- package/docs/lifecycle-invariants.md +13 -8
- package/docs/machine-output.md +36 -19
- package/docs/patch-workflow.md +50 -1
- package/docs/testing.md +77 -12
- package/package.json +10 -5
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
import { copyFile, mkdir, mkdtemp, rm } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { dirname, join, resolve } from 'node:path';
|
|
5
|
+
import { InvalidArgumentError } from '../errors/base.js';
|
|
6
|
+
import { toError } from '../utils/errors.js';
|
|
7
|
+
import { warn } from '../utils/logger.js';
|
|
8
|
+
/** Prepares profile overlays and browser/port ownership preconditions. */
|
|
9
|
+
export async function stageProfileFiles(projectRoot, files) {
|
|
10
|
+
const root = await mkdtemp(join(tmpdir(), 'fireforge-profile-overlay-'));
|
|
11
|
+
const top = new Set();
|
|
12
|
+
try {
|
|
13
|
+
for (const file of files) {
|
|
14
|
+
const separator = file.indexOf('=');
|
|
15
|
+
const source = file.slice(0, separator);
|
|
16
|
+
const dest = file.slice(separator + 1).replaceAll('\\', '/');
|
|
17
|
+
if (separator <= 0 ||
|
|
18
|
+
!dest ||
|
|
19
|
+
dest.startsWith('/') ||
|
|
20
|
+
dest.includes(':') ||
|
|
21
|
+
dest.split('/').some((part) => !part || part === '.' || part === '..')) {
|
|
22
|
+
throw new InvalidArgumentError('--profile-file requires source=relative/destination with no traversal.', '--profile-file');
|
|
23
|
+
}
|
|
24
|
+
const target = join(root, dest);
|
|
25
|
+
await mkdir(dirname(target), { recursive: true });
|
|
26
|
+
await copyFile(resolve(projectRoot, source), target);
|
|
27
|
+
top.add(dest.split('/')[0] ?? dest);
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
root,
|
|
31
|
+
args: [...top].map((path) => `--extra-profile-file=${join(root, path)}`),
|
|
32
|
+
env: { FIREFORGE_PROFILE_MERGE_ROOT: root },
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
await cleanupProfileFiles(root);
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/** Python startup overlay: allow merging ONLY the explicitly staged profile directories. */
|
|
41
|
+
export const PROFILE_MERGE_PYTHON = `
|
|
42
|
+
import os
|
|
43
|
+
import shutil
|
|
44
|
+
_fireforge_profile_root = os.environ.get("FIREFORGE_PROFILE_MERGE_ROOT")
|
|
45
|
+
if _fireforge_profile_root and not getattr(shutil.copytree, "_fireforge_profile_merge", False):
|
|
46
|
+
_fireforge_copytree = shutil.copytree
|
|
47
|
+
def _fireforge_merge_copytree(src, dst, *args, **kwargs):
|
|
48
|
+
if os.path.realpath(src).startswith(os.path.realpath(_fireforge_profile_root) + os.sep):
|
|
49
|
+
# Python's recursive calls may supply dirs_exist_ok positionally.
|
|
50
|
+
if len(args) >= 5:
|
|
51
|
+
args = args[:4] + (True,) + args[5:]
|
|
52
|
+
else:
|
|
53
|
+
kwargs["dirs_exist_ok"] = True
|
|
54
|
+
return _fireforge_copytree(src, dst, *args, **kwargs)
|
|
55
|
+
_fireforge_merge_copytree._fireforge_profile_merge = True
|
|
56
|
+
shutil.copytree = _fireforge_merge_copytree
|
|
57
|
+
`;
|
|
58
|
+
/** Classifies the profile setup traceback separately from stale packaging. */
|
|
59
|
+
export function classifyProfileArgumentFailure(output) {
|
|
60
|
+
if (!/copyExtraFilesToProfile/.test(output) || !/FileExistsError/.test(output))
|
|
61
|
+
return undefined;
|
|
62
|
+
return {
|
|
63
|
+
kind: 'harness-arguments',
|
|
64
|
+
note: 'profile directory collision; use --profile-file source=chrome/userChrome.css to merge into the existing profile',
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/** Cleanup is independent of the harness verdict and cannot hide its primary error. */
|
|
68
|
+
export async function cleanupProfileFiles(root) {
|
|
69
|
+
try {
|
|
70
|
+
await rm(root, { recursive: true, force: true });
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
warn(`Could not remove staged profile files ${root}: ${toError(error).message}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -27,8 +27,8 @@ export interface StaleBuildResult {
|
|
|
27
27
|
* Probes the engine tree for packageable changes since the last successful
|
|
28
28
|
* `fireforge build`. Returns a summary the `fireforge test` handler renders
|
|
29
29
|
* as an up-front warning when `--build` was not passed. The probe never
|
|
30
|
-
*
|
|
31
|
-
*
|
|
30
|
+
* refuses a malformed persisted baseline. Git failures and a missing baseline
|
|
31
|
+
* degrade to `stale: false` because they provide no comparison anchor.
|
|
32
32
|
*
|
|
33
33
|
* @param projectRoot Root directory of the project.
|
|
34
34
|
* @param engineDir Path to the engine directory.
|
|
@@ -52,11 +52,10 @@ export declare const FULL_SUITE_REQUEST = "(entire suite)";
|
|
|
52
52
|
* `_tests/` support fixtures and hang rather than fail.
|
|
53
53
|
*
|
|
54
54
|
* Coverage semantics: an absent claim and `'full'` cover everything. A
|
|
55
|
-
* scoped list covers a request path when the request equals a covered entry
|
|
56
|
-
* sits beneath
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* covered file is packaged too). Both sides are normalized to forward
|
|
55
|
+
* scoped list covers a request path when the request equals a covered entry
|
|
56
|
+
* or sits beneath it. A file claim never widens to its containing directory:
|
|
57
|
+
* sibling files can belong to different manifests or harnesses, and a dot in
|
|
58
|
+
* a directory name does not make it a file. Both sides are normalized to forward
|
|
60
59
|
* slashes so Windows-style CLI input cannot defeat the prefix rule (baseline
|
|
61
60
|
* paths are POSIX by convention). A request with no paths is a full-suite
|
|
62
61
|
* run and is never covered by a scoped list, so the
|
|
@@ -35,14 +35,14 @@ const STALE_PATHS_LIMIT = 10;
|
|
|
35
35
|
* Probes the engine tree for packageable changes since the last successful
|
|
36
36
|
* `fireforge build`. Returns a summary the `fireforge test` handler renders
|
|
37
37
|
* as an up-front warning when `--build` was not passed. The probe never
|
|
38
|
-
*
|
|
39
|
-
*
|
|
38
|
+
* refuses a malformed persisted baseline. Git failures and a missing baseline
|
|
39
|
+
* degrade to `stale: false` because they provide no comparison anchor.
|
|
40
40
|
*
|
|
41
41
|
* @param projectRoot Root directory of the project.
|
|
42
42
|
* @param engineDir Path to the engine directory.
|
|
43
43
|
*/
|
|
44
44
|
export async function checkStaleBuildForTest(projectRoot, engineDir) {
|
|
45
|
-
const baseline = await readBuildBaseline(projectRoot);
|
|
45
|
+
const baseline = await readBuildBaseline(projectRoot, 'refuse');
|
|
46
46
|
if (!baseline) {
|
|
47
47
|
return { stale: false, changedPaths: [], truncated: 0, baseline: undefined };
|
|
48
48
|
}
|
|
@@ -93,11 +93,10 @@ export const FULL_SUITE_REQUEST = '(entire suite)';
|
|
|
93
93
|
* `_tests/` support fixtures and hang rather than fail.
|
|
94
94
|
*
|
|
95
95
|
* Coverage semantics: an absent claim and `'full'` cover everything. A
|
|
96
|
-
* scoped list covers a request path when the request equals a covered entry
|
|
97
|
-
* sits beneath
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
* covered file is packaged too). Both sides are normalized to forward
|
|
96
|
+
* scoped list covers a request path when the request equals a covered entry
|
|
97
|
+
* or sits beneath it. A file claim never widens to its containing directory:
|
|
98
|
+
* sibling files can belong to different manifests or harnesses, and a dot in
|
|
99
|
+
* a directory name does not make it a file. Both sides are normalized to forward
|
|
101
100
|
* slashes so Windows-style CLI input cannot defeat the prefix rule (baseline
|
|
102
101
|
* paths are POSIX by convention). A request with no paths is a full-suite
|
|
103
102
|
* run and is never covered by a scoped list, so the
|
|
@@ -114,29 +113,9 @@ export function findUncoveredRequestPaths(coverage, requestedPaths) {
|
|
|
114
113
|
}
|
|
115
114
|
return requestedPaths.filter((requested) => {
|
|
116
115
|
const path = normalizeCoveragePath(requested);
|
|
117
|
-
|
|
118
|
-
return !covered.some((c) => path === c || path.startsWith(`${c}/`) || granule === toManifestGranule(c));
|
|
116
|
+
return !covered.some((c) => path === c || path.startsWith(`${c}/`));
|
|
119
117
|
});
|
|
120
118
|
}
|
|
121
|
-
/**
|
|
122
|
-
* Maps a normalized request/coverage path to the "manifest granule" the
|
|
123
|
-
* packaged runtime actually staged: an extension-bearing basename (a test
|
|
124
|
-
* FILE) maps to its containing directory, a directory (no dot in the
|
|
125
|
-
* basename) maps to itself. Purely lexical: the directory-as-manifest
|
|
126
|
-
* approximation holds because xpcshell/mochitest manifests live next to
|
|
127
|
-
* their test files and a scoped `test --build` stages the whole manifest
|
|
128
|
-
* directory into `obj-*`/`_tests/`, not single files. Caveat: a directory
|
|
129
|
-
* whose basename contains a dot is misread as a file and mapped to its
|
|
130
|
-
* parent, which widens (never narrows) the covered granule.
|
|
131
|
-
*/
|
|
132
|
-
function toManifestGranule(path) {
|
|
133
|
-
const slash = path.lastIndexOf('/');
|
|
134
|
-
const base = slash === -1 ? path : path.slice(slash + 1);
|
|
135
|
-
if (!base.includes('.')) {
|
|
136
|
-
return path;
|
|
137
|
-
}
|
|
138
|
-
return slash === -1 ? '' : path.slice(0, slash);
|
|
139
|
-
}
|
|
140
119
|
/** Normalizes a path for coverage comparison: forward slashes, no trailing slash. */
|
|
141
120
|
function normalizeCoveragePath(path) {
|
|
142
121
|
return normalizePathSlashes(path.trim()).replace(/\/+$/, '');
|
|
@@ -196,9 +175,7 @@ export async function findChangedTestManifestsForPaths(engineDir, baseline, unco
|
|
|
196
175
|
verbose(`Coverage refusal: manifest probe failed — ${toError(error).message}`);
|
|
197
176
|
return [];
|
|
198
177
|
}
|
|
199
|
-
const requestDirs = uncovered
|
|
200
|
-
.filter((p) => p !== FULL_SUITE_REQUEST)
|
|
201
|
-
.map((p) => toManifestGranule(normalizeCoveragePath(p)));
|
|
178
|
+
const requestDirs = uncovered.filter((p) => p !== FULL_SUITE_REQUEST).map(normalizeCoveragePath);
|
|
202
179
|
const hits = changed.filter((path) => {
|
|
203
180
|
if (!isTestManifestPath(path))
|
|
204
181
|
return false;
|
|
@@ -5,32 +5,13 @@
|
|
|
5
5
|
* agree on what "exists" means.
|
|
6
6
|
*/
|
|
7
7
|
export declare function categoryHeaderExists(lines: string[], category: string): boolean;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
* - Multi-line: `/* =====` opening, `Foo` on any of the next ~5 lines,
|
|
16
|
-
* closing `*\/`.
|
|
17
|
-
*
|
|
18
|
-
* This helper exists as a pure inspector. It never throws on malformed
|
|
19
|
-
* headers and silently skips shapes it cannot parse.
|
|
20
|
-
*/
|
|
21
|
-
/**
|
|
22
|
-
* Category name declared by the banner starting at `index`, or `undefined`
|
|
23
|
-
* when that line opens no banner.
|
|
24
|
-
*
|
|
25
|
-
* The single reader for both banner shapes, so the inventory walk, the
|
|
26
|
-
* "available categories" error body and the section finder cannot drift on
|
|
27
|
-
* what counts as a header.
|
|
28
|
-
*
|
|
29
|
-
* @param lines - Tokens CSS split into lines
|
|
30
|
-
* @param index - 0-based line index to test
|
|
31
|
-
* @returns The declared category name, or undefined
|
|
32
|
-
*/
|
|
33
|
-
export declare function categoryBannerNameAt(lines: string[], index: number): string | undefined;
|
|
8
|
+
export interface TokenCategorySection {
|
|
9
|
+
name: string;
|
|
10
|
+
categoryLine: number;
|
|
11
|
+
sectionEnd: number;
|
|
12
|
+
}
|
|
13
|
+
/** Parses category regions owned by the base root, including nested CSS rules. */
|
|
14
|
+
export declare function parseTokenCategorySections(lines: string[]): TokenCategorySection[];
|
|
34
15
|
/**
|
|
35
16
|
* Asserts the category banner exists (or that creation was requested).
|
|
36
17
|
* Throws a GeneralError naming the available categories and the
|
|
@@ -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,46 @@ 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
|
-
// Skip past the current header block first
|
|
241
|
-
let scanStart = categoryLine + 1;
|
|
242
|
-
for (let i = categoryLine + 1; i < lines.length; i++) {
|
|
243
|
-
const line = lines[i] ?? '';
|
|
244
|
-
if (/^\s*\/\*\s*=/.test(line) || /^\s*\*\s*=/.test(line) || /^\s*\*\//.test(line)) {
|
|
245
|
-
scanStart = i + 1;
|
|
246
|
-
continue;
|
|
247
|
-
}
|
|
248
|
-
break;
|
|
249
|
-
}
|
|
250
|
-
let sectionEnd = lines.length;
|
|
251
|
-
for (let i = scanStart; i < lines.length; i++) {
|
|
252
|
-
const line = lines[i] ?? '';
|
|
253
|
-
if (isSingleLineBannerLine(line) ||
|
|
254
|
-
(/^\s*\/\*\s*=+/.test(line) && !/\*\//.test(line)) ||
|
|
255
|
-
/^\s*\}/.test(line)) {
|
|
256
|
-
sectionEnd = i;
|
|
257
|
-
break;
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
return { categoryLine, sectionEnd };
|
|
210
|
+
return { categoryLine: section.categoryLine, sectionEnd: section.sectionEnd };
|
|
261
211
|
}
|
|
262
212
|
/** 0-based open/close line indices of the base `:root {` block. */
|
|
263
213
|
export function findBaseRootBounds(lines) {
|
|
264
214
|
const open = lines.findIndex((line) => /:root\s*\{/.test(line));
|
|
265
215
|
if (open === -1)
|
|
266
216
|
return undefined;
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
217
|
+
const masked = maskCommentLines(lines);
|
|
218
|
+
let depth = 0;
|
|
219
|
+
for (let i = open; i < lines.length; i++) {
|
|
220
|
+
// Ignore braces in CSS strings (including data URLs).
|
|
221
|
+
const code = (masked[i] ?? '').replace(/(["'])(?:\\.|(?!\1).)*\1/g, '');
|
|
222
|
+
for (const char of code) {
|
|
223
|
+
if (char === '{')
|
|
224
|
+
depth++;
|
|
225
|
+
if (char === '}' && --depth === 0)
|
|
226
|
+
return { open, close: i };
|
|
270
227
|
}
|
|
271
228
|
}
|
|
272
229
|
return { open, close: -1 };
|
|
273
230
|
}
|
|
274
231
|
/** Masks block-comment content per line so declarations inside comments never match. */
|
|
275
232
|
export function maskCommentLines(lines) {
|
|
276
|
-
const
|
|
277
|
-
|
|
233
|
+
const source = lines.join('\n');
|
|
234
|
+
const parts = [];
|
|
235
|
+
let cursor = 0;
|
|
236
|
+
for (;;) {
|
|
237
|
+
const open = source.indexOf('/*', cursor);
|
|
238
|
+
if (open === -1)
|
|
239
|
+
break;
|
|
240
|
+
const close = source.indexOf('*/', open + 2);
|
|
241
|
+
// An unfinished edit has no complete comment to mask. Searching only
|
|
242
|
+
// once prevents restarting a regex at every repeated comment opener.
|
|
243
|
+
if (close === -1)
|
|
244
|
+
break;
|
|
245
|
+
parts.push(source.slice(cursor, open), source.slice(open, close + 2).replace(/[^\n]/g, ' '));
|
|
246
|
+
cursor = close + 2;
|
|
247
|
+
}
|
|
248
|
+
parts.push(source.slice(cursor));
|
|
249
|
+
return parts.join('').split('\n');
|
|
278
250
|
}
|
|
279
251
|
/**
|
|
280
252
|
* Finds an existing declaration of `tokenName` inside the base `:root`
|
|
@@ -288,38 +260,13 @@ export function findTokenDeclarationInRoot(lines, tokenName) {
|
|
|
288
260
|
if (bounds === undefined || bounds.close === -1)
|
|
289
261
|
return undefined;
|
|
290
262
|
const masked = maskCommentLines(lines);
|
|
263
|
+
const sections = parseTokenCategorySections(lines);
|
|
291
264
|
const declPattern = new RegExp(`^\\s*${escapeRegex(tokenName)}\\s*:`);
|
|
292
265
|
for (let i = bounds.open + 1; i < bounds.close; i++) {
|
|
293
266
|
if (!declPattern.test(masked[i] ?? ''))
|
|
294
267
|
continue;
|
|
295
|
-
const category =
|
|
268
|
+
const category = sections.find((section) => i > section.categoryLine && i < section.sectionEnd)?.name;
|
|
296
269
|
return category === undefined ? { line: i + 1 } : { line: i + 1, category };
|
|
297
270
|
}
|
|
298
271
|
return undefined;
|
|
299
272
|
}
|
|
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() });
|
|
@@ -27,6 +27,12 @@
|
|
|
27
27
|
*/
|
|
28
28
|
/** Tools the preflight knows how to probe. */
|
|
29
29
|
export type ToolchainTool = 'cbindgen' | 'rustc';
|
|
30
|
+
/**
|
|
31
|
+
* Resolves the mach state directory the same way mach itself does:
|
|
32
|
+
* `$MOZBUILD_STATE_PATH`, else `~/.mozbuild` (same resolution as
|
|
33
|
+
* `mach-resource-shim.ts`).
|
|
34
|
+
*/
|
|
35
|
+
export declare function mozbuildStateDir(): string;
|
|
30
36
|
/** One probed candidate binary for a tool. */
|
|
31
37
|
export interface ToolchainCandidate {
|
|
32
38
|
/** Binary path (or bare name for the PATH candidate). */
|
|
@@ -37,7 +37,7 @@ import { verbose } from '../utils/logger.js';
|
|
|
37
37
|
* `$MOZBUILD_STATE_PATH`, else `~/.mozbuild` (same resolution as
|
|
38
38
|
* `mach-resource-shim.ts`).
|
|
39
39
|
*/
|
|
40
|
-
function mozbuildStateDir() {
|
|
40
|
+
export function mozbuildStateDir() {
|
|
41
41
|
return process.env['MOZBUILD_STATE_PATH'] ?? join(homedir(), '.mozbuild');
|
|
42
42
|
}
|
|
43
43
|
/** Where each tool's minimum is declared inside the Firefox tree. */
|
|
@@ -26,6 +26,16 @@ import type { TypecheckProjectResult } from '../types/typecheck.js';
|
|
|
26
26
|
* in CI logs.
|
|
27
27
|
*/
|
|
28
28
|
export declare const CHECK_JS_DISABLED_NOTICE = "Project sets \"checkJs: false\" \u2014 skipping (override the jsconfig to enable typecheck).";
|
|
29
|
+
/** Run-level options for {@link runTypecheck}. */
|
|
30
|
+
export interface RunTypecheckOptions {
|
|
31
|
+
/**
|
|
32
|
+
* Directory for the per-project incremental build info. Defaults to
|
|
33
|
+
* `<projectRoot>/.fireforge/typecheck`.
|
|
34
|
+
*/
|
|
35
|
+
cacheDir?: string;
|
|
36
|
+
/** Build every project from scratch and write no build info. */
|
|
37
|
+
noCache?: boolean;
|
|
38
|
+
}
|
|
29
39
|
/**
|
|
30
40
|
* Runs `fireforge typecheck` against every project listed in `cfg.projects`.
|
|
31
41
|
*
|
|
@@ -55,7 +65,7 @@ export declare const CHECK_JS_DISABLED_NOTICE = "Project sets \"checkJs: false\"
|
|
|
55
65
|
* in declared order. The CLI is responsible for printing the issues
|
|
56
66
|
* and computing the exit code.
|
|
57
67
|
*/
|
|
58
|
-
export declare function runTypecheck(projectRoot: string, cfg: TypecheckConfig): Promise<TypecheckProjectResult[]>;
|
|
68
|
+
export declare function runTypecheck(projectRoot: string, cfg: TypecheckConfig, runOptions?: RunTypecheckOptions): Promise<TypecheckProjectResult[]>;
|
|
59
69
|
/**
|
|
60
70
|
* Converts an absolute path to a path relative to the project root,
|
|
61
71
|
* for display in CLI output. Falls back to the absolute path when
|