@hominis/fireforge 0.19.5 → 0.20.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 +30 -0
- package/README.md +21 -8
- package/dist/src/commands/config.js +1 -0
- package/dist/src/commands/download.js +188 -185
- package/dist/src/commands/export-flow.js +2 -13
- package/dist/src/commands/furnace/create-validation.d.ts +6 -0
- package/dist/src/commands/furnace/create-validation.js +59 -0
- package/dist/src/commands/furnace/create.d.ts +7 -7
- package/dist/src/commands/furnace/create.js +21 -96
- package/dist/src/commands/furnace/index.js +2 -2
- package/dist/src/commands/furnace/refresh.js +11 -2
- package/dist/src/commands/furnace/remove-state.d.ts +5 -0
- package/dist/src/commands/furnace/remove-state.js +14 -0
- package/dist/src/commands/furnace/remove.js +30 -45
- package/dist/src/commands/furnace/rename-helpers.d.ts +13 -0
- package/dist/src/commands/furnace/rename-helpers.js +42 -0
- package/dist/src/commands/furnace/rename.js +27 -47
- 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 +5 -0
- package/dist/src/core/config.js +11 -7
- package/dist/src/core/file-lock.js +2 -2
- package/dist/src/core/firefox-cache.d.ts +1 -1
- package/dist/src/core/firefox-cache.js +43 -17
- package/dist/src/core/firefox-download.js +12 -4
- package/dist/src/core/firefox.d.ts +1 -1
- package/dist/src/core/firefox.js +2 -2
- package/dist/src/core/furnace-refresh.js +16 -5
- package/dist/src/core/patch-lint-imports.d.ts +5 -0
- package/dist/src/core/patch-lint-imports.js +68 -0
- package/dist/src/core/patch-lint.js +2 -3
- package/dist/src/types/commands/options.d.ts +9 -9
- package/dist/src/types/config.d.ts +2 -0
- package/dist/src/utils/fs.d.ts +5 -0
- package/dist/src/utils/fs.js +54 -1
- package/dist/src/utils/process.js +4 -1
- package/package.json +1 -1
|
@@ -14,52 +14,12 @@ import { FurnaceError } from '../../errors/furnace.js';
|
|
|
14
14
|
import { toError } from '../../utils/errors.js';
|
|
15
15
|
import { copyFile, ensureDir, pathExists, readText, removeDir, removeFile, writeText, } from '../../utils/fs.js';
|
|
16
16
|
import { info, intro, note, outro, warn } from '../../utils/logger.js';
|
|
17
|
+
import { renameComponentFileName, updateConfigForCustomRename, updateConfigForOverrideRename, } from './rename-helpers.js';
|
|
17
18
|
import { renameXpcshellTestFiles } from './rename-xpcshell.js';
|
|
18
19
|
/** Escapes regex metacharacters so a user-supplied name is literal inside a RegExp. */
|
|
19
20
|
function escapeRegex(input) {
|
|
20
21
|
return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
21
22
|
}
|
|
22
|
-
/**
|
|
23
|
-
* Applies the component rename to a filename. Only replaces the leading
|
|
24
|
-
* component name when it is followed by `.` (extension) or equals the
|
|
25
|
-
* filename exactly; every other filename is returned unchanged so stray
|
|
26
|
-
* assets, editor backups, or files whose name coincidentally contains the
|
|
27
|
-
* old component name in the middle or at the end are not accidentally
|
|
28
|
-
* renamed.
|
|
29
|
-
*/
|
|
30
|
-
function renameComponentFileName(fileName, oldName, newName) {
|
|
31
|
-
if (fileName === oldName)
|
|
32
|
-
return newName;
|
|
33
|
-
if (fileName.startsWith(oldName + '.')) {
|
|
34
|
-
return newName + fileName.slice(oldName.length);
|
|
35
|
-
}
|
|
36
|
-
return fileName;
|
|
37
|
-
}
|
|
38
|
-
function updateConfigForCustomRename(config, oldName, newName) {
|
|
39
|
-
const oldConfig = config.custom[oldName];
|
|
40
|
-
if (!oldConfig)
|
|
41
|
-
return;
|
|
42
|
-
config.custom[newName] = {
|
|
43
|
-
...oldConfig,
|
|
44
|
-
targetPath: oldConfig.targetPath.replace(new RegExp(`(^|/)${oldName}$`), `$1${newName}`),
|
|
45
|
-
};
|
|
46
|
-
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete -- idiomatic key removal from config record
|
|
47
|
-
delete config.custom[oldName];
|
|
48
|
-
// Update composes references in other components
|
|
49
|
-
for (const customConfig of Object.values(config.custom)) {
|
|
50
|
-
if (customConfig.composes) {
|
|
51
|
-
customConfig.composes = customConfig.composes.map((ref) => (ref === oldName ? newName : ref));
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
function updateConfigForOverrideRename(config, oldName, newName) {
|
|
56
|
-
const oldConfig = config.overrides[oldName];
|
|
57
|
-
if (!oldConfig)
|
|
58
|
-
return;
|
|
59
|
-
config.overrides[newName] = { ...oldConfig };
|
|
60
|
-
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete -- idiomatic key removal from config record
|
|
61
|
-
delete config.overrides[oldName];
|
|
62
|
-
}
|
|
63
23
|
/**
|
|
64
24
|
* Derives the test file name for a component, matching the convention used by
|
|
65
25
|
* `furnace create --with-tests`.
|
|
@@ -215,18 +175,37 @@ async function renameMochikitTestFiles(engineDir, oldName, newName, journal) {
|
|
|
215
175
|
* Performs the transactional rename mutation inside a furnace lock.
|
|
216
176
|
*/
|
|
217
177
|
async function performRenameMutations(args) {
|
|
218
|
-
const { projectRoot, oldName, newName
|
|
178
|
+
const { projectRoot, oldName, newName } = args;
|
|
219
179
|
const oldClassName = tagNameToClassName(oldName);
|
|
220
180
|
const newClassName = tagNameToClassName(newName);
|
|
221
|
-
// Capture the pre-rename deployed target path so we know what to
|
|
222
|
-
// clean up in the engine tree. `updateConfigForCustomRename` rewrites
|
|
223
|
-
// `targetPath` in-place once the mutation enters phase 2, so we read
|
|
224
|
-
// it here while it still points at the old name's deployment.
|
|
225
|
-
const oldCustomTargetPath = isCustom ? config.custom[oldName]?.targetPath : undefined;
|
|
226
181
|
await runFurnaceMutation(projectRoot, 'rename-rollback', async (ctx) => {
|
|
227
182
|
const journal = createRollbackJournal();
|
|
228
183
|
ctx.registerJournal(journal);
|
|
184
|
+
let newDir = args.newDir;
|
|
229
185
|
try {
|
|
186
|
+
const config = await loadFurnaceConfig(projectRoot);
|
|
187
|
+
const isCustom = oldName in config.custom;
|
|
188
|
+
const isOverride = oldName in config.overrides;
|
|
189
|
+
if (!isCustom && !isOverride) {
|
|
190
|
+
throw new FurnaceError(`Component "${oldName}" not found in furnace.json. Only custom and override components can be renamed.`, oldName);
|
|
191
|
+
}
|
|
192
|
+
if (newName in config.custom ||
|
|
193
|
+
newName in config.overrides ||
|
|
194
|
+
config.stock.includes(newName)) {
|
|
195
|
+
throw new FurnaceError(`A component named "${newName}" already exists in furnace.json.`, newName);
|
|
196
|
+
}
|
|
197
|
+
const componentType = isCustom ? 'custom' : 'override';
|
|
198
|
+
const componentDirLabel = isCustom ? 'custom' : 'overrides';
|
|
199
|
+
const baseDir = isCustom ? args.furnacePaths.customDir : args.furnacePaths.overridesDir;
|
|
200
|
+
const oldDir = join(baseDir, oldName);
|
|
201
|
+
newDir = join(baseDir, newName);
|
|
202
|
+
const oldCustomTargetPath = isCustom ? config.custom[oldName]?.targetPath : undefined;
|
|
203
|
+
if (!(await pathExists(oldDir))) {
|
|
204
|
+
throw new FurnaceError(`Component directory not found: components/${componentDirLabel}/${oldName}`, oldName);
|
|
205
|
+
}
|
|
206
|
+
if (await pathExists(newDir)) {
|
|
207
|
+
throw new FurnaceError(`Target directory already exists: components/${componentDirLabel}/${newName}`, newName);
|
|
208
|
+
}
|
|
230
209
|
await snapshotDir(journal, oldDir);
|
|
231
210
|
await snapshotFile(journal, args.furnaceConfigPath);
|
|
232
211
|
// 1. Create new directory with renamed files and updated content
|
|
@@ -472,6 +451,7 @@ export async function furnaceRenameCommand(projectRoot, oldName, newName) {
|
|
|
472
451
|
componentType,
|
|
473
452
|
config,
|
|
474
453
|
furnaceConfigPath: furnacePaths.furnaceConfig,
|
|
454
|
+
furnacePaths,
|
|
475
455
|
engineDir: paths.engine,
|
|
476
456
|
});
|
|
477
457
|
note(`Component renamed: ${oldName} → ${newName}\n\n` +
|
|
@@ -19,7 +19,7 @@ export declare const SRC_DIR = "src";
|
|
|
19
19
|
/** Supported top-level fireforge.json keys backed by the current schema. */
|
|
20
20
|
export declare const SUPPORTED_CONFIG_ROOT_KEYS: readonly ["name", "vendor", "appId", "binaryName", "firefox", "build", "license", "wire", "patchLint", "typecheck", "markerComment"];
|
|
21
21
|
/** Supported config paths that can be read or set without --force. */
|
|
22
|
-
export declare const SUPPORTED_CONFIG_PATHS: readonly ["name", "vendor", "appId", "binaryName", "license", "firefox", "firefox.version", "firefox.product", "build", "build.jobs", "wire", "wire.subscriptDir", "patchLint", "patchLint.checkJs", "patchLint.checkJsStrict", "patchLint.checkJsCompilerOptions", "patchLint.checkJsExtraShim", "patchLint.rawColorAllowlist", "patchLint.jsdocClassMethods", "patchLint.testAssertionFloor", "patchLint.chromeScriptJsDoc", "typecheck", "typecheck.projects", "typecheck.extraShim", "markerComment"];
|
|
22
|
+
export declare const SUPPORTED_CONFIG_PATHS: readonly ["name", "vendor", "appId", "binaryName", "license", "firefox", "firefox.version", "firefox.product", "firefox.sha256", "build", "build.jobs", "wire", "wire.subscriptDir", "patchLint", "patchLint.checkJs", "patchLint.checkJsStrict", "patchLint.checkJsCompilerOptions", "patchLint.checkJsExtraShim", "patchLint.rawColorAllowlist", "patchLint.jsdocClassMethods", "patchLint.testAssertionFloor", "patchLint.chromeScriptJsDoc", "typecheck", "typecheck.projects", "typecheck.extraShim", "markerComment"];
|
|
23
23
|
/**
|
|
24
24
|
* Gets all project paths based on a root directory.
|
|
25
25
|
* @param root - Root directory of the project
|
|
@@ -75,6 +75,10 @@ export function validateConfig(data) {
|
|
|
75
75
|
if (compatError) {
|
|
76
76
|
throw new ConfigError(compatError);
|
|
77
77
|
}
|
|
78
|
+
const firefoxSha256 = optionalConfigString(firefoxRec, 'sha256', 'firefox.sha256');
|
|
79
|
+
if (firefoxSha256 !== undefined && !/^[a-f0-9]{64}$/i.test(firefoxSha256)) {
|
|
80
|
+
throw new ConfigError('Config field "firefox.sha256" must be a 64-character SHA-256 hex digest');
|
|
81
|
+
}
|
|
78
82
|
// Optional configs
|
|
79
83
|
const config = {
|
|
80
84
|
name,
|
|
@@ -84,6 +88,7 @@ export function validateConfig(data) {
|
|
|
84
88
|
firefox: {
|
|
85
89
|
version: firefoxVersion,
|
|
86
90
|
product: firefoxProduct,
|
|
91
|
+
...(firefoxSha256 !== undefined ? { sha256: firefoxSha256.toLowerCase() } : {}),
|
|
87
92
|
},
|
|
88
93
|
};
|
|
89
94
|
// Build
|
package/dist/src/core/config.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { basename } from 'node:path';
|
|
12
12
|
import { ConfigError, ConfigNotFoundError } from '../errors/config.js';
|
|
13
13
|
import { toError } from '../utils/errors.js';
|
|
14
|
-
import
|
|
14
|
+
import * as fsUtils from '../utils/fs.js';
|
|
15
15
|
import { getProjectPaths } from './config-paths.js';
|
|
16
16
|
import { validateConfig } from './config-validate.js';
|
|
17
17
|
import { createSiblingLockPath, withFileLock } from './file-lock.js';
|
|
@@ -21,6 +21,10 @@ export { CONFIG_FILENAME, FIREFORGE_DIR, getProjectPaths, STATE_FILENAME, SUPPOR
|
|
|
21
21
|
export { loadState, saveState, updateState } from './config-state.js';
|
|
22
22
|
export { validateConfig } from './config-validate.js';
|
|
23
23
|
// ---- config I/O (stays here because it bridges paths + validation) ----
|
|
24
|
+
async function configPathExists(path) {
|
|
25
|
+
const fs = fsUtils;
|
|
26
|
+
return (fs.pathExistsStrict ?? fsUtils.pathExists)(path);
|
|
27
|
+
}
|
|
24
28
|
/**
|
|
25
29
|
* Checks if a fireforge.json exists in the given directory.
|
|
26
30
|
* @param root - Root directory to check
|
|
@@ -28,7 +32,7 @@ export { validateConfig } from './config-validate.js';
|
|
|
28
32
|
*/
|
|
29
33
|
export async function configExists(root) {
|
|
30
34
|
const paths = getProjectPaths(root);
|
|
31
|
-
return
|
|
35
|
+
return configPathExists(paths.config);
|
|
32
36
|
}
|
|
33
37
|
/**
|
|
34
38
|
* Loads and validates the fireforge.json configuration.
|
|
@@ -38,11 +42,11 @@ export async function configExists(root) {
|
|
|
38
42
|
*/
|
|
39
43
|
export async function loadConfig(root) {
|
|
40
44
|
const paths = getProjectPaths(root);
|
|
41
|
-
if (!(await
|
|
45
|
+
if (!(await configPathExists(paths.config))) {
|
|
42
46
|
throw new ConfigNotFoundError(paths.config);
|
|
43
47
|
}
|
|
44
48
|
try {
|
|
45
|
-
const data = await readJson(paths.config);
|
|
49
|
+
const data = await fsUtils.readJson(paths.config);
|
|
46
50
|
return validateConfig(data);
|
|
47
51
|
}
|
|
48
52
|
catch (error) {
|
|
@@ -70,11 +74,11 @@ export async function loadConfig(root) {
|
|
|
70
74
|
*/
|
|
71
75
|
export async function loadRawConfigDocument(root) {
|
|
72
76
|
const paths = getProjectPaths(root);
|
|
73
|
-
if (!(await
|
|
77
|
+
if (!(await configPathExists(paths.config))) {
|
|
74
78
|
throw new ConfigNotFoundError(paths.config);
|
|
75
79
|
}
|
|
76
80
|
try {
|
|
77
|
-
const data = await readJson(paths.config);
|
|
81
|
+
const data = await fsUtils.readJson(paths.config);
|
|
78
82
|
if (data === null || typeof data !== 'object' || Array.isArray(data)) {
|
|
79
83
|
throw new ConfigError(`Invalid fireforge.json at ${paths.config}: expected an object`);
|
|
80
84
|
}
|
|
@@ -110,7 +114,7 @@ export async function writeConfig(root, config) {
|
|
|
110
114
|
*/
|
|
111
115
|
export async function writeConfigDocument(root, config) {
|
|
112
116
|
const paths = getProjectPaths(root);
|
|
113
|
-
await writeJson(paths.config, config);
|
|
117
|
+
await fsUtils.writeJson(paths.config, config);
|
|
114
118
|
}
|
|
115
119
|
/**
|
|
116
120
|
* Runs an operation while holding a sidecar lock on `fireforge.json`.
|
|
@@ -35,8 +35,8 @@ function isProcessAlive(pid) {
|
|
|
35
35
|
process.kill(pid, 0);
|
|
36
36
|
return true;
|
|
37
37
|
}
|
|
38
|
-
catch {
|
|
39
|
-
return
|
|
38
|
+
catch (error) {
|
|
39
|
+
return getNodeErrorCode(error) !== 'ESRCH';
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
async function removeIfStaleLock(lockPath, staleMs, onStaleLockMessage) {
|
|
@@ -14,7 +14,7 @@ export declare function sha256File(filePath: string): Promise<string>;
|
|
|
14
14
|
* @param cacheDir - Cache directory
|
|
15
15
|
* @param onProgress - Optional progress callback
|
|
16
16
|
*/
|
|
17
|
-
export declare function ensureCachedArchive(archive: ResolvedArchive, cacheDir: string, onProgress?: ProgressCallback): Promise<void>;
|
|
17
|
+
export declare function ensureCachedArchive(archive: ResolvedArchive, cacheDir: string, onProgress?: ProgressCallback, expectedSha256?: string): Promise<void>;
|
|
18
18
|
/**
|
|
19
19
|
* Removes cached tarball, metadata, and partial download files for an archive.
|
|
20
20
|
* @param archive - Resolved archive descriptor
|
|
@@ -7,9 +7,11 @@ import { createReadStream } from 'node:fs';
|
|
|
7
7
|
import { rename } from 'node:fs/promises';
|
|
8
8
|
import { join } from 'node:path';
|
|
9
9
|
import { pipeline } from 'node:stream/promises';
|
|
10
|
+
import { DownloadError } from '../errors/download.js';
|
|
10
11
|
import { toError } from '../utils/errors.js';
|
|
11
12
|
import { pathExists, readJson, removeFile, writeJson } from '../utils/fs.js';
|
|
12
13
|
import { verbose } from '../utils/logger.js';
|
|
14
|
+
import { createSiblingLockPath, withFileLock } from './file-lock.js';
|
|
13
15
|
import { validateArchiveMetadata } from './firefox-archive.js';
|
|
14
16
|
import { downloadFile } from './firefox-download.js';
|
|
15
17
|
/**
|
|
@@ -28,12 +30,24 @@ export async function sha256File(filePath) {
|
|
|
28
30
|
* @param cacheDir - Cache directory
|
|
29
31
|
* @param onProgress - Optional progress callback
|
|
30
32
|
*/
|
|
31
|
-
export async function ensureCachedArchive(archive, cacheDir, onProgress) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
export async function ensureCachedArchive(archive, cacheDir, onProgress, expectedSha256) {
|
|
34
|
+
const lockPath = createSiblingLockPath(join(cacheDir, archive.filename), '.fireforge-cache.lock');
|
|
35
|
+
await withFileLock(lockPath, async () => {
|
|
36
|
+
if (await validateCachedArchive(archive, cacheDir, expectedSha256)) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (await cacheEntryExists(archive, cacheDir)) {
|
|
40
|
+
await invalidateArchiveCache(archive, cacheDir);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
await removeArchivePartFiles(archive, cacheDir);
|
|
44
|
+
}
|
|
45
|
+
await downloadToCache(archive, cacheDir, onProgress, expectedSha256);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
async function cacheEntryExists(archive, cacheDir) {
|
|
49
|
+
return ((await pathExists(join(cacheDir, archive.filename))) ||
|
|
50
|
+
(await pathExists(join(cacheDir, archive.metadataFilename))));
|
|
37
51
|
}
|
|
38
52
|
/**
|
|
39
53
|
* Validates a cached archive using sidecar metadata and SHA-256 checksum.
|
|
@@ -41,7 +55,7 @@ export async function ensureCachedArchive(archive, cacheDir, onProgress) {
|
|
|
41
55
|
* @param cacheDir - Cache directory
|
|
42
56
|
* @returns True if the cache entry is valid
|
|
43
57
|
*/
|
|
44
|
-
async function validateCachedArchive(archive, cacheDir) {
|
|
58
|
+
async function validateCachedArchive(archive, cacheDir, expectedSha256) {
|
|
45
59
|
const tarballPath = join(cacheDir, archive.filename);
|
|
46
60
|
const metadataPath = join(cacheDir, archive.metadataFilename);
|
|
47
61
|
if (!(await pathExists(tarballPath)) || !(await pathExists(metadataPath))) {
|
|
@@ -61,9 +75,12 @@ async function validateCachedArchive(archive, cacheDir) {
|
|
|
61
75
|
return false;
|
|
62
76
|
}
|
|
63
77
|
}
|
|
64
|
-
if (metadata.sha256) {
|
|
78
|
+
if (expectedSha256 || metadata.sha256) {
|
|
65
79
|
const actualHash = await sha256File(tarballPath);
|
|
66
|
-
if (actualHash !==
|
|
80
|
+
if (expectedSha256 && actualHash !== expectedSha256) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
if (metadata.sha256 && actualHash !== metadata.sha256) {
|
|
67
84
|
return false;
|
|
68
85
|
}
|
|
69
86
|
}
|
|
@@ -80,16 +97,21 @@ async function validateCachedArchive(archive, cacheDir) {
|
|
|
80
97
|
* @param cacheDir - Cache directory
|
|
81
98
|
* @param onProgress - Optional progress callback
|
|
82
99
|
*/
|
|
83
|
-
async function downloadToCache(archive, cacheDir, onProgress) {
|
|
100
|
+
async function downloadToCache(archive, cacheDir, onProgress, expectedSha256) {
|
|
84
101
|
const tarballPath = join(cacheDir, archive.filename);
|
|
85
102
|
// Use a unique .part path so concurrent downloads for the same archive
|
|
86
103
|
// do not clobber each other's partial files.
|
|
87
104
|
const partPath = `${tarballPath}.part-${randomUUID()}`;
|
|
88
105
|
const metadataPath = join(cacheDir, archive.metadataFilename);
|
|
106
|
+
let promotedTarball = false;
|
|
89
107
|
try {
|
|
90
108
|
const contentLength = await downloadFile(archive.url, partPath, onProgress);
|
|
91
109
|
await rename(partPath, tarballPath);
|
|
110
|
+
promotedTarball = true;
|
|
92
111
|
const sha256 = await sha256File(tarballPath);
|
|
112
|
+
if (expectedSha256 && sha256 !== expectedSha256) {
|
|
113
|
+
throw new DownloadError(`Downloaded archive SHA-256 mismatch: expected ${expectedSha256}, got ${sha256}`, archive.url);
|
|
114
|
+
}
|
|
93
115
|
await writeJson(metadataPath, {
|
|
94
116
|
requestedVersion: archive.requestedVersion,
|
|
95
117
|
product: archive.product,
|
|
@@ -102,8 +124,10 @@ async function downloadToCache(archive, cacheDir, onProgress) {
|
|
|
102
124
|
}
|
|
103
125
|
catch (error) {
|
|
104
126
|
await removeFile(partPath);
|
|
105
|
-
|
|
106
|
-
|
|
127
|
+
if (promotedTarball) {
|
|
128
|
+
await removeFile(tarballPath);
|
|
129
|
+
await removeFile(metadataPath);
|
|
130
|
+
}
|
|
107
131
|
throw error;
|
|
108
132
|
}
|
|
109
133
|
}
|
|
@@ -115,8 +139,11 @@ async function downloadToCache(archive, cacheDir, onProgress) {
|
|
|
115
139
|
export async function invalidateArchiveCache(archive, cacheDir) {
|
|
116
140
|
const tarballPath = join(cacheDir, archive.filename);
|
|
117
141
|
const metadataPath = join(cacheDir, archive.metadataFilename);
|
|
118
|
-
|
|
119
|
-
|
|
142
|
+
await removeArchivePartFiles(archive, cacheDir);
|
|
143
|
+
await removeFile(tarballPath);
|
|
144
|
+
await removeFile(metadataPath);
|
|
145
|
+
}
|
|
146
|
+
async function removeArchivePartFiles(archive, cacheDir) {
|
|
120
147
|
const partPrefix = `${archive.filename}.part`;
|
|
121
148
|
try {
|
|
122
149
|
const { readdir } = await import('node:fs/promises');
|
|
@@ -125,10 +152,9 @@ export async function invalidateArchiveCache(archive, cacheDir) {
|
|
|
125
152
|
.filter((name) => name.startsWith(partPrefix))
|
|
126
153
|
.map((name) => removeFile(join(cacheDir, name))));
|
|
127
154
|
}
|
|
128
|
-
catch {
|
|
155
|
+
catch (error) {
|
|
156
|
+
void error;
|
|
129
157
|
// Cache dir may not exist yet — that's fine.
|
|
130
158
|
}
|
|
131
|
-
await removeFile(tarballPath);
|
|
132
|
-
await removeFile(metadataPath);
|
|
133
159
|
}
|
|
134
160
|
//# sourceMappingURL=firefox-cache.js.map
|
|
@@ -73,20 +73,28 @@ export async function fetchWithRetry(url) {
|
|
|
73
73
|
*/
|
|
74
74
|
function createStallDetector(url, timeoutMs = DOWNLOAD_STALL_TIMEOUT_MS) {
|
|
75
75
|
let timer;
|
|
76
|
+
const clearTimer = () => {
|
|
77
|
+
if (timer !== undefined) {
|
|
78
|
+
clearTimeout(timer);
|
|
79
|
+
timer = undefined;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
76
82
|
const detector = new Transform({
|
|
77
83
|
transform(chunk, _encoding, callback) {
|
|
78
|
-
|
|
79
|
-
clearTimeout(timer);
|
|
84
|
+
clearTimer();
|
|
80
85
|
timer = setTimeout(() => {
|
|
81
86
|
detector.destroy(new DownloadError(`Download stalled: no data received for ${Math.round(timeoutMs / 1000)} seconds`, url));
|
|
82
87
|
}, timeoutMs);
|
|
83
88
|
callback(null, chunk);
|
|
84
89
|
},
|
|
85
90
|
flush(callback) {
|
|
86
|
-
|
|
87
|
-
clearTimeout(timer);
|
|
91
|
+
clearTimer();
|
|
88
92
|
callback();
|
|
89
93
|
},
|
|
94
|
+
destroy(error, callback) {
|
|
95
|
+
clearTimer();
|
|
96
|
+
callback(error);
|
|
97
|
+
},
|
|
90
98
|
});
|
|
91
99
|
// Start the initial timer (covers the case where the first chunk never arrives).
|
|
92
100
|
timer = setTimeout(() => {
|
|
@@ -45,4 +45,4 @@ export type FirefoxSourcePhaseCallback = (phase: FirefoxSourcePhase) => void;
|
|
|
45
45
|
* between phases (`'download'` → `'extract'`). Fires exactly once per
|
|
46
46
|
* phase even if the cached archive path skips the wire entirely.
|
|
47
47
|
*/
|
|
48
|
-
export declare function downloadFirefoxSource(version: string, product: FirefoxProduct, destDir: string, cacheDir: string, onProgress?: ProgressCallback, onPhase?: FirefoxSourcePhaseCallback): Promise<void>;
|
|
48
|
+
export declare function downloadFirefoxSource(version: string, product: FirefoxProduct, destDir: string, cacheDir: string, onProgress?: ProgressCallback, onPhase?: FirefoxSourcePhaseCallback, expectedSha256?: string): Promise<void>;
|
package/dist/src/core/firefox.js
CHANGED
|
@@ -44,13 +44,13 @@ export function getTarballFilename(version, product = 'firefox') {
|
|
|
44
44
|
* between phases (`'download'` → `'extract'`). Fires exactly once per
|
|
45
45
|
* phase even if the cached archive path skips the wire entirely.
|
|
46
46
|
*/
|
|
47
|
-
export async function downloadFirefoxSource(version, product, destDir, cacheDir, onProgress, onPhase) {
|
|
47
|
+
export async function downloadFirefoxSource(version, product, destDir, cacheDir, onProgress, onPhase, expectedSha256) {
|
|
48
48
|
const archive = resolveArchive(version, product);
|
|
49
49
|
const tarballPath = join(cacheDir, archive.filename);
|
|
50
50
|
// Ensure cache directory exists
|
|
51
51
|
await ensureDir(cacheDir);
|
|
52
52
|
onPhase?.('download');
|
|
53
|
-
await ensureCachedArchive(archive, cacheDir, onProgress);
|
|
53
|
+
await ensureCachedArchive(archive, cacheDir, onProgress, expectedSha256);
|
|
54
54
|
// Extract to a unique temporary directory so concurrent downloads for
|
|
55
55
|
// the same destination do not clobber each other.
|
|
56
56
|
onPhase?.('extract');
|
|
@@ -17,6 +17,19 @@ import { readText, writeText } from '../utils/fs.js';
|
|
|
17
17
|
import { exec } from '../utils/process.js';
|
|
18
18
|
import { ensureGit } from './git-base.js';
|
|
19
19
|
import { getFileContentAtRef } from './git-file-ops.js';
|
|
20
|
+
function isFatalMergeStderr(stderr) {
|
|
21
|
+
return /(?:^|\n)\s*(?:fatal|error):/i.test(stderr);
|
|
22
|
+
}
|
|
23
|
+
function classifyMergeFileResult(result) {
|
|
24
|
+
if (result.exitCode === 0 && !isFatalMergeStderr(result.stderr)) {
|
|
25
|
+
return 0;
|
|
26
|
+
}
|
|
27
|
+
if (result.exitCode >= 1 && result.exitCode <= 127 && !isFatalMergeStderr(result.stderr)) {
|
|
28
|
+
return result.exitCode;
|
|
29
|
+
}
|
|
30
|
+
const detail = result.stderr.trim() || `exit code ${result.exitCode}`;
|
|
31
|
+
throw new FurnaceError(`git merge-file failed: ${detail}`);
|
|
32
|
+
}
|
|
20
33
|
/**
|
|
21
34
|
* Performs a three-way merge on a single file.
|
|
22
35
|
*
|
|
@@ -40,7 +53,8 @@ async function threeWayMergeFile(base, ours, theirs, label, strategy) {
|
|
|
40
53
|
await writeText(tempOurs, ours);
|
|
41
54
|
await writeText(tempTheirs, theirs);
|
|
42
55
|
// git merge-file writes the result to the first file (ours) in-place.
|
|
43
|
-
// Exit code 0 = clean merge,
|
|
56
|
+
// Exit code 0 = clean merge, 1..127 = conflict count, shell-exposed
|
|
57
|
+
// fatal errors typically arrive as >=128 (for example, 255).
|
|
44
58
|
const mergeArgs = [
|
|
45
59
|
'merge-file',
|
|
46
60
|
...(strategy ? [`--${strategy}`] : []),
|
|
@@ -55,11 +69,8 @@ async function threeWayMergeFile(base, ours, theirs, label, strategy) {
|
|
|
55
69
|
tempTheirs,
|
|
56
70
|
];
|
|
57
71
|
const result = await exec('git', mergeArgs);
|
|
72
|
+
const conflicts = classifyMergeFileResult(result);
|
|
58
73
|
const merged = await readText(tempOurs);
|
|
59
|
-
const conflicts = result.exitCode > 0 ? result.exitCode : 0;
|
|
60
|
-
if (result.exitCode < 0) {
|
|
61
|
-
throw new FurnaceError(`git merge-file failed: ${result.stderr}`);
|
|
62
|
-
}
|
|
63
74
|
return { merged, conflicts };
|
|
64
75
|
}
|
|
65
76
|
finally {
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { stripJsComments } from '../utils/regex.js';
|
|
2
|
+
import { parseModule, parseScript, walkAST } from './ast-utils.js';
|
|
3
|
+
const RELATIVE_IMPORT_FALLBACK_PATTERN = /(?:\bimport\s*(?:\(\s*)?(?:[\s\S]*?\bfrom\s*)?|\bexport\s+(?:[\s\S]*?\bfrom\s*)?|(?:ChromeUtils\.import(?:ESModule)?|Cu\.import)\s*\(\s*)["'](?:\.\.?\/)/m;
|
|
4
|
+
function literalString(node) {
|
|
5
|
+
if (!node || node.type !== 'Literal')
|
|
6
|
+
return undefined;
|
|
7
|
+
return typeof node.value === 'string' ? node.value : undefined;
|
|
8
|
+
}
|
|
9
|
+
function isRelativeSpecifier(value) {
|
|
10
|
+
return value !== undefined && (value.startsWith('./') || value.startsWith('../'));
|
|
11
|
+
}
|
|
12
|
+
function isChromeImportCall(node) {
|
|
13
|
+
const callee = node.callee;
|
|
14
|
+
if (callee.type !== 'MemberExpression' || callee.computed)
|
|
15
|
+
return false;
|
|
16
|
+
if (callee.object.type !== 'Identifier' || callee.property.type !== 'Identifier')
|
|
17
|
+
return false;
|
|
18
|
+
if (callee.object.name === 'Cu') {
|
|
19
|
+
return callee.property.name === 'import';
|
|
20
|
+
}
|
|
21
|
+
return (callee.object.name === 'ChromeUtils' &&
|
|
22
|
+
(callee.property.name === 'import' || callee.property.name === 'importESModule'));
|
|
23
|
+
}
|
|
24
|
+
function astHasRelativeImport(content, sourceType) {
|
|
25
|
+
const ast = sourceType === 'module' ? parseModule(content) : parseScript(content);
|
|
26
|
+
let found = false;
|
|
27
|
+
walkAST(ast, {
|
|
28
|
+
enter(node) {
|
|
29
|
+
if (found) {
|
|
30
|
+
this.skip();
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (node.type === 'ImportDeclaration') {
|
|
34
|
+
found = isRelativeSpecifier(literalString(node.source));
|
|
35
|
+
}
|
|
36
|
+
else if (node.type === 'ImportExpression') {
|
|
37
|
+
found = isRelativeSpecifier(literalString(node.source));
|
|
38
|
+
}
|
|
39
|
+
else if (node.type === 'ExportNamedDeclaration' || node.type === 'ExportAllDeclaration') {
|
|
40
|
+
found = isRelativeSpecifier(literalString(node.source));
|
|
41
|
+
}
|
|
42
|
+
else if (node.type === 'CallExpression' && isChromeImportCall(node)) {
|
|
43
|
+
found = isRelativeSpecifier(literalString(node.arguments[0]));
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
return found;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Detects relative JS imports while avoiding comment/template false positives.
|
|
51
|
+
* Falls back to stripped-text matching for legacy chrome scripts Acorn cannot parse.
|
|
52
|
+
*/
|
|
53
|
+
export function hasRelativeImport(content) {
|
|
54
|
+
try {
|
|
55
|
+
return astHasRelativeImport(content, 'module');
|
|
56
|
+
}
|
|
57
|
+
catch (moduleError) {
|
|
58
|
+
void moduleError;
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
return astHasRelativeImport(content, 'script');
|
|
62
|
+
}
|
|
63
|
+
catch (scriptError) {
|
|
64
|
+
void scriptError;
|
|
65
|
+
}
|
|
66
|
+
return RELATIVE_IMPORT_FALLBACK_PATTERN.test(stripJsComments(content));
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=patch-lint-imports.js.map
|
|
@@ -10,6 +10,7 @@ import { invokePatchLintCheckJs } from './patch-lint-checkjs.js';
|
|
|
10
10
|
import { lintChromeScriptJsDocForFile } from './patch-lint-chrome-jsdoc.js';
|
|
11
11
|
import { detectNewFilesInDiff, extractAddedLinesPerFile } from './patch-lint-diff.js';
|
|
12
12
|
import { AGGREGATE_PATCH_FILE } from './patch-lint-diff-tag.js';
|
|
13
|
+
import { hasRelativeImport } from './patch-lint-imports.js';
|
|
13
14
|
import { validateExportJsDoc } from './patch-lint-jsdoc.js';
|
|
14
15
|
import { resolvePatchOwnedChromeScripts, resolvePatchOwnedSysMjs } from './patch-lint-ownership.js';
|
|
15
16
|
// ---------------------------------------------------------------------------
|
|
@@ -423,9 +424,7 @@ export async function lintPatchedJs(repoDir, affectedFiles, newFiles, config, pa
|
|
|
423
424
|
const isSysMjs = file.endsWith('.sys.mjs');
|
|
424
425
|
// 1. Relative import check
|
|
425
426
|
const strippedContent = stripJsComments(content);
|
|
426
|
-
|
|
427
|
-
const esRelativePattern = /\bimport\s+.*?\s+from\s+["'](?:\.\.?\/)/gm;
|
|
428
|
-
if (relativeImportPattern.test(strippedContent) || esRelativePattern.test(strippedContent)) {
|
|
427
|
+
if (hasRelativeImport(content)) {
|
|
429
428
|
issues.push({
|
|
430
429
|
file,
|
|
431
430
|
check: 'relative-import',
|
|
@@ -426,15 +426,15 @@ export interface FurnaceCreateOptions {
|
|
|
426
426
|
/**
|
|
427
427
|
* Test harness style to scaffold when `--with-tests` is set.
|
|
428
428
|
*
|
|
429
|
-
* - `
|
|
430
|
-
* test
|
|
431
|
-
*
|
|
432
|
-
*
|
|
433
|
-
*
|
|
434
|
-
* `
|
|
435
|
-
*
|
|
436
|
-
*
|
|
437
|
-
*
|
|
429
|
+
* - `browser-chrome` (default when `--with-tests` is set without
|
|
430
|
+
* `--test-style`) — browser mochitest scaffold; requires a working
|
|
431
|
+
* `tabbrowser`. Prefer this for interactive chrome/widget coverage
|
|
432
|
+
* (including on macOS).
|
|
433
|
+
* - `mochikit` — opt-in MochiKit test at
|
|
434
|
+
* `engine/toolkit/content/tests/widgets/test_<tag>.html` that loads
|
|
435
|
+
* the component via `chrome://global/`. Use when the top-level chrome
|
|
436
|
+
* document lacks a `tabbrowser`; on macOS the toolkit mochitest-chrome
|
|
437
|
+
* flavor can be unreliable (long idle timeout).
|
|
438
438
|
* - `xpcshell` — equivalent to setting `--xpcshell`; headless, storage-only.
|
|
439
439
|
*/
|
|
440
440
|
testStyle?: 'mochikit' | 'browser-chrome' | 'xpcshell';
|
package/dist/src/utils/fs.d.ts
CHANGED
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
* @param path - Path to check
|
|
4
4
|
*/
|
|
5
5
|
export declare function pathExists(path: string): Promise<boolean>;
|
|
6
|
+
/**
|
|
7
|
+
* Checks if a path exists while surfacing permission errors.
|
|
8
|
+
* @param path - Path to check
|
|
9
|
+
*/
|
|
10
|
+
export declare function pathExistsStrict(path: string): Promise<boolean>;
|
|
6
11
|
/**
|
|
7
12
|
* Ensures a directory exists, creating it recursively if needed.
|
|
8
13
|
* @param path - Directory path to ensure
|