mirror of https://github.com/status-im/metro.git
metro: extract metro-resolver package
Reviewed By: rafeca Differential Revision: D7011526 fbshipit-source-id: 523c54792cef1c31eef281e6e39bc25b20e2ceec
This commit is contained in:
parent
5aa19c7626
commit
d110c7b9ea
|
@ -0,0 +1,5 @@
|
|||
**/__mocks__/**
|
||||
**/__tests__/**
|
||||
build
|
||||
src.real
|
||||
yarn.lock
|
|
@ -0,0 +1,3 @@
|
|||
# metro-resolver
|
||||
|
||||
🚇 [Metro](https://facebook.github.io/metro/) resolution logic
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"version": "0.26.0",
|
||||
"name": "metro-resolver",
|
||||
"description": "🚇 Metro resolution logic",
|
||||
"main": "src",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git@github.com:facebook/metro.git"
|
||||
},
|
||||
"scripts": {
|
||||
"prepare-release": "test -d build && rm -rf src.real && mv src src.real && mv build src",
|
||||
"cleanup-release": "test ! -e build && mv src build && mv src.real src"
|
||||
},
|
||||
"dependencies": {
|
||||
"absolute-path": "^0.0.0"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"presets": [ "react-native" ],
|
||||
"plugins": []
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
* @format
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const formatFileCandidates = require('./formatFileCandidates');
|
||||
|
||||
import type {FileCandidates} from './types';
|
||||
|
||||
class InvalidPackageError extends Error {
|
||||
/**
|
||||
* The file candidates we tried to find to resolve the `main` field of the
|
||||
* package. Ex. `/js/foo/beep(.js|.json)?` if `main` is specifying `./beep`
|
||||
* as the entry point.
|
||||
*/
|
||||
fileCandidates: FileCandidates;
|
||||
/**
|
||||
* The 'index' file candidates we tried to find to resolve the `main` field of
|
||||
* the package. Ex. `/js/foo/beep/index(.js|.json)?` if `main` is specifying
|
||||
* `./beep` as the entry point.
|
||||
*/
|
||||
indexCandidates: FileCandidates;
|
||||
/**
|
||||
* The module path prefix we where trying to resolve. For example './beep'.
|
||||
*/
|
||||
mainPrefixPath: string;
|
||||
/**
|
||||
* Full path the package we were trying to resolve.
|
||||
* Ex. `/js/foo/package.json`.
|
||||
*/
|
||||
packageJsonPath: string;
|
||||
|
||||
constructor(opts: {|
|
||||
+fileCandidates: FileCandidates,
|
||||
+indexCandidates: FileCandidates,
|
||||
+mainPrefixPath: string,
|
||||
+packageJsonPath: string,
|
||||
|}) {
|
||||
super(
|
||||
`The package \`${opts.packageJsonPath}\` is invalid because it ` +
|
||||
`specifies a \`main\` module field that could not be resolved (` +
|
||||
`\`${opts.mainPrefixPath}\`. Indeed, none of these files exist:\n\n` +
|
||||
` * \`${formatFileCandidates(opts.fileCandidates)}\`\n` +
|
||||
` * \`${formatFileCandidates(opts.indexCandidates)}\``,
|
||||
);
|
||||
Object.assign(this, opts);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = InvalidPackageError;
|
|
@ -0,0 +1,8 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`throws on invalid node package 1`] = `
|
||||
"The package \`/root/node_modules/invalid/package.json\` is invalid because it specifies a \`main\` module field that could not be resolved (\`/root/node_modules/invalid/main\`. Indeed, none of these files exist:
|
||||
|
||||
* \`/root/node_modules/invalid/main(|.js)\`
|
||||
* \`/root/node_modules/invalid/main/index(|.js)\`"
|
||||
`;
|
|
@ -0,0 +1,139 @@
|
|||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
* @format
|
||||
* @emails oncall+javascript_foundation
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const Resolver = require('../index');
|
||||
|
||||
const path = require('path');
|
||||
|
||||
import type {ResolutionContext} from '../index';
|
||||
|
||||
const CONTEXT: ResolutionContext = (() => {
|
||||
const fileSet = new Set();
|
||||
(function fillFileSet(fileTree, prefix) {
|
||||
for (const entName in fileTree) {
|
||||
const entPath = path.join(prefix, entName);
|
||||
if (fileTree[entName] === true) {
|
||||
fileSet.add(entPath);
|
||||
continue;
|
||||
}
|
||||
fillFileSet(fileTree[entName], entPath);
|
||||
}
|
||||
})(
|
||||
{
|
||||
root: {
|
||||
project: {
|
||||
'foo.js': true,
|
||||
'bar.js': true,
|
||||
},
|
||||
smth: {
|
||||
'beep.js': true,
|
||||
},
|
||||
node_modules: {
|
||||
tadam: {
|
||||
'package.json': true,
|
||||
'main.js': true,
|
||||
},
|
||||
invalid: {
|
||||
'package.json': true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
'/',
|
||||
);
|
||||
return {
|
||||
allowHaste: true,
|
||||
doesFileExist: filePath => fileSet.has(filePath),
|
||||
extraNodeModules: null,
|
||||
getPackageMainPath: dirPath => path.join(path.dirname(dirPath), 'main'),
|
||||
isAssetFile: () => false,
|
||||
originModulePath: '/root/project/foo.js',
|
||||
preferNativePlatform: false,
|
||||
redirectModulePath: filePath => filePath,
|
||||
resolveAsset: filePath => null,
|
||||
resolveHasteModule: name => null,
|
||||
resolveHastePackage: name => null,
|
||||
sourceExts: ['js'],
|
||||
};
|
||||
})();
|
||||
|
||||
it('resolves relative path', () => {
|
||||
expect(Resolver.resolve(CONTEXT, './bar', null)).toEqual({
|
||||
type: 'resolved',
|
||||
resolution: {type: 'sourceFile', filePath: '/root/project/bar.js'},
|
||||
});
|
||||
});
|
||||
|
||||
it('resolves relative path in another folder', () => {
|
||||
expect(Resolver.resolve(CONTEXT, '../smth/beep', null)).toEqual({
|
||||
type: 'resolved',
|
||||
resolution: {type: 'sourceFile', filePath: '/root/smth/beep.js'},
|
||||
});
|
||||
});
|
||||
|
||||
it('resolves a simple node_modules', () => {
|
||||
expect(Resolver.resolve(CONTEXT, 'tadam', null)).toEqual({
|
||||
type: 'resolved',
|
||||
resolution: {
|
||||
type: 'sourceFile',
|
||||
filePath: '/root/node_modules/tadam/main.js',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('fails to resolve relative path', () => {
|
||||
expect(Resolver.resolve(CONTEXT, './tadam', null)).toEqual({
|
||||
type: 'failed',
|
||||
candidates: {
|
||||
type: 'modulePath',
|
||||
which: {
|
||||
dir: {
|
||||
candidateExts: ['', '.js'],
|
||||
filePathPrefix: '/root/project/tadam/index',
|
||||
type: 'sourceFile',
|
||||
},
|
||||
file: {
|
||||
candidateExts: ['', '.js'],
|
||||
filePathPrefix: '/root/project/tadam',
|
||||
type: 'sourceFile',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('throws on invalid node package', () => {
|
||||
try {
|
||||
Resolver.resolve(CONTEXT, 'invalid', null);
|
||||
throw new Error('should have thrown');
|
||||
} catch (error) {
|
||||
if (!(error instanceof Resolver.InvalidPackageError)) {
|
||||
throw error;
|
||||
}
|
||||
expect(error.message).toMatchSnapshot();
|
||||
expect(error.fileCandidates).toEqual({
|
||||
candidateExts: ['', '.js'],
|
||||
filePathPrefix: '/root/node_modules/invalid/main',
|
||||
type: 'sourceFile',
|
||||
});
|
||||
expect(error.indexCandidates).toEqual({
|
||||
candidateExts: ['', '.js'],
|
||||
filePathPrefix: '/root/node_modules/invalid/main/index',
|
||||
type: 'sourceFile',
|
||||
});
|
||||
expect(error.mainPrefixPath).toBe('/root/node_modules/invalid/main');
|
||||
expect(error.packageJsonPath).toBe(
|
||||
'/root/node_modules/invalid/package.json',
|
||||
);
|
||||
}
|
||||
});
|
|
@ -0,0 +1,22 @@
|
|||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
* @format
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import type {FileCandidates} from './types';
|
||||
|
||||
function formatFileCandidates(candidates: FileCandidates): string {
|
||||
if (candidates.type === 'asset') {
|
||||
return candidates.name;
|
||||
}
|
||||
return `${candidates.filePathPrefix}(${candidates.candidateExts.join('|')})`;
|
||||
}
|
||||
|
||||
module.exports = formatFileCandidates;
|
|
@ -0,0 +1,35 @@
|
|||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
* @format
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
export type {
|
||||
DoesFileExist,
|
||||
IsAssetFile,
|
||||
ResolutionContext,
|
||||
ResolveAsset,
|
||||
} from './resolve';
|
||||
export type {
|
||||
AssetFileResolution,
|
||||
Candidates,
|
||||
FileAndDirCandidates,
|
||||
FileCandidates,
|
||||
FileResolution,
|
||||
Resolution,
|
||||
Result,
|
||||
} from './types';
|
||||
|
||||
const Resolver = {
|
||||
resolve: require('./resolve'),
|
||||
InvalidPackageError: require('./InvalidPackageError'),
|
||||
formatFileCandidates: require('./formatFileCandidates'),
|
||||
};
|
||||
|
||||
module.exports = Resolver;
|
|
@ -0,0 +1,519 @@
|
|||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
* @format
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const InvalidPackageError = require('./InvalidPackageError');
|
||||
|
||||
const formatFileCandidates = require('./formatFileCandidates');
|
||||
const isAbsolutePath = require('absolute-path');
|
||||
const path = require('path');
|
||||
|
||||
import type {
|
||||
AssetFileResolution,
|
||||
Candidates,
|
||||
FileAndDirCandidates,
|
||||
FileCandidates,
|
||||
FileResolution,
|
||||
Resolution,
|
||||
Result,
|
||||
} from './types';
|
||||
|
||||
export type ResolutionContext = ModulePathContext &
|
||||
HasteContext & {
|
||||
originModulePath: string,
|
||||
allowHaste: boolean,
|
||||
extraNodeModules: ?{[string]: string},
|
||||
};
|
||||
|
||||
function resolve(
|
||||
context: ResolutionContext,
|
||||
moduleName: string,
|
||||
platform: string | null,
|
||||
): Result<Resolution, Candidates> {
|
||||
if (isRelativeImport(moduleName) || isAbsolutePath(moduleName)) {
|
||||
return resolveModulePath(context, moduleName, platform);
|
||||
}
|
||||
const realModuleName = context.redirectModulePath(moduleName);
|
||||
// exclude
|
||||
if (realModuleName === false) {
|
||||
return resolvedAs({type: 'empty'});
|
||||
}
|
||||
|
||||
const {originModulePath} = context;
|
||||
if (isRelativeImport(realModuleName) || isAbsolutePath(realModuleName)) {
|
||||
// derive absolute path /.../node_modules/originModuleDir/realModuleName
|
||||
const fromModuleParentIdx =
|
||||
originModulePath.lastIndexOf('node_modules' + path.sep) + 13;
|
||||
const originModuleDir = originModulePath.slice(
|
||||
0,
|
||||
originModulePath.indexOf(path.sep, fromModuleParentIdx),
|
||||
);
|
||||
const absPath = path.join(originModuleDir, realModuleName);
|
||||
return resolveModulePath(context, absPath, platform);
|
||||
}
|
||||
|
||||
// At that point we only have module names that
|
||||
// aren't relative paths nor absolute paths.
|
||||
if (context.allowHaste) {
|
||||
const normalizedName = normalizePath(realModuleName);
|
||||
const result = resolveHasteName(context, normalizedName, platform);
|
||||
if (result.type === 'resolved') {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
const dirPaths = [];
|
||||
for (
|
||||
let currDir = path.dirname(originModulePath);
|
||||
currDir !== '.' && currDir !== path.parse(originModulePath).root;
|
||||
currDir = path.dirname(currDir)
|
||||
) {
|
||||
const searchPath = path.join(currDir, 'node_modules');
|
||||
dirPaths.push(path.join(searchPath, realModuleName));
|
||||
}
|
||||
|
||||
const extraPaths = [];
|
||||
const {extraNodeModules} = context;
|
||||
if (extraNodeModules) {
|
||||
const bits = path.normalize(moduleName).split(path.sep);
|
||||
const packageName = bits[0];
|
||||
if (extraNodeModules[packageName]) {
|
||||
bits[0] = extraNodeModules[packageName];
|
||||
extraPaths.push(path.join.apply(path, bits));
|
||||
}
|
||||
}
|
||||
|
||||
const allDirPaths = dirPaths.concat(extraPaths);
|
||||
for (let i = 0; i < allDirPaths.length; ++i) {
|
||||
const result = resolveFileOrDir(context, allDirPaths[i], platform);
|
||||
if (result.type === 'resolved') {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return failedFor({type: 'moduleName', dirPaths, extraPaths});
|
||||
}
|
||||
|
||||
type ModulePathContext = FileOrDirContext & {
|
||||
/**
|
||||
* Full path of the module that is requiring or importing the module to be
|
||||
* resolved.
|
||||
*/
|
||||
+originModulePath: string,
|
||||
/**
|
||||
* Lookup the module's closest `package.json` and process the redirects
|
||||
* metadata. Return an absolute path to the resolved path.
|
||||
*/
|
||||
+redirectModulePath: (modulePath: string) => string | false,
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolve any kind of module path, whether it's a file or a directory.
|
||||
* For example we may want to resolve './foobar'. The closest
|
||||
* `package.json` may define a redirection for this path, for example
|
||||
* `/smth/lib/foobar`, that may be further resolved to
|
||||
* `/smth/lib/foobar/index.ios.js`.
|
||||
*/
|
||||
function resolveModulePath(
|
||||
context: ModulePathContext,
|
||||
toModuleName: string,
|
||||
platform: string | null,
|
||||
): Result<Resolution, Candidates> {
|
||||
const modulePath = isAbsolutePath(toModuleName)
|
||||
? resolveWindowsPath(toModuleName)
|
||||
: path.join(path.dirname(context.originModulePath), toModuleName);
|
||||
const redirectedPath = context.redirectModulePath(modulePath);
|
||||
if (redirectedPath === false) {
|
||||
return resolvedAs({type: 'empty'});
|
||||
}
|
||||
const result = resolveFileOrDir(context, redirectedPath, platform);
|
||||
if (result.type === 'resolved') {
|
||||
return result;
|
||||
}
|
||||
return failedFor({type: 'modulePath', which: result.candidates});
|
||||
}
|
||||
|
||||
type HasteContext = FileOrDirContext & {
|
||||
/**
|
||||
* Given a name, this should return the full path to the file that provides
|
||||
* a Haste module of that name. Ex. for `Foo` it may return `/smth/Foo.js`.
|
||||
*/
|
||||
+resolveHasteModule: (name: string) => ?string,
|
||||
/**
|
||||
* Given a name, this should return the full path to the package manifest that
|
||||
* provides a Haste package of that name. Ex. for `Foo` it may return
|
||||
* `/smth/Foo/package.json`.
|
||||
*/
|
||||
+resolveHastePackage: (name: string) => ?string,
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolve a module as a Haste module or package. For example we might try to
|
||||
* resolve `Foo`, that is provided by file `/smth/Foo.js`. Or, in the case of
|
||||
* a Haste package, it could be `/smth/Foo/index.js`.
|
||||
*/
|
||||
function resolveHasteName(
|
||||
context: HasteContext,
|
||||
moduleName: string,
|
||||
platform: string | null,
|
||||
): Result<FileResolution, void> {
|
||||
const modulePath = context.resolveHasteModule(moduleName);
|
||||
if (modulePath != null) {
|
||||
return resolvedAs({type: 'sourceFile', filePath: modulePath});
|
||||
}
|
||||
let packageName = moduleName;
|
||||
let packageJsonPath = context.resolveHastePackage(packageName);
|
||||
while (packageJsonPath == null && packageName && packageName !== '.') {
|
||||
packageName = path.dirname(packageName);
|
||||
packageJsonPath = context.resolveHastePackage(packageName);
|
||||
}
|
||||
if (packageJsonPath == null) {
|
||||
return failedFor();
|
||||
}
|
||||
const packageDirPath = path.dirname(packageJsonPath);
|
||||
const pathInModule = moduleName.substring(packageName.length + 1);
|
||||
const potentialModulePath = path.join(packageDirPath, pathInModule);
|
||||
const result = resolveFileOrDir(context, potentialModulePath, platform);
|
||||
if (result.type === 'resolved') {
|
||||
return result;
|
||||
}
|
||||
const {candidates} = result;
|
||||
const opts = {moduleName, packageName, pathInModule, candidates};
|
||||
throw new MissingFileInHastePackageError(opts);
|
||||
}
|
||||
|
||||
class MissingFileInHastePackageError extends Error {
|
||||
candidates: FileAndDirCandidates;
|
||||
moduleName: string;
|
||||
packageName: string;
|
||||
pathInModule: string;
|
||||
|
||||
constructor(opts: {|
|
||||
+candidates: FileAndDirCandidates,
|
||||
+moduleName: string,
|
||||
+packageName: string,
|
||||
+pathInModule: string,
|
||||
|}) {
|
||||
super(
|
||||
`While resolving module \`${opts.moduleName}\`, ` +
|
||||
`the Haste package \`${opts.packageName}\` was found. However the ` +
|
||||
`module \`${opts.pathInModule}\` could not be found within ` +
|
||||
`the package. Indeed, none of these files exist:\n\n` +
|
||||
` * \`${formatFileCandidates(opts.candidates.file)}\`\n` +
|
||||
` * \`${formatFileCandidates(opts.candidates.dir)}\``,
|
||||
);
|
||||
Object.assign(this, opts);
|
||||
}
|
||||
}
|
||||
|
||||
type FileOrDirContext = FileContext & {
|
||||
/**
|
||||
* This should return the path of the "main" module of the specified
|
||||
* `package.json` file, after post-processing: for example, applying the
|
||||
* 'browser' field if necessary.
|
||||
*
|
||||
* FIXME: move the post-processing here. Right now it is
|
||||
* located in `node-haste/Package.js`, and fully duplicated in
|
||||
* `ModuleGraph/node-haste/Package.js` (!)
|
||||
*/
|
||||
+getPackageMainPath: (packageJsonPath: string) => string,
|
||||
};
|
||||
|
||||
/**
|
||||
* In the NodeJS-style module resolution scheme we want to check potential
|
||||
* paths both as directories and as files. For example, `/foo/bar` may resolve
|
||||
* to `/foo/bar.js` (preferred), but it might also be `/foo/bar/index.js`, or
|
||||
* even a package directory.
|
||||
*/
|
||||
function resolveFileOrDir(
|
||||
context: FileOrDirContext,
|
||||
potentialModulePath: string,
|
||||
platform: string | null,
|
||||
): Result<FileResolution, FileAndDirCandidates> {
|
||||
const dirPath = path.dirname(potentialModulePath);
|
||||
const fileNameHint = path.basename(potentialModulePath);
|
||||
const fileResult = resolveFile(context, dirPath, fileNameHint, platform);
|
||||
if (fileResult.type === 'resolved') {
|
||||
return fileResult;
|
||||
}
|
||||
const dirResult = resolveDir(context, potentialModulePath, platform);
|
||||
if (dirResult.type === 'resolved') {
|
||||
return dirResult;
|
||||
}
|
||||
return failedFor({file: fileResult.candidates, dir: dirResult.candidates});
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to resolve a potential path as if it was a directory-based module.
|
||||
* Either this is a directory that contains a package, or that the directory
|
||||
* contains an index file. If it fails to resolve these options, it returns
|
||||
* `null` and fills the array of `candidates` that were tried.
|
||||
*
|
||||
* For example we could try to resolve `/foo/bar`, that would eventually
|
||||
* resolve to `/foo/bar/lib/index.ios.js` if we're on platform iOS and that
|
||||
* `bar` contains a package which entry point is `./lib/index` (or `./lib`).
|
||||
*/
|
||||
function resolveDir(
|
||||
context: FileOrDirContext,
|
||||
potentialDirPath: string,
|
||||
platform: string | null,
|
||||
): Result<FileResolution, FileCandidates> {
|
||||
const packageJsonPath = path.join(potentialDirPath, 'package.json');
|
||||
if (context.doesFileExist(packageJsonPath)) {
|
||||
const resolution = resolvePackage(context, packageJsonPath, platform);
|
||||
return {resolution, type: 'resolved'};
|
||||
}
|
||||
return resolveFile(context, potentialDirPath, 'index', platform);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the main module of a package that we know exist. The resolution
|
||||
* itself cannot fail because we already resolved the path to the package.
|
||||
* If the `main` of the package is invalid, this is not a resolution failure,
|
||||
* this means the package is invalid, and should purposefully stop the
|
||||
* resolution process altogether.
|
||||
*/
|
||||
function resolvePackage(
|
||||
context: FileOrDirContext,
|
||||
packageJsonPath: string,
|
||||
platform: string | null,
|
||||
): FileResolution {
|
||||
const mainPrefixPath = context.getPackageMainPath(packageJsonPath);
|
||||
const dirPath = path.dirname(mainPrefixPath);
|
||||
const prefixName = path.basename(mainPrefixPath);
|
||||
const fileResult = resolveFile(context, dirPath, prefixName, platform);
|
||||
if (fileResult.type === 'resolved') {
|
||||
return fileResult.resolution;
|
||||
}
|
||||
const indexResult = resolveFile(context, mainPrefixPath, 'index', platform);
|
||||
if (indexResult.type === 'resolved') {
|
||||
return indexResult.resolution;
|
||||
}
|
||||
throw new InvalidPackageError({
|
||||
packageJsonPath,
|
||||
mainPrefixPath,
|
||||
indexCandidates: indexResult.candidates,
|
||||
fileCandidates: fileResult.candidates,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a directory path and the base asset name, return a list of all the
|
||||
* asset file names that match the given base name in that directory. Return
|
||||
* null if there's no such named asset. `platform` is used to identify
|
||||
* platform-specific assets, ex. `foo.ios.js` instead of a generic `foo.js`.
|
||||
*/
|
||||
export type ResolveAsset = (
|
||||
dirPath: string,
|
||||
assetName: string,
|
||||
platform: string | null,
|
||||
) => ?$ReadOnlyArray<string>;
|
||||
|
||||
/**
|
||||
* Check existence of a single file.
|
||||
*/
|
||||
export type DoesFileExist = (filePath: string) => boolean;
|
||||
|
||||
export type IsAssetFile = (fileName: string) => boolean;
|
||||
|
||||
type FileContext = {
|
||||
+doesFileExist: DoesFileExist,
|
||||
+isAssetFile: IsAssetFile,
|
||||
+preferNativePlatform: boolean,
|
||||
+resolveAsset: ResolveAsset,
|
||||
+sourceExts: $ReadOnlyArray<string>,
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a file name for a particular directory, return a resolution result
|
||||
* depending on whether or not we found the corresponding module as a file. For
|
||||
* example, we might ask for `foo.png`, that resolves to
|
||||
* `['/js/beep/foo.ios.png']`. Or we may ask for `boop`, that resolves to
|
||||
* `/js/boop.android.ts`. On the other hand this function does not resolve
|
||||
* directory-based module names: for example `boop` will not resolve to
|
||||
* `/js/boop/index.js` (see `_loadAsDir` for that).
|
||||
*/
|
||||
function resolveFile(
|
||||
context: FileContext,
|
||||
dirPath: string,
|
||||
fileNameHint: string,
|
||||
platform: string | null,
|
||||
): Result<FileResolution, FileCandidates> {
|
||||
const {isAssetFile, resolveAsset} = context;
|
||||
if (isAssetFile(fileNameHint)) {
|
||||
const result = resolveAssetFiles(
|
||||
resolveAsset,
|
||||
dirPath,
|
||||
fileNameHint,
|
||||
platform,
|
||||
);
|
||||
return mapResult(result, filePaths => ({type: 'assetFiles', filePaths}));
|
||||
}
|
||||
const candidateExts = [];
|
||||
const filePathPrefix = path.join(dirPath, fileNameHint);
|
||||
const sfContext = {...context, candidateExts, filePathPrefix};
|
||||
const filePath = resolveSourceFile(sfContext, platform);
|
||||
if (filePath != null) {
|
||||
return resolvedAs({type: 'sourceFile', filePath});
|
||||
}
|
||||
return failedFor({type: 'sourceFile', filePathPrefix, candidateExts});
|
||||
}
|
||||
|
||||
type SourceFileContext = SourceFileForAllExtsContext & {
|
||||
+sourceExts: $ReadOnlyArray<string>,
|
||||
};
|
||||
|
||||
/**
|
||||
* A particular 'base path' can resolve to a number of possibilities depending
|
||||
* on the context. For example `foo/bar` could resolve to `foo/bar.ios.js`, or
|
||||
* to `foo/bar.js`. If can also resolve to the bare path `foo/bar` itself, as
|
||||
* supported by Node.js resolution. On the other hand it doesn't support
|
||||
* `foo/bar.ios`, for historical reasons.
|
||||
*
|
||||
* Return the full path of the resolved module, `null` if no resolution could
|
||||
* be found.
|
||||
*/
|
||||
function resolveSourceFile(
|
||||
context: SourceFileContext,
|
||||
platform: ?string,
|
||||
): ?string {
|
||||
let filePath = resolveSourceFileForAllExts(context, '');
|
||||
if (filePath) {
|
||||
return filePath;
|
||||
}
|
||||
const {sourceExts} = context;
|
||||
for (let i = 0; i < sourceExts.length; i++) {
|
||||
const ext = `.${sourceExts[i]}`;
|
||||
filePath = resolveSourceFileForAllExts(context, ext, platform);
|
||||
if (filePath != null) {
|
||||
return filePath;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
type SourceFileForAllExtsContext = SourceFileForExtContext & {
|
||||
+preferNativePlatform: boolean,
|
||||
};
|
||||
|
||||
/**
|
||||
* For a particular extension, ex. `js`, we want to try a few possibilities,
|
||||
* such as `foo.ios.js`, `foo.native.js`, and of course `foo.js`. Return the
|
||||
* full path of the resolved module, `null` if no resolution could be found.
|
||||
*/
|
||||
function resolveSourceFileForAllExts(
|
||||
context: SourceFileForAllExtsContext,
|
||||
sourceExt: string,
|
||||
platform: ?string,
|
||||
): ?string {
|
||||
if (platform != null) {
|
||||
const ext = `.${platform}${sourceExt}`;
|
||||
const filePath = resolveSourceFileForExt(context, ext);
|
||||
if (filePath) {
|
||||
return filePath;
|
||||
}
|
||||
}
|
||||
if (context.preferNativePlatform) {
|
||||
const filePath = resolveSourceFileForExt(context, `.native${sourceExt}`);
|
||||
if (filePath) {
|
||||
return filePath;
|
||||
}
|
||||
}
|
||||
const filePath = resolveSourceFileForExt(context, sourceExt);
|
||||
return filePath;
|
||||
}
|
||||
|
||||
type SourceFileForExtContext = {
|
||||
+candidateExts: Array<string>,
|
||||
+doesFileExist: DoesFileExist,
|
||||
+filePathPrefix: string,
|
||||
};
|
||||
|
||||
/**
|
||||
* We try to resolve a single possible extension. If it doesn't exist, then
|
||||
* we make sure to add the extension to a list of candidates for reporting.
|
||||
*/
|
||||
function resolveSourceFileForExt(
|
||||
context: SourceFileForExtContext,
|
||||
extension: string,
|
||||
): ?string {
|
||||
const filePath = `${context.filePathPrefix}${extension}`;
|
||||
if (context.doesFileExist(filePath)) {
|
||||
return filePath;
|
||||
}
|
||||
context.candidateExts.push(extension);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all the asset files corresponding to the file base name, and return
|
||||
* it wrapped as a resolution result.
|
||||
*/
|
||||
function resolveAssetFiles(
|
||||
resolveAsset: ResolveAsset,
|
||||
dirPath: string,
|
||||
fileNameHint: string,
|
||||
platform: string | null,
|
||||
): Result<AssetFileResolution, FileCandidates> {
|
||||
const assetNames = resolveAsset(dirPath, fileNameHint, platform);
|
||||
if (assetNames != null) {
|
||||
const res = assetNames.map(assetName => path.join(dirPath, assetName));
|
||||
return resolvedAs(res);
|
||||
}
|
||||
return failedFor({type: 'asset', name: fileNameHint});
|
||||
}
|
||||
|
||||
// HasteFS stores paths with backslashes on Windows, this ensures the path is in
|
||||
// the proper format. Will also add drive letter if not present so `/root` will
|
||||
// resolve to `C:\root`. Noop on other platforms.
|
||||
function resolveWindowsPath(modulePath) {
|
||||
if (path.sep !== '\\') {
|
||||
return modulePath;
|
||||
}
|
||||
return path.resolve(modulePath);
|
||||
}
|
||||
|
||||
function isRelativeImport(filePath: string) {
|
||||
return /^[.][.]?(?:[/]|$)/.test(filePath);
|
||||
}
|
||||
|
||||
function normalizePath(modulePath) {
|
||||
if (path.sep === '/') {
|
||||
modulePath = path.normalize(modulePath);
|
||||
} else if (path.posix) {
|
||||
modulePath = path.posix.normalize(modulePath);
|
||||
}
|
||||
|
||||
return modulePath.replace(/\/$/, '');
|
||||
}
|
||||
|
||||
function resolvedAs<TResolution, TCandidates>(
|
||||
resolution: TResolution,
|
||||
): Result<TResolution, TCandidates> {
|
||||
return {type: 'resolved', resolution};
|
||||
}
|
||||
|
||||
function failedFor<TResolution, TCandidates>(
|
||||
candidates: TCandidates,
|
||||
): Result<TResolution, TCandidates> {
|
||||
return {type: 'failed', candidates};
|
||||
}
|
||||
|
||||
function mapResult<TResolution, TNewResolution, TCandidates>(
|
||||
result: Result<TResolution, TCandidates>,
|
||||
mapper: TResolution => TNewResolution,
|
||||
): Result<TNewResolution, TCandidates> {
|
||||
if (result.type === 'failed') {
|
||||
return result;
|
||||
}
|
||||
return {type: 'resolved', resolution: mapper(result.resolution)};
|
||||
}
|
||||
|
||||
module.exports = resolve;
|
|
@ -0,0 +1,51 @@
|
|||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
* @flow
|
||||
* @format
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
export type Result<+TResolution, +TCandidates> =
|
||||
| {|+type: 'resolved', +resolution: TResolution|}
|
||||
| {|+type: 'failed', +candidates: TCandidates|};
|
||||
|
||||
export type Resolution = FileResolution | {|+type: 'empty'|};
|
||||
export type Candidates =
|
||||
| {|+type: 'modulePath', +which: FileAndDirCandidates|}
|
||||
| {|
|
||||
+type: 'moduleName',
|
||||
+dirPaths: $ReadOnlyArray<string>,
|
||||
+extraPaths: $ReadOnlyArray<string>,
|
||||
|};
|
||||
|
||||
export type AssetFileResolution = $ReadOnlyArray<string>;
|
||||
export type FileResolution =
|
||||
| {|+type: 'sourceFile', +filePath: string|}
|
||||
| {|+type: 'assetFiles', +filePaths: AssetFileResolution|};
|
||||
|
||||
export type FileAndDirCandidates = {|
|
||||
+dir: FileCandidates,
|
||||
+file: FileCandidates,
|
||||
|};
|
||||
|
||||
/**
|
||||
* This is a way to describe what files we tried to look for when resolving
|
||||
* a module name as file. This is mainly used for error reporting, so that
|
||||
* we can explain why we cannot resolve a module.
|
||||
*/
|
||||
export type FileCandidates =
|
||||
// We only tried to resolve a specific asset.
|
||||
| {|+type: 'asset', +name: string|}
|
||||
// We attempted to resolve a name as being a source file (ex. JavaScript,
|
||||
// JSON...), in which case there can be several extensions we tried, for
|
||||
// example `/js/foo.ios.js`, `/js/foo.js`, etc. for a single prefix '/js/foo'.
|
||||
| {|
|
||||
+type: 'sourceFile',
|
||||
+filePathPrefix: string,
|
||||
+candidateExts: $ReadOnlyArray<string>,
|
||||
|};
|
|
@ -74,6 +74,7 @@
|
|||
"lodash.throttle": "^4.1.1",
|
||||
"merge-stream": "^1.0.1",
|
||||
"metro-core": "0.26.0",
|
||||
"metro-resolver": "0.26.0",
|
||||
"metro-source-map": "0.26.0",
|
||||
"mime-types": "2.1.11",
|
||||
"mkdirp": "^0.5.1",
|
||||
|
|
|
@ -10,11 +10,19 @@
|
|||
|
||||
'use strict';
|
||||
|
||||
const Resolver = require('metro-resolver');
|
||||
|
||||
const invariant = require('fbjs/lib/invariant');
|
||||
const isAbsolutePath = require('absolute-path');
|
||||
const path = require('path');
|
||||
const util = require('util');
|
||||
|
||||
import type {
|
||||
DoesFileExist,
|
||||
IsAssetFile,
|
||||
ResolveAsset,
|
||||
Resolution,
|
||||
} from 'metro-resolver';
|
||||
|
||||
export type DirExistsFn = (filePath: string) => boolean;
|
||||
|
||||
/**
|
||||
|
@ -53,25 +61,6 @@ export type ModuleishCache<TModule, TPackage> = {
|
|||
getAssetModule(path: string): TModule,
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a directory path and the base asset name, return a list of all the
|
||||
* asset file names that match the given base name in that directory. Return
|
||||
* null if there's no such named asset. `platform` is used to identify
|
||||
* platform-specific assets, ex. `foo.ios.js` instead of a generic `foo.js`.
|
||||
*/
|
||||
type ResolveAsset = (
|
||||
dirPath: string,
|
||||
assetName: string,
|
||||
platform: string | null,
|
||||
) => ?$ReadOnlyArray<string>;
|
||||
|
||||
/**
|
||||
* Check existence of a single file.
|
||||
*/
|
||||
type DoesFileExist = (filePath: string) => boolean;
|
||||
|
||||
type IsAssetFile = (fileName: string) => boolean;
|
||||
|
||||
type Options<TModule, TPackage> = {|
|
||||
+dirExists: DirExistsFn,
|
||||
+doesFileExist: DoesFileExist,
|
||||
|
@ -84,43 +73,6 @@ type Options<TModule, TPackage> = {|
|
|||
+sourceExts: Array<string>,
|
||||
|};
|
||||
|
||||
/**
|
||||
* This is a way to describe what files we tried to look for when resolving
|
||||
* a module name as file. This is mainly used for error reporting, so that
|
||||
* we can explain why we cannot resolve a module.
|
||||
*/
|
||||
type FileCandidates =
|
||||
// We only tried to resolve a specific asset.
|
||||
| {|+type: 'asset', +name: string|}
|
||||
// We attempted to resolve a name as being a source file (ex. JavaScript,
|
||||
// JSON...), in which case there can be several extensions we tried, for
|
||||
// example `/js/foo.ios.js`, `/js/foo.js`, etc. for a single prefix '/js/foo'.
|
||||
| {|
|
||||
+type: 'sourceFile',
|
||||
+filePathPrefix: string,
|
||||
+candidateExts: $ReadOnlyArray<string>,
|
||||
|};
|
||||
|
||||
type FileAndDirCandidates = {|+dir: FileCandidates, +file: FileCandidates|};
|
||||
|
||||
type Result<+TResolution, +TCandidates> =
|
||||
| {|+type: 'resolved', +resolution: TResolution|}
|
||||
| {|+type: 'failed', +candidates: TCandidates|};
|
||||
|
||||
type AssetFileResolution = $ReadOnlyArray<string>;
|
||||
type FileResolution =
|
||||
| {|+type: 'sourceFile', +filePath: string|}
|
||||
| {|+type: 'assetFiles', +filePaths: AssetFileResolution|};
|
||||
|
||||
type Resolution = FileResolution | {|+type: 'empty'|};
|
||||
type Candidates =
|
||||
| {|+type: 'modulePath', +which: FileAndDirCandidates|}
|
||||
| {|
|
||||
+type: 'moduleName',
|
||||
+dirPaths: $ReadOnlyArray<string>,
|
||||
+extraPaths: $ReadOnlyArray<string>,
|
||||
|};
|
||||
|
||||
class ModuleResolver<TModule: Moduleish, TPackage: Packageish> {
|
||||
_options: Options<TModule, TPackage>;
|
||||
|
||||
|
@ -144,7 +96,7 @@ class ModuleResolver<TModule: Moduleish, TPackage: Packageish> {
|
|||
allowHaste: boolean,
|
||||
platform: string | null,
|
||||
): TModule {
|
||||
const result = resolveDependency(
|
||||
const result = Resolver.resolve(
|
||||
{
|
||||
...this._options,
|
||||
originModulePath: fromModule.path,
|
||||
|
@ -172,8 +124,8 @@ class ModuleResolver<TModule: Moduleish, TPackage: Packageish> {
|
|||
`The module \`${moduleName}\` could not be found ` +
|
||||
`from \`${fromModule.path}\`. ` +
|
||||
`Indeed, none of these files exist:\n\n` +
|
||||
` * \`${formatFileCandidates(which.file)}\`\n` +
|
||||
` * \`${formatFileCandidates(which.dir)}\``,
|
||||
` * \`${Resolver.formatFileCandidates(which.file)}\`\n` +
|
||||
` * \`${Resolver.formatFileCandidates(which.dir)}\``,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -231,502 +183,6 @@ class ModuleResolver<TModule: Moduleish, TPackage: Packageish> {
|
|||
}
|
||||
}
|
||||
|
||||
type ResolutionContext = ModulePathContext &
|
||||
HasteContext & {
|
||||
originModulePath: string,
|
||||
allowHaste: boolean,
|
||||
extraNodeModules: ?Object,
|
||||
};
|
||||
|
||||
function resolveDependency(
|
||||
context: ResolutionContext,
|
||||
moduleName: string,
|
||||
platform: string | null,
|
||||
): Result<Resolution, Candidates> {
|
||||
if (isRelativeImport(moduleName) || isAbsolutePath(moduleName)) {
|
||||
return resolveModulePath(context, moduleName, platform);
|
||||
}
|
||||
const realModuleName = context.redirectModulePath(moduleName);
|
||||
// exclude
|
||||
if (realModuleName === false) {
|
||||
return resolvedAs({type: 'empty'});
|
||||
}
|
||||
|
||||
const {originModulePath} = context;
|
||||
if (isRelativeImport(realModuleName) || isAbsolutePath(realModuleName)) {
|
||||
// derive absolute path /.../node_modules/originModuleDir/realModuleName
|
||||
const fromModuleParentIdx =
|
||||
originModulePath.lastIndexOf('node_modules' + path.sep) + 13;
|
||||
const originModuleDir = originModulePath.slice(
|
||||
0,
|
||||
originModulePath.indexOf(path.sep, fromModuleParentIdx),
|
||||
);
|
||||
const absPath = path.join(originModuleDir, realModuleName);
|
||||
return resolveModulePath(context, absPath, platform);
|
||||
}
|
||||
|
||||
// At that point we only have module names that
|
||||
// aren't relative paths nor absolute paths.
|
||||
if (context.allowHaste) {
|
||||
const normalizedName = normalizePath(realModuleName);
|
||||
const result = resolveHasteName(context, normalizedName, platform);
|
||||
if (result.type === 'resolved') {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
const dirPaths = [];
|
||||
for (
|
||||
let currDir = path.dirname(originModulePath);
|
||||
currDir !== '.' && currDir !== path.parse(originModulePath).root;
|
||||
currDir = path.dirname(currDir)
|
||||
) {
|
||||
const searchPath = path.join(currDir, 'node_modules');
|
||||
dirPaths.push(path.join(searchPath, realModuleName));
|
||||
}
|
||||
|
||||
const extraPaths = [];
|
||||
const {extraNodeModules} = context;
|
||||
if (extraNodeModules) {
|
||||
const bits = path.normalize(moduleName).split(path.sep);
|
||||
const packageName = bits[0];
|
||||
if (extraNodeModules[packageName]) {
|
||||
bits[0] = extraNodeModules[packageName];
|
||||
extraPaths.push(path.join.apply(path, bits));
|
||||
}
|
||||
}
|
||||
|
||||
const allDirPaths = dirPaths.concat(extraPaths);
|
||||
for (let i = 0; i < allDirPaths.length; ++i) {
|
||||
const result = resolveFileOrDir(context, allDirPaths[i], platform);
|
||||
if (result.type === 'resolved') {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return failedFor({type: 'moduleName', dirPaths, extraPaths});
|
||||
}
|
||||
|
||||
type ModulePathContext = FileOrDirContext & {
|
||||
/**
|
||||
* Full path of the module that is requiring or importing the module to be
|
||||
* resolved.
|
||||
*/
|
||||
+originModulePath: string,
|
||||
/**
|
||||
* Lookup the module's closest `package.json` and process the redirects
|
||||
* metadata. Return an absolute path to the resolved path.
|
||||
*/
|
||||
+redirectModulePath: (modulePath: string) => string | false,
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolve any kind of module path, whether it's a file or a directory.
|
||||
* For example we may want to resolve './foobar'. The closest
|
||||
* `package.json` may define a redirection for this path, for example
|
||||
* `/smth/lib/foobar`, that may be further resolved to
|
||||
* `/smth/lib/foobar/index.ios.js`.
|
||||
*/
|
||||
function resolveModulePath(
|
||||
context: ModulePathContext,
|
||||
toModuleName: string,
|
||||
platform: string | null,
|
||||
): Result<Resolution, Candidates> {
|
||||
const modulePath = isAbsolutePath(toModuleName)
|
||||
? resolveWindowsPath(toModuleName)
|
||||
: path.join(path.dirname(context.originModulePath), toModuleName);
|
||||
const redirectedPath = context.redirectModulePath(modulePath);
|
||||
if (redirectedPath === false) {
|
||||
return resolvedAs({type: 'empty'});
|
||||
}
|
||||
const result = resolveFileOrDir(context, redirectedPath, platform);
|
||||
if (result.type === 'resolved') {
|
||||
return result;
|
||||
}
|
||||
return failedFor({type: 'modulePath', which: result.candidates});
|
||||
}
|
||||
|
||||
type HasteContext = FileOrDirContext & {
|
||||
/**
|
||||
* Given a name, this should return the full path to the file that provides
|
||||
* a Haste module of that name. Ex. for `Foo` it may return `/smth/Foo.js`.
|
||||
*/
|
||||
+resolveHasteModule: (name: string) => ?string,
|
||||
/**
|
||||
* Given a name, this should return the full path to the package manifest that
|
||||
* provides a Haste package of that name. Ex. for `Foo` it may return
|
||||
* `/smth/Foo/package.json`.
|
||||
*/
|
||||
+resolveHastePackage: (name: string) => ?string,
|
||||
};
|
||||
|
||||
/**
|
||||
* Resolve a module as a Haste module or package. For example we might try to
|
||||
* resolve `Foo`, that is provided by file `/smth/Foo.js`. Or, in the case of
|
||||
* a Haste package, it could be `/smth/Foo/index.js`.
|
||||
*/
|
||||
function resolveHasteName(
|
||||
context: HasteContext,
|
||||
moduleName: string,
|
||||
platform: string | null,
|
||||
): Result<FileResolution, void> {
|
||||
const modulePath = context.resolveHasteModule(moduleName);
|
||||
if (modulePath != null) {
|
||||
return resolvedAs({type: 'sourceFile', filePath: modulePath});
|
||||
}
|
||||
let packageName = moduleName;
|
||||
let packageJsonPath = context.resolveHastePackage(packageName);
|
||||
while (packageJsonPath == null && packageName && packageName !== '.') {
|
||||
packageName = path.dirname(packageName);
|
||||
packageJsonPath = context.resolveHastePackage(packageName);
|
||||
}
|
||||
if (packageJsonPath == null) {
|
||||
return failedFor();
|
||||
}
|
||||
const packageDirPath = path.dirname(packageJsonPath);
|
||||
const pathInModule = moduleName.substring(packageName.length + 1);
|
||||
const potentialModulePath = path.join(packageDirPath, pathInModule);
|
||||
const result = resolveFileOrDir(context, potentialModulePath, platform);
|
||||
if (result.type === 'resolved') {
|
||||
return result;
|
||||
}
|
||||
const {candidates} = result;
|
||||
const opts = {moduleName, packageName, pathInModule, candidates};
|
||||
throw new MissingFileInHastePackageError(opts);
|
||||
}
|
||||
|
||||
class MissingFileInHastePackageError extends Error {
|
||||
candidates: FileAndDirCandidates;
|
||||
moduleName: string;
|
||||
packageName: string;
|
||||
pathInModule: string;
|
||||
|
||||
constructor(opts: {|
|
||||
+candidates: FileAndDirCandidates,
|
||||
+moduleName: string,
|
||||
+packageName: string,
|
||||
+pathInModule: string,
|
||||
|}) {
|
||||
super(
|
||||
`While resolving module \`${opts.moduleName}\`, ` +
|
||||
`the Haste package \`${opts.packageName}\` was found. However the ` +
|
||||
`module \`${opts.pathInModule}\` could not be found within ` +
|
||||
`the package. Indeed, none of these files exist:\n\n` +
|
||||
` * \`${formatFileCandidates(opts.candidates.file)}\`\n` +
|
||||
` * \`${formatFileCandidates(opts.candidates.dir)}\``,
|
||||
);
|
||||
Object.assign(this, opts);
|
||||
}
|
||||
}
|
||||
|
||||
type FileOrDirContext = FileContext & {
|
||||
/**
|
||||
* This should return the path of the "main" module of the specified
|
||||
* `package.json` file, after post-processing: for example, applying the
|
||||
* 'browser' field if necessary.
|
||||
*
|
||||
* FIXME: move the post-processing here. Right now it is
|
||||
* located in `node-haste/Package.js`, and fully duplicated in
|
||||
* `ModuleGraph/node-haste/Package.js` (!)
|
||||
*/
|
||||
+getPackageMainPath: (packageJsonPath: string) => string,
|
||||
};
|
||||
|
||||
/**
|
||||
* In the NodeJS-style module resolution scheme we want to check potential
|
||||
* paths both as directories and as files. For example, `/foo/bar` may resolve
|
||||
* to `/foo/bar.js` (preferred), but it might also be `/foo/bar/index.js`, or
|
||||
* even a package directory.
|
||||
*/
|
||||
function resolveFileOrDir(
|
||||
context: FileOrDirContext,
|
||||
potentialModulePath: string,
|
||||
platform: string | null,
|
||||
): Result<FileResolution, FileAndDirCandidates> {
|
||||
const dirPath = path.dirname(potentialModulePath);
|
||||
const fileNameHint = path.basename(potentialModulePath);
|
||||
const fileResult = resolveFile(context, dirPath, fileNameHint, platform);
|
||||
if (fileResult.type === 'resolved') {
|
||||
return fileResult;
|
||||
}
|
||||
const dirResult = resolveDir(context, potentialModulePath, platform);
|
||||
if (dirResult.type === 'resolved') {
|
||||
return dirResult;
|
||||
}
|
||||
return failedFor({file: fileResult.candidates, dir: dirResult.candidates});
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to resolve a potential path as if it was a directory-based module.
|
||||
* Either this is a directory that contains a package, or that the directory
|
||||
* contains an index file. If it fails to resolve these options, it returns
|
||||
* `null` and fills the array of `candidates` that were tried.
|
||||
*
|
||||
* For example we could try to resolve `/foo/bar`, that would eventually
|
||||
* resolve to `/foo/bar/lib/index.ios.js` if we're on platform iOS and that
|
||||
* `bar` contains a package which entry point is `./lib/index` (or `./lib`).
|
||||
*/
|
||||
function resolveDir(
|
||||
context: FileOrDirContext,
|
||||
potentialDirPath: string,
|
||||
platform: string | null,
|
||||
): Result<FileResolution, FileCandidates> {
|
||||
const packageJsonPath = path.join(potentialDirPath, 'package.json');
|
||||
if (context.doesFileExist(packageJsonPath)) {
|
||||
const resolution = resolvePackage(context, packageJsonPath, platform);
|
||||
return {resolution, type: 'resolved'};
|
||||
}
|
||||
return resolveFile(context, potentialDirPath, 'index', platform);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the main module of a package that we know exist. The resolution
|
||||
* itself cannot fail because we already resolved the path to the package.
|
||||
* If the `main` of the package is invalid, this is not a resolution failure,
|
||||
* this means the package is invalid, and should purposefully stop the
|
||||
* resolution process altogether.
|
||||
*/
|
||||
function resolvePackage(
|
||||
context: FileOrDirContext,
|
||||
packageJsonPath: string,
|
||||
platform: string | null,
|
||||
): FileResolution {
|
||||
const mainPrefixPath = context.getPackageMainPath(packageJsonPath);
|
||||
const dirPath = path.dirname(mainPrefixPath);
|
||||
const prefixName = path.basename(mainPrefixPath);
|
||||
const fileResult = resolveFile(context, dirPath, prefixName, platform);
|
||||
if (fileResult.type === 'resolved') {
|
||||
return fileResult.resolution;
|
||||
}
|
||||
const indexResult = resolveFile(context, mainPrefixPath, 'index', platform);
|
||||
if (indexResult.type === 'resolved') {
|
||||
return indexResult.resolution;
|
||||
}
|
||||
throw new InvalidPackageError({
|
||||
packageJsonPath,
|
||||
mainPrefixPath,
|
||||
indexCandidates: indexResult.candidates,
|
||||
fileCandidates: fileResult.candidates,
|
||||
});
|
||||
}
|
||||
|
||||
function formatFileCandidates(candidates: FileCandidates): string {
|
||||
if (candidates.type === 'asset') {
|
||||
return candidates.name;
|
||||
}
|
||||
return `${candidates.filePathPrefix}(${candidates.candidateExts.join('|')})`;
|
||||
}
|
||||
|
||||
class InvalidPackageError extends Error {
|
||||
/**
|
||||
* The file candidates we tried to find to resolve the `main` field of the
|
||||
* package. Ex. `/js/foo/beep(.js|.json)?` if `main` is specifying `./beep`
|
||||
* as the entry point.
|
||||
*/
|
||||
fileCandidates: FileCandidates;
|
||||
/**
|
||||
* The 'index' file candidates we tried to find to resolve the `main` field of
|
||||
* the package. Ex. `/js/foo/beep/index(.js|.json)?` if `main` is specifying
|
||||
* `./beep` as the entry point.
|
||||
*/
|
||||
indexCandidates: FileCandidates;
|
||||
/**
|
||||
* The module path prefix we where trying to resolve. For example './beep'.
|
||||
*/
|
||||
mainPrefixPath: string;
|
||||
/**
|
||||
* Full path the package we were trying to resolve.
|
||||
* Ex. `/js/foo/package.json`.
|
||||
*/
|
||||
packageJsonPath: string;
|
||||
|
||||
constructor(opts: {|
|
||||
+fileCandidates: FileCandidates,
|
||||
+indexCandidates: FileCandidates,
|
||||
+mainPrefixPath: string,
|
||||
+packageJsonPath: string,
|
||||
|}) {
|
||||
super(
|
||||
`The package \`${opts.packageJsonPath}\` is invalid because it ` +
|
||||
`specifies a \`main\` module field that could not be resolved (` +
|
||||
`\`${opts.mainPrefixPath}\`. Indeed, none of these files exist:\n\n` +
|
||||
` * \`${formatFileCandidates(opts.fileCandidates)}\`\n` +
|
||||
` * \`${formatFileCandidates(opts.indexCandidates)}\``,
|
||||
);
|
||||
Object.assign(this, opts);
|
||||
}
|
||||
}
|
||||
|
||||
type FileContext = {
|
||||
+doesFileExist: DoesFileExist,
|
||||
+isAssetFile: IsAssetFile,
|
||||
+preferNativePlatform: boolean,
|
||||
+resolveAsset: ResolveAsset,
|
||||
+sourceExts: $ReadOnlyArray<string>,
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a file name for a particular directory, return a resolution result
|
||||
* depending on whether or not we found the corresponding module as a file. For
|
||||
* example, we might ask for `foo.png`, that resolves to
|
||||
* `['/js/beep/foo.ios.png']`. Or we may ask for `boop`, that resolves to
|
||||
* `/js/boop.android.ts`. On the other hand this function does not resolve
|
||||
* directory-based module names: for example `boop` will not resolve to
|
||||
* `/js/boop/index.js` (see `_loadAsDir` for that).
|
||||
*/
|
||||
function resolveFile(
|
||||
context: FileContext,
|
||||
dirPath: string,
|
||||
fileNameHint: string,
|
||||
platform: string | null,
|
||||
): Result<FileResolution, FileCandidates> {
|
||||
const {isAssetFile, resolveAsset} = context;
|
||||
if (isAssetFile(fileNameHint)) {
|
||||
const result = resolveAssetFiles(
|
||||
resolveAsset,
|
||||
dirPath,
|
||||
fileNameHint,
|
||||
platform,
|
||||
);
|
||||
return mapResult(result, filePaths => ({type: 'assetFiles', filePaths}));
|
||||
}
|
||||
const candidateExts = [];
|
||||
const filePathPrefix = path.join(dirPath, fileNameHint);
|
||||
const sfContext = {...context, candidateExts, filePathPrefix};
|
||||
const filePath = resolveSourceFile(sfContext, platform);
|
||||
if (filePath != null) {
|
||||
return resolvedAs({type: 'sourceFile', filePath});
|
||||
}
|
||||
return failedFor({type: 'sourceFile', filePathPrefix, candidateExts});
|
||||
}
|
||||
|
||||
type SourceFileContext = SourceFileForAllExtsContext & {
|
||||
+sourceExts: $ReadOnlyArray<string>,
|
||||
};
|
||||
|
||||
/**
|
||||
* A particular 'base path' can resolve to a number of possibilities depending
|
||||
* on the context. For example `foo/bar` could resolve to `foo/bar.ios.js`, or
|
||||
* to `foo/bar.js`. If can also resolve to the bare path `foo/bar` itself, as
|
||||
* supported by Node.js resolution. On the other hand it doesn't support
|
||||
* `foo/bar.ios`, for historical reasons.
|
||||
*
|
||||
* Return the full path of the resolved module, `null` if no resolution could
|
||||
* be found.
|
||||
*/
|
||||
function resolveSourceFile(
|
||||
context: SourceFileContext,
|
||||
platform: ?string,
|
||||
): ?string {
|
||||
let filePath = resolveSourceFileForAllExts(context, '');
|
||||
if (filePath) {
|
||||
return filePath;
|
||||
}
|
||||
const {sourceExts} = context;
|
||||
for (let i = 0; i < sourceExts.length; i++) {
|
||||
const ext = `.${sourceExts[i]}`;
|
||||
filePath = resolveSourceFileForAllExts(context, ext, platform);
|
||||
if (filePath != null) {
|
||||
return filePath;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
type SourceFileForAllExtsContext = SourceFileForExtContext & {
|
||||
+preferNativePlatform: boolean,
|
||||
};
|
||||
|
||||
/**
|
||||
* For a particular extension, ex. `js`, we want to try a few possibilities,
|
||||
* such as `foo.ios.js`, `foo.native.js`, and of course `foo.js`. Return the
|
||||
* full path of the resolved module, `null` if no resolution could be found.
|
||||
*/
|
||||
function resolveSourceFileForAllExts(
|
||||
context: SourceFileForAllExtsContext,
|
||||
sourceExt: string,
|
||||
platform: ?string,
|
||||
): ?string {
|
||||
if (platform != null) {
|
||||
const ext = `.${platform}${sourceExt}`;
|
||||
const filePath = resolveSourceFileForExt(context, ext);
|
||||
if (filePath) {
|
||||
return filePath;
|
||||
}
|
||||
}
|
||||
if (context.preferNativePlatform) {
|
||||
const filePath = resolveSourceFileForExt(context, `.native${sourceExt}`);
|
||||
if (filePath) {
|
||||
return filePath;
|
||||
}
|
||||
}
|
||||
const filePath = resolveSourceFileForExt(context, sourceExt);
|
||||
return filePath;
|
||||
}
|
||||
|
||||
type SourceFileForExtContext = {
|
||||
+candidateExts: Array<string>,
|
||||
+doesFileExist: DoesFileExist,
|
||||
+filePathPrefix: string,
|
||||
};
|
||||
|
||||
/**
|
||||
* We try to resolve a single possible extension. If it doesn't exist, then
|
||||
* we make sure to add the extension to a list of candidates for reporting.
|
||||
*/
|
||||
function resolveSourceFileForExt(
|
||||
context: SourceFileForExtContext,
|
||||
extension: string,
|
||||
): ?string {
|
||||
const filePath = `${context.filePathPrefix}${extension}`;
|
||||
if (context.doesFileExist(filePath)) {
|
||||
return filePath;
|
||||
}
|
||||
context.candidateExts.push(extension);
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all the asset files corresponding to the file base name, and return
|
||||
* it wrapped as a resolution result.
|
||||
*/
|
||||
function resolveAssetFiles(
|
||||
resolveAsset: ResolveAsset,
|
||||
dirPath: string,
|
||||
fileNameHint: string,
|
||||
platform: string | null,
|
||||
): Result<AssetFileResolution, FileCandidates> {
|
||||
const assetNames = resolveAsset(dirPath, fileNameHint, platform);
|
||||
if (assetNames != null) {
|
||||
const res = assetNames.map(assetName => path.join(dirPath, assetName));
|
||||
return resolvedAs(res);
|
||||
}
|
||||
return failedFor({type: 'asset', name: fileNameHint});
|
||||
}
|
||||
|
||||
// HasteFS stores paths with backslashes on Windows, this ensures the path is in
|
||||
// the proper format. Will also add drive letter if not present so `/root` will
|
||||
// resolve to `C:\root`. Noop on other platforms.
|
||||
function resolveWindowsPath(modulePath) {
|
||||
if (path.sep !== '\\') {
|
||||
return modulePath;
|
||||
}
|
||||
return path.resolve(modulePath);
|
||||
}
|
||||
|
||||
function isRelativeImport(filePath: string) {
|
||||
return /^[.][.]?(?:[/]|$)/.test(filePath);
|
||||
}
|
||||
|
||||
function normalizePath(modulePath) {
|
||||
if (path.sep === '/') {
|
||||
modulePath = path.normalize(modulePath);
|
||||
} else if (path.posix) {
|
||||
modulePath = path.posix.normalize(modulePath);
|
||||
}
|
||||
|
||||
return modulePath.replace(/\/$/, '');
|
||||
}
|
||||
|
||||
function getArrayLowestItem(a: $ReadOnlyArray<string>): string | void {
|
||||
if (a.length === 0) {
|
||||
return undefined;
|
||||
|
@ -740,28 +196,6 @@ function getArrayLowestItem(a: $ReadOnlyArray<string>): string | void {
|
|||
return lowest;
|
||||
}
|
||||
|
||||
function resolvedAs<TResolution, TCandidates>(
|
||||
resolution: TResolution,
|
||||
): Result<TResolution, TCandidates> {
|
||||
return {type: 'resolved', resolution};
|
||||
}
|
||||
|
||||
function failedFor<TResolution, TCandidates>(
|
||||
candidates: TCandidates,
|
||||
): Result<TResolution, TCandidates> {
|
||||
return {type: 'failed', candidates};
|
||||
}
|
||||
|
||||
function mapResult<TResolution, TNewResolution, TCandidates>(
|
||||
result: Result<TResolution, TCandidates>,
|
||||
mapper: TResolution => TNewResolution,
|
||||
): Result<TNewResolution, TCandidates> {
|
||||
if (result.type === 'failed') {
|
||||
return result;
|
||||
}
|
||||
return {type: 'resolved', resolution: mapper(result.resolution)};
|
||||
}
|
||||
|
||||
class UnableToResolveError extends Error {
|
||||
/**
|
||||
* File path of the module that tried to require a module, ex. `/js/foo.js`.
|
||||
|
@ -791,9 +225,6 @@ class UnableToResolveError extends Error {
|
|||
}
|
||||
|
||||
module.exports = {
|
||||
formatFileCandidates,
|
||||
InvalidPackageError,
|
||||
isRelativeImport,
|
||||
ModuleResolver,
|
||||
UnableToResolveError,
|
||||
};
|
||||
|
|
|
@ -10,8 +10,6 @@
|
|||
|
||||
'use strict';
|
||||
|
||||
const ModuleResolution = require('./ModuleResolution');
|
||||
|
||||
const path = require('path');
|
||||
|
||||
const {DuplicateHasteCandidatesError} = require('jest-haste-map').ModuleMap;
|
||||
|
@ -21,7 +19,7 @@ import type {Options as TransformWorkerOptions} from '../../JSTransformer/worker
|
|||
import type {ReadResult, CachedReadResult} from '../Module';
|
||||
import type {ModuleResolver} from './ModuleResolution';
|
||||
|
||||
const {InvalidPackageError, formatFileCandidates} = ModuleResolution;
|
||||
const {InvalidPackageError, formatFileCandidates} = require('metro-resolver');
|
||||
|
||||
export type Packageish = {
|
||||
isHaste(): boolean,
|
||||
|
|
Loading…
Reference in New Issue