@hominis/fireforge 0.38.0 → 0.40.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 +41 -0
- package/README.md +25 -1
- package/dist/src/cli.js +7 -0
- package/dist/src/commands/build.js +8 -3
- package/dist/src/commands/discard.js +75 -50
- package/dist/src/commands/export-flow.d.ts +1 -1
- package/dist/src/commands/export-flow.js +21 -10
- package/dist/src/commands/export-placement-gate.js +1 -1
- package/dist/src/commands/export-shared.js +31 -5
- package/dist/src/commands/import.js +5 -0
- package/dist/src/commands/lint-per-patch.js +97 -18
- package/dist/src/commands/lint-report.d.ts +21 -0
- package/dist/src/commands/lint-report.js +62 -0
- package/dist/src/commands/lint.js +23 -19
- package/dist/src/commands/manifest.js +2 -0
- package/dist/src/commands/patch/lint-ignore.js +20 -1
- package/dist/src/commands/patch/move-files-into.d.ts +47 -0
- package/dist/src/commands/patch/move-files-into.js +245 -0
- package/dist/src/commands/patch/move-files.js +27 -13
- package/dist/src/commands/patch/rename.js +5 -2
- package/dist/src/commands/patch/staged-dependency.js +102 -14
- package/dist/src/commands/re-export-adjacent.d.ts +34 -0
- package/dist/src/commands/re-export-adjacent.js +106 -0
- package/dist/src/commands/re-export-options.js +3 -0
- package/dist/src/commands/re-export.js +42 -53
- package/dist/src/commands/reset.js +5 -0
- package/dist/src/commands/run.js +18 -2
- package/dist/src/commands/status-check.d.ts +35 -0
- package/dist/src/commands/status-check.js +88 -0
- package/dist/src/commands/status-output.d.ts +9 -0
- package/dist/src/commands/status-output.js +33 -1
- package/dist/src/commands/status.js +33 -8
- package/dist/src/commands/test-diagnose.d.ts +1 -1
- package/dist/src/commands/test-diagnose.js +9 -3
- package/dist/src/commands/test-modes.d.ts +21 -0
- package/dist/src/commands/test-modes.js +42 -0
- package/dist/src/commands/test-run.d.ts +2 -0
- package/dist/src/commands/test-run.js +26 -5
- package/dist/src/commands/test.js +38 -32
- package/dist/src/commands/token.js +4 -1
- package/dist/src/commands/tree.d.ts +16 -0
- package/dist/src/commands/tree.js +174 -0
- package/dist/src/core/build-baseline-types.d.ts +7 -0
- package/dist/src/core/build-baseline.d.ts +1 -1
- package/dist/src/core/build-baseline.js +2 -1
- package/dist/src/core/build-prepare.d.ts +9 -0
- package/dist/src/core/build-prepare.js +24 -0
- package/dist/src/core/config-validate.js +25 -0
- package/dist/src/core/discard-baseline.d.ts +60 -0
- package/dist/src/core/discard-baseline.js +232 -0
- package/dist/src/core/license-headers.d.ts +12 -0
- package/dist/src/core/license-headers.js +27 -0
- package/dist/src/core/lint-cache.d.ts +15 -5
- package/dist/src/core/lint-cache.js +17 -5
- package/dist/src/core/patch-export.d.ts +12 -3
- package/dist/src/core/patch-export.js +13 -3
- package/dist/src/core/patch-lint-checkjs.d.ts +25 -2
- package/dist/src/core/patch-lint-checkjs.js +85 -16
- package/dist/src/core/patch-lint-cross.d.ts +9 -1
- package/dist/src/core/patch-lint-cross.js +69 -9
- package/dist/src/core/patch-lint-css.js +64 -16
- package/dist/src/core/patch-lint-diff.d.ts +8 -0
- package/dist/src/core/patch-lint-diff.js +31 -0
- package/dist/src/core/patch-lint-mozbuild.d.ts +18 -0
- package/dist/src/core/patch-lint-mozbuild.js +104 -0
- package/dist/src/core/patch-lint-ownership.d.ts +21 -0
- package/dist/src/core/patch-lint-ownership.js +30 -0
- package/dist/src/core/patch-lint-reexports.d.ts +1 -1
- package/dist/src/core/patch-lint.d.ts +23 -0
- package/dist/src/core/patch-lint.js +34 -3
- package/dist/src/core/patch-manifest.d.ts +1 -1
- package/dist/src/core/patch-policy.d.ts +14 -1
- package/dist/src/core/patch-policy.js +13 -2
- package/dist/src/core/smoke-patterns.d.ts +24 -8
- package/dist/src/core/smoke-patterns.js +23 -12
- package/dist/src/core/status-classify.d.ts +16 -0
- package/dist/src/core/status-classify.js +20 -6
- package/dist/src/core/test-harness-crash.d.ts +10 -0
- package/dist/src/core/test-harness-crash.js +16 -0
- package/dist/src/core/test-stale-check.d.ts +15 -0
- package/dist/src/core/test-stale-check.js +37 -1
- package/dist/src/core/token-category.d.ts +40 -0
- package/dist/src/core/token-category.js +264 -0
- package/dist/src/core/token-manager.d.ts +7 -0
- package/dist/src/core/token-manager.js +82 -193
- package/dist/src/core/token-variant.d.ts +7 -0
- package/dist/src/core/token-variant.js +9 -0
- package/dist/src/core/tree-cow.d.ts +18 -0
- package/dist/src/core/tree-cow.js +71 -0
- package/dist/src/core/tree-guard.d.ts +35 -0
- package/dist/src/core/tree-guard.js +124 -0
- package/dist/src/core/tree-store.d.ts +53 -0
- package/dist/src/core/tree-store.js +214 -0
- package/dist/src/core/typecheck-shim.d.ts +27 -0
- package/dist/src/core/typecheck-shim.js +58 -4
- package/dist/src/core/typecheck.js +14 -3
- package/dist/src/core/xpcshell-profile-dir.d.ts +12 -0
- package/dist/src/core/xpcshell-profile-dir.js +45 -0
- package/dist/src/errors/git.d.ts +8 -0
- package/dist/src/errors/git.js +17 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +1 -0
- package/dist/src/types/commands/options.d.ts +45 -2
- package/dist/src/types/config.d.ts +34 -0
- package/dist/src/utils/validation.d.ts +10 -0
- package/dist/src/utils/validation.js +29 -0
- package/package.json +6 -6
|
@@ -107,6 +107,15 @@ function findVariantBlockInsertionPoint(lines) {
|
|
|
107
107
|
}
|
|
108
108
|
return lines.length;
|
|
109
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* True when a top-level `:root<variant>` block exists in `lines`. Used by
|
|
112
|
+
* the override path to write theme-attribute blocks only when the scaffold
|
|
113
|
+
* already declares them — `insertVariantDeclaration` would otherwise create
|
|
114
|
+
* the block, which is only wanted for the explicit `--variant` flow.
|
|
115
|
+
*/
|
|
116
|
+
export function variantBlockExists(lines, variant) {
|
|
117
|
+
return findVariantBlock(lines, variant) !== null;
|
|
118
|
+
}
|
|
110
119
|
/** True when the `:root<variant>` block already declares `tokenName`. */
|
|
111
120
|
export function variantBlockHasToken(lines, variant, tokenName) {
|
|
112
121
|
const block = findVariantBlock(lines, variant);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Runs a clone command; throws on non-zero exit. */
|
|
2
|
+
export type CloneExecutor = (command: string, args: string[]) => Promise<void>;
|
|
3
|
+
/** How this host can materialise a tree. */
|
|
4
|
+
export type CowCapability = 'clonefile' | 'reflink' | 'none';
|
|
5
|
+
/** Per-platform `cp` argv prefix for a CoW copy of one entry. */
|
|
6
|
+
export declare function cowCopyArgs(capability: Exclude<CowCapability, 'none'>): string[];
|
|
7
|
+
/**
|
|
8
|
+
* Probes CoW support in `dir` by cloning a tiny probe file there.
|
|
9
|
+
* Non-darwin/linux platforms report 'none' without probing.
|
|
10
|
+
*/
|
|
11
|
+
export declare function detectCowSupport(dir: string, platform?: NodeJS.Platform, executor?: CloneExecutor): Promise<CowCapability>;
|
|
12
|
+
/**
|
|
13
|
+
* Copies one directory entry (file or directory, recursively) from
|
|
14
|
+
* `sourcePath` to `destinationPath` using the given capability —
|
|
15
|
+
* clonefile/reflink when available, a plain physical copy under
|
|
16
|
+
* 'none' (the `--force-copy` path; the CALLER gates on the flag).
|
|
17
|
+
*/
|
|
18
|
+
export declare function cloneEntry(capability: CowCapability, sourcePath: string, destinationPath: string, platform?: NodeJS.Platform, executor?: CloneExecutor): Promise<void>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Copy-on-write cloning for `fireforge tree` (FORGE G15).
|
|
4
|
+
*
|
|
5
|
+
* Capability is probed BY DOING — a tiny `cp -c` (APFS clonefile) or
|
|
6
|
+
* `cp --reflink=always` (btrfs/XFS) in the actual destination directory —
|
|
7
|
+
* so the verdict is truthful for the volume the trees will live on. A
|
|
8
|
+
* filesystem without CoW support REFUSES honestly; `--force-copy` opts
|
|
9
|
+
* into a full physical copy but is never implied (a Firefox tree can be
|
|
10
|
+
* tens of gigabytes).
|
|
11
|
+
*
|
|
12
|
+
* The exec layer is injected so unit tests assert the exact `cp` argv per
|
|
13
|
+
* platform without touching the filesystem.
|
|
14
|
+
*/
|
|
15
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
import { toError } from '../utils/errors.js';
|
|
18
|
+
import { verbose } from '../utils/logger.js';
|
|
19
|
+
import { exec } from '../utils/process.js';
|
|
20
|
+
const defaultExecutor = async (command, args) => {
|
|
21
|
+
const result = await exec(command, args);
|
|
22
|
+
if (result.exitCode !== 0) {
|
|
23
|
+
throw new Error(`${command} ${args.join(' ')} failed (exit ${String(result.exitCode)}): ${result.stderr}`);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
/** Per-platform `cp` argv prefix for a CoW copy of one entry. */
|
|
27
|
+
export function cowCopyArgs(capability) {
|
|
28
|
+
return capability === 'clonefile' ? ['-c', '-R', '-p'] : ['--reflink=always', '-a'];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Probes CoW support in `dir` by cloning a tiny probe file there.
|
|
32
|
+
* Non-darwin/linux platforms report 'none' without probing.
|
|
33
|
+
*/
|
|
34
|
+
export async function detectCowSupport(dir, platform = process.platform, executor = defaultExecutor) {
|
|
35
|
+
if (platform !== 'darwin' && platform !== 'linux')
|
|
36
|
+
return 'none';
|
|
37
|
+
const capability = platform === 'darwin' ? 'clonefile' : 'reflink';
|
|
38
|
+
let probeDir;
|
|
39
|
+
try {
|
|
40
|
+
probeDir = await mkdtemp(join(dir, '.fireforge-cow-probe-'));
|
|
41
|
+
const src = join(probeDir, 'src');
|
|
42
|
+
const dst = join(probeDir, 'dst');
|
|
43
|
+
await writeFile(src, 'probe');
|
|
44
|
+
const flag = capability === 'clonefile' ? '-c' : '--reflink=always';
|
|
45
|
+
await executor('cp', [flag, src, dst]);
|
|
46
|
+
return capability;
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
verbose(`CoW probe in ${dir} failed: ${toError(error).message}`);
|
|
50
|
+
return 'none';
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
if (probeDir !== undefined) {
|
|
54
|
+
await rm(probeDir, { recursive: true, force: true }).catch(() => undefined);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Copies one directory entry (file or directory, recursively) from
|
|
60
|
+
* `sourcePath` to `destinationPath` using the given capability —
|
|
61
|
+
* clonefile/reflink when available, a plain physical copy under
|
|
62
|
+
* 'none' (the `--force-copy` path; the CALLER gates on the flag).
|
|
63
|
+
*/
|
|
64
|
+
export async function cloneEntry(capability, sourcePath, destinationPath, platform = process.platform, executor = defaultExecutor) {
|
|
65
|
+
const args = capability === 'none'
|
|
66
|
+
? platform === 'darwin'
|
|
67
|
+
? ['-R', '-p']
|
|
68
|
+
: ['-a']
|
|
69
|
+
: cowCopyArgs(capability);
|
|
70
|
+
await executor('cp', [...args, sourcePath, destinationPath]);
|
|
71
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type TreeMarker } from './tree-store.js';
|
|
2
|
+
/**
|
|
3
|
+
* Per-command verdicts. `allowed` runs unconditionally; `refused` never
|
|
4
|
+
* runs in a tree; `conditional` consults `TREE_CONDITIONAL_CHECKS` with
|
|
5
|
+
* the invoked subcommand chain and parsed options.
|
|
6
|
+
*/
|
|
7
|
+
export declare const TREE_COMMAND_VERDICTS: Readonly<Record<string, 'allowed' | 'refused' | 'conditional'>>;
|
|
8
|
+
interface ConditionalInput {
|
|
9
|
+
/** Subcommand chain below the top-level command (e.g. ['staged-dependency']). */
|
|
10
|
+
subcommands: readonly string[];
|
|
11
|
+
/** Merged options of the ACTION command (leaf). */
|
|
12
|
+
options: Readonly<Record<string, unknown>>;
|
|
13
|
+
/** Positional args of the action command. */
|
|
14
|
+
args: readonly unknown[];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Throws when `commandName` (with its subcommand chain and options) must
|
|
18
|
+
* not run inside the tree described by `marker`. Exported for the
|
|
19
|
+
* preAction hook in cli.ts and for the guard tests.
|
|
20
|
+
*/
|
|
21
|
+
export declare function enforceTreeGuard(marker: TreeMarker, commandName: string, input: ConditionalInput): void;
|
|
22
|
+
/**
|
|
23
|
+
* Commander preAction hook body: locates the enclosing tree (if any) and
|
|
24
|
+
* enforces the verdict table for the invoked command chain.
|
|
25
|
+
*/
|
|
26
|
+
export declare function runTreeGuardHook(rootCommandName: string | undefined, actionCommand: {
|
|
27
|
+
name: () => string;
|
|
28
|
+
optsWithGlobals: () => Record<string, unknown>;
|
|
29
|
+
args: unknown[];
|
|
30
|
+
parent: {
|
|
31
|
+
name: () => string;
|
|
32
|
+
parent: unknown;
|
|
33
|
+
} | null;
|
|
34
|
+
}): Promise<void>;
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Read-only guard for verification trees (FORGE G15).
|
|
4
|
+
*
|
|
5
|
+
* A tree is a snapshot: its `patches/` and `components/` are copies with
|
|
6
|
+
* no merge-back model, so any command that mutates project or engine
|
|
7
|
+
* state must run in the primary tree. Enforcement is DEFAULT-DENY via an
|
|
8
|
+
* explicit verdict table over every top-level command — a newly added
|
|
9
|
+
* command is refused inside trees until its author classifies it (the
|
|
10
|
+
* drift test in `tree-guard.test.ts` fails otherwise) — and lives in ONE
|
|
11
|
+
* commander `preAction` hook installed by `createProgram()`, so the
|
|
12
|
+
* guard fires no matter how the cwd ended up inside a tree (`tree exec`
|
|
13
|
+
* or a plain `cd`).
|
|
14
|
+
*/
|
|
15
|
+
import { existsSync } from 'node:fs';
|
|
16
|
+
import { dirname, join, resolve } from 'node:path';
|
|
17
|
+
import { GeneralError } from '../errors/base.js';
|
|
18
|
+
import { readTreeMarker } from './tree-store.js';
|
|
19
|
+
/**
|
|
20
|
+
* Per-command verdicts. `allowed` runs unconditionally; `refused` never
|
|
21
|
+
* runs in a tree; `conditional` consults `TREE_CONDITIONAL_CHECKS` with
|
|
22
|
+
* the invoked subcommand chain and parsed options.
|
|
23
|
+
*/
|
|
24
|
+
export const TREE_COMMAND_VERDICTS = {
|
|
25
|
+
setup: 'refused',
|
|
26
|
+
source: 'refused',
|
|
27
|
+
download: 'refused',
|
|
28
|
+
bootstrap: 'refused',
|
|
29
|
+
import: 'refused',
|
|
30
|
+
resolve: 'refused',
|
|
31
|
+
build: 'refused',
|
|
32
|
+
run: 'refused',
|
|
33
|
+
status: 'allowed',
|
|
34
|
+
reset: 'refused',
|
|
35
|
+
discard: 'refused',
|
|
36
|
+
export: 'conditional',
|
|
37
|
+
'export-all': 'conditional',
|
|
38
|
+
're-export': 'refused',
|
|
39
|
+
patch: 'refused',
|
|
40
|
+
rebase: 'refused',
|
|
41
|
+
package: 'refused',
|
|
42
|
+
watch: 'refused',
|
|
43
|
+
// Deferred with the objdir clone: obj-* embeds absolute primary paths.
|
|
44
|
+
test: 'refused',
|
|
45
|
+
config: 'conditional',
|
|
46
|
+
doctor: 'conditional',
|
|
47
|
+
register: 'refused',
|
|
48
|
+
wire: 'refused',
|
|
49
|
+
token: 'refused',
|
|
50
|
+
lint: 'allowed',
|
|
51
|
+
typecheck: 'allowed',
|
|
52
|
+
verify: 'allowed',
|
|
53
|
+
furnace: 'refused',
|
|
54
|
+
// No nesting; `tree list` inside a tree is pointless but harmless — the
|
|
55
|
+
// subcommand check below allows only `list`.
|
|
56
|
+
tree: 'conditional',
|
|
57
|
+
};
|
|
58
|
+
/** The command set named in refusal messages. */
|
|
59
|
+
const ALLOWED_SUMMARY = 'Verification trees support: status, lint, typecheck, verify, doctor (read-only), ' +
|
|
60
|
+
'config (read), export --dry-run, and export-all --dry-run.';
|
|
61
|
+
const TREE_CONDITIONAL_CHECKS = {
|
|
62
|
+
// Exports write patches/ + patches.json; only the dry-run preview is a read.
|
|
63
|
+
export: ({ options }) => options['dryRun'] === true,
|
|
64
|
+
'export-all': ({ options }) => options['dryRun'] === true,
|
|
65
|
+
// `config` with no positional value prints; with a value it writes.
|
|
66
|
+
config: ({ args }) => args.filter((a) => typeof a === 'string').length <= 1,
|
|
67
|
+
// Doctor reads unless a repair flag is set.
|
|
68
|
+
doctor: ({ options }) => options['repairFurnace'] !== true && options['fix'] !== true,
|
|
69
|
+
// No tree lifecycle inside a tree; `tree list` is the only read.
|
|
70
|
+
tree: ({ subcommands }) => subcommands[0] === 'list',
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Throws when `commandName` (with its subcommand chain and options) must
|
|
74
|
+
* not run inside the tree described by `marker`. Exported for the
|
|
75
|
+
* preAction hook in cli.ts and for the guard tests.
|
|
76
|
+
*/
|
|
77
|
+
export function enforceTreeGuard(marker, commandName, input) {
|
|
78
|
+
const verdict = TREE_COMMAND_VERDICTS[commandName] ?? 'refused';
|
|
79
|
+
if (verdict === 'allowed')
|
|
80
|
+
return;
|
|
81
|
+
const conditional = TREE_CONDITIONAL_CHECKS[commandName];
|
|
82
|
+
if (verdict === 'conditional' && conditional && conditional(input))
|
|
83
|
+
return;
|
|
84
|
+
throw new GeneralError(`This is a FireForge verification tree ("${marker.name}", created from ${marker.primaryRoot}). ` +
|
|
85
|
+
`"${[commandName, ...input.subcommands].join(' ')}" mutates project or engine state and must run in the primary tree. ` +
|
|
86
|
+
ALLOWED_SUMMARY);
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Commander preAction hook body: locates the enclosing tree (if any) and
|
|
90
|
+
* enforces the verdict table for the invoked command chain.
|
|
91
|
+
*/
|
|
92
|
+
export async function runTreeGuardHook(rootCommandName, actionCommand) {
|
|
93
|
+
// Resolve the top-level command and subcommand chain from the action
|
|
94
|
+
// command's parent links (commander nests subcommands).
|
|
95
|
+
const chain = [];
|
|
96
|
+
let node = actionCommand;
|
|
97
|
+
while (node && node.parent) {
|
|
98
|
+
chain.unshift(node.name());
|
|
99
|
+
node = node.parent;
|
|
100
|
+
}
|
|
101
|
+
const [commandName, ...subcommands] = chain;
|
|
102
|
+
if (commandName === undefined || commandName === rootCommandName)
|
|
103
|
+
return;
|
|
104
|
+
const marker = await (async () => {
|
|
105
|
+
let current = resolve(process.cwd());
|
|
106
|
+
for (let depth = 0; depth < 50; depth++) {
|
|
107
|
+
if (existsSync(join(current, 'fireforge.json'))) {
|
|
108
|
+
return readTreeMarker(current);
|
|
109
|
+
}
|
|
110
|
+
const parent = dirname(current);
|
|
111
|
+
if (parent === current)
|
|
112
|
+
break;
|
|
113
|
+
current = parent;
|
|
114
|
+
}
|
|
115
|
+
return undefined;
|
|
116
|
+
})();
|
|
117
|
+
if (!marker)
|
|
118
|
+
return;
|
|
119
|
+
enforceTreeGuard(marker, commandName, {
|
|
120
|
+
subcommands,
|
|
121
|
+
options: actionCommand.optsWithGlobals(),
|
|
122
|
+
args: actionCommand.args,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type CowCapability } from './tree-cow.js';
|
|
2
|
+
/** `.fireforge/tree.json` — identifies a directory as a verification tree. */
|
|
3
|
+
export interface TreeMarker {
|
|
4
|
+
schemaVersion: 1;
|
|
5
|
+
name: string;
|
|
6
|
+
primaryRoot: string;
|
|
7
|
+
createdAt: string;
|
|
8
|
+
engineHead: string | null;
|
|
9
|
+
patchesFingerprint: string | null;
|
|
10
|
+
}
|
|
11
|
+
/** One row of `tree list`. */
|
|
12
|
+
export interface TreeListEntry {
|
|
13
|
+
name: string;
|
|
14
|
+
path: string;
|
|
15
|
+
createdAt: string;
|
|
16
|
+
staleness: 'fresh' | 'stale (engine advanced)' | 'stale (patches changed)' | 'unknown';
|
|
17
|
+
}
|
|
18
|
+
/** Absolute trees directory for a primary root. */
|
|
19
|
+
export declare function getTreesDir(primaryRoot: string): string;
|
|
20
|
+
/** Serialises tree create/remove against each other (primary-side sidecar lock). */
|
|
21
|
+
export declare function withTreeLifecycleLock<T>(primaryRoot: string, operation: () => Promise<T>): Promise<T>;
|
|
22
|
+
/** Reads the tree marker for a project root, or undefined when not a tree. */
|
|
23
|
+
export declare function readTreeMarker(root: string): Promise<TreeMarker | undefined>;
|
|
24
|
+
/** Current primary-side snapshot identity (engine HEAD + patches.json hash). */
|
|
25
|
+
export declare function computePrimaryFingerprint(primaryRoot: string): Promise<{
|
|
26
|
+
engineHead: string | null;
|
|
27
|
+
patchesFingerprint: string | null;
|
|
28
|
+
}>;
|
|
29
|
+
/** Validates a tree name: one path segment, no traversal. */
|
|
30
|
+
export declare function assertValidTreeName(name: string): void;
|
|
31
|
+
/**
|
|
32
|
+
* Materialises the tree at `treeRoot` from `primaryRoot` using
|
|
33
|
+
* `capability` ('none' = plain copy, already gated on --force-copy by
|
|
34
|
+
* the caller). The engine tree is cloned whole and its `obj-*` build
|
|
35
|
+
* directories removed afterwards — CoW removal is cheap, and per-entry
|
|
36
|
+
* recursion into a Firefox tree is fragile. A fresh `.fireforge/` is
|
|
37
|
+
* created with only `state.json` (when present) and the tree marker.
|
|
38
|
+
*/
|
|
39
|
+
export declare function cloneTree(args: {
|
|
40
|
+
primaryRoot: string;
|
|
41
|
+
treeRoot: string;
|
|
42
|
+
name: string;
|
|
43
|
+
capability: CowCapability;
|
|
44
|
+
createdAt: string;
|
|
45
|
+
}): Promise<TreeMarker>;
|
|
46
|
+
/** Lists trees with staleness against the current primary state. */
|
|
47
|
+
export declare function listTrees(primaryRoot: string): Promise<TreeListEntry[]>;
|
|
48
|
+
/**
|
|
49
|
+
* Removes one tree. Refuses when a live process holds the tree's build
|
|
50
|
+
* or engine-session lock, and never deletes a path outside the trees
|
|
51
|
+
* directory (containment check before `rm -rf`).
|
|
52
|
+
*/
|
|
53
|
+
export declare function removeTree(primaryRoot: string, name: string): Promise<void>;
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
/**
|
|
3
|
+
* Verification-tree storage and lifecycle for `fireforge tree` (FORGE G15).
|
|
4
|
+
*
|
|
5
|
+
* A tree is a FULL project-root CoW snapshot under
|
|
6
|
+
* `.fireforge/trees/<name>` — `getProjectRoot()` walks up from cwd, so
|
|
7
|
+
* every read command (status, lint, typecheck, verify, doctor,
|
|
8
|
+
* `export --dry-run`) runs inside the tree unmodified and its build /
|
|
9
|
+
* engine-session locks key on the tree root automatically. Trees are
|
|
10
|
+
* snapshots by design: there is no merge-back model, mutation commands
|
|
11
|
+
* are refused via the tree marker (see `tree-guard.ts`), and refresh is
|
|
12
|
+
* `tree remove` + `tree create`.
|
|
13
|
+
*/
|
|
14
|
+
import { createHash } from 'node:crypto';
|
|
15
|
+
import { readdir, readFile, rm } from 'node:fs/promises';
|
|
16
|
+
import { basename, join, resolve, sep } from 'node:path';
|
|
17
|
+
import { GeneralError } from '../errors/base.js';
|
|
18
|
+
import { pathExists, readJson, writeJson } from '../utils/fs.js';
|
|
19
|
+
import { verbose } from '../utils/logger.js';
|
|
20
|
+
import { withFileLock } from './file-lock.js';
|
|
21
|
+
import { git } from './git-base.js';
|
|
22
|
+
import { cloneEntry } from './tree-cow.js';
|
|
23
|
+
const TREE_MARKER_FILENAME = 'tree.json';
|
|
24
|
+
const TREES_DIRNAME = 'trees';
|
|
25
|
+
/**
|
|
26
|
+
* Top-level entries never cloned into a tree: per-tree state must start
|
|
27
|
+
* clean (`.fireforge/`), locks are never inherited, the destructive-
|
|
28
|
+
* operation audit log stays primary-only, and dependency dirs are
|
|
29
|
+
* neither needed nor cheap.
|
|
30
|
+
*/
|
|
31
|
+
function isExcludedRootEntry(name) {
|
|
32
|
+
return (name === '.fireforge' ||
|
|
33
|
+
name === 'node_modules' ||
|
|
34
|
+
name === '.fireforge-history.jsonl' ||
|
|
35
|
+
name.endsWith('.lock'));
|
|
36
|
+
}
|
|
37
|
+
/** Absolute trees directory for a primary root. */
|
|
38
|
+
export function getTreesDir(primaryRoot) {
|
|
39
|
+
return join(primaryRoot, '.fireforge', TREES_DIRNAME);
|
|
40
|
+
}
|
|
41
|
+
/** Serialises tree create/remove against each other (primary-side sidecar lock). */
|
|
42
|
+
export async function withTreeLifecycleLock(primaryRoot, operation) {
|
|
43
|
+
return withFileLock(join(primaryRoot, '.fireforge', 'trees.lock'), operation, {
|
|
44
|
+
timeoutMs: 30_000,
|
|
45
|
+
onTimeoutMessage: 'Another fireforge tree create/remove is in progress. Wait for it to finish, then retry.',
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/** Reads the tree marker for a project root, or undefined when not a tree. */
|
|
49
|
+
export async function readTreeMarker(root) {
|
|
50
|
+
const markerPath = join(root, '.fireforge', TREE_MARKER_FILENAME);
|
|
51
|
+
if (!(await pathExists(markerPath)))
|
|
52
|
+
return undefined;
|
|
53
|
+
try {
|
|
54
|
+
const raw = await readJson(markerPath);
|
|
55
|
+
if (raw.schemaVersion !== 1 || typeof raw.name !== 'string')
|
|
56
|
+
return undefined;
|
|
57
|
+
return raw;
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
verbose(`Unreadable tree marker at ${markerPath}: ${String(error)}`);
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** Current primary-side snapshot identity (engine HEAD + patches.json hash). */
|
|
65
|
+
export async function computePrimaryFingerprint(primaryRoot) {
|
|
66
|
+
let engineHead;
|
|
67
|
+
try {
|
|
68
|
+
engineHead = (await git(['rev-parse', 'HEAD'], join(primaryRoot, 'engine'))).trim();
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
engineHead = null;
|
|
72
|
+
}
|
|
73
|
+
const patchesJson = join(primaryRoot, 'patches', 'patches.json');
|
|
74
|
+
const patchesFingerprint = (await pathExists(patchesJson))
|
|
75
|
+
? createHash('sha256')
|
|
76
|
+
.update(await readFile(patchesJson))
|
|
77
|
+
.digest('hex')
|
|
78
|
+
: null;
|
|
79
|
+
return { engineHead, patchesFingerprint };
|
|
80
|
+
}
|
|
81
|
+
/** Validates a tree name: one path segment, no traversal. */
|
|
82
|
+
export function assertValidTreeName(name) {
|
|
83
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name) || name.includes('..')) {
|
|
84
|
+
throw new GeneralError(`Invalid tree name "${name}". Use a single path segment of letters, digits, ".", "_" or "-".`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Materialises the tree at `treeRoot` from `primaryRoot` using
|
|
89
|
+
* `capability` ('none' = plain copy, already gated on --force-copy by
|
|
90
|
+
* the caller). The engine tree is cloned whole and its `obj-*` build
|
|
91
|
+
* directories removed afterwards — CoW removal is cheap, and per-entry
|
|
92
|
+
* recursion into a Firefox tree is fragile. A fresh `.fireforge/` is
|
|
93
|
+
* created with only `state.json` (when present) and the tree marker.
|
|
94
|
+
*/
|
|
95
|
+
export async function cloneTree(args) {
|
|
96
|
+
const { primaryRoot, treeRoot, name, capability, createdAt } = args;
|
|
97
|
+
const { mkdir, cp } = await import('node:fs/promises');
|
|
98
|
+
await mkdir(treeRoot, { recursive: true });
|
|
99
|
+
for (const entry of await readdir(primaryRoot)) {
|
|
100
|
+
if (isExcludedRootEntry(entry))
|
|
101
|
+
continue;
|
|
102
|
+
await cloneEntry(capability, join(primaryRoot, entry), join(treeRoot, entry));
|
|
103
|
+
}
|
|
104
|
+
// A cloned objdir embeds absolute topsrcdir/topobjdir paths back into
|
|
105
|
+
// the PRIMARY tree (mach-build-artifacts.ts parses exactly those), so
|
|
106
|
+
// in-tree builds/tests would silently operate against the primary —
|
|
107
|
+
// remove the clones; `tree` is read-verification only in 0.40.0.
|
|
108
|
+
const engineDir = join(treeRoot, 'engine');
|
|
109
|
+
if (await pathExists(engineDir)) {
|
|
110
|
+
for (const entry of await readdir(engineDir)) {
|
|
111
|
+
if (entry.startsWith('obj-')) {
|
|
112
|
+
await rm(join(engineDir, entry), { recursive: true, force: true });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
// Defensive: never inherit a mid-operation git index lock.
|
|
116
|
+
await rm(join(engineDir, '.git', 'index.lock'), { force: true });
|
|
117
|
+
}
|
|
118
|
+
await mkdir(join(treeRoot, '.fireforge'), { recursive: true });
|
|
119
|
+
const stateJson = join(primaryRoot, '.fireforge', 'state.json');
|
|
120
|
+
if (await pathExists(stateJson)) {
|
|
121
|
+
await cp(stateJson, join(treeRoot, '.fireforge', 'state.json'));
|
|
122
|
+
}
|
|
123
|
+
const { engineHead, patchesFingerprint } = await computePrimaryFingerprint(primaryRoot);
|
|
124
|
+
const marker = {
|
|
125
|
+
schemaVersion: 1,
|
|
126
|
+
name,
|
|
127
|
+
primaryRoot,
|
|
128
|
+
createdAt,
|
|
129
|
+
engineHead,
|
|
130
|
+
patchesFingerprint,
|
|
131
|
+
};
|
|
132
|
+
await writeJson(join(treeRoot, '.fireforge', TREE_MARKER_FILENAME), marker);
|
|
133
|
+
return marker;
|
|
134
|
+
}
|
|
135
|
+
/** Lists trees with staleness against the current primary state. */
|
|
136
|
+
export async function listTrees(primaryRoot) {
|
|
137
|
+
const treesDir = getTreesDir(primaryRoot);
|
|
138
|
+
if (!(await pathExists(treesDir)))
|
|
139
|
+
return [];
|
|
140
|
+
const current = await computePrimaryFingerprint(primaryRoot);
|
|
141
|
+
const entries = [];
|
|
142
|
+
for (const name of (await readdir(treesDir)).sort((a, b) => a.localeCompare(b))) {
|
|
143
|
+
if (name.endsWith('.lock'))
|
|
144
|
+
continue;
|
|
145
|
+
const path = join(treesDir, name);
|
|
146
|
+
const marker = await readTreeMarker(path);
|
|
147
|
+
if (!marker) {
|
|
148
|
+
entries.push({ name, path, createdAt: '(no marker)', staleness: 'unknown' });
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const staleness = marker.engineHead !== current.engineHead
|
|
152
|
+
? 'stale (engine advanced)'
|
|
153
|
+
: marker.patchesFingerprint !== current.patchesFingerprint
|
|
154
|
+
? 'stale (patches changed)'
|
|
155
|
+
: 'fresh';
|
|
156
|
+
entries.push({ name, path, createdAt: marker.createdAt, staleness });
|
|
157
|
+
}
|
|
158
|
+
return entries;
|
|
159
|
+
}
|
|
160
|
+
/** Uses `kill(pid, 0)` — no signal sent, just an existence check. */
|
|
161
|
+
function isProcessAlive(pid) {
|
|
162
|
+
try {
|
|
163
|
+
process.kill(pid, 0);
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Returns the live holder PID of a mkdir-style lock directory, if any.
|
|
172
|
+
* Reads the lock's `pid` file directly (line 1 = PID — the format
|
|
173
|
+
* `file-lock.ts` documents as stable) instead of exporting file-lock
|
|
174
|
+
* internals.
|
|
175
|
+
*/
|
|
176
|
+
async function liveLockHolder(lockPath) {
|
|
177
|
+
try {
|
|
178
|
+
const pidLine = (await readFile(join(lockPath, 'pid'), 'utf-8')).split('\n')[0] ?? '';
|
|
179
|
+
const pid = parseInt(pidLine.trim(), 10);
|
|
180
|
+
if (Number.isFinite(pid) && isProcessAlive(pid))
|
|
181
|
+
return pid;
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
// No lock / unreadable pid file — treat as not held.
|
|
185
|
+
}
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Removes one tree. Refuses when a live process holds the tree's build
|
|
190
|
+
* or engine-session lock, and never deletes a path outside the trees
|
|
191
|
+
* directory (containment check before `rm -rf`).
|
|
192
|
+
*/
|
|
193
|
+
export async function removeTree(primaryRoot, name) {
|
|
194
|
+
assertValidTreeName(name);
|
|
195
|
+
const treesDir = getTreesDir(primaryRoot);
|
|
196
|
+
const treeRoot = resolve(treesDir, name);
|
|
197
|
+
if (!treeRoot.startsWith(treesDir + sep) || basename(treeRoot) !== name) {
|
|
198
|
+
throw new GeneralError(`Refusing to remove "${name}": resolved path escapes the trees directory.`);
|
|
199
|
+
}
|
|
200
|
+
if (!(await pathExists(treeRoot))) {
|
|
201
|
+
throw new GeneralError(`No verification tree named "${name}" exists under ${treesDir}.`);
|
|
202
|
+
}
|
|
203
|
+
for (const lockPath of [
|
|
204
|
+
join(treeRoot, '.fireforge-build.lock'),
|
|
205
|
+
join(treeRoot, '.fireforge', 'engine-session.lock'),
|
|
206
|
+
]) {
|
|
207
|
+
const holder = await liveLockHolder(lockPath);
|
|
208
|
+
if (holder !== undefined) {
|
|
209
|
+
throw new GeneralError(`Refusing to remove tree "${name}": a live process (pid ${String(holder)}) holds ${lockPath}. ` +
|
|
210
|
+
'Wait for it to finish, then retry.');
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
await rm(treeRoot, { recursive: true, force: true });
|
|
214
|
+
}
|
|
@@ -11,6 +11,20 @@
|
|
|
11
11
|
*/
|
|
12
12
|
/** Filename used for the synthetic Firefox-globals shim source file. */
|
|
13
13
|
export declare const SHIM_FILENAME = "__fireforge_firefox_globals.d.ts";
|
|
14
|
+
/**
|
|
15
|
+
* Loose declarations for Firefox test-harness globals (mochitest
|
|
16
|
+
* browser-chrome and xpcshell), appended AFTER the composed
|
|
17
|
+
* Firefox-globals + consumer shim when `patchLint.checkJsTestFiles`
|
|
18
|
+
* extends the checkJs pass to patch-adopted test scripts (FORGE G5).
|
|
19
|
+
* Deliberately `any`-typed — the pragmatic posture matches the main
|
|
20
|
+
* shim. A consumer that wants TYPED harness members (so e.g. a call to
|
|
21
|
+
* a method the harness does not declare fails at the patch boundary)
|
|
22
|
+
* declares them in `patchLint.checkJsTestShim`; because that shim
|
|
23
|
+
* composes BEFORE this baseline and TypeScript resolves conflicting
|
|
24
|
+
* `declare var` redeclarations to the FIRST declaration, the typed
|
|
25
|
+
* consumer declaration wins over the loose fallback here.
|
|
26
|
+
*/
|
|
27
|
+
export declare const TEST_HARNESS_SHIM = "\n// \u2500\u2500 FireForge test-harness shim (loose baseline) \u2500\u2500\ndeclare var TestUtils: any;\ndeclare var BrowserTestUtils: any;\ndeclare var SpecialPowers: any;\ndeclare var EventUtils: any;\ndeclare var SimpleTest: any;\ndeclare var Assert: any;\ndeclare var gTestPath: string;\ndeclare var content: any;\ndeclare function add_task(task: (...args: any[]) => any): any;\ndeclare function add_setup(task: (...args: any[]) => any): any;\ndeclare function ok(condition: any, message?: string): void;\ndeclare function is(actual: any, expected: any, message?: string): void;\ndeclare function isnot(actual: any, expected: any, message?: string): void;\ndeclare function todo(condition: any, message?: string): void;\ndeclare function todo_is(actual: any, expected: any, message?: string): void;\ndeclare function info(message: string): void;\ndeclare function registerCleanupFunction(callback: (...args: any[]) => any): void;\ndeclare function waitForExplicitFinish(): void;\ndeclare function finish(): void;\ndeclare function do_get_profile(): any;\ndeclare function run_test(): any;\n\n";
|
|
14
28
|
/**
|
|
15
29
|
* TS diagnostic codes suppressed by both the patch-lint checkJs pass
|
|
16
30
|
* and the whole-project typecheck command. Each is a known false
|
|
@@ -24,6 +38,19 @@ export declare const SHIM_FILENAME = "__fireforge_firefox_globals.d.ts";
|
|
|
24
38
|
* reason the operator couldn't infer from the docs.
|
|
25
39
|
*/
|
|
26
40
|
export declare const SUPPRESSED_DIAGNOSTIC_CODES: ReadonlySet<number>;
|
|
41
|
+
/**
|
|
42
|
+
* Undefined-free-identifier codes, split out of
|
|
43
|
+
* {@link SUPPRESSED_DIAGNOSTIC_CODES} (FORGE F12). Unconditional
|
|
44
|
+
* suppression let a module reference a name with no import or declaration
|
|
45
|
+
* anywhere and still typecheck with 0 errors — the failure then surfaced
|
|
46
|
+
* as a runtime `ReferenceError`. Both flows now report these at a
|
|
47
|
+
* configurable severity (default `'warning'`: visible without breaking
|
|
48
|
+
* gates; genuine shim gaps are silenced by adding the global to
|
|
49
|
+
* `extraShim`, or per-run via the `'off'` setting).
|
|
50
|
+
*/
|
|
51
|
+
export declare const UNDEFINED_IDENTIFIER_CODES: ReadonlySet<number>;
|
|
52
|
+
/** Hint appended to reported undefined-identifier diagnostics. */
|
|
53
|
+
export declare const UNDEFINED_IDENTIFIER_HINT: string;
|
|
27
54
|
/**
|
|
28
55
|
* Result of {@link composeShimSource}: the source body to feed into
|
|
29
56
|
* the TS host plus a flag indicating whether the user-supplied extra
|
|
@@ -128,6 +128,44 @@ declare var Localization: LocalizationShim;
|
|
|
128
128
|
declare module 'resource:*';
|
|
129
129
|
declare module 'chrome:*';
|
|
130
130
|
|
|
131
|
+
`;
|
|
132
|
+
/**
|
|
133
|
+
* Loose declarations for Firefox test-harness globals (mochitest
|
|
134
|
+
* browser-chrome and xpcshell), appended AFTER the composed
|
|
135
|
+
* Firefox-globals + consumer shim when `patchLint.checkJsTestFiles`
|
|
136
|
+
* extends the checkJs pass to patch-adopted test scripts (FORGE G5).
|
|
137
|
+
* Deliberately `any`-typed — the pragmatic posture matches the main
|
|
138
|
+
* shim. A consumer that wants TYPED harness members (so e.g. a call to
|
|
139
|
+
* a method the harness does not declare fails at the patch boundary)
|
|
140
|
+
* declares them in `patchLint.checkJsTestShim`; because that shim
|
|
141
|
+
* composes BEFORE this baseline and TypeScript resolves conflicting
|
|
142
|
+
* `declare var` redeclarations to the FIRST declaration, the typed
|
|
143
|
+
* consumer declaration wins over the loose fallback here.
|
|
144
|
+
*/
|
|
145
|
+
export const TEST_HARNESS_SHIM = `
|
|
146
|
+
// ── FireForge test-harness shim (loose baseline) ──
|
|
147
|
+
declare var TestUtils: any;
|
|
148
|
+
declare var BrowserTestUtils: any;
|
|
149
|
+
declare var SpecialPowers: any;
|
|
150
|
+
declare var EventUtils: any;
|
|
151
|
+
declare var SimpleTest: any;
|
|
152
|
+
declare var Assert: any;
|
|
153
|
+
declare var gTestPath: string;
|
|
154
|
+
declare var content: any;
|
|
155
|
+
declare function add_task(task: (...args: any[]) => any): any;
|
|
156
|
+
declare function add_setup(task: (...args: any[]) => any): any;
|
|
157
|
+
declare function ok(condition: any, message?: string): void;
|
|
158
|
+
declare function is(actual: any, expected: any, message?: string): void;
|
|
159
|
+
declare function isnot(actual: any, expected: any, message?: string): void;
|
|
160
|
+
declare function todo(condition: any, message?: string): void;
|
|
161
|
+
declare function todo_is(actual: any, expected: any, message?: string): void;
|
|
162
|
+
declare function info(message: string): void;
|
|
163
|
+
declare function registerCleanupFunction(callback: (...args: any[]) => any): void;
|
|
164
|
+
declare function waitForExplicitFinish(): void;
|
|
165
|
+
declare function finish(): void;
|
|
166
|
+
declare function do_get_profile(): any;
|
|
167
|
+
declare function run_test(): any;
|
|
168
|
+
|
|
131
169
|
`;
|
|
132
170
|
/**
|
|
133
171
|
* TS diagnostic codes suppressed by both the patch-lint checkJs pass
|
|
@@ -146,11 +184,26 @@ export const SUPPRESSED_DIAGNOSTIC_CODES = new Set([
|
|
|
146
184
|
2306, // File '{0}' is not a module.
|
|
147
185
|
2305, // Module '{0}' has no exported member '{1}'.
|
|
148
186
|
2792, // Cannot find module '{0}'. Did you mean to set the 'moduleResolution' option...
|
|
149
|
-
2304, // Cannot find name '{0}'. (for globals we missed in the shim)
|
|
150
|
-
2552, // Cannot find name '{0}'. Did you mean '{1}'?
|
|
151
187
|
2580, // Cannot find name '{0}'. Do you need to install type definitions...
|
|
152
188
|
7016, // Could not find a declaration file for module '{0}'.
|
|
153
189
|
]);
|
|
190
|
+
/**
|
|
191
|
+
* Undefined-free-identifier codes, split out of
|
|
192
|
+
* {@link SUPPRESSED_DIAGNOSTIC_CODES} (FORGE F12). Unconditional
|
|
193
|
+
* suppression let a module reference a name with no import or declaration
|
|
194
|
+
* anywhere and still typecheck with 0 errors — the failure then surfaced
|
|
195
|
+
* as a runtime `ReferenceError`. Both flows now report these at a
|
|
196
|
+
* configurable severity (default `'warning'`: visible without breaking
|
|
197
|
+
* gates; genuine shim gaps are silenced by adding the global to
|
|
198
|
+
* `extraShim`, or per-run via the `'off'` setting).
|
|
199
|
+
*/
|
|
200
|
+
export const UNDEFINED_IDENTIFIER_CODES = new Set([
|
|
201
|
+
2304, // Cannot find name '{0}'.
|
|
202
|
+
2552, // Cannot find name '{0}'. Did you mean '{1}'?
|
|
203
|
+
]);
|
|
204
|
+
/** Hint appended to reported undefined-identifier diagnostics. */
|
|
205
|
+
export const UNDEFINED_IDENTIFIER_HINT = '(undefined identifier — import or declare it, add the global to the extra shim, ' +
|
|
206
|
+
'or tune the "undefinedIdentifiers" setting)';
|
|
154
207
|
/** Matches a lone triple-slash `/// <reference path="…" />` directive line. */
|
|
155
208
|
const TRIPLE_SLASH_REFERENCE = /^\s*\/\/\/\s*<reference\s+path\s*=\s*["']([^"']+)["']\s*\/?>\s*$/;
|
|
156
209
|
/**
|
|
@@ -211,8 +264,9 @@ async function inlineTripleSlashReferences(source, baseDir, seen) {
|
|
|
211
264
|
* @param extraShimPath - Optional project-relative path to an extra `.d.ts`
|
|
212
265
|
*/
|
|
213
266
|
export async function composeShimSource(projectRoot, extraShimPath) {
|
|
267
|
+
const base = FIREFOX_GLOBALS_SHIM;
|
|
214
268
|
if (!extraShimPath) {
|
|
215
|
-
return { source:
|
|
269
|
+
return { source: base, extraShimAppended: false };
|
|
216
270
|
}
|
|
217
271
|
const absoluteShim = resolve(projectRoot, extraShimPath);
|
|
218
272
|
if (!(await pathExists(absoluteShim))) {
|
|
@@ -222,7 +276,7 @@ export async function composeShimSource(projectRoot, extraShimPath) {
|
|
|
222
276
|
const extra = await readText(absoluteShim);
|
|
223
277
|
const inlinedExtra = await inlineTripleSlashReferences(extra, dirname(absoluteShim), new Set([absoluteShim]));
|
|
224
278
|
return {
|
|
225
|
-
source: `${
|
|
279
|
+
source: `${base}\n// ── extraShim: ${extraShimPath} ──\n${inlinedExtra}`,
|
|
226
280
|
extraShimAppended: true,
|
|
227
281
|
};
|
|
228
282
|
}
|