@hominis/fireforge 0.9.0 → 0.10.1
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 +27 -1
- package/README.md +11 -5
- package/dist/src/commands/build.js +4 -1
- package/dist/src/commands/re-export.js +2 -10
- package/dist/src/core/file-lock.js +15 -1
- package/dist/src/core/furnace-validate-accessibility.js +3 -3
- package/dist/src/core/furnace-validate-helpers.d.ts +2 -0
- package/dist/src/core/furnace-validate-helpers.js +4 -0
- package/dist/src/core/git-diff.js +29 -16
- package/dist/src/core/git-file-ops.js +8 -4
- package/dist/src/core/git-status.js +16 -27
- package/dist/src/core/patch-lint.d.ts +4 -2
- package/dist/src/core/patch-lint.js +13 -7
- package/dist/src/utils/package-root.d.ts +10 -2
- package/dist/src/utils/package-root.js +12 -4
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,7 +1,33 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.10.0
|
|
4
|
+
|
|
5
|
+
### Patch workflow validation
|
|
6
|
+
|
|
7
|
+
- Re-export now runs the same patch lint gate as export and export-all before writing patch files or manifest metadata.
|
|
8
|
+
- `re-export --skip-lint` now downgrades lint errors to warnings consistently, while default re-export blocks on lint errors and keeps artifacts unchanged.
|
|
9
|
+
- Raw CSS colors introduced by a patch are now patch lint errors, matching Furnace validation, without blocking on unrelated pre-existing upstream raw colors.
|
|
10
|
+
- Furnace accessibility validation now warns about missing ARIA roles only for generic interactive markup, so native semantic elements are not pushed toward redundant ARIA.
|
|
11
|
+
|
|
12
|
+
### General improvements
|
|
13
|
+
|
|
14
|
+
- getPackageRoot up to this point expected hardcoded `@hominis/fireforge`, was changed to just the package name for potential forks and more flexibility when changing project name.
|
|
15
|
+
- Some test generators were derived from early Hominis Browser fork additions, the references to Hominis have been replaced with generic naming.
|
|
16
|
+
|
|
17
|
+
### Build and Git reliability
|
|
18
|
+
|
|
19
|
+
- Build preflight now fails clearly when multiple build artifact directories make the target ambiguous.
|
|
20
|
+
- Git diff and status helpers now surface command failures instead of silently treating failed commands as empty output.
|
|
21
|
+
- Stale lock cleanup now distinguishes disappearance races from real cleanup failures.
|
|
22
|
+
|
|
23
|
+
### Packaging
|
|
24
|
+
|
|
25
|
+
- Package metadata and smoke tests now use version `0.10.0`.
|
|
26
|
+
- npm install instructions use the scoped `@hominis/fireforge` package name.
|
|
27
|
+
- Packaging and full Firefox integration helpers now handle platform-specific npm and mozconfig names more consistently.
|
|
28
|
+
|
|
3
29
|
## 0.9.0
|
|
4
30
|
|
|
5
31
|
### npm release
|
|
6
32
|
|
|
7
|
-
- Package is now installable via `npm install fireforge` or `npm install -g fireforge`.
|
|
33
|
+
- Package is now installable via `npm install @hominis/fireforge` or `npm install -g @hominis/fireforge`.
|
package/README.md
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
# FireForge
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@hominis/fireforge)
|
|
4
|
+
[](LICENSE.md)
|
|
5
|
+
[](package.json)
|
|
6
|
+
[](https://www.npmjs.com/package/@hominis/fireforge)
|
|
7
|
+
[](https://www.npmjs.com/package/@hominis/fireforge)
|
|
8
|
+
|
|
3
9
|
**Build and maintain your own Firefox-based browser with a patch-first workflow.**
|
|
4
10
|
|
|
5
11
|
FireForge gives you a toolkit for forking Firefox: download a specific ESR release, manage your customisations as an ordered stack of contextual patches, survive version upgrades with semi-automated rebase, wire custom code into Mozilla's startup paths, and build the result. It also ships **Furnace**, a component system for creating and overriding Firefox custom elements.
|
|
6
12
|
|
|
7
|
-
Inspired by [fern.js](https://github.com/
|
|
13
|
+
Inspired by [fern.js](https://github.com/ghostery/user-agent-desktop) and [Melon](https://github.com/dothq/melon).
|
|
8
14
|
|
|
9
15
|
---
|
|
10
16
|
|
|
@@ -13,7 +19,7 @@ Inspired by [fern.js](https://github.com/nicktrosper/user-agent-desktop?tab=read
|
|
|
13
19
|
```bash
|
|
14
20
|
mkdir mybrowser && cd mybrowser
|
|
15
21
|
npm init -y
|
|
16
|
-
npm install --save-dev fireforge
|
|
22
|
+
npm install --save-dev @hominis/fireforge
|
|
17
23
|
|
|
18
24
|
npx fireforge setup # interactive project init
|
|
19
25
|
npx fireforge download # fetch Firefox source (~1 GB)
|
|
@@ -165,14 +171,14 @@ If the manifest drifts after an interrupted export or manual edits, `fireforge i
|
|
|
165
171
|
<details>
|
|
166
172
|
<summary>Patch lint checks</summary>
|
|
167
173
|
|
|
168
|
-
`fireforge lint` runs automatically during export. Use `--skip-lint` to downgrade errors to warnings, though I would recommend against making that a habit.
|
|
174
|
+
`fireforge lint` runs automatically during export, export-all, and re-export. Use `--skip-lint` to downgrade errors to warnings, though I would recommend against making that a habit.
|
|
169
175
|
|
|
170
176
|
| Check | Scope | Severity |
|
|
171
177
|
| ------------------------------ | ------------------------------- | -------- |
|
|
172
178
|
| `missing-license-header` | New files (JS/CSS/FTL) | error |
|
|
173
179
|
| `relative-import` | JS/MJS files | error |
|
|
174
180
|
| `token-prefix-violation` | CSS files (with furnace) | error |
|
|
175
|
-
| `raw-color-value` | CSS
|
|
181
|
+
| `raw-color-value` | Introduced CSS color values | error |
|
|
176
182
|
| `missing-modification-comment` | Modified upstream JS/MJS | warning |
|
|
177
183
|
| `file-too-large` | New files >650 lines | warning |
|
|
178
184
|
| `missing-jsdoc` | Exports in new `.sys.mjs` | warning |
|
|
@@ -259,7 +265,7 @@ Furnace validates components on deploy. Errors block apply; warnings are advisor
|
|
|
259
265
|
| `missing-css` | warning | No `.css` file |
|
|
260
266
|
| `filename-mismatch` | error | File name does not match tag name |
|
|
261
267
|
| `missing-override-json` | error | Override missing `override.json` |
|
|
262
|
-
| `no-aria-role` | warning |
|
|
268
|
+
| `no-aria-role` | warning | Generic interactive markup lacks semantics |
|
|
263
269
|
| `no-keyboard-handler` | warning | Has `@click` but no keyboard handler |
|
|
264
270
|
| `relative-import` | error | Imports must use `chrome://` URIs |
|
|
265
271
|
| `raw-color-value` | error | Raw hex/rgb/hsl (use CSS custom properties) |
|
|
@@ -5,7 +5,7 @@ import { prepareBuildEnvironment } from '../core/build-prepare.js';
|
|
|
5
5
|
import { getProjectPaths, loadConfig } from '../core/config.js';
|
|
6
6
|
import { build, buildArtifactMismatchMessage, buildUI, hasBuildArtifacts } from '../core/mach.js';
|
|
7
7
|
import { GeneralError } from '../errors/base.js';
|
|
8
|
-
import { BuildError } from '../errors/build.js';
|
|
8
|
+
import { AmbiguousBuildArtifactsError, BuildError } from '../errors/build.js';
|
|
9
9
|
import { pathExists } from '../utils/fs.js';
|
|
10
10
|
import { error, info, intro, outro, verbose } from '../utils/logger.js';
|
|
11
11
|
import { pickDefined } from '../utils/options.js';
|
|
@@ -45,6 +45,9 @@ export async function buildCommand(projectRoot, options) {
|
|
|
45
45
|
throw new GeneralError('Firefox source not found. Run "fireforge download" first.');
|
|
46
46
|
}
|
|
47
47
|
const buildCheck = await hasBuildArtifacts(paths.engine);
|
|
48
|
+
if (buildCheck.ambiguous && buildCheck.objDirs && buildCheck.objDirs.length > 0) {
|
|
49
|
+
throw new AmbiguousBuildArtifactsError(buildCheck.objDirs);
|
|
50
|
+
}
|
|
48
51
|
const mismatchMessage = buildArtifactMismatchMessage(paths.engine, buildCheck, 'Build');
|
|
49
52
|
if (mismatchMessage) {
|
|
50
53
|
throw new GeneralError(mismatchMessage);
|
|
@@ -6,13 +6,13 @@ import { isGitRepository } from '../core/git.js';
|
|
|
6
6
|
import { getDiffForFilesAgainstHead } from '../core/git-diff.js';
|
|
7
7
|
import { getModifiedFilesInDir, getUntrackedFilesInDir } from '../core/git-status.js';
|
|
8
8
|
import { updatePatch, updatePatchMetadata } from '../core/patch-export.js';
|
|
9
|
-
import { lintExportedPatch } from '../core/patch-lint.js';
|
|
10
9
|
import { getClaimedFiles, loadPatchesManifest } from '../core/patch-manifest.js';
|
|
11
10
|
import { GeneralError, InvalidArgumentError } from '../errors/base.js';
|
|
12
11
|
import { toError } from '../utils/errors.js';
|
|
13
12
|
import { pathExists } from '../utils/fs.js';
|
|
14
13
|
import { cancel, info, intro, isCancel, outro, spinner, success, warn } from '../utils/logger.js';
|
|
15
14
|
import { pickDefined } from '../utils/options.js';
|
|
15
|
+
import { runPatchLint } from './export-shared.js';
|
|
16
16
|
/**
|
|
17
17
|
* Resolves patch identifiers (numbers or filenames) to manifest entries.
|
|
18
18
|
* @param identifier - Patch number (e.g. "005") or filename (e.g. "005-ui-storage-modules.patch")
|
|
@@ -96,6 +96,7 @@ async function reExportSinglePatch(patch, paths, manifest, options, isDryRun, co
|
|
|
96
96
|
warn(`Skipped ${patch.filename}: no changes (files unchanged from HEAD)`);
|
|
97
97
|
return false;
|
|
98
98
|
}
|
|
99
|
+
await runPatchLint(paths.engine, existingFiles, diffContent, config, options.skipLint);
|
|
99
100
|
if (isDryRun) {
|
|
100
101
|
info(`[dry-run] ${patch.filename}: ${existingFiles.length} file(s)`);
|
|
101
102
|
}
|
|
@@ -115,15 +116,6 @@ async function reExportSinglePatch(patch, paths, manifest, options, isDryRun, co
|
|
|
115
116
|
};
|
|
116
117
|
}
|
|
117
118
|
}
|
|
118
|
-
const lintIssues = await lintExportedPatch(paths.engine, existingFiles, diffContent, config);
|
|
119
|
-
for (const issue of lintIssues) {
|
|
120
|
-
const prefix = issue.severity === 'error' && !options.skipLint ? 'ERROR ' : '';
|
|
121
|
-
warn(`${prefix}[${issue.check}] ${issue.file}: ${issue.message}`);
|
|
122
|
-
}
|
|
123
|
-
const lintErrors = lintIssues.filter((i) => i.severity === 'error');
|
|
124
|
-
if (lintErrors.length > 0 && !options.skipLint) {
|
|
125
|
-
warn(`${patch.filename}: ${lintErrors.length} lint error(s). Use --skip-lint to bypass.`);
|
|
126
|
-
}
|
|
127
119
|
success(`Re-exported ${patch.filename}`);
|
|
128
120
|
}
|
|
129
121
|
return true;
|
|
@@ -12,6 +12,15 @@ function sleep(ms) {
|
|
|
12
12
|
setTimeout(resolve, ms);
|
|
13
13
|
});
|
|
14
14
|
}
|
|
15
|
+
function getNodeErrorCode(error) {
|
|
16
|
+
if (typeof error === 'object' &&
|
|
17
|
+
error !== null &&
|
|
18
|
+
'code' in error &&
|
|
19
|
+
typeof error.code === 'string') {
|
|
20
|
+
return error.code;
|
|
21
|
+
}
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
15
24
|
/** Derives the sibling lock-directory path used to guard a file-based resource. */
|
|
16
25
|
export function createSiblingLockPath(filePath, suffix = '.fireforge.lock') {
|
|
17
26
|
return `${filePath}${suffix}`;
|
|
@@ -31,8 +40,13 @@ async function removeIfStaleLock(lockPath, staleMs, onStaleLockMessage) {
|
|
|
31
40
|
return true;
|
|
32
41
|
}
|
|
33
42
|
catch (error) {
|
|
43
|
+
const code = getNodeErrorCode(error);
|
|
44
|
+
if (code === 'ENOENT') {
|
|
45
|
+
verbose(`Stale lock disappeared before cleanup completed: ${lockPath}`);
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
34
48
|
verbose(`Stale lock check failed for ${lockPath}: ${toError(error).message}`);
|
|
35
|
-
|
|
49
|
+
throw toError(error);
|
|
36
50
|
}
|
|
37
51
|
}
|
|
38
52
|
/** Runs an async operation while holding a directory lock, with stale-lock recovery. */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { pathExists, readText } from '../utils/fs.js';
|
|
4
|
-
import { containsHardcodedTemplateText, createIssue, hasAriaRole, hasDelegatesFocusEnabled, hasTemplateClickHandler, hasTemplateKeyboardHandler, } from './furnace-validate-helpers.js';
|
|
4
|
+
import { containsHardcodedTemplateText, createIssue, hasAriaRole, hasDelegatesFocusEnabled, hasGenericInteractiveElement, hasTemplateClickHandler, hasTemplateKeyboardHandler, } from './furnace-validate-helpers.js';
|
|
5
5
|
/**
|
|
6
6
|
* Validates accessibility patterns in a component's .mjs file.
|
|
7
7
|
* Checks for ARIA roles, keyboard handlers, l10n, and focus delegation.
|
|
@@ -12,8 +12,8 @@ export async function validateAccessibility(componentDir, tagName) {
|
|
|
12
12
|
return [];
|
|
13
13
|
const content = await readText(mjsPath);
|
|
14
14
|
const issues = [];
|
|
15
|
-
if (!hasAriaRole(content)) {
|
|
16
|
-
issues.push(createIssue(tagName, 'warning', 'no-aria-role', '
|
|
15
|
+
if (!hasAriaRole(content) && hasGenericInteractiveElement(content)) {
|
|
16
|
+
issues.push(createIssue(tagName, 'warning', 'no-aria-role', 'Generic interactive markup has no native semantics. Prefer native elements, or add role= when native markup cannot provide the semantics.'));
|
|
17
17
|
}
|
|
18
18
|
const hasClick = hasTemplateClickHandler(content);
|
|
19
19
|
const hasKeyboardHandler = hasTemplateKeyboardHandler(content);
|
|
@@ -3,6 +3,8 @@ import type { ComponentType, FurnaceConfig, ValidationIssue } from '../types/fur
|
|
|
3
3
|
export declare function createIssue(component: string, severity: ValidationIssue['severity'], check: ValidationIssue['check'], message: string): ValidationIssue;
|
|
4
4
|
/** Detects whether template or script content assigns an ARIA role. */
|
|
5
5
|
export declare function hasAriaRole(content: string): boolean;
|
|
6
|
+
/** Detects generic elements being used as custom interactive controls. */
|
|
7
|
+
export declare function hasGenericInteractiveElement(content: string): boolean;
|
|
6
8
|
/** Detects Lit-style template click handlers. */
|
|
7
9
|
export declare function hasTemplateClickHandler(content: string): boolean;
|
|
8
10
|
/** Detects Lit-style template keyboard handlers. */
|
|
@@ -12,6 +12,10 @@ export function hasAriaRole(content) {
|
|
|
12
12
|
/\.role\s*=/.test(content) ||
|
|
13
13
|
/setAttribute\(\s*["']role["']/.test(content));
|
|
14
14
|
}
|
|
15
|
+
/** Detects generic elements being used as custom interactive controls. */
|
|
16
|
+
export function hasGenericInteractiveElement(content) {
|
|
17
|
+
return /<(div|span)\b(?=[^>]*(?:@click|@key(?:down|press|up)|\btabindex\s*=|\.onclick\s*=))/i.test(content);
|
|
18
|
+
}
|
|
15
19
|
/** Detects Lit-style template click handlers. */
|
|
16
20
|
export function hasTemplateClickHandler(content) {
|
|
17
21
|
return /@click\s*=\s*\$\{/.test(content);
|
|
@@ -2,13 +2,21 @@
|
|
|
2
2
|
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { tmpdir } from 'node:os';
|
|
4
4
|
import { basename, join } from 'node:path';
|
|
5
|
+
import { GitError } from '../errors/git.js';
|
|
5
6
|
import { toError } from '../utils/errors.js';
|
|
6
7
|
import { pathExists, readText } from '../utils/fs.js';
|
|
7
8
|
import { verbose } from '../utils/logger.js';
|
|
8
9
|
import { exec } from '../utils/process.js';
|
|
9
|
-
import { ensureGit } from './git-base.js';
|
|
10
|
+
import { ensureGit, git } from './git-base.js';
|
|
10
11
|
import { fileExistsInHead } from './git-file-ops.js';
|
|
11
12
|
import { getUntrackedFiles } from './git-status.js';
|
|
13
|
+
async function execGitWithAllowedExitCodes(repoDir, args, allowedExitCodes = [0]) {
|
|
14
|
+
const result = await exec('git', args, { cwd: repoDir });
|
|
15
|
+
if (allowedExitCodes.includes(result.exitCode)) {
|
|
16
|
+
return result;
|
|
17
|
+
}
|
|
18
|
+
throw new GitError(result.stderr.trim() || 'Git command failed', args.join(' '));
|
|
19
|
+
}
|
|
12
20
|
/**
|
|
13
21
|
* Gets the diff for a specific file.
|
|
14
22
|
* @param repoDir - Repository directory
|
|
@@ -17,8 +25,7 @@ import { getUntrackedFiles } from './git-status.js';
|
|
|
17
25
|
*/
|
|
18
26
|
export async function getFileDiff(repoDir, filePath) {
|
|
19
27
|
await ensureGit();
|
|
20
|
-
|
|
21
|
-
return result.stdout;
|
|
28
|
+
return git(['diff', 'HEAD', '--', filePath], repoDir);
|
|
22
29
|
}
|
|
23
30
|
/**
|
|
24
31
|
* Generates a unified diff for a new (untracked) file.
|
|
@@ -32,8 +39,7 @@ export async function generateNewFileDiff(repoDir, filePath) {
|
|
|
32
39
|
// Compute the abbreviated git blob hash for the index line
|
|
33
40
|
let blobHash = '0000000000';
|
|
34
41
|
try {
|
|
35
|
-
const
|
|
36
|
-
const fullHash = hashResult.stdout.trim();
|
|
42
|
+
const fullHash = (await git(['hash-object', fullPath], repoDir)).trim();
|
|
37
43
|
if (fullHash.length >= 10) {
|
|
38
44
|
blobHash = fullHash.slice(0, 10);
|
|
39
45
|
}
|
|
@@ -113,9 +119,7 @@ export async function generateModificationDiff(repoDir, filePath, baseContent) {
|
|
|
113
119
|
try {
|
|
114
120
|
await writeFile(tempFile, baseContent);
|
|
115
121
|
// git diff --no-index exits code 1 when files differ — that's normal
|
|
116
|
-
const result = await
|
|
117
|
-
cwd: repoDir,
|
|
118
|
-
});
|
|
122
|
+
const result = await execGitWithAllowedExitCodes(repoDir, ['diff', '--no-index', '--', tempFile, fullPath], [0, 1]);
|
|
119
123
|
const output = result.stdout;
|
|
120
124
|
if (!output) {
|
|
121
125
|
return '';
|
|
@@ -154,8 +158,7 @@ export async function generateModificationDiff(repoDir, filePath, baseContent) {
|
|
|
154
158
|
export async function getAllDiff(repoDir) {
|
|
155
159
|
await ensureGit();
|
|
156
160
|
// Get diff for tracked files
|
|
157
|
-
const
|
|
158
|
-
const trackedDiff = result.stdout;
|
|
161
|
+
const trackedDiff = await git(['diff', 'HEAD'], repoDir);
|
|
159
162
|
// Get untracked files (properly expanded, not directories)
|
|
160
163
|
const untrackedFiles = await getUntrackedFiles(repoDir);
|
|
161
164
|
// Generate diffs for untracked files
|
|
@@ -215,8 +218,7 @@ export async function getDiffForFilesAgainstHead(repoDir, files) {
|
|
|
215
218
|
*/
|
|
216
219
|
export async function getStagedDiffForFiles(repoDir, files) {
|
|
217
220
|
await ensureGit();
|
|
218
|
-
|
|
219
|
-
return result.stdout;
|
|
221
|
+
return git(['diff', '--cached', 'HEAD', '--', ...files], repoDir);
|
|
220
222
|
}
|
|
221
223
|
/**
|
|
222
224
|
* Generates a GIT binary patch for a binary file.
|
|
@@ -229,18 +231,29 @@ export async function getStagedDiffForFiles(repoDir, files) {
|
|
|
229
231
|
export async function generateBinaryFilePatch(repoDir, filePath) {
|
|
230
232
|
await ensureGit();
|
|
231
233
|
// Try tracked file diff first
|
|
232
|
-
const result = await
|
|
234
|
+
const result = await execGitWithAllowedExitCodes(repoDir, [
|
|
235
|
+
'diff',
|
|
236
|
+
'--binary',
|
|
237
|
+
'HEAD',
|
|
238
|
+
'--',
|
|
239
|
+
filePath,
|
|
240
|
+
]);
|
|
233
241
|
if (result.stdout.trim())
|
|
234
242
|
return result.stdout;
|
|
235
243
|
// For untracked files, stage temporarily to produce a binary diff
|
|
236
244
|
try {
|
|
237
|
-
await
|
|
238
|
-
const diffResult = await
|
|
245
|
+
await execGitWithAllowedExitCodes(repoDir, ['add', '--intent-to-add', '--', filePath]);
|
|
246
|
+
const diffResult = await execGitWithAllowedExitCodes(repoDir, [
|
|
247
|
+
'diff',
|
|
248
|
+
'--binary',
|
|
249
|
+
'--',
|
|
250
|
+
filePath,
|
|
251
|
+
]);
|
|
239
252
|
return diffResult.stdout;
|
|
240
253
|
}
|
|
241
254
|
finally {
|
|
242
255
|
// Always unstage, even if diff fails
|
|
243
|
-
await
|
|
256
|
+
await execGitWithAllowedExitCodes(repoDir, ['reset', 'HEAD', '--', filePath]);
|
|
244
257
|
}
|
|
245
258
|
}
|
|
246
259
|
//# sourceMappingURL=git-diff.js.map
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// SPDX-License-Identifier: EUPL-1.2
|
|
2
2
|
import { open } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
+
import { GitError } from '../errors/git.js';
|
|
4
5
|
import { removeFile } from '../utils/fs.js';
|
|
5
6
|
import { exec } from '../utils/process.js';
|
|
6
7
|
import { ensureGit, git } from './git-base.js';
|
|
@@ -37,7 +38,7 @@ export async function removeUntrackedPath(repoDir, filePath) {
|
|
|
37
38
|
*/
|
|
38
39
|
export async function removeAddedPath(repoDir, filePath) {
|
|
39
40
|
await ensureGit();
|
|
40
|
-
await
|
|
41
|
+
await git(['reset', 'HEAD', '--', filePath], repoDir);
|
|
41
42
|
await removeUntrackedPath(repoDir, filePath);
|
|
42
43
|
}
|
|
43
44
|
/**
|
|
@@ -92,8 +93,7 @@ export async function unstageFiles(repoDir, files) {
|
|
|
92
93
|
*/
|
|
93
94
|
export async function fileExistsInHead(repoDir, filePath) {
|
|
94
95
|
await ensureGit();
|
|
95
|
-
|
|
96
|
-
return result.stdout.trim().length > 0;
|
|
96
|
+
return (await git(['ls-tree', 'HEAD', '--', filePath], repoDir)).trim().length > 0;
|
|
97
97
|
}
|
|
98
98
|
/**
|
|
99
99
|
* Gets the content of a file from HEAD commit.
|
|
@@ -105,7 +105,11 @@ export async function getFileContentFromHead(repoDir, filePath) {
|
|
|
105
105
|
await ensureGit();
|
|
106
106
|
const result = await exec('git', ['show', `HEAD:${filePath}`], { cwd: repoDir });
|
|
107
107
|
if (result.exitCode !== 0) {
|
|
108
|
-
|
|
108
|
+
const stderr = result.stderr.trim();
|
|
109
|
+
if (/exists on disk, but not in 'HEAD'|path '.*' exists, but not '.*'|path '.*' does not exist in 'HEAD'/i.test(stderr)) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
throw new GitError(stderr || 'Git command failed', `show HEAD:${filePath}`);
|
|
109
113
|
}
|
|
110
114
|
return result.stdout;
|
|
111
115
|
}
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
import { exec } from '../utils/process.js';
|
|
3
|
-
import { ensureGit } from './git-base.js';
|
|
1
|
+
import { ensureGit, git } from './git-base.js';
|
|
4
2
|
/**
|
|
5
3
|
* Parses NUL-delimited porcelain status output.
|
|
6
4
|
* @param output - Raw git status output
|
|
@@ -43,8 +41,7 @@ export function parsePorcelainStatus(output) {
|
|
|
43
41
|
*/
|
|
44
42
|
export async function getWorkingTreeStatus(repoDir) {
|
|
45
43
|
await ensureGit();
|
|
46
|
-
|
|
47
|
-
return parsePorcelainStatus(result.stdout);
|
|
44
|
+
return parsePorcelainStatus(await git(['status', '--porcelain=v1', '-z'], repoDir));
|
|
48
45
|
}
|
|
49
46
|
/**
|
|
50
47
|
* Expands collapsed untracked directory entries into individual file entries.
|
|
@@ -92,10 +89,8 @@ export async function getModifiedFiles(repoDir) {
|
|
|
92
89
|
export async function getUntrackedFiles(repoDir) {
|
|
93
90
|
await ensureGit();
|
|
94
91
|
// Use git ls-files to get all untracked files, which properly expands directories
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
});
|
|
98
|
-
return result.stdout.split('\n').filter((line) => line.trim().length > 0);
|
|
92
|
+
const output = await git(['ls-files', '--others', '--exclude-standard'], repoDir);
|
|
93
|
+
return output.split('\n').filter((line) => line.trim().length > 0);
|
|
99
94
|
}
|
|
100
95
|
/**
|
|
101
96
|
* Gets untracked files within a specific directory.
|
|
@@ -106,10 +101,8 @@ export async function getUntrackedFiles(repoDir) {
|
|
|
106
101
|
*/
|
|
107
102
|
export async function getUntrackedFilesInDir(repoDir, dir) {
|
|
108
103
|
await ensureGit();
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
});
|
|
112
|
-
return result.stdout.split('\n').filter((line) => line.trim().length > 0);
|
|
104
|
+
const output = await git(['ls-files', '--others', '--exclude-standard', '--', dir], repoDir);
|
|
105
|
+
return output.split('\n').filter((line) => line.trim().length > 0);
|
|
113
106
|
}
|
|
114
107
|
/**
|
|
115
108
|
* Gets modified (tracked) files within a specific directory.
|
|
@@ -120,8 +113,8 @@ export async function getUntrackedFilesInDir(repoDir, dir) {
|
|
|
120
113
|
*/
|
|
121
114
|
export async function getModifiedFilesInDir(repoDir, dir) {
|
|
122
115
|
await ensureGit();
|
|
123
|
-
const
|
|
124
|
-
return
|
|
116
|
+
const output = await git(['diff', '--name-only', 'HEAD', '--', dir], repoDir);
|
|
117
|
+
return output.split('\n').filter((line) => line.trim().length > 0);
|
|
125
118
|
}
|
|
126
119
|
/**
|
|
127
120
|
* Checks if any of the specified files have uncommitted changes.
|
|
@@ -134,13 +127,11 @@ export async function getDirtyFiles(repoDir, files) {
|
|
|
134
127
|
return [];
|
|
135
128
|
await ensureGit();
|
|
136
129
|
// Check both staged and unstaged changes for the given files
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
});
|
|
140
|
-
const tracked = result.stdout.split('\n').filter((line) => line.trim().length > 0);
|
|
130
|
+
const trackedOutput = await git(['diff', '--name-only', 'HEAD', '--', ...files], repoDir);
|
|
131
|
+
const tracked = trackedOutput.split('\n').filter((line) => line.trim().length > 0);
|
|
141
132
|
// Also check for untracked files
|
|
142
|
-
const
|
|
143
|
-
const untracked =
|
|
133
|
+
const untrackedOutput = await git(['ls-files', '--others', '--exclude-standard', '--', ...files], repoDir);
|
|
134
|
+
const untracked = untrackedOutput.split('\n').filter((line) => line.trim().length > 0);
|
|
144
135
|
return [...new Set([...tracked, ...untracked])].sort();
|
|
145
136
|
}
|
|
146
137
|
/**
|
|
@@ -152,12 +143,10 @@ export async function getDirtyFiles(repoDir, files) {
|
|
|
152
143
|
*/
|
|
153
144
|
export async function listAllFilesInDir(repoDir, dir) {
|
|
154
145
|
await ensureGit();
|
|
155
|
-
const
|
|
156
|
-
const trackedFiles =
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
});
|
|
160
|
-
const untrackedFiles = untracked.stdout.split('\n').filter((line) => line.trim().length > 0);
|
|
146
|
+
const trackedOutput = await git(['ls-files', '--', dir], repoDir);
|
|
147
|
+
const trackedFiles = trackedOutput.split('\n').filter((line) => line.trim().length > 0);
|
|
148
|
+
const untrackedOutput = await git(['ls-files', '--others', '--exclude-standard', '--', dir], repoDir);
|
|
149
|
+
const untrackedFiles = untrackedOutput.split('\n').filter((line) => line.trim().length > 0);
|
|
161
150
|
return [...new Set([...trackedFiles, ...untrackedFiles])].sort();
|
|
162
151
|
}
|
|
163
152
|
//# sourceMappingURL=git-status.js.map
|
|
@@ -10,13 +10,15 @@ export declare function commentStyleForFile(file: string): CommentStyle | null;
|
|
|
10
10
|
*/
|
|
11
11
|
export declare function detectNewFilesInDiff(diffContent: string): Set<string>;
|
|
12
12
|
/**
|
|
13
|
-
* Lints patched CSS files for raw color values and non-tokenized
|
|
13
|
+
* Lints patched CSS files for introduced raw color values and non-tokenized
|
|
14
|
+
* custom properties.
|
|
14
15
|
*
|
|
15
16
|
* @param repoDir - Absolute path to the engine (repository) directory
|
|
16
17
|
* @param affectedFiles - File paths (relative to repoDir) affected by the patch
|
|
18
|
+
* @param diffContent - Optional unified diff used to scope raw color checks to introduced lines
|
|
17
19
|
* @returns Array of lint issues found
|
|
18
20
|
*/
|
|
19
|
-
export declare function lintPatchedCss(repoDir: string, affectedFiles: string[]): Promise<PatchLintIssue[]>;
|
|
21
|
+
export declare function lintPatchedCss(repoDir: string, affectedFiles: string[], diffContent?: string): Promise<PatchLintIssue[]>;
|
|
20
22
|
/**
|
|
21
23
|
* Checks new files for required license headers.
|
|
22
24
|
*
|
|
@@ -80,16 +80,18 @@ function extractAddedLinesPerFile(diffContent) {
|
|
|
80
80
|
return result;
|
|
81
81
|
}
|
|
82
82
|
// ---------------------------------------------------------------------------
|
|
83
|
-
// CSS lint
|
|
83
|
+
// CSS lint
|
|
84
84
|
// ---------------------------------------------------------------------------
|
|
85
85
|
/**
|
|
86
|
-
* Lints patched CSS files for raw color values and non-tokenized
|
|
86
|
+
* Lints patched CSS files for introduced raw color values and non-tokenized
|
|
87
|
+
* custom properties.
|
|
87
88
|
*
|
|
88
89
|
* @param repoDir - Absolute path to the engine (repository) directory
|
|
89
90
|
* @param affectedFiles - File paths (relative to repoDir) affected by the patch
|
|
91
|
+
* @param diffContent - Optional unified diff used to scope raw color checks to introduced lines
|
|
90
92
|
* @returns Array of lint issues found
|
|
91
93
|
*/
|
|
92
|
-
export async function lintPatchedCss(repoDir, affectedFiles) {
|
|
94
|
+
export async function lintPatchedCss(repoDir, affectedFiles, diffContent) {
|
|
93
95
|
const cssFiles = affectedFiles.filter((f) => f.endsWith('.css'));
|
|
94
96
|
if (cssFiles.length === 0)
|
|
95
97
|
return [];
|
|
@@ -108,6 +110,7 @@ export async function lintPatchedCss(repoDir, affectedFiles) {
|
|
|
108
110
|
verbose(`Skipping furnace token-prefix lint hints because furnace.json could not be loaded: ${toError(error).message}`);
|
|
109
111
|
}
|
|
110
112
|
const issues = [];
|
|
113
|
+
const addedLinesByFile = diffContent ? extractAddedLinesPerFile(diffContent) : undefined;
|
|
111
114
|
for (const file of cssFiles) {
|
|
112
115
|
const filePath = join(repoDir, file);
|
|
113
116
|
if (!(await pathExists(filePath)))
|
|
@@ -115,13 +118,16 @@ export async function lintPatchedCss(repoDir, affectedFiles) {
|
|
|
115
118
|
const rawCss = await readText(filePath);
|
|
116
119
|
// Strip block comments before scanning
|
|
117
120
|
const cssContent = rawCss.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
118
|
-
|
|
119
|
-
|
|
121
|
+
const rawColorContent = addedLinesByFile
|
|
122
|
+
? (addedLinesByFile.get(file) ?? []).join('\n').replace(/\/\*[\s\S]*?\*\//g, '')
|
|
123
|
+
: cssContent;
|
|
124
|
+
// Check only introduced raw color values when diff context is available.
|
|
125
|
+
if (hasRawCssColors(rawColorContent)) {
|
|
120
126
|
issues.push({
|
|
121
127
|
file,
|
|
122
128
|
check: 'raw-color-value',
|
|
123
129
|
message: 'Raw color value found. Use CSS custom properties (var(--...)) for design token consistency.',
|
|
124
|
-
severity: '
|
|
130
|
+
severity: 'error',
|
|
125
131
|
});
|
|
126
132
|
}
|
|
127
133
|
// Check for non-tokenized custom properties
|
|
@@ -384,7 +390,7 @@ export async function lintExportedPatch(repoDir, affectedFiles, diffContent, con
|
|
|
384
390
|
const newFiles = detectNewFilesInDiff(diffContent);
|
|
385
391
|
const lineCount = diffContent.split('\n').length;
|
|
386
392
|
const [cssIssues, headerIssues, jsIssues, modifiedHeaderIssues] = await Promise.all([
|
|
387
|
-
lintPatchedCss(repoDir, affectedFiles),
|
|
393
|
+
lintPatchedCss(repoDir, affectedFiles, diffContent),
|
|
388
394
|
lintNewFileHeaders(repoDir, [...newFiles], config),
|
|
389
395
|
lintPatchedJs(repoDir, affectedFiles, newFiles, config),
|
|
390
396
|
lintModifiedFileHeaders(repoDir, affectedFiles, newFiles),
|
|
@@ -1,10 +1,18 @@
|
|
|
1
|
+
interface PackageMetadata {
|
|
2
|
+
name: string;
|
|
3
|
+
version: string;
|
|
4
|
+
bin?: unknown;
|
|
5
|
+
}
|
|
1
6
|
/**
|
|
2
7
|
* Finds the fireforge package root by walking up from the current module.
|
|
3
8
|
*
|
|
4
9
|
* Works from both the source tree (`src/utils/`) and the compiled
|
|
5
|
-
* tree (`dist/src/utils/`) by looking for a `package.json`
|
|
6
|
-
* `
|
|
10
|
+
* tree (`dist/src/utils/`) by looking for a `package.json` that exposes
|
|
11
|
+
* the `fireforge` CLI entrypoint, regardless of the npm package scope.
|
|
7
12
|
*/
|
|
8
13
|
export declare function getPackageRoot(): string;
|
|
14
|
+
/** @internal */
|
|
15
|
+
export declare function isFireForgePackageMetadata(pkg: PackageMetadata): boolean;
|
|
9
16
|
/** Reads the current package version from the repository root package manifest. */
|
|
10
17
|
export declare function getPackageVersion(): string;
|
|
18
|
+
export {};
|
|
@@ -11,7 +11,7 @@ function validatePackageMetadata(data, filePath) {
|
|
|
11
11
|
if (typeof name !== 'string' || typeof version !== 'string') {
|
|
12
12
|
throw new Error(`Invalid package metadata in ${filePath}: expected string "name" and "version" fields`);
|
|
13
13
|
}
|
|
14
|
-
return { name, version };
|
|
14
|
+
return { name, version, bin: 'bin' in data ? data.bin : undefined };
|
|
15
15
|
}
|
|
16
16
|
function readPackageMetadata(filePath) {
|
|
17
17
|
const raw = readFileSync(filePath, 'utf-8');
|
|
@@ -21,8 +21,8 @@ function readPackageMetadata(filePath) {
|
|
|
21
21
|
* Finds the fireforge package root by walking up from the current module.
|
|
22
22
|
*
|
|
23
23
|
* Works from both the source tree (`src/utils/`) and the compiled
|
|
24
|
-
* tree (`dist/src/utils/`) by looking for a `package.json`
|
|
25
|
-
* `
|
|
24
|
+
* tree (`dist/src/utils/`) by looking for a `package.json` that exposes
|
|
25
|
+
* the `fireforge` CLI entrypoint, regardless of the npm package scope.
|
|
26
26
|
*/
|
|
27
27
|
export function getPackageRoot() {
|
|
28
28
|
let current = dirname(fileURLToPath(import.meta.url));
|
|
@@ -30,7 +30,7 @@ export function getPackageRoot() {
|
|
|
30
30
|
try {
|
|
31
31
|
const packagePath = join(current, 'package.json');
|
|
32
32
|
const pkg = readPackageMetadata(packagePath);
|
|
33
|
-
if (pkg
|
|
33
|
+
if (isFireForgePackageMetadata(pkg)) {
|
|
34
34
|
return current;
|
|
35
35
|
}
|
|
36
36
|
}
|
|
@@ -45,6 +45,14 @@ export function getPackageRoot() {
|
|
|
45
45
|
current = parent;
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
|
+
/** @internal */
|
|
49
|
+
export function isFireForgePackageMetadata(pkg) {
|
|
50
|
+
if (typeof pkg.bin !== 'object' || pkg.bin === null || Array.isArray(pkg.bin)) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
const bin = pkg.bin;
|
|
54
|
+
return typeof bin['fireforge'] === 'string';
|
|
55
|
+
}
|
|
48
56
|
/** Reads the current package version from the repository root package manifest. */
|
|
49
57
|
export function getPackageVersion() {
|
|
50
58
|
const packageRoot = getPackageRoot();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hominis/fireforge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"description": "FireForge — a build tool for customizing Firefox",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/src/index.js",
|
|
@@ -85,11 +85,11 @@
|
|
|
85
85
|
"license": "EUPL-1.2",
|
|
86
86
|
"repository": {
|
|
87
87
|
"type": "git",
|
|
88
|
-
"url": "https://github.com/
|
|
88
|
+
"url": "https://github.com/HominisBrowser/FireForge.git"
|
|
89
89
|
},
|
|
90
|
-
"homepage": "https://github.com/
|
|
90
|
+
"homepage": "https://github.com/HominisBrowser/FireForge",
|
|
91
91
|
"bugs": {
|
|
92
|
-
"url": "https://github.com/
|
|
92
|
+
"url": "https://github.com/HominisBrowser/FireForge/issues"
|
|
93
93
|
},
|
|
94
94
|
"keywords": [
|
|
95
95
|
"fireforge",
|