Create integration types and code to use module resolution logic from node-haste

Summary:
This creates flow types for the module resolution code in node-haste, and implementations of types used by that code.

The idea is to use that code until we can replace it with something leaner, and provide objects that don’t touch the file system. These objects will be initialized with the static data provided by ModuleGraph/worker.

Reviewed By: cpojer

Differential Revision: D4037372

fbshipit-source-id: 2698dbb630f4122fc1d839d06e414d0963bd6ff2
This commit is contained in:
David Aurelio 2016-10-19 06:48:45 -07:00 committed by Facebook Github Bot
parent a9d94c2dfb
commit 0c7bc9801c
9 changed files with 531 additions and 0 deletions

View File

@ -0,0 +1,68 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
*/
'use strict';
const {dirname, parse} = require('path');
module.exports = class FastFS {
directories: Set<string>;
directoryEntries: Map<string, Array<string>>;
files: Set<string>;
constructor(files: Array<string>) {
this.directories = buildDirectorySet(files);
this.directoryEntries = buildDirectoryEntries(files.map(parse));
this.files = new Set(files);
}
dirExists(path: string) {
return this.directories.has(path);
}
fileExists(path: string) {
return this.files.has(path);
}
getAllFiles() {
return Array.from(this.files.keys());
}
matches(directory: string, pattern: RegExp) {
const entries = this.directoryEntries.get(directory);
return entries ? entries.filter(pattern.test, pattern) : [];
}
};
function buildDirectorySet(files) {
const directories = new Set();
files.forEach(path => {
let {dir, root} = parse(path);
while (dir !== '.' && dir !== root && !directories.has(dir)) {
directories.add(path);
dir = dirname(path);
}
});
return directories;
}
function buildDirectoryEntries(files) {
const directoryEntries = new Map();
files.forEach(({base, dir}) => {
const entries = directoryEntries.get(dir);
if (entries) {
entries.push(base);
} else {
directoryEntries.set(dir, [base]);
}
});
return directoryEntries;
}

View File

