Skip to content

Commit a2224e9

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 is intended here, but this does incidentally fix a Windows bug that was affecting the original implementation. Changelog: * **[Fix]:** Fix `/[metro-project]/` and `/[metro-watchFolders]/` bundle serving on Windows. Reviewed By: huntie Differential Revision: D104223068
1 parent e51a59a commit a2224e9

6 files changed

Lines changed: 438 additions & 187 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
}

packages/metro/src/integration_tests/__tests__/server-test.js

Lines changed: 52 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -122,75 +122,61 @@ describe('Metro development server serves bundles via HTTP', () => {
122122
);
123123
});
124124

125-
// TODO(T000000): Fix virtual-prefix URL resolution on Windows.
126-
// path.sep differences cause entry point resolution to fail.
127-
(process.platform === 'win32' ? test.skip : test)(
128-
'should serve bundles with [metro-watchFolders] entry point',
129-
async () => {
130-
expect(
131-
await downloadAndExec(
132-
'/[metro-watchFolders]/1/metro/src/integration_tests/basic_bundle/TestBundle.bundle?platform=ios&dev=true&minify=false',
133-
),
134-
).toBeDefined();
135-
},
136-
);
125+
test('should serve bundles with [metro-watchFolders] entry point', async () => {
126+
expect(
127+
await downloadAndExec(
128+
'/[metro-watchFolders]/1/metro/src/integration_tests/basic_bundle/TestBundle.bundle?platform=ios&dev=true&minify=false',
129+
),
130+
).toBeDefined();
131+
});
137132

138-
(process.platform === 'win32' ? test.skip : test)(
139-
'should serve bundles with [metro-project] entry point',
140-
async () => {
141-
expect(
142-
await downloadAndExec(
143-
'/[metro-project]/TestBundle.bundle?platform=ios&dev=true&minify=false',
144-
),
145-
).toBeDefined();
146-
},
147-
);
133+
test('should serve bundles with [metro-project] entry point', async () => {
134+
expect(
135+
await downloadAndExec(
136+
'/[metro-project]/TestBundle.bundle?platform=ios&dev=true&minify=false',
137+
),
138+
).toBeDefined();
139+
});
148140

149-
(process.platform === 'win32' ? test.skip : test)(
150-
'[metro-project] source map resolves same modules as non-prefixed',
151-
async () => {
152-
const directResponse = await fetchAndClose(
153-
'http://localhost:' +
154-
httpServer.address().port +
155-
'/TestBundle.map?platform=ios&dev=true&minify=false',
156-
);
157-
const prefixedResponse = await fetchAndClose(
158-
'http://localhost:' +
159-
httpServer.address().port +
160-
'/[metro-project]/TestBundle.map?platform=ios&dev=true&minify=false',
161-
);
162-
expect(directResponse.ok).toBe(true);
163-
expect(prefixedResponse.ok).toBe(true);
164-
const directMap = await directResponse.json();
165-
const prefixedMap = await prefixedResponse.json();
166-
expect([...prefixedMap.sources].sort()).toEqual(
167-
[...directMap.sources].sort(),
168-
);
169-
},
170-
);
141+
test('[metro-project] source map resolves same modules as non-prefixed', async () => {
142+
const directResponse = await fetchAndClose(
143+
'http://localhost:' +
144+
httpServer.address().port +
145+
'/TestBundle.map?platform=ios&dev=true&minify=false',
146+
);
147+
const prefixedResponse = await fetchAndClose(
148+
'http://localhost:' +
149+
httpServer.address().port +
150+
'/[metro-project]/TestBundle.map?platform=ios&dev=true&minify=false',
151+
);
152+
expect(directResponse.ok).toBe(true);
153+
expect(prefixedResponse.ok).toBe(true);
154+
const directMap = await directResponse.json();
155+
const prefixedMap = await prefixedResponse.json();
156+
expect([...prefixedMap.sources].sort()).toEqual(
157+
[...directMap.sources].sort(),
158+
);
159+
});
171160

172-
(process.platform === 'win32' ? test.skip : test)(
173-
'[metro-watchFolders] source map resolves same modules as non-prefixed',
174-
async () => {
175-
const directResponse = await fetchAndClose(
176-
'http://localhost:' +
177-
httpServer.address().port +
178-
'/TestBundle.map?platform=ios&dev=true&minify=false',
179-
);
180-
const watchFolderResponse = await fetchAndClose(
181-
'http://localhost:' +
182-
httpServer.address().port +
183-
'/[metro-watchFolders]/1/metro/src/integration_tests/basic_bundle/TestBundle.map?platform=ios&dev=true&minify=false',
184-
);
185-
expect(directResponse.ok).toBe(true);
186-
expect(watchFolderResponse.ok).toBe(true);
187-
const directMap = await directResponse.json();
188-
const watchFolderMap = await watchFolderResponse.json();
189-
expect([...watchFolderMap.sources].sort()).toEqual(
190-
[...directMap.sources].sort(),
191-
);
192-
},
193-
);
161+
test('[metro-watchFolders] source map resolves same modules as non-prefixed', async () => {
162+
const directResponse = await fetchAndClose(
163+
'http://localhost:' +
164+
httpServer.address().port +
165+
'/TestBundle.map?platform=ios&dev=true&minify=false',
166+
);
167+
const watchFolderResponse = await fetchAndClose(
168+
'http://localhost:' +
169+
httpServer.address().port +
170+
'/[metro-watchFolders]/1/metro/src/integration_tests/basic_bundle/TestBundle.map?platform=ios&dev=true&minify=false',
171+
);
172+
expect(directResponse.ok).toBe(true);
173+
expect(watchFolderResponse.ok).toBe(true);
174+
const directMap = await directResponse.json();
175+
const watchFolderMap = await watchFolderResponse.json();
176+
expect([...watchFolderMap.sources].sort()).toEqual(
177+
[...directMap.sources].sort(),
178+
);
179+
});
194180

195181
test('responds with 404 for [metro-watchFolders] with out-of-bounds index', async () => {
196182
const response = await fetchAndClose(

0 commit comments

Comments
 (0)