@deeeed/metamask-harness 0.23.0 → 0.23.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
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.23.1 - 2026-07-29
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Mobile recipe evidence no longer triggers Metro Fast Refresh: harness-managed Metro ignores checkout-local `temp` artifacts, and screenshot scratch files stay outside the watched project.
|
|
10
|
+
|
|
5
11
|
## 0.23.0 - 2026-07-28
|
|
6
12
|
|
|
7
13
|
### Added
|
package/adapters/manifest.json
CHANGED
|
@@ -42,6 +42,14 @@
|
|
|
42
42
|
"inputs": "--target --port --log --pid-file [--workers] [--clear]",
|
|
43
43
|
"outputs": "Metro PID/launch metadata and redirected log; exit 0/1/2"
|
|
44
44
|
},
|
|
45
|
+
{
|
|
46
|
+
"id": "mobile/metro-config",
|
|
47
|
+
"entry": "adapters/mobile/metro-config.cjs",
|
|
48
|
+
"kind": "module",
|
|
49
|
+
"purpose": "Preserve the product Metro config while excluding harness-owned checkout temp artifacts.",
|
|
50
|
+
"inputs": "base Metro config; env MM_HARNESS_METRO_PROJECT_ROOT, MM_HARNESS_METRO_BASE_CONFIG",
|
|
51
|
+
"outputs": "merged Metro config with resolver.blockList for <projectRoot>/temp"
|
|
52
|
+
},
|
|
45
53
|
{
|
|
46
54
|
"id": "mobile/stop-metro",
|
|
47
55
|
"entry": "adapters/mobile/stop-metro.sh",
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const { createRequire } = require('node:module');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const { pathToFileURL } = require('node:url');
|
|
7
|
+
|
|
8
|
+
function escapeRegExp(value) {
|
|
9
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function appendBlockList(blockList, pattern) {
|
|
13
|
+
if (Array.isArray(blockList)) return [...blockList, pattern];
|
|
14
|
+
return blockList instanceof RegExp ? [blockList, pattern] : [pattern];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function blockListFlags(blockList) {
|
|
18
|
+
const first = Array.isArray(blockList) ? blockList[0] : blockList;
|
|
19
|
+
return first instanceof RegExp ? first.flags : '';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function resolveExpoConfigLoader(projectRoot) {
|
|
23
|
+
const requireFromProject = createRequire(path.join(projectRoot, 'package.json'));
|
|
24
|
+
let requireUtilsPath;
|
|
25
|
+
try {
|
|
26
|
+
requireUtilsPath = requireFromProject.resolve('@expo/require-utils');
|
|
27
|
+
} catch (error) {
|
|
28
|
+
if (error?.code !== 'MODULE_NOT_FOUND') throw error;
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const { loadModuleSync } = requireFromProject(requireUtilsPath);
|
|
32
|
+
return typeof loadModuleSync === 'function' ? loadModuleSync : null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function loadProductConfig(configPath, baseConfig, projectRoot) {
|
|
36
|
+
let loaded;
|
|
37
|
+
const expoConfigLoader = resolveExpoConfigLoader(projectRoot);
|
|
38
|
+
if (expoConfigLoader) {
|
|
39
|
+
loaded = expoConfigLoader(configPath);
|
|
40
|
+
} else {
|
|
41
|
+
try {
|
|
42
|
+
loaded = require(configPath);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (error?.code !== 'ERR_REQUIRE_ESM') throw error;
|
|
45
|
+
loaded = await import(pathToFileURL(configPath).href);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const moduleValue = await loaded;
|
|
50
|
+
const exported =
|
|
51
|
+
moduleValue?.__esModule ||
|
|
52
|
+
moduleValue?.[Symbol.toStringTag] === 'Module'
|
|
53
|
+
? moduleValue.default
|
|
54
|
+
: moduleValue;
|
|
55
|
+
const config = await exported;
|
|
56
|
+
return typeof config === 'function' ? config(baseConfig) : config;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = async function harnessMetroConfig(baseConfig) {
|
|
60
|
+
const projectRoot = path.resolve(
|
|
61
|
+
process.env.MM_HARNESS_METRO_PROJECT_ROOT || process.cwd(),
|
|
62
|
+
);
|
|
63
|
+
const productConfigPath = path.resolve(
|
|
64
|
+
process.env.MM_HARNESS_METRO_BASE_CONFIG ||
|
|
65
|
+
path.join(projectRoot, 'metro.config.js'),
|
|
66
|
+
);
|
|
67
|
+
let productConfig = baseConfig;
|
|
68
|
+
if (fs.existsSync(productConfigPath)) {
|
|
69
|
+
productConfig = await loadProductConfig(
|
|
70
|
+
productConfigPath,
|
|
71
|
+
baseConfig,
|
|
72
|
+
projectRoot,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const tempRoot = path.join(projectRoot, 'temp');
|
|
77
|
+
const existingBlockList =
|
|
78
|
+
productConfig.resolver?.blockList ?? baseConfig.resolver?.blockList;
|
|
79
|
+
const tempPattern = new RegExp(
|
|
80
|
+
`^${escapeRegExp(tempRoot)}(?:${escapeRegExp(path.sep)}|$)`,
|
|
81
|
+
blockListFlags(existingBlockList),
|
|
82
|
+
);
|
|
83
|
+
return {
|
|
84
|
+
...productConfig,
|
|
85
|
+
resolver: {
|
|
86
|
+
...productConfig.resolver,
|
|
87
|
+
blockList: appendBlockList(
|
|
88
|
+
existingBlockList,
|
|
89
|
+
tempPattern,
|
|
90
|
+
),
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
};
|
|
@@ -49,6 +49,7 @@ fi
|
|
|
49
49
|
# only ever inspect or signal a Metro listening on $PORT.
|
|
50
50
|
# shellcheck disable=SC1091
|
|
51
51
|
. "$SCRIPT_DIR/lib/metro-listener.sh"
|
|
52
|
+
MOBILE_METRO_REQUIRED_ENV="${MOBILE_METRO_REQUIRED_ENV:-MM_INFURA_PROJECT_ID} MM_HARNESS_METRO_PROJECT_ROOT EXPO_OVERRIDE_METRO_CONFIG"
|
|
52
53
|
|
|
53
54
|
# RECIPE_RUNTIME_DIR (relative, validated) makes runtime state per-run so jobs
|
|
54
55
|
# sharing one checkout do not collide on metro.log / metro.pid / metro.tmux.
|
|
@@ -98,6 +99,13 @@ set +a
|
|
|
98
99
|
export EXPO_NO_TYPESCRIPT_SETUP=1
|
|
99
100
|
export WATCHER_PORT="$(printf '%q' "$PORT")" METRO_PORT="$(printf '%q' "$PORT")"
|
|
100
101
|
export METRO_MAX_WORKERS="$(printf '%q' "$METRO_WORKERS")"
|
|
102
|
+
export MM_HARNESS_METRO_PROJECT_ROOT="$(printf '%q' "$TARGET")"
|
|
103
|
+
if [ -n "\${EXPO_OVERRIDE_METRO_CONFIG:-}" ]; then
|
|
104
|
+
export MM_HARNESS_METRO_BASE_CONFIG="\$EXPO_OVERRIDE_METRO_CONFIG"
|
|
105
|
+
else
|
|
106
|
+
export MM_HARNESS_METRO_BASE_CONFIG=$(printf '%q' "$TARGET/metro.config.js")
|
|
107
|
+
fi
|
|
108
|
+
export EXPO_OVERRIDE_METRO_CONFIG="$(printf '%q' "$SCRIPT_DIR/metro-config.cjs")"
|
|
101
109
|
yarn expo start --port "$(printf '%q' "$PORT")"$clear_flag 2>&1 | tee -a "$(printf '%q' "$LOG_FILE")"
|
|
102
110
|
EOF
|
|
103
111
|
chmod +x "$runner"
|
|
@@ -184,6 +192,9 @@ if ! start_metro_tmux; then
|
|
|
184
192
|
export EXPO_NO_TYPESCRIPT_SETUP=1
|
|
185
193
|
export WATCHER_PORT="${PORT}" METRO_PORT="${PORT}"
|
|
186
194
|
export METRO_MAX_WORKERS="${METRO_WORKERS}"
|
|
195
|
+
export MM_HARNESS_METRO_PROJECT_ROOT="$TARGET"
|
|
196
|
+
export MM_HARNESS_METRO_BASE_CONFIG="${EXPO_OVERRIDE_METRO_CONFIG:-$TARGET/metro.config.js}"
|
|
197
|
+
export EXPO_OVERRIDE_METRO_CONFIG="$SCRIPT_DIR/metro-config.cjs"
|
|
187
198
|
launcher_args=(
|
|
188
199
|
--target "$TARGET"
|
|
189
200
|
--port "$PORT"
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { constants as fsConstants } from 'node:fs';
|
|
3
|
-
import { mkdir, open, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { mkdir, mkdtemp, open, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { execFile, spawn } from 'node:child_process';
|
|
5
5
|
import { promisify } from 'node:util';
|
|
6
|
+
import os from 'node:os';
|
|
6
7
|
import path from 'node:path';
|
|
7
8
|
import { fileURLToPath } from 'node:url';
|
|
8
9
|
import bridgeErrors from '../../../../adapters/mobile/bridge-runtime/lib/bridge-errors.cjs';
|
|
@@ -420,7 +421,7 @@ export async function simulatorScreenshot(input, relPath) {
|
|
|
420
421
|
await rm(absolute, { force: true });
|
|
421
422
|
let temporaryDirectory;
|
|
422
423
|
try {
|
|
423
|
-
temporaryDirectory = await createScreenshotTemporaryDirectory(
|
|
424
|
+
temporaryDirectory = await createScreenshotTemporaryDirectory();
|
|
424
425
|
const captured = path.join(temporaryDirectory, 'captured.png');
|
|
425
426
|
await createPrivateScreenshotFile(captured);
|
|
426
427
|
let result;
|
|
@@ -446,7 +447,7 @@ export async function simulatorScreenshot(input, relPath) {
|
|
|
446
447
|
` Next: mm-harness doctor --adapter mobile --device ${target} --json`,
|
|
447
448
|
);
|
|
448
449
|
}
|
|
449
|
-
await publishPrivateScreenshot(png,
|
|
450
|
+
await publishPrivateScreenshot(png, absolute);
|
|
450
451
|
await rm(temporaryDirectory, { recursive: true, force: true });
|
|
451
452
|
} catch (error) {
|
|
452
453
|
await removeScreenshotResidue(temporaryDirectory, absolute);
|
|
@@ -477,7 +478,7 @@ async function androidScreenshot(input, relPath) {
|
|
|
477
478
|
await rm(absolute, { force: true });
|
|
478
479
|
let temporaryDirectory;
|
|
479
480
|
try {
|
|
480
|
-
temporaryDirectory = await createScreenshotTemporaryDirectory(
|
|
481
|
+
temporaryDirectory = await createScreenshotTemporaryDirectory();
|
|
481
482
|
const adbSerial = await resolveAndroidScreenshotSerial(input);
|
|
482
483
|
let result;
|
|
483
484
|
try {
|
|
@@ -501,7 +502,7 @@ async function androidScreenshot(input, relPath) {
|
|
|
501
502
|
` Next: mm-harness doctor --adapter mobile --device ${adbSerial} --json`,
|
|
502
503
|
);
|
|
503
504
|
}
|
|
504
|
-
await publishPrivateScreenshot(result.stdout,
|
|
505
|
+
await publishPrivateScreenshot(result.stdout, absolute);
|
|
505
506
|
await rm(temporaryDirectory, { recursive: true, force: true });
|
|
506
507
|
return {
|
|
507
508
|
path: relative,
|
|
@@ -672,10 +673,8 @@ function pngCrc32(bytes, start, end) {
|
|
|
672
673
|
return (crc ^ 0xffffffff) >>> 0;
|
|
673
674
|
}
|
|
674
675
|
|
|
675
|
-
async function createScreenshotTemporaryDirectory(
|
|
676
|
-
|
|
677
|
-
await mkdir(temporary, { mode: 0o700 });
|
|
678
|
-
return temporary;
|
|
676
|
+
async function createScreenshotTemporaryDirectory() {
|
|
677
|
+
return mkdtemp(path.join(os.tmpdir(), 'mm-harness-screenshot-'));
|
|
679
678
|
}
|
|
680
679
|
|
|
681
680
|
async function createPrivateScreenshotFile(file) {
|
|
@@ -706,21 +705,28 @@ async function readPrivateScreenshotFile(file) {
|
|
|
706
705
|
}
|
|
707
706
|
}
|
|
708
707
|
|
|
709
|
-
async function publishPrivateScreenshot(png,
|
|
710
|
-
const publication = path.join(
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_NOFOLLOW,
|
|
714
|
-
0o600,
|
|
708
|
+
async function publishPrivateScreenshot(png, absolute) {
|
|
709
|
+
const publication = path.join(
|
|
710
|
+
path.dirname(absolute),
|
|
711
|
+
`.${path.basename(absolute)}.${process.pid}.${randomUUID()}.tmp`,
|
|
715
712
|
);
|
|
713
|
+
let handle;
|
|
716
714
|
try {
|
|
715
|
+
handle = await open(
|
|
716
|
+
publication,
|
|
717
|
+
fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_NOFOLLOW,
|
|
718
|
+
0o600,
|
|
719
|
+
);
|
|
717
720
|
const stats = await handle.stat();
|
|
718
721
|
if (!stats.isFile() || stats.nlink !== 1) throw new Error('validated screenshot is not a private regular file');
|
|
719
722
|
await handle.writeFile(png);
|
|
720
723
|
await handle.sync();
|
|
724
|
+
await handle.close();
|
|
725
|
+
handle = undefined;
|
|
721
726
|
await rename(publication, absolute);
|
|
722
727
|
} finally {
|
|
723
|
-
await handle
|
|
728
|
+
await handle?.close();
|
|
729
|
+
await rm(publication, { force: true });
|
|
724
730
|
}
|
|
725
731
|
}
|
|
726
732
|
|