@hominis/fireforge 0.27.0 → 0.27.2
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 +11 -1
- package/README.md +6 -6
- package/dist/src/cli.js +5 -1
- package/dist/src/commands/build.js +61 -1
- package/dist/src/commands/doctor/post-rebase-audit.d.ts +2 -0
- package/dist/src/commands/doctor/post-rebase-audit.js +86 -0
- package/dist/src/commands/doctor-working-tree.js +5 -1
- package/dist/src/commands/doctor.js +3 -0
- package/dist/src/commands/download.js +41 -42
- package/dist/src/commands/export-all.js +3 -2
- package/dist/src/commands/export-flow.d.ts +2 -0
- package/dist/src/commands/export-flow.js +2 -0
- package/dist/src/commands/export.js +5 -4
- package/dist/src/commands/import.js +2 -1
- package/dist/src/commands/manifest.js +2 -0
- package/dist/src/commands/re-export.js +10 -8
- package/dist/src/commands/rebase/conflict-summary.d.ts +12 -0
- package/dist/src/commands/rebase/conflict-summary.js +38 -0
- package/dist/src/commands/rebase/continue.js +2 -0
- package/dist/src/commands/rebase/index.d.ts +2 -2
- package/dist/src/commands/rebase/index.js +9 -4
- package/dist/src/commands/rebase/patch-loop.js +29 -11
- package/dist/src/commands/rebase/summary.js +7 -2
- package/dist/src/commands/resolve.js +2 -1
- package/dist/src/commands/setup-support.js +6 -2
- package/dist/src/commands/setup.js +1 -0
- package/dist/src/commands/source.d.ts +9 -0
- package/dist/src/commands/source.js +92 -0
- package/dist/src/commands/status-output.d.ts +13 -0
- package/dist/src/commands/status-output.js +186 -0
- package/dist/src/commands/status.js +4 -247
- package/dist/src/commands/verify.js +32 -16
- package/dist/src/core/build-prepare.js +12 -4
- package/dist/src/core/config-validate.js +1 -1
- package/dist/src/core/firefox-cache.d.ts +1 -1
- package/dist/src/core/firefox-cache.js +10 -3
- package/dist/src/core/firefox-extract.d.ts +1 -1
- package/dist/src/core/firefox-extract.js +13 -1
- package/dist/src/core/firefox.d.ts +2 -1
- package/dist/src/core/firefox.js +3 -3
- package/dist/src/core/furnace-registration-validate.d.ts +7 -0
- package/dist/src/core/furnace-registration-validate.js +29 -12
- package/dist/src/core/furnace-validate-registration.js +5 -37
- package/dist/src/core/git.js +25 -5
- package/dist/src/core/ownership-table.d.ts +3 -1
- package/dist/src/core/ownership-table.js +31 -7
- package/dist/src/core/patch-artifact-normalize.d.ts +9 -0
- package/dist/src/core/patch-artifact-normalize.js +13 -0
- package/dist/src/core/patch-export-update.js +2 -1
- package/dist/src/core/patch-export.d.ts +4 -0
- package/dist/src/core/patch-export.js +7 -2
- package/dist/src/core/patch-manifest-consistency.d.ts +1 -1
- package/dist/src/core/patch-manifest-consistency.js +4 -2
- package/dist/src/core/patch-manifest-query.d.ts +4 -3
- package/dist/src/core/patch-manifest-query.js +12 -4
- package/dist/src/core/patch-manifest-validate.js +22 -4
- package/dist/src/core/patch-source-metadata.d.ts +8 -0
- package/dist/src/core/patch-source-metadata.js +17 -0
- package/dist/src/core/rebase-session.d.ts +8 -3
- package/dist/src/core/rebase-session.js +1 -1
- package/dist/src/core/status-classify.d.ts +4 -1
- package/dist/src/core/status-classify.js +4 -5
- package/dist/src/types/commands/index.d.ts +1 -1
- package/dist/src/types/commands/options.d.ts +16 -1
- package/dist/src/types/commands/patches.d.ts +9 -1
- package/dist/src/types/config.d.ts +1 -1
- package/dist/src/utils/elapsed.d.ts +4 -0
- package/dist/src/utils/elapsed.js +15 -0
- package/dist/src/utils/validation.d.ts +2 -2
- package/dist/src/utils/validation.js +5 -5
- package/package.json +1 -1
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Used after both AST and legacy code paths to avoid duplicating logic.
|
|
5
5
|
*/
|
|
6
6
|
import { FurnaceError } from '../errors/furnace.js';
|
|
7
|
+
import { stripJsComments } from '../utils/regex.js';
|
|
7
8
|
/**
|
|
8
9
|
* Regex for valid custom element tag names. A valid name is lowercase, starts
|
|
9
10
|
* with a letter, and contains one or more hyphen-separated groups where each
|
|
@@ -36,25 +37,41 @@ export function validateTagName(tagName) {
|
|
|
36
37
|
* @param isESModule - Whether the module uses ESM (Pattern B) or not (Pattern A)
|
|
37
38
|
*/
|
|
38
39
|
export function validateRegistrationPlacement(result, tagName, isESModule) {
|
|
39
|
-
const dclPattern = /document\.addEventListener\(\s*["']DOMContentLoaded["']/;
|
|
40
40
|
const insertedPos = result.lastIndexOf(`"${tagName}"`);
|
|
41
41
|
if (insertedPos === -1)
|
|
42
42
|
return;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
if (!isTagInCorrectCustomElementsPlacement(result, tagName, isESModule)) {
|
|
44
|
+
if (!isESModule) {
|
|
45
|
+
throw new FurnaceError(`${tagName} was registered in the DOMContentLoaded/importESModule block (Pattern B) instead of the loadSubScript block (Pattern A). This will cause the component to fail at runtime. The customElements.js file structure may have changed upstream — manual intervention required.`, tagName);
|
|
46
|
+
}
|
|
46
47
|
throw new FurnaceError(`${tagName} was registered in the loadSubScript block (Pattern A) instead of the DOMContentLoaded/importESModule block (Pattern B). This will cause the component to fail at runtime. The customElements.js file structure may have changed upstream — manual intervention required.`, tagName);
|
|
47
48
|
}
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Returns whether a tag appears in the correct customElements.js placement.
|
|
52
|
+
* ESM entries may either appear textually inside/after DOMContentLoaded or
|
|
53
|
+
* inside an array declared before DOMContentLoaded and consumed by a for-of
|
|
54
|
+
* loop inside the listener, as Firefox 152 Beta does for acornElements.
|
|
55
|
+
*/
|
|
56
|
+
export function isTagInCorrectCustomElementsPlacement(content, tagName, isESModule) {
|
|
57
|
+
const stripped = stripJsComments(content);
|
|
58
|
+
const tagPattern = new RegExp(`["']${escapeRegex(tagName)}["']`);
|
|
59
|
+
const dclMatch = /document\.addEventListener\(\s*["']DOMContentLoaded["']/.exec(stripped);
|
|
60
|
+
if (!dclMatch) {
|
|
61
|
+
return !isESModule && tagPattern.test(stripped);
|
|
62
|
+
}
|
|
63
|
+
const beforeDcl = stripped.slice(0, dclMatch.index);
|
|
64
|
+
const afterDcl = stripped.slice(dclMatch.index);
|
|
65
|
+
const tagBeforeDcl = tagPattern.test(beforeDcl);
|
|
66
|
+
const tagAfterDcl = tagPattern.test(afterDcl);
|
|
67
|
+
if (!isESModule) {
|
|
68
|
+
return tagBeforeDcl && !tagAfterDcl;
|
|
50
69
|
}
|
|
70
|
+
return (tagAfterDcl || isTagInArrayConsumedInsideDOMContentLoaded(stripped, dclMatch.index, tagName));
|
|
51
71
|
}
|
|
52
|
-
function isTagInArrayConsumedInsideDOMContentLoaded(content, tagName) {
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
return false;
|
|
56
|
-
const beforeDcl = content.slice(0, dclMatch.index);
|
|
57
|
-
const afterDcl = content.slice(dclMatch.index);
|
|
72
|
+
function isTagInArrayConsumedInsideDOMContentLoaded(content, domContentLoadedIdx, tagName) {
|
|
73
|
+
const beforeDcl = content.slice(0, domContentLoadedIdx);
|
|
74
|
+
const afterDcl = content.slice(domContentLoadedIdx);
|
|
58
75
|
const consumedArrays = new Set();
|
|
59
76
|
const forOfPattern = /for\s*\(\s*(?:let|const|var)\s*\[[^)]*\]\s+of\s+([A-Za-z_$][\w$]*)\s*\)/g;
|
|
60
77
|
let match;
|
|
@@ -9,6 +9,7 @@ import { stripJsComments } from '../utils/regex.js';
|
|
|
9
9
|
import { getProjectPaths, loadConfig } from './config.js';
|
|
10
10
|
import { getFurnacePaths } from './furnace-config.js';
|
|
11
11
|
import { CUSTOM_ELEMENTS_JS, FTL_DIR, JAR_MN } from './furnace-constants.js';
|
|
12
|
+
import { isTagInCorrectCustomElementsPlacement } from './furnace-registration-validate.js';
|
|
12
13
|
import { getTokensCssPath } from './token-manager.js';
|
|
13
14
|
/**
|
|
14
15
|
* Validates that all Furnace-managed .mjs components are registered in the
|
|
@@ -27,21 +28,13 @@ export async function validateRegistrationPatterns(root, config) {
|
|
|
27
28
|
return issues;
|
|
28
29
|
}
|
|
29
30
|
const content = await readText(filePath);
|
|
30
|
-
// Find the DOMContentLoaded block boundary (handles multi-line addEventListener)
|
|
31
|
-
const dclMatch = /document\.addEventListener\(\s*["']DOMContentLoaded["']/.exec(content);
|
|
32
|
-
if (!dclMatch) {
|
|
33
|
-
return issues;
|
|
34
|
-
}
|
|
35
|
-
const domContentLoadedIdx = dclMatch.index;
|
|
36
31
|
// Get all custom component tag names that use .mjs (all custom components do)
|
|
37
32
|
for (const [name, customConfig] of Object.entries(config.custom)) {
|
|
38
33
|
if (!customConfig.register)
|
|
39
34
|
continue;
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
if (tagPattern.test(contentBeforeDCL) &&
|
|
44
|
-
!isArrayDeclaredBeforeDclAndConsumedInsideDcl(content, domContentLoadedIdx, name)) {
|
|
35
|
+
const stripped = stripJsComments(content);
|
|
36
|
+
const tagPattern = new RegExp(`["']${escapeRegex(name)}["']`);
|
|
37
|
+
if (tagPattern.test(stripped) && !isTagInCorrectCustomElementsPlacement(content, name, true)) {
|
|
45
38
|
issues.push({
|
|
46
39
|
component: name,
|
|
47
40
|
severity: 'error',
|
|
@@ -52,25 +45,6 @@ export async function validateRegistrationPatterns(root, config) {
|
|
|
52
45
|
}
|
|
53
46
|
return issues;
|
|
54
47
|
}
|
|
55
|
-
function isArrayDeclaredBeforeDclAndConsumedInsideDcl(content, domContentLoadedIdx, tagName) {
|
|
56
|
-
const contentBeforeDCL = stripJsComments(content.slice(0, domContentLoadedIdx));
|
|
57
|
-
const contentAfterDCL = stripJsComments(content.slice(domContentLoadedIdx));
|
|
58
|
-
const consumedArrays = new Set();
|
|
59
|
-
const forOfPattern = /for\s*\(\s*(?:let|const|var)\s*\[[^)]*\]\s+of\s+([A-Za-z_$][\w$]*)\s*\)/g;
|
|
60
|
-
let match;
|
|
61
|
-
while ((match = forOfPattern.exec(contentAfterDCL)) !== null) {
|
|
62
|
-
if (match[1])
|
|
63
|
-
consumedArrays.add(match[1]);
|
|
64
|
-
}
|
|
65
|
-
for (const arrayName of consumedArrays) {
|
|
66
|
-
const declarationPattern = new RegExp(`(?:const|let|var)\\s+${escapeRegex(arrayName)}\\s*=\\s*\\[([\\s\\S]*?)\\];`);
|
|
67
|
-
const declaration = declarationPattern.exec(contentBeforeDCL);
|
|
68
|
-
if (declaration?.[1] && new RegExp(`["']${escapeRegex(tagName)}["']`).test(declaration[1])) {
|
|
69
|
-
return true;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
return false;
|
|
73
|
-
}
|
|
74
48
|
function escapeRegex(value) {
|
|
75
49
|
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
76
50
|
}
|
|
@@ -173,13 +147,7 @@ export async function checkRegistrationConsistency(root, name, config, ftlDir) {
|
|
|
173
147
|
status.customElementsPresent =
|
|
174
148
|
ceContent.includes(`"${name}"`) || ceContent.includes(`'${name}'`);
|
|
175
149
|
if (status.customElementsPresent) {
|
|
176
|
-
|
|
177
|
-
const dclMatch = /document\.addEventListener\(\s*["']DOMContentLoaded["']/.exec(ceContent);
|
|
178
|
-
if (dclMatch) {
|
|
179
|
-
const afterDcl = ceContent.slice(dclMatch.index);
|
|
180
|
-
status.customElementsCorrectBlock =
|
|
181
|
-
afterDcl.includes(`"${name}"`) || afterDcl.includes(`'${name}'`);
|
|
182
|
-
}
|
|
150
|
+
status.customElementsCorrectBlock = isTagInCorrectCustomElementsPlacement(ceContent, name, true);
|
|
183
151
|
}
|
|
184
152
|
}
|
|
185
153
|
return status;
|
package/dist/src/core/git.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { readdir, stat } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { GitError, GitIndexingTimeoutError, GitIndexLockError, PatchApplyError, } from '../errors/git.js';
|
|
5
|
+
import { elapsedSince } from '../utils/elapsed.js';
|
|
5
6
|
import { toError } from '../utils/errors.js';
|
|
6
7
|
import { pathExists, removeFile } from '../utils/fs.js';
|
|
7
8
|
import { verbose } from '../utils/logger.js';
|
|
@@ -158,6 +159,7 @@ async function stageAllFilesChunked(dir, scan, options = {}) {
|
|
|
158
159
|
* SIGINT'd mid-way assuming the process had stalled.
|
|
159
160
|
*/
|
|
160
161
|
const GIT_ADD_HEARTBEAT_MS = 15_000;
|
|
162
|
+
const GIT_COMMIT_HEARTBEAT_MS = 15_000;
|
|
161
163
|
async function scanTopLevelSource(dir) {
|
|
162
164
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
163
165
|
return {
|
|
@@ -204,9 +206,10 @@ export async function stageAllFiles(dir, options = {}) {
|
|
|
204
206
|
heartbeatTimer?.unref();
|
|
205
207
|
try {
|
|
206
208
|
try {
|
|
209
|
+
reportProgress?.('Git phase: starting git add -A source indexing.');
|
|
207
210
|
reportProgress?.(`Starting monolithic git add -A for ${scan.directories.length} director${scan.directories.length === 1 ? 'y' : 'ies'} and ${scan.topLevelFiles.length} top-level file${scan.topLevelFiles.length === 1 ? '' : 's'}...`);
|
|
208
211
|
await git(['add', '-A'], dir, { timeout, env: GIT_ADD_ENV });
|
|
209
|
-
reportProgress?.('
|
|
212
|
+
reportProgress?.('Git phase complete: git add -A source indexing finished.');
|
|
210
213
|
return;
|
|
211
214
|
}
|
|
212
215
|
catch (error) {
|
|
@@ -231,6 +234,7 @@ export async function stageAllFiles(dir, options = {}) {
|
|
|
231
234
|
phaseStartedAt = Date.now();
|
|
232
235
|
try {
|
|
233
236
|
await stageAllFilesChunked(dir, scan, options);
|
|
237
|
+
reportProgress?.('Git phase complete: chunked source indexing finished.');
|
|
234
238
|
}
|
|
235
239
|
catch (error) {
|
|
236
240
|
if (error instanceof GitIndexingTimeoutError)
|
|
@@ -243,6 +247,22 @@ export async function stageAllFiles(dir, options = {}) {
|
|
|
243
247
|
clearInterval(heartbeatTimer);
|
|
244
248
|
}
|
|
245
249
|
}
|
|
250
|
+
async function createInitialSourceCommit(dir, reportProgress) {
|
|
251
|
+
const startedAt = Date.now();
|
|
252
|
+
reportProgress('Git phase: creating initial source commit.');
|
|
253
|
+
reportProgress(`Creating initial Firefox source commit (${elapsedSince(startedAt)} elapsed)...`);
|
|
254
|
+
const heartbeat = setInterval(() => {
|
|
255
|
+
reportProgress(`Creating initial Firefox source commit (${elapsedSince(startedAt)} elapsed)...`);
|
|
256
|
+
}, GIT_COMMIT_HEARTBEAT_MS);
|
|
257
|
+
heartbeat.unref();
|
|
258
|
+
try {
|
|
259
|
+
await git(['commit', '-m', 'Initial Firefox source'], dir);
|
|
260
|
+
}
|
|
261
|
+
finally {
|
|
262
|
+
clearInterval(heartbeat);
|
|
263
|
+
}
|
|
264
|
+
reportProgress(`Git phase complete: initial source commit created (${elapsedSince(startedAt)} elapsed).`);
|
|
265
|
+
}
|
|
246
266
|
/**
|
|
247
267
|
* Initializes a new git repository with an orphan branch.
|
|
248
268
|
* @param dir - Directory to initialize
|
|
@@ -252,6 +272,7 @@ export async function initRepository(dir, branchName = 'main', options = {}) {
|
|
|
252
272
|
await ensureGit();
|
|
253
273
|
const reportProgress = options.onProgress ?? (() => { });
|
|
254
274
|
// Initialize repository
|
|
275
|
+
reportProgress('Git phase: initializing source git repository.');
|
|
255
276
|
reportProgress('Creating git repository...');
|
|
256
277
|
await git(['init'], dir);
|
|
257
278
|
// Create orphan branch
|
|
@@ -269,6 +290,7 @@ export async function initRepository(dir, branchName = 'main', options = {}) {
|
|
|
269
290
|
// fail. Nothing is ever fetched from or pushed to this remote.
|
|
270
291
|
reportProgress('Configuring origin remote for build compatibility...');
|
|
271
292
|
await git(['remote', 'add', 'origin', 'https://github.com/mozilla-firefox/firefox'], dir);
|
|
293
|
+
reportProgress('Git phase complete: source git repository metadata initialized.');
|
|
272
294
|
// Add all files
|
|
273
295
|
reportProgress('Indexing Firefox source with git add -A (this can take several minutes on large trees)...');
|
|
274
296
|
await assertNoGitIndexLock(dir);
|
|
@@ -279,9 +301,8 @@ export async function initRepository(dir, branchName = 'main', options = {}) {
|
|
|
279
301
|
throw await maybeWrapIndexLockError(dir, error);
|
|
280
302
|
}
|
|
281
303
|
// Create initial commit
|
|
282
|
-
reportProgress('Creating initial Firefox source commit...');
|
|
283
304
|
try {
|
|
284
|
-
await
|
|
305
|
+
await createInitialSourceCommit(dir, reportProgress);
|
|
285
306
|
}
|
|
286
307
|
catch (error) {
|
|
287
308
|
throw await maybeWrapIndexLockError(dir, error);
|
|
@@ -316,9 +337,8 @@ export async function resumeRepository(dir, options = {}) {
|
|
|
316
337
|
throw await maybeWrapIndexLockError(dir, error);
|
|
317
338
|
}
|
|
318
339
|
// Create initial commit
|
|
319
|
-
reportProgress('Creating initial Firefox source commit...');
|
|
320
340
|
try {
|
|
321
|
-
await
|
|
341
|
+
await createInitialSourceCommit(dir, reportProgress);
|
|
322
342
|
}
|
|
323
343
|
catch (error) {
|
|
324
344
|
throw await maybeWrapIndexLockError(dir, error);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { PatchMetadata } from '../types/commands/index.js';
|
|
2
|
+
import type { FileClassification } from './status-classify.js';
|
|
2
3
|
/**
|
|
3
4
|
* A row in the flat path → owning-patch ownership table.
|
|
4
5
|
*/
|
|
@@ -8,6 +9,7 @@ export interface OwnershipRow {
|
|
|
8
9
|
conflict: boolean;
|
|
9
10
|
conflictReason: 'files-affected' | 'duplicate-create' | null;
|
|
10
11
|
unmanaged: boolean;
|
|
12
|
+
state: 'owned' | 'patch-backed' | 'patch-owned-drift' | 'unmanaged' | 'conflict';
|
|
11
13
|
}
|
|
12
14
|
interface StatusFile {
|
|
13
15
|
status: string;
|
|
@@ -42,7 +44,7 @@ interface StatusFile {
|
|
|
42
44
|
* {@link import('../core/patch-lint.js').collectNewFileCreatorsByPath};
|
|
43
45
|
* paths with a `.length > 1` owner list become duplicate-create conflicts
|
|
44
46
|
*/
|
|
45
|
-
export declare function buildOwnershipTable(manifestPatches: PatchMetadata[], worktreeFiles: StatusFile[], newFileCreatorsByPath: Map<string, string[]>): OwnershipRow[];
|
|
47
|
+
export declare function buildOwnershipTable(manifestPatches: PatchMetadata[], worktreeFiles: StatusFile[], newFileCreatorsByPath: Map<string, string[]>, classifications?: Map<string, FileClassification>): OwnershipRow[];
|
|
46
48
|
/**
|
|
47
49
|
* Renders the ownership table as a GitHub-flavored Markdown pipe table.
|
|
48
50
|
* Using markdown-table's own serializer would require a seed document to
|
|
@@ -28,7 +28,7 @@ import { info } from '../utils/logger.js';
|
|
|
28
28
|
* {@link import('../core/patch-lint.js').collectNewFileCreatorsByPath};
|
|
29
29
|
* paths with a `.length > 1` owner list become duplicate-create conflicts
|
|
30
30
|
*/
|
|
31
|
-
export function buildOwnershipTable(manifestPatches, worktreeFiles, newFileCreatorsByPath) {
|
|
31
|
+
export function buildOwnershipTable(manifestPatches, worktreeFiles, newFileCreatorsByPath, classifications = new Map()) {
|
|
32
32
|
const ownersByPath = new Map();
|
|
33
33
|
for (const patch of manifestPatches) {
|
|
34
34
|
for (const file of patch.filesAffected) {
|
|
@@ -86,6 +86,13 @@ export function buildOwnershipTable(manifestPatches, worktreeFiles, newFileCreat
|
|
|
86
86
|
conflict: isFilesAffectedConflict || isDuplicateCreateConflict,
|
|
87
87
|
conflictReason,
|
|
88
88
|
unmanaged: false,
|
|
89
|
+
state: isFilesAffectedConflict || isDuplicateCreateConflict
|
|
90
|
+
? 'conflict'
|
|
91
|
+
: classifications.get(path) === 'patch-backed'
|
|
92
|
+
? 'patch-backed'
|
|
93
|
+
: classifications.get(path) === 'patch-owned-drift'
|
|
94
|
+
? 'patch-owned-drift'
|
|
95
|
+
: 'owned',
|
|
89
96
|
});
|
|
90
97
|
}
|
|
91
98
|
for (const path of unmanagedOnly) {
|
|
@@ -95,6 +102,7 @@ export function buildOwnershipTable(manifestPatches, worktreeFiles, newFileCreat
|
|
|
95
102
|
conflict: false,
|
|
96
103
|
conflictReason: null,
|
|
97
104
|
unmanaged: true,
|
|
105
|
+
state: 'unmanaged',
|
|
98
106
|
});
|
|
99
107
|
}
|
|
100
108
|
rows.sort((a, b) => a.path.localeCompare(b.path));
|
|
@@ -115,6 +123,22 @@ function renderConflictCell(row) {
|
|
|
115
123
|
return 'CONFLICT (dup-create)';
|
|
116
124
|
return 'CONFLICT';
|
|
117
125
|
}
|
|
126
|
+
function renderStateCell(row) {
|
|
127
|
+
if (row.conflict)
|
|
128
|
+
return renderConflictCell(row);
|
|
129
|
+
switch (row.state) {
|
|
130
|
+
case 'owned':
|
|
131
|
+
return 'owned';
|
|
132
|
+
case 'patch-backed':
|
|
133
|
+
return 'patch-backed';
|
|
134
|
+
case 'patch-owned-drift':
|
|
135
|
+
return 'patch-owned drift';
|
|
136
|
+
case 'unmanaged':
|
|
137
|
+
return 'unmanaged';
|
|
138
|
+
case 'conflict':
|
|
139
|
+
return renderConflictCell(row);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
118
142
|
/**
|
|
119
143
|
* Renders the ownership table as a GitHub-flavored Markdown pipe table.
|
|
120
144
|
* Using markdown-table's own serializer would require a seed document to
|
|
@@ -128,17 +152,17 @@ export function renderOwnershipTable(rows) {
|
|
|
128
152
|
}
|
|
129
153
|
const pathHeader = 'path';
|
|
130
154
|
const ownerHeader = 'owning patch';
|
|
131
|
-
const
|
|
155
|
+
const stateHeader = 'state';
|
|
132
156
|
const pathWidth = Math.max(pathHeader.length, ...rows.map((r) => r.path.length));
|
|
133
157
|
const ownerWidth = Math.max(ownerHeader.length, ...rows.map((r) => (r.unmanaged ? 1 : r.owners.join(', ').length)));
|
|
134
|
-
const
|
|
158
|
+
const stateWidth = Math.max(stateHeader.length, ...rows.map((r) => renderStateCell(r).length), 8);
|
|
135
159
|
const pad = (text, width) => text + ' '.repeat(width - text.length);
|
|
136
|
-
info(`| ${pad(pathHeader, pathWidth)} | ${pad(ownerHeader, ownerWidth)} | ${pad(
|
|
137
|
-
info(`| ${'-'.repeat(pathWidth)} | ${'-'.repeat(ownerWidth)} | ${'-'.repeat(
|
|
160
|
+
info(`| ${pad(pathHeader, pathWidth)} | ${pad(ownerHeader, ownerWidth)} | ${pad(stateHeader, stateWidth)} |`);
|
|
161
|
+
info(`| ${'-'.repeat(pathWidth)} | ${'-'.repeat(ownerWidth)} | ${'-'.repeat(stateWidth)} |`);
|
|
138
162
|
for (const row of rows) {
|
|
139
163
|
const ownerCell = row.unmanaged ? '-' : row.owners.join(', ');
|
|
140
|
-
const
|
|
141
|
-
info(`| ${pad(row.path, pathWidth)} | ${pad(ownerCell, ownerWidth)} | ${pad(
|
|
164
|
+
const stateCell = renderStateCell(row);
|
|
165
|
+
info(`| ${pad(row.path, pathWidth)} | ${pad(ownerCell, ownerWidth)} | ${pad(stateCell, stateWidth)} |`);
|
|
142
166
|
}
|
|
143
167
|
}
|
|
144
168
|
//# sourceMappingURL=ownership-table.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalizes generated patch files for repository whitespace checks.
|
|
3
|
+
*
|
|
4
|
+
* Unified diffs conventionally encode a blank context line as a physical line
|
|
5
|
+
* containing one space. `git apply` also accepts the same hunk as an empty
|
|
6
|
+
* physical line, while repository-level `git diff --check` flags the
|
|
7
|
+
* single-space artifact as trailing whitespace in `patches/*.patch`.
|
|
8
|
+
*/
|
|
9
|
+
export declare function normalizePatchArtifact(content: string): string;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Normalizes generated patch files for repository whitespace checks.
|
|
4
|
+
*
|
|
5
|
+
* Unified diffs conventionally encode a blank context line as a physical line
|
|
6
|
+
* containing one space. `git apply` also accepts the same hunk as an empty
|
|
7
|
+
* physical line, while repository-level `git diff --check` flags the
|
|
8
|
+
* single-space artifact as trailing whitespace in `patches/*.patch`.
|
|
9
|
+
*/
|
|
10
|
+
export function normalizePatchArtifact(content) {
|
|
11
|
+
return content.replace(/^ $/gm, '');
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=patch-artifact-normalize.js.map
|
|
@@ -4,6 +4,7 @@ import { toError } from '../utils/errors.js';
|
|
|
4
4
|
import { pathExists, readText, writeText } from '../utils/fs.js';
|
|
5
5
|
import { warn } from '../utils/logger.js';
|
|
6
6
|
import { withPatchDirectoryLock } from './patch-apply.js';
|
|
7
|
+
import { normalizePatchArtifact } from './patch-artifact-normalize.js';
|
|
7
8
|
import { loadPatchesManifest, savePatchesManifest } from './patch-manifest.js';
|
|
8
9
|
import { buildProjectedManifest, enforcePatchPolicy } from './patch-policy.js';
|
|
9
10
|
/**
|
|
@@ -38,7 +39,7 @@ export async function updatePatchAndMetadata(patchesDir, filename, newContent, u
|
|
|
38
39
|
}
|
|
39
40
|
let patchWritten = false;
|
|
40
41
|
try {
|
|
41
|
-
await writeText(patchPath, newContent);
|
|
42
|
+
await writeText(patchPath, normalizePatchArtifact(newContent));
|
|
42
43
|
patchWritten = true;
|
|
43
44
|
await savePatchesManifest(patchesDir, manifest);
|
|
44
45
|
}
|
|
@@ -36,6 +36,8 @@ export interface CommitExportedPatchInput {
|
|
|
36
36
|
diff: string;
|
|
37
37
|
filesAffected: string[];
|
|
38
38
|
sourceEsrVersion: string;
|
|
39
|
+
sourceProduct?: FireForgeConfig['firefox']['product'];
|
|
40
|
+
sourceVersion?: string;
|
|
39
41
|
/** Optional `PatchMetadata.tier` opt-in (only `"branding"` recognised). */
|
|
40
42
|
tier?: 'branding';
|
|
41
43
|
/** Optional `PatchMetadata.lintIgnore` (empty array treated as absent). */
|
|
@@ -126,6 +128,8 @@ export interface PlanExportInput {
|
|
|
126
128
|
description: string;
|
|
127
129
|
filesAffected: string[];
|
|
128
130
|
sourceEsrVersion: string;
|
|
131
|
+
sourceProduct?: FireForgeConfig['firefox']['product'];
|
|
132
|
+
sourceVersion?: string;
|
|
129
133
|
/**
|
|
130
134
|
* Optional `PatchMetadata.tier` opt-in carried from the CLI flag.
|
|
131
135
|
* Only `"branding"` is currently recognised. When provided the field
|
|
@@ -6,6 +6,7 @@ import { pathExists, readText, removeFile, writeText } from '../utils/fs.js';
|
|
|
6
6
|
import { warn } from '../utils/logger.js';
|
|
7
7
|
import { PATCH_CATEGORIES } from '../utils/validation.js';
|
|
8
8
|
import { discoverPatches, withPatchDirectoryLock } from './patch-apply.js';
|
|
9
|
+
import { normalizePatchArtifact } from './patch-artifact-normalize.js';
|
|
9
10
|
import { findAllPatchesForFilesWithDetails, } from './patch-export-coverage.js';
|
|
10
11
|
import { addPatchToManifest, loadPatchesManifest, PATCHES_MANIFEST, savePatchesManifest, } from './patch-manifest.js';
|
|
11
12
|
import { allocatePolicyOrder, enforcePatchPolicy } from './patch-policy.js';
|
|
@@ -74,6 +75,8 @@ export async function commitExportedPatch(input) {
|
|
|
74
75
|
description: input.description,
|
|
75
76
|
filesAffected: input.filesAffected,
|
|
76
77
|
sourceEsrVersion: input.sourceEsrVersion,
|
|
78
|
+
...(input.sourceProduct !== undefined ? { sourceProduct: input.sourceProduct } : {}),
|
|
79
|
+
...(input.sourceVersion !== undefined ? { sourceVersion: input.sourceVersion } : {}),
|
|
77
80
|
...(input.tier !== undefined ? { tier: input.tier } : {}),
|
|
78
81
|
...(input.lintIgnore !== undefined ? { lintIgnore: input.lintIgnore } : {}),
|
|
79
82
|
...(input.config !== undefined ? { config: input.config } : {}),
|
|
@@ -95,7 +98,7 @@ export async function commitExportedPatch(input) {
|
|
|
95
98
|
}
|
|
96
99
|
}
|
|
97
100
|
try {
|
|
98
|
-
await writeText(patchPath, input.diff);
|
|
101
|
+
await writeText(patchPath, normalizePatchArtifact(input.diff));
|
|
99
102
|
await addPatchToManifest(input.patchesDir, plan.metadata, plan.supersededPatches.map((p) => p.filename));
|
|
100
103
|
for (const oldPatch of plan.supersededPatches) {
|
|
101
104
|
await removeFile(oldPatch.path);
|
|
@@ -195,7 +198,7 @@ export async function findExistingPatchForFile(patchesDir, filePath) {
|
|
|
195
198
|
* @param newContent - New patch content
|
|
196
199
|
*/
|
|
197
200
|
export async function updatePatch(patchPath, newContent) {
|
|
198
|
-
await writeText(patchPath, newContent);
|
|
201
|
+
await writeText(patchPath, normalizePatchArtifact(newContent));
|
|
199
202
|
}
|
|
200
203
|
/**
|
|
201
204
|
* Deletes a patch file and removes it from the manifest.
|
|
@@ -261,6 +264,8 @@ async function computeExportPlanUnderLock(input) {
|
|
|
261
264
|
description: input.description,
|
|
262
265
|
createdAt: new Date().toISOString(),
|
|
263
266
|
sourceEsrVersion: input.sourceEsrVersion,
|
|
267
|
+
...(input.sourceProduct !== undefined ? { sourceProduct: input.sourceProduct } : {}),
|
|
268
|
+
sourceVersion: input.sourceVersion ?? input.sourceEsrVersion,
|
|
264
269
|
filesAffected: input.filesAffected,
|
|
265
270
|
...(input.tier !== undefined ? { tier: input.tier } : {}),
|
|
266
271
|
...(input.lintIgnore !== undefined && input.lintIgnore.length > 0
|
|
@@ -37,7 +37,7 @@ export interface RebuildPatchesManifestResult {
|
|
|
37
37
|
* Existing metadata is preserved when possible; missing entries are recovered
|
|
38
38
|
* from filename structure, patch contents, and file mtimes.
|
|
39
39
|
* @param patchesDir - Path to the patches directory
|
|
40
|
-
* @param fallbackSourceEsrVersion -
|
|
40
|
+
* @param fallbackSourceEsrVersion - source version to use for recovered legacy entries
|
|
41
41
|
* @returns {@link RebuildPatchesManifestResult} — the persisted manifest
|
|
42
42
|
* plus the filenames that were reconstructed from generic defaults.
|
|
43
43
|
*/
|
|
@@ -84,7 +84,7 @@ export async function validatePatchesManifestConsistency(patchesDir) {
|
|
|
84
84
|
* Existing metadata is preserved when possible; missing entries are recovered
|
|
85
85
|
* from filename structure, patch contents, and file mtimes.
|
|
86
86
|
* @param patchesDir - Path to the patches directory
|
|
87
|
-
* @param fallbackSourceEsrVersion -
|
|
87
|
+
* @param fallbackSourceEsrVersion - source version to use for recovered legacy entries
|
|
88
88
|
* @returns {@link RebuildPatchesManifestResult} — the persisted manifest
|
|
89
89
|
* plus the filenames that were reconstructed from generic defaults.
|
|
90
90
|
*/
|
|
@@ -135,9 +135,11 @@ export async function rebuildPatchesManifest(patchesDir, fallbackSourceEsrVersio
|
|
|
135
135
|
category: existing?.category ?? inferred.category,
|
|
136
136
|
name: existing?.name ?? inferred.name,
|
|
137
137
|
description: existing?.description ??
|
|
138
|
-
`Recovered manifest entry for ${patch.filename}. Review description and
|
|
138
|
+
`Recovered manifest entry for ${patch.filename}. Review description and source version.`,
|
|
139
139
|
createdAt: existing?.createdAt ?? new Date(patchStats.mtimeMs).toISOString(),
|
|
140
140
|
sourceEsrVersion: existing?.sourceEsrVersion ?? fallbackSourceEsrVersion,
|
|
141
|
+
sourceVersion: existing?.sourceVersion ?? existing?.sourceEsrVersion ?? fallbackSourceEsrVersion,
|
|
142
|
+
...(existing?.sourceProduct !== undefined ? { sourceProduct: existing.sourceProduct } : {}),
|
|
141
143
|
filesAffected,
|
|
142
144
|
};
|
|
143
145
|
if (existing?.lintIgnore !== undefined)
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Query helpers: finding patches by file, integrity checks, version compat, stamping.
|
|
3
3
|
*/
|
|
4
4
|
import type { PatchesManifest, PatchInfo, PatchMetadata } from '../types/commands/index.js';
|
|
5
|
+
import type { FirefoxProduct } from '../types/config.js';
|
|
5
6
|
/**
|
|
6
7
|
* Gets all file paths claimed by patches other than the excluded one.
|
|
7
8
|
* @param manifest - The patches manifest
|
|
@@ -10,7 +11,7 @@ import type { PatchesManifest, PatchInfo, PatchMetadata } from '../types/command
|
|
|
10
11
|
*/
|
|
11
12
|
export declare function getClaimedFiles(manifest: PatchesManifest, excludeFilename: string): Set<string>;
|
|
12
13
|
/**
|
|
13
|
-
* Checks
|
|
14
|
+
* Checks Firefox source version compatibility.
|
|
14
15
|
* @param patchVersion - Version the patch was created for
|
|
15
16
|
* @param currentVersion - Current project version
|
|
16
17
|
* @returns Warning message if versions differ, null if compatible
|
|
@@ -39,10 +40,10 @@ export declare function validatePatchIntegrity(patchesDir: string, engineDir: st
|
|
|
39
40
|
targetFile: string | null;
|
|
40
41
|
}>>;
|
|
41
42
|
/**
|
|
42
|
-
* Stamps multiple patches with a new
|
|
43
|
+
* Stamps multiple patches with a new source version in a single
|
|
43
44
|
* manifest read-modify-write cycle.
|
|
44
45
|
* @param patchesDir - Path to the patches directory
|
|
45
46
|
* @param filenames - Patch filenames to update
|
|
46
47
|
* @param newVersion - Version string to set (e.g. "140.9.0esr")
|
|
47
48
|
*/
|
|
48
|
-
export declare function stampPatchVersions(patchesDir: string, filenames: string[], newVersion: string): Promise<void>;
|
|
49
|
+
export declare function stampPatchVersions(patchesDir: string, filenames: string[], newVersion: string, newProduct?: FirefoxProduct): Promise<void>;
|
|
@@ -25,7 +25,7 @@ export function getClaimedFiles(manifest, excludeFilename) {
|
|
|
25
25
|
return claimed;
|
|
26
26
|
}
|
|
27
27
|
/**
|
|
28
|
-
* Checks
|
|
28
|
+
* Checks Firefox source version compatibility.
|
|
29
29
|
* @param patchVersion - Version the patch was created for
|
|
30
30
|
* @param currentVersion - Current project version
|
|
31
31
|
* @returns Warning message if versions differ, null if compatible
|
|
@@ -99,21 +99,29 @@ export async function validatePatchIntegrity(patchesDir, engineDir) {
|
|
|
99
99
|
return issues;
|
|
100
100
|
}
|
|
101
101
|
/**
|
|
102
|
-
* Stamps multiple patches with a new
|
|
102
|
+
* Stamps multiple patches with a new source version in a single
|
|
103
103
|
* manifest read-modify-write cycle.
|
|
104
104
|
* @param patchesDir - Path to the patches directory
|
|
105
105
|
* @param filenames - Patch filenames to update
|
|
106
106
|
* @param newVersion - Version string to set (e.g. "140.9.0esr")
|
|
107
107
|
*/
|
|
108
|
-
export async function stampPatchVersions(patchesDir, filenames, newVersion) {
|
|
108
|
+
export async function stampPatchVersions(patchesDir, filenames, newVersion, newProduct) {
|
|
109
109
|
const manifest = await loadPatchesManifest(patchesDir);
|
|
110
110
|
if (!manifest)
|
|
111
111
|
return;
|
|
112
112
|
const filenameSet = new Set(filenames);
|
|
113
113
|
let modified = false;
|
|
114
114
|
for (const patch of manifest.patches) {
|
|
115
|
-
if (filenameSet.has(patch.filename)
|
|
115
|
+
if (!filenameSet.has(patch.filename))
|
|
116
|
+
continue;
|
|
117
|
+
if (patch.sourceEsrVersion !== newVersion ||
|
|
118
|
+
patch.sourceVersion !== newVersion ||
|
|
119
|
+
(newProduct !== undefined && patch.sourceProduct !== newProduct)) {
|
|
116
120
|
patch.sourceEsrVersion = newVersion;
|
|
121
|
+
patch.sourceVersion = newVersion;
|
|
122
|
+
if (newProduct !== undefined) {
|
|
123
|
+
patch.sourceProduct = newProduct;
|
|
124
|
+
}
|
|
117
125
|
modified = true;
|
|
118
126
|
}
|
|
119
127
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Schema validation for patches.json manifest data.
|
|
4
4
|
*/
|
|
5
5
|
import { parseObject } from '../utils/parse.js';
|
|
6
|
-
import { isArray, isObject, isValidFirefoxVersion, PATCH_CATEGORIES } from '../utils/validation.js';
|
|
6
|
+
import { isArray, isObject, isValidFirefoxProduct, isValidFirefoxVersion, PATCH_CATEGORIES, } from '../utils/validation.js';
|
|
7
7
|
function parseForwardImports(data, label) {
|
|
8
8
|
if (!isArray(data)) {
|
|
9
9
|
throw new Error(`${label} must be an array`);
|
|
@@ -45,10 +45,25 @@ export function validatePatchMetadata(data, index) {
|
|
|
45
45
|
const name = rec.string('name');
|
|
46
46
|
const description = rec.string('description');
|
|
47
47
|
const createdAt = rec.string('createdAt');
|
|
48
|
-
const sourceEsrVersion = rec.
|
|
48
|
+
const sourceEsrVersion = rec.optionalString('sourceEsrVersion');
|
|
49
|
+
const sourceVersion = rec.optionalString('sourceVersion') ?? sourceEsrVersion;
|
|
50
|
+
if (sourceVersion === undefined) {
|
|
51
|
+
throw new Error(`patches[${index}] must include sourceVersion or legacy sourceEsrVersion`);
|
|
52
|
+
}
|
|
53
|
+
const sourceProductRaw = rec.optionalString('sourceProduct');
|
|
54
|
+
let sourceProduct;
|
|
55
|
+
if (sourceProductRaw !== undefined) {
|
|
56
|
+
if (!isValidFirefoxProduct(sourceProductRaw)) {
|
|
57
|
+
throw new Error(`patches[${index}].sourceProduct must be one of: firefox, firefox-esr, firefox-beta, firefox-devedition`);
|
|
58
|
+
}
|
|
59
|
+
sourceProduct = sourceProductRaw;
|
|
60
|
+
}
|
|
49
61
|
const order = rec.nonNegativeInteger('order');
|
|
50
62
|
const category = rec.validatedString('category', (value) => /^[a-z][a-z0-9-]*$/.test(value), 'a lowercase category identifier (letters, numbers, hyphens)');
|
|
51
|
-
if (!isValidFirefoxVersion(
|
|
63
|
+
if (!isValidFirefoxVersion(sourceVersion)) {
|
|
64
|
+
throw new Error(`patches[${index}].sourceVersion must be a valid Firefox version string`);
|
|
65
|
+
}
|
|
66
|
+
if (sourceEsrVersion !== undefined && !isValidFirefoxVersion(sourceEsrVersion)) {
|
|
52
67
|
throw new Error(`patches[${index}].sourceEsrVersion must be a valid Firefox version string`);
|
|
53
68
|
}
|
|
54
69
|
const filesAffected = rec.stringArray('filesAffected');
|
|
@@ -78,9 +93,12 @@ export function validatePatchMetadata(data, index) {
|
|
|
78
93
|
name,
|
|
79
94
|
description,
|
|
80
95
|
createdAt,
|
|
81
|
-
sourceEsrVersion,
|
|
96
|
+
sourceEsrVersion: sourceEsrVersion ?? sourceVersion,
|
|
97
|
+
sourceVersion,
|
|
82
98
|
filesAffected,
|
|
83
99
|
};
|
|
100
|
+
if (sourceProduct !== undefined)
|
|
101
|
+
result.sourceProduct = sourceProduct;
|
|
84
102
|
if (lintIgnore !== undefined)
|
|
85
103
|
result.lintIgnore = lintIgnore;
|
|
86
104
|
if (tier !== undefined)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { PatchMetadata } from '../types/commands/index.js';
|
|
2
|
+
import type { FirefoxConfig, FirefoxProduct } from '../types/config.js';
|
|
3
|
+
/** Metadata fields stamped on new or refreshed patch entries. */
|
|
4
|
+
export declare function buildPatchSourceMetadata(firefox: Pick<FirefoxConfig, 'product' | 'version'>): Pick<PatchMetadata, 'sourceEsrVersion' | 'sourceProduct' | 'sourceVersion'>;
|
|
5
|
+
/** Backward-compatible source version reader for legacy manifests. */
|
|
6
|
+
export declare function getPatchSourceVersion(patch: Pick<PatchMetadata, 'sourceEsrVersion' | 'sourceVersion'>): string;
|
|
7
|
+
/** Backward-compatible source product reader for legacy manifests. */
|
|
8
|
+
export declare function getPatchSourceProduct(patch: Pick<PatchMetadata, 'sourceProduct'>): FirefoxProduct | undefined;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Metadata fields stamped on new or refreshed patch entries. */
|
|
2
|
+
export function buildPatchSourceMetadata(firefox) {
|
|
3
|
+
return {
|
|
4
|
+
sourceEsrVersion: firefox.version,
|
|
5
|
+
sourceProduct: firefox.product,
|
|
6
|
+
sourceVersion: firefox.version,
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
/** Backward-compatible source version reader for legacy manifests. */
|
|
10
|
+
export function getPatchSourceVersion(patch) {
|
|
11
|
+
return patch.sourceVersion ?? patch.sourceEsrVersion;
|
|
12
|
+
}
|
|
13
|
+
/** Backward-compatible source product reader for legacy manifests. */
|
|
14
|
+
export function getPatchSourceProduct(patch) {
|
|
15
|
+
return patch.sourceProduct;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=patch-source-metadata.js.map
|