Skip to content

Use indexed watch folder paths for external assets - #1892

Draft
robhogan wants to merge 4 commits into
mainfrom
robhogan/watch-folder-asset-urls
Draft

Use indexed watch folder paths for external assets#1892
robhogan wants to merge 4 commits into
mainfrom
robhogan/watch-folder-asset-urls

Conversation

@robhogan

Copy link
Copy Markdown
Collaborator

Summary

Assets outside projectRoot currently derive their httpServerLocation from a path relative to projectRoot. This can produce .. paths which escape /assets, and does not identify which configured watch folder owns the asset.

This uses Metro's existing [metro-watchFolders]/N/ namespace for external asset URLs:

  • asset transforms and getAssets encode the path relative to the containing watch folder
  • the encoded path is included in the transform cache key
  • the /assets endpoint resolves the watch folder prefix before selecting platform and density variants
  • assets within projectRoot retain their existing URLs

Keeping these requests under /assets preserves Metro's platform-specific and density-aware asset resolution.

Fixes #19
Fixes #290

Changelog: [Fix] Use indexed watch folder paths for assets outside projectRoot

Test plan

Created a minimal project which imports an asset from a sibling watch folder:

metro-watch-folder-e2e/
├── project/
│   ├── AssetRegistry.js
│   └── index.js
└── shared/
    └── media/
        ├── tone.mp3
        ├── tone@2x.mp3
        ├── tone@1x.ios.mp3
        └── tone@2x.ios.mp3

index.js imports the sibling asset:

const asset = require('../shared/media/tone.mp3');

Started Metro from this checkout with shared configured as a watch folder:

$ yarn start serve \
    --config /Users/robhogan/cowork/metro-watch-folder-e2e/metro.config.js \
    --host 127.0.0.1 \
    --port 8099 \
    --reset-cache

Metro ready

Requested an iOS bundle and inspected the generated asset metadata:

$ curl --fail --silent --show-error \
    --output /private/tmp/metro-watch-folder-bundle.js \
    'http://127.0.0.1:8099/index.bundle?platform=ios&dev=true&minify=false'

$ rg -A 5 '"httpServerLocation"' /private/tmp/metro-watch-folder-bundle.js
    "httpServerLocation": "/assets/[metro-watchFolders]/1/media",
    "scales": [1, 2],
    "hash": "6db41d4f75e3e7e47d792c57abc17cb9",
    "name": "tone",
    "type": "mp3"
  });

Metro normalises projectRoot as watch folder 0, so the configured sibling directory is watch folder 1.

Requested the emitted asset path at 2x density for iOS:

$ curl --silent --show-error --globoff --include \
    'http://127.0.0.1:8099/assets/[metro-watchFolders]/1/media/tone@2x.mp3?platform=ios&hash=6db41d4f75e3e7e47d792c57abc17cb9'

HTTP/1.1 200 OK
Content-Type: audio/mpeg
Content-Length: 7

ios-2x

The response body matches tone@2x.ios.mp3, confirming that the indexed URL resolves through the sibling watch folder while retaining platform and density selection.

Also ran:

$ yarn jest packages/metro/src packages/metro-transform-worker/src --runInBand
Test Suites: 63 passed, 63 total
Tests:       1 skipped, 900 passed, 901 total
Snapshots:   160 passed, 160 total

$ yarn flow check --temp-dir /private/tmp/metro-flow-pr19
$ yarn typecheck-ts
$ yarn verify-api-snapshots
$ yarn lint

All checks passed.

## Summary

`metro-symbolicate` aborts with `TypeError: Line must be greater than or equal to 1, got 0` and emits nothing at all if any frame in the input has line 0. One such frame discards the whole trace, which is why folks in #966 are hand-editing their stack traces first.

Line 0 isn't necessarily malformed input - it's how some crash reporters, Crashlytics for example, represent a frame with no JS source position once every frame has been normalised to `file:line:column`, rendering `[native code]` as `[native code]:0:0`. We already accept the un-normalised form: `symbolicate()`'s regex has an explicit `[native code]` alternative and reports those frames as `null:null:null`.

This makes the normalised form behave the same. `getOriginalPositionDetailsFor` returns an unresolved frame instead of passing a position `source-map` rejects, and the regex consumes a trailing `:0:0` after `[native code]` so the frame matches once rather than twice.

Fixes: #966

Changelog:
```
 - **[Fix]**: `metro-symbolicate` no longer fails on stack frames with no source position, such as `[native code]:0:0` in crash reporter output
```

## Test plan

```
yarn jest packages/metro-symbolicate
yarn flow check
```

New snapshot test over a crash-reporter-formatted trace containing `[native code]:0:0` frames, which fails on `main` with the `TypeError`.
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Aug 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Asset paths (httpServerLocation) invalid when asset is in watchFolders Assets not picked up with multi project roots

1 participant