@hominis/fireforge 0.9.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 +7 -0
- package/LICENSE.md +294 -0
- package/README.md +435 -0
- package/dist/bin/fireforge.d.ts +10 -0
- package/dist/bin/fireforge.js +29 -0
- package/dist/src/cli.d.ts +33 -0
- package/dist/src/cli.js +180 -0
- package/dist/src/commands/bootstrap.d.ts +9 -0
- package/dist/src/commands/bootstrap.js +73 -0
- package/dist/src/commands/build.d.ts +11 -0
- package/dist/src/commands/build.js +102 -0
- package/dist/src/commands/config.d.ts +13 -0
- package/dist/src/commands/config.js +135 -0
- package/dist/src/commands/discard.d.ts +12 -0
- package/dist/src/commands/discard.js +84 -0
- package/dist/src/commands/doctor.d.ts +18 -0
- package/dist/src/commands/doctor.js +356 -0
- package/dist/src/commands/download.d.ts +11 -0
- package/dist/src/commands/download.js +127 -0
- package/dist/src/commands/export-all.d.ts +11 -0
- package/dist/src/commands/export-all.js +122 -0
- package/dist/src/commands/export-shared.d.ts +48 -0
- package/dist/src/commands/export-shared.js +208 -0
- package/dist/src/commands/export.d.ts +13 -0
- package/dist/src/commands/export.js +178 -0
- package/dist/src/commands/furnace/apply.d.ts +7 -0
- package/dist/src/commands/furnace/apply.js +80 -0
- package/dist/src/commands/furnace/create.d.ts +8 -0
- package/dist/src/commands/furnace/create.js +377 -0
- package/dist/src/commands/furnace/deploy.d.ts +8 -0
- package/dist/src/commands/furnace/deploy.js +338 -0
- package/dist/src/commands/furnace/diff.d.ts +7 -0
- package/dist/src/commands/furnace/diff.js +119 -0
- package/dist/src/commands/furnace/index.d.ts +16 -0
- package/dist/src/commands/furnace/index.js +121 -0
- package/dist/src/commands/furnace/list.d.ts +5 -0
- package/dist/src/commands/furnace/list.js +65 -0
- package/dist/src/commands/furnace/override.d.ts +8 -0
- package/dist/src/commands/furnace/override.js +188 -0
- package/dist/src/commands/furnace/preview.d.ts +7 -0
- package/dist/src/commands/furnace/preview.js +96 -0
- package/dist/src/commands/furnace/remove.d.ts +8 -0
- package/dist/src/commands/furnace/remove.js +159 -0
- package/dist/src/commands/furnace/scan.d.ts +5 -0
- package/dist/src/commands/furnace/scan.js +112 -0
- package/dist/src/commands/furnace/status.d.ts +7 -0
- package/dist/src/commands/furnace/status.js +137 -0
- package/dist/src/commands/furnace/validate.d.ts +6 -0
- package/dist/src/commands/furnace/validate.js +91 -0
- package/dist/src/commands/furnace/validation-output.d.ts +7 -0
- package/dist/src/commands/furnace/validation-output.js +22 -0
- package/dist/src/commands/import.d.ts +11 -0
- package/dist/src/commands/import.js +241 -0
- package/dist/src/commands/lint.d.ts +10 -0
- package/dist/src/commands/lint.js +118 -0
- package/dist/src/commands/package.d.ts +11 -0
- package/dist/src/commands/package.js +80 -0
- package/dist/src/commands/re-export.d.ts +12 -0
- package/dist/src/commands/re-export.js +242 -0
- package/dist/src/commands/rebase/abort.d.ts +7 -0
- package/dist/src/commands/rebase/abort.js +49 -0
- package/dist/src/commands/rebase/confirm.d.ts +18 -0
- package/dist/src/commands/rebase/confirm.js +33 -0
- package/dist/src/commands/rebase/continue.d.ts +7 -0
- package/dist/src/commands/rebase/continue.js +81 -0
- package/dist/src/commands/rebase/index.d.ts +22 -0
- package/dist/src/commands/rebase/index.js +127 -0
- package/dist/src/commands/rebase/patch-loop.d.ts +9 -0
- package/dist/src/commands/rebase/patch-loop.js +135 -0
- package/dist/src/commands/rebase/summary.d.ts +12 -0
- package/dist/src/commands/rebase/summary.js +43 -0
- package/dist/src/commands/rebase.d.ts +4 -0
- package/dist/src/commands/rebase.js +6 -0
- package/dist/src/commands/register.d.ts +13 -0
- package/dist/src/commands/register.js +67 -0
- package/dist/src/commands/reset.d.ts +11 -0
- package/dist/src/commands/reset.js +83 -0
- package/dist/src/commands/resolve.d.ts +9 -0
- package/dist/src/commands/resolve.js +124 -0
- package/dist/src/commands/run.d.ts +9 -0
- package/dist/src/commands/run.js +91 -0
- package/dist/src/commands/setup-support.d.ts +23 -0
- package/dist/src/commands/setup-support.js +310 -0
- package/dist/src/commands/setup.d.ts +11 -0
- package/dist/src/commands/setup.js +94 -0
- package/dist/src/commands/status.d.ts +11 -0
- package/dist/src/commands/status.js +268 -0
- package/dist/src/commands/test.d.ts +12 -0
- package/dist/src/commands/test.js +182 -0
- package/dist/src/commands/token-coverage.d.ts +5 -0
- package/dist/src/commands/token-coverage.js +57 -0
- package/dist/src/commands/token.d.ts +14 -0
- package/dist/src/commands/token.js +121 -0
- package/dist/src/commands/watch.d.ts +9 -0
- package/dist/src/commands/watch.js +112 -0
- package/dist/src/commands/wire.d.ts +13 -0
- package/dist/src/commands/wire.js +149 -0
- package/dist/src/core/ast-utils.d.ts +47 -0
- package/dist/src/core/ast-utils.js +57 -0
- package/dist/src/core/brand-validation.d.ts +7 -0
- package/dist/src/core/brand-validation.js +15 -0
- package/dist/src/core/branding.d.ts +49 -0
- package/dist/src/core/branding.js +229 -0
- package/dist/src/core/browser-wire.d.ts +40 -0
- package/dist/src/core/browser-wire.js +66 -0
- package/dist/src/core/build-prepare.d.ts +25 -0
- package/dist/src/core/build-prepare.js +93 -0
- package/dist/src/core/config-mutate.d.ts +15 -0
- package/dist/src/core/config-mutate.js +51 -0
- package/dist/src/core/config-paths.d.ts +28 -0
- package/dist/src/core/config-paths.js +65 -0
- package/dist/src/core/config-state.d.ts +28 -0
- package/dist/src/core/config-state.js +152 -0
- package/dist/src/core/config-validate.d.ts +11 -0
- package/dist/src/core/config-validate.js +141 -0
- package/dist/src/core/config.d.ts +39 -0
- package/dist/src/core/config.js +70 -0
- package/dist/src/core/file-lock.d.ts +11 -0
- package/dist/src/core/file-lock.js +80 -0
- package/dist/src/core/firefox-archive.d.ts +40 -0
- package/dist/src/core/firefox-archive.js +63 -0
- package/dist/src/core/firefox-cache.d.ts +23 -0
- package/dist/src/core/firefox-cache.js +134 -0
- package/dist/src/core/firefox-download.d.ts +21 -0
- package/dist/src/core/firefox-download.js +129 -0
- package/dist/src/core/firefox-extract.d.ts +21 -0
- package/dist/src/core/firefox-extract.js +53 -0
- package/dist/src/core/firefox.d.ts +34 -0
- package/dist/src/core/firefox.js +78 -0
- package/dist/src/core/furnace-apply-helpers.d.ts +21 -0
- package/dist/src/core/furnace-apply-helpers.js +244 -0
- package/dist/src/core/furnace-apply.d.ts +16 -0
- package/dist/src/core/furnace-apply.js +147 -0
- package/dist/src/core/furnace-config.d.ts +94 -0
- package/dist/src/core/furnace-config.js +372 -0
- package/dist/src/core/furnace-constants.d.ts +4 -0
- package/dist/src/core/furnace-constants.js +6 -0
- package/dist/src/core/furnace-registration-ast.d.ts +24 -0
- package/dist/src/core/furnace-registration-ast.js +218 -0
- package/dist/src/core/furnace-registration-remove.d.ts +14 -0
- package/dist/src/core/furnace-registration-remove.js +89 -0
- package/dist/src/core/furnace-registration-validate.d.ts +20 -0
- package/dist/src/core/furnace-registration-validate.js +40 -0
- package/dist/src/core/furnace-registration.d.ts +29 -0
- package/dist/src/core/furnace-registration.js +96 -0
- package/dist/src/core/furnace-rollback.d.ts +20 -0
- package/dist/src/core/furnace-rollback.js +66 -0
- package/dist/src/core/furnace-scanner.d.ts +40 -0
- package/dist/src/core/furnace-scanner.js +143 -0
- package/dist/src/core/furnace-stories.d.ts +37 -0
- package/dist/src/core/furnace-stories.js +185 -0
- package/dist/src/core/furnace-validate-accessibility.d.ts +6 -0
- package/dist/src/core/furnace-validate-accessibility.js +32 -0
- package/dist/src/core/furnace-validate-checks.d.ts +4 -0
- package/dist/src/core/furnace-validate-checks.js +7 -0
- package/dist/src/core/furnace-validate-compatibility.d.ts +6 -0
- package/dist/src/core/furnace-validate-compatibility.js +57 -0
- package/dist/src/core/furnace-validate-helpers.d.ts +28 -0
- package/dist/src/core/furnace-validate-helpers.js +129 -0
- package/dist/src/core/furnace-validate-registration.d.ts +37 -0
- package/dist/src/core/furnace-validate-registration.js +220 -0
- package/dist/src/core/furnace-validate-structure.d.ts +6 -0
- package/dist/src/core/furnace-validate-structure.js +66 -0
- package/dist/src/core/furnace-validate.d.ts +16 -0
- package/dist/src/core/furnace-validate.js +103 -0
- package/dist/src/core/git-base.d.ts +47 -0
- package/dist/src/core/git-base.js +50 -0
- package/dist/src/core/git-diff.d.ts +63 -0
- package/dist/src/core/git-diff.js +246 -0
- package/dist/src/core/git-file-ops.d.ts +65 -0
- package/dist/src/core/git-file-ops.js +141 -0
- package/dist/src/core/git-status.d.ts +65 -0
- package/dist/src/core/git-status.js +163 -0
- package/dist/src/core/git.d.ts +113 -0
- package/dist/src/core/git.js +363 -0
- package/dist/src/core/license-headers.d.ts +36 -0
- package/dist/src/core/license-headers.js +83 -0
- package/dist/src/core/mach-build-artifacts.d.ts +29 -0
- package/dist/src/core/mach-build-artifacts.js +117 -0
- package/dist/src/core/mach-mozconfig.d.ts +17 -0
- package/dist/src/core/mach-mozconfig.js +50 -0
- package/dist/src/core/mach-python.d.ts +16 -0
- package/dist/src/core/mach-python.js +126 -0
- package/dist/src/core/mach.d.ts +106 -0
- package/dist/src/core/mach.js +166 -0
- package/dist/src/core/manifest-helpers.d.ts +25 -0
- package/dist/src/core/manifest-helpers.js +96 -0
- package/dist/src/core/manifest-register.d.ts +30 -0
- package/dist/src/core/manifest-register.js +65 -0
- package/dist/src/core/manifest-rules.d.ts +39 -0
- package/dist/src/core/manifest-rules.js +151 -0
- package/dist/src/core/manifest-tokenizers.d.ts +34 -0
- package/dist/src/core/manifest-tokenizers.js +84 -0
- package/dist/src/core/parser-fallback.d.ts +36 -0
- package/dist/src/core/parser-fallback.js +43 -0
- package/dist/src/core/patch-apply-fuzz.d.ts +29 -0
- package/dist/src/core/patch-apply-fuzz.js +70 -0
- package/dist/src/core/patch-apply.d.ts +46 -0
- package/dist/src/core/patch-apply.js +235 -0
- package/dist/src/core/patch-export.d.ts +99 -0
- package/dist/src/core/patch-export.js +314 -0
- package/dist/src/core/patch-files.d.ts +11 -0
- package/dist/src/core/patch-files.js +51 -0
- package/dist/src/core/patch-lint.d.ts +72 -0
- package/dist/src/core/patch-lint.js +403 -0
- package/dist/src/core/patch-lock.d.ts +8 -0
- package/dist/src/core/patch-lock.js +29 -0
- package/dist/src/core/patch-manifest-consistency.d.ts +24 -0
- package/dist/src/core/patch-manifest-consistency.js +135 -0
- package/dist/src/core/patch-manifest-io.d.ts +36 -0
- package/dist/src/core/patch-manifest-io.js +77 -0
- package/dist/src/core/patch-manifest-query.d.ts +48 -0
- package/dist/src/core/patch-manifest-query.js +124 -0
- package/dist/src/core/patch-manifest-validate.d.ts +22 -0
- package/dist/src/core/patch-manifest-validate.js +72 -0
- package/dist/src/core/patch-manifest.d.ts +11 -0
- package/dist/src/core/patch-manifest.js +12 -0
- package/dist/src/core/patch-parse.d.ts +43 -0
- package/dist/src/core/patch-parse.js +143 -0
- package/dist/src/core/patch-transform.d.ts +21 -0
- package/dist/src/core/patch-transform.js +138 -0
- package/dist/src/core/rebase-session.d.ts +47 -0
- package/dist/src/core/rebase-session.js +65 -0
- package/dist/src/core/register-browser-content.d.ts +11 -0
- package/dist/src/core/register-browser-content.js +116 -0
- package/dist/src/core/register-module.d.ts +11 -0
- package/dist/src/core/register-module.js +76 -0
- package/dist/src/core/register-shared-css.d.ts +11 -0
- package/dist/src/core/register-shared-css.js +117 -0
- package/dist/src/core/register-test-manifest.d.ts +18 -0
- package/dist/src/core/register-test-manifest.js +99 -0
- package/dist/src/core/state-file.d.ts +4 -0
- package/dist/src/core/state-file.js +25 -0
- package/dist/src/core/token-coverage.d.ts +12 -0
- package/dist/src/core/token-coverage.js +74 -0
- package/dist/src/core/token-manager.d.ts +55 -0
- package/dist/src/core/token-manager.js +387 -0
- package/dist/src/core/wire-destroy.d.ts +21 -0
- package/dist/src/core/wire-destroy.js +103 -0
- package/dist/src/core/wire-dom-fragment.d.ts +23 -0
- package/dist/src/core/wire-dom-fragment.js +129 -0
- package/dist/src/core/wire-init.d.ts +23 -0
- package/dist/src/core/wire-init.js +201 -0
- package/dist/src/core/wire-subscript.d.ts +20 -0
- package/dist/src/core/wire-subscript.js +134 -0
- package/dist/src/core/wire-targets.d.ts +7 -0
- package/dist/src/core/wire-targets.js +9 -0
- package/dist/src/core/wire-utils.d.ts +88 -0
- package/dist/src/core/wire-utils.js +279 -0
- package/dist/src/errors/base.d.ts +60 -0
- package/dist/src/errors/base.js +87 -0
- package/dist/src/errors/build.d.ts +52 -0
- package/dist/src/errors/build.js +114 -0
- package/dist/src/errors/codes.d.ts +29 -0
- package/dist/src/errors/codes.js +30 -0
- package/dist/src/errors/config.d.ts +31 -0
- package/dist/src/errors/config.js +61 -0
- package/dist/src/errors/download.d.ts +42 -0
- package/dist/src/errors/download.js +95 -0
- package/dist/src/errors/furnace.d.ts +10 -0
- package/dist/src/errors/furnace.js +22 -0
- package/dist/src/errors/git.d.ts +41 -0
- package/dist/src/errors/git.js +99 -0
- package/dist/src/errors/patch.d.ts +10 -0
- package/dist/src/errors/patch.js +26 -0
- package/dist/src/errors/rebase.d.ts +20 -0
- package/dist/src/errors/rebase.js +30 -0
- package/dist/src/index.d.ts +21 -0
- package/dist/src/index.js +21 -0
- package/dist/src/types/cli.d.ts +14 -0
- package/dist/src/types/cli.js +2 -0
- package/dist/src/types/commands/index.d.ts +6 -0
- package/dist/src/types/commands/index.js +6 -0
- package/dist/src/types/commands/options.d.ts +239 -0
- package/dist/src/types/commands/options.js +6 -0
- package/dist/src/types/commands/patches.d.ts +89 -0
- package/dist/src/types/commands/patches.js +6 -0
- package/dist/src/types/commands/project.d.ts +71 -0
- package/dist/src/types/commands/project.js +6 -0
- package/dist/src/types/config.d.ts +101 -0
- package/dist/src/types/config.js +2 -0
- package/dist/src/types/furnace.d.ts +158 -0
- package/dist/src/types/furnace.js +2 -0
- package/dist/src/types/index.d.ts +6 -0
- package/dist/src/types/index.js +6 -0
- package/dist/src/utils/errors.d.ts +2 -0
- package/dist/src/utils/errors.js +15 -0
- package/dist/src/utils/fs.d.ts +72 -0
- package/dist/src/utils/fs.js +179 -0
- package/dist/src/utils/logger.d.ts +58 -0
- package/dist/src/utils/logger.js +120 -0
- package/dist/src/utils/options.d.ts +8 -0
- package/dist/src/utils/options.js +16 -0
- package/dist/src/utils/package-root.d.ts +10 -0
- package/dist/src/utils/package-root.js +53 -0
- package/dist/src/utils/parse.d.ts +110 -0
- package/dist/src/utils/parse.js +200 -0
- package/dist/src/utils/paths.d.ts +10 -0
- package/dist/src/utils/paths.js +43 -0
- package/dist/src/utils/platform.d.ts +38 -0
- package/dist/src/utils/platform.js +56 -0
- package/dist/src/utils/process.d.ts +80 -0
- package/dist/src/utils/process.js +188 -0
- package/dist/src/utils/regex.d.ts +24 -0
- package/dist/src/utils/regex.js +40 -0
- package/dist/src/utils/validation.d.ts +133 -0
- package/dist/src/utils/validation.js +250 -0
- package/package.json +106 -0
- package/templates/configs/common.mozconfig +24 -0
- package/templates/configs/darwin.mozconfig +10 -0
- package/templates/configs/linux.mozconfig +12 -0
- package/templates/configs/win32.mozconfig +14 -0
- package/templates/licenses/0BSD.md +14 -0
- package/templates/licenses/EUPL-1.2.md +294 -0
- package/templates/licenses/GPL-2.0-or-later.md +339 -0
- package/templates/licenses/MPL-2.0.md +383 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Project state file management (.fireforge/state.json).
|
|
4
|
+
*/
|
|
5
|
+
import { ConfigError } from '../errors/config.js';
|
|
6
|
+
import { toError } from '../utils/errors.js';
|
|
7
|
+
import { pathExists, readJson, writeJson } from '../utils/fs.js';
|
|
8
|
+
import { warn } from '../utils/logger.js';
|
|
9
|
+
import { isObject, isString } from '../utils/validation.js';
|
|
10
|
+
import { getProjectPaths } from './config-paths.js';
|
|
11
|
+
import { quarantineStateFile, withStateFileLock } from './state-file.js';
|
|
12
|
+
function sanitizeProjectState(data) {
|
|
13
|
+
if (!isObject(data)) {
|
|
14
|
+
return {
|
|
15
|
+
state: {},
|
|
16
|
+
issues: ['the root value must be a JSON object'],
|
|
17
|
+
recoveredFields: [],
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
const state = {};
|
|
21
|
+
const issues = [];
|
|
22
|
+
const recoveredFields = [];
|
|
23
|
+
const stringFields = [
|
|
24
|
+
'brand',
|
|
25
|
+
'buildMode',
|
|
26
|
+
'lastBuild',
|
|
27
|
+
'downloadedVersion',
|
|
28
|
+
'baseCommit',
|
|
29
|
+
];
|
|
30
|
+
for (const key of stringFields) {
|
|
31
|
+
const value = data[key];
|
|
32
|
+
if (value === undefined) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (!isString(value)) {
|
|
36
|
+
issues.push(`field "${key}" must be a string`);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (key === 'buildMode' && !['dev', 'debug', 'release'].includes(value)) {
|
|
40
|
+
issues.push('field "buildMode" must be one of: dev, debug, release');
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (key === 'buildMode') {
|
|
44
|
+
state.buildMode = value;
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
state[key] = value;
|
|
48
|
+
}
|
|
49
|
+
recoveredFields.push(key);
|
|
50
|
+
}
|
|
51
|
+
const pendingResolution = data['pendingResolution'];
|
|
52
|
+
if (pendingResolution !== undefined) {
|
|
53
|
+
if (isObject(pendingResolution) &&
|
|
54
|
+
isString(pendingResolution['patchFilename']) &&
|
|
55
|
+
isString(pendingResolution['originalError'])) {
|
|
56
|
+
state.pendingResolution = {
|
|
57
|
+
patchFilename: pendingResolution['patchFilename'],
|
|
58
|
+
originalError: pendingResolution['originalError'],
|
|
59
|
+
};
|
|
60
|
+
recoveredFields.push('pendingResolution');
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
issues.push('field "pendingResolution" must be an object with string fields "patchFilename" and "originalError"');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { state, issues, recoveredFields };
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Validates a parsed project state object and returns a typed FireForgeState.
|
|
70
|
+
* @param data - Parsed JSON state data
|
|
71
|
+
* @returns Validated FireForgeState
|
|
72
|
+
*/
|
|
73
|
+
export function validateFireForgeState(data) {
|
|
74
|
+
const result = sanitizeProjectState(data);
|
|
75
|
+
if (result.issues.length > 0) {
|
|
76
|
+
throw new ConfigError(`Invalid FireForge state: ${result.issues.join('; ')}`);
|
|
77
|
+
}
|
|
78
|
+
return result.state;
|
|
79
|
+
}
|
|
80
|
+
async function recoverInvalidProjectState(statePath, result, alreadyLocked = false) {
|
|
81
|
+
const recover = async () => {
|
|
82
|
+
const quarantinedFile = await quarantineStateFile(statePath);
|
|
83
|
+
if (result.recoveredFields.length > 0) {
|
|
84
|
+
await writeJson(statePath, result.state);
|
|
85
|
+
}
|
|
86
|
+
const quarantineMessage = quarantinedFile
|
|
87
|
+
? ` Quarantined the original file as ${quarantinedFile}.`
|
|
88
|
+
: '';
|
|
89
|
+
const recoveryMessage = result.recoveredFields.length > 0
|
|
90
|
+
? ` Recovered valid field${result.recoveredFields.length === 1 ? '' : 's'}: ${result.recoveredFields.join(', ')}.`
|
|
91
|
+
: ' No valid state fields could be recovered; using defaults.';
|
|
92
|
+
warn(`State file (.fireforge/state.json) was invalid: ${result.issues.join('; ')}.${recoveryMessage}${quarantineMessage} ` +
|
|
93
|
+
'Run "fireforge doctor" to check project health.');
|
|
94
|
+
return result.state;
|
|
95
|
+
};
|
|
96
|
+
return alreadyLocked ? recover() : withStateFileLock(statePath, recover);
|
|
97
|
+
}
|
|
98
|
+
async function loadStateFromPath(statePath, alreadyLocked = false) {
|
|
99
|
+
if (!(await pathExists(statePath))) {
|
|
100
|
+
return {};
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
const data = await readJson(statePath);
|
|
104
|
+
const result = sanitizeProjectState(data);
|
|
105
|
+
if (result.issues.length === 0) {
|
|
106
|
+
return result.state;
|
|
107
|
+
}
|
|
108
|
+
return await recoverInvalidProjectState(statePath, result, alreadyLocked);
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
return await recoverInvalidProjectState(statePath, {
|
|
112
|
+
state: {},
|
|
113
|
+
issues: [`the file could not be parsed: ${toError(error).message}`],
|
|
114
|
+
recoveredFields: [],
|
|
115
|
+
}, alreadyLocked);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Loads the fireforge state, or returns defaults if it doesn't exist.
|
|
120
|
+
* @param root - Root directory of the project
|
|
121
|
+
* @returns FireForge state
|
|
122
|
+
*/
|
|
123
|
+
export async function loadState(root) {
|
|
124
|
+
const paths = getProjectPaths(root);
|
|
125
|
+
return loadStateFromPath(paths.state);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Saves the fireforge state.
|
|
129
|
+
* @param root - Root directory of the project
|
|
130
|
+
* @param state - State to save
|
|
131
|
+
*/
|
|
132
|
+
export async function saveState(root, state) {
|
|
133
|
+
const paths = getProjectPaths(root);
|
|
134
|
+
const validatedState = validateFireForgeState(state);
|
|
135
|
+
await withStateFileLock(paths.state, async () => {
|
|
136
|
+
await writeJson(paths.state, validatedState);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Updates specific fields in the fireforge state.
|
|
141
|
+
* @param root - Root directory of the project
|
|
142
|
+
* @param updates - Fields to update, or a transactional updater function
|
|
143
|
+
*/
|
|
144
|
+
export async function updateState(root, updates) {
|
|
145
|
+
const paths = getProjectPaths(root);
|
|
146
|
+
await withStateFileLock(paths.state, async () => {
|
|
147
|
+
const current = await loadStateFromPath(paths.state, true);
|
|
148
|
+
const nextState = typeof updates === 'function' ? updates(current) : { ...current, ...updates };
|
|
149
|
+
await writeJson(paths.state, validateFireForgeState(nextState));
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=config-state.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config schema validation for fireforge.json.
|
|
3
|
+
*/
|
|
4
|
+
import type { FireForgeConfig } from '../types/config.js';
|
|
5
|
+
/**
|
|
6
|
+
* Validates a raw config object and returns a typed FireForgeConfig.
|
|
7
|
+
* @param data - Raw data to validate
|
|
8
|
+
* @returns Validated FireForgeConfig
|
|
9
|
+
* @throws Error if validation fails
|
|
10
|
+
*/
|
|
11
|
+
export declare function validateConfig(data: unknown): FireForgeConfig;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Config schema validation for fireforge.json.
|
|
4
|
+
*/
|
|
5
|
+
import { ConfigError } from '../errors/config.js';
|
|
6
|
+
import { verbose } from '../utils/logger.js';
|
|
7
|
+
import { parseObject } from '../utils/parse.js';
|
|
8
|
+
import { isContainedRelativePath } from '../utils/paths.js';
|
|
9
|
+
import { isValidAppId, isValidFirefoxVersion, isValidProjectLicense, PROJECT_LICENSES, validateFirefoxProductVersionCompatibility, } from '../utils/validation.js';
|
|
10
|
+
import { SUPPORTED_CONFIG_ROOT_KEYS } from './config-paths.js';
|
|
11
|
+
/**
|
|
12
|
+
* Validates a raw config object and returns a typed FireForgeConfig.
|
|
13
|
+
* @param data - Raw data to validate
|
|
14
|
+
* @returns Validated FireForgeConfig
|
|
15
|
+
* @throws Error if validation fails
|
|
16
|
+
*/
|
|
17
|
+
export function validateConfig(data) {
|
|
18
|
+
let rec;
|
|
19
|
+
try {
|
|
20
|
+
rec = parseObject(data, 'Config');
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
throw new ConfigError('Config must be an object');
|
|
24
|
+
}
|
|
25
|
+
// Required string fields
|
|
26
|
+
const name = requireConfigString(rec, 'name');
|
|
27
|
+
const vendor = requireConfigString(rec, 'vendor');
|
|
28
|
+
const appId = requireConfigString(rec, 'appId');
|
|
29
|
+
const binaryName = requireConfigString(rec, 'binaryName');
|
|
30
|
+
if (binaryName.includes('..') || binaryName.includes('/') || binaryName.includes('\\')) {
|
|
31
|
+
throw new ConfigError('Config field "binaryName" must not contain path separators or ".."');
|
|
32
|
+
}
|
|
33
|
+
if (!isValidAppId(appId)) {
|
|
34
|
+
throw new ConfigError('Config field "appId" must be a valid reverse-domain identifier (e.g., "org.example.browser")');
|
|
35
|
+
}
|
|
36
|
+
// Firefox config
|
|
37
|
+
let firefoxRec;
|
|
38
|
+
try {
|
|
39
|
+
firefoxRec = rec.object('firefox');
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
throw new ConfigError('Config field "firefox" must be an object');
|
|
43
|
+
}
|
|
44
|
+
const firefoxVersion = requireConfigString(firefoxRec, 'version', 'firefox.version');
|
|
45
|
+
if (!isValidFirefoxVersion(firefoxVersion)) {
|
|
46
|
+
throw new ConfigError('Config field "firefox.version" must be a valid Firefox version (e.g., "145.0")');
|
|
47
|
+
}
|
|
48
|
+
const firefoxProduct = requireConfigString(firefoxRec, 'product', 'firefox.product');
|
|
49
|
+
const validProducts = ['firefox', 'firefox-esr', 'firefox-beta'];
|
|
50
|
+
if (!validProducts.includes(firefoxProduct)) {
|
|
51
|
+
throw new ConfigError(`Config field "firefox.product" must be one of: ${validProducts.join(', ')}`);
|
|
52
|
+
}
|
|
53
|
+
// Cross-field validation: product and version must be compatible
|
|
54
|
+
const compatError = validateFirefoxProductVersionCompatibility(firefoxVersion, firefoxProduct);
|
|
55
|
+
if (compatError) {
|
|
56
|
+
throw new ConfigError(compatError);
|
|
57
|
+
}
|
|
58
|
+
// Optional configs
|
|
59
|
+
const config = {
|
|
60
|
+
name,
|
|
61
|
+
vendor,
|
|
62
|
+
appId,
|
|
63
|
+
binaryName,
|
|
64
|
+
firefox: {
|
|
65
|
+
version: firefoxVersion,
|
|
66
|
+
product: firefoxProduct,
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
// Build
|
|
70
|
+
const buildRec = optionalConfigObject(rec, 'build');
|
|
71
|
+
if (buildRec) {
|
|
72
|
+
config.build = {};
|
|
73
|
+
const jobs = buildRec.raw('jobs');
|
|
74
|
+
if (jobs !== undefined) {
|
|
75
|
+
if (typeof jobs !== 'number' || !Number.isInteger(jobs) || jobs <= 0) {
|
|
76
|
+
throw new ConfigError('Config field "build.jobs" must be a positive integer');
|
|
77
|
+
}
|
|
78
|
+
config.build.jobs = jobs;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Wire
|
|
82
|
+
const wireRec = optionalConfigObject(rec, 'wire');
|
|
83
|
+
if (wireRec) {
|
|
84
|
+
config.wire = {};
|
|
85
|
+
const subscriptDir = optionalConfigString(wireRec, 'subscriptDir', 'wire.subscriptDir');
|
|
86
|
+
if (subscriptDir !== undefined) {
|
|
87
|
+
if (!isContainedRelativePath(subscriptDir)) {
|
|
88
|
+
throw new ConfigError('Config field "wire.subscriptDir" must stay within engine/');
|
|
89
|
+
}
|
|
90
|
+
config.wire.subscriptDir = subscriptDir;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// License
|
|
94
|
+
const licenseRaw = rec.raw('license');
|
|
95
|
+
if (licenseRaw !== undefined) {
|
|
96
|
+
if (typeof licenseRaw !== 'string') {
|
|
97
|
+
throw new ConfigError('Config field "license" must be a string');
|
|
98
|
+
}
|
|
99
|
+
if (!isValidProjectLicense(licenseRaw)) {
|
|
100
|
+
throw new ConfigError(`Config field "license" must be one of: ${PROJECT_LICENSES.join(', ')}`);
|
|
101
|
+
}
|
|
102
|
+
config.license = licenseRaw;
|
|
103
|
+
}
|
|
104
|
+
// Warn on unknown root keys
|
|
105
|
+
const knownRootKeys = new Set(SUPPORTED_CONFIG_ROOT_KEYS);
|
|
106
|
+
for (const key of rec.keys()) {
|
|
107
|
+
if (!knownRootKeys.has(key)) {
|
|
108
|
+
verbose(`Unknown config key "${key}" in fireforge.json — it will be ignored.`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return config;
|
|
112
|
+
}
|
|
113
|
+
// ── Internal helpers (wrap parseObject errors with ConfigError) ──
|
|
114
|
+
function requireConfigString(rec, key, label) {
|
|
115
|
+
const value = rec.raw(key);
|
|
116
|
+
if (typeof value !== 'string') {
|
|
117
|
+
throw new ConfigError(`Config field "${label ?? key}" must be a string`);
|
|
118
|
+
}
|
|
119
|
+
return value;
|
|
120
|
+
}
|
|
121
|
+
function optionalConfigString(rec, key, label) {
|
|
122
|
+
const value = rec.raw(key);
|
|
123
|
+
if (value === undefined)
|
|
124
|
+
return undefined;
|
|
125
|
+
if (typeof value !== 'string') {
|
|
126
|
+
throw new ConfigError(`Config field "${label}" must be a string`);
|
|
127
|
+
}
|
|
128
|
+
return value;
|
|
129
|
+
}
|
|
130
|
+
function optionalConfigObject(rec, key) {
|
|
131
|
+
const value = rec.raw(key);
|
|
132
|
+
if (value === undefined)
|
|
133
|
+
return undefined;
|
|
134
|
+
try {
|
|
135
|
+
return rec.object(key);
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
throw new ConfigError(`Config field "${key}" must be an object`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=config-validate.js.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project configuration — barrel module.
|
|
3
|
+
*
|
|
4
|
+
* Re-exports from focused sub-modules:
|
|
5
|
+
* config-paths.ts — constants and project path derivation
|
|
6
|
+
* config-validate.ts — fireforge.json schema validation
|
|
7
|
+
* config-mutate.ts — immutable config mutation
|
|
8
|
+
* config-state.ts — state file management
|
|
9
|
+
*/
|
|
10
|
+
import type { FireForgeConfig } from '../types/config.js';
|
|
11
|
+
export { mutateConfig } from './config-mutate.js';
|
|
12
|
+
export { CONFIG_FILENAME, CONFIGS_DIR, ENGINE_DIR, FIREFORGE_DIR, getProjectPaths, PATCHES_DIR, SRC_DIR, STATE_FILENAME, SUPPORTED_CONFIG_PATHS, SUPPORTED_CONFIG_ROOT_KEYS, } from './config-paths.js';
|
|
13
|
+
export { loadState, saveState, updateState, validateFireForgeState } from './config-state.js';
|
|
14
|
+
export { validateConfig } from './config-validate.js';
|
|
15
|
+
/**
|
|
16
|
+
* Checks if a fireforge.json exists in the given directory.
|
|
17
|
+
* @param root - Root directory to check
|
|
18
|
+
* @returns True if fireforge.json exists
|
|
19
|
+
*/
|
|
20
|
+
export declare function configExists(root: string): Promise<boolean>;
|
|
21
|
+
/**
|
|
22
|
+
* Loads and validates the fireforge.json configuration.
|
|
23
|
+
* @param root - Root directory of the project
|
|
24
|
+
* @returns Validated FireForgeConfig
|
|
25
|
+
* @throws Error if config doesn't exist or is invalid
|
|
26
|
+
*/
|
|
27
|
+
export declare function loadConfig(root: string): Promise<FireForgeConfig>;
|
|
28
|
+
/**
|
|
29
|
+
* Writes a configuration to fireforge.json.
|
|
30
|
+
* @param root - Root directory of the project
|
|
31
|
+
* @param config - Configuration to write
|
|
32
|
+
*/
|
|
33
|
+
export declare function writeConfig(root: string, config: FireForgeConfig): Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Writes a raw config document to fireforge.json.
|
|
36
|
+
* This is used by CLI `config --force`, where callers may intentionally write
|
|
37
|
+
* keys or value shapes outside the validated FireForgeConfig schema.
|
|
38
|
+
*/
|
|
39
|
+
export declare function writeConfigDocument(root: string, config: FireForgeConfig | Record<string, unknown>): Promise<void>;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Project configuration — barrel module.
|
|
4
|
+
*
|
|
5
|
+
* Re-exports from focused sub-modules:
|
|
6
|
+
* config-paths.ts — constants and project path derivation
|
|
7
|
+
* config-validate.ts — fireforge.json schema validation
|
|
8
|
+
* config-mutate.ts — immutable config mutation
|
|
9
|
+
* config-state.ts — state file management
|
|
10
|
+
*/
|
|
11
|
+
import { ConfigError, ConfigNotFoundError } from '../errors/config.js';
|
|
12
|
+
import { toError } from '../utils/errors.js';
|
|
13
|
+
import { pathExists, readJson, writeJson } from '../utils/fs.js';
|
|
14
|
+
import { getProjectPaths } from './config-paths.js';
|
|
15
|
+
import { validateConfig } from './config-validate.js';
|
|
16
|
+
// ---- re-exports ----
|
|
17
|
+
export { mutateConfig } from './config-mutate.js';
|
|
18
|
+
export { CONFIG_FILENAME, CONFIGS_DIR, ENGINE_DIR, FIREFORGE_DIR, getProjectPaths, PATCHES_DIR, SRC_DIR, STATE_FILENAME, SUPPORTED_CONFIG_PATHS, SUPPORTED_CONFIG_ROOT_KEYS, } from './config-paths.js';
|
|
19
|
+
export { loadState, saveState, updateState, validateFireForgeState } from './config-state.js';
|
|
20
|
+
export { validateConfig } from './config-validate.js';
|
|
21
|
+
// ---- config I/O (stays here because it bridges paths + validation) ----
|
|
22
|
+
/**
|
|
23
|
+
* Checks if a fireforge.json exists in the given directory.
|
|
24
|
+
* @param root - Root directory to check
|
|
25
|
+
* @returns True if fireforge.json exists
|
|
26
|
+
*/
|
|
27
|
+
export async function configExists(root) {
|
|
28
|
+
const paths = getProjectPaths(root);
|
|
29
|
+
return pathExists(paths.config);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Loads and validates the fireforge.json configuration.
|
|
33
|
+
* @param root - Root directory of the project
|
|
34
|
+
* @returns Validated FireForgeConfig
|
|
35
|
+
* @throws Error if config doesn't exist or is invalid
|
|
36
|
+
*/
|
|
37
|
+
export async function loadConfig(root) {
|
|
38
|
+
const paths = getProjectPaths(root);
|
|
39
|
+
if (!(await pathExists(paths.config))) {
|
|
40
|
+
throw new ConfigNotFoundError(paths.config);
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
const data = await readJson(paths.config);
|
|
44
|
+
return validateConfig(data);
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
if (error instanceof ConfigError) {
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
throw new ConfigError(`Invalid fireforge.json at ${paths.config}: ${toError(error).message}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Writes a configuration to fireforge.json.
|
|
55
|
+
* @param root - Root directory of the project
|
|
56
|
+
* @param config - Configuration to write
|
|
57
|
+
*/
|
|
58
|
+
export async function writeConfig(root, config) {
|
|
59
|
+
await writeConfigDocument(root, config);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Writes a raw config document to fireforge.json.
|
|
63
|
+
* This is used by CLI `config --force`, where callers may intentionally write
|
|
64
|
+
* keys or value shapes outside the validated FireForgeConfig schema.
|
|
65
|
+
*/
|
|
66
|
+
export async function writeConfigDocument(root, config) {
|
|
67
|
+
const paths = getProjectPaths(root);
|
|
68
|
+
await writeJson(paths.config, config);
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface FileLockOptions {
|
|
2
|
+
timeoutMs?: number;
|
|
3
|
+
pollMs?: number;
|
|
4
|
+
staleMs?: number;
|
|
5
|
+
onTimeoutMessage?: string;
|
|
6
|
+
onStaleLockMessage?: (ageMs: number) => string | undefined;
|
|
7
|
+
}
|
|
8
|
+
/** Derives the sibling lock-directory path used to guard a file-based resource. */
|
|
9
|
+
export declare function createSiblingLockPath(filePath: string, suffix?: string): string;
|
|
10
|
+
/** Runs an async operation while holding a directory lock, with stale-lock recovery. */
|
|
11
|
+
export declare function withFileLock<T>(lockPath: string, operation: () => Promise<T>, options?: FileLockOptions): Promise<T>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
import { mkdir, rm, stat } from 'node:fs/promises';
|
|
3
|
+
import { dirname } from 'node:path';
|
|
4
|
+
import { toError } from '../utils/errors.js';
|
|
5
|
+
import { ensureDir } from '../utils/fs.js';
|
|
6
|
+
import { verbose, warn } from '../utils/logger.js';
|
|
7
|
+
const DEFAULT_LOCK_TIMEOUT_MS = 30_000;
|
|
8
|
+
const DEFAULT_LOCK_POLL_MS = 50;
|
|
9
|
+
const DEFAULT_STALE_LOCK_MS = 5 * 60_000;
|
|
10
|
+
function sleep(ms) {
|
|
11
|
+
return new Promise((resolve) => {
|
|
12
|
+
setTimeout(resolve, ms);
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
/** Derives the sibling lock-directory path used to guard a file-based resource. */
|
|
16
|
+
export function createSiblingLockPath(filePath, suffix = '.fireforge.lock') {
|
|
17
|
+
return `${filePath}${suffix}`;
|
|
18
|
+
}
|
|
19
|
+
async function removeIfStaleLock(lockPath, staleMs, onStaleLockMessage) {
|
|
20
|
+
try {
|
|
21
|
+
const lockStat = await stat(lockPath);
|
|
22
|
+
const ageMs = Date.now() - lockStat.mtimeMs;
|
|
23
|
+
if (ageMs <= staleMs) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
const staleMessage = onStaleLockMessage?.(ageMs);
|
|
27
|
+
if (staleMessage) {
|
|
28
|
+
warn(staleMessage);
|
|
29
|
+
}
|
|
30
|
+
await rm(lockPath, { recursive: true, force: true });
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
verbose(`Stale lock check failed for ${lockPath}: ${toError(error).message}`);
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Runs an async operation while holding a directory lock, with stale-lock recovery. */
|
|
39
|
+
export async function withFileLock(lockPath, operation, options = {}) {
|
|
40
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_LOCK_TIMEOUT_MS;
|
|
41
|
+
const pollMs = options.pollMs ?? DEFAULT_LOCK_POLL_MS;
|
|
42
|
+
const staleMs = options.staleMs ?? DEFAULT_STALE_LOCK_MS;
|
|
43
|
+
const deadline = Date.now() + timeoutMs;
|
|
44
|
+
let attemptedStaleRecovery = false;
|
|
45
|
+
await ensureDir(dirname(lockPath));
|
|
46
|
+
for (;;) {
|
|
47
|
+
try {
|
|
48
|
+
await mkdir(lockPath);
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
const isAlreadyLocked = typeof error === 'object' &&
|
|
53
|
+
error !== null &&
|
|
54
|
+
'code' in error &&
|
|
55
|
+
typeof error.code === 'string' &&
|
|
56
|
+
error.code === 'EEXIST';
|
|
57
|
+
if (!isAlreadyLocked) {
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
if (!attemptedStaleRecovery) {
|
|
61
|
+
attemptedStaleRecovery = true;
|
|
62
|
+
if (await removeIfStaleLock(lockPath, staleMs, options.onStaleLockMessage)) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (Date.now() >= deadline) {
|
|
67
|
+
throw new Error(options.onTimeoutMessage ??
|
|
68
|
+
`Timed out waiting for file lock ${lockPath}. Remove the lock directory if it is stale.`, { cause: error });
|
|
69
|
+
}
|
|
70
|
+
await sleep(pollMs);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
return await operation();
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
await rm(lockPath, { recursive: true, force: true });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
//# sourceMappingURL=file-lock.js.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Archive metadata validation and archive identity resolution.
|
|
3
|
+
*/
|
|
4
|
+
import type { FirefoxProduct } from '../types/config.js';
|
|
5
|
+
/**
|
|
6
|
+
* Resolved archive descriptor for URL generation and cache storage.
|
|
7
|
+
*/
|
|
8
|
+
export interface ResolvedArchive {
|
|
9
|
+
requestedVersion: string;
|
|
10
|
+
product: FirefoxProduct;
|
|
11
|
+
archiveVersion: string;
|
|
12
|
+
url: string;
|
|
13
|
+
filename: string;
|
|
14
|
+
metadataFilename: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Sidecar metadata stored alongside a cached archive.
|
|
18
|
+
*/
|
|
19
|
+
export interface ArchiveMetadata {
|
|
20
|
+
requestedVersion: string;
|
|
21
|
+
product: FirefoxProduct;
|
|
22
|
+
archiveVersion: string;
|
|
23
|
+
url: string;
|
|
24
|
+
contentLength?: number | undefined;
|
|
25
|
+
sha256?: string | undefined;
|
|
26
|
+
downloadedAt: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Validates raw JSON data as ArchiveMetadata.
|
|
30
|
+
* @param data - Unknown data to validate
|
|
31
|
+
* @returns Validated ArchiveMetadata
|
|
32
|
+
*/
|
|
33
|
+
export declare function validateArchiveMetadata(data: unknown): ArchiveMetadata;
|
|
34
|
+
/**
|
|
35
|
+
* Resolves archive identity for URL generation and cache storage.
|
|
36
|
+
* @param version - Requested Firefox version
|
|
37
|
+
* @param product - Firefox product type
|
|
38
|
+
* @returns Resolved archive descriptor
|
|
39
|
+
*/
|
|
40
|
+
export declare function resolveArchive(version: string, product?: FirefoxProduct): ResolvedArchive;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Archive metadata validation and archive identity resolution.
|
|
4
|
+
*/
|
|
5
|
+
import { ConfigError } from '../errors/config.js';
|
|
6
|
+
import { parseObject } from '../utils/parse.js';
|
|
7
|
+
import { isValidFirefoxProduct } from '../utils/validation.js';
|
|
8
|
+
/**
|
|
9
|
+
* Base URL for Firefox releases on archive.mozilla.org.
|
|
10
|
+
*/
|
|
11
|
+
const ARCHIVE_BASE_URL = 'https://archive.mozilla.org/pub/firefox/releases';
|
|
12
|
+
/**
|
|
13
|
+
* Validates raw JSON data as ArchiveMetadata.
|
|
14
|
+
* @param data - Unknown data to validate
|
|
15
|
+
* @returns Validated ArchiveMetadata
|
|
16
|
+
*/
|
|
17
|
+
export function validateArchiveMetadata(data) {
|
|
18
|
+
const rec = parseObject(data, 'Archive metadata');
|
|
19
|
+
const requestedVersion = rec.string('requestedVersion');
|
|
20
|
+
const product = rec.stringEnum('product', (v) => isValidFirefoxProduct(v), 'a supported Firefox product');
|
|
21
|
+
const archiveVersion = rec.string('archiveVersion');
|
|
22
|
+
const url = rec.string('url');
|
|
23
|
+
const downloadedAt = rec.string('downloadedAt');
|
|
24
|
+
const contentLength = rec.optionalNonNegativeInteger('contentLength');
|
|
25
|
+
const sha256 = rec.optionalString('sha256');
|
|
26
|
+
return {
|
|
27
|
+
requestedVersion,
|
|
28
|
+
product,
|
|
29
|
+
archiveVersion,
|
|
30
|
+
url,
|
|
31
|
+
downloadedAt,
|
|
32
|
+
...(contentLength !== undefined ? { contentLength } : {}),
|
|
33
|
+
...(sha256 !== undefined ? { sha256 } : {}),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Resolves archive identity for URL generation and cache storage.
|
|
38
|
+
* @param version - Requested Firefox version
|
|
39
|
+
* @param product - Firefox product type
|
|
40
|
+
* @returns Resolved archive descriptor
|
|
41
|
+
*/
|
|
42
|
+
export function resolveArchive(version, product = 'firefox') {
|
|
43
|
+
// Reject versions containing path traversal characters
|
|
44
|
+
if (version.includes('/') || version.includes('..') || version.includes('\\')) {
|
|
45
|
+
throw new ConfigError(`Invalid Firefox version "${version}": contains disallowed characters`, 'firefox.version');
|
|
46
|
+
}
|
|
47
|
+
// ESR status is determined solely by the product field. Config validation
|
|
48
|
+
// ensures product and version are consistent, so we never need to infer
|
|
49
|
+
// ESR from the version string independently.
|
|
50
|
+
const cleanVersion = version.replace(/esr$/i, '');
|
|
51
|
+
const isEsr = product === 'firefox-esr';
|
|
52
|
+
const archiveVersion = isEsr ? `${cleanVersion}esr` : cleanVersion;
|
|
53
|
+
const safeProduct = product.replace(/[^a-z0-9-]/gi, '-');
|
|
54
|
+
return {
|
|
55
|
+
requestedVersion: version,
|
|
56
|
+
product,
|
|
57
|
+
archiveVersion,
|
|
58
|
+
url: `${ARCHIVE_BASE_URL}/${archiveVersion}/source/firefox-${archiveVersion}.source.tar.xz`,
|
|
59
|
+
filename: `firefox-${safeProduct}-${archiveVersion}.source.tar.xz`,
|
|
60
|
+
metadataFilename: `firefox-${safeProduct}-${archiveVersion}.source.tar.xz.json`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=firefox-archive.js.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache validation, invalidation, and download-to-cache logic.
|
|
3
|
+
*/
|
|
4
|
+
import type { ResolvedArchive } from './firefox-archive.js';
|
|
5
|
+
import type { ProgressCallback } from './firefox-download.js';
|
|
6
|
+
/**
|
|
7
|
+
* Computes the SHA-256 hex digest of a file.
|
|
8
|
+
* @param filePath - Path to the file
|
|
9
|
+
*/
|
|
10
|
+
export declare function sha256File(filePath: string): Promise<string>;
|
|
11
|
+
/**
|
|
12
|
+
* Ensures a valid cached archive exists, downloading it if needed.
|
|
13
|
+
* @param archive - Resolved archive descriptor
|
|
14
|
+
* @param cacheDir - Cache directory
|
|
15
|
+
* @param onProgress - Optional progress callback
|
|
16
|
+
*/
|
|
17
|
+
export declare function ensureCachedArchive(archive: ResolvedArchive, cacheDir: string, onProgress?: ProgressCallback): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* Removes cached tarball, metadata, and partial download files for an archive.
|
|
20
|
+
* @param archive - Resolved archive descriptor
|
|
21
|
+
* @param cacheDir - Cache directory
|
|
22
|
+
*/
|
|
23
|
+
export declare function invalidateArchiveCache(archive: ResolvedArchive, cacheDir: string): Promise<void>;
|