Skip to content

Commit 4e9cc4b

Browse files
robhoganfacebook-github-bot
authored andcommitted
Disallow Haste modules with separators, and require Haste packages to have standard valid package names
Summary: Currently, with `allowHaste`, an input `moduleName` of `foo/bar/baz` will call `resolveHasteModule` with `foo/bar/baz` and `resolveHastePackage` with each of `foo/bar/baz`, `foo/bar` and `foo` until a match is found. This is quite inefficient at the top of the resolver algorithm, especially for deeply nested paths. We can tidy up a bit here by narrowing the definition of a Haste module name and a Haste package name. - A Haste module name must not contain path separators - A Haste package name must be a valid (npm) package name, i.e. `'simple'` or `'scoped/package'`. ``` - **[Breaking]**: Disallow Haste modules with separators and Haste packages with invalid package names. ``` Reviewed By: huntie Differential Revision: D64323177 fbshipit-source-id: 0f3e0342beb13499fc91f086b26aff339f96e877
1 parent c1c80c7 commit 4e9cc4b

File tree

2 files changed

+108
-38
lines changed

2 files changed

+108
-38
lines changed

packages/metro-resolver/src/__tests__/index-test.js

+57-1
Original file line numberDiff line numberDiff line change
@@ -313,13 +313,69 @@ test('resolves Haste modules', () => {
313313
});
314314
});
315315

316+
test('does not call resolveHasteModule for a specifier with separators', () => {
317+
const resolveHasteModule = jest.fn();
318+
expect(() =>
319+
Resolver.resolve(
320+
{
321+
...CONTEXT,
322+
resolveHasteModule,
323+
},
324+
'Foo/bar',
325+
null,
326+
),
327+
).toThrow();
328+
expect(resolveHasteModule).not.toHaveBeenCalled();
329+
});
330+
316331
test('resolves a Haste package', () => {
317332
expect(Resolver.resolve(CONTEXT, 'some-package', null)).toEqual({
318333
type: 'sourceFile',
319334
filePath: '/haste/some-package/main.js',
320335
});
321336
});
322337

