Skip to content

Commit 031eb88

Browse files
huntiemeta-codesync[bot]
authored andcommitted
Resolve [metro-watchFolders] URL prefix in bundle and entry point paths (#1695)
Summary: Pull Request resolved: #1695 Extends Metro `Server.js` to handle `[metro-watchFolders]/N/...` prefixed entry file paths in `.bundle` and `.map` requests. This convention is already used for source file serving (powering React Native DevTools), and this change extends it to bundle resolution. **Implementation** Adds `_resolveWatchFolderPrefix()`, which parses `[metro-watchFolders]/N/relative/path` URLs and resolves them against the corresponding `watchFolders[N]` entry from the Metro config. Also handles `[metro-project]/...` as a prefix for the project root. This method is called from two sites: - `_resolveRelativePath()` — used for resolving module paths in bundle/map requests - `_getEntryPointAbsolutePath()` — used for resolving the entry file to an absolute path **Motivation** The primary use case is environments where the entry point resolves to a path outside the Metro server root (e.g. via a symlink to a different filesystem mount). In these cases, `path.relative(serverRoot, entryPath)` produces a broken `../../...` path. A client (such as Expo CLI) can instead construct a `[metro-watchFolders]/N/...` URL referencing the watchFolder that contains the entry file, allowing Metro to resolve it correctly. We have an open PR in Expo CLI that aims to use this configuration path: expo/expo#45010. Changelog: [Internal] Differential Revision: D102004228
1 parent 47131bc commit 031eb88

2 files changed

Lines changed: 127 additions & 5 deletions

File tree

packages/metro/src/Server.js

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1622,6 +1622,32 @@ export default class Server {
16221622
);
16231623
}
16241624
1625+
_resolveWatchFolderPrefix(
1626+
filePath: string,
1627+
): {rootDir: string, filePath: string} | null {
1628+
const watchFolderMatch = filePath.match(
1629+
/^\.\/\[metro-watchFolders\]\/(\d+)\/(.*)/,
1630+
);
1631+
if (watchFolderMatch != null) {
1632+
const index = parseInt(watchFolderMatch[1], 10);
1633+
const watchFolder = this._config.watchFolders[index];
1634+
if (watchFolder != null) {
1635+
return {
1636+
rootDir: path.resolve(watchFolder),
1637+
filePath: './' + watchFolderMatch[2],
1638+
};
1639+
}
1640+
}
1641+
const projectMatch = filePath.match(/^\.\/\[metro-project\]\/(.*)/);
1642+
if (projectMatch != null) {
1643+
return {
1644+
rootDir: path.resolve(this._config.projectRoot),
1645+
filePath: './' + projectMatch[1],
1646+
};
1647+
}
1648+
return null;
1649+
}
1650+
16251651
async _resolveRelativePath(
16261652
filePath: string,
16271653
{
@@ -1639,13 +1665,22 @@ export default class Server {
16391665
transformOptions.platform,
16401666
resolverOptions,
16411667
);
1668+
const resolved = this._resolveWatchFolderPrefix(filePath);
16421669
const rootDir =
1643-
relativeTo === 'server'
1644-
? this._getServerRootDir()
1645-
: this._config.projectRoot;
1670+
resolved != null
1671+
? resolved.rootDir
1672+
: relativeTo === 'server'
1673+
? this._getServerRootDir()
1674+
: this._config.projectRoot;
1675+
const resolvedFilePath = resolved != null ? resolved.filePath : filePath;
16461676
return resolutionFn(`${rootDir}/.`, {
1647-
name: filePath,
1648-
data: {key: filePath, locs: [], asyncType: null, isESMImport: false},
1677+
name: resolvedFilePath,
1678+
data: {
1679+
key: resolvedFilePath,
1680+
locs: [],
1681+
asyncType: null,
1682+
isESMImport: false,
1683+
},
16491684
}).filePath;
16501685
}
16511686

@@ -1706,6 +1741,10 @@ export default class Server {
17061741
}
17071742

17081743
_getEntryPointAbsolutePath(entryFile: string): string {
1744+
const resolved = this._resolveWatchFolderPrefix(entryFile);
1745+
if (resolved != null) {
1746+
return path.resolve(resolved.rootDir, resolved.filePath);
1747+
}
17091748
return path.resolve(this._getServerRootDir(), entryFile);
17101749
}
17111750

packages/metro/src/Server/__tests__/Server-test.js

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1436,4 +1436,87 @@ describe('processRequest', () => {
14361436
},
14371437
);
14381438
});
1439+
1440+
describe('watchFolder prefix resolution', () => {
1441+
let watchFolderServer: $FlowFixMe;
1442+
1443+
beforeEach(() => {
1444+
watchFolderServer = new Server(
1445+
mergeConfig(getDefaultValues('/'), {
1446+
projectRoot: '/project',
1447+
watchFolders: ['/project', '/external/packages'],
1448+
resolver: {blockList: []},
1449+
cacheVersion: '',
1450+
serializer: {
1451+
getRunModuleStatement: moduleId =>
1452+
`require(${JSON.stringify(moduleId)});`,
1453+
polyfillModuleNames: [],
1454+
getModulesRunBeforeMainModule: () => ['InitializeCore'],
1455+
},
1456+
reporter: require('../../lib/reporting').nullReporter,
1457+
} as InputConfigT),
1458+
);
1459+
});
1460+
1461+
test('resolves [metro-watchFolders]/N/ prefix against the Nth watch folder', () => {
1462+
expect(
1463+
watchFolderServer._resolveWatchFolderPrefix(
1464+
'./[metro-watchFolders]/1/expo-router/entry',
1465+
),
1466+
).toEqual({
1467+
rootDir: '/external/packages',
1468+
filePath: './expo-router/entry',
1469+
});
1470+
});
1471+
1472+
test('resolves [metro-watchFolders]/0/ prefix against the first watch folder', () => {
1473+
expect(
1474+
watchFolderServer._resolveWatchFolderPrefix(
1475+
'./[metro-watchFolders]/0/app/index',
1476+
),
1477+
).toEqual({
1478+
rootDir: '/project',
1479+
filePath: './app/index',
1480+
});
1481+
});
1482+
1483+
test('resolves [metro-project]/ prefix against projectRoot', () => {
1484+
expect(
1485+
watchFolderServer._resolveWatchFolderPrefix(
1486+
'./[metro-project]/src/App',
1487+
),
1488+
).toEqual({
1489+
rootDir: '/project',
1490+
filePath: './src/App',
1491+
});
1492+
});
1493+
1494+
test('returns null for paths without a recognized prefix', () => {
1495+
expect(
1496+
watchFolderServer._resolveWatchFolderPrefix('./mybundle'),
1497+
).toBeNull();
1498+
});
1499+
1500+
test('returns null for out-of-bounds watchFolder index', () => {
1501+
expect(
1502+
watchFolderServer._resolveWatchFolderPrefix(
1503+
'./[metro-watchFolders]/99/mybundle',
1504+
),
1505+
).toBeNull();
1506+
});
1507+
1508+
test('_getEntryPointAbsolutePath resolves prefixed entry against the corresponding watch folder', () => {
1509+
expect(
1510+
watchFolderServer._getEntryPointAbsolutePath(
1511+
'./[metro-watchFolders]/1/expo-router/entry',
1512+
),
1513+
).toBe('/external/packages/expo-router/entry');
1514+
});
1515+
1516+
test('_getEntryPointAbsolutePath resolves non-prefixed entry against server root', () => {
1517+
expect(
1518+
watchFolderServer._getEntryPointAbsolutePath('./mybundle'),
1519+
).toBe('/project/mybundle');
1520+
});
1521+
});
14391522
});

0 commit comments

Comments
 (0)