Skip to content

Commit f504224

Browse files
author
Shubham Agarwal
committed
Merge remote-tracking branch into fix/mobile-metro-session-portability
2 parents b3b8089 + 6e421df commit f504224

7 files changed

Lines changed: 72 additions & 38 deletions

File tree

.github/workflows/mobile-apps-script-tests.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ on:
99
- "plugins/mobile-apps/scripts/**"
1010
- "plugins/mobile-apps/skills/create-mobile-app/SKILL.md"
1111
- "plugins/mobile-apps/skills/debug-app/SKILL.md"
12+
- "plugins/mobile-apps/template/metro.config.js"
13+
- "plugins/mobile-apps/template/package.json"
1214
- ".github/workflows/mobile-apps-script-tests.yml"
1315

1416
jobs:

plugins/mobile-apps/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ connector wiring.
9292

9393
5. Start mobile app:
9494

95-
`/create-mobile-app` starts Metro with the app's normal `npm run dev` command.
95+
`/create-mobile-app` starts Metro after its explicit schema and type-check gates.
9696
The template's Metro config delegates sanitized logging to the native host package, which writes `.powernative/metro-logs/`,
9797
so `/debug-app` works after switching between VS Code, Copilot CLI, and
9898
Claude Code without asking for a terminal ID.

plugins/mobile-apps/scripts/tests/metro-logging-contract.test.js

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,25 @@ const fs = require('node:fs');
55
const path = require('node:path');
66
const test = require('node:test');
77

8-
test('template delegates Metro logging to the host helper and fails open', () => {
8+
test('template imports the host Metro logger at config startup', () => {
99
const pluginRoot = path.resolve(__dirname, '..', '..');
10+
const workflow = fs.readFileSync(
11+
path.resolve(pluginRoot, '..', '..', '.github', 'workflows', 'mobile-apps-script-tests.yml'),
12+
'utf8',
13+
);
1014
const metroConfig = fs.readFileSync(path.join(pluginRoot, 'template', 'metro.config.js'), 'utf8');
1115
const gitignore = fs.readFileSync(path.join(pluginRoot, 'template', '.gitignore'), 'utf8');
16+
const packageJson = JSON.parse(fs.readFileSync(path.join(pluginRoot, 'template', 'package.json'), 'utf8'));
1217

13-
assert.match(metroConfig, /@microsoft\/power-apps-native-host\/metro-logger/);
14-
assert.match(metroConfig, /withPowerNativeMetroLogging/);
15-
assert.match(metroConfig, /catch \{/);
18+
assert.match(
19+
metroConfig,
20+
/const \{ withPowerNativeMetroLogging \} = require\('@microsoft\/power-apps-native-host\/metro-logger'\);/,
21+
);
1622
assert.doesNotMatch(metroConfig, /SENSITIVE_LINE_PATTERN|appendMetroLog|process\.stdout\.write/);
23+
assert.equal(packageJson.dependencies['@microsoft/power-apps-native-host'], '^0.2.26');
1724
assert.match(gitignore, /^\.powernative\//m);
25+
assert.match(workflow, /plugins\/mobile-apps\/template\/metro\.config\.js/);
26+
assert.match(workflow, /plugins\/mobile-apps\/template\/package\.json/);
1827
});
1928

2029
test('skill contracts read .powernative logs directly', () => {
@@ -27,9 +36,16 @@ test('skill contracts read .powernative logs directly', () => {
2736
assert.match(createFrontmatter, /allowed-tools:.*\bSkill\b/);
2837
assert.match(createSkill, /\.powernative\/metro-logs/);
2938
assert.match(createSkill, /npm run dev/);
39+
assert.match(createSkill, /npx expo start/);
40+
assert.match(createSkill, /without rerunning the `predev` schema hook/);
3041
assert.doesNotMatch(createSkill, /scripts\/metro-session\.js|dev:expo|copy the plugin wrapper/i);
3142
assert.match(debugSkill, /\.powernative\/metro-logs/);
3243
assert.match(debugSkill, /latest .*\.powernative/i);
44+
assert.match(debugSkill, /"logPath":/);
45+
assert.match(debugSkill, /"pid":/);
46+
assert.match(debugSkill, /predates project-local Metro logging/);
47+
assert.match(debugSkill, /\^0\.2\.26/);
48+
assert.doesNotMatch(debugSkill, /tail -n 500 "\$LOG_PATH"/);
3349
assert.doesNotMatch(debugSkill, /BashOutput|METRO_TERMINAL_ID|metro-session\.js|start --project-root/);
3450
assert.match(deploySkill, /\.powernative/);
3551
});

plugins/mobile-apps/skills/create-mobile-app/SKILL.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1899,7 +1899,7 @@ After `tsc` passes, offer a static HTML preview. The dev server starts next (Ste
18991899
**Print before starting:**
19001900
> "→ [Step 12/13] Launching Metro so you can scan the QR; logs will be written under .powernative/."
19011901

1902-
This skill launches the template's normal `npm run dev` command so:
1902+
This skill runs the schema/typecheck gates explicitly, then launches `npx expo start`. Manual `npm run dev` remains the template's normal entry point and produces the same project-local log because logging is configured in `metro.config.js`.
19031903

19041904
1. The native Metro URL is printed by Expo — the user can scan it immediately.
19051905
2. Hot-reload works on file edits — no restart needed for screen tweaks.
@@ -1921,15 +1921,15 @@ npm run generate-schemas # refresh schema map for any data sources added sinc
19211921
npx tsc --noEmit # final gate — dev server starts only from a clean TypeScript state
19221922
```
19231923

1924-
Run the schema regen and final `tsc` synchronously and check both exits. If either fails, do not launch Metro. Capture the full output once, batch-fix by root cause, rerun the final gate, and continue only when clean. Then start the dev server:
1924+
Run the schema regen and final `tsc` synchronously and check both exits. If either fails, do not launch Metro. Capture the full output once, batch-fix by root cause, rerun the final gate, and continue only when clean. Then start the dev server without rerunning the `predev` schema hook:
19251925

19261926
```bash
1927-
npm run dev
1927+
npx expo start
19281928
```
19291929

19301930
This is a long-running dev server. In hosts that support background terminals, run it as a background/async terminal only for process lifetime; do not persist or depend on the terminal ID. `/debug-app` discovers logs from `.powernative/metro-logs/`, not from terminal output.
19311931

1932-
The orchestrator already ran `npm run generate-schemas` for the final gate; `predev` remains a safety net for manual starts.
1932+
The orchestrator already ran `npm run generate-schemas` for the final gate; `predev` remains a safety net for manual `npm run dev` starts.
19331933

19341934
Read the initial terminal output and locate the generated `.powernative` log directly:
19351935

plugins/mobile-apps/skills/debug-app/SKILL.md

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,8 @@ Branch as follows:
8383
| Log exists and PID still owns the logged port | Treat as live. Capture `port`, `pid`, `logPath`, and continue. |
8484
| Log exists, PID is gone, and another process owns the logged port | Do NOT diagnose from this log. Tell the user which PID holds the port and ask them to restart `npm run dev`. |
8585
| Log exists but PID/port contradict each other | The device may be talking to the wrong server. Ask the user to stop stale Metro processes and rerun `npm run dev`. |
86-
| No log exists | Tell the user Metro is not running or has not emitted `.powernative` logs. Ask them to run `npm run dev`, open the native app, then rerun `/debug-app`. |
86+
| No log exists and `metro.config.js` does not directly import `@microsoft/power-apps-native-host/metro-logger`, or `package.json` does not require `@microsoft/power-apps-native-host` `^0.2.26` or newer | This project predates project-local Metro logging. Stop and report both missing contract pieces. Do not enter a restart loop or edit customer-owned config from `/debug-app`; the user must adopt the current template's Metro config and host dependency first. |
87+
| No log exists and the Metro config/dependency contract is current | Tell the user Metro is not running or has not emitted `.powernative` logs. Ask them to run `npm run dev`, open the native app, then rerun `/debug-app`. |
8788

8889
The port check prevents stale-log diagnosis: a log file can outlive its Metro process, so only the socket probe reveals that the log stopped belonging to the app under test.
8990

@@ -114,13 +115,29 @@ If `fixes.md` is empty, write a session header:
114115

115116
### 0.2 Verify Metro bundled and the app is running
116117

117-
Read the latest sanitized log window:
118+
Read a bounded baseline window and return a real byte cursor:
118119

119120
```bash
120-
tail -n 500 "$LOG_PATH"
121+
node - "$LOG_PATH" 262144 <<'NODE'
122+
const fs = require('node:fs');
123+
const [file, maxText] = process.argv.slice(2);
124+
const maxBytes = Number(maxText);
125+
const size = fs.statSync(file).size;
126+
const start = Math.max(0, size - maxBytes);
127+
const fd = fs.openSync(file, 'r');
128+
const buffer = Buffer.alloc(size - start);
129+
const bytesRead = fs.readSync(fd, buffer, 0, buffer.length, start);
130+
fs.closeSync(fd);
131+
process.stdout.write(JSON.stringify({
132+
cursor: start,
133+
nextCursor: start + bytesRead,
134+
truncated: start > 0,
135+
output: buffer.subarray(0, bytesRead).toString('utf8')
136+
}, null, 2));
137+
NODE
121138
```
122139

123-
Parse `output`, `port`, and `nextCursor`. Scan `output`:
140+
Parse `output`, `cursor`, `nextCursor`, and `truncated`; use the `pid`, `port`, and `logPath` resolved in Phase 0.0. Scan `output`:
124141

125142
- Most recent error-class line is `SyntaxError`, `Unable to resolve module`, `transform failed`, or `error: Bundling failed` → bundle is broken. Treat as a Step B "Import / Bundle" critical error and route through Step D immediately. Do NOT enter the steady-state loop until the bundle is healthy.
126143
- Output contains `Bundling complete` / `iOS Bundled` / `Android Bundled` with no later error-class line → Metro is healthy. Proceed.
@@ -129,6 +146,8 @@ Parse `output`, `port`, and `nextCursor`. Scan `output`:
129146
Stop here.
130147
- Output is empty despite `status.running: true` → Metro has not emitted enough state yet. Tell the user to wait for the native URL, then re-run `/debug-app`; do not guess readiness.
131148

149+
If `truncated: true`, the baseline covers only the latest 256 KiB. Record that older history was omitted; do not claim the full session history was inspected.
150+
132151
Before initializing the cursor, pass **all** classifiable entries in this initial
133152
window through Step B, including JS runtime errors, React warnings, network/API
134153
failures, native errors, and host diagnostics. Do not advance past a prior error
@@ -183,17 +202,19 @@ Write `.claude/debug-app/metro-cursor.json` using structured JSON:
183202

184203
```json
185204
{
205+
"logPath": "/absolute/project/.powernative/metro-logs/metro-<timestamp>-pid-12345-port-8081.log",
206+
"pid": 12345,
186207
"port": 8081,
187208
"cursor": 12345,
188209
"updatedAt": "<ISO timestamp>"
189210
}
190211
```
191212

192-
Set `cursor` to `nextCursor` from Phase 0.2. On later invocations:
213+
Set `logPath`, `pid`, and `port` from Phase 0.0, and set `cursor` to `nextCursor` from Phase 0.2. On later invocations:
193214

194-
- Same `port` and `running: true` → reuse the saved cursor so old errors are not processed again.
195-
- Different `port` → a new Metro session is in play. Discard the old cursor and initialize from the latest log window.
196-
- `tail.rotationLost: true` (the saved cursor is past end-of-file, which can only happen when the log rotated) → accept the returned reset cursor and record the rotation in `fixes.md`.
215+
- Same `logPath`, `pid`, and `port`, with the process still owning the port → reuse the saved cursor so old errors are not processed again.
216+
- Different `logPath`, `pid`, or `port` → a new Metro session is in play. Discard the old cursor and initialize from the latest log window.
217+
- `rotationLost: true` from Step A → accept the returned reset cursor and record the file shrink/replacement in `fixes.md`.
197218

198219
---
199220

@@ -341,7 +362,7 @@ Repeat until **3 consecutive clean cycles**, OR the user types `stop`, OR the es
341362

342363
### Step A — Collect logs
343364

344-
Read `.claude/debug-app/metro-cursor.json`, confirm its `logPath` still points at the latest live `.powernative` log, then read newly appended bytes:
365+
Read `.claude/debug-app/metro-cursor.json`, rediscover the latest live `.powernative` log using Phase 0.0, and confirm the saved `logPath`, `pid`, and `port` still match. If any differ, return to Phase 0.2 and initialize a new baseline. Otherwise read newly appended bytes:
345366

346367
```bash
347368
node - "$LOG_PATH" <saved-cursor> 262144 <<'NODE'
@@ -365,7 +386,7 @@ process.stdout.write(JSON.stringify({
365386
NODE
366387
```
367388

368-
Use only the returned `output` for this cycle. Immediately persist `nextCursor` and the current `port` to `metro-cursor.json`, even when `output` is empty or contains an error; this prevents duplicate processing after interruption and handles log rotation safely.
389+
Use only the returned `output` for this cycle. Immediately persist the current `logPath`, `pid`, `port`, `nextCursor`, and a new `updatedAt` to `metro-cursor.json`, even when `output` is empty or contains an error; this prevents duplicate processing after interruption and handles file replacement safely.
369390

370391
Count a clean cycle only after observing a full 5-second interval with empty or non-error new output. If the log file changes, the PID/port check becomes contradictory, or the cursor resets because the file shrank/rotated, never count that cycle as clean.
371392

plugins/mobile-apps/template/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ connector wiring.
9797

9898
5. Start mobile app:
9999

100-
`/create-mobile-app` starts Metro with the app's normal `npm run dev` command.
100+
`/create-mobile-app` starts Metro after its explicit schema and type-check gates.
101101
The template's Metro config delegates sanitized logging to the native host package, which writes `.powernative/metro-logs/`,
102102
allowing `/debug-app` to work across VS Code, Copilot CLI, and Claude Code
103103
without a terminal ID.

plugins/mobile-apps/template/metro.config.js

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,5 @@
11
const { getDefaultConfig } = require('expo/metro-config');
2-
3-
function withPowerNativeMetroLogging(middleware) {
4-
try {
5-
return require('@microsoft/power-apps-native-host/metro-logger')
6-
.withPowerNativeMetroLogging(middleware, { projectRoot: __dirname });
7-
} catch {
8-
return middleware;
9-
}
10-
}
2+
const { withPowerNativeMetroLogging } = require('@microsoft/power-apps-native-host/metro-logger');
113

124
// CUSTOMIZATION START - DO NOT REMOVE OR RENAME THE COMMENT
135
// Add Metro config changes in this function only.
@@ -21,15 +13,18 @@ const config = getDefaultConfig(__dirname);
2113
config.resolver.sourceExts = [...config.resolver.sourceExts, 'mjs'];
2214
config.server = {
2315
...config.server,
24-
enhanceMiddleware: (middleware) => withPowerNativeMetroLogging((req, res, next) => {
25-
if (req.url === '/__pawrap_verify') {
26-
res.setHeader('Content-Type', 'application/json');
27-
res.setHeader('Access-Control-Allow-Origin', '*');
28-
res.end(JSON.stringify({ type: 'pawrap-app', version: '1' }));
29-
return;
30-
}
31-
middleware(req, res, next);
32-
}),
16+
enhanceMiddleware: (middleware) => withPowerNativeMetroLogging(
17+
(req, res, next) => {
18+
if (req.url === '/__pawrap_verify') {
19+
res.setHeader('Content-Type', 'application/json');
20+
res.setHeader('Access-Control-Allow-Origin', '*');
21+
res.end(JSON.stringify({ type: 'pawrap-app', version: '1' }));
22+
return;
23+
}
24+
middleware(req, res, next);
25+
},
26+
{ projectRoot: __dirname },
27+
),
3328
};
3429

3530
// Force a single copy of these regardless of where the importing module lives.

0 commit comments

Comments
 (0)