338+
test.each([
339+
['simple', 'simple'],
340+
['simple/with/subpath', 'simple'],
341+
['@scoped/package', '@scoped/package'],
342+
['@scoped/with/subpath', '@scoped/with'],
343+
])(
344+
'calls resolveHastePackage for specifier %s with %s',
345+
(specifier, expectedHastePackageCandidate) => {
346+
const resolveHastePackage = jest.fn();
347+
expect(() =>
348+
Resolver.resolve(
349+
{
350+
...CONTEXT,
351+
resolveHastePackage,
352+
},
353+
specifier,
354+
null,
355+
),
356+
).toThrow();
357+
expect(resolveHastePackage).toHaveBeenCalledWith(
358+
expectedHastePackageCandidate,
359+
);
360+
expect(resolveHastePackage).toHaveBeenCalledTimes(1);
361+
},
362+
);
363+
364+
test('does not call resolveHastePackage for invalid specifier @notvalid', () => {
365+
const resolveHastePackage = jest.fn();
366+
expect(() =>
367+
Resolver.resolve(
368+
{
369+
...CONTEXT,
370+
resolveHastePackage,
371+
},
372+
'@notvalid',
373+
null,
374+
),
375+
).toThrow();
376+
expect(resolveHastePackage).not.toHaveBeenCalled();
377+
});
378+
323379
test('resolves a file inside a Haste package', () => {
324380
expect(
325381
Resolver.resolve(CONTEXT, 'some-package/subdir/other-file', null),
@@ -333,7 +389,7 @@ test('throws a descriptive error when a file inside a Haste package cannot be re
333389
expect(() => {
334390
Resolver.resolve(CONTEXT, 'some-package/subdir/does-not-exist', null);
335391
}).toThrowErrorMatchingInlineSnapshot(`
336-
"While resolving module \`some-package/subdir/does-not-exist\`, the Haste package \`some-package\` was found. However the module \`subdir/does-not-exist\` could not be found within the package. Indeed, none of these files exist:
392+
"While resolving module \`some-package/subdir/does-not-exist\`, the Haste package \`some-package\` was found. However the subpath \`./subdir/does-not-exist\` could not be found within the package. Indeed, none of these files exist:
337393
338394
* \`/haste/some-package/subdir/does-not-exist(.js|.jsx|.json|.ts|.tsx)\`
339395
* \`/haste/some-package/subdir/does-not-exist\`"

packages/metro-resolver/src/resolve.js

+51-37
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,15 @@ import resolveAsset from './resolveAsset';
3232
import isAssetFile from './utils/isAssetFile';
3333
import path from 'path';
3434

35+
type ParsedBareSpecifier = $ReadOnly<{
36+
isSinglePart: boolean,
37+
isValidPackageName: boolean,
38+
firstPart: string,
39+
normalizedSpecifier: string,
40+
packageName: string,
41+
posixSubpath: string,
42+
}>;
43+
3544
function resolve(
3645
context: ResolutionContext,
3746
moduleName: string,
@@ -94,14 +103,24 @@ function resolve(
94103

95104
/**
96105
* At this point, realModuleName is not a "direct" (absolute or relative)
97-
* import, so it's either Haste name or a package specifier.
106+
* import, so it's a bare specifier - for our purposes either Haste name
107+
* or a package specifier.
98108
*/
99109

110+
const parsedSpecifier = parseBareSpecifier(realModuleName);
111+
100112
if (context.allowHaste) {
101-
const normalizedName = normalizePath(realModuleName);
102-
const result = resolveHasteName(context, normalizedName, platform);
103-
if (result.type === 'resolved') {
104-
return result.resolution;
113+
if (parsedSpecifier.isSinglePart) {
114+
const result = context.resolveHasteModule(parsedSpecifier.firstPart);
115+
if (result != null) {
116+
return {type: 'sourceFile', filePath: result};
117+
}
118+
}
119+
if (parsedSpecifier.isValidPackageName) {
120+
const result = resolveHastePackage(context, parsedSpecifier, platform);
121+
if (result.type === 'resolved') {
122+
return result.resolution;
123+
}
105124
}
106125
}
107126

@@ -131,8 +150,6 @@ function resolve(
131150

132151
const extraPaths = [];
133152

134-
const parsedSpecifier = parsePackageSpecifier(realModuleName);
135-
136153
const {extraNodeModules} = context;
137154
if (extraNodeModules && extraNodeModules[parsedSpecifier.packageName]) {
138155
const newPackageName = extraNodeModules[parsedSpecifier.packageName];
@@ -179,35 +196,51 @@ function resolve(
179196
throw new FailedToResolveNameError(nodeModulesPaths, extraPaths);
180197
}
181198

182-
function parsePackageSpecifier(specifier: string) {
199+
function parseBareSpecifier(specifier: string): ParsedBareSpecifier {
183200
const normalized =
184201
path.sep === '/' ? specifier : specifier.replaceAll('\\', '/');
185202
const firstSepIdx = normalized.indexOf('/');
186203
if (normalized.startsWith('@') && firstSepIdx !== -1) {
187204
const secondSepIdx = normalized.indexOf('/', firstSepIdx + 1);
188205
if (secondSepIdx === -1) {
206+
// @foo/bar (valid scoped, no subpath)
189207
return {
208+
isSinglePart: false,
209+
isValidPackageName: true,
190210
firstPart: normalized.slice(0, firstSepIdx),
211+
normalizedSpecifier: normalized,
191212
packageName: normalized,
192213
posixSubpath: '.',
193214
};
194215
}
216+
// @foo/bar[/subpath] (valid scoped with subpath)
195217
return {
218+
isSinglePart: false,
219+
isValidPackageName: true,
196220
firstPart: normalized.slice(0, firstSepIdx),
221+
normalizedSpecifier: normalized,
197222
packageName: normalized.slice(0, secondSepIdx),
198223
posixSubpath: '.' + normalized.slice(secondSepIdx),
199224
};
200225
}
226+
// foo or @foo, no subpath. Valid if doesn't start with '@'.
201227
if (firstSepIdx === -1) {
202228
return {
229+
isSinglePart: true,
230+
isValidPackageName: !normalized.startsWith('@'),
203231
firstPart: normalized,
232+
normalizedSpecifier: normalized,
204233
packageName: normalized,
205234
posixSubpath: '.',
206235
};
207236
}
208237
const packageName = normalized.slice(0, firstSepIdx);
238+
// foo/subpath, valid, not scoped, with subpath
209239
return {
240+
isSinglePart: false,
241+
isValidPackageName: true,
210242
firstPart: packageName,
243+
normalizedSpecifier: normalized,
211244
packageName,
212245
posixSubpath: '.' + normalized.slice(firstSepIdx),
213246
};
@@ -260,31 +293,22 @@ function resolveModulePath(
260293
}
261294

262295
/**
263-
* Resolve a module as a Haste module or package. For example we might try to
264-
* resolve `Foo`, that is provided by file `/smth/Foo.js`. Or, in the case of
265-
* a Haste package, it could be `/smth/Foo/index.js`.
296+
* Resolve a specifier as a Haste package.
266297
*/
267-
function resolveHasteName(
298+
function resolveHastePackage(
268299
context: ResolutionContext,
269-
moduleName: string,
300+
{
301+
normalizedSpecifier: moduleName,
302+
packageName,
303+
posixSubpath: pathInModule,
304+
}: ParsedBareSpecifier,
270305
platform: string | null,
271306
): Result<Resolution, void> {
272-
const modulePath = context.resolveHasteModule(moduleName);
273-
if (modulePath != null) {
274-
return resolvedAs({type: 'sourceFile', filePath: modulePath});
275-
}
276-
let packageName = moduleName;
277-
let packageJsonPath = context.resolveHastePackage(packageName);
278-
while (packageJsonPath == null && packageName && packageName !== '.') {
279-
packageName = path.dirname(packageName);
280-
packageJsonPath = context.resolveHastePackage(packageName);
281-
}
307+
const packageJsonPath = context.resolveHastePackage(packageName);
282308
if (packageJsonPath == null) {
283309
return failedFor();
284310
}
285-
const packageDirPath = path.dirname(packageJsonPath);
286-
const pathInModule = moduleName.substring(packageName.length + 1);
287-
const potentialModulePath = path.join(packageDirPath, pathInModule);
311+
const potentialModulePath = path.join(packageJsonPath, '..', pathInModule);
288312
const result = resolvePackage(context, potentialModulePath, platform);
289313
if (result.type === 'resolved') {
290314
return result;
@@ -309,7 +333,7 @@ class MissingFileInHastePackageError extends Error {
309333
super(
310334
`While resolving module \`${opts.moduleName}\`, ` +
311335
`the Haste package \`${opts.packageName}\` was found. However the ` +
312-
`module \`${opts.pathInModule}\` could not be found within ` +
336+
`subpath \`${opts.pathInModule}\` could not be found within ` +
313337
'the package. Indeed, none of these files exist:\n\n' +
314338
[opts.candidates.file, opts.candidates.dir]
315339
.filter(Boolean)
@@ -588,16 +612,6 @@ function isRelativeImport(filePath: string) {
588612
return /^[.][.]?(?:[/]|$)/.test(filePath);
589613
}
590614

591-
function normalizePath(modulePath: any | string) {
592-
if (path.sep === '/') {
593-
modulePath = path.normalize(modulePath);
594-
} else if (path.posix) {
595-
modulePath = path.posix.normalize(modulePath);
596-
}
597-
598-
return modulePath.replace(/\/$/, '');
599-
}
600-
601615
function resolvedAs<TResolution, TCandidates>(
602616
resolution: TResolution,
603617
): Result<TResolution, TCandidates> {

0 commit comments

Comments
 (0)