@ -0,0 +1,40 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
*/
'use strict';
import type {TransformedFile} from '../types.flow';
module.exports = class Module {
hasteID: Promise<?string>;
name: Promise<string>;
path: string;
type: 'Module';
constructor(path: string, info: Promise<TransformedFile>) {
this.hasteID = info.then(({hasteID}) => hasteID);
this.name = this.hasteID.then(name => name || getName(path));
this.path = path;
this.type = 'Module';
}
getName() {
return this.name;
}
isHaste() {
return this.hasteID.then(Boolean);
}
};
function getName(path) {
return path.replace(/^.*[\/\\]node_modules[\///]/, '');
}

View File

@ -0,0 +1,57 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
*/
'use strict';
const Module = require('./Module');
const Package = require('./Package');
import type {PackageData, TransformedFile} from '../types.flow';
type GetFn<T> = (path: string) => Promise<T>;
module.exports = class ModuleCache {
getPackageData: GetFn<PackageData>;
getTransformedFile: GetFn<TransformedFile>;
modules: Map<string, Module>;
packages: Map<string, Package>;
constructor(getTransformedFile: GetFn<TransformedFile>) {
this.getTransformedFile = getTransformedFile;
this.getPackageData = path => getTransformedFile(path).then(
f => f.package || Promise.reject(new Error(`"${path}" does not exist`))
);
this.modules = new Map();
this.packages = new Map();
}
getAssetModule(path: string) {
return this.getModule(path);
}
getModule(path: string) {
let m = this.modules.get(path);
if (!m) {
m = new Module(path, this.getTransformedFile(path));
this.modules.set(path, m);
}
return m;
}
getPackage(path: string) {
let p = this.packages.get(path);
if (!p) {
p = new Package(path, this.getPackageData(path));
this.packages.set(path, p);
}
return p;
}
};

View File

@ -0,0 +1,132 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
*/
'use strict';
const path = require('path');
import type {PackageData} from '../types.flow';
module.exports = class Package {
data: Promise<PackageData>;
root: string;
type: 'Package';
constructor(packagePath: string, data: Promise<PackageData>) {
this.data = data;
this.root = path.dirname(packagePath);
this.type = 'Package';
}
getMain() {
// Copied from node-haste/Package.js
return this.data.then(data => {
const replacements = getReplacements(data);
if (typeof replacements === 'string') {
return path.join(this.root, replacements);
}
let main = getMain(data);
if (replacements && typeof replacements === 'object') {
main = replacements[main] ||
replacements[main + '.js'] ||
replacements[main + '.json'] ||
replacements[main.replace(/(\.js|\.json)$/, '')] ||
main;
}
return path.join(this.root, main);
});
}
getName() {
return this.data.then(p => p.name);
}
redirectRequire(name: string) {
// Copied from node-haste/Package.js
return this.data.then(data => {
const replacements = getReplacements(data);
if (!replacements || typeof replacements !== 'object') {
return name;
}
if (!path.isAbsolute(name)) {
const replacement = replacements[name];
// support exclude with "someDependency": false
return replacement === false
? false
: replacement || name;
}
let relPath = './' + path.relative(this.root, name);
if (path.sep !== '/') {
relPath = relPath.replace(path.sep, '/');
}
let redirect = replacements[relPath];
// false is a valid value
if (redirect == null) {
redirect = replacements[relPath + '.js'];
if (redirect == null) {
redirect = replacements[relPath + '.json'];
}
}
// support exclude with "./someFile": false
if (redirect === false) {
return false;
}
if (redirect) {
return path.join(
this.root,
redirect
);
}
return name;
});
}
};
function getMain(pkg) {
return pkg.main || 'index';
}
// Copied from node-haste/Package.js
function getReplacements(pkg) {
let rn = pkg['react-native'];
let browser = pkg.browser;
if (rn == null) {
return browser;
}
if (browser == null) {
return rn;
}
const main = getMain(pkg);
if (typeof rn !== 'object') {
rn = { [main]: rn };
}
if (typeof browser !== 'object') {
browser = { [main]: browser };
}
// merge with "browser" as default,
// "react-native" as override
return { ...browser, ...rn };
}

View File

@ -0,0 +1,105 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
*/
type ModuleID = string;
export type Path = string;
type Platform = string;
type Platforms = Set<Platform>;
export type Extensions = Array<string>;
export type Module = {
path: Path,
type: 'Module',
getName(): Promise<ModuleID>,
isHaste(): Promise<boolean>,
};
export type Package = {
type: 'Package',
root: Path,
getMain(): Promise<Path>,
getName(): Promise<ModuleID>,
redirectRequire(id: ModuleID): Promise<Path | false>,
};
// when changing this to `type`, the code does not typecheck any more
export interface ModuleCache {
getAssetModule(path: Path): Module,
getModule(path: Path): Module,
getPackage(path: Path): Package,
}
export type FastFS = {
dirExists(path: Path): boolean,
fileExists(path: Path): boolean,
getAllFiles(): Array<Path>,
matches(directory: Path, pattern: RegExp): Array<Path>,
};
type HelpersOptions = {|
assetExts: Extensions,
providesModuleNodeModules: Array<string>,
|};
declare class Helpers {
// node-haste/DependencyGraph/DependencyGraphHelpers.js
constructor(options: HelpersOptions): void,
}
export type HelpersT = Helpers;
type DeprecatedAssetMapOptions = {|
assetExts: Extensions,
files: Array<Path>,
helpers: Helpers,
platforms: Platforms,
|};
declare class DeprecatedAssetMap {
// node-haste/DependencyGraph/DeprecatedAssetMap.js
constructor(options: DeprecatedAssetMapOptions): void,
}
export type DeprecatedAssetMapT = DeprecatedAssetMap;
type HasteMapOptions = {|
extensions: Extensions,
fastfs: FastFS,
moduleCache: ModuleCache,
preferNativePlatform: true,
helpers: Helpers,
platforms: Platforms,
|};
declare class HasteMap {
// node-haste/DependencyGraph/HasteMap.js
constructor(options: HasteMapOptions): void,
}
export type HasteMapT = HasteMap;
type ResolutionRequestOptions = {|
platform: Platform,
platforms: Platforms,
preferNativePlatform: true,
hasteMap: HasteMap,
deprecatedAssetMap: DeprecatedAssetMap,
helpers: Helpers,
moduleCache: ModuleCache,
fastfs: FastFS,
shouldThrowOnUnresolvedErrors: () => true,
extraNodeModules: {[id: ModuleID]: Path},
|};
declare class ResolutionRequest {
// node-haste/DependencyGraph/ResolutionRequest.js
constructor(options: ResolutionRequestOptions): void,
resolveDependency(from: Module, to: ModuleID): Promise<Module>,
}
export type ResolutionRequestT = ResolutionRequest;

View File

@ -0,0 +1,106 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
*/
'use strict';
import type { // eslint-disable-line sort-requires
DeprecatedAssetMapT,
Extensions,
HasteMapT,
HelpersT,
Path,
ResolutionRequestT,
} from './node-haste.flow';
import type {
ResolveFn,
TransformedFile,
} from '../types.flow';
const DependencyGraphHelpers: Class<HelpersT> = require('../../node-haste/DependencyGraph/DependencyGraphHelpers');
const DeprecatedAssetMap: Class<DeprecatedAssetMapT> = require('../../node-haste/DependencyGraph/DeprecatedAssetMap');
const FastFS = require('./FastFS');
const HasteMap: Class<HasteMapT> = require('../../node-haste/DependencyGraph/HasteMap');
const Module = require('./Module');
const ModuleCache = require('./ModuleCache');
const ResolutionRequest: Class<ResolutionRequestT> = require('../../node-haste/DependencyGraph/ResolutionRequest');
type ResolveOptions = {
assetExts: Extensions,
extraNodeModules: {[id: string]: string},
providesModuleNodeModules: Array<string>,
transformedFiles: {[path: Path]: TransformedFile},
};
const platforms = new Set(['android', 'ios']);
const returnTrue = () => true;
exports.createResolveFn = function(options: ResolveOptions): ResolveFn {
const {
assetExts,
extraNodeModules,
providesModuleNodeModules,
transformedFiles,
} = options;
const files = Object.keys(transformedFiles);
const getTransformedFile =
path => Promise.resolve(
transformedFiles[path] || Promise.reject(new Error(`"${path} does not exist`))
);
const helpers = new DependencyGraphHelpers({
assetExts,
providesModuleNodeModules,
});
const deprecatedAssetMap = new DeprecatedAssetMap({
assetExts,
files,
helpers,
platforms,
});
const fastfs = new FastFS(files);
const moduleCache = new ModuleCache(getTransformedFile);
const hasteMap = new HasteMap({
extensions: ['js', 'json'],
fastfs,
helpers,
moduleCache,
platforms,
preferNativePlatform: true,
});
const resolutionRequests = {};
return (id, source, platform, _, callback) => {
let resolutionRequest = resolutionRequests[platform];
if (!resolutionRequest) {
resolutionRequest = resolutionRequests[platform] = new ResolutionRequest({
deprecatedAssetMap,
extraNodeModules,
fastfs,
hasteMap,
helpers,
moduleCache,
platform,
platforms,
preferNativePlatform: true,
shouldThrowOnUnresolvedErrors: returnTrue,
});
}
const from = new Module(source, getTransformedFile(source));
resolutionRequest.resolveDependency(from, id).then(
// nextTick to escape promise error handling
module => process.nextTick(callback, null, module.path),
error => process.nextTick(callback, error),
);
};
};

View File

@ -0,0 +1 @@
{"main":"node-haste.js"}

View File

@ -68,3 +68,24 @@ export type LoadFn = (
options: LoadOptions, options: LoadOptions,
callback: callback2<File, Array<string>>, callback: callback2<File, Array<string>>,
) => void; ) => void;
type TransformResult = {
code: string,
map: ?Object,
dependencies: Array<String>,
};
export type TransformedFile = {
file: string,
code: string,
transformed: {[variant: string]: TransformResult},
hasteID: ?string,
package?: PackageData,
};
export type PackageData = {
name?: string,
main?: string,
browser?: Object | string,
'react-native'?: Object | string,
};

View File

@ -65,6 +65,7 @@ function transformJSON(infile, options, outfile, callback) {
if (basename(filename) === 'package.json') { if (basename(filename) === 'package.json') {
result.package = { result.package = {
name: value.name, name: value.name,
main: value.main,
browser: value.browser, browser: value.browser,
'react-native': value['react-native'], 'react-native': value['react-native'],
}; };