@hominis/fireforge 0.32.0 → 0.34.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 +25 -0
- package/dist/src/commands/build.js +9 -1
- package/dist/src/commands/doctor-furnace-jar.d.ts +16 -0
- package/dist/src/commands/doctor-furnace-jar.js +47 -0
- package/dist/src/commands/doctor-furnace.js +2 -0
- package/dist/src/commands/export-flow.js +4 -1
- package/dist/src/commands/export-placement-gate.js +4 -1
- package/dist/src/commands/export.js +61 -4
- package/dist/src/commands/furnace/chrome-doc-templates.d.ts +20 -0
- package/dist/src/commands/furnace/chrome-doc-templates.js +52 -0
- package/dist/src/commands/furnace/chrome-doc.d.ts +10 -0
- package/dist/src/commands/furnace/chrome-doc.js +31 -4
- package/dist/src/commands/furnace/create-browser-test.d.ts +30 -0
- package/dist/src/commands/furnace/create-browser-test.js +180 -0
- package/dist/src/commands/furnace/create-mochikit.js +11 -4
- package/dist/src/commands/furnace/create-xpcshell.d.ts +1 -1
- package/dist/src/commands/furnace/create-xpcshell.js +38 -12
- package/dist/src/commands/furnace/create.js +7 -114
- package/dist/src/commands/furnace/index.js +6 -1
- package/dist/src/commands/furnace/scan.d.ts +1 -0
- package/dist/src/commands/furnace/scan.js +57 -27
- package/dist/src/commands/furnace/validate.js +17 -1
- package/dist/src/commands/patch/split-plan.d.ts +18 -2
- package/dist/src/commands/patch/split-plan.js +90 -16
- package/dist/src/commands/patch/split.js +12 -3
- package/dist/src/commands/re-export-options.d.ts +26 -0
- package/dist/src/commands/re-export-options.js +48 -1
- package/dist/src/commands/re-export.js +4 -1
- package/dist/src/commands/register.js +10 -1
- package/dist/src/commands/test-diagnose.js +12 -0
- package/dist/src/commands/test.js +29 -22
- package/dist/src/commands/token.js +12 -1
- package/dist/src/commands/typecheck.js +35 -0
- package/dist/src/core/build-prepare.js +23 -3
- package/dist/src/core/config-validate.js +26 -0
- package/dist/src/core/furnace-apply-dry-run.d.ts +17 -0
- package/dist/src/core/furnace-apply-dry-run.js +105 -0
- package/dist/src/core/furnace-apply-ftl.d.ts +12 -0
- package/dist/src/core/furnace-apply-ftl.js +97 -1
- package/dist/src/core/furnace-apply-helpers.js +10 -80
- package/dist/src/core/furnace-registration.d.ts +24 -0
- package/dist/src/core/furnace-registration.js +56 -3
- package/dist/src/core/furnace-validate-registration.js +18 -0
- package/dist/src/core/mach-resource-shim.d.ts +58 -0
- package/dist/src/core/mach-resource-shim.js +290 -0
- package/dist/src/core/mach.d.ts +51 -10
- package/dist/src/core/mach.js +76 -25
- package/dist/src/core/manifest-helpers.d.ts +13 -0
- package/dist/src/core/manifest-helpers.js +29 -4
- package/dist/src/core/manifest-rules.d.ts +13 -3
- package/dist/src/core/manifest-rules.js +44 -17
- package/dist/src/core/patch-export.d.ts +10 -0
- package/dist/src/core/patch-export.js +23 -2
- package/dist/src/core/patch-lint-cross.d.ts +31 -0
- package/dist/src/core/patch-lint-cross.js +83 -63
- package/dist/src/core/patch-lint-reexports.d.ts +1 -1
- package/dist/src/core/patch-lint-reexports.js +1 -1
- package/dist/src/core/register-module.d.ts +1 -1
- package/dist/src/core/register-module.js +15 -2
- package/dist/src/core/register-result.d.ts +9 -0
- package/dist/src/core/register-scaffold.d.ts +55 -0
- package/dist/src/core/register-scaffold.js +146 -0
- package/dist/src/core/register-test-manifest.js +3 -1
- package/dist/src/core/register-xpcshell-test.d.ts +30 -0
- package/dist/src/core/register-xpcshell-test.js +96 -0
- package/dist/src/core/test-harness-crash.d.ts +32 -4
- package/dist/src/core/test-harness-crash.js +152 -14
- package/dist/src/core/token-dark-mode.d.ts +9 -0
- package/dist/src/core/token-dark-mode.js +1 -1
- package/dist/src/core/token-docs.d.ts +32 -0
- package/dist/src/core/token-docs.js +101 -0
- package/dist/src/core/token-manager.d.ts +8 -0
- package/dist/src/core/token-manager.js +77 -95
- package/dist/src/core/token-variant.d.ts +39 -0
- package/dist/src/core/token-variant.js +141 -0
- package/dist/src/core/typecheck.js +56 -28
- package/dist/src/core/xpcshell-appdir.d.ts +7 -0
- package/dist/src/core/xpcshell-appdir.js +12 -3
- package/dist/src/types/commands/options.d.ts +17 -0
- package/dist/src/types/config.d.ts +13 -0
- package/package.json +4 -4
|
@@ -5,8 +5,15 @@ import { pathExists, readText } from '../utils/fs.js';
|
|
|
5
5
|
import { escapeRegex } from '../utils/regex.js';
|
|
6
6
|
import { getProjectPaths, loadConfig } from './config.js';
|
|
7
7
|
import { registerBrowserContent, registerFireForgeModule, registerSharedCSS, registerTestManifest, registerToolkitWidget, } from './manifest-register.js';
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
import { isXpcshellTestRegistered, registerXpcshellTest } from './register-xpcshell-test.js';
|
|
9
|
+
/**
|
|
10
|
+
* Returns manifest registration rules for the supported engine file
|
|
11
|
+
* patterns. `createManifest` (from `register --create-manifest`) lets the
|
|
12
|
+
* module and xpcshell-test rules scaffold a missing manifest (directory
|
|
13
|
+
* moz.build / xpcshell.toml) and wire the parent chain instead of failing
|
|
14
|
+
* with "Manifest not found".
|
|
15
|
+
*/
|
|
16
|
+
export function getRules(binaryName, createManifest = false) {
|
|
10
17
|
const moduleDir = `browser/modules/${binaryName}`;
|
|
11
18
|
return [
|
|
12
19
|
{
|
|
@@ -39,7 +46,10 @@ export function getRules(binaryName) {
|
|
|
39
46
|
extractArgs: (m) => [m[1] ?? ''],
|
|
40
47
|
},
|
|
41
48
|
{
|
|
42
|
-
|
|
49
|
+
// Fork-owned browser.toml manifests at ARBITRARY depth under
|
|
50
|
+
// browser/base/content/test/ (0.34.0 field report: only one level
|
|
51
|
+
// was supported, rejecting nested browser-chrome manifests).
|
|
52
|
+
pattern: /^browser\/base\/content\/test\/((?:[^/]+\/)*[^/]+)\/browser\.toml$/,
|
|
43
53
|
isRegistered: (_engineDir, testDir) => isTestManifestRegistered(_engineDir, testDir),
|
|
44
54
|
register: (_engineDir, _after, dryRun, testDir) => registerTestManifest(_engineDir, testDir, dryRun),
|
|
45
55
|
extractArgs: (m) => [m[1] ?? ''],
|
|
@@ -47,7 +57,7 @@ export function getRules(binaryName) {
|
|
|
47
57
|
{
|
|
48
58
|
pattern: new RegExp(`^browser/modules/${escapeRegex(binaryName)}/(.+\\.sys\\.mjs)$`),
|
|
49
59
|
isRegistered: (_engineDir, fileName) => isFireForgeModuleRegistered(_engineDir, fileName, moduleDir),
|
|
50
|
-
register: (_engineDir, _after, dryRun, fileName) => registerFireForgeModule(_engineDir, fileName, moduleDir, dryRun),
|
|
60
|
+
register: (_engineDir, _after, dryRun, fileName) => registerFireForgeModule(_engineDir, fileName, moduleDir, dryRun, createManifest),
|
|
51
61
|
extractArgs: (m) => [m[1] ?? ''],
|
|
52
62
|
},
|
|
53
63
|
{
|
|
@@ -56,6 +66,20 @@ export function getRules(binaryName) {
|
|
|
56
66
|
register: (_engineDir, _after, dryRun, tagName, fileName) => registerToolkitWidget(_engineDir, tagName, fileName, dryRun),
|
|
57
67
|
extractArgs: (m) => [m[1] ?? '', m[2] ?? ''],
|
|
58
68
|
},
|
|
69
|
+
{
|
|
70
|
+
// xpcshell test files (0.34.0 field report: rejected as "Unknown
|
|
71
|
+
// file pattern"). Any `test_*.js` outside browser/base/content/test/
|
|
72
|
+
// (which stays browser-chrome territory with its browser.toml
|
|
73
|
+
// guidance) registers into the directory's xpcshell.toml; with
|
|
74
|
+
// --create-manifest a missing manifest is scaffolded and wired via
|
|
75
|
+
// XPCSHELL_TESTS_MANIFESTS.
|
|
76
|
+
pattern: /^(?!browser\/base\/content\/test\/)(.+)\/(test_[^/]+\.js)$/,
|
|
77
|
+
isRegistered: (engineDir, dirRel, fileName) => isXpcshellTestRegistered(engineDir, dirRel, fileName),
|
|
78
|
+
register: (engineDir, _after, dryRun, dirRel, fileName) => registerXpcshellTest(engineDir, dirRel, fileName, dryRun, createManifest),
|
|
79
|
+
// Both groups are guaranteed by the pattern; slice avoids dead
|
|
80
|
+
// fallback branches.
|
|
81
|
+
extractArgs: (m) => m.slice(1, 3),
|
|
82
|
+
},
|
|
59
83
|
];
|
|
60
84
|
}
|
|
61
85
|
async function isSharedCSSRegistered(engineDir, fileName) {
|
|
@@ -149,7 +173,8 @@ function getUnregistrableAdvice(filePath) {
|
|
|
149
173
|
return (`xpcshell.toml manifests are not auto-registered by "fireforge register". ` +
|
|
150
174
|
`Add "XPCSHELL_TESTS_MANIFESTS += ['${basename(filePath)}']" to the ` +
|
|
151
175
|
`moz.build that covers ${filePath.replace(/\/xpcshell\.toml$/, '/')}, ` +
|
|
152
|
-
'or
|
|
176
|
+
'or register one of its test files with "fireforge register <path>/test_*.js --create-manifest", ' +
|
|
177
|
+
'which scaffolds the manifest and wires XPCSHELL_TESTS_MANIFESTS automatically. ' +
|
|
153
178
|
'Browser-chrome tests (browser.toml) are auto-registered via "fireforge register <path>/browser.toml".');
|
|
154
179
|
}
|
|
155
180
|
const testMatch = filePath.match(/^browser\/base\/content\/test\/([^/]+)\/(?!browser\.toml$).+$/);
|
|
@@ -183,12 +208,17 @@ export async function isFileRegistered(root, filePath) {
|
|
|
183
208
|
if (advice) {
|
|
184
209
|
throw new InvalidArgumentError(advice, 'path');
|
|
185
210
|
}
|
|
186
|
-
throw new InvalidArgumentError(
|
|
211
|
+
throw new InvalidArgumentError(buildUnknownPatternMessage(normalizedPath, config.binaryName), 'path');
|
|
212
|
+
}
|
|
213
|
+
/** Shared "Unknown file pattern" message naming every supported pattern. */
|
|
214
|
+
function buildUnknownPatternMessage(normalizedPath, binaryName) {
|
|
215
|
+
return (`Unknown file pattern: "${normalizedPath}". Supported patterns:\n` +
|
|
187
216
|
' browser/themes/shared/*.css\n' +
|
|
188
217
|
' browser/base/content/*.{js,mjs,xhtml,css}\n' +
|
|
189
|
-
' browser/base/content/test
|
|
190
|
-
` browser/modules/${
|
|
191
|
-
' toolkit/content/widgets/*/*.{mjs,css}'
|
|
218
|
+
' browser/base/content/test/**/browser.toml\n' +
|
|
219
|
+
` browser/modules/${binaryName}/*.sys.mjs\n` +
|
|
220
|
+
' toolkit/content/widgets/*/*.{mjs,css}\n' +
|
|
221
|
+
' **/test_*.js (xpcshell test files; registers into the nearest xpcshell.toml)');
|
|
192
222
|
}
|
|
193
223
|
/**
|
|
194
224
|
* Registers a file in the appropriate build manifest.
|
|
@@ -197,12 +227,14 @@ export async function isFileRegistered(root, filePath) {
|
|
|
197
227
|
* @param filePath - Path relative to engine/
|
|
198
228
|
* @param dryRun - If true, return what would be done without writing
|
|
199
229
|
* @param after - Optional substring to place entry after (instead of alphabetical)
|
|
230
|
+
* @param options - `createManifest` scaffolds a missing manifest (directory
|
|
231
|
+
* moz.build / xpcshell.toml) and wires the parent chain instead of failing
|
|
200
232
|
* @returns Registration result
|
|
201
233
|
*/
|
|
202
|
-
export async function registerFile(root, filePath, dryRun = false, after) {
|
|
234
|
+
export async function registerFile(root, filePath, dryRun = false, after, options = {}) {
|
|
203
235
|
const { engine: engineDir } = getProjectPaths(root);
|
|
204
236
|
const config = await loadConfig(root);
|
|
205
|
-
const rules = getRules(config.binaryName);
|
|
237
|
+
const rules = getRules(config.binaryName, options.createManifest === true);
|
|
206
238
|
// Normalize path separators
|
|
207
239
|
const normalizedPath = filePath.replace(/\\/g, '/');
|
|
208
240
|
for (const rule of rules) {
|
|
@@ -216,11 +248,6 @@ export async function registerFile(root, filePath, dryRun = false, after) {
|
|
|
216
248
|
if (advice) {
|
|
217
249
|
throw new InvalidArgumentError(advice, 'path');
|
|
218
250
|
}
|
|
219
|
-
throw new InvalidArgumentError(
|
|
220
|
-
' browser/themes/shared/*.css\n' +
|
|
221
|
-
' browser/base/content/*.{js,mjs,xhtml,css}\n' +
|
|
222
|
-
' browser/base/content/test/*/browser.toml\n' +
|
|
223
|
-
` browser/modules/${config.binaryName}/*.sys.mjs\n` +
|
|
224
|
-
' toolkit/content/widgets/*/*.{mjs,css}', 'path');
|
|
251
|
+
throw new InvalidArgumentError(buildUnknownPatternMessage(normalizedPath, config.binaryName), 'path');
|
|
225
252
|
}
|
|
226
253
|
//# sourceMappingURL=manifest-rules.js.map
|
|
@@ -20,6 +20,16 @@ export declare function getNextPatchNumber(patchesDir: string): Promise<string>;
|
|
|
20
20
|
* to a slug `export` could never reach.
|
|
21
21
|
*/
|
|
22
22
|
export declare function sanitizeName(name: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* Strips a leading `NNN-<category>-` prefix from a sanitized name slug.
|
|
25
|
+
* Operators frequently pass the DESIRED FILENAME stem to `--name`
|
|
26
|
+
* (`--name 203-ui-foo --category ui`), which 0.33.0 double-prefixed into
|
|
27
|
+
* `203-ui-203-ui-foo.patch`. The filename builders prepend the order and
|
|
28
|
+
* category themselves, so a matching prefix in the name is always
|
|
29
|
+
* redundant. Applied repeatedly so a twice-prefixed slug (from a previous
|
|
30
|
+
* double-prefix incident) also collapses. Exported for direct testing.
|
|
31
|
+
*/
|
|
32
|
+
export declare function stripRedundantCategoryPrefix(sanitizedName: string, category: string): string;
|
|
23
33
|
/**
|
|
24
34
|
* Generates the next patch filename with category.
|
|
25
35
|
* @param patchesDir - Path to the patches directory
|
|
@@ -46,6 +46,27 @@ export function sanitizeName(name) {
|
|
|
46
46
|
.replace(/^-+|-+$/g, '')
|
|
47
47
|
.slice(0, 50);
|
|
48
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Strips a leading `NNN-<category>-` prefix from a sanitized name slug.
|
|
51
|
+
* Operators frequently pass the DESIRED FILENAME stem to `--name`
|
|
52
|
+
* (`--name 203-ui-foo --category ui`), which 0.33.0 double-prefixed into
|
|
53
|
+
* `203-ui-203-ui-foo.patch`. The filename builders prepend the order and
|
|
54
|
+
* category themselves, so a matching prefix in the name is always
|
|
55
|
+
* redundant. Applied repeatedly so a twice-prefixed slug (from a previous
|
|
56
|
+
* double-prefix incident) also collapses. Exported for direct testing.
|
|
57
|
+
*/
|
|
58
|
+
export function stripRedundantCategoryPrefix(sanitizedName, category) {
|
|
59
|
+
const prefix = new RegExp(`^\\d+-${category}-`);
|
|
60
|
+
let stripped = sanitizedName;
|
|
61
|
+
while (prefix.test(stripped)) {
|
|
62
|
+
stripped = stripped.replace(prefix, '');
|
|
63
|
+
}
|
|
64
|
+
return stripped.length > 0 ? stripped : sanitizedName;
|
|
65
|
+
}
|
|
66
|
+
/** Sanitizes a patch name and drops a redundant `NNN-<category>-` prefix. */
|
|
67
|
+
function patchNameSlug(name, category) {
|
|
68
|
+
return stripRedundantCategoryPrefix(sanitizeName(name), category);
|
|
69
|
+
}
|
|
49
70
|
/**
|
|
50
71
|
* Generates the next patch filename with category.
|
|
51
72
|
* @param patchesDir - Path to the patches directory
|
|
@@ -55,7 +76,7 @@ export function sanitizeName(name) {
|
|
|
55
76
|
*/
|
|
56
77
|
export async function getNextPatchFilename(patchesDir, category, name) {
|
|
57
78
|
const patchNumber = await getNextPatchNumber(patchesDir);
|
|
58
|
-
const sanitizedName =
|
|
79
|
+
const sanitizedName = patchNameSlug(name, category);
|
|
59
80
|
return `${patchNumber}-${category}-${sanitizedName}.patch`;
|
|
60
81
|
}
|
|
61
82
|
/**
|
|
@@ -253,7 +274,7 @@ async function computeExportPlanUnderLock(input) {
|
|
|
253
274
|
? allocatePolicyOrder(input.config, manifestBefore?.patches ?? [], input.category)
|
|
254
275
|
: null;
|
|
255
276
|
const patchFilename = policyOrder !== null
|
|
256
|
-
? `${String(policyOrder).padStart(3, '0')}-${input.category}-${
|
|
277
|
+
? `${String(policyOrder).padStart(3, '0')}-${input.category}-${patchNameSlug(input.name, input.category)}.patch`
|
|
257
278
|
: await getNextPatchFilename(input.patchesDir, input.category, input.name);
|
|
258
279
|
const patchPath = join(input.patchesDir, patchFilename);
|
|
259
280
|
const metadata = {
|
|
@@ -180,6 +180,37 @@ export declare function findForwardImportIgnoreLines(source: string): Set<number
|
|
|
180
180
|
* engine file — not our concern).
|
|
181
181
|
* - Imports on a line suppressed by the ignore marker.
|
|
182
182
|
*/
|
|
183
|
+
/** One discovered forward-import edge: a site importing a later-created file. */
|
|
184
|
+
export interface ForwardImportEdge {
|
|
185
|
+
/** Importing patch filename. */
|
|
186
|
+
entry: string;
|
|
187
|
+
/** Importing file path relative to engine/. */
|
|
188
|
+
sitePath: string;
|
|
189
|
+
/** Exact import specifier as it appears in source. */
|
|
190
|
+
specifier: string;
|
|
191
|
+
/** Specifier with any query/hash stripped (used for fingerprints). */
|
|
192
|
+
cleaned: string;
|
|
193
|
+
/** Later-ordered patch creating the imported file. */
|
|
194
|
+
owner: string;
|
|
195
|
+
/** Path of the later-created file (relative to engine/). */
|
|
196
|
+
creates: string;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Returns every forward-import edge in the queue, one per (site, later
|
|
200
|
+
* creator) pair, regardless of any staged-dependency declarations. Used by
|
|
201
|
+
* `patch split` to discover the forward edges it introduces into the new
|
|
202
|
+
* patch so it can auto-declare them (and so its projected lint matches the
|
|
203
|
+
* real per-patch gate).
|
|
204
|
+
*/
|
|
205
|
+
export declare function collectForwardImportEdges(ctx: PatchQueueContext): ForwardImportEdge[];
|
|
206
|
+
/**
|
|
207
|
+
* Cross-patch lint rule: flag imports of a module a later-ordered patch is
|
|
208
|
+
* responsible for creating, unless the patch declares an exact staged
|
|
209
|
+
* forward-import for it. Also warns on staged-dependency declarations that
|
|
210
|
+
* never matched (stale). Matching is conservative — by basename, not by
|
|
211
|
+
* resolving `resource://`/`chrome://` URLs — with an inline ignore marker as
|
|
212
|
+
* an escape hatch for unrelated basename collisions.
|
|
213
|
+
*/
|
|
183
214
|
export declare function lintPatchQueueForwardImports(ctx: PatchQueueContext): PatchLintIssue[];
|
|
184
215
|
/**
|
|
185
216
|
* Cross-patch lint orchestrator. Runs every cross-patch rule against the
|
|
@@ -343,27 +343,8 @@ function findMatchingStagedDependency(entry, sitePath, specifier, laterOwners) {
|
|
|
343
343
|
laterOwners.some((owner) => owner.fullPath === dependency.creates &&
|
|
344
344
|
(dependency.owner === undefined || dependency.owner === owner.filename)));
|
|
345
345
|
}
|
|
346
|
-
/**
|
|
347
|
-
|
|
348
|
-
* responsible for creating.
|
|
349
|
-
*
|
|
350
|
-
* Approach is deliberately conservative — we do not resolve `resource://`
|
|
351
|
-
* URLs to engine file paths. Instead we build a cross-queue index of
|
|
352
|
-
* newly-created files keyed by their basename, and flag imports whose leaf
|
|
353
|
-
* matches an entry owned by a later-ordered patch. False positives from
|
|
354
|
-
* unrelated basename collisions (two different directories happening to
|
|
355
|
-
* create files named `Helper.sys.mjs`) are possible; the README documents
|
|
356
|
-
* the limitation and the inline ignore marker above provides an escape
|
|
357
|
-
* hatch.
|
|
358
|
-
*
|
|
359
|
-
* Rules out:
|
|
360
|
-
* - Imports whose leaf matches a newly-created file in the *same* or an
|
|
361
|
-
* *earlier* patch (legitimate use).
|
|
362
|
-
* - Imports whose leaf is not in the new-file index at all (pre-existing
|
|
363
|
-
* engine file — not our concern).
|
|
364
|
-
* - Imports on a line suppressed by the ignore marker.
|
|
365
|
-
*/
|
|
366
|
-
export function lintPatchQueueForwardImports(ctx) {
|
|
346
|
+
/** Builds the basename → later-creators index used by the forward-import scan. */
|
|
347
|
+
function buildNewFileIndex(ctx) {
|
|
367
348
|
const newFileIndex = new Map();
|
|
368
349
|
for (const entry of ctx.entries) {
|
|
369
350
|
for (const fullPath of entry.newFiles.keys()) {
|
|
@@ -376,23 +357,24 @@ export function lintPatchQueueForwardImports(ctx) {
|
|
|
376
357
|
owners.push({ filename: entry.filename, order: entry.order, fullPath });
|
|
377
358
|
}
|
|
378
359
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
360
|
+
return newFileIndex;
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Visits every forward-import site (an import whose leaf is created by a
|
|
364
|
+
* later-ordered patch). Shared by {@link lintPatchQueueForwardImports} (which
|
|
365
|
+
* resolves/reports each) and {@link collectForwardImportEdges} (which returns
|
|
366
|
+
* them structurally). We scan a created file's full body and a modified
|
|
367
|
+
* file's added lines only — flagging imports *this patch introduces*, not
|
|
368
|
+
* pre-existing HEAD imports that happen to collide with a later-created file.
|
|
369
|
+
*/
|
|
370
|
+
function eachForwardImportSite(ctx, newFileIndex, visit) {
|
|
387
371
|
const checkSite = (entry, sitePath, content) => {
|
|
388
372
|
if (!isForwardImportableFile(sitePath))
|
|
389
373
|
return;
|
|
390
374
|
const ignoreLines = findForwardImportIgnoreLines(content);
|
|
391
|
-
const
|
|
392
|
-
for (const { specifier, line } of extracted) {
|
|
375
|
+
for (const { specifier, line } of extractImportSpecifiersWithLines(content)) {
|
|
393
376
|
if (ignoreLines.has(line))
|
|
394
377
|
continue;
|
|
395
|
-
// Take the leaf and strip query/hash if any.
|
|
396
378
|
const cleaned = specifier.split(/[?#]/)[0] ?? specifier;
|
|
397
379
|
const leaf = basename(cleaned);
|
|
398
380
|
if (!leaf || !isForwardImportableFile(leaf))
|
|
@@ -400,41 +382,11 @@ export function lintPatchQueueForwardImports(ctx) {
|
|
|
400
382
|
const owners = newFileIndex.get(leaf);
|
|
401
383
|
if (!owners)
|
|
402
384
|
continue;
|
|
403
|
-
// Is the owner a later-ordered patch (or one ordered equal but
|
|
404
|
-
// lexicographically later as a tiebreaker)?
|
|
405
385
|
const laterOwners = owners.filter((owner) => owner.order > entry.order ||
|
|
406
386
|
(owner.order === entry.order && owner.filename > entry.filename));
|
|
407
387
|
if (laterOwners.length === 0)
|
|
408
388
|
continue;
|
|
409
|
-
|
|
410
|
-
if (stagedDependency) {
|
|
411
|
-
usedStagedDeclarations.add(stagedDependencyKey(entry, stagedDependency));
|
|
412
|
-
continue;
|
|
413
|
-
}
|
|
414
|
-
const ownersSummary = laterOwners
|
|
415
|
-
.map((o) => `${o.filename} (creates ${o.fullPath})`)
|
|
416
|
-
.join(', ');
|
|
417
|
-
const fingerprintOwners = [...laterOwners]
|
|
418
|
-
.map((o) => `${o.filename}:${o.fullPath}`)
|
|
419
|
-
.sort((a, b) => a.localeCompare(b))
|
|
420
|
-
.join(',');
|
|
421
|
-
// Lowest ordinal that lands AFTER every later-ordered creator —
|
|
422
|
-
// turns the operator's "guess and re-run" loop into a single shot
|
|
423
|
-
// when the only fix is reordering.
|
|
424
|
-
const suggestedOrder = Math.max(...laterOwners.map((o) => o.order)) + 1;
|
|
425
|
-
issues.push({
|
|
426
|
-
file: sitePath,
|
|
427
|
-
check: 'forward-import',
|
|
428
|
-
fingerprint: `forward-import|${sitePath}|${cleaned}|${fingerprintOwners}`,
|
|
429
|
-
message: `${sitePath} in ${entry.filename} imports "${specifier}", ` +
|
|
430
|
-
`but the matching new file is created by a later patch: ${ownersSummary}. ` +
|
|
431
|
-
'Reorder the patches so the dependency is created first, move the import ' +
|
|
432
|
-
'into the later patch, declare the intentional staged dependency with ' +
|
|
433
|
-
'"fireforge patch staged-dependency --add", or mark the import with ' +
|
|
434
|
-
`"// ${FORWARD_IMPORT_IGNORE_MARKER}" if the basename collision is a false positive. ` +
|
|
435
|
-
`Closest legal ordinal that satisfies this dependency: ${suggestedOrder}.`,
|
|
436
|
-
severity: 'error',
|
|
437
|
-
});
|
|
389
|
+
visit(entry, sitePath, specifier, cleaned, laterOwners);
|
|
438
390
|
}
|
|
439
391
|
};
|
|
440
392
|
for (const entry of ctx.entries) {
|
|
@@ -443,6 +395,74 @@ export function lintPatchQueueForwardImports(ctx) {
|
|
|
443
395
|
for (const [path, added] of entry.modifiedFileAdditions)
|
|
444
396
|
checkSite(entry, path, added);
|
|
445
397
|
}
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Returns every forward-import edge in the queue, one per (site, later
|
|
401
|
+
* creator) pair, regardless of any staged-dependency declarations. Used by
|
|
402
|
+
* `patch split` to discover the forward edges it introduces into the new
|
|
403
|
+
* patch so it can auto-declare them (and so its projected lint matches the
|
|
404
|
+
* real per-patch gate).
|
|
405
|
+
*/
|
|
406
|
+
export function collectForwardImportEdges(ctx) {
|
|
407
|
+
const newFileIndex = buildNewFileIndex(ctx);
|
|
408
|
+
const edges = [];
|
|
409
|
+
eachForwardImportSite(ctx, newFileIndex, (entry, sitePath, specifier, cleaned, laterOwners) => {
|
|
410
|
+
for (const owner of laterOwners) {
|
|
411
|
+
edges.push({
|
|
412
|
+
entry: entry.filename,
|
|
413
|
+
sitePath,
|
|
414
|
+
specifier,
|
|
415
|
+
cleaned,
|
|
416
|
+
owner: owner.filename,
|
|
417
|
+
creates: owner.fullPath,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
return edges;
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Cross-patch lint rule: flag imports of a module a later-ordered patch is
|
|
425
|
+
* responsible for creating, unless the patch declares an exact staged
|
|
426
|
+
* forward-import for it. Also warns on staged-dependency declarations that
|
|
427
|
+
* never matched (stale). Matching is conservative — by basename, not by
|
|
428
|
+
* resolving `resource://`/`chrome://` URLs — with an inline ignore marker as
|
|
429
|
+
* an escape hatch for unrelated basename collisions.
|
|
430
|
+
*/
|
|
431
|
+
export function lintPatchQueueForwardImports(ctx) {
|
|
432
|
+
const newFileIndex = buildNewFileIndex(ctx);
|
|
433
|
+
const issues = [];
|
|
434
|
+
const usedStagedDeclarations = new Set();
|
|
435
|
+
eachForwardImportSite(ctx, newFileIndex, (entry, sitePath, specifier, cleaned, laterOwners) => {
|
|
436
|
+
const stagedDependency = findMatchingStagedDependency(entry, sitePath, specifier, laterOwners);
|
|
437
|
+
if (stagedDependency) {
|
|
438
|
+
usedStagedDeclarations.add(stagedDependencyKey(entry, stagedDependency));
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
const ownersSummary = laterOwners
|
|
442
|
+
.map((o) => `${o.filename} (creates ${o.fullPath})`)
|
|
443
|
+
.join(', ');
|
|
444
|
+
const fingerprintOwners = [...laterOwners]
|
|
445
|
+
.map((o) => `${o.filename}:${o.fullPath}`)
|
|
446
|
+
.sort((a, b) => a.localeCompare(b))
|
|
447
|
+
.join(',');
|
|
448
|
+
// Lowest ordinal that lands AFTER every later-ordered creator —
|
|
449
|
+
// turns the operator's "guess and re-run" loop into a single shot
|
|
450
|
+
// when the only fix is reordering.
|
|
451
|
+
const suggestedOrder = Math.max(...laterOwners.map((o) => o.order)) + 1;
|
|
452
|
+
issues.push({
|
|
453
|
+
file: sitePath,
|
|
454
|
+
check: 'forward-import',
|
|
455
|
+
fingerprint: `forward-import|${sitePath}|${cleaned}|${fingerprintOwners}`,
|
|
456
|
+
message: `${sitePath} in ${entry.filename} imports "${specifier}", ` +
|
|
457
|
+
`but the matching new file is created by a later patch: ${ownersSummary}. ` +
|
|
458
|
+
'Reorder the patches so the dependency is created first, move the import ' +
|
|
459
|
+
'into the later patch, declare the intentional staged dependency with ' +
|
|
460
|
+
'"fireforge patch staged-dependency --add", or mark the import with ' +
|
|
461
|
+
`"// ${FORWARD_IMPORT_IGNORE_MARKER}" if the basename collision is a false positive. ` +
|
|
462
|
+
`Closest legal ordinal that satisfies this dependency: ${suggestedOrder}.`,
|
|
463
|
+
severity: 'error',
|
|
464
|
+
});
|
|
465
|
+
});
|
|
446
466
|
for (const entry of ctx.entries) {
|
|
447
467
|
for (const dependency of entry.metadata?.stagedDependencies?.forwardImports ?? []) {
|
|
448
468
|
if (usedStagedDeclarations.has(stagedDependencyKey(entry, dependency)))
|
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
* Public re-exports for {@link ./patch-lint.ts}. Split out so the
|
|
3
3
|
* orchestrator stays within the ESLint `max-lines` budget.
|
|
4
4
|
*/
|
|
5
|
-
export { buildPatchQueueContext, collectNewFileCreatorsByPath, extractImportSpecifiers, extractImportSpecifiersWithLines, findForwardImportIgnoreLines, FORWARD_IMPORT_IGNORE_MARKER, isForwardImportableFile, lintPatchQueue, lintPatchQueueDuplicateCreations, lintPatchQueueForwardImports, type PatchQueueContext, type PatchQueueEntry, } from './patch-lint-cross.js';
|
|
5
|
+
export { buildPatchQueueContext, collectForwardImportEdges, collectNewFileCreatorsByPath, extractImportSpecifiers, extractImportSpecifiersWithLines, findForwardImportIgnoreLines, FORWARD_IMPORT_IGNORE_MARKER, type ForwardImportEdge, isForwardImportableFile, lintPatchQueue, lintPatchQueueDuplicateCreations, lintPatchQueueForwardImports, type PatchQueueContext, type PatchQueueEntry, } from './patch-lint-cross.js';
|
|
6
6
|
export { buildModifiedFileAdditionsFromDiff, detectNewFilesInDiff } from './patch-lint-diff.js';
|
|
@@ -3,6 +3,6 @@
|
|
|
3
3
|
* Public re-exports for {@link ./patch-lint.ts}. Split out so the
|
|
4
4
|
* orchestrator stays within the ESLint `max-lines` budget.
|
|
5
5
|
*/
|
|
6
|
-
export { buildPatchQueueContext, collectNewFileCreatorsByPath, extractImportSpecifiers, extractImportSpecifiersWithLines, findForwardImportIgnoreLines, FORWARD_IMPORT_IGNORE_MARKER, isForwardImportableFile, lintPatchQueue, lintPatchQueueDuplicateCreations, lintPatchQueueForwardImports, } from './patch-lint-cross.js';
|
|
6
|
+
export { buildPatchQueueContext, collectForwardImportEdges, collectNewFileCreatorsByPath, extractImportSpecifiers, extractImportSpecifiersWithLines, findForwardImportIgnoreLines, FORWARD_IMPORT_IGNORE_MARKER, isForwardImportableFile, lintPatchQueue, lintPatchQueueDuplicateCreations, lintPatchQueueForwardImports, } from './patch-lint-cross.js';
|
|
7
7
|
export { buildModifiedFileAdditionsFromDiff, detectNewFilesInDiff } from './patch-lint-diff.js';
|
|
8
8
|
//# sourceMappingURL=patch-lint-reexports.js.map
|
|
@@ -8,4 +8,4 @@ import type { RegisterResult } from './register-result.js';
|
|
|
8
8
|
* Entry format:
|
|
9
9
|
* "{name}.sys.mjs",
|
|
10
10
|
*/
|
|
11
|
-
export declare function registerFireForgeModule(engineDir: string, fileName: string, moduleDir: string, dryRun?: boolean): Promise<RegisterResult>;
|
|
11
|
+
export declare function registerFireForgeModule(engineDir: string, fileName: string, moduleDir: string, dryRun?: boolean, createManifest?: boolean): Promise<RegisterResult>;
|
|
@@ -8,6 +8,7 @@ import { pathExists, readText, writeText } from '../utils/fs.js';
|
|
|
8
8
|
import { findAlphabeticalMozBuildPosition, findAlphabeticalPosition } from './manifest-helpers.js';
|
|
9
9
|
import { tokenizeMozBuildList } from './manifest-tokenizers.js';
|
|
10
10
|
import { withParserFallback } from './parser-fallback.js';
|
|
11
|
+
import { scaffoldModuleMozBuild } from './register-scaffold.js';
|
|
11
12
|
/**
|
|
12
13
|
* Tokenizer-based implementation for module registration.
|
|
13
14
|
*/
|
|
@@ -55,11 +56,23 @@ function legacyRegisterFireForgeModule(content, fileName, entry, moduleDir) {
|
|
|
55
56
|
* Entry format:
|
|
56
57
|
* "{name}.sys.mjs",
|
|
57
58
|
*/
|
|
58
|
-
export async function registerFireForgeModule(engineDir, fileName, moduleDir, dryRun = false) {
|
|
59
|
+
export async function registerFireForgeModule(engineDir, fileName, moduleDir, dryRun = false, createManifest = false) {
|
|
59
60
|
const manifest = `${moduleDir}/moz.build`;
|
|
60
61
|
const manifestPath = join(engineDir, manifest);
|
|
61
62
|
if (!(await pathExists(manifestPath))) {
|
|
62
|
-
|
|
63
|
+
if (createManifest) {
|
|
64
|
+
// 0.34.0 field report: registering a module under a directory with
|
|
65
|
+
// no moz.build failed with "Manifest not found". Scaffold the
|
|
66
|
+
// directory manifest and wire the parent DIRS chain.
|
|
67
|
+
const scaffoldActions = await scaffoldModuleMozBuild(engineDir, moduleDir, fileName, dryRun);
|
|
68
|
+
return {
|
|
69
|
+
manifest,
|
|
70
|
+
entry: ` "${fileName}",`,
|
|
71
|
+
skipped: false,
|
|
72
|
+
scaffoldActions,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
throw new GeneralError(`Manifest not found: ${manifest}. Pass --create-manifest to scaffold it and wire the parent DIRS entry.`);
|
|
63
76
|
}
|
|
64
77
|
const entry = ` "${fileName}",`.replace(/\\/g, '/');
|
|
65
78
|
const content = await readText(manifestPath);
|
|
@@ -18,4 +18,13 @@ export interface RegisterResult {
|
|
|
18
18
|
skipped: boolean;
|
|
19
19
|
/** Whether --after target was not found and fell back to alphabetical */
|
|
20
20
|
afterFallback?: boolean | undefined;
|
|
21
|
+
/**
|
|
22
|
+
* Manifests created or wired by `--create-manifest` scaffolding
|
|
23
|
+
* (directory moz.build, parent DIRS chain, XPCSHELL_TESTS_MANIFESTS),
|
|
24
|
+
* for user display.
|
|
25
|
+
*/
|
|
26
|
+
scaffoldActions?: {
|
|
27
|
+
manifest: string;
|
|
28
|
+
change: string;
|
|
29
|
+
}[] | undefined;
|
|
21
30
|
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manifest scaffolding for `fireforge register --create-manifest`
|
|
3
|
+
* (0.34.0 field report): registering a module under a directory with no
|
|
4
|
+
* moz.build used to fail with "Manifest not found" because `register`
|
|
5
|
+
* only inserts into EXISTING manifests. This module owns the two
|
|
6
|
+
* scaffolding primitives:
|
|
7
|
+
*
|
|
8
|
+
* - creating a directory `moz.build` (license header + the requested
|
|
9
|
+
* list directive) and wiring the parent chain's `DIRS` entries up to
|
|
10
|
+
* the nearest existing moz.build;
|
|
11
|
+
* - wiring an `XPCSHELL_TESTS_MANIFESTS` entry for a (possibly freshly
|
|
12
|
+
* created) `xpcshell.toml` into the nearest existing moz.build.
|
|
13
|
+
*
|
|
14
|
+
* All writers reuse the tokenizer + alphabetical-position helpers the
|
|
15
|
+
* existing register writers use, so inserted entries respect mozbuild's
|
|
16
|
+
* case-insensitive StrictOrderingOnAppendList rule.
|
|
17
|
+
*/
|
|
18
|
+
/** One manifest mutation performed (or planned, on dry-run) by a scaffold. */
|
|
19
|
+
export interface ScaffoldAction {
|
|
20
|
+
/** Engine-relative manifest path that was created or modified. */
|
|
21
|
+
manifest: string;
|
|
22
|
+
/** Human-readable description of the change. */
|
|
23
|
+
change: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Inserts `value` into the named moz.build list directive, creating the
|
|
27
|
+
* `NAME += [ ... ]` block at the end of the file when the directive does
|
|
28
|
+
* not exist yet. Returns the updated content, or null when the value is
|
|
29
|
+
* already present.
|
|
30
|
+
*/
|
|
31
|
+
export declare function upsertMozBuildListEntry(content: string, directive: string, value: string): string | null;
|
|
32
|
+
/**
|
|
33
|
+
* Ensures every parent directory between `childRelDir` and the nearest
|
|
34
|
+
* existing moz.build carries a `DIRS` entry pointing at the next segment
|
|
35
|
+
* down, creating intermediate moz.build files as needed. Walks upward
|
|
36
|
+
* from the child's parent; throws when no moz.build exists anywhere up
|
|
37
|
+
* to the engine root (an engine checkout always has one at the root, so
|
|
38
|
+
* this indicates a bogus path).
|
|
39
|
+
*/
|
|
40
|
+
export declare function ensureParentDirsWiring(engineDir: string, childRelDir: string, dryRun: boolean): Promise<ScaffoldAction[]>;
|
|
41
|
+
/**
|
|
42
|
+
* Creates `<moduleDir>/moz.build` with an `EXTRA_JS_MODULES.<namespace>`
|
|
43
|
+
* list containing `fileName`, and wires the parent DIRS chain. The
|
|
44
|
+
* namespace is the module directory's basename (the fork's binary name
|
|
45
|
+
* for `browser/modules/<binaryName>/`), matching Firefox's convention of
|
|
46
|
+
* mapping `EXTRA_JS_MODULES.<ns>` into `resource:///modules/<ns>/`.
|
|
47
|
+
*/
|
|
48
|
+
export declare function scaffoldModuleMozBuild(engineDir: string, moduleDir: string, fileName: string, dryRun: boolean): Promise<ScaffoldAction[]>;
|
|
49
|
+
/**
|
|
50
|
+
* Wires an `XPCSHELL_TESTS_MANIFESTS` entry for `manifestRelPath` (an
|
|
51
|
+
* engine-relative `.../xpcshell.toml`) into the nearest EXISTING
|
|
52
|
+
* moz.build above it. Returns the performed actions (empty when the
|
|
53
|
+
* entry already exists).
|
|
54
|
+
*/
|
|
55
|
+
export declare function ensureXpcshellManifestWiring(engineDir: string, manifestRelPath: string, dryRun: boolean): Promise<ScaffoldAction[]>;
|