Skip to content

Commit 9d77300

Browse files
motiz88facebook-github-bot
authored andcommitted
Extract ProjectRouteMap, refactor path<-->URL mappings in Server (react#1707)
Summary: Extracts a `ProjectRouteMap` class from `Server`, centralising the `[metro-watchFolders]` and `[metro-project]` virtual prefix resolution previously spread across `_resolveWatchFolderPrefix`, `_sourceRequestRoutingMap`, and `_getModuleSourceUrl`. `ProjectRouteMap` provides: - `filePathOfUrlDecodedPathname`: resolves `[metro-project]/...` and `[metro-watchFolders]/N/...` prefixed pathnames to absolute file paths. Returns null for non-prefixed paths or invalid indices. - `urlPathnameOfFilePath`: maps an absolute file path to the corresponding prefixed URL pathname. Server now delegates to `ProjectRouteMap` from `_resolveRelativePath`, `_getEntryPointAbsolutePath`, `_getModuleSourceUrl`, and source file serving. No behavioural change. Changelog: Internal Differential Revision: D104223068
1 parent a5dbeed commit 9d77300

5 files changed

Lines changed: 386 additions & 121 deletions

File tree

packages/metro/src/Server.js

Lines changed: 33 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ import formatBundlingError from './lib/formatBundlingError';
5858
import getGraphId from './lib/getGraphId';
5959
import parseBundleOptionsFromBundleRequestUrl from './lib/parseBundleOptionsFromBundleRequestUrl';
6060
import parseJsonBody from './lib/parseJsonBody';
61+
import ProjectRouteMap from './lib/ProjectRouteMap';
6162
import splitBundleOptions from './lib/splitBundleOptions';
6263
import * as transformHelpers from './lib/transformHelpers';
6364
import {UnableToResolveError} from './node-haste/DependencyGraph/ModuleResolution';
@@ -149,9 +150,7 @@ export default class Server {
149150
_reporter: Reporter;
150151
_serverOptions: ServerOptions | void;
151152
_allowedSuffixesForSourceRequests: ReadonlyArray<string>;
152-
_sourceRequestRoutingMap: ReadonlyArray<
153-
[pathnamePrefix: string, normalizedRootDir: string],
154-
>;
153+
_routeMap: ProjectRouteMap;
155154
_fetchTimings: Array<FetchTiming>;
156155
_activeFetchCount: number;
157156

@@ -178,13 +177,7 @@ export default class Server {
178177
].map(ext => '.' + ext),
179178
),
180179
];
181-
this._sourceRequestRoutingMap = [
182-
['/[metro-project]/', path.resolve(this._config.projectRoot)],
183-
...this._config.watchFolders.map((watchFolder, index) => [
184-
`/[metro-watchFolders]/${index}/`,
185-
path.resolve(watchFolder),
186-
]),
187-
];
180+
this._routeMap = new ProjectRouteMap(config);
188181
this._isEnded = false;
189182
this._fetchTimings = [];
190183
this._activeFetchCount = 0;
@@ -260,8 +253,7 @@ export default class Server {
260253
sourceMapUrl: serializerOptions.sourceMapUrl,
261254
sourceUrl: serializerOptions.sourceUrl,
262255
inlineSourceMap: serializerOptions.inlineSourceMap,
263-
serverRoot:
264-
this._config.server.unstable_serverRoot ?? this._config.projectRoot,
256+
serverRoot: this._routeMap.serverRootDir,
265257
shouldAddToIgnoreList: (module: Module<>) =>
266258
this._shouldAddModuleToIgnoreList(module),
267259
getSourceUrl: (module: Module<>) =>
@@ -365,6 +357,8 @@ export default class Server {
365357
transformOptions,
366358
} = splitBundleOptions(options);
367359

360+
const entryPoint = this._getEntryPointAbsolutePath(entryFile);
361+
368362
const {prepend, graph} = await this._bundler.buildGraph(
369363
entryFile,
370364
transformOptions,
@@ -376,8 +370,6 @@ export default class Server {
376370
},
377371
);
378372

379-
const entryPoint = this._getEntryPointAbsolutePath(entryFile);
380-
381373
return await getRamBundleInfo(entryPoint, prepend, graph, {
382374
asyncRequireModulePath: await this._resolveRelativePath(
383375
this._config.transformer.asyncRequireModulePath,
@@ -406,8 +398,7 @@ export default class Server {
406398
sourceMapUrl: serializerOptions.sourceMapUrl,
407399
sourceUrl: serializerOptions.sourceUrl,
408400
inlineSourceMap: serializerOptions.inlineSourceMap,
409-
serverRoot:
410-
this._config.server.unstable_serverRoot ?? this._config.projectRoot,
401+
serverRoot: this._routeMap.serverRootDir,
411402
shouldAddToIgnoreList: (module: Module<>) =>
412403
this._shouldAddModuleToIgnoreList(module),
413404
getSourceUrl: (module: Module<>) =>
@@ -440,7 +431,7 @@ export default class Server {
440431
processModuleFilter: this._config.serializer.processModuleFilter,
441432
assetPlugins: this._config.transformer.assetPlugins,
442433
platform,
443-
projectRoot: this._getServerRootDir(),
434+
projectRoot: this._routeMap.serverRootDir,
444435
publicPath: this._config.transformer.publicPath,
445436
});
446437
}
@@ -700,52 +691,38 @@ export default class Server {
700691
} else if (pathname === '/symbolicate') {
701692
await this._symbolicate(req, res);
702693
} else {
703-
let handled = false;
704-
for (const [pathnamePrefix, normalizedRootDir] of this
705-
._sourceRequestRoutingMap) {
706-
if (filePathname.startsWith(pathnamePrefix)) {
707-
const relativeFilePathname = filePathname.substr(
708-
pathnamePrefix.length,
709-
);
710-
await this._processSourceRequest(
711-
relativeFilePathname,
712-
normalizedRootDir,
713-
res,
714-
);
715-
handled = true;
716-
break;
717-
}
718-
}
719-
if (!handled) {
694+
const sourceFilePath =
695+
this._routeMap.filePathOfUrlDecodedPathname(filePathname);
696+
if (sourceFilePath != null) {
697+
await this._processSourceRequest(sourceFilePath, res);
698+
} else {
720699
next();
721700
}
722701
}
723702
}
724703

725704
async _processSourceRequest(
726-
relativeFilePathname: string,
727-
rootDir: string,
705+
filePath: string,
728706
res: ServerResponse,
729707
): Promise<void> {
730708
if (
731709
!this._allowedSuffixesForSourceRequests.some(suffix =>
732-
relativeFilePathname.endsWith(suffix),
710+
filePath.endsWith(suffix),
733711
)
734712
) {
735713
res.writeHead(404);
736714
res.end();
737715
return;
738716
}
739717
const depGraph = await this._bundler.getBundler().getDependencyGraph();
740-
const filePath = path.join(rootDir, relativeFilePathname);
741718
try {
742719
await depGraph.getOrComputeSha1(filePath);
743720
} catch {
744721
res.writeHead(404);
745722
res.end();
746723
return;
747724
}
748-
const mimeType = mime.lookup(path.basename(relativeFilePathname));
725+
const mimeType = mime.lookup(path.basename(filePath));
749726
res.setHeader('Content-Type', mimeType);
750727
const stream = fs.createReadStream(filePath);
751728
stream.pipe(res);
@@ -1150,8 +1127,7 @@ export default class Server {
11501127
sourceMapUrl: serializerOptions.sourceMapUrl,
11511128
sourceUrl: serializerOptions.sourceUrl,
11521129
inlineSourceMap: serializerOptions.inlineSourceMap,
1153-
serverRoot:
1154-
this._config.server.unstable_serverRoot ?? this._config.projectRoot,
1130+
serverRoot: this._routeMap.serverRootDir,
11551131
shouldAddToIgnoreList: (module: Module<>) =>
11561132
this._shouldAddModuleToIgnoreList(module),
11571133
getSourceUrl: (module: Module<>) =>
@@ -1622,33 +1598,6 @@ export default class Server {
16221598
);
16231599
}
16241600
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:
1638-
'.' + path.sep + watchFolderMatch[2].split('/').join(path.sep),
1639-
};
1640-
}
1641-
}
1642-
const projectMatch = filePath.match(/^\.\/\[metro-project\]\/(.*)/);
1643-
if (projectMatch != null) {
1644-
return {
1645-
rootDir: path.resolve(this._config.projectRoot),
1646-
filePath: '.' + path.sep + projectMatch[1].split('/').join(path.sep),
1647-
};
1648-
}
1649-
return null;
1650-
}
1651-
16521601
async _resolveRelativePath(
16531602
filePath: string,
16541603
{
@@ -1666,14 +1615,17 @@ export default class Server {
16661615
transformOptions.platform,
16671616
resolverOptions,
16681617
);
1669-
const resolved = this._resolveWatchFolderPrefix(filePath);
1670-
const rootDir =
1671-
resolved != null
1672-
? resolved.rootDir
1673-
: relativeTo === 'server'
1674-
? this._getServerRootDir()
1675-
: this._config.projectRoot;
1676-
const resolvedFilePath = resolved != null ? resolved.filePath : filePath;
1618+
let rootDir;
1619+
let resolvedFilePath;
1620+
if (relativeTo === 'project') {
1621+
rootDir = this._config.projectRoot;
1622+
resolvedFilePath = filePath;
1623+
} else {
1624+
const absolutePath =
1625+
this._routeMap.filePathOfUrlDecodedPathname(filePath);
1626+
rootDir = absolutePath != null ? '/' : this._routeMap.serverRootDir;
1627+
resolvedFilePath = absolutePath ?? filePath;
1628+
}
16771629
return resolutionFn(`${rootDir}/.`, {
16781630
name: resolvedFilePath,
16791631
data: {
@@ -1737,16 +1689,11 @@ export default class Server {
17371689
sourcePaths: SourcePathsMode.Absolute,
17381690
};
17391691

1740-
_getServerRootDir(): string {
1741-
return this._config.server.unstable_serverRoot ?? this._config.projectRoot;
1742-
}
1743-
17441692
_getEntryPointAbsolutePath(entryFile: string): string {
1745-
const resolved = this._resolveWatchFolderPrefix(entryFile);
1746-
if (resolved != null) {
1747-
return path.resolve(resolved.rootDir, resolved.filePath);
1748-
}
1749-
return path.resolve(this._getServerRootDir(), entryFile);
1693+
return (
1694+
this._routeMap.filePathOfUrlDecodedPathname(entryFile) ??
1695+
path.resolve(this._routeMap.serverRootDir, entryFile)
1696+
);
17501697
}
17511698

17521699
// Wait for the server to finish initializing.
@@ -1771,29 +1718,7 @@ export default class Server {
17711718
_getModuleSourceUrl(module: Module<>, mode: SourcePathsMode): string {
17721719
switch (mode) {
17731720
case SourcePathsMode.ServerUrl:
1774-
for (const [pathnamePrefix, normalizedRootDir] of this
1775-
._sourceRequestRoutingMap) {
1776-
if (module.path.startsWith(normalizedRootDir + path.sep)) {
1777-
const relativePath = module.path.slice(
1778-
normalizedRootDir.length + 1,
1779-
);
1780-
const relativePathPosix = relativePath
1781-
.split(path.sep)
1782-
.map(segment => encodeURIComponent(segment))
1783-
.join('/');
1784-
return pathnamePrefix + relativePathPosix;
1785-
}
1786-
}
1787-
// Ordinarily all files should match one of the roots above. If they
1788-
// don't, try to preserve useful information, even if fetching the path
1789-
// from Metro might fail.
1790-
const modulePathPosix = module.path
1791-
.split(path.sep)
1792-
.map(segment => encodeURIComponent(segment))
1793-
.join('/');
1794-
return modulePathPosix.startsWith('/')
1795-
? modulePathPosix
1796-
: '/' + modulePathPosix;
1721+
return this._routeMap.urlPathnameOfFilePath(module.path);
17971722
case SourcePathsMode.Absolute:
17981723
return module.path;
17991724
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
* @format
9+
*/
10+
11+
import type {ConfigT} from 'metro-config';
12+
13+
import path from 'path';
14+
15+
// Matches /[metro-watchFolders]/<index>/... and /[metro-project]/...
16+
// Applied after normalizing ./ and bare paths to start with /.
17+
const EXPLICIT_ROUTE_RE =
18+
/^\/(?:\[metro-watchFolders\]\/(\d+)|\[metro-project\])\/(.*)/s;
19+
20+
/**
21+
* Immutable bidirectional map between URL pathnames and filesystem paths,
22+
* encoding the `[metro-project]` and `[metro-watchFolders]` virtual prefix
23+
* conventions.
24+
*/
25+
export default class ProjectRouteMap {
26+
+serverRootDir: string;
27+
+_projectRootDirPrefix: string;
28+
+_watchFolderDirPrefixes: ReadonlyArray<string>;
29+
+_filePathRoutes: ReadonlyArray<{
30+
rootDirPrefix: string,
31+
pathnamePrefix: string,
32+
}>;
33+
34+
constructor(config: ConfigT) {
35+
this.serverRootDir =
36+
config.server.unstable_serverRoot ?? config.projectRoot;
37+
this._projectRootDirPrefix = path.normalize(config.projectRoot + path.sep);
38+
this._watchFolderDirPrefixes = config.watchFolders.map(wf =>
39+
path.normalize(wf + path.sep),
40+
);
41+
this._filePathRoutes = [
42+
{
43+
rootDirPrefix: this._projectRootDirPrefix,
44+
pathnamePrefix: '/[metro-project]/',
45+
},
46+
...this._watchFolderDirPrefixes.map((wfDir, i) => ({
47+
rootDirPrefix: wfDir,
48+
pathnamePrefix: `/[metro-watchFolders]/${i}/`,
49+
})),
50+
];
51+
}
52+
53+
/**
54+
* Decode a URL pathname and resolve it to an absolute filesystem path.
55+
*/
56+
filePathOfUrlPathname(pathname: string): string | null {
57+
const decoded = pathname
58+
.split('/')
59+
.map(segment => decodeURIComponent(segment))
60+
.join('/');
61+
62+
return this.filePathOfUrlDecodedPathname(decoded);
63+
}
64+
65+
/**
66+
* Convert a URL pathname or entry-file path to an absolute filesystem path.
67+
*
68+
* Accepts both URL-style (`/[metro-watchFolders]/1/foo`) and entry-file-style
69+
* (`./[metro-watchFolders]/1/foo`) prefixes.
70+
*
71+
* Returns `null` when the pathname does not match a known virtual prefix,
72+
* or for out-of-bounds watchFolder indices.
73+
*/
74+
filePathOfUrlDecodedPathname(pathname: string): string | null {
75+
let normalized = pathname;
76+
if (normalized.startsWith('./')) {
77+
normalized = '/' + normalized.slice(2);
78+
} else if (!normalized.startsWith('/')) {
79+
normalized = '/' + normalized;
80+
}
81+
82+
const match = EXPLICIT_ROUTE_RE.exec(normalized);
83+
if (match != null) {
84+
const watchFolderIndexStr = match[1];
85+
const rest = match[2];
86+
let rootDirPrefix;
87+
if (watchFolderIndexStr != null) {
88+
const index = parseInt(watchFolderIndexStr, 10);
89+
if (index >= this._watchFolderDirPrefixes.length) {
90+
return null;
91+
}
92+
rootDirPrefix = this._watchFolderDirPrefixes[index];
93+
} else {
94+
rootDirPrefix = this._projectRootDirPrefix;
95+
}
96+
return path.join(rootDirPrefix, rest.split('/').join(path.sep));
97+
}
98+
99+
return null;
100+
}
101+
102+
/**
103+
* Convert an absolute filesystem path to a URL pathname using the first
104+
* matching virtual prefix.
105+
*
106+
* Falls back to the absolute path (as a POSIX-style URL) when the file is
107+
* not under any configured route.
108+
*/
109+
urlPathnameOfFilePath(filePath: string): string {
110+
for (const {rootDirPrefix, pathnamePrefix} of this._filePathRoutes) {
111+
if (filePath.startsWith(rootDirPrefix)) {
112+
return (
113+
pathnamePrefix +
114+
filePath
115+
.slice(rootDirPrefix.length)
116+
.split(path.sep)
117+
.map(segment => encodeURIComponent(segment))
118+
.join('/')
119+
);
120+
}
121+
}
122+
const pathPosix = filePath
123+
.split(path.sep)
124+
.map(segment => encodeURIComponent(segment))
125+
.join('/');
126+
return pathPosix.startsWith('/') ? pathPosix : '/' + pathPosix;
127+
}
128+
}

0 commit comments

Comments
 (0)