@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
@@ -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(absolute);
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, temporaryDirectory, absolute);
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(absolute);
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, temporaryDirectory, absolute);
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(absolute) {
676
- const temporary = path.join(path.dirname(absolute), `.${path.basename(absolute)}.${process.pid}.${randomUUID()}.tmp`);
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, temporaryDirectory, absolute) {
710
- const publication = path.join(temporaryDirectory, 'validated.png');
711
- const handle = await open(
712
- publication,
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.close();
728
+ await handle?.close();
729
+ await rm(publication, { force: true });
724
730
  }
725
731
  }
726
732
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deeeed/metamask-harness",
3
- "version": "0.23.0",
3
+ "version": "0.23.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mm-harness": "bin/mm-harness"