diff --git a/react-packager/src/ModuleGraph/node-haste/FastFS.js b/react-packager/src/ModuleGraph/node-haste/FastFS.js new file mode 100644 index 00000000..1378debf --- /dev/null +++ b/react-packager/src/ModuleGraph/node-haste/FastFS.js @@ -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; + directoryEntries: Map>; + files: Set; + + constructor(files: Array) { + 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; +} diff --git a/react-packager/src/ModuleGraph/node-haste/Module.js b/react-packager/src/ModuleGraph/node-haste/Module.js new file mode 100644 index 00000000..fac654d9 --- /dev/null +++ b/react-packager/src/ModuleGraph/node-haste/Module.js @@ -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; + name: Promise; + path: string; + type: 'Module'; + + constructor(path: string, info: Promise) { + 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[\///]/, ''); +} diff --git a/react-packager/src/ModuleGraph/node-haste/ModuleCache.js b/react-packager/src/ModuleGraph/node-haste/ModuleCache.js new file mode 100644 index 00000000..91de8307 --- /dev/null +++ b/react-packager/src/ModuleGraph/node-haste/ModuleCache.js @@ -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 = (path: string) => Promise; + +module.exports = class ModuleCache { + getPackageData: GetFn; + getTransformedFile: GetFn; + modules: Map; + packages: Map; + + constructor(getTransformedFile: GetFn) { + 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; + } +}; diff --git a/react-packager/src/ModuleGraph/node-haste/Package.js b/react-packager/src/ModuleGraph/node-haste/Package.js new file mode 100644 index 00000000..a9639361 --- /dev/null +++ b/react-packager/src/ModuleGraph/node-haste/Package.js @@ -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; + root: string; + type: 'Package'; + + constructor(packagePath: string, data: Promise) { + 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 }; +} diff --git a/react-packager/src/ModuleGraph/node-haste/node-haste.flow.js b/react-packager/src/ModuleGraph/node-haste/node-haste.flow.js new file mode 100644 index 00000000..cae82416 --- /dev/null +++ b/react-packager/src/ModuleGraph/node-haste/node-haste.flow.js @@ -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; + +export type Extensions = Array; + +export type Module = { + path: Path, + type: 'Module', + getName(): Promise, + isHaste(): Promise, +}; + +export type Package = { + type: 'Package', + root: Path, + getMain(): Promise, + getName(): Promise, + redirectRequire(id: ModuleID): Promise, +}; + +// 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, + matches(directory: Path, pattern: RegExp): Array, +}; + +type HelpersOptions = {| + assetExts: Extensions, + providesModuleNodeModules: Array, +|}; + +declare class Helpers { + // node-haste/DependencyGraph/DependencyGraphHelpers.js + constructor(options: HelpersOptions): void, +} +export type HelpersT = Helpers; + +type DeprecatedAssetMapOptions = {| + assetExts: Extensions, + files: Array, + 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, +} +export type ResolutionRequestT = ResolutionRequest; diff --git a/react-packager/src/ModuleGraph/node-haste/node-haste.js b/react-packager/src/ModuleGraph/node-haste/node-haste.js new file mode 100644 index 00000000..6013e689 --- /dev/null +++ b/react-packager/src/ModuleGraph/node-haste/node-haste.js @@ -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 = require('../../node-haste/DependencyGraph/DependencyGraphHelpers'); +const DeprecatedAssetMap: Class = require('../../node-haste/DependencyGraph/DeprecatedAssetMap'); +const FastFS = require('./FastFS'); +const HasteMap: Class = require('../../node-haste/DependencyGraph/HasteMap'); +const Module = require('./Module'); +const ModuleCache = require('./ModuleCache'); +const ResolutionRequest: Class = require('../../node-haste/DependencyGraph/ResolutionRequest'); + +type ResolveOptions = { + assetExts: Extensions, + extraNodeModules: {[id: string]: string}, + providesModuleNodeModules: Array, + 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), + ); + }; +}; diff --git a/react-packager/src/ModuleGraph/node-haste/package.json b/react-packager/src/ModuleGraph/node-haste/package.json new file mode 100644 index 00000000..f642436d --- /dev/null +++ b/react-packager/src/ModuleGraph/node-haste/package.json @@ -0,0 +1 @@ +{"main":"node-haste.js"} diff --git a/react-packager/src/ModuleGraph/types.flow.js b/react-packager/src/ModuleGraph/types.flow.js index ae62209f..f168e9ad 100644 --- a/react-packager/src/ModuleGraph/types.flow.js +++ b/react-packager/src/ModuleGraph/types.flow.js @@ -68,3 +68,24 @@ export type LoadFn = ( options: LoadOptions, callback: callback2>, ) => void; + +type TransformResult = { + code: string, + map: ?Object, + dependencies: Array, +}; + +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, +}; diff --git a/react-packager/src/ModuleGraph/worker.js b/react-packager/src/ModuleGraph/worker.js index 20b97a1d..c604d5fb 100644 --- a/react-packager/src/ModuleGraph/worker.js +++ b/react-packager/src/ModuleGraph/worker.js @@ -65,6 +65,7 @@ function transformJSON(infile, options, outfile, callback) { if (basename(filename) === 'package.json') { result.package = { name: value.name, + main: value.main, browser: value.browser, 'react-native': value['react-native'], };