Skip to content

Navigation Menu

Sign in
Sign up

Use indexed watch folder paths for external assets - #1892

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

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

Conversation

@robhogan

@robhogan robhogan commented Aug 31, 2026
edited
Loading

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 - the same namespace _sourceRequestRoutingMap already serves source requests from, and which _resolveWatchFolderPrefix already decodes:

  • asset transforms and getAssets encode the path relative to the containing watch folder
  • the encoded path is in the transform cache key, since it reaches the transformer and is baked into the module output
  • 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.

The URL path reaches the default transformer as a separate TransformExtras argument rather than as a field on JsTransformOptions, because those options are spread wholesale into BabelTransformerArgs and this isn't something custom Babel transformers should be given. It's a named object rather than a bare positional argument so that the next Metro-computed input doesn't need a seventh parameter.

The last commit is a related fix which stands on its own. _getAssetsFromDependencies built asset URLs relative to server.unstable_serverRoot, while both readers of those URLs use projectRoot - the /assets endpoint resolves a bare relative path against this._config.projectRoot, and [metro-project] maps to projectRoot in _sourceRequestRoutingMap. With unstable_serverRoot set to a monorepo root, an in-project asset was served at a URL the endpoint then failed to resolve. That line dates to the original unstable_serverRoot change, which applied the server root broadly across Server and left the other asset call site on projectRoot. All three callers now share one options object, so the root can't drift again.

Fixes: #19
Fixes: #290
Fixes: #1614
Fixes: #1615

Changelog:

 - **[Fix]**: Use indexed watch folder paths for assets outside `projectRoot`
 - **[Fix]**: Asset URLs are relative to `projectRoot` rather than `server.unstable_serverRoot`, so they resolve at the `/assets` endpoint

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.

New unit tests cover the two halves of that URL scheme meeting in the middle: getAssetUrlPath composed with _resolveWatchFolderPrefix round-trips back to the original absolute path for in-project, nested and watch-folder assets, and _getAssetsFromDependencies is pinned to projectRoot with unstable_serverRoot set. Both fail if the mapping is perturbed - an off-by-one in the watch folder index breaks the former, restoring _getServerRootDir() breaks the latter.

Also ran:

$ yarn jest packages/metro/src packages/metro-transform-worker/src
Test Suites: 63 passed, 63 total
Tests: 1 skipped, 908 passed, 909 total
Snapshots: 160 passed, 160 total
$ yarn flow check
$ yarn typecheck-ts
$ yarn verify-api-snapshots
$ yarn lint

All checks passed.

@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
robhogan force-pushed the robhogan/watch-folder-asset-urls branch 2 times, most recently from 2db7b1c to 8c7e66f Compare September 7, 2026 13:17
`_getAssetsFromDependencies` built asset URLs relative to `unstable_serverRoot`, while both readers of those URLs use `projectRoot`: the `/assets` endpoint resolves a bare relative path against `this._config.projectRoot`, and `[metro-project]` maps to `projectRoot` in `_sourceRequestRoutingMap`. With `unstable_serverRoot` set to a monorepo root, an in-project asset was served at a URL the endpoint then failed to resolve.
That line dates to the original `unstable_serverRoot` change, which applied the server root broadly across `Server` and left the other asset call site on `projectRoot`.
Anchor asset URLs on `projectRoot`, and route the `/assets` manifest endpoint through `_getAssetsFromDependencies` so all three callers share one options object and the root cannot drift again.
Changelog:
```
 - **[Fix]**: Asset URLs are relative to `projectRoot` rather than `server.unstable_serverRoot`, so they resolve at the `/assets` endpoint
```
robhogan force-pushed the robhogan/watch-folder-asset-urls branch from 8c7e66f to ec1b3fc Compare September 7, 2026 13:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

No reviews

Assignees

No one assigned

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

Milestone

No milestone

1 participant

AltStyle によって変換されたページ (->オリジナル) /