Merge branch 'master' of ../react-native

This commit is contained in:
cpojer 2017-01-26 13:33:56 +00:00
commit a35016bd49
148 changed files with 24982 additions and 0 deletions

View File

@ -0,0 +1,155 @@
React Native Packager
--------------------
React Native Packager is a project similar in scope to browserify or
webpack, it provides a CommonJS-like module system, JavaScript
compilation (ES6, Flow, JSX), bundling, and asset loading.
The main difference is the Packager's focus on compilation and
bundling speed. We aim for a sub-second edit-reload
cycles. Additionally, we don't want users -- with large code bases --
to wait more than a few seconds after starting the packager.
The main deviation from the node module system is the support for our
proprietary module format known as `@providesModule`. However, we
discourage people from using this module format because going forward we
want to completely separate our infrastructure from React Native and
provide an experience most JavaScript developers are familiar with,
namely the node module format. We want to even go further, and let you
choose your own packager and asset pipeline or even integrate into
your existing infrastructure.
React Native users need not to understand how the packager work,
however, this documentation might be useful for advanced users and
people who want to fix bugs or add features to the packager (patches
welcome!).
## HTTP interface
The main way you'd interact with the packager is via the HTTP
interface. The following is the list of endpoints and their respective
functions.
### /path/to/moduleName.bundle
Does the following in order:
* parse out `path/to/moduleName`
* add a `.js` suffix to the path
* looks in your project root(s) for the file
* recursively collects all the dependencies from an in memory graph
* runs the modules through the transformer (might just be cached)
* concatenate the modules' content into a bundle
* responds to the client with the bundle (and a SourceMap URL)
### /path/to/moduleName.map
* if the package has been previously generated via the `.bundle`
endpoint then the source map will be generated from that package
* if the package has not been previously asked for, this will go
through the same steps outlined in the `.bundle` endpoint then
generate the source map.
Note that source map generation currently assumes that the code has
been compiled with jstransform, which preserves line and column
numbers which allows us to generate source maps super fast.
### /path/to/moduleName.(map|bundle) query params
You can pass options for the bundle creation through the query params,
if the option is boolean `1/0` or `true/false` is accepted.
Here are the current options the packager accepts:
* `dev` boolean, defaults to true: sets a global `__DEV__` variable
which will effect how the React Native core libraries behave.
* `minify` boolean, defaults to false: whether to minify the bundle.
* `runModule` boolean, defaults to true: whether to require your entry
point module. So if you requested `moduleName`, this option will add
a `require('moduleName')` the end of your bundle.
* `inlineSourceMap` boolean, defaults to false: whether to inline
source maps.
### /debug
This is a page used for debugging, it offers a link to a single page :
* Cached Packages: which shows you the packages that's been already
generated and cached
## Programmatic API
The packager is made of two things:
* The core packager (which we're calling ReactPackager)
* The scripts, devtools launcher, server run etc.
ReactPackager is how you mainly interact with the API.
```js
var ReactPackager = require('./react-packager');
```
### ReactPackager.buildBundle(serverOptions, bundleOptions)
Builds a bundle according to the provided options.
#### `serverOptions`
* `projectRoots` array (required): Is the roots where your JavaScript
file will exist
* `blacklistRE` regexp: Is a pattern to ignore certain paths from the
packager
* `polyfillModuleName` array: Paths to polyfills you want to be
included at the start of the bundle
* `cacheVersion` string: used in creating the cache file
* `resetCache` boolean, defaults to false: whether to use the cache on
disk
* `transformModulePath` string: Path to the module used as a
JavaScript transformer
* `nonPersistent` boolean, defaults to false: Whether the server
should be used as a persistent deamon to watch files and update
itself
* `getTransformOptions` function: A function that acts as a middleware for
generating options to pass to the transformer based on the bundle being built.
* `reporter` object (required): An object with a single function `update` that
is called when events are happening: build updates, warnings, errors.
#### `bundleOptions`
* `entryFile` string (required): the entry file of the bundle, relative to one
of the roots.
* `dev` boolean (defaults to `true`): sets a global `__DEV__` variable
which will effect how the React Native core libraries behave.
* `minify` boolean: Whether to minify code and apply production optimizations.
* `runModule` boolean (defaults to `true`): whether to require your entry
point module.
* `inlineSourceMap` boolean, defaults to false: whether to inline
source maps.
* `platform` string: The target platform for the build
* `generateSourceMaps` boolean: Whether to generate source maps.
* `sourceMapUrl` string: The url of the source map (will be appended to
the bundle).
## Debugging
To get verbose output when running the packager, define an environment variable:
export DEBUG=RNP:*
You can combine this with other values, e.g. `DEBUG=babel,RNP:*`. Under the hood this uses the [`debug`](https://www.npmjs.com/package/debug) package, see its documentation for all the available options.
The `/debug` endpoint discussed above is also useful.
## FAQ
### Can I use this in my own non-React Native project?
Yes. It's not really tied to React Native, however feature development
is informed by React Native needs.
### Why didn't you use webpack?
We love webpack, however, when we tried on our codebase it was slower
than our developers would like it to be. You can find more discussion about
the subject [here](https://github.com/facebook/react-native/issues/5).

View File

@ -0,0 +1,38 @@
/**
* 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.
*/
'use strict';
Array.prototype.values || require('core-js/fn/array/values');
Object.entries || require('core-js/fn/object/entries');
Object.values || require('core-js/fn/object/values');
var _only = [];
function registerOnly(onlyList) {
require('babel-register')(config(onlyList));
}
function config(onlyList) {
_only = _only.concat(onlyList);
return {
presets: ['es2015-node'],
plugins: [
'transform-flow-strip-types',
'syntax-trailing-function-commas',
'transform-object-rest-spread',
],
only: _only,
retainLines: true,
sourceMaps: 'inline',
babelrc: false,
};
}
module.exports = exports = registerOnly;
exports.config = config;

View File

@ -0,0 +1,47 @@
/**
* 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.
*/
'use strict';
var path = require('path');
// Don't forget to everything listed here to `package.json`
// modulePathIgnorePatterns.
var sharedBlacklist = [
/node_modules[/\\]react[/\\]dist[/\\].*/,
/website\/node_modules\/.*/,
// TODO(jkassens, #9876132): Remove this rule when it's no longer needed.
'Libraries/Relay/relay/tools/relayUnstableBatchedUpdates.js',
/heapCapture\/bundle\.js/,
];
function escapeRegExp(pattern) {
if (Object.prototype.toString.call(pattern) === '[object RegExp]') {
return pattern.source.replace(/\//g, path.sep);
} else if (typeof pattern === 'string') {
var escaped = pattern.replace(/[\-\[\]\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
// convert the '/' into an escaped local file separator
return escaped.replace(/\//g,'\\' + path.sep);
} else {
throw new Error('Unexpected packager blacklist pattern: ' + pattern);
}
}
function blacklist(additionalBlacklist) {
return new RegExp('(' +
(additionalBlacklist || []).concat(sharedBlacklist)
.map(escapeRegExp)
.join('|') +
')$'
);
}
module.exports = blacklist;

View File

@ -0,0 +1,44 @@
/**
* 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';
exports.assetExts = [
'bmp', 'gif', 'jpg', 'jpeg', 'png', 'psd', 'svg', 'webp', // Image formats
'm4v', 'mov', 'mp4', 'mpeg', 'mpg', 'webm', // Video formats
'aac', 'aiff', 'caf', 'm4a', 'mp3', 'wav', // Audio formats
'html', 'pdf', // Document formats
];
exports.moduleSystem = require.resolve('./react-packager/src/Resolver/polyfills/require.js');
exports.platforms = ['ios', 'android', 'windows', 'web'];
exports.polyfills = [
require.resolve('./react-packager/src/Resolver/polyfills/polyfills.js'),
require.resolve('./react-packager/src/Resolver/polyfills/console.js'),
require.resolve('./react-packager/src/Resolver/polyfills/error-guard.js'),
require.resolve('./react-packager/src/Resolver/polyfills/Number.es6.js'),
require.resolve('./react-packager/src/Resolver/polyfills/String.prototype.es6.js'),
require.resolve('./react-packager/src/Resolver/polyfills/Array.prototype.es6.js'),
require.resolve('./react-packager/src/Resolver/polyfills/Array.es6.js'),
require.resolve('./react-packager/src/Resolver/polyfills/Object.es7.js'),
require.resolve('./react-packager/src/Resolver/polyfills/babelHelpers.js'),
];
exports.providesModuleNodeModules = [
'react-native',
'react-native-windows',
];
exports.runBeforeMainModule = [
// Ensures essential globals are available and are patched correctly.
'InitializeCore',
];

View File

@ -0,0 +1,12 @@
:: 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.
@echo off
title React Packager
node "%~dp0..\local-cli\cli.js" start
pause
exit

View File

@ -0,0 +1,20 @@
#!/usr/bin/env bash
# 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.
# Set terminal title
echo -en "\033]0;React Packager\a"
clear
THIS_DIR=$(dirname "$0")
pushd "$THIS_DIR"
source ./packager.sh
popd
echo "Process terminated. Press <enter> to close the window"
read

View File

@ -0,0 +1,11 @@
#!/usr/bin/env bash
# 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.
THIS_DIR=$(dirname "$0")
node "$THIS_DIR/../local-cli/cli.js" start "$@"

View File

@ -0,0 +1,100 @@
#!/bin/bash
# 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.
# Bundle React Native app's code and image assets.
# This script is supposed to be invoked as part of Xcode build process
# and relies on environment variables (including PWD) set by Xcode
case "$CONFIGURATION" in
Debug)
# Speed up build times by skipping the creation of the offline package for debug
# builds on the simulator since the packager is supposed to be running anyways.
if [[ "$PLATFORM_NAME" == *simulator ]]; then
echo "Skipping bundling for Simulator platform"
exit 0;
fi
DEV=true
;;
"")
echo "$0 must be invoked by Xcode"
exit 1
;;
*)
DEV=false
;;
esac
# Path to react-native folder inside node_modules
REACT_NATIVE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# Xcode project file for React Native apps is located in ios/ subfolder
cd "${REACT_NATIVE_DIR}"/../..
# Define NVM_DIR and source the nvm.sh setup script
[ -z "$NVM_DIR" ] && export NVM_DIR="$HOME/.nvm"
# Define entry file
ENTRY_FILE=${1:-index.ios.js}
if [[ -s "$HOME/.nvm/nvm.sh" ]]; then
. "$HOME/.nvm/nvm.sh"
elif [[ -x "$(command -v brew)" && -s "$(brew --prefix nvm)/nvm.sh" ]]; then
. "$(brew --prefix nvm)/nvm.sh"
fi
# Set up the nodenv node version manager if present
if [[ -x "$HOME/.nodenv/bin/nodenv" ]]; then
eval "$("$HOME/.nodenv/bin/nodenv" init -)"
fi
[ -z "$NODE_BINARY" ] && export NODE_BINARY="node"
nodejs_not_found()
{
echo "error: Can't find '$NODE_BINARY' binary to build React Native bundle" >&2
echo "If you have non-standard nodejs installation, select your project in Xcode," >&2
echo "find 'Build Phases' - 'Bundle React Native code and images'" >&2
echo "and change NODE_BINARY to absolute path to your node executable" >&2
echo "(you can find it by invoking 'which node' in the terminal)" >&2
exit 2
}
type $NODE_BINARY >/dev/null 2>&1 || nodejs_not_found
# Print commands before executing them (useful for troubleshooting)
set -x
DEST=$CONFIGURATION_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH
if [[ "$CONFIGURATION" = "Debug" && ! "$PLATFORM_NAME" == *simulator ]]; then
PLISTBUDDY='/usr/libexec/PlistBuddy'
PLIST=$TARGET_BUILD_DIR/$INFOPLIST_PATH
IP=$(ipconfig getifaddr en0)
if [ -z "$IP" ]; then
IP=$(ifconfig | grep 'inet ' | grep -v 127.0.0.1 | cut -d\ -f2 | awk 'NR==1{print $1}')
fi
$PLISTBUDDY -c "Add NSAppTransportSecurity:NSExceptionDomains:localhost:NSTemporaryExceptionAllowsInsecureHTTPLoads bool true" "$PLIST"
$PLISTBUDDY -c "Add NSAppTransportSecurity:NSExceptionDomains:$IP.xip.io:NSTemporaryExceptionAllowsInsecureHTTPLoads bool true" "$PLIST"
echo "$IP.xip.io" > "$DEST/ip.txt"
fi
BUNDLE_FILE="$DEST/main.jsbundle"
$NODE_BINARY "$REACT_NATIVE_DIR/local-cli/cli.js" bundle \
--entry-file "$ENTRY_FILE" \
--platform ios \
--dev $DEV \
--reset-cache \
--bundle-output "$BUNDLE_FILE" \
--assets-dest "$DEST"
if [[ ! $DEV && ! -f "$BUNDLE_FILE" ]]; then
echo "error: File $BUNDLE_FILE does not exist. This must be a bug with" >&2
echo "React Native, please report it here: https://github.com/facebook/react-native/issues"
exit 2
fi

View File

@ -0,0 +1,8 @@
*~
*.swm
*.swn
*.swp
*.DS_STORE
npm-debug.log
.cache
node_modules

View File

@ -0,0 +1,15 @@
/**
* 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';
require('../../setupBabel')();
module.exports = require('./react-packager');

View File

@ -0,0 +1,99 @@
/**
* 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 Logger = require('./src/Logger');
const debug = require('debug');
const invariant = require('fbjs/lib/invariant');
import type GlobalTransformCache from './src/lib/GlobalTransformCache';
import type {Reporter} from './src/lib/reporting';
exports.createServer = createServer;
exports.Logger = Logger;
type Options = {
globalTransformCache: ?GlobalTransformCache,
nonPersistent?: boolean,
projectRoots: Array<string>,
reporter?: Reporter,
watch?: boolean,
};
type StrictOptions = {
globalTransformCache: ?GlobalTransformCache,
nonPersistent?: boolean,
projectRoots: Array<string>,
reporter: Reporter,
watch?: boolean,
};
exports.buildBundle = function(options: Options, bundleOptions: {}) {
var server = createNonPersistentServer(options);
return server.buildBundle(bundleOptions)
.then(p => {
server.end();
return p;
});
};
exports.getOrderedDependencyPaths = function(options: Options, bundleOptions: {}) {
var server = createNonPersistentServer(options);
return server.getOrderedDependencyPaths(bundleOptions)
.then(function(paths) {
server.end();
return paths;
});
};
function enableDebug() {
// react-packager logs debug messages using the 'debug' npm package, and uses
// the following prefix throughout.
// To enable debugging, we need to set our pattern or append it to any
// existing pre-configured pattern to avoid disabling logging for
// other packages
var debugPattern = 'RNP:*';
var existingPattern = debug.load();
if (existingPattern) {
debugPattern += ',' + existingPattern;
}
debug.enable(debugPattern);
}
function createServer(options: StrictOptions) {
// the debug module is configured globally, we need to enable debugging
// *before* requiring any packages that use `debug` for logging
if (options.verbose) {
enableDebug();
}
// Some callsites may not be Flowified yet.
invariant(options.reporter != null, 'createServer() requires reporter');
const serverOptions = Object.assign({}, options);
delete serverOptions.verbose;
var Server = require('./src/Server');
return new Server(serverOptions);
}
function createNonPersistentServer(options: Options) {
const serverOptions = {
// It's unsound to set-up the reporter here,
// but this allows backward compatibility.
reporter: options.reporter == null
? require('./src/lib/reporting').nullReporter
: options.reporter,
...options,
watch: !options.nonPersistent,
};
return createServer(serverOptions);
}

View File

@ -0,0 +1,4 @@
{
"presets": [ "react-native" ],
"plugins": []
}

View File

@ -0,0 +1,307 @@
/**
* Copyright (c) 2013-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.
*/
'use strict';
jest.disableAutomock();
jest.mock('fs');
const AssetServer = require('../');
const crypto = require('crypto');
const fs = require('fs');
const {objectContaining} = jasmine;
describe('AssetServer', () => {
beforeEach(() => {
const NodeHaste = require('../../node-haste');
NodeHaste.getAssetDataFromName =
require.requireActual('../../node-haste/lib/getAssetDataFromName');
});
describe('assetServer.get', () => {
it('should work for the simple case', () => {
const server = new AssetServer({
projectRoots: ['/root'],
assetExts: ['png'],
});
fs.__setMockFilesystem({
'root': {
imgs: {
'b.png': 'b image',
'b@2x.png': 'b2 image',
}
}
});
return Promise.all([
server.get('imgs/b.png'),
server.get('imgs/b@1x.png'),
]).then(resp =>
resp.forEach(data =>
expect(data).toBe('b image')
)
);
});
it('should work for the simple case with platform ext', () => {
const server = new AssetServer({
projectRoots: ['/root'],
assetExts: ['png'],
});
fs.__setMockFilesystem({
'root': {
imgs: {
'b.ios.png': 'b ios image',
'b.android.png': 'b android image',
'c.png': 'c general image',
'c.android.png': 'c android image',
}
}
});
return Promise.all([
server.get('imgs/b.png', 'ios').then(
data => expect(data).toBe('b ios image')
),
server.get('imgs/b.png', 'android').then(
data => expect(data).toBe('b android image')
),
server.get('imgs/c.png', 'android').then(
data => expect(data).toBe('c android image')
),
server.get('imgs/c.png', 'ios').then(
data => expect(data).toBe('c general image')
),
server.get('imgs/c.png').then(
data => expect(data).toBe('c general image')
),
]);
});
it('should work for the simple case with jpg', () => {
const server = new AssetServer({
projectRoots: ['/root'],
assetExts: ['png', 'jpg'],
});
fs.__setMockFilesystem({
'root': {
imgs: {
'b.png': 'png image',
'b.jpg': 'jpeg image',
}
}
});
return Promise.all([
server.get('imgs/b.jpg'),
server.get('imgs/b.png'),
]).then(data =>
expect(data).toEqual([
'jpeg image',
'png image',
])
);
});
it('should pick the bigger one', () => {
const server = new AssetServer({
projectRoots: ['/root'],
assetExts: ['png'],
});
fs.__setMockFilesystem({
'root': {
imgs: {
'b@1x.png': 'b1 image',
'b@2x.png': 'b2 image',
'b@4x.png': 'b4 image',
'b@4.5x.png': 'b4.5 image',
}
}
});
return server.get('imgs/b@3x.png').then(data =>
expect(data).toBe('b4 image')
);
});
it('should pick the bigger one with platform ext', () => {
const server = new AssetServer({
projectRoots: ['/root'],
assetExts: ['png'],
});
fs.__setMockFilesystem({
'root': {
imgs: {
'b@1x.png': 'b1 image',
'b@2x.png': 'b2 image',
'b@4x.png': 'b4 image',
'b@4.5x.png': 'b4.5 image',
'b@1x.ios.png': 'b1 ios image',
'b@2x.ios.png': 'b2 ios image',
'b@4x.ios.png': 'b4 ios image',
'b@4.5x.ios.png': 'b4.5 ios image',
}
}
});
return Promise.all([
server.get('imgs/b@3x.png').then(data =>
expect(data).toBe('b4 image')
),
server.get('imgs/b@3x.png', 'ios').then(data =>
expect(data).toBe('b4 ios image')
),
]);
});
it('should support multiple project roots', () => {
const server = new AssetServer({
projectRoots: ['/root', '/root2'],
assetExts: ['png'],
});
fs.__setMockFilesystem({
'root': {
imgs: {
'b.png': 'b image',
},
},
'root2': {
'newImages': {
'imgs': {
'b@1x.png': 'b1 image',
},
},
},
});
return server.get('newImages/imgs/b.png').then(data =>
expect(data).toBe('b1 image')
);
});
});
describe('assetServer.getAssetData', () => {
it('should get assetData', () => {
const server = new AssetServer({
projectRoots: ['/root'],
assetExts: ['png'],
});
fs.__setMockFilesystem({
'root': {
imgs: {
'b@1x.png': 'b1 image',
'b@2x.png': 'b2 image',
'b@4x.png': 'b4 image',
'b@4.5x.png': 'b4.5 image',
}
}
});
return server.getAssetData('imgs/b.png').then(data => {
expect(data).toEqual(objectContaining({
type: 'png',
name: 'b',
scales: [1, 2, 4, 4.5],
files: [
'/root/imgs/b@1x.png',
'/root/imgs/b@2x.png',
'/root/imgs/b@4x.png',
'/root/imgs/b@4.5x.png',
],
}));
});
});
it('should get assetData for non-png images', () => {
const server = new AssetServer({
projectRoots: ['/root'],
assetExts: ['png', 'jpeg'],
});
fs.__setMockFilesystem({
'root': {
imgs: {
'b@1x.jpg': 'b1 image',
'b@2x.jpg': 'b2 image',
'b@4x.jpg': 'b4 image',
'b@4.5x.jpg': 'b4.5 image',
}
}
});
return server.getAssetData('imgs/b.jpg').then(data => {
expect(data).toEqual(objectContaining({
type: 'jpg',
name: 'b',
scales: [1, 2, 4, 4.5],
files: [
'/root/imgs/b@1x.jpg',
'/root/imgs/b@2x.jpg',
'/root/imgs/b@4x.jpg',
'/root/imgs/b@4.5x.jpg',
],
}));
});
});
describe('hash:', () => {
let server, mockFS;
beforeEach(() => {
server = new AssetServer({
projectRoots: ['/root'],
assetExts: ['jpg'],
});
mockFS = {
'root': {
imgs: {
'b@1x.jpg': 'b1 image',
'b@2x.jpg': 'b2 image',
'b@4x.jpg': 'b4 image',
'b@4.5x.jpg': 'b4.5 image',
}
}
};
fs.__setMockFilesystem(mockFS);
});
it('uses the file contents to build the hash', () => {
const hash = crypto.createHash('md5');
for (const name in mockFS.root.imgs) {
hash.update(mockFS.root.imgs[name]);
}
return server.getAssetData('imgs/b.jpg').then(data =>
expect(data).toEqual(objectContaining({hash: hash.digest('hex')}))
);
});
it('changes the hash when the passed-in file watcher emits an `all` event', () => {
return server.getAssetData('imgs/b.jpg').then(initialData => {
mockFS.root.imgs['b@4x.jpg'] = 'updated data';
server.onFileChange('all', '/root/imgs/b@4x.jpg');
return server.getAssetData('imgs/b.jpg').then(data =>
expect(data.hash).not.toEqual(initialData.hash)
);
});
});
});
});
});

View File

@ -0,0 +1,242 @@
/**
* 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.
*/
'use strict';
const crypto = require('crypto');
const declareOpts = require('../lib/declareOpts');
const denodeify = require('denodeify');
const fs = require('fs');
const getAssetDataFromName = require('../node-haste').getAssetDataFromName;
const path = require('path');
const createTimeoutPromise = (timeout) => new Promise((resolve, reject) => {
setTimeout(reject, timeout, 'fs operation timeout');
});
function timeoutableDenodeify(fsFunc, timeout) {
return function raceWrapper(...args) {
return Promise.race([
createTimeoutPromise(timeout),
denodeify(fsFunc).apply(this, args)
]);
};
}
const FS_OP_TIMEOUT = parseInt(process.env.REACT_NATIVE_FSOP_TIMEOUT, 10) || 15000;
const stat = timeoutableDenodeify(fs.stat, FS_OP_TIMEOUT);
const readDir = timeoutableDenodeify(fs.readdir, FS_OP_TIMEOUT);
const readFile = timeoutableDenodeify(fs.readFile, FS_OP_TIMEOUT);
const validateOpts = declareOpts({
projectRoots: {
type: 'array',
required: true,
},
assetExts: {
type: 'array',
required: true,
},
});
class AssetServer {
constructor(options) {
const opts = validateOpts(options);
this._roots = opts.projectRoots;
this._assetExts = opts.assetExts;
this._hashes = new Map();
this._files = new Map();
}
get(assetPath, platform = null) {
const assetData = getAssetDataFromName(assetPath, new Set([platform]));
return this._getAssetRecord(assetPath, platform).then(record => {
for (let i = 0; i < record.scales.length; i++) {
if (record.scales[i] >= assetData.resolution) {
return readFile(record.files[i]);
}
}
return readFile(record.files[record.files.length - 1]);
});
}
getAssetData(assetPath, platform = null) {
const nameData = getAssetDataFromName(assetPath, new Set([platform]));
const data = {
name: nameData.name,
type: nameData.type,
};
return this._getAssetRecord(assetPath, platform).then(record => {
data.scales = record.scales;
data.files = record.files;
if (this._hashes.has(assetPath)) {
data.hash = this._hashes.get(assetPath);
return data;
}
return new Promise((resolve, reject) => {
const hash = crypto.createHash('md5');
hashFiles(data.files.slice(), hash, error => {
if (error) {
reject(error);
} else {
data.hash = hash.digest('hex');
this._hashes.set(assetPath, data.hash);
data.files.forEach(f => this._files.set(f, assetPath));
resolve(data);
}
});
});
});
}
onFileChange(type, filePath) {
this._hashes.delete(this._files.get(filePath));
}
/**
* Given a request for an image by path. That could contain a resolution
* postfix, we need to find that image (or the closest one to it's resolution)
* in one of the project roots:
*
* 1. We first parse the directory of the asset
* 2. We check to find a matching directory in one of the project roots
* 3. We then build a map of all assets and their scales in this directory
* 4. Then try to pick platform-specific asset records
* 5. Then pick the closest resolution (rounding up) to the requested one
*/
_getAssetRecord(assetPath, platform = null) {
const filename = path.basename(assetPath);
return (
this._findRoot(
this._roots,
path.dirname(assetPath),
assetPath,
)
.then(dir => Promise.all([
dir,
readDir(dir),
]))
.then(res => {
const dir = res[0];
const files = res[1];
const assetData = getAssetDataFromName(filename, new Set([platform]));
const map = this._buildAssetMap(dir, files, platform);
let record;
if (platform != null){
record = map[getAssetKey(assetData.assetName, platform)] ||
map[assetData.assetName];
} else {
record = map[assetData.assetName];
}
if (!record) {
throw new Error(
`Asset not found: ${assetPath} for platform: ${platform}`
);
}
return record;
})
);
}
_findRoot(roots, dir, debugInfoFile) {
return Promise.all(
roots.map(root => {
const absRoot = path.resolve(root);
// important: we want to resolve root + dir
// to ensure the requested path doesn't traverse beyond root
const absPath = path.resolve(root, dir);
return stat(absPath).then(fstat => {
// keep asset requests from traversing files
// up from the root (e.g. ../../../etc/hosts)
if (!absPath.startsWith(absRoot)) {
return {path: absPath, isValid: false};
}
return {path: absPath, isValid: fstat.isDirectory()};
}, _ => {
return {path: absPath, isValid: false};
});
})
).then(stats => {
for (let i = 0; i < stats.length; i++) {
if (stats[i].isValid) {
return stats[i].path;
}
}
const rootsString = roots.map(s => `'${s}'`).join(', ');
throw new Error(
`'${debugInfoFile}' could not be found, because '${dir}' is not a ` +
`subdirectory of any of the roots (${rootsString})`,
);
});
}
_buildAssetMap(dir, files, platform) {
const assets = files.map(this._getAssetDataFromName.bind(this, new Set([platform])));
const map = Object.create(null);
assets.forEach(function(asset, i) {
const file = files[i];
const assetKey = getAssetKey(asset.assetName, asset.platform);
let record = map[assetKey];
if (!record) {
record = map[assetKey] = {
scales: [],
files: [],
};
}
let insertIndex;
const length = record.scales.length;
for (insertIndex = 0; insertIndex < length; insertIndex++) {
if (asset.resolution < record.scales[insertIndex]) {
break;
}
}
record.scales.splice(insertIndex, 0, asset.resolution);
record.files.splice(insertIndex, 0, path.join(dir, file));
});
return map;
}
_getAssetDataFromName(platform, file) {
return getAssetDataFromName(file, platform);
}
}
function getAssetKey(assetName, platform) {
if (platform != null) {
return `${assetName} : ${platform}`;
} else {
return assetName;
}
}
function hashFiles(files, hash, callback) {
if (!files.length) {
callback(null);
return;
}
fs.createReadStream(files.shift())
.on('data', data => hash.update(data))
.once('end', () => hashFiles(files, hash, callback))
.once('error', error => callback(error));
}
module.exports = AssetServer;

View File

@ -0,0 +1,437 @@
/**
* 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 BundleBase = require('./BundleBase');
const ModuleTransport = require('../lib/ModuleTransport');
const _ = require('lodash');
const crypto = require('crypto');
const debug = require('debug')('RNP:Bundle');
const invariant = require('fbjs/lib/invariant');
const {fromRawMappings} = require('./source-map');
import type {SourceMap, CombinedSourceMap, MixedSourceMap} from '../lib/SourceMap';
import type {GetSourceOptions, FinalizeOptions} from './BundleBase';
export type Unbundle = {
startupModules: Array<*>,
lazyModules: Array<*>,
groups: Map<number, Set<number>>,
};
type SourceMapFormat = 'undetermined' | 'indexed' | 'flattened';
const SOURCEMAPPING_URL = '\n\/\/# sourceMappingURL=';
class Bundle extends BundleBase {
_dev: boolean | void;
_inlineSourceMap: string | void;
_minify: boolean | void;
_numRequireCalls: number;
_ramBundle: Unbundle | null;
_ramGroups: Array<string> | void;
_sourceMap: string | null;
_sourceMapFormat: SourceMapFormat;
_sourceMapUrl: string | void;
constructor({sourceMapUrl, dev, minify, ramGroups}: {
sourceMapUrl?: string,
dev?: boolean,
minify?: boolean,
ramGroups?: Array<string>,
} = {}) {
super();
this._sourceMap = null;
this._sourceMapFormat = 'undetermined';
this._sourceMapUrl = sourceMapUrl;
this._numRequireCalls = 0;
this._dev = dev;
this._minify = minify;
this._ramGroups = ramGroups;
this._ramBundle = null; // cached RAM Bundle
}
addModule(
/**
* $FlowFixMe: this code is inherently incorrect, because it modifies the
* signature of the base class function "addModule". That means callsites
* using an instance typed as the base class would be broken. This must be
* refactored.
*/
resolver: {wrapModule: (options: any) => Promise<{code: any, map: any}>},
resolutionResponse: mixed,
module: mixed,
/* $FlowFixMe: erroneous change of signature. */
moduleTransport: ModuleTransport,
/* $FlowFixMe: erroneous change of signature. */
): Promise<void> {
const index = super.addModule(moduleTransport);
return resolver.wrapModule({
resolutionResponse,
module,
name: moduleTransport.name,
code: moduleTransport.code,
map: moduleTransport.map,
meta: moduleTransport.meta,
minify: this._minify,
dev: this._dev,
}).then(({code, map}) => {
// If we get a map from the transformer we'll switch to a mode
// were we're combining the source maps as opposed to
if (map) {
const usesRawMappings = isRawMappings(map);
if (this._sourceMapFormat === 'undetermined') {
this._sourceMapFormat = usesRawMappings ? 'flattened' : 'indexed';
} else if (usesRawMappings && this._sourceMapFormat === 'indexed') {
throw new Error(
`Got at least one module with a full source map, but ${
moduleTransport.sourcePath} has raw mappings`
);
} else if (!usesRawMappings && this._sourceMapFormat === 'flattened') {
throw new Error(
`Got at least one module with raw mappings, but ${
moduleTransport.sourcePath} has a full source map`
);
}
}
this.replaceModuleAt(
index, new ModuleTransport({...moduleTransport, code, map}));
});
}
finalize(options: FinalizeOptions) {
options = options || {};
if (options.runMainModule) {
/* $FlowFixMe: this is unsound, as nothing enforces runBeforeMainModule
* to be available if `runMainModule` is true. Refactor. */
options.runBeforeMainModule.forEach(this._addRequireCall, this);
/* $FlowFixMe: this is unsound, as nothing enforces the module ID to have
* been set beforehand. */
this._addRequireCall(this.getMainModuleId());
}
super.finalize(options);
}
_addRequireCall(moduleId: string) {
const code = `;require(${JSON.stringify(moduleId)});`;
const name = 'require-' + moduleId;
super.addModule(new ModuleTransport({
name,
id: -this._numRequireCalls - 1,
code,
virtual: true,
sourceCode: code,
sourcePath: name + '.js',
meta: {preloaded: true},
}));
this._numRequireCalls += 1;
}
_getInlineSourceMap(dev) {
if (this._inlineSourceMap == null) {
const sourceMap = this.getSourceMapString({excludeSource: true, dev});
/*eslint-env node*/
const encoded = new Buffer(sourceMap).toString('base64');
this._inlineSourceMap = 'data:application/json;base64,' + encoded;
}
return this._inlineSourceMap;
}
getSource(options: GetSourceOptions) {
this.assertFinalized();
options = options || {};
let source = super.getSource(options);
if (options.inlineSourceMap) {
source += SOURCEMAPPING_URL + this._getInlineSourceMap(options.dev);
} else if (this._sourceMapUrl) {
source += SOURCEMAPPING_URL + this._sourceMapUrl;
}
return source;
}
getUnbundle(): Unbundle {
this.assertFinalized();
if (!this._ramBundle) {
const modules = this.getModules().slice();
// separate modules we need to preload from the ones we don't
const [startupModules, lazyModules] = partition(modules, shouldPreload);
const ramGroups = this._ramGroups;
let groups;
this._ramBundle = {
startupModules,
lazyModules,
get groups() {
if (!groups) {
groups = createGroups(ramGroups || [], lazyModules);
}
return groups;
},
};
}
return this._ramBundle;
}
invalidateSource() {
debug('invalidating bundle');
super.invalidateSource();
this._sourceMap = null;
}
/**
* Combine each of the sourcemaps multiple modules have into a single big
* one. This works well thanks to a neat trick defined on the sourcemap spec
* that makes use of of the `sections` field to combine sourcemaps by adding
* an offset. This is supported only by Chrome for now.
*/
_getCombinedSourceMaps(options): CombinedSourceMap {
const result = {
version: 3,
file: this._getSourceMapFile(),
sections: [],
};
let line = 0;
this.getModules().forEach(module => {
let map = module.map == null || module.virtual
? generateSourceMapForVirtualModule(module)
: module.map;
invariant(
!Array.isArray(map),
`Unexpected raw mappings for ${module.sourcePath}`,
);
if (options.excludeSource && 'sourcesContent' in map) {
map = {...map, sourcesContent: []};
}
result.sections.push({
offset: { line: line, column: 0 },
map: (map: MixedSourceMap),
});
line += module.code.split('\n').length;
});
return result;
}
getSourceMap(options: {excludeSource?: boolean}): MixedSourceMap {
this.assertFinalized();
return this._sourceMapFormat === 'indexed'
? this._getCombinedSourceMaps(options)
: fromRawMappings(this.getModules()).toMap();
}
getSourceMapString(options: {excludeSource?: boolean}): string {
if (this._sourceMapFormat === 'indexed') {
return JSON.stringify(this.getSourceMap(options));
}
// The following code is an optimization specific to the development server:
// 1. generator.toSource() is faster than JSON.stringify(generator.toMap()).
// 2. caching the source map unless there are changes saves time in
// development settings.
let map = this._sourceMap;
if (map == null) {
debug('Start building flat source map');
map = this._sourceMap = fromRawMappings(this.getModules()).toString();
debug('End building flat source map');
} else {
debug('Returning cached source map');
}
return map;
}
getEtag() {
/* $FlowFixMe: we must pass options, or rename the
* base `getSource` function, as it does not actually need options. */
var eTag = crypto.createHash('md5').update(this.getSource()).digest('hex');
return eTag;
}
_getSourceMapFile() {
return this._sourceMapUrl
? this._sourceMapUrl.replace('.map', '.bundle')
: 'bundle.js';
}
getJSModulePaths() {
return this.getModules()
// Filter out non-js files. Like images etc.
.filter(module => !module.virtual)
.map(module => module.sourcePath);
}
getDebugInfo() {
return [
/* $FlowFixMe: this is unsound as the module ID could be unset. */
'<div><h3>Main Module:</h3> ' + this.getMainModuleId() + '</div>',
'<style>',
'pre.collapsed {',
' height: 10px;',
' width: 100px;',
' display: block;',
' text-overflow: ellipsis;',
' overflow: hidden;',
' cursor: pointer;',
'}',
'</style>',
'<h3> Module paths and transformed code: </h3>',
this.getModules().map(function(m) {
return '<div> <h4> Path: </h4>' + m.sourcePath + '<br/> <h4> Source: </h4>' +
'<code><pre class="collapsed" onclick="this.classList.remove(\'collapsed\')">' +
_.escape(m.code) + '</pre></code></div>';
}).join('\n'),
].join('\n');
}
setRamGroups(ramGroups: Array<string>) {
this._ramGroups = ramGroups;
}
}
function generateSourceMapForVirtualModule(module): SourceMap {
// All lines map 1-to-1
let mappings = 'AAAA;';
for (let i = 1; i < module.code.split('\n').length; i++) {
mappings += 'AACA;';
}
return {
version: 3,
sources: [ module.sourcePath ],
names: [],
mappings: mappings,
file: module.sourcePath,
sourcesContent: [ module.sourceCode ],
};
}
function shouldPreload({meta}) {
return meta && meta.preloaded;
}
function partition(array, predicate) {
const included = [];
const excluded = [];
array.forEach(item => (predicate(item) ? included : excluded).push(item));
return [included, excluded];
}
function * filter(iterator, predicate) {
for (const value of iterator) {
if (predicate(value)) {
yield value;
}
}
}
function * subtree(moduleTransport: ModuleTransport, moduleTransportsByPath, seen = new Set()) {
seen.add(moduleTransport.id);
/* $FlowFixMe: there may not be a `meta` object */
for (const [, {path}] of moduleTransport.meta.dependencyPairs || []) {
const dependency = moduleTransportsByPath.get(path);
if (dependency && !seen.has(dependency.id)) {
yield dependency.id;
yield * subtree(dependency, moduleTransportsByPath, seen);
}
}
}
class ArrayMap extends Map {
get(key) {
let array = super.get(key);
if (!array) {
array = [];
this.set(key, array);
}
return array;
}
}
function createGroups(ramGroups: Array<string>, lazyModules) {
// build two maps that allow to lookup module data
// by path or (numeric) module id;
const byPath = new Map();
const byId = new Map();
lazyModules.forEach(m => {
byPath.set(m.sourcePath, m);
byId.set(m.id, m.sourcePath);
});
// build a map of group root IDs to an array of module IDs in the group
const result: Map<number, Set<number>> = new Map(
ramGroups
.map(modulePath => {
const root = byPath.get(modulePath);
if (!root) {
throw Error(`Group root ${modulePath} is not part of the bundle`);
}
return [
root.id,
// `subtree` yields the IDs of all transitive dependencies of a module
/* $FlowFixMe: assumes the module is always in the Map */
new Set(subtree(byPath.get(root.sourcePath), byPath)),
];
})
);
if (ramGroups.length > 1) {
// build a map of all grouped module IDs to an array of group root IDs
const all = new ArrayMap();
for (const [parent, children] of result) {
for (const module of children) {
all.get(module).push(parent);
}
}
// find all module IDs that are part of more than one group
const doubles = filter(all, ([, parents]) => parents.length > 1);
for (const [moduleId, parents] of doubles) {
// remove them from their groups
/* $FlowFixMe: this assumes the element exists. */
parents.forEach(p => result.get(p).delete(moduleId));
// print a warning for each removed module
const parentNames = parents.map(byId.get, byId);
const lastName = parentNames.pop();
console.warn(
/* $FlowFixMe: this assumes the element exists. */
`Module ${byId.get(moduleId)} belongs to groups ${
parentNames.join(', ')}, and ${lastName
}. Removing it from all groups.`
);
}
}
return result;
}
const isRawMappings = Array.isArray;
module.exports = Bundle;

View File

@ -0,0 +1,114 @@
/**
* 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 ModuleTransport = require('../lib/ModuleTransport');
export type FinalizeOptions = {
allowUpdates?: boolean,
runBeforeMainModule?: Array<string>,
runMainModule?: boolean,
};
export type GetSourceOptions = {
inlineSourceMap?: boolean,
dev: boolean,
};
class BundleBase {
_assets: Array<mixed>;
_finalized: boolean;
_mainModuleId: number | void;
_modules: Array<ModuleTransport>;
_source: ?string;
constructor() {
this._finalized = false;
this._modules = [];
this._assets = [];
this._mainModuleId = undefined;
}
isEmpty() {
return this._modules.length === 0 && this._assets.length === 0;
}
getMainModuleId() {
return this._mainModuleId;
}
setMainModuleId(moduleId: number) {
this._mainModuleId = moduleId;
}
addModule(module: ModuleTransport) {
if (!(module instanceof ModuleTransport)) {
throw new Error('Expected a ModuleTransport object');
}
return this._modules.push(module) - 1;
}
replaceModuleAt(index: number, module: ModuleTransport) {
if (!(module instanceof ModuleTransport)) {
throw new Error('Expeceted a ModuleTransport object');
}
this._modules[index] = module;
}
getModules() {
return this._modules;
}
getAssets() {
return this._assets;
}
addAsset(asset: mixed) {
this._assets.push(asset);
}
finalize(options: FinalizeOptions) {
if (!options.allowUpdates) {
Object.freeze(this._modules);
Object.freeze(this._assets);
}
this._finalized = true;
}
getSource(options: GetSourceOptions) {
this.assertFinalized();
if (this._source) {
return this._source;
}
this._source = this._modules.map((module) => module.code).join('\n');
return this._source;
}
invalidateSource() {
this._source = null;
}
assertFinalized(message?: string) {
if (!this._finalized) {
throw new Error(message || 'Bundle needs to be finalized before getting any source');
}
}
setRamGroups(ramGroups: Array<string>) {}
}
module.exports = BundleBase;

View File

@ -0,0 +1,55 @@
/**
* 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.
*/
'use strict';
const BundleBase = require('./BundleBase');
const ModuleTransport = require('../lib/ModuleTransport');
class HMRBundle extends BundleBase {
constructor({sourceURLFn, sourceMappingURLFn}) {
super();
this._sourceURLFn = sourceURLFn
this._sourceMappingURLFn = sourceMappingURLFn;
this._sourceURLs = [];
this._sourceMappingURLs = [];
}
addModule(resolver, response, module, moduleTransport) {
const code = resolver.resolveRequires(
response,
module,
moduleTransport.code,
moduleTransport.meta.dependencyOffsets,
);
super.addModule(new ModuleTransport({...moduleTransport, code}));
this._sourceMappingURLs.push(this._sourceMappingURLFn(moduleTransport.sourcePath));
this._sourceURLs.push(this._sourceURLFn(moduleTransport.sourcePath));
return Promise.resolve();
}
getModulesIdsAndCode() {
return this._modules.map(module => {
return {
id: JSON.stringify(module.id),
code: module.code,
};
});
}
getSourceURLs() {
return this._sourceURLs;
}
getSourceMappingURLs() {
return this._sourceMappingURLs;
}
}
module.exports = HMRBundle;

View File

@ -0,0 +1,427 @@
/**
* 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.
*/
'use strict';
jest.disableAutomock();
const Bundle = require('../Bundle');
const ModuleTransport = require('../../lib/ModuleTransport');
const SourceMapGenerator = require('source-map').SourceMapGenerator;
const crypto = require('crypto');
describe('Bundle', () => {
var bundle;
beforeEach(() => {
bundle = new Bundle({sourceMapUrl: 'test_url'});
bundle.getSourceMap = jest.fn(() => {
return 'test-source-map';
});
});
describe('source bundle', () => {
it('should create a bundle and get the source', () => {
return Promise.resolve().then(() => {
return addModule({
bundle,
code: 'transformed foo;',
sourceCode: 'source foo',
sourcePath: 'foo path',
});
}).then(() => {
return addModule({
bundle,
code: 'transformed bar;',
sourceCode: 'source bar',
sourcePath: 'bar path',
});
}).then(() => {
bundle.finalize({});
expect(bundle.getSource({dev: true})).toBe([
'transformed foo;',
'transformed bar;',
'\/\/# sourceMappingURL=test_url'
].join('\n'));
});
});
it('should be ok to leave out the source map url', () => {
const otherBundle = new Bundle();
return Promise.resolve().then(() => {
return addModule({
bundle: otherBundle,
code: 'transformed foo;',
sourceCode: 'source foo',
sourcePath: 'foo path',
});
}).then(() => {
return addModule({
bundle: otherBundle,
code: 'transformed bar;',
sourceCode: 'source bar',
sourcePath: 'bar path',
});
}).then(() => {
otherBundle.finalize({});
expect(otherBundle.getSource({dev: true})).toBe([
'transformed foo;',
'transformed bar;',
].join('\n'));
});
});
it('should create a bundle and add run module code', () => {
return Promise.resolve().then(() => {
return addModule({
bundle,
code: 'transformed foo;',
sourceCode: 'source foo',
sourcePath: 'foo path',
});
}).then(() => {
return addModule({
bundle,
code: 'transformed bar;',
sourceCode: 'source bar',
sourcePath: 'bar path',
});
}).then(() => {
bundle.setMainModuleId('foo');
bundle.finalize({
runBeforeMainModule: ['bar'],
runMainModule: true,
});
expect(bundle.getSource({dev: true})).toBe([
'transformed foo;',
'transformed bar;',
';require("bar");',
';require("foo");',
'\/\/# sourceMappingURL=test_url',
].join('\n'));
});
});
it('should insert modules in a deterministic order, independent from timing of the wrapping process', () => {
const moduleTransports = [
createModuleTransport({name: 'module1'}),
createModuleTransport({name: 'module2'}),
createModuleTransport({name: 'module3'}),
];
const resolves = {};
const resolver = {
wrapModule({name}) {
return new Promise(resolve => resolves[name] = resolve);
}
};
const promise = Promise.all(
moduleTransports.map(m => bundle.addModule(resolver, null, {isPolyfill: () => false}, m)))
.then(() => {
expect(bundle.getModules())
.toEqual(moduleTransports);
});
resolves.module2({code: ''});
resolves.module3({code: ''});
resolves.module1({code: ''});
return promise;
});
});
describe('sourcemap bundle', () => {
it('should create sourcemap', () => {
//TODO: #15357872 add a meaningful test here
});
it('should combine sourcemaps', () => {
const otherBundle = new Bundle({sourceMapUrl: 'test_url'});
return Promise.resolve().then(() => {
return addModule({
bundle: otherBundle,
code: 'transformed foo;\n',
sourceCode: 'source foo',
map: {name: 'sourcemap foo'},
sourcePath: 'foo path',
});
}).then(() => {
return addModule({
bundle: otherBundle,
code: 'transformed bar;\n',
sourceCode: 'source bar',
map: {name: 'sourcemap bar'},
sourcePath: 'bar path',
});
}).then(() => {
return addModule({
bundle: otherBundle,
code: 'image module;\nimage module;',
virtual: true,
sourceCode: 'image module;\nimage module;',
sourcePath: 'image.png',
});
}).then(() => {
otherBundle.setMainModuleId('foo');
otherBundle.finalize({
runBeforeMainModule: ['InitializeCore'],
runMainModule: true,
});
const sourceMap = otherBundle.getSourceMap({dev: true});
expect(sourceMap).toEqual({
file: 'test_url',
version: 3,
sections: [
{ offset: { line: 0, column: 0 }, map: { name: 'sourcemap foo' } },
{ offset: { line: 2, column: 0 }, map: { name: 'sourcemap bar' } },
{
offset: {
column: 0,
line: 4
},
map: {
file: 'image.png',
mappings: 'AAAA;AACA;',
names: [],
sources: [ 'image.png' ],
sourcesContent: ['image module;\nimage module;'],
version: 3,
}
},
{
offset: {
column: 0,
line: 6
},
map: {
file: 'require-InitializeCore.js',
mappings: 'AAAA;',
names: [],
sources: [ 'require-InitializeCore.js' ],
sourcesContent: [';require("InitializeCore");'],
version: 3,
}
},
{
offset: {
column: 0,
line: 7
},
map: {
file: 'require-foo.js',
mappings: 'AAAA;',
names: [],
sources: [ 'require-foo.js' ],
sourcesContent: [';require("foo");'],
version: 3,
}
},
],
});
});
});
});
describe('getAssets()', () => {
it('should save and return asset objects', () => {
var p = new Bundle({sourceMapUrl: 'test_url'});
var asset1 = {};
var asset2 = {};
p.addAsset(asset1);
p.addAsset(asset2);
p.finalize();
expect(p.getAssets()).toEqual([asset1, asset2]);
});
});
describe('getJSModulePaths()', () => {
it('should return module paths', () => {
var otherBundle = new Bundle({sourceMapUrl: 'test_url'});
return Promise.resolve().then(() => {
return addModule({
bundle: otherBundle,
code: 'transformed foo;\n',
sourceCode: 'source foo',
sourcePath: 'foo path',
});
}).then(() => {
return addModule({
bundle: otherBundle,
code: 'image module;\nimage module;',
virtual: true,
sourceCode: 'image module;\nimage module;',
sourcePath: 'image.png',
});
}).then(() => {
expect(otherBundle.getJSModulePaths()).toEqual(['foo path']);
});
});
});
describe('getEtag()', function() {
it('should return an etag', function() {
var bundle = new Bundle({sourceMapUrl: 'test_url'});
bundle.finalize({});
var eTag = crypto.createHash('md5').update(bundle.getSource()).digest('hex');
expect(bundle.getEtag()).toEqual(eTag);
});
});
describe('main module id:', function() {
it('can save a main module ID', function() {
const id = 'arbitrary module ID';
bundle.setMainModuleId(id);
expect(bundle.getMainModuleId()).toEqual(id);
});
});
describe('random access bundle groups:', () => {
let moduleTransports;
beforeEach(() => {
moduleTransports = [
transport('Product1', ['React', 'Relay']),
transport('React', ['ReactFoo', 'ReactBar']),
transport('ReactFoo', ['invariant']),
transport('invariant', []),
transport('ReactBar', ['cx']),
transport('cx', []),
transport('OtherFramework', ['OtherFrameworkFoo', 'OtherFrameworkBar']),
transport('OtherFrameworkFoo', ['invariant']),
transport('OtherFrameworkBar', ['crc32']),
transport('crc32', ['OtherFrameworkBar']),
];
});
it('can create a single group', () => {
bundle = createBundle([fsLocation('React')]);
const {groups} = bundle.getUnbundle();
expect(groups).toEqual(new Map([
[idFor('React'), new Set(['ReactFoo', 'invariant', 'ReactBar', 'cx'].map(idFor))],
]));
});
it('can create two groups', () => {
bundle = createBundle([fsLocation('ReactFoo'), fsLocation('ReactBar')]);
const {groups} = bundle.getUnbundle();
expect(groups).toEqual(new Map([
[idFor('ReactFoo'), new Set([idFor('invariant')])],
[idFor('ReactBar'), new Set([idFor('cx')])],
]));
});
it('can handle circular dependencies', () => {
bundle = createBundle([fsLocation('OtherFramework')]);
const {groups} = bundle.getUnbundle();
expect(groups).toEqual(new Map([[
idFor('OtherFramework'),
new Set(['OtherFrameworkFoo', 'invariant', 'OtherFrameworkBar', 'crc32'].map(idFor)),
]]));
});
it('omits modules that are contained by more than one group', () => {
bundle = createBundle([fsLocation('React'), fsLocation('OtherFramework')]);
const {groups} = bundle.getUnbundle();
expect(groups).toEqual(new Map([
[idFor('React'),
new Set(['ReactFoo', 'ReactBar', 'cx'].map(idFor))],
[idFor('OtherFramework'),
new Set(['OtherFrameworkFoo', 'OtherFrameworkBar', 'crc32'].map(idFor))],
]));
});
it('ignores missing dependencies', () => {
bundle = createBundle([fsLocation('Product1')]);
const {groups} = bundle.getUnbundle();
expect(groups).toEqual(new Map([[
idFor('Product1'),
new Set(['React', 'ReactFoo', 'invariant', 'ReactBar', 'cx'].map(idFor))
]]));
});
it('throws for group roots that do not exist', () => {
bundle = createBundle([fsLocation('DoesNotExist')]);
expect(() => {
const {groups} = bundle.getUnbundle(); //eslint-disable-line no-unused-vars
}).toThrow(new Error(`Group root ${fsLocation('DoesNotExist')} is not part of the bundle`));
});
function idFor(name) {
const {map} = idFor;
if (!map) {
idFor.map = new Map([[name, 0]]);
idFor.next = 1;
return 0;
}
if (map.has(name)) {
return map.get(name);
}
const id = idFor.next++;
map.set(name, id);
return id;
}
function createBundle(ramGroups, options = {}) {
const b = new Bundle(Object.assign(options, {ramGroups}));
moduleTransports.forEach(t => addModule({bundle: b, ...t}));
b.finalize();
return b;
}
function fsLocation(name) {
return `/fs/${name}.js`;
}
function module(name) {
return {path: fsLocation(name)};
}
function transport(name, deps) {
return createModuleTransport({
name,
id: idFor(name),
sourcePath: fsLocation(name),
meta: {dependencyPairs: deps.map(d => [d, module(d)])},
});
}
});
});
function resolverFor(code, map) {
return {
wrapModule: () => Promise.resolve({code, map}),
};
}
function addModule({bundle, code, sourceCode, sourcePath, map, virtual, polyfill, meta, id = ''}) {
return bundle.addModule(
resolverFor(code, map),
null,
{isPolyfill: () => polyfill},
createModuleTransport({
code,
sourceCode,
sourcePath,
id,
map,
meta,
virtual,
polyfill,
}),
);
}
function createModuleTransport(data) {
return new ModuleTransport({
code: '',
sourceCode: '',
sourcePath: '',
id: 'id' in data ? data.id : '',
...data,
});
}

View File

@ -0,0 +1,351 @@
/**
* 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.
*/
'use strict';
jest.disableAutomock();
jest
.setMock('worker-farm', () => () => undefined)
.setMock('uglify-js')
.mock('image-size')
.mock('fs')
.mock('assert')
.mock('progress')
.mock('../../node-haste')
.mock('../../JSTransformer')
.mock('../../lib/declareOpts')
.mock('../../Resolver')
.mock('../Bundle')
.mock('../HMRBundle')
.mock('../../Logger')
.mock('../../lib/declareOpts');
var Bundler = require('../');
var Resolver = require('../../Resolver');
var defaults = require('../../../../defaults');
var sizeOf = require('image-size');
var fs = require('fs');
var commonOptions = {
allowBundleUpdates: false,
assetExts: defaults.assetExts,
cacheVersion: 'smth',
extraNodeModules: {},
platforms: defaults.platforms,
resetCache: false,
watch: false,
};
describe('Bundler', function() {
function createModule({
path,
id,
dependencies,
isAsset,
isJSON,
isPolyfill,
resolution,
}) {
return {
path,
resolution,
getDependencies: () => Promise.resolve(dependencies),
getName: () => Promise.resolve(id),
isJSON: () => isJSON,
isAsset: () => isAsset,
isPolyfill: () => isPolyfill,
read: () => ({
code: 'arbitrary',
source: 'arbitrary',
}),
};
}
var getDependencies;
var getModuleSystemDependencies;
var bundler;
var assetServer;
var modules;
var projectRoots;
beforeEach(function() {
getDependencies = jest.fn();
getModuleSystemDependencies = jest.fn();
projectRoots = ['/root'];
Resolver.mockImplementation(function() {
return {
getDependencies: getDependencies,
getModuleSystemDependencies: getModuleSystemDependencies,
};
});
fs.statSync.mockImplementation(function() {
return {
isDirectory: () => true
};
});
fs.readFile.mockImplementation(function(file, callback) {
callback(null, '{"json":true}');
});
assetServer = {
getAssetData: jest.fn(),
};
bundler = new Bundler({
...commonOptions,
projectRoots,
assetServer: assetServer,
});
modules = [
createModule({id: 'foo', path: '/root/foo.js', dependencies: []}),
createModule({id: 'bar', path: '/root/bar.js', dependencies: []}),
createModule({
id: 'new_image.png',
path: '/root/img/new_image.png',
isAsset: true,
resolution: 2,
dependencies: []
}),
createModule({
id: 'package/file.json',
path: '/root/file.json',
isJSON: true,
dependencies: [],
}),
];
getDependencies.mockImplementation((main, options, transformOptions) =>
Promise.resolve({
mainModuleId: 'foo',
dependencies: modules,
transformOptions,
getModuleId: () => 123,
getResolvedDependencyPairs: () => [],
})
);
getModuleSystemDependencies.mockImplementation(function() {
return [];
});
sizeOf.mockImplementation(function(path, cb) {
cb(null, { width: 50, height: 100 });
});
});
it('create a bundle', function() {
assetServer.getAssetData.mockImplementation(() => {
return Promise.resolve({
scales: [1,2,3],
files: [
'/root/img/img.png',
'/root/img/img@2x.png',
'/root/img/img@3x.png',
],
hash: 'i am a hash',
name: 'img',
type: 'png',
});
});
return bundler.bundle({
entryFile: '/root/foo.js',
runBeforeMainModule: [],
runModule: true,
sourceMapUrl: 'source_map_url',
}).then(bundle => {
const ithAddedModule = (i) => bundle.addModule.mock.calls[i][2].path;
expect(ithAddedModule(0)).toEqual('/root/foo.js');
expect(ithAddedModule(1)).toEqual('/root/bar.js');
expect(ithAddedModule(2)).toEqual('/root/img/new_image.png');
expect(ithAddedModule(3)).toEqual('/root/file.json');
expect(bundle.finalize.mock.calls[0]).toEqual([{
runMainModule: true,
runBeforeMainModule: [],
allowUpdates: false,
}]);
expect(bundle.addAsset.mock.calls[0]).toEqual([{
__packager_asset: true,
fileSystemLocation: '/root/img',
httpServerLocation: '/assets/img',
width: 50,
height: 100,
scales: [1, 2, 3],
files: [
'/root/img/img.png',
'/root/img/img@2x.png',
'/root/img/img@3x.png',
],
hash: 'i am a hash',
name: 'img',
type: 'png',
}]);
// TODO(amasad) This fails with 0 != 5 in OSS
//expect(ProgressBar.prototype.tick.mock.calls.length).toEqual(modules.length);
});
});
it('loads and runs asset plugins', function() {
jest.mock('mockPlugin1', () => {
return asset => {
asset.extraReverseHash = asset.hash.split('').reverse().join('');
return asset;
};
}, {virtual: true});
jest.mock('asyncMockPlugin2', () => {
return asset => {
expect(asset.extraReverseHash).toBeDefined();
return new Promise((resolve) => {
asset.extraPixelCount = asset.width * asset.height;
resolve(asset);
});
};
}, {virtual: true});
const mockAsset = {
scales: [1,2,3],
files: [
'/root/img/img.png',
'/root/img/img@2x.png',
'/root/img/img@3x.png',
],
hash: 'i am a hash',
name: 'img',
type: 'png',
};
assetServer.getAssetData.mockImplementation(() => Promise.resolve(mockAsset));
return bundler.bundle({
entryFile: '/root/foo.js',
runBeforeMainModule: [],
runModule: true,
sourceMapUrl: 'source_map_url',
assetPlugins: ['mockPlugin1', 'asyncMockPlugin2'],
}).then(bundle => {
expect(bundle.addAsset.mock.calls[0]).toEqual([{
__packager_asset: true,
fileSystemLocation: '/root/img',
httpServerLocation: '/assets/img',
width: 50,
height: 100,
scales: [1, 2, 3],
files: [
'/root/img/img.png',
'/root/img/img@2x.png',
'/root/img/img@3x.png',
],
hash: 'i am a hash',
name: 'img',
type: 'png',
extraReverseHash: 'hsah a ma i',
extraPixelCount: 5000,
}]);
});
});
it('gets the list of dependencies from the resolver', function() {
const entryFile = '/root/foo.js';
return bundler.getDependencies({entryFile, recursive: true}).then(() =>
// jest calledWith does not support jasmine.any
expect(getDependencies.mock.calls[0].slice(0, -2)).toEqual([
'/root/foo.js',
{ dev: true, recursive: true },
{ minify: false,
dev: true,
transform: {
dev: true,
hot: false,
generateSourceMaps: false,
projectRoots,
}
},
])
);
});
it('allows overriding the platforms array', () => {
expect(bundler._opts.platforms).toEqual(['ios', 'android', 'windows', 'web']);
const b = new Bundler({
...commonOptions,
projectRoots,
assetServer: assetServer,
platforms: ['android', 'vr'],
});
expect(b._opts.platforms).toEqual(['android', 'vr']);
});
describe('getOrderedDependencyPaths', () => {
beforeEach(() => {
assetServer.getAssetData.mockImplementation(function(relPath) {
if (relPath === 'img/new_image.png') {
return Promise.resolve({
scales: [1,2,3],
files: [
'/root/img/new_image.png',
'/root/img/new_image@2x.png',
'/root/img/new_image@3x.png',
],
hash: 'i am a hash',
name: 'img',
type: 'png',
});
} else if (relPath === 'img/new_image2.png') {
return Promise.resolve({
scales: [1,2,3],
files: [
'/root/img/new_image2.png',
'/root/img/new_image2@2x.png',
'/root/img/new_image2@3x.png',
],
hash: 'i am a hash',
name: 'img',
type: 'png',
});
}
throw new Error('unknown image ' + relPath);
});
});
it('should get the concrete list of all dependency files', () => {
modules.push(
createModule({
id: 'new_image2.png',
path: '/root/img/new_image2.png',
isAsset: true,
resolution: 2,
dependencies: []
}),
);
return bundler.getOrderedDependencyPaths('/root/foo.js', true)
.then((paths) => expect(paths).toEqual([
'/root/foo.js',
'/root/bar.js',
'/root/img/new_image.png',
'/root/img/new_image@2x.png',
'/root/img/new_image@3x.png',
'/root/file.json',
'/root/img/new_image2.png',
'/root/img/new_image2@2x.png',
'/root/img/new_image2@3x.png',
]));
});
});
});

View File

@ -0,0 +1,772 @@
/**
* 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 assert = require('assert');
const crypto = require('crypto');
const debug = require('debug')('RNP:Bundler');
const fs = require('fs');
const Cache = require('../node-haste').Cache;
const Transformer = require('../JSTransformer');
const Resolver = require('../Resolver');
const Bundle = require('./Bundle');
const HMRBundle = require('./HMRBundle');
const ModuleTransport = require('../lib/ModuleTransport');
const imageSize = require('image-size');
const path = require('path');
const version = require('../../../package.json').version;
const denodeify = require('denodeify');
const {
sep: pathSeparator,
join: joinPath,
relative: relativePath,
dirname: pathDirname,
extname,
} = require('path');
import type AssetServer from '../AssetServer';
import type Module from '../node-haste/Module';
import type ResolutionResponse from '../node-haste/DependencyGraph/ResolutionResponse';
import type {Options as JSTransformerOptions, TransformOptions} from '../JSTransformer/worker/worker';
import type {Reporter} from '../lib/reporting';
import type GlobalTransformCache from '../lib/GlobalTransformCache';
export type GetTransformOptions = (
mainModuleName: string,
options: {},
getDependencies: string => Promise<Array<string>>,
) => {} | Promise<{}>;
const sizeOf = denodeify(imageSize);
const noop = () => {};
const {
createActionStartEntry,
createActionEndEntry,
log,
} = require('../Logger');
const assetPropertyBlacklist = new Set([
'files',
'fileSystemLocation',
'path',
]);
type Options = {
allowBundleUpdates: boolean,
assetExts: Array<string>,
assetServer: AssetServer,
blacklistRE?: RegExp,
cacheVersion: string,
extraNodeModules: {},
getTransformOptions?: GetTransformOptions,
globalTransformCache: ?GlobalTransformCache,
moduleFormat: string,
platforms: Array<string>,
polyfillModuleNames: Array<string>,
projectRoots: Array<string>,
providesModuleNodeModules?: Array<string>,
reporter: Reporter,
resetCache: boolean,
transformModulePath?: string,
transformTimeoutInterval: ?number,
watch: boolean,
};
class Bundler {
_opts: Options;
_getModuleId: (opts: Module) => number;
_cache: Cache;
_transformer: Transformer;
_resolver: Resolver;
_projectRoots: Array<string>;
_assetServer: AssetServer;
_getTransformOptions: void | GetTransformOptions;
constructor(opts: Options) {
this._opts = opts;
opts.projectRoots.forEach(verifyRootExists);
let transformModuleHash;
try {
/* $FlowFixMe: if transformModulePath is null it'll just be caught */
const transformModuleStr = fs.readFileSync(opts.transformModulePath);
transformModuleHash =
crypto.createHash('sha1').update(transformModuleStr).digest('hex');
} catch (error) {
transformModuleHash = '';
}
const stableProjectRoots = opts.projectRoots.map(p => {
return path.relative(path.join(__dirname, '../../../..'), p);
});
const cacheKeyParts = [
'react-packager-cache',
version,
opts.cacheVersion,
stableProjectRoots.join(',').split(pathSeparator).join('-'),
transformModuleHash,
];
this._getModuleId = createModuleIdFactory();
if (opts.transformModulePath) {
/* $FlowFixMe: dynamic requires prevent static typing :'( */
const transformer = require(opts.transformModulePath);
if (typeof transformer.cacheKey !== 'undefined') {
cacheKeyParts.push(transformer.cacheKey);
}
}
const transformCacheKey = crypto.createHash('sha1').update(
cacheKeyParts.join('$'),
).digest('hex');
debug(`Using transform cache key "${transformCacheKey}"`);
this._cache = new Cache({
resetCache: opts.resetCache,
cacheKey: transformCacheKey,
});
this._transformer = new Transformer({
transformModulePath: opts.transformModulePath,
});
this._resolver = new Resolver({
assetExts: opts.assetExts,
blacklistRE: opts.blacklistRE,
cache: this._cache,
extraNodeModules: opts.extraNodeModules,
globalTransformCache: opts.globalTransformCache,
minifyCode: this._transformer.minify,
moduleFormat: opts.moduleFormat,
platforms: opts.platforms,
polyfillModuleNames: opts.polyfillModuleNames,
projectRoots: opts.projectRoots,
providesModuleNodeModules: opts.providesModuleNodeModules,
reporter: opts.reporter,
resetCache: opts.resetCache,
transformCacheKey,
transformCode:
(module, code, transformCodeOptions) => this._transformer.transformFile(
module.path,
code,
transformCodeOptions,
),
watch: opts.watch,
});
this._projectRoots = opts.projectRoots;
this._assetServer = opts.assetServer;
this._getTransformOptions = opts.getTransformOptions;
}
end() {
this._transformer.kill();
return Promise.all([
this._cache.end(),
this.getResolver().getDependencyGraph().getWatcher().end(),
]);
}
bundle(options: {
dev: boolean,
minify: boolean,
unbundle: boolean,
sourceMapUrl: string,
}) {
const {dev, minify, unbundle} = options;
const moduleSystemDeps =
this._resolver.getModuleSystemDependencies({dev, unbundle});
return this._bundle({
...options,
bundle: new Bundle({dev, minify, sourceMapUrl: options.sourceMapUrl}),
moduleSystemDeps,
});
}
_sourceHMRURL(platform, hmrpath) {
return this._hmrURL(
'',
platform,
'bundle',
hmrpath,
);
}
_sourceMappingHMRURL(platform, hmrpath) {
// Chrome expects `sourceURL` when eval'ing code
return this._hmrURL(
'\/\/# sourceURL=',
platform,
'map',
hmrpath,
);
}
_hmrURL(prefix, platform, extensionOverride, filePath) {
const matchingRoot = this._projectRoots.find(root => filePath.startsWith(root));
if (!matchingRoot) {
throw new Error('No matching project root for ', filePath);
}
// Replaces '\' with '/' for Windows paths.
if (pathSeparator === '\\') {
filePath = filePath.replace(/\\/g, '/');
}
const extensionStart = filePath.lastIndexOf('.');
const resource = filePath.substring(
matchingRoot.length,
extensionStart !== -1 ? extensionStart : undefined,
);
return (
prefix + resource +
'.' + extensionOverride + '?' +
'platform=' + platform + '&runModule=false&entryModuleOnly=true&hot=true'
);
}
hmrBundle(options: {platform: ?string}, host: string, port: number) {
return this._bundle({
...options,
bundle: new HMRBundle({
sourceURLFn: this._sourceHMRURL.bind(this, options.platform),
sourceMappingURLFn: this._sourceMappingHMRURL.bind(
this,
options.platform,
),
}),
hot: true,
dev: true,
});
}
_bundle({
bundle,
entryFile,
runModule: runMainModule,
runBeforeMainModule,
dev,
minify,
platform,
moduleSystemDeps = [],
hot,
unbundle,
entryModuleOnly,
resolutionResponse,
isolateModuleIDs,
generateSourceMaps,
assetPlugins,
onProgress,
}) {
const onResolutionResponse = (response: ResolutionResponse) => {
/* $FlowFixMe: looks like ResolutionResponse is monkey-patched
* with `getModuleId`. */
bundle.setMainModuleId(response.getModuleId(getMainModule(response)));
if (entryModuleOnly) {
response.dependencies = response.dependencies.filter(module =>
module.path.endsWith(entryFile)
);
} else {
response.dependencies = moduleSystemDeps.concat(response.dependencies);
}
};
const finalizeBundle = ({bundle: finalBundle, transformedModules, response, modulesByName}: {
bundle: Bundle,
transformedModules: Array<{module: Module, transformed: ModuleTransport}>,
response: ResolutionResponse,
modulesByName: {[name: string]: Module},
}) =>
Promise.all(
transformedModules.map(({module, transformed}) =>
finalBundle.addModule(this._resolver, response, module, transformed)
)
).then(() => {
const runBeforeMainModuleIds = Array.isArray(runBeforeMainModule)
? runBeforeMainModule
.map(name => modulesByName[name])
.filter(Boolean)
.map(response.getModuleId)
: undefined;
finalBundle.finalize({
runMainModule,
runBeforeMainModule: runBeforeMainModuleIds,
allowUpdates: this._opts.allowBundleUpdates,
});
return finalBundle;
});
return this._buildBundle({
entryFile,
dev,
minify,
platform,
bundle,
hot,
unbundle,
resolutionResponse,
onResolutionResponse,
finalizeBundle,
isolateModuleIDs,
generateSourceMaps,
assetPlugins,
onProgress,
});
}
_buildBundle({
entryFile,
dev,
minify,
platform,
bundle,
hot,
unbundle,
resolutionResponse,
isolateModuleIDs,
generateSourceMaps,
assetPlugins,
onResolutionResponse = noop,
onModuleTransformed = noop,
finalizeBundle = noop,
onProgress = noop,
}: *) {
const transformingFilesLogEntry =
log(createActionStartEntry({
action_name: 'Transforming files',
entry_point: entryFile,
environment: dev ? 'dev' : 'prod',
}));
const modulesByName = Object.create(null);
if (!resolutionResponse) {
resolutionResponse = this.getDependencies({
entryFile,
dev,
platform,
hot,
onProgress,
minify,
isolateModuleIDs,
generateSourceMaps: unbundle || minify || generateSourceMaps,
});
}
return Promise.resolve(resolutionResponse).then(response => {
bundle.setRamGroups(response.transformOptions.transform.ramGroups);
log(createActionEndEntry(transformingFilesLogEntry));
onResolutionResponse(response);
// get entry file complete path (`entryFile` is relative to roots)
let entryFilePath;
if (response.dependencies.length > 1) { // skip HMR requests
const numModuleSystemDependencies =
this._resolver.getModuleSystemDependencies({dev, unbundle}).length;
const dependencyIndex =
(response.numPrependedDependencies || 0) + numModuleSystemDependencies;
if (dependencyIndex in response.dependencies) {
entryFilePath = response.dependencies[dependencyIndex].path;
}
}
const toModuleTransport = module =>
this._toModuleTransport({
module,
bundle,
entryFilePath,
assetPlugins,
transformOptions: response.transformOptions,
/* $FlowFixMe: `getModuleId` is monkey-patched */
getModuleId: (response.getModuleId: () => number),
dependencyPairs: response.getResolvedDependencyPairs(module),
}).then(transformed => {
modulesByName[transformed.name] = module;
onModuleTransformed({
module,
response,
bundle,
transformed,
});
return {module, transformed};
});
return Promise.all(response.dependencies.map(toModuleTransport))
.then(transformedModules =>
Promise.resolve(
finalizeBundle({bundle, transformedModules, response, modulesByName})
).then(() => bundle)
);
});
}
invalidateFile(filePath: string) {
this._cache.invalidate(filePath);
}
getShallowDependencies({
entryFile,
platform,
dev = true,
minify = !dev,
hot = false,
generateSourceMaps = false,
}: {
entryFile: string,
platform: string,
dev?: boolean,
minify?: boolean,
hot?: boolean,
generateSourceMaps?: boolean,
}) {
return this.getTransformOptions(
entryFile,
{
dev,
platform,
hot,
generateSourceMaps,
projectRoots: this._projectRoots,
},
).then(transformSpecificOptions => {
const transformOptions = {
minify,
dev,
platform,
transform: transformSpecificOptions,
};
return this._resolver.getShallowDependencies(entryFile, transformOptions);
});
}
getModuleForPath(entryFile: string) {
return this._resolver.getModuleForPath(entryFile);
}
getDependencies({
entryFile,
platform,
dev = true,
minify = !dev,
hot = false,
recursive = true,
generateSourceMaps = false,
isolateModuleIDs = false,
onProgress,
}: {
entryFile: string,
platform: string,
dev?: boolean,
minify?: boolean,
hot?: boolean,
recursive?: boolean,
generateSourceMaps?: boolean,
isolateModuleIDs?: boolean,
onProgress?: ?(finishedModules: number, totalModules: number) => mixed,
}) {
return this.getTransformOptions(
entryFile,
{
dev,
platform,
hot,
generateSourceMaps,
projectRoots: this._projectRoots,
},
).then(transformSpecificOptions => {
const transformOptions = {
minify,
dev,
platform,
transform: transformSpecificOptions,
};
return this._resolver.getDependencies(
entryFile,
{dev, platform, recursive},
transformOptions,
onProgress,
isolateModuleIDs ? createModuleIdFactory() : this._getModuleId,
);
});
}
getOrderedDependencyPaths({ entryFile, dev, platform }: {
entryFile: string,
dev: boolean,
platform: string,
}) {
return this.getDependencies({entryFile, dev, platform}).then(
({ dependencies }) => {
const ret = [];
const promises = [];
const placeHolder = {};
dependencies.forEach(dep => {
if (dep.isAsset()) {
const relPath = getPathRelativeToRoot(
this._projectRoots,
dep.path
);
promises.push(
this._assetServer.getAssetData(relPath, platform)
);
ret.push(placeHolder);
} else {
ret.push(dep.path);
}
});
return Promise.all(promises).then(assetsData => {
assetsData.forEach(({ files }) => {
const index = ret.indexOf(placeHolder);
ret.splice(index, 1, ...files);
});
return ret;
});
}
);
}
_toModuleTransport({
module,
bundle,
entryFilePath,
transformOptions,
getModuleId,
dependencyPairs,
assetPlugins,
}: {
module: Module,
bundle: Bundle,
entryFilePath: string,
transformOptions: JSTransformerOptions,
getModuleId: () => number,
dependencyPairs: Array<[mixed, {path: string}]>,
assetPlugins: Array<string>,
}): Promise<ModuleTransport> {
let moduleTransport;
const moduleId = getModuleId(module);
if (module.isAsset()) {
moduleTransport = this._generateAssetModule(
bundle, module, moduleId, assetPlugins, transformOptions.platform);
}
if (moduleTransport) {
return Promise.resolve(moduleTransport);
}
return Promise.all([
module.getName(),
module.read(transformOptions),
]).then((
[name, {code, dependencies, dependencyOffsets, map, source}]
) => {
const {preloadedModules} = transformOptions.transform;
const preloaded =
module.path === entryFilePath ||
module.isPolyfill() ||
preloadedModules && preloadedModules.hasOwnProperty(module.path);
return new ModuleTransport({
name,
id: moduleId,
code,
map,
meta: {dependencies, dependencyOffsets, preloaded, dependencyPairs},
sourceCode: source,
sourcePath: module.path
});
});
}
_generateAssetObjAndCode(module, assetPlugins, platform: ?string = null) {
const relPath = getPathRelativeToRoot(this._projectRoots, module.path);
var assetUrlPath = joinPath('/assets', pathDirname(relPath));
// On Windows, change backslashes to slashes to get proper URL path from file path.
if (pathSeparator === '\\') {
assetUrlPath = assetUrlPath.replace(/\\/g, '/');
}
// Test extension against all types supported by image-size module.
// If it's not one of these, we won't treat it as an image.
const isImage = [
'png', 'jpg', 'jpeg', 'bmp', 'gif', 'webp', 'psd', 'svg', 'tiff'
].indexOf(extname(module.path).slice(1)) !== -1;
return this._assetServer.getAssetData(relPath, platform).then((assetData) => {
return Promise.all([isImage ? sizeOf(assetData.files[0]) : null, assetData]);
}).then((res) => {
const dimensions = res[0];
const assetData = res[1];
const scale = assetData.scales[0];
const asset = {
__packager_asset: true,
fileSystemLocation: pathDirname(module.path),
httpServerLocation: assetUrlPath,
width: dimensions ? dimensions.width / scale : undefined,
height: dimensions ? dimensions.height / scale : undefined,
scales: assetData.scales,
files: assetData.files,
hash: assetData.hash,
name: assetData.name,
type: assetData.type,
};
return this._applyAssetPlugins(assetPlugins, asset);
}).then((asset) => {
const json = JSON.stringify(filterObject(asset, assetPropertyBlacklist));
const assetRegistryPath = 'react-native/Libraries/Image/AssetRegistry';
const code =
`module.exports = require(${JSON.stringify(assetRegistryPath)}).registerAsset(${json});`;
const dependencies = [assetRegistryPath];
const dependencyOffsets = [code.indexOf(assetRegistryPath) - 1];
return {
asset,
code,
meta: {dependencies, dependencyOffsets}
};
});
}
_applyAssetPlugins(assetPlugins, asset) {
if (!assetPlugins.length) {
return asset;
}
const [currentAssetPlugin, ...remainingAssetPlugins] = assetPlugins;
/* $FlowFixMe: dynamic requires prevent static typing :'( */
const assetPluginFunction = require(currentAssetPlugin);
const result = assetPluginFunction(asset);
// If the plugin was an async function, wait for it to fulfill before
// applying the remaining plugins
if (typeof result.then === 'function') {
return result.then(resultAsset =>
this._applyAssetPlugins(remainingAssetPlugins, resultAsset)
);
} else {
return this._applyAssetPlugins(remainingAssetPlugins, result);
}
}
_generateAssetModule(
bundle: Bundle,
module: Module,
moduleId: number,
assetPlugins: Array<string> = [],
platform: ?string = null,
) {
return Promise.all([
module.getName(),
this._generateAssetObjAndCode(module, assetPlugins, platform),
]).then(([name, {asset, code, meta}]) => {
bundle.addAsset(asset);
return new ModuleTransport({
name,
id: moduleId,
code,
meta: meta,
sourceCode: code,
sourcePath: module.path,
virtual: true,
});
});
}
getTransformOptions(
mainModuleName: string,
options: {
dev?: boolean,
generateSourceMaps?: boolean,
hot?: boolean,
platform: string,
projectRoots: Array<string>,
},
): Promise<TransformOptions> {
const getDependencies = (entryFile: string) =>
this.getDependencies({...options, entryFile})
.then(r => r.dependencies.map(d => d.path));
const extraOptions = this._getTransformOptions
? this._getTransformOptions(mainModuleName, options, getDependencies)
: null;
return Promise.resolve(extraOptions)
.then(extraOpts => {
return {...options, ...extraOpts};
});
}
getResolver() {
return this._resolver;
}
}
function getPathRelativeToRoot(roots, absPath) {
for (let i = 0; i < roots.length; i++) {
const relPath = relativePath(roots[i], absPath);
if (relPath[0] !== '.') {
return relPath;
}
}
throw new Error(
'Expected root module to be relative to one of the project roots'
);
}
function verifyRootExists(root) {
// Verify that the root exists.
assert(fs.statSync(root).isDirectory(), 'Root has to be a valid directory');
}
function createModuleIdFactory() {
const fileToIdMap = Object.create(null);
let nextId = 0;
return ({path: modulePath}) => {
if (!(modulePath in fileToIdMap)) {
fileToIdMap[modulePath] = nextId;
nextId += 1;
}
return fileToIdMap[modulePath];
};
}
function getMainModule({dependencies, numPrependedDependencies = 0}) {
return dependencies[numPrependedDependencies];
}
function filterObject(object, blacklist) {
const copied = Object.assign({}, object);
for (const key of blacklist) {
delete copied[key];
}
return copied;
}
module.exports = Bundler;

View File

@ -0,0 +1,108 @@
/**
* 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 encode = require('./encode');
const MAX_SEGMENT_LENGTH = 7;
const ONE_MEG = 1024 * 1024;
const COMMA = 0x2c;
const SEMICOLON = 0x3b;
/**
* Efficient builder for base64 VLQ mappings strings.
*
* This class uses a buffer that is preallocated with one megabyte and is
* reallocated dynamically as needed, doubling its size.
*
* Encoding never creates any complex value types (strings, objects), and only
* writes character values to the buffer.
*
* For details about source map terminology and specification, check
* https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit
*/
class B64Builder {
buffer: Buffer;
pos: number;
hasSegment: boolean;
constructor() {
this.buffer = new Buffer(ONE_MEG);
this.pos = 0;
this.hasSegment = false;
}
/**
* Adds `n` markers for generated lines to the mappings.
*/
markLines(n: number) {
if (n < 1) {
return this;
}
this.hasSegment = false;
if (this.pos + n >= this.buffer.length) {
this._realloc();
}
while (n--) {
this.buffer[this.pos++] = SEMICOLON;
}
return this;
}
/**
* Starts a segment at the specified column offset in the current line.
*/
startSegment(column: number) {
if (this.hasSegment) {
this._writeByte(COMMA);
} else {
this.hasSegment = true;
}
this.append(column);
return this;
}
/**
* Appends a single number to the mappings.
*/
append(value: number) {
if (this.pos + MAX_SEGMENT_LENGTH >= this.buffer.length) {
this._realloc();
}
this.pos = encode(value, this.buffer, this.pos);
return this;
}
/**
* Returns the string representation of the mappings.
*/
toString() {
return this.buffer.toString('ascii', 0, this.pos);
}
_writeByte(byte) {
if (this.pos === this.buffer.length) {
this._realloc();
}
this.buffer[this.pos++] = byte;
}
_realloc() {
const {buffer} = this;
this.buffer = new Buffer(buffer.length * 2);
buffer.copy(this.buffer);
}
}
module.exports = B64Builder;

View File

@ -0,0 +1,195 @@
/**
* 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 B64Builder = require('./B64Builder');
import type {SourceMap} from 'babel-core';
/**
* Generates a source map from raw mappings.
*
* Raw mappings are a set of 2, 4, or five elements:
*
* - line and column number in the generated source
* - line and column number in the original source
* - symbol name in the original source
*
* Mappings have to be passed in the order appearance in the generated source.
*/
class Generator {
builder: B64Builder;
last: {|
generatedColumn: number,
generatedLine: number,
name: number,
source: number,
sourceColumn: number,
sourceLine: number,
|};
names: IndexedSet;
source: number;
sources: Array<string>;
sourcesContent: Array<string>;
constructor() {
this.builder = new B64Builder();
this.last = {
generatedColumn: 0,
generatedLine: 1, // lines are passed in 1-indexed
name: 0,
source: 0,
sourceColumn: 0,
sourceLine: 1,
};
this.names = new IndexedSet();
this.source = -1;
this.sources = [];
this.sourcesContent = [];
}
/**
* Mark the beginning of a new source file.
*/
startFile(file: string, code: string) {
this.source = this.sources.push(file) - 1;
this.sourcesContent.push(code);
}
/**
* Mark the end of the current source file
*/
endFile() {
this.source = -1;
}
/**
* Adds a mapping for generated code without a corresponding source location.
*/
addSimpleMapping(generatedLine: number, generatedColumn: number): void {
const last = this.last;
if (this.source === -1 ||
generatedLine === last.generatedLine &&
generatedColumn < last.generatedColumn ||
generatedLine < last.generatedLine) {
const msg = this.source === -1
? 'Cannot add mapping before starting a file with `addFile()`'
: 'Mapping is for a position preceding an earlier mapping';
throw new Error(msg);
}
if (generatedLine > last.generatedLine) {
this.builder.markLines(generatedLine - last.generatedLine);
last.generatedLine = generatedLine;
last.generatedColumn = 0;
}
this.builder.startSegment(generatedColumn - last.generatedColumn);
last.generatedColumn = generatedColumn;
}
/**
* Adds a mapping for generated code with a corresponding source location.
*/
addSourceMapping(
generatedLine: number,
generatedColumn: number,
sourceLine: number,
sourceColumn: number,
): void {
this.addSimpleMapping(generatedLine, generatedColumn);
const last = this.last;
this.builder
.append(this.source - last.source)
.append(sourceLine - last.sourceLine)
.append(sourceColumn - last.sourceColumn);
last.source = this.source;
last.sourceColumn = sourceColumn;
last.sourceLine = sourceLine;
}
/**
* Adds a mapping for code with a corresponding source location + symbol name.
*/
addNamedSourceMapping(
generatedLine: number,
generatedColumn: number,
sourceLine: number,
sourceColumn: number,
name: string,
): void {
this.addSourceMapping(
generatedLine, generatedColumn, sourceLine, sourceColumn);
const last = this.last;
const nameIndex = this.names.indexFor(name);
this.builder.append(nameIndex - last.name);
last.name = nameIndex;
}
/**
* Return the source map as object.
*/
toMap(file?: string): SourceMap {
return {
version: 3,
file,
sources: this.sources.slice(),
sourcesContent: this.sourcesContent.slice(),
names: this.names.items(),
mappings: this.builder.toString(),
};
}
/**
* Return the source map as string.
*
* This is ~2.5x faster than calling `JSON.stringify(generator.toMap())`
*/
toString(file?: string): string {
return ('{' +
'"version":3,' +
(file ? `"file":${JSON.stringify(file)},` : '') +
`"sources":${JSON.stringify(this.sources)},` +
`"sourcesContent":${JSON.stringify(this.sourcesContent)},` +
`"names":${JSON.stringify(this.names.items())},` +
`"mappings":"${this.builder.toString()}"` +
'}');
}
}
class IndexedSet {
map: Map<string, number>;
nextIndex: number;
constructor() {
this.map = new Map();
this.nextIndex = 0;
}
indexFor(x: string) {
let index = this.map.get(x);
if (index == null) {
index = this.nextIndex++;
this.map.set(x, index);
}
return index;
}
items() {
return Array.from(this.map.keys());
}
}
module.exports = Generator;

View File

@ -0,0 +1,126 @@
/**
* Copyright (c) 2017-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.
*/
'use strict';
jest.disableAutomock();
const B64Builder = require('../B64Builder');
let builder;
beforeEach(() => {
builder = new B64Builder();
});
it('exposes a fluent interface', () => {
expect(builder.markLines(0)).toBe(builder);
expect(builder.markLines(3)).toBe(builder);
expect(builder.startSegment()).toBe(builder);
expect(builder.append(4)).toBe(builder);
});
it('can create an empty string', () => {
expect(builder.toString()).toEqual('');
});
it('can mark a new line in the generated code', () => {
builder.markLines(1);
expect(builder.toString()).toEqual(';');
});
it('can mark multiple new lines in the generated code', () => {
builder.markLines(4);
expect(builder.toString()).toEqual(';;;;');
});
it('can mark zero new lines in the generated code', () => {
builder.markLines(0);
expect(builder.toString()).toEqual('');
});
it('does not add commas when just starting a segment', () => {
builder.startSegment(0);
expect(builder.toString()).toEqual('A');
});
it('adds a comma when starting a segment after another segment', () => {
builder.startSegment(0);
builder.startSegment(1);
expect(builder.toString()).toEqual('A,C');
});
it('does not add a comma when starting a segment after marking a line', () => {
builder.startSegment(0);
builder.markLines(1);
builder.startSegment(0);
expect(builder.toString()).toEqual('A;A');
});
it('adds a comma when starting a segment after calling `markLines(0)`', () => {
builder.startSegment(0);
builder.markLines(0);
builder.startSegment(1);
expect(builder.toString()).toEqual('A,C');
});
it('can append values that fit within 5 bits (including sign bit)', () => {
builder.append(0b1111);
builder.append(-0b1111);
expect(builder.toString()).toEqual('ef');
});
it('can append values that fit within 10 bits (including sign bit)', () => {
builder.append(0b111100110);
builder.append(-0b110110011);
expect(builder.toString()).toEqual('senb');
});
it('can append values that fit within 15 bits (including sign bit)', () => {
builder.append(0b10011111011001);
builder.append(-0b11001010001001);
expect(builder.toString()).toEqual('y9TzoZ');
});
it('can append values that fit within 20 bits (including sign bit)', () => {
builder.append(0b1110010011101110110);
builder.append(-0b1011000010100100110);
expect(builder.toString()).toEqual('s3zctyiW');
});
it('can append values that fit within 25 bits (including sign bit)', () => {
builder.append(0b100010001111011010110111);
builder.append(-0b100100111100001110101111);
expect(builder.toString()).toEqual('ur7jR/6hvS');
});
it('can append values that fit within 30 bits (including sign bit)', () => {
builder.append(0b10001100100001101010001011111);
builder.append(-0b11111000011000111110011111101);
expect(builder.toString()).toEqual('+lqjyR7v+xhf');
});
it('can append values that fit within 32 bits (including sign bit)', () => {
builder.append(0b1001100101000101001011111110011);
builder.append(-0b1101101101011000110011001110000);
expect(builder.toString()).toEqual('m/rq0sChnzx1tD');
});
it('can handle multiple operations', () => {
builder
.markLines(3)
.startSegment(4)
.append(2)
.append(2)
.append(0)
.append(2345)
.startSegment(12)
.append(987543)
.markLines(1)
.startSegment(0);
expect(builder.toString()).toEqual(';;;IEEAyyE,Yu5o8B;A');
});

View File

@ -0,0 +1,113 @@
/**
* Copyright (c) 2017-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.
*/
'use strict';
jest.disableAutomock();
const Generator = require('../Generator');
const {objectContaining} = expect;
let generator;
beforeEach(() => {
generator = new Generator();
});
it('adds file name and source code when starting a file', () => {
const file1 = 'just/a/file';
const file2 = 'another/file';
const source1 = 'var a = 1;';
const source2 = 'var a = 2;';
generator.startFile(file1, source1);
generator.startFile(file2, source2);
expect(generator.toMap())
.toEqual(objectContaining({
sources: [file1, file2],
sourcesContent: [source1, source2],
}));
});
it('throws when adding a mapping without starting a file', () => {
expect(() => generator.addSimpleMapping(1, 2)).toThrow();
});
it('throws when adding a mapping after ending a file', () => {
generator.startFile('apples', 'pears');
generator.endFile();
expect(() => generator.addSimpleMapping(1, 2)).toThrow();
});
it('can add a mapping for generated code without corresponding original source', () => {
generator.startFile('apples', 'pears');
generator.addSimpleMapping(12, 87);
expect(generator.toMap())
.toEqual(objectContaining({
mappings: ';;;;;;;;;;;uF',
}));
});
it('can add a mapping with corresponding location in the original source', () => {
generator.startFile('apples', 'pears');
generator.addSourceMapping(2, 3, 456, 7);
expect(generator.toMap())
.toEqual(objectContaining({
mappings: ';GAucO',
}));
});
it('can add a mapping with source location and symbol name', () => {
generator.startFile('apples', 'pears');
generator.addNamedSourceMapping(9, 876, 54, 3, 'arbitrary');
expect(generator.toMap())
.toEqual(objectContaining({
mappings: ';;;;;;;;42BAqDGA',
names: ['arbitrary'],
}));
});
describe('full map generation', () => {
beforeEach(() => {
generator.startFile('apples', 'pears');
generator.addSimpleMapping(1, 2);
generator.addNamedSourceMapping(3, 4, 5, 6, 'plums');
generator.endFile();
generator.startFile('lemons', 'oranges');
generator.addNamedSourceMapping(7, 8, 9, 10, 'tangerines');
generator.addNamedSourceMapping(11, 12, 13, 14, 'tangerines');
generator.addSimpleMapping(15, 16);
});
it('can add multiple mappings for each file', () => {
expect(generator.toMap()).toEqual({
version: 3,
mappings: 'E;;IAIMA;;;;QCIIC;;;;YAIIA;;;;gB',
sources: ['apples', 'lemons'],
sourcesContent: ['pears', 'oranges'],
names: ['plums', 'tangerines'],
});
});
it('can add a `file` property to the map', () => {
expect(generator.toMap('arbitrary'))
.toEqual(objectContaining({
file: 'arbitrary',
}));
});
it('supports direct JSON serialization', () => {
expect(JSON.parse(generator.toString())).toEqual(generator.toMap());
});
it('supports direct JSON serialization with a file name', () => {
const file = 'arbitrary/file';
expect(JSON.parse(generator.toString(file))).toEqual(generator.toMap(file));
});
});

View File

@ -0,0 +1,85 @@
/**
* Copyright (c) 2017-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.
*/
'use strict';
jest.disableAutomock();
const Generator = require('../Generator');
const {compactMapping, fromRawMappings} = require('..');
describe('flattening mappings / compacting', () => {
it('flattens simple mappings', () => {
expect(compactMapping({generated: {line: 12, column: 34}}))
.toEqual([12, 34]);
});
it('flattens mappings with a source location', () => {
expect(compactMapping({
generated: {column: 34, line: 12},
original: {column: 78, line: 56},
})).toEqual([12, 34, 56, 78]);
});
it('flattens mappings with a source location and a symbol name', () => {
expect(compactMapping({
generated: {column: 34, line: 12},
name: 'arbitrary',
original: {column: 78, line: 56},
})).toEqual([12, 34, 56, 78, 'arbitrary']);
});
});
describe('build map from raw mappings', () => {
it('returns a `Generator` instance', () => {
expect(fromRawMappings([])).toBeInstanceOf(Generator);
});
it('returns a working source map containing all mappings', () => {
const input = [{
code: lines(11),
map: [
[1, 2],
[3, 4, 5, 6, 'apples'],
[7, 8, 9, 10],
[11, 12, 13, 14, 'pears']
],
sourceCode: 'code1',
sourcePath: 'path1',
}, {
code: lines(3),
map: [
[1, 2],
[3, 4, 15, 16, 'bananas'],
],
sourceCode: 'code2',
sourcePath: 'path2',
}, {
code: lines(23),
map: [
[11, 12],
[13, 14, 15, 16, 'bananas'],
[17, 18, 19, 110],
[21, 112, 113, 114, 'pears']
],
sourceCode: 'code3',
sourcePath: 'path3',
}];
expect(fromRawMappings(input).toMap())
.toEqual({
mappings: 'E;;IAIMA;;;;QAII;;;;YAIIC;E;;ICEEC;;;;;;;;;;;Y;;cCAAA;;;;kBAI8F;;;;gHA8FID',
names: ['apples', 'pears', 'bananas'],
sources: ['path1', 'path2', 'path3'],
sourcesContent: ['code1', 'code2', 'code3'],
version: 3,
});
});
});
const lines = n => Array(n).join('\n');

View File

@ -0,0 +1,125 @@
/**
* 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
*/
/**
* Copyright 2011 Mozilla Foundation and contributors
* Licensed under the New BSD license. See LICENSE or:
* http://opensource.org/licenses/BSD-3-Clause
*
* Based on the Base 64 VLQ implementation in Closure Compiler:
* https://code.google.com/p/closure-compiler/source/browse/trunk/src/com/google/debugging/sourcemap/Base64VLQ.java
*
* Copyright 2011 The Closure Compiler Authors. All rights reserved.
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided
* with the distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* @copyright
*/
/* eslint-disable no-bitwise */
'use strict';
// A map of values to characters for the b64 encoding
const CHAR_MAP = [
0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48,
0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50,
0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58,
0x59, 0x5a, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66,
0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e,
0x6f, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76,
0x77, 0x78, 0x79, 0x7a, 0x30, 0x31, 0x32, 0x33,
0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x2b, 0x2f,
];
// A single base 64 digit can contain 6 bits of data. For the base 64 variable
// length quantities we use in the source map spec, the first bit is the sign,
// the next four bits are the actual value, and the 6th bit is the
// continuation bit. The continuation bit tells us whether there are more
// digits in this value following this digit.
//
// Continuation
// | Sign
// | |
// V V
// 101011
const VLQ_BASE_SHIFT = 5;
// binary: 100000
const VLQ_BASE = 1 << VLQ_BASE_SHIFT;
// binary: 011111
const VLQ_BASE_MASK = VLQ_BASE - 1;
// binary: 100000
const VLQ_CONTINUATION_BIT = VLQ_BASE;
/**
* Converts from a two-complement value to a value where the sign bit is
* placed in the least significant bit. For example, as decimals:
* 1 becomes 2 (10 binary), -1 becomes 3 (11 binary)
* 2 becomes 4 (100 binary), -2 becomes 5 (101 binary)
*/
function toVLQSigned(value) {
return value < 0
? ((-value) << 1) + 1
: (value << 1) + 0;
}
/**
* Encodes a number to base64 VLQ format and appends it to the passed-in buffer
*
* DON'T USE COMPOUND OPERATORS (eg `>>>=`) ON `let`-DECLARED VARIABLES!
* V8 WILL DEOPTIMIZE THIS FUNCTION AND MAP CREATION WILL BE 25% SLOWER!
*
* DON'T ADD MORE COMMENTS TO THIS FUNCTION TO KEEP ITS LENGTH SHORT ENOUGH FOR
* V8 OPTIMIZATION!
*/
function encode(value: number, buffer: Buffer, position: number): number {
let digit, vlq = toVLQSigned(value);
do {
digit = vlq & VLQ_BASE_MASK;
vlq = vlq >>> VLQ_BASE_SHIFT;
if (vlq > 0) {
// There are still more digits in this value, so we must make sure the
// continuation bit is marked.
digit = digit | VLQ_CONTINUATION_BIT;
}
buffer[position++] = CHAR_MAP[digit];
} while (vlq > 0);
return position;
}
module.exports = encode;

View File

@ -0,0 +1 @@
{"main": "source-map.js"}

View File

@ -0,0 +1,104 @@
/**
* 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 Generator = require('./Generator');
import type ModuleTransport from '../../lib/ModuleTransport';
import type {RawMapping as BabelRawMapping} from 'babel-generator';
type GeneratedCodeMapping = [number, number];
type SourceMapping = [number, number, number, number];
type SourceMappingWithName = [number, number, number, number, string];
export type RawMapping =
SourceMappingWithName | SourceMapping | GeneratedCodeMapping;
/**
* Creates a source map from modules with "raw mappings", i.e. an array of
* tuples with either 2, 4, or 5 elements:
* generated line, generated column, source line, source line, symbol name.
*/
function fromRawMappings(modules: Array<ModuleTransport>): Generator {
const generator = new Generator();
let carryOver = 0;
for (var j = 0, o = modules.length; j < o; ++j) {
var module = modules[j];
var {code, map} = module;
if (Array.isArray(map)) {
addMappingsForFile(generator, map, module, carryOver);
} else if (map != null) {
throw new Error(
`Unexpected module with full source map found: ${module.sourcePath}`
);
}
carryOver = carryOver + countLines(code);
}
return generator;
}
function compactMapping(mapping: BabelRawMapping): RawMapping {
const {column, line} = mapping.generated;
const {name, original} = mapping;
if (original == null) {
return [line, column];
}
if (typeof name !== 'string') {
return [line, column, original.line, original.column];
}
return [line, column, original.line, original.column, name];
}
function addMappingsForFile(generator, mappings, module, carryOver) {
generator.startFile(module.sourcePath, module.sourceCode);
const columnOffset = module.code.indexOf('{') + 1;
for (let i = 0, n = mappings.length; i < n; ++i) {
addMapping(generator, mappings[i], carryOver, columnOffset);
}
generator.endFile();
}
function addMapping(generator, mapping, carryOver, columnOffset) {
const n = mapping.length;
const line = mapping[0] + carryOver;
// lines start at 1, columns start at 0
const column = mapping[0] === 1 ? mapping[1] + columnOffset : mapping[1];
if (n === 2) {
generator.addSimpleMapping(line, column);
} else if (n === 4) {
// $FlowIssue #15579526
generator.addSourceMapping(line, column, mapping[2], mapping[3]);
} else if (n === 5) {
generator.addNamedSourceMapping(
// $FlowIssue #15579526
line, column, mapping[2], mapping[3], mapping[4]);
} else {
throw new Error(`Invalid mapping: [${mapping.join(', ')}]`);
}
}
function countLines(string) {
return string.split('\n').length;
}
exports.fromRawMappings = fromRawMappings;
exports.compactMapping = compactMapping;

View File

@ -0,0 +1,33 @@
/**
* 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.
*/
'use strict';
const mockColor = () => {
return {
bold: () => { return { }; },
};
};
mockColor.bold = function() {
return {};
};
module.exports = {
dim: s => s,
magenta: mockColor,
white: mockColor,
blue: mockColor,
yellow: mockColor,
green: mockColor,
bold: mockColor,
red: mockColor,
cyan: mockColor,
gray: mockColor,
black: mockColor,
};

View File

@ -0,0 +1,13 @@
/**
* 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.
*/
'use strict';
// Bug with Jest because we're going to the node_modules that is a sibling
// of what jest thinks our root (the dir with the package.json) should be.
module.exports = require.requireActual('lodash');

View File

@ -0,0 +1,14 @@
/**
* 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.
*/
'use strict';
// Bug with Jest because we're going to the node_modules that is a sibling
// of what jest thinks our root (the dir with the package.json) should be.
module.exports = require.requireActual('q');

View File

@ -0,0 +1,13 @@
/**
* 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.
*/
'use strict';
module.exports = function (data, callback) {
callback(null, {});
};

View File

@ -0,0 +1,90 @@
/**
* 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.
*/
'use strict';
jest
.unmock('imurmurhash')
.unmock('../../lib/ModuleTransport')
.unmock('../');
const fs = {writeFileSync: jest.fn()};
const temp = {path: () => '/arbitrary/path'};
const workerFarm = jest.fn();
jest.setMock('fs', fs);
jest.setMock('temp', temp);
jest.setMock('worker-farm', workerFarm);
var Transformer = require('../');
const {any} = jasmine;
describe('Transformer', function() {
let options, workers, Cache;
const fileName = '/an/arbitrary/file.js';
const transformModulePath = __filename;
beforeEach(function() {
Cache = jest.fn();
Cache.prototype.get = jest.fn((a, b, c) => c());
fs.writeFileSync.mockClear();
options = {transformModulePath};
workerFarm.mockClear();
workerFarm.mockImplementation((opts, path, methods) => {
const api = workers = {};
methods.forEach(method => {api[method] = jest.fn();});
return api;
});
});
it('passes transform module path, file path, source code,' +
' and options to the worker farm when transforming', () => {
const transformOptions = {arbitrary: 'options'};
const code = 'arbitrary(code)';
new Transformer(options).transformFile(fileName, code, transformOptions);
expect(workers.transformAndExtractDependencies).toBeCalledWith(
transformModulePath,
fileName,
code,
transformOptions,
any(Function),
);
});
it('should add file info to parse errors', function() {
const transformer = new Transformer(options);
var message = 'message';
var snippet = 'snippet';
workers.transformAndExtractDependencies.mockImplementation(
function(transformPath, filename, code, opts, callback) {
var babelError = new SyntaxError(message);
babelError.type = 'SyntaxError';
babelError.description = message;
babelError.loc = {
line: 2,
column: 15,
};
babelError.codeFrame = snippet;
callback(babelError);
},
);
return transformer.transformFile(fileName, '', {})
.catch(function(error) {
expect(error.type).toEqual('TransformError');
expect(error.message).toBe('SyntaxError ' + message);
expect(error.lineNumber).toBe(2);
expect(error.column).toBe(15);
expect(error.filename).toBe(fileName);
expect(error.description).toBe(message);
expect(error.snippet).toBe(snippet);
});
});
});

View File

@ -0,0 +1,228 @@
/**
* 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 Logger = require('../Logger');
const declareOpts = require('../lib/declareOpts');
const denodeify = require('denodeify');
const os = require('os');
const util = require('util');
const workerFarm = require('worker-farm');
const debug = require('debug')('RNP:JStransformer');
import type {Data as TransformData, Options as TransformOptions} from './worker/worker';
import type {SourceMap} from '../lib/SourceMap';
// Avoid memory leaks caused in workers. This number seems to be a good enough number
// to avoid any memory leak while not slowing down initial builds.
// TODO(amasad): Once we get bundle splitting, we can drive this down a bit more.
const MAX_CALLS_PER_WORKER = 600;
// Worker will timeout if one of the callers timeout.
const DEFAULT_MAX_CALL_TIME = 301000;
// How may times can we tolerate failures from the worker.
const MAX_RETRIES = 2;
const validateOpts = declareOpts({
transformModulePath: {
type:'string',
required: false,
},
transformTimeoutInterval: {
type: 'number',
default: DEFAULT_MAX_CALL_TIME,
},
worker: {
type: 'string',
},
methods: {
type: 'array',
default: [],
},
});
type Options = {
transformModulePath?: ?string,
transformTimeoutInterval?: ?number,
worker?: ?string,
methods?: ?Array<string>,
};
const maxConcurrentWorkers = ((cores, override) => {
if (override) {
return Math.min(cores, override);
}
if (cores < 3) {
return cores;
}
if (cores < 8) {
return Math.floor(cores * 0.75);
}
if (cores < 24) {
return Math.floor(3 / 8 * cores + 3); // between cores *.75 and cores / 2
}
return cores / 2;
})(os.cpus().length, parseInt(process.env.REACT_NATIVE_MAX_WORKERS, 10));
function makeFarm(worker, methods, timeout) {
return workerFarm(
{
autoStart: true,
maxConcurrentCallsPerWorker: 1,
maxConcurrentWorkers: maxConcurrentWorkers,
maxCallsPerWorker: MAX_CALLS_PER_WORKER,
maxCallTime: timeout,
maxRetries: MAX_RETRIES,
},
worker,
methods,
);
}
class Transformer {
_opts: {
transformModulePath?: ?string,
transformTimeoutInterval: number,
worker: ?string,
methods: Array<string>,
};
_workers: {[name: string]: mixed};
_transformModulePath: ?string;
_transform: (
transform: string,
filename: string,
sourceCode: string,
options: ?TransformOptions,
) => Promise<TransformData>;
minify: (
filename: string,
code: string,
sourceMap: SourceMap,
) => Promise<{code: string, map: SourceMap}>;
constructor(options: Options) {
const opts = this._opts = validateOpts(options);
const {transformModulePath} = opts;
if (opts.worker) {
this._workers =
makeFarm(opts.worker, opts.methods, opts.transformTimeoutInterval);
opts.methods.forEach(name => {
/* $FlowFixMe: assigning the class object fields directly is
* questionable, because it's prone to conflicts. */
this[name] = this._workers[name];
});
}
else if (transformModulePath) {
this._transformModulePath = require.resolve(transformModulePath);
this._workers = makeFarm(
require.resolve('./worker'),
['minify', 'transformAndExtractDependencies'],
opts.transformTimeoutInterval,
);
this._transform = denodeify(this._workers.transformAndExtractDependencies);
this.minify = denodeify(this._workers.minify);
}
}
kill() {
this._workers && workerFarm.end(this._workers);
}
transformFile(fileName: string, code: string, options: TransformOptions) {
if (!this._transform) {
return Promise.reject(new Error('No transform module'));
}
debug('transforming file', fileName);
return this
/* $FlowFixMe: _transformModulePath may be empty, see constructor */
._transform(this._transformModulePath, fileName, code, options)
.then(data => {
Logger.log(data.transformFileStartLogEntry);
Logger.log(data.transformFileEndLogEntry);
debug('done transforming file', fileName);
return data.result;
})
.catch(error => {
if (error.type === 'TimeoutError') {
const timeoutErr = new Error(
`TimeoutError: transforming ${fileName} took longer than ` +
`${this._opts.transformTimeoutInterval / 1000} seconds.\n` +
'You can adjust timeout via the \'transformTimeoutInterval\' option'
);
/* $FlowFixMe: monkey-patch Error */
timeoutErr.type = 'TimeoutError';
throw timeoutErr;
} else if (error.type === 'ProcessTerminatedError') {
const uncaughtError = new Error(
'Uncaught error in the transformer worker: ' +
/* $FlowFixMe: _transformModulePath may be empty, see constructor */
this._opts.transformModulePath
);
/* $FlowFixMe: monkey-patch Error */
uncaughtError.type = 'ProcessTerminatedError';
throw uncaughtError;
}
throw formatError(error, fileName);
});
}
static TransformError;
}
module.exports = Transformer;
Transformer.TransformError = TransformError;
function TransformError() {
Error.captureStackTrace && Error.captureStackTrace(this, TransformError);
}
util.inherits(TransformError, SyntaxError);
function formatError(err, filename, source) {
if (err.loc) {
return formatBabelError(err, filename, source);
} else {
return formatGenericError(err, filename, source);
}
}
function formatGenericError(err, filename) {
var msg = 'TransformError: ' + filename + ': ' + err.message;
var error = new TransformError();
var stack = (err.stack || '').split('\n').slice(0, -1);
stack.push(msg);
error.stack = stack.join('\n');
error.message = msg;
error.type = 'TransformError';
return error;
}
function formatBabelError(err, filename) {
var error = new TransformError();
error.type = 'TransformError';
error.message = (err.type || error.type) + ' ' + err.message;
error.stack = err.stack;
error.snippet = err.codeFrame;
error.lineNumber = err.loc.line;
error.column = err.loc.column;
error.filename = filename;
error.description = err.message;
return error;
}

View File

@ -0,0 +1,122 @@
/**
* 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.
*/
'use strict';
jest.disableAutomock();
const babel = require('babel-core');
const constantFolding = require('../constant-folding');
function parse(code) {
return babel.transform(code, {code: false, babelrc: false, compact: true});
}
const babelOptions = {
babelrc: false,
compact: true,
retainLines: false,
};
function normalize({code}) {
return babel.transform(code, babelOptions).code;
}
describe('constant expressions', () => {
it('can optimize conditional expressions with constant conditions', () => {
const code = `
a(
'production'=="production",
'production'!=='development',
false && 1 || 0 || 2,
true || 3,
'android'==='ios' ? null : {},
'android'==='android' ? {a:1} : {a:0},
'foo'==='bar' ? b : c,
f() ? g() : h()
);`;
expect(normalize(constantFolding('arbitrary.js', parse(code))))
.toEqual('a(true,true,2,true,{},{a:1},c,f()?g():h());');
});
it('can optimize ternary expressions with constant conditions', () => {
const code =
`var a = true ? 1 : 2;
var b = 'android' == 'android'
? ('production' != 'production' ? 'a' : 'A')
: 'i';`;
expect(normalize(constantFolding('arbitrary.js', parse(code))))
.toEqual('var a=1;var b=\'A\';');
});
it('can optimize logical operator expressions with constant conditions', () => {
const code = `
var a = true || 1;
var b = 'android' == 'android' &&
'production' != 'production' || null || "A";`;
expect(normalize(constantFolding('arbitrary.js', parse(code))))
.toEqual('var a=true;var b="A";');
});
it('can optimize logical operators with partly constant operands', () => {
const code = `
var a = "truthy" || z();
var b = "truthy" && z();
var c = null && z();
var d = null || z();
var e = !1 && z();
`;
expect(normalize(constantFolding('arbitrary.js', parse(code))))
.toEqual('var a="truthy";var b=z();var c=null;var d=z();var e=false;');
});
it('can remode an if statement with a falsy constant test', () => {
const code = `
if ('production' === 'development' || false) {
var a = 1;
}
`;
expect(normalize(constantFolding('arbitrary.js', parse(code))))
.toEqual('');
});
it('can optimize if-else-branches with constant conditions', () => {
const code = `
if ('production' == 'development') {
var a = 1;
var b = a + 2;
} else if ('development' == 'development') {
var a = 3;
var b = a + 4;
} else {
var a = 'b';
}
`;
expect(normalize(constantFolding('arbitrary.js', parse(code))))
.toEqual('{var a=3;var b=a+4;}');
});
it('can optimize nested if-else constructs', () => {
const code = `
if ('ios' === "android") {
if (true) {
require('a');
} else {
require('b');
}
} else if ('android' === 'android') {
if (true) {
require('c');
} else {
require('d');
}
}
`;
expect(normalize(constantFolding('arbitrary.js', parse(code))))
.toEqual('{{require(\'c\');}}');
});
});

View File

@ -0,0 +1,112 @@
/**
* 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.
*/
'use strict';
jest.disableAutomock();
const extractDependencies = require('../extract-dependencies');
describe('Dependency extraction:', () => {
it('can extract calls to require', () => {
const code = `require('foo/bar');
var React = require("React");
var A = React.createClass({
render: function() {
return require ( "Component" );
}
});
require
('more');`;
const {dependencies, dependencyOffsets} = extractDependencies(code);
expect(dependencies)
.toEqual(['foo/bar', 'React', 'Component', 'more']);
expect(dependencyOffsets).toEqual([8, 46, 147, 203]);
});
it('does not extract require method calls', () => {
const code = `
require('a');
foo.require('b');
bar.
require ( 'c').require('d');require('e')`;
const {dependencies, dependencyOffsets} = extractDependencies(code);
expect(dependencies).toEqual(['a', 'e']);
expect(dependencyOffsets).toEqual([15, 98]);
});
it('does not extract require calls from strings', () => {
const code = `require('foo');
var React = '\\'require("React")';
var a = ' // require("yadda")';
var a = ' /* require("yadda") */';
var A = React.createClass({
render: function() {
return require ( "Component" );
}
});
" \\" require('more')";`;
const {dependencies, dependencyOffsets} = extractDependencies(code);
expect(dependencies).toEqual(['foo', 'Component']);
expect(dependencyOffsets).toEqual([8, 226]);
});
it('does not extract require calls in comments', () => {
const code = `require('foo')//require("not/this")
/* A comment here with a require('call') that should not be extracted */require('bar')
// ending comment without newline require("baz")`;
const {dependencies, dependencyOffsets} = extractDependencies(code);
expect(dependencies).toEqual(['foo', 'bar']);
expect(dependencyOffsets).toEqual([8, 122]);
});
it('deduplicates dependencies', () => {
const code = `require('foo');require( "foo" );
require("foo");`;
const {dependencies, dependencyOffsets} = extractDependencies(code);
expect(dependencies).toEqual(['foo']);
expect(dependencyOffsets).toEqual([8, 24, 47]);
});
it('does not extract calls to function with names that start with "require"', () => {
const code = 'arbitraryrequire(\'foo\');';
const {dependencies, dependencyOffsets} = extractDependencies(code);
expect(dependencies).toEqual([]);
expect(dependencyOffsets).toEqual([]);
});
it('does not extract calls to require with non-static arguments', () => {
const code = 'require(\'foo/\' + bar)';
const {dependencies, dependencyOffsets} = extractDependencies(code);
expect(dependencies).toEqual([]);
expect(dependencyOffsets).toEqual([]);
});
it('does not get confused by previous states', () => {
// yes, this was a bug
const code = 'require("a");/* a comment */ var a = /[a]/.test(\'a\');';
const {dependencies, dependencyOffsets} = extractDependencies(code);
expect(dependencies).toEqual(['a']);
expect(dependencyOffsets).toEqual([8]);
});
it('can handle regular expressions', () => {
const code = 'require(\'a\'); /["\']/.test(\'foo\'); require("b");';
const {dependencies, dependencyOffsets} = extractDependencies(code);
expect(dependencies).toEqual(['a', 'b']);
expect(dependencyOffsets).toEqual([8, 42]);
});
});

View File

@ -0,0 +1,306 @@
/**
* 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.
*/
'use strict';
jest.disableAutomock();
const inline = require('../inline');
const {transform, transformFromAst} = require('babel-core');
const babelOptions = {
babelrc: false,
compact: true,
};
function toString(ast) {
return normalize(transformFromAst(ast, babelOptions).code);
}
function normalize(code) {
return transform(code, babelOptions).code;
}
function toAst(code) {
return transform(code, {...babelOptions, code: false}).ast;
}
describe('inline constants', () => {
it('replaces __DEV__ in the code', () => {
const code = `function a() {
var a = __DEV__ ? 1 : 2;
var b = a.__DEV__;
var c = function __DEV__(__DEV__) {};
}`;
const {ast} = inline('arbitrary.js', {code}, {dev: true});
expect(toString(ast)).toEqual(normalize(code.replace(/__DEV__/, 'true')));
});
it('replaces Platform.OS in the code if Platform is a global', () => {
const code = `function a() {
var a = Platform.OS;
var b = a.Platform.OS;
}`;
const {ast} = inline('arbitrary.js', {code}, {platform: 'ios'});
expect(toString(ast)).toEqual(normalize(code.replace(/Platform\.OS/, '"ios"')));
});
it('replaces Platform.OS in the code if Platform is a top level import', () => {
const code = `
var Platform = require('Platform');
function a() {
if (Platform.OS === 'android') a = function() {};
var b = a.Platform.OS;
}`;
const {ast} = inline('arbitrary.js', {code}, {platform: 'ios'});
expect(toString(ast)).toEqual(normalize(code.replace(/Platform\.OS/, '"ios"')));
});
it('replaces Platform.OS in the code if Platform is a top level import from react-native', () => {
const code = `
var Platform = require('react-native').Platform;
function a() {
if (Platform.OS === 'android') a = function() {};
var b = a.Platform.OS;
}`;
const {ast} = inline('arbitrary.js', {code}, {platform: 'ios'});
expect(toString(ast)).toEqual(normalize(code.replace(/Platform\.OS/, '"ios"')));
});
it('replaces require("Platform").OS in the code', () => {
const code = `function a() {
var a = require('Platform').OS;
var b = a.require('Platform').OS;
}`;
const {ast} = inline('arbitrary.js', {code}, {platform: 'android'});
expect(toString(ast)).toEqual(
normalize(code.replace(/require\('Platform'\)\.OS/, '"android"')));
});
it('replaces React.Platform.OS in the code if React is a global', () => {
const code = `function a() {
var a = React.Platform.OS;
var b = a.React.Platform.OS;
}`;
const {ast} = inline('arbitrary.js', {code}, {platform: 'ios'});
expect(toString(ast)).toEqual(normalize(code.replace(/React\.Platform\.OS/, '"ios"')));
});
it('replaces ReactNative.Platform.OS in the code if ReactNative is a global', () => {
const code = `function a() {
var a = ReactNative.Platform.OS;
var b = a.ReactNative.Platform.OS;
}`;
const {ast} = inline('arbitrary.js', {code}, {platform: 'ios'});
expect(toString(ast)).toEqual(normalize(code.replace(/ReactNative\.Platform\.OS/, '"ios"')));
});
it('replaces React.Platform.OS in the code if React is a top level import', () => {
const code = `
var React = require('React');
function a() {
if (React.Platform.OS === 'android') a = function() {};
var b = a.React.Platform.OS;
}`;
const {ast} = inline('arbitrary.js', {code}, {platform: 'ios'});
expect(toString(ast)).toEqual(normalize(code.replace(/React.Platform\.OS/, '"ios"')));
});
it('replaces require("React").Platform.OS in the code', () => {
const code = `function a() {
var a = require('React').Platform.OS;
var b = a.require('React').Platform.OS;
}`;
const {ast} = inline('arbitrary.js', {code}, {platform: 'android'});
expect(toString(ast)).toEqual(
normalize(code.replace(/require\('React'\)\.Platform\.OS/, '"android"')));
});
it('replaces ReactNative.Platform.OS in the code if ReactNative is a top level import', () => {
const code = `
var ReactNative = require('react-native');
function a() {
if (ReactNative.Platform.OS === 'android') a = function() {};
var b = a.ReactNative.Platform.OS;
}`;
const {ast} = inline('arbitrary.js', {code}, {platform: 'android'});
expect(toString(ast)).toEqual(normalize(code.replace(/ReactNative.Platform\.OS/, '"android"')));
});
it('replaces require("react-native").Platform.OS in the code', () => {
const code = `function a() {
var a = require('react-native').Platform.OS;
var b = a.require('react-native').Platform.OS;
}`;
const {ast} = inline('arbitrary.js', {code}, {platform: 'android'});
expect(toString(ast)).toEqual(
normalize(code.replace(/require\('react-native'\)\.Platform\.OS/, '"android"')));
});
it('inlines Platform.select in the code if Platform is a global and the argument is an object literal', () => {
const code = `function a() {
var a = Platform.select({ios: 1, android: 2});
var b = a.Platform.select({ios: 1, android: 2});
}`;
const {ast} = inline('arbitrary.js', {code}, {platform: 'ios'});
expect(toString(ast)).toEqual(normalize(code.replace(/Platform\.select[^;]+/, '1')));
});
it('replaces Platform.select in the code if Platform is a top level import', () => {
const code = `
var Platform = require('Platform');
function a() {
Platform.select({ios: 1, android: 2});
var b = a.Platform.select({});
}`;
const {ast} = inline('arbitrary.js', {code}, {platform: 'android'});
expect(toString(ast)).toEqual(normalize(code.replace(/Platform\.select[^;]+/, '2')));
});
it('replaces Platform.select in the code if Platform is a top level import from react-native', () => {
const code = `
var Platform = require('react-native').Platform;
function a() {
Platform.select({ios: 1, android: 2});
var b = a.Platform.select({});
}`;
const {ast} = inline('arbitrary.js', {code}, {platform: 'ios'});
expect(toString(ast)).toEqual(normalize(code.replace(/Platform\.select[^;]+/, '1')));
});
it('replaces require("Platform").select in the code', () => {
const code = `function a() {
var a = require('Platform').select({ios: 1, android: 2});
var b = a.require('Platform').select({});
}`;
const {ast} = inline('arbitrary.js', {code}, {platform: 'android'});
expect(toString(ast)).toEqual(normalize(code.replace(/Platform\.select[^;]+/, '2')));
});
it('replaces React.Platform.select in the code if React is a global', () => {
const code = `function a() {
var a = React.Platform.select({ios: 1, android: 2});
var b = a.React.Platform.select({});
}`;
const {ast} = inline('arbitrary.js', {code}, {platform: 'ios'});
expect(toString(ast)).toEqual(normalize(code.replace(/React\.Platform\.select[^;]+/, '1')));
});
it('replaces ReactNative.Platform.select in the code if ReactNative is a global', () => {
const code = `function a() {
var a = ReactNative.Platform.select({ios: 1, android: 2});
var b = a.ReactNative.Platform.select({});
}`;
const {ast} = inline('arbitrary.js', {code}, {platform: 'ios'});
expect(toString(ast)).toEqual(normalize(code.replace(/ReactNative\.Platform\.select[^;]+/, '1')));
});
it('replaces React.Platform.select in the code if React is a top level import', () => {
const code = `
var React = require('React');
function a() {
var a = React.Platform.select({ios: 1, android: 2});
var b = a.React.Platform.select({});
}`;
const {ast} = inline('arbitrary.js', {code}, {platform: 'ios'});
expect(toString(ast)).toEqual(normalize(code.replace(/React\.Platform\.select[^;]+/, '1')));
});
it('replaces require("React").Platform.select in the code', () => {
const code = `function a() {
var a = require('React').Platform.select({ios: 1, android: 2});
var b = a.require('React').Platform.select({});
}`;
const {ast} = inline('arbitrary.js', {code}, {platform: 'android'});
expect(toString(ast)).toEqual(
normalize(code.replace(/require\('React'\)\.Platform\.select[^;]+/, '2')));
});
it('replaces ReactNative.Platform.select in the code if ReactNative is a top level import', () => {
const code = `
var ReactNative = require('react-native');
function a() {
var a = ReactNative.Plaftform.select({ios: 1, android: 2});
var b = a.ReactNative.Platform.select;
}`;
const {ast} = inline('arbitrary.js', {code}, {platform: 'android'});
expect(toString(ast)).toEqual(normalize(code.replace(/ReactNative.Platform\.select[^;]+/, '2')));
});
it('replaces require("react-native").Platform.select in the code', () => {
const code = `
var a = require('react-native').Platform.select({ios: 1, android: 2});
var b = a.require('react-native').Platform.select({});
`;
const {ast} = inline('arbitrary.js', {code}, {platform: 'android'});
expect(toString(ast)).toEqual(
normalize(code.replace(/require\('react-native'\)\.Platform\.select[^;]+/, '2')));
});
it('replaces non-existing properties with `undefined`', () => {
const code = 'var a = Platform.select({ios: 1, android: 2})';
const {ast} = inline('arbitrary.js', {code}, {platform: 'doesnotexist'});
expect(toString(ast)).toEqual(
normalize(code.replace(/Platform\.select[^;]+/, 'undefined')));
});
it('replaces process.env.NODE_ENV in the code', () => {
const code = `function a() {
if (process.env.NODE_ENV === 'production') {
return require('Prod');
}
return require('Dev');
}`;
const {ast} = inline('arbitrary.js', {code}, {dev: false});
expect(toString(ast)).toEqual(
normalize(code.replace(/process\.env\.NODE_ENV/, '"production"')));
});
it('replaces process.env.NODE_ENV in the code', () => {
const code = `function a() {
if (process.env.NODE_ENV === 'production') {
return require('Prod');
}
return require('Dev');
}`;
const {ast} = inline('arbitrary.js', {code}, {dev: true});
expect(toString(ast)).toEqual(
normalize(code.replace(/process\.env\.NODE_ENV/, '"development"')));
});
it('accepts an AST as input', function() {
const code = 'function ifDev(a,b){return __DEV__?a:b;}';
const {ast} = inline('arbitrary.hs', {ast: toAst(code)}, {dev: false});
expect(toString(ast)).toEqual(code.replace(/__DEV__/, 'false'));
});
it('can work with wrapped modules', () => {
const code = `__arbitrary(function() {
var Platform = require('react-native').Platform;
var a = Platform.OS, b = Platform.select({android: 1, ios: 2});
});`;
const {ast} = inline(
'arbitrary', {code}, {dev: true, platform: 'android', isWrapped: true});
expect(toString(ast)).toEqual(
normalize(
code
.replace(/Platform\.OS/, '"android"')
.replace(/Platform\.select[^)]+\)/, 1)
)
);
});
it('can work with transformed require calls', () => {
const code = `__arbitrary(require, function(arbitraryMapName) {
var a = require(arbitraryMapName[123], 'react-native').Platform.OS;
});`;
const {ast} = inline(
'arbitrary', {code}, {dev: true, platform: 'android', isWrapped: true});
expect(toString(ast)).toEqual(
normalize(code.replace(/require\([^)]+\)\.Platform\.OS/, '"android"')));
});
});

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.
*/
'use strict';
jest.disableAutomock();
const uglify = {
minify: jest.fn(code => {
return {
code: code.replace(/(^|\W)\s+/g, '$1'),
map: {},
};
}),
};
jest.setMock('uglify-js', uglify);
const minify = require('../minify');
const {objectContaining} = jasmine;
describe('Minification:', () => {
const filename = '/arbitrary/file.js';
const code = 'arbitrary(code)';
let map;
beforeEach(() => {
uglify.minify.mockClear();
uglify.minify.mockReturnValue({code: '', map: '{}'});
map = {version: 3, sources: ['?'], mappings: ''};
});
it('passes file name, code, and source map to `uglify`', () => {
minify(filename, code, map);
expect(uglify.minify).toBeCalledWith(code, objectContaining({
fromString: true,
inSourceMap: map,
outSourceMap: true,
}));
});
it('returns the code provided by uglify', () => {
uglify.minify.mockReturnValue({code, map: '{}'});
const result = minify('', '', {});
expect(result.code).toBe(code);
});
it('parses the source map object provided by uglify and sets the sources property', () => {
uglify.minify.mockReturnValue({map: JSON.stringify(map), code: ''});
const result = minify(filename, '', {});
expect(result.map).toEqual({...map, sources: [filename]});
});
});

View File

@ -0,0 +1,234 @@
/**
* 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.
*/
'use strict';
jest.disableAutomock();
jest.mock('../constant-folding');
jest.mock('../extract-dependencies');
jest.mock('../inline');
jest.mock('../minify');
const {any, objectContaining} = jasmine;
describe('code transformation worker:', () => {
let transformCode;
let extractDependencies, transform;
beforeEach(() => {
jest.resetModules();
({transformCode} = require('..'));
extractDependencies =
require('../extract-dependencies').mockReturnValue({});
transform = jest.fn();
});
it('calls the transform with file name, source code, and transform options', function() {
const filename = 'arbitrary/file.js';
const sourceCode = 'arbitrary(code)';
const transformOptions = {arbitrary: 'options'};
transformCode(transform, filename, sourceCode, {transform: transformOptions});
expect(transform).toBeCalledWith(
{filename, sourceCode, options: transformOptions}, any(Function));
});
it('prefixes JSON files with an assignment to module.exports to make the code valid', function() {
const filename = 'arbitrary/file.json';
const sourceCode = '{"arbitrary":"property"}';
transformCode(transform, filename, sourceCode, {});
expect(transform).toBeCalledWith(
{filename, sourceCode: `module.exports=${sourceCode}`}, any(Function));
});
it('calls back with the result of the transform in the cache', done => {
const result = {
code: 'some.other(code)',
map: {}
};
transform.mockImplementation((_, callback) =>
callback(null, result));
transformCode(transform, 'filename', 'code', {}, (error, data) => {
expect(error).toBeNull();
expect(data.result).toEqual(objectContaining(result));
done();
});
});
it(
'removes the leading assignment to `module.exports` before passing ' +
'on the result if the file is a JSON file, even if minified',
done => {
const result = {
code: 'p.exports={a:1,b:2}',
};
transform.mockImplementation((_, callback) => callback(null, result));
const filePath = 'arbitrary/file.json';
transformCode(transform, filePath, 'b', {}, (error, data) => {
expect(error).toBeNull();
expect(data.result.code).toEqual('{a:1,b:2}');
done();
},
);
}
);
it('removes shebang when present', done => {
const shebang = '#!/usr/bin/env node';
const result = {
code: `${shebang} \n arbitrary(code)`,
};
transform.mockImplementation((_, callback) => callback(null, result));
const filePath = 'arbitrary/file.js';
transformCode(transform, filePath, 'b', {}, (error, data) => {
expect(error).toBeNull();
const {code} = data.result;
expect(code).not.toContain(shebang);
expect(code.split('\n').length).toEqual(result.code.split('\n').length);
done();
});
});
it('calls back with any error yielded by the transform', done => {
const error = Error('arbitrary error');
transform.mockImplementation((_, callback) => callback(error));
transformCode(transform, 'filename', 'code', {}, e => {
expect(e).toBe(error);
done();
});
});
describe('dependency extraction:', () => {
let code;
beforeEach(() => {
transform.mockImplementation(
(_, callback) => callback(null, {code}));
});
it('passes the transformed code the `extractDependencies`', done => {
code = 'arbitrary(code)';
transformCode(transform, 'filename', 'code', {}, (error) => {
expect(error).toBeNull();
expect(extractDependencies).toBeCalledWith(code);
done();
});
});
it(
'uses `dependencies` and `dependencyOffsets` ' +
'provided by `extractDependencies` for the result',
done => {
const dependencyData = {
dependencies: ['arbitrary', 'list', 'of', 'dependencies'],
dependencyOffsets: [12, 119, 185, 328, 471],
};
extractDependencies.mockReturnValue(dependencyData);
transformCode(transform, 'filename', 'code', {}, (error, data) => {
expect(error).toBeNull();
expect(data.result).toEqual(objectContaining(dependencyData));
done();
});
}
);
it('does not extract requires if files are marked as "extern"', done => {
const opts = {extern: true};
transformCode(transform, 'filename', 'code', opts, (error, data) => {
expect(error).toBeNull();
const {dependencies, dependencyOffsets} = data.result;
expect(extractDependencies).not.toBeCalled();
expect(dependencies).toEqual([]);
expect(dependencyOffsets).toEqual([]);
done();
});
});
it('does not extract requires of JSON files', done => {
const jsonStr = '{"arbitrary":"json"}';
transformCode(transform, 'arbitrary.json', jsonStr, {}, (error, data) => {
expect(error).toBeNull();
const {dependencies, dependencyOffsets} = data.result;
expect(extractDependencies).not.toBeCalled();
expect(dependencies).toEqual([]);
expect(dependencyOffsets).toEqual([]);
done();
}
);
});
});
describe('Minifications:', () => {
let constantFolding, inline, options;
let transformResult, dependencyData;
const filename = 'arbitrary/file.js';
const foldedCode = 'arbitrary(folded(code));';
const foldedMap = {version: 3, sources: ['fold.js']};
beforeEach(() => {
constantFolding = require('../constant-folding')
.mockReturnValue({code: foldedCode, map: foldedMap});
extractDependencies = require('../extract-dependencies');
inline = require('../inline');
options = {minify: true, transform: {generateSourceMaps: true}};
dependencyData = {
dependencies: ['a', 'b', 'c'],
dependencyOffsets: [100, 120, 140]
};
extractDependencies.mockImplementation(
code => code === foldedCode ? dependencyData : {});
transform.mockImplementation(
(_, callback) => callback(null, transformResult));
});
it('passes the transform result to `inline` for constant inlining', done => {
transformResult = {map: {version: 3}, code: 'arbitrary(code)'};
transformCode(transform, filename, 'code', options, () => {
expect(inline).toBeCalledWith(filename, transformResult, options);
done();
});
});
it('passes the result obtained from `inline` on to `constant-folding`', done => {
const inlineResult = {map: {version: 3, sources: []}, ast: {}};
inline.mockReturnValue(inlineResult);
transformCode(transform, filename, 'code', options, () => {
expect(constantFolding).toBeCalledWith(filename, inlineResult);
done();
});
});
it('Uses the code obtained from `constant-folding` to extract dependencies', done => {
transformCode(transform, filename, 'code', options, () => {
expect(extractDependencies).toBeCalledWith(foldedCode);
done();
});
});
it('uses the dependencies obtained from the optimized result', done => {
transformCode(transform, filename, 'code', options, (_, data) => {
const result = data.result;
expect(result.dependencies).toEqual(dependencyData.dependencies);
done();
});
});
it('uses data produced by `constant-folding` for the result', done => {
transformCode(transform, 'filename', 'code', options, (_, data) => {
expect(data.result)
.toEqual(objectContaining({code: foldedCode, map: foldedMap}));
done();
});
});
});
});

View File

@ -0,0 +1,91 @@
/**
* Copyright (c) 2016-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 babel = require('babel-core');
import type {Ast, SourceMap} from 'babel-core';
const t = babel.types;
const Conditional = {
exit(path) {
const node = path.node;
const test = node.test;
if (t.isLiteral(test)) {
if (test.value || node.alternate) {
path.replaceWith(test.value ? node.consequent : node.alternate);
} else if (!test.value) {
path.remove();
}
}
},
};
const plugin = {
visitor: {
BinaryExpression: {
exit(path) {
const node = path.node;
if (t.isLiteral(node.left) && t.isLiteral(node.right)) {
const result = path.evaluate();
if (result.confident) {
path.replaceWith(t.valueToNode(result.value));
}
}
},
},
ConditionalExpression: Conditional,
IfStatement: Conditional,
LogicalExpression: {
exit(path) {
const node = path.node;
const left = node.left;
if (t.isLiteral(left)) {
const value = t.isNullLiteral(left) ? null : left.value;
if (node.operator === '||') {
path.replaceWith(value ? left : node.right);
} else {
path.replaceWith(value ? node.right : left);
}
}
}
},
UnaryExpression: {
exit(path) {
const node = path.node;
if (node.operator === '!' && t.isLiteral(node.argument)) {
path.replaceWith(t.valueToNode(!node.argument.value));
}
}
},
},
};
function constantFolding(filename: string, transformResult: {
ast: Ast,
code?: ?string,
map: ?SourceMap,
}) {
return babel.transformFromAst(transformResult.ast, transformResult.code, {
filename,
plugins: [plugin],
inputSourceMap: transformResult.map,
sourceMaps: true,
sourceFileName: filename,
babelrc: false,
compact: true,
retainLines: true,
});
}
constantFolding.plugin = plugin;
module.exports = constantFolding;

View File

@ -0,0 +1,47 @@
/**
* Copyright (c) 2016-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 babel = require('babel-core');
const babylon = require('babylon');
/**
* Extracts dependencies (module IDs imported with the `require` function) from
* a string containing code. This walks the full AST for correctness (versus
* using, for example, regular expressions, that would be faster but inexact.)
*
* The result of the dependency extraction is an de-duplicated array of
* dependencies, and an array of offsets to the string literals with module IDs.
* The index points to the opening quote.
*/
function extractDependencies(code: string) {
const ast = babylon.parse(code);
const dependencies = new Set();
const dependencyOffsets = [];
babel.traverse(ast, {
CallExpression(path) {
const node = path.node;
const callee = node.callee;
const arg = node.arguments[0];
if (callee.type !== 'Identifier' || callee.name !== 'require' || !arg || arg.type !== 'StringLiteral') {
return;
}
dependencyOffsets.push(arg.start);
dependencies.add(arg.value);
}
});
return {dependencyOffsets, dependencies: Array.from(dependencies)};
}
module.exports = extractDependencies;

View File

@ -0,0 +1,13 @@
/**
* Copyright (c) 2016-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.
*/
'use strict';
require('../../../../../setupBabel')();
module.exports = require('./worker');

View File

@ -0,0 +1,188 @@
/**
* Copyright (c) 2016-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 babel = require('babel-core');
const invariant = require('fbjs/lib/invariant');
import type {Ast, SourceMap} from 'babel-core';
const t = babel.types;
const React = {name: 'React'};
const ReactNative = {name: 'ReactNative'};
const platform = {name: 'Platform'};
const os = {name: 'OS'};
const select = {name: 'select'};
const requirePattern = {name: 'require'};
const env = {name: 'env'};
const nodeEnv = {name: 'NODE_ENV'};
const processId = {name: 'process'};
const dev = {name: '__DEV__'};
const importMap = new Map([['ReactNative', 'react-native']]);
const isGlobal = (binding) => !binding;
const isToplevelBinding = (binding, isWrappedModule) =>
isGlobal(binding) ||
!binding.scope.parent ||
isWrappedModule && !binding.scope.parent.parent;
const isRequireCall = (node, dependencyId, scope) =>
t.isCallExpression(node) &&
t.isIdentifier(node.callee, requirePattern) &&
checkRequireArgs(node.arguments, dependencyId);
const isImport = (node, scope, patterns) =>
patterns.some(pattern => {
const importName = importMap.get(pattern.name) || pattern.name;
return isRequireCall(node, importName, scope);
});
function isImportOrGlobal(node, scope, patterns, isWrappedModule) {
const identifier = patterns.find(pattern => t.isIdentifier(node, pattern));
return (
identifier &&
isToplevelBinding(scope.getBinding(identifier.name), isWrappedModule) ||
isImport(node, scope, patterns)
);
}
const isPlatformOS = (node, scope, isWrappedModule) =>
t.isIdentifier(node.property, os) &&
isImportOrGlobal(node.object, scope, [platform], isWrappedModule);
const isReactPlatformOS = (node, scope, isWrappedModule) =>
t.isIdentifier(node.property, os) &&
t.isMemberExpression(node.object) &&
t.isIdentifier(node.object.property, platform) &&
isImportOrGlobal(
node.object.object, scope, [React, ReactNative], isWrappedModule);
const isProcessEnvNodeEnv = (node, scope) =>
t.isIdentifier(node.property, nodeEnv) &&
t.isMemberExpression(node.object) &&
t.isIdentifier(node.object.property, env) &&
t.isIdentifier(node.object.object, processId) &&
isGlobal(scope.getBinding(processId.name));
const isPlatformSelect = (node, scope, isWrappedModule) =>
t.isMemberExpression(node.callee) &&
t.isIdentifier(node.callee.object, platform) &&
t.isIdentifier(node.callee.property, select) &&
isImportOrGlobal(node.callee.object, scope, [platform], isWrappedModule);
const isReactPlatformSelect = (node, scope, isWrappedModule) =>
t.isMemberExpression(node.callee) &&
t.isIdentifier(node.callee.property, select) &&
t.isMemberExpression(node.callee.object) &&
t.isIdentifier(node.callee.object.property, platform) &&
isImportOrGlobal(
node.callee.object.object, scope, [React, ReactNative], isWrappedModule);
const isDev = (node, parent, scope) =>
t.isIdentifier(node, dev) &&
isGlobal(scope.getBinding(dev.name)) &&
!(t.isMemberExpression(parent));
function findProperty(objectExpression, key) {
const property = objectExpression.properties.find(p => p.key.name === key);
return property ? property.value : t.identifier('undefined');
}
const inlinePlugin = {
visitor: {
Identifier(path, state) {
if (isDev(path.node, path.parent, path.scope)) {
path.replaceWith(t.booleanLiteral(state.opts.dev));
}
},
MemberExpression(path, state) {
const node = path.node;
const scope = path.scope;
const opts = state.opts;
if (
isPlatformOS(node, scope, opts.isWrapped) ||
isReactPlatformOS(node, scope, opts.isWrapped)
) {
path.replaceWith(t.stringLiteral(opts.platform));
} else if (isProcessEnvNodeEnv(node, scope)) {
path.replaceWith(
t.stringLiteral(opts.dev ? 'development' : 'production'));
}
},
CallExpression(path, state) {
const node = path.node;
const scope = path.scope;
const arg = node.arguments[0];
const opts = state.opts;
if (
isPlatformSelect(node, scope, opts.isWrapped) ||
isReactPlatformSelect(node, scope, opts.isWrapped)
) {
const replacement = t.isObjectExpression(arg)
? findProperty(arg, opts.platform)
: node;
path.replaceWith(replacement);
}
}
},
};
const plugin = () => inlinePlugin;
function checkRequireArgs(args, dependencyId) {
const pattern = t.stringLiteral(dependencyId);
return t.isStringLiteral(args[0], pattern) ||
t.isMemberExpression(args[0]) &&
t.isNumericLiteral(args[0].property) &&
t.isStringLiteral(args[1], pattern);
}
type AstResult = {
ast: Ast,
code: ?string,
map: ?SourceMap,
};
function inline(
filename: string,
transformResult: {ast?: ?Ast, code: string, map: ?SourceMap},
options: {},
): AstResult {
const code = transformResult.code;
const babelOptions = {
filename,
plugins: [[plugin, options]],
inputSourceMap: transformResult.map,
sourceMaps: true,
sourceFileName: filename,
code: false,
babelrc: false,
compact: true,
};
const result = transformResult.ast
? babel.transformFromAst(transformResult.ast, code, babelOptions)
: babel.transform(code, babelOptions);
const {ast} = result;
invariant(ast != null, 'Missing AST in babel transform results.');
return {ast, code: result.code, map: result.map};
}
inline.plugin = inlinePlugin;
module.exports = inline;

View File

@ -0,0 +1,32 @@
/**
* Copyright (c) 2016-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 uglify = require('uglify-js');
function minify(filename: string, code: string, sourceMap: ?string) {
const minifyResult = uglify.minify(code, {
fromString: true,
inSourceMap: sourceMap,
outSourceMap: true,
output: {
ascii_only: true,
screw_ie8: true,
},
});
minifyResult.map = JSON.parse(minifyResult.map);
minifyResult.map.sources = [filename];
return minifyResult;
}
module.exports = minify;

View File

@ -0,0 +1,181 @@
/**
* Copyright (c) 2016-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 constantFolding = require('./constant-folding');
const extractDependencies = require('./extract-dependencies');
const inline = require('./inline');
const invariant = require('fbjs/lib/invariant');
const minify = require('./minify');
import type {LogEntry} from '../../Logger/Types';
import type {Ast, SourceMap, TransformOptions as BabelTransformOptions} from 'babel-core';
function makeTransformParams(filename, sourceCode, options, willMinify) {
invariant(
!willMinify || options.generateSourceMaps,
'Minifying source code requires the `generateSourceMaps` option to be `true`',
);
if (filename.endsWith('.json')) {
sourceCode = 'module.exports=' + sourceCode;
}
return {filename, sourceCode, options};
}
export type TransformedCode = {
code: string,
dependencies: Array<string>,
dependencyOffsets: Array<number>,
map?: ?SourceMap,
};
type Transform = (
params: {
filename: string,
sourceCode: string,
options: ?{},
},
callback: (
error?: Error,
tranformed?: {ast: ?Ast, code: string, map: ?SourceMap},
) => mixed,
) => void;
export type TransformOptions = {
generateSourceMaps: boolean,
platform: string,
preloadedModules?: Array<string>,
projectRoots: Array<string>,
ramGroups?: Array<string>,
} & BabelTransformOptions;
export type Options = {
+dev: boolean,
+minify: boolean,
platform: string,
transform: TransformOptions,
};
export type Data = {
result: TransformedCode,
transformFileStartLogEntry: LogEntry,
transformFileEndLogEntry: LogEntry,
};
type Callback = (
error: ?Error,
data: ?Data,
) => mixed;
function transformCode(
transform: Transform,
filename: string,
sourceCode: string,
options: Options,
callback: Callback,
) {
const params = makeTransformParams(
filename,
sourceCode,
options.transform,
options.minify,
);
const isJson = filename.endsWith('.json');
const transformFileStartLogEntry = {
action_name: 'Transforming file',
action_phase: 'start',
file_name: filename,
log_entry_label: 'Transforming file',
start_timestamp: process.hrtime(),
};
transform(params, (error, transformed) => {
if (error) {
callback(error);
return;
}
invariant(
transformed != null,
'Missing transform results despite having no error.',
);
var code, map;
if (options.minify) {
({code, map} =
constantFolding(filename, inline(filename, transformed, options)));
invariant(code != null, 'Missing code from constant-folding transform.');
} else {
({code, map} = transformed);
}
if (isJson) {
code = code.replace(/^\w+\.exports=/, '');
} else {
// Remove shebang
code = code.replace(/^#!.*/, '');
}
const depsResult = isJson || options.extern
? {dependencies: [], dependencyOffsets: []}
: extractDependencies(code);
const timeDelta = process.hrtime(transformFileStartLogEntry.start_timestamp);
const duration_ms = Math.round((timeDelta[0] * 1e9 + timeDelta[1]) / 1e6);
const transformFileEndLogEntry = {
action_name: 'Transforming file',
action_phase: 'end',
file_name: filename,
duration_ms: duration_ms,
log_entry_label: 'Transforming file',
};
return callback(null, {
result: {...depsResult, code, map},
transformFileStartLogEntry,
transformFileEndLogEntry,
});
});
}
exports.transformAndExtractDependencies = (
transform: string,
filename: string,
sourceCode: string,
options: Options,
callback: Callback,
) => {
/* $FlowFixMe: impossible to type a dynamic require */
const transformModule = require(transform);
transformCode(transformModule, filename, sourceCode, options || {}, callback);
};
exports.minify = (
filename: string,
code: string,
sourceMap: string,
callback: (error: ?Error, result: mixed) => mixed,
) => {
var result;
try {
result = minify(filename, code, sourceMap);
} catch (error) {
callback(error);
}
callback(null, result);
};
exports.transformCode = transformCode; // for easier testing

View File

@ -0,0 +1,33 @@
/**
* 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';
export type ActionLogEntryData = {
action_name: string,
};
export type ActionStartLogEntry = {
action_name?: string,
action_phase?: string,
log_entry_label: string,
log_session?: string,
start_timestamp?: [number, number],
};
export type LogEntry = {
action_name?: string,
action_phase?: string,
duration_ms?: number,
log_entry_label: string,
log_session?: string,
start_timestamp?: [number, number],
};

View File

@ -0,0 +1,37 @@
/**
* 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.
*/
'use strict';
const mockColor = () => {
return {
bold: () => { return { }; },
};
};
mockColor.bold = function() {
return {};
};
mockColor.bgRed = function() {
return {};
};
module.exports = {
dim: s => s,
magenta: mockColor,
white: mockColor,
blue: mockColor,
yellow: mockColor,
green: mockColor,
bold: mockColor,
red: mockColor,
cyan: mockColor,
gray: mockColor,
black: mockColor,
};

View File

@ -0,0 +1,66 @@
/**
* 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.
*
* eslint-disable no-console-disallow
*
*/
'use strict';
jest.disableAutomock();
const {
createEntry,
createActionStartEntry,
createActionEndEntry,
} = require('../');
describe('Logger', () => {
const originalConsoleLog = console.log;
beforeEach(() => {
console.log = jest.fn();
});
afterEach(() => {
console.log = originalConsoleLog;
});
it('creates simple log entries', () => {
const logEntry = createEntry('Test');
expect(logEntry).toEqual({
log_entry_label: 'Test',
log_session: jasmine.any(String),
packager_version: jasmine.any(String),
});
});
it('creates action start log entries', () => {
const actionStartLogEntry = createActionStartEntry('Test');
expect(actionStartLogEntry).toEqual({
action_name: 'Test',
action_phase: 'start',
log_entry_label: 'Test',
log_session: jasmine.any(String),
packager_version: jasmine.any(String),
start_timestamp: jasmine.any(Object),
});
});
it('creates action end log entries', () => {
const actionEndLogEntry = createActionEndEntry(createActionStartEntry('Test'));
expect(actionEndLogEntry).toEqual({
action_name: 'Test',
action_phase: 'end',
duration_ms: jasmine.any(Number),
log_entry_label: 'Test',
log_session: jasmine.any(String),
packager_version: jasmine.any(String),
start_timestamp: jasmine.any(Object),
});
});
});

View File

@ -0,0 +1,89 @@
/**
* 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 os = require('os');
const pkgjson = require('../../../package.json');
const {EventEmitter} = require('events');
import type {
ActionLogEntryData,
ActionStartLogEntry,
LogEntry,
} from './Types';
const log_session = `${os.hostname()}-${Date.now()}`;
const eventEmitter = new EventEmitter();
function on(event: string, handler: (logEntry: LogEntry) => void): void {
eventEmitter.on(event, handler);
}
function createEntry(data: LogEntry | string): LogEntry {
const logEntry = typeof data === 'string' ? {log_entry_label: data} : data;
return {
...logEntry,
log_session,
packager_version: pkgjson.version,
};
}
function createActionStartEntry(data: ActionLogEntryData | string): LogEntry {
const logEntry = typeof data === 'string' ? {action_name: data} : data;
const {action_name} = logEntry;
return createEntry({
...logEntry,
action_name,
action_phase: 'start',
log_entry_label: action_name,
start_timestamp: process.hrtime(),
});
}
function createActionEndEntry(logEntry: ActionStartLogEntry): LogEntry {
const {
action_name,
action_phase,
start_timestamp,
} = logEntry;
if (action_phase !== 'start' || !Array.isArray(start_timestamp)) {
throw new Error('Action has not started or has already ended');
}
const timeDelta = process.hrtime(start_timestamp);
const duration_ms = Math.round((timeDelta[0] * 1e9 + timeDelta[1]) / 1e6);
return createEntry({
...logEntry,
action_name,
action_phase: 'end',
duration_ms,
log_entry_label: action_name,
});
}
function log(logEntry: LogEntry): LogEntry {
eventEmitter.emit('log', logEntry);
return logEntry;
}
module.exports = {
on,
createEntry,
createActionStartEntry,
createActionEndEntry,
log,
};

View File

@ -0,0 +1,173 @@
/**
* Copyright (c) 2016-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 invariant = require('fbjs/lib/invariant');
const memoize = require('async/memoize');
const nullthrows = require('fbjs/lib/nullthrows');
const queue = require('async/queue');
const seq = require('async/seq');
import type {
Callback,
File,
GraphFn,
LoadFn,
ResolveFn,
} from './types.flow';
type Async$Queue<T, C> = {
buffer: number,
concurrency: number,
drain: () => mixed,
empty: () => mixed,
error: (Error, T) => mixed,
idle(): boolean,
kill(): void,
length(): number,
pause(): void,
paused: boolean,
push(T | Array<T>, void | C): void,
resume(): void,
running(): number,
saturated: () => mixed,
started: boolean,
unsaturated: () => mixed,
unshift(T, void | C): void,
workersList(): Array<T>,
};
type LoadQueue =
Async$Queue<{id: string, parent: string}, Callback<File, Array<string>>>;
const createParentModule =
() => ({file: {code: '', type: 'script', path: ''}, dependencies: []});
const noop = () => {};
const NO_OPTIONS = {};
exports.create = function create(resolve: ResolveFn, load: LoadFn): GraphFn {
function Graph(entryPoints, platform, options, callback = noop) {
const {
cwd = '',
log = (console: any),
optimize = false,
skip,
} = options || NO_OPTIONS;
if (typeof platform !== 'string') {
log.error('`Graph`, called without a platform');
callback(Error('The target platform has to be passed'));
return;
}
const loadQueue: LoadQueue = queue(seq(
({id, parent}, cb) => resolve(id, parent, platform, options || NO_OPTIONS, cb),
memoize((file, cb) => load(file, {log, optimize}, cb)),
), Number.MAX_SAFE_INTEGER);
const {collect, loadModule} = createGraphHelpers(loadQueue, cwd, skip);
loadQueue.drain = () => {
loadQueue.kill();
callback(null, collect());
};
loadQueue.error = error => {
loadQueue.error = noop;
loadQueue.kill();
callback(error);
};
let i = 0;
for (const entryPoint of entryPoints) {
loadModule(entryPoint, null, i++);
}
if (i === 0) {
log.error('`Graph` called without any entry points');
loadQueue.kill();
callback(Error('At least one entry point has to be passed.'));
}
}
return Graph;
};
function createGraphHelpers(loadQueue, cwd, skip) {
const modules = new Map([[null, createParentModule()]]);
function collect(
path = null,
serialized = {entryModules: [], modules: []},
seen = new Set(),
) {
const module = modules.get(path);
if (module == null || seen.has(path)) {
return serialized;
}
const {dependencies} = module;
if (path === null) {
serialized.entryModules =
dependencies.map(dep => nullthrows(modules.get(dep.path)));
} else {
serialized.modules.push(module);
seen.add(path);
}
for (const dependency of dependencies) {
collect(dependency.path, serialized, seen);
}
return serialized;
}
function loadModule(id, parent, parentDepIndex) {
loadQueue.push(
{id, parent: parent != null ? parent : cwd},
(error, file, dependencyIDs) =>
onFileLoaded(error, file, dependencyIDs, id, parent, parentDepIndex),
);
}
function onFileLoaded(
error,
file,
dependencyIDs,
id,
parent,
parentDependencyIndex,
) {
if (error) {
return;
}
const {path} = nullthrows(file);
dependencyIDs = nullthrows(dependencyIDs);
const parentModule = modules.get(parent);
invariant(parentModule, 'Invalid parent module: ' + String(parent));
parentModule.dependencies[parentDependencyIndex] = {id, path};
if ((!skip || !skip.has(path)) && !modules.has(path)) {
const module = {
dependencies: Array(dependencyIDs.length),
file: nullthrows(file),
};
modules.set(path, module);
for (let i = 0; i < dependencyIDs.length; ++i) {
loadModule(dependencyIDs[i], path, i);
}
}
}
return {collect, loadModule};
}

View File

@ -0,0 +1,101 @@
/**
* Copyright (c) 2016-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 defaults = require('../../../defaults');
const nullthrows = require('fbjs/lib/nullthrows');
const parallel = require('async/parallel');
const seq = require('async/seq');
const {virtualModule} = require('./output/util');
import type {
Callback,
GraphFn,
GraphResult,
Module,
} from './types.flow';
type BuildFn = (
entryPoints: Iterable<string>,
options: BuildOptions,
callback: Callback<{modules: Iterable<Module>, entryModules: Iterable<Module>}>,
) => void;
type BuildOptions = {|
optimize?: boolean,
platform?: string,
|};
exports.createBuildSetup = (
graph: GraphFn,
translateDefaultsPath: string => string = x => x,
): BuildFn =>
(entryPoints, options, callback) => {
const {
optimize = false,
platform = defaults.platforms[0],
} = options;
const graphOptions = {optimize};
const graphWithOptions =
(entry, cb) => graph(entry, platform, graphOptions, cb);
const graphOnlyModules = seq(graphWithOptions, getModules);
parallel({
graph: cb => graphWithOptions(
concat(defaults.runBeforeMainModule, entryPoints),
cb,
),
moduleSystem: cb => graphOnlyModules(
[translateDefaultsPath(defaults.moduleSystem)],
cb,
),
polyfills: cb => graphOnlyModules(
defaults.polyfills.map(translateDefaultsPath),
cb,
),
}, (
error: ?Error,
result?: {graph: GraphResult, moduleSystem: Array<Module>, polyfills: Array<Module>},
) => {
if (error) {
callback(error);
return;
}
const {
graph: {modules, entryModules},
moduleSystem,
polyfills,
} = nullthrows(result);
callback(null, {
entryModules,
modules: concat([prelude(optimize)], moduleSystem, polyfills, modules),
});
});
};
const getModules = (x, cb) => cb(null, x.modules);
function* concat<T>(...iterables: Array<Iterable<T>>): Iterable<T> {
for (const it of iterables) {
yield* it;
}
}
function prelude(optimize) {
return virtualModule(
`var __DEV__=${String(!optimize)},__BUNDLE_START_TIME__=Date.now();`
);
}

View File

@ -0,0 +1,376 @@
/**
* Copyright (c) 2016-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.
*/
'use strict';
jest
.disableAutomock()
.useRealTimers()
.mock('console');
const {Console} = require('console');
const Graph = require('../Graph');
const {fn} = require('../test-helpers');
const {any, objectContaining} = jasmine;
const quiet = new Console();
describe('Graph:', () => {
const anyEntry = ['arbitrary/entry/point'];
const anyPlatform = 'arbitrary platform';
const noOpts = undefined;
let graph, load, resolve;
beforeEach(() => {
load = fn();
resolve = fn();
resolve.stub.yields(null, 'arbitrary file');
load.stub.yields(null, createFile('arbitrary file'), []);
graph = Graph.create(resolve, load);
});
it('calls back an error when called without any entry point', done => {
graph([], anyPlatform, {log: quiet}, (error) => {
expect(error).toEqual(any(Error));
done();
});
});
it('resolves the entry point with the passed-in `resolve` function', done => {
const entryPoint = '/arbitrary/path';
graph([entryPoint], anyPlatform, noOpts, () => {
expect(resolve).toBeCalledWith(
entryPoint, '', any(String), any(Object), any(Function));
done();
});
});
it('allows to specify multiple entry points', done => {
const entryPoints = ['Arbitrary', '../entry.js'];
graph(entryPoints, anyPlatform, noOpts, () => {
expect(resolve).toBeCalledWith(
entryPoints[0], '', any(String), any(Object), any(Function));
expect(resolve).toBeCalledWith(
entryPoints[1], '', any(String), any(Object), any(Function));
done();
});
});
it('calls back with an error when called without `platform` option', done => {
graph(anyEntry, undefined, {log: quiet}, error => {
expect(error).toEqual(any(Error));
done();
});
});
it('forwards a passed-in `platform` to `resolve`', done => {
const platform = 'any';
graph(anyEntry, platform, noOpts, () => {
expect(resolve).toBeCalledWith(
any(String), '', platform, any(Object), any(Function));
done();
});
});
it('forwards a passed-in `log` option to `resolve`', done => {
const log = new Console();
graph(anyEntry, anyPlatform, {log}, () => {
expect(resolve).toBeCalledWith(
any(String), '', any(String), objectContaining({log}), any(Function));
done();
});
});
it('calls back with every error produced by `resolve`', done => {
const error = Error();
resolve.stub.yields(error);
graph(anyEntry, anyPlatform, noOpts, e => {
expect(e).toBe(error);
done();
});
});
it('only calls back once if two parallel invocations of `resolve` fail', done => {
load.stub.yields(null, createFile('with two deps'), ['depA', 'depB']);
resolve.stub
.withArgs('depA').yieldsAsync(new Error())
.withArgs('depB').yieldsAsync(new Error());
let calls = 0;
function callback() {
if (calls === 0) {
process.nextTick(() => {
expect(calls).toEqual(1);
done();
});
}
++calls;
}
graph(['entryA', 'entryB'], anyPlatform, noOpts, callback);
});
it('passes the files returned by `resolve` on to the `load` function', done => {
const modules = new Map([
['Arbitrary', '/absolute/path/to/Arbitrary.js'],
['../entry.js', '/whereever/is/entry.js'],
]);
for (const [id, file] of modules) {
resolve.stub.withArgs(id).yields(null, file);
}
const [file1, file2] = modules.values();
graph(modules.keys(), anyPlatform, noOpts, () => {
expect(load).toBeCalledWith(file1, any(Object), any(Function));
expect(load).toBeCalledWith(file2, any(Object), any(Function));
done();
});
});
it('passes the `optimize` flag on to `load`', done => {
graph(anyEntry, anyPlatform, {optimize: true}, () => {
expect(load).toBeCalledWith(
any(String), objectContaining({optimize: true}), any(Function));
done();
});
});
it('uses `false` as the default for the `optimize` flag', done => {
graph(anyEntry, anyPlatform, noOpts, () => {
expect(load).toBeCalledWith(
any(String), objectContaining({optimize: false}), any(Function));
done();
});
});
it('forwards a passed-in `log` to `load`', done => {
const log = new Console();
graph(anyEntry, anyPlatform, {log}, () => {
expect(load)
.toBeCalledWith(any(String), objectContaining({log}), any(Function));
done();
});
});
it('calls back with every error produced by `load`', done => {
const error = Error();
load.stub.yields(error);
graph(anyEntry, anyPlatform, noOpts, e => {
expect(e).toBe(error);
done();
});
});
it('resolves any dependencies provided by `load`', done => {
const entryPath = '/path/to/entry.js';
const id1 = 'required/id';
const id2 = './relative/import';
resolve.stub.withArgs('entry').yields(null, entryPath);
load.stub.withArgs(entryPath)
.yields(null, {path: entryPath}, [id1, id2]);
graph(['entry'], anyPlatform, noOpts, () => {
expect(resolve).toBeCalledWith(
id1, entryPath, any(String), any(Object), any(Function));
expect(resolve).toBeCalledWith(
id2, entryPath, any(String), any(Object), any(Function));
done();
});
});
it('loads transitive dependencies', done => {
const entryPath = '/path/to/entry.js';
const id1 = 'required/id';
const id2 = './relative/import';
const path1 = '/path/to/dep/1';
const path2 = '/path/to/dep/2';
resolve.stub
.withArgs(id1).yields(null, path1)
.withArgs(id2).yields(null, path2)
.withArgs('entry').yields(null, entryPath);
load.stub
.withArgs(entryPath).yields(null, {path: entryPath}, [id1])
.withArgs(path1).yields(null, {path: path1}, [id2]);
graph(['entry'], anyPlatform, noOpts, () => {
expect(resolve).toBeCalledWith(id2, path1, any(String), any(Object), any(Function));
expect(load).toBeCalledWith(path1, any(Object), any(Function));
expect(load).toBeCalledWith(path2, any(Object), any(Function));
done();
});
});
it('calls back with an array of modules in depth-first traversal order, regardless of the order of resolution', done => {
load.stub.reset();
resolve.stub.reset();
const ids = [
'a',
'b',
'c', 'd',
'e',
'f', 'g',
'h',
];
ids.forEach(id => {
const path = idToPath(id);
resolve.stub.withArgs(id).yields(null, path);
load.stub.withArgs(path).yields(null, createFile(id), []);
});
load.stub.withArgs(idToPath('a')).yields(null, createFile('a'), ['b', 'e', 'h']);
load.stub.withArgs(idToPath('b')).yields(null, createFile('b'), ['c', 'd']);
load.stub.withArgs(idToPath('e')).yields(null, createFile('e'), ['f', 'g']);
// load certain ids later
['b', 'e', 'h'].forEach(id => resolve.stub.withArgs(id).resetBehavior());
resolve.stub.withArgs('h').func = (a, b, c, d, callback) => {
callback(null, idToPath('h'));
['e', 'b'].forEach(
id => resolve.stub.withArgs(id).yield(null, idToPath(id)));
};
graph(['a'], anyPlatform, noOpts, (error, result) => {
expect(error).toEqual(null);
expect(result.modules).toEqual([
createModule('a', ['b', 'e', 'h']),
createModule('b', ['c', 'd']),
createModule('c'),
createModule('d'),
createModule('e', ['f', 'g']),
createModule('f'),
createModule('g'),
createModule('h'),
]);
done();
});
});
it('calls back with the resolved modules of the entry points', done => {
load.stub.reset();
resolve.stub.reset();
load.stub.withArgs(idToPath('a')).yields(null, createFile('a'), ['b']);
load.stub.withArgs(idToPath('b')).yields(null, createFile('b'), []);
load.stub.withArgs(idToPath('c')).yields(null, createFile('c'), ['d']);
load.stub.withArgs(idToPath('d')).yields(null, createFile('d'), []);
'abcd'.split('')
.forEach(id => resolve.stub.withArgs(id).yields(null, idToPath(id)));
graph(['a', 'c'], anyPlatform, noOpts, (error, result) => {
expect(result.entryModules).toEqual([
createModule('a', ['b']),
createModule('c', ['d']),
]);
done();
});
});
it('calls back with the resolved modules of the entry points if one entry point is a dependency of another', done => {
load.stub.reset();
resolve.stub.reset();
load.stub.withArgs(idToPath('a')).yields(null, createFile('a'), ['b']);
load.stub.withArgs(idToPath('b')).yields(null, createFile('b'), []);
'ab'.split('')
.forEach(id => resolve.stub.withArgs(id).yields(null, idToPath(id)));
graph(['a', 'b'], anyPlatform, noOpts, (error, result) => {
expect(result.entryModules).toEqual([
createModule('a', ['b']),
createModule('b', []),
]);
done();
});
});
it('does not include dependencies more than once', done => {
const ids = ['a', 'b', 'c', 'd'];
ids.forEach(id => {
const path = idToPath(id);
resolve.stub.withArgs(id).yields(null, path);
load.stub.withArgs(path).yields(null, createFile(id), []);
});
['a', 'd'].forEach(id =>
load.stub
.withArgs(idToPath(id)).yields(null, createFile(id), ['b', 'c']));
graph(['a', 'd', 'b'], anyPlatform, noOpts, (error, result) => {
expect(error).toEqual(null);
expect(result.modules).toEqual([
createModule('a', ['b', 'c']),
createModule('b'),
createModule('c'),
createModule('d', ['b', 'c']),
]);
done();
});
});
it('handles dependency cycles', done => {
resolve.stub
.withArgs('a').yields(null, idToPath('a'))
.withArgs('b').yields(null, idToPath('b'))
.withArgs('c').yields(null, idToPath('c'));
load.stub
.withArgs(idToPath('a')).yields(null, createFile('a'), ['b'])
.withArgs(idToPath('b')).yields(null, createFile('b'), ['c'])
.withArgs(idToPath('c')).yields(null, createFile('c'), ['a']);
graph(['a'], anyPlatform, noOpts, (error, result) => {
expect(result.modules).toEqual([
createModule('a', ['b']),
createModule('b', ['c']),
createModule('c', ['a']),
]);
done();
});
});
it('can skip files', done => {
['a', 'b', 'c', 'd', 'e'].forEach(
id => resolve.stub.withArgs(id).yields(null, idToPath(id)));
load.stub
.withArgs(idToPath('a')).yields(null, createFile('a'), ['b', 'c', 'd'])
.withArgs(idToPath('b')).yields(null, createFile('b'), ['e']);
['c', 'd', 'e'].forEach(id =>
load.stub.withArgs(idToPath(id)).yields(null, createFile(id), []));
const skip = new Set([idToPath('b'), idToPath('c')]);
graph(['a'], anyPlatform, {skip}, (error, result) => {
expect(result.modules).toEqual([
createModule('a', ['b', 'c', 'd']),
createModule('d', []),
]);
done();
});
});
});
function createDependency(id) {
return {id, path: idToPath(id)};
}
function createFile(id) {
return {ast: {}, path: idToPath(id)};
}
function createModule(id, dependencies = []): Module {
return {
file: createFile(id),
dependencies: dependencies.map(createDependency)
};
}
function idToPath(id) {
return '/path/to/' + id;
}

View File

@ -0,0 +1,119 @@
/**
* Copyright (c) 2017-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.
*/
'use strict';
jest.disableAutomock();
const ModuleGraph = require('../ModuleGraph');
const defaults = require('../../../../defaults');
const FILE_TYPE = 'module';
describe('build setup', () => {
const buildSetup = ModuleGraph.createBuildSetup(graph);
const noOptions = {};
const noEntryPoints = [];
it('adds a prelude containing start time and `__DEV__` to the build', done => {
buildSetup(noEntryPoints, noOptions, (error, result) => {
expect(error).toEqual(null);
const [prelude] = result.modules;
expect(prelude).toEqual({
dependencies: [],
file: {
code: 'var __DEV__=true,__BUNDLE_START_TIME__=Date.now();',
path: '',
type: 'script',
},
});
done();
});
});
it('sets `__DEV__` to false in the prelude if optimization is enabled', done => {
buildSetup(noEntryPoints, {optimize: true}, (error, result) => {
const [prelude] = result.modules;
expect(prelude.file.code)
.toEqual('var __DEV__=false,__BUNDLE_START_TIME__=Date.now();');
done();
});
});
it('places the module system implementation directly after the prelude', done => {
buildSetup(noEntryPoints, noOptions, (error, result) => {
const [, moduleSystem] = result.modules;
expect(moduleSystem).toEqual({
dependencies: [],
file: {
code: '',
path: defaults.moduleSystem,
type: FILE_TYPE,
},
});
done();
});
});
it('places polyfills after the module system', done => {
buildSetup(noEntryPoints, noOptions, (error, result) => {
const polyfills =
Array.from(result.modules).slice(2, 2 + defaults.polyfills.length);
expect(polyfills).toEqual(defaults.polyfills.map(moduleFromPath));
done();
});
});
it('places all modules from `defaults.runBeforeMainModule` after the polyfills', done => {
buildSetup(noEntryPoints, noOptions, (error, result) => {
const additionalModules =
Array.from(result.modules).slice(-defaults.runBeforeMainModule.length);
expect(additionalModules)
.toEqual(defaults.runBeforeMainModule.map(moduleFromPath));
done();
});
});
it('places all entry points at the end', done => {
const entryPoints = ['a', 'b', 'c'];
buildSetup(entryPoints, noOptions, (error, result) => {
expect(Array.from(result.modules).slice(-3))
.toEqual(entryPoints.map(moduleFromPath));
done();
});
});
it('concatenates `runBeforeMainModule` and entry points as `entryModules`', done => {
const entryPoints = ['a', 'b', 'c'];
buildSetup(entryPoints, noOptions, (error, result) => {
expect(Array.from(result.entryModules)).toEqual(
defaults.runBeforeMainModule.concat(entryPoints).map(moduleFromPath));
done();
});
});
});
function moduleFromPath(path) {
return {
dependencies: [],
file: {
code: '',
path,
type: FILE_TYPE,
},
};
}
function graph(entryPoints, platform, options, callback) {
const modules = Array.from(entryPoints, moduleFromPath);
callback(null, {
entryModules: modules,
modules,
});
}

View File

@ -0,0 +1,80 @@
/**
* 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, join, parse} = require('path');
module.exports = class HasteFS {
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);
}
closest(path: string, fileName: string): ?string {
let {dir, root} = parse(path);
do {
const candidate = join(dir, fileName);
if (this.files.has(candidate)) {
return candidate;
}
dir = dirname(dir);
} while (dir !== '.' && dir !== root);
return null;
}
dirExists(path: string) {
return this.directories.has(path);
}
exists(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(dir);
dir = dirname(dir);
}
});
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,51 @@
/**
* 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';
import type {ModuleCache} from './node-haste.flow';
module.exports = class Module {
hasteID: Promise<?string>;
moduleCache: ModuleCache;
name: Promise<string>;
path: string;
type: 'Module';
constructor(
path: string,
moduleCache: ModuleCache,
info: Promise<TransformedFile>,
) {
this.hasteID = info.then(({hasteID}) => hasteID);
this.moduleCache = moduleCache;
this.name = this.hasteID.then(name => name || getName(path));
this.path = path;
this.type = 'Module';
}
getName() {
return this.name;
}
getPackage() {
return this.moduleCache.getPackageOf(this.path);
}
isHaste() {
return this.hasteID.then(Boolean);
}
};
function getName(path) {
return path.replace(/^.*[\/\\]node_modules[\///]/, '');
}

View File

@ -0,0 +1,65 @@
/**
* 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>;
type GetClosestPackageFn = (filePath: string) => ?string;
module.exports = class ModuleCache {
_getClosestPackage: GetClosestPackageFn;
getPackageData: GetFn<PackageData>;
getTransformedFile: GetFn<TransformedFile>;
modules: Map<string, Module>;
packages: Map<string, Package>;
constructor(getClosestPackage: GetClosestPackageFn, getTransformedFile: GetFn<TransformedFile>) {
this._getClosestPackage = getClosestPackage;
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, 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;
}
getPackageOf(filePath: string) {
const candidate = this._getClosestPackage(filePath);
return candidate != null ? this.getPackage(candidate) : null;
}
};

View File

@ -0,0 +1,138 @@
/**
* 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>;
path: string;
root: string;
type: 'Package';
constructor(packagePath: string, data: Promise<PackageData>) {
this.data = data;
this.path = packagePath;
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);
}
isHaste() {
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(new RegExp('\\' + path.sep, 'g'), '/');
}
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,71 @@
/**
* 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';
'use strict';
import DependencyGraphHelpers from '../../node-haste/DependencyGraph/DependencyGraphHelpers';
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>,
getPackage(): ?Package,
isHaste(): Promise<boolean>,
};
export type Package = {
path: Path,
root: Path,
type: 'Package',
getMain(): Promise<Path>,
getName(): Promise<ModuleID>,
isHaste(): Promise<boolean>,
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,
getPackageOf(path: Path): ?Package,
}
export type FastFS = {
dirExists(path: Path): boolean,
closest(path: string, fileName: string): ?string,
fileExists(path: Path): boolean,
getAllFiles(): Array<Path>,
matches(directory: Path, pattern: RegExp): Array<Path>,
};
type HasteMapOptions = {|
extensions: Extensions,
files: Array<string>,
helpers: DependencyGraphHelpers,
moduleCache: ModuleCache,
platforms: Platforms,
preferNativePlatform: true,
|};
declare class HasteMap {
// node-haste/DependencyGraph/HasteMap.js
build(): Promise<Object>,
constructor(options: HasteMapOptions): void,
}

View File

@ -0,0 +1,101 @@
/**
* 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
Extensions,
Path,
} from './node-haste.flow';
import type {
ResolveFn,
TransformedFile,
} from '../types.flow';
const DependencyGraphHelpers = require('../../node-haste/DependencyGraph/DependencyGraphHelpers');
const HasteFS = require('./HasteFS');
const HasteMap = require('../../node-haste/DependencyGraph/HasteMap');
const Module = require('./Module');
const ModuleCache = require('./ModuleCache');
const ResolutionRequest = require('../../node-haste/DependencyGraph/ResolutionRequest');
const defaults = require('../../../../defaults');
type ResolveOptions = {|
assetExts: Extensions,
extraNodeModules: {[id: string]: string},
transformedFiles: {[path: Path]: TransformedFile},
|};
const platforms = new Set(defaults.platforms);
exports.createResolveFn = function(options: ResolveOptions): ResolveFn {
const {
assetExts,
extraNodeModules,
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: defaults.providesModuleNodeModules,
});
const hasteFS = new HasteFS(files);
const moduleCache = new ModuleCache(
filePath => hasteFS.closest(filePath, 'package.json'),
getTransformedFile,
);
const hasteMap = new HasteMap({
extensions: ['js', 'json'],
files,
helpers,
moduleCache,
platforms,
preferNativePlatform: true,
});
const hasteMapBuilt = hasteMap.build();
const resolutionRequests = {};
return (id, source, platform, _, callback) => {
let resolutionRequest = resolutionRequests[platform];
if (!resolutionRequest) {
resolutionRequest = resolutionRequests[platform] = new ResolutionRequest({
dirExists: filePath => hasteFS.dirExists(filePath),
entryPath: '',
extraNodeModules,
/* $FlowFixMe: object is missing matchFiles method */
hasteFS,
hasteMap,
helpers,
moduleCache,
platform,
platforms,
preferNativePlatform: true,
});
}
const from = new Module(source, moduleCache, getTransformedFile(source));
hasteMapBuilt
.then(() => 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

@ -0,0 +1,83 @@
/**
* Copyright (c) 2016-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.
*/
'use strict';
jest.disableAutomock();
const {match} = require('sinon');
const {fn} = require('../../test-helpers');
const {
addModuleIdsToModuleWrapper,
createIdForPathFn,
} = require('../util');
const {any} = jasmine;
describe('`addModuleIdsToModuleWrapper`:', () => {
const path = 'path/to/file';
const createModule = (dependencies = []) => ({
dependencies,
file: {code: '__d(function(){});', isModule: true, path},
});
it('completes the module wrapped with module ID, and an array of dependency IDs', () => {
const dependencies = [
{id: 'a', path: 'path/to/a.js'},
{id: 'b', path: 'location/of/b.js'},
];
const module = createModule(dependencies);
const idForPath = fn();
idForPath.stub
.withArgs(match({path})).returns(12)
.withArgs(match({path: dependencies[0].path})).returns(345)
.withArgs(match({path: dependencies[1].path})).returns(6);
expect(addModuleIdsToModuleWrapper(module, idForPath))
.toEqual('__d(function(){},12,[345,6]);');
});
it('omits the array of dependency IDs if it is empty', () => {
const module = createModule();
expect(addModuleIdsToModuleWrapper(module, () => 98))
.toEqual(`__d(function(){},${98});`);
});
});
describe('`createIdForPathFn`', () => {
let idForPath;
beforeEach(() => {
idForPath = createIdForPathFn();
});
it('returns a number for a string', () => {
expect(idForPath({path: 'arbitrary'})).toEqual(any(Number));
});
it('returns consecutive numbers', () => {
const strings = [
'arbitrary string',
'looking/like/a/path',
'/absolute/path/to/file.js',
'/more files/are here',
];
strings.forEach((string, i) => {
expect(idForPath({path: string})).toEqual(i);
});
});
it('returns the same id if the same string is passed in again', () => {
const path = 'this/is/an/arbitrary/path.js';
const id = idForPath({path});
idForPath({path: '/other/file'});
idForPath({path: 'and/another/file'});
expect(idForPath({path})).toEqual(id);
});
});

View File

@ -0,0 +1,47 @@
/**
* Copyright (c) 2016-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 {createIndexMap} = require('./source-map');
const {addModuleIdsToModuleWrapper} = require('./util');
import type {OutputFn} from '../types.flow';
module.exports = (
(modules, filename, idForPath) => {
let code = '';
let line = 0;
const sections = [];
for (const module of modules) {
const {file} = module;
const moduleCode = file.type === 'module'
? addModuleIdsToModuleWrapper(module, idForPath)
: file.code;
code += moduleCode + '\n';
if (file.map) {
sections.push({
map: file.map,
offset: {column: 0, line}
});
}
line += countLines(moduleCode);
}
return {code, map: createIndexMap({file: filename, sections})};
}: OutputFn);
const reLine = /^/gm;
function countLines(string: string): number {
//$FlowFixMe This regular expression always matches
return string.match(reLine).length;
}

View File

@ -0,0 +1,48 @@
/**
* Copyright (c) 2016-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';
type CreateIndexMapOptions = {|
file?: string,
sections?: Array<IndexMapSection>
|};
type IndexMap = MapBase & {
sections: Array<IndexMapSection>,
};
type IndexMapSection = {
map: IndexMap | MappingsMap,
offset: {line: number, column: number},
};
type MapBase = {
// always the first entry in the source map entry object per
// https://fburl.com/source-map-spec#heading=h.qz3o9nc69um5
version: 3,
file?: string,
};
type MappingsMap = MapBase & {
mappings: string,
names: Array<string>,
sourceRoot?: string,
sources: Array<string>,
sourcesContent?: Array<?string>,
};
export type SourceMap = IndexMap | MappingsMap;
exports.createIndexMap = (opts?: CreateIndexMapOptions): IndexMap => ({
version: 3,
file: opts && opts.file,
sections: opts && opts.sections || [],
});

View File

@ -0,0 +1,85 @@
/**
* Copyright (c) 2016-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 {IdForPathFn, Module} from '../types.flow';
// Transformed modules have the form
// __d(function(require, module, global, exports, dependencyMap) {
// /* code */
// });
//
// This function adds the numeric module ID, and an array with dependencies of
// the dependencies of the module before the closing parenthesis.
exports.addModuleIdsToModuleWrapper = (
module: Module,
idForPath: {path: string} => number,
): string => {
const {dependencies, file} = module;
const {code} = file;
const index = code.lastIndexOf(')');
// calling `idForPath` on the module itself first gives us a lower module id
// for the file itself than for its dependencies. That reflects their order
// in the bundle.
const fileId = idForPath(file);
// This code runs for both development and production builds, after
// minification. That's why we leave out all spaces.
const depencyIds =
dependencies.length ? `,[${dependencies.map(idForPath).join(',')}]` : '';
return (
code.slice(0, index) +
`,${fileId}` +
depencyIds +
code.slice(index)
);
};
// Creates an idempotent function that returns numeric IDs for objects based
// on their `path` property.
exports.createIdForPathFn = (): ({path: string} => number) => {
const seen = new Map();
let next = 0;
return ({path}) => {
let id = seen.get(path);
if (id == null) {
id = next++;
seen.set(path, id);
}
return id;
};
};
// creates a series of virtual modules with require calls to the passed-in
// modules.
exports.requireCallsTo = function* (
modules: Iterable<Module>,
idForPath: IdForPathFn,
): Iterable<Module> {
for (const module of modules) {
yield virtualModule(`require(${idForPath(module.file)});`);
}
};
// creates a virtual module (i.e. not corresponding to a file on disk)
// with the given source code.
exports.virtualModule = virtualModule;
function virtualModule(code: string) {
return {
dependencies: [],
file: {
code,
path: '',
type: 'script',
}
};
}

View File

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

View File

@ -0,0 +1,15 @@
/**
* Copyright (c) 2016-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.
*/
'use strict';
const {Console} = require('console');
const {Writable} = require('stream');
const write = (_, __, callback) => callback();
module.exports = new Console(new Writable({write, writev: write}));

View File

@ -0,0 +1,23 @@
/**
* Copyright (c) 2016-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.
*/
'use strict';
const generate = require('babel-generator').default;
const stub = require('sinon/lib/sinon/stub');
exports.fn = () => {
const s = stub();
const f = jest.fn(s);
f.stub = s;
return f;
};
const generateOptions = {concise: true};
exports.codeFromAst = ast => generate(ast, generateOptions).code;
exports.comparableCode = code => code.trim().replace(/\s\s+/g, ' ');

View File

@ -0,0 +1,134 @@
/**
* Copyright (c) 2016-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 {SourceMap} from './output/source-map';
import type {Console} from 'console';
export type Callback<A = void, B = void>
= (Error => void)
& ((null | void, A, B) => void);
type Dependency = {|
id: string,
path: string,
|};
export type File = {|
code: string,
map?: ?Object,
path: string,
type: FileTypes,
|};
type FileTypes = 'module' | 'script';
export type GraphFn = (
entryPoints: Iterable<string>,
platform: string,
options?: ?GraphOptions,
callback?: Callback<GraphResult>,
) => void;
type GraphOptions = {|
cwd?: string,
log?: Console,
optimize?: boolean,
skip?: Set<string>,
|};
export type GraphResult = {
entryModules: Array<Module>,
modules: Array<Module>,
};
export type IdForPathFn = {path: string} => number;
export type LoadFn = (
file: string,
options: LoadOptions,
callback: Callback<File, Array<string>>,
) => void;
type LoadOptions = {|
log?: Console,
optimize?: boolean,
platform?: string,
|};
export type Module = {|
dependencies: Array<Dependency>,
file: File,
|};
export type OutputFn = (
modules: Iterable<Module>,
filename?: string,
idForPath: IdForPathFn,
) => OutputResult;
type OutputResult = {
code: string,
map: SourceMap,
};
export type PackageData = {|
browser?: Object | string,
main?: string,
name?: string,
'react-native'?: Object | string,
|};
export type ResolveFn = (
id: string,
source: string,
platform: string,
options?: ResolveOptions,
callback: Callback<string>,
) => void;
type ResolveOptions = {
log?: Console,
};
export type TransformFn = (
data: {|
filename: string,
options?: Object,
plugins?: Array<string | Object | [string | Object, any]>,
sourceCode: string,
|},
callback: Callback<TransformFnResult>
) => void;
export type TransformFnResult = {
ast: Object,
};
export type TransformResult = {|
code: string,
dependencies: Array<string>,
dependencyMapName?: string,
map: ?Object,
|};
export type TransformResults = {[string]: TransformResult};
export type TransformVariants = {[key: string]: Object};
export type TransformedFile = {
code: string,
file: string,
hasteID: ?string,
package?: PackageData,
transformed: TransformResults,
type: FileTypes,
};

View File

@ -0,0 +1,25 @@
/**
* Copyright (c) 2016-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 asyncify = require('async/asyncify');
const optimizeModule = require('./worker/optimize-module');
const transformModule = require('./worker/transform-module');
const wrapWorkerFn = require('./worker/wrap-worker-fn');
import type {OptimizationOptions} from './worker/optimize-module';
import type {TransformOptions} from './worker/transform-module';
import type {WorkerFnWithIO} from './worker/wrap-worker-fn';
exports.optimizeModule =
(wrapWorkerFn(asyncify(optimizeModule)): WorkerFnWithIO<OptimizationOptions>);
exports.transformModule =
(wrapWorkerFn(transformModule): WorkerFnWithIO<TransformOptions>);

View File

@ -0,0 +1,133 @@
/**
* Copyright (c) 2017-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.
*/
'use strict';
jest.disableAutomock();
const collectDependencies = require('../collect-dependencies');
const astFromCode = require('babylon').parse;
const {codeFromAst, comparableCode} = require('../../test-helpers');
const {any} = expect;
describe('dependency collection from ASTs:', () => {
it('collects dependency identifiers from the code', () => {
const ast = astFromCode(`
const a = require('b/lib/a');
exports.do = () => require("do");
if (!something) {
require("setup/something");
}
`);
expect(collectDependencies(ast).dependencies)
.toEqual(['b/lib/a', 'do', 'setup/something']);
});
it('supports template literals as arguments', () => {
const ast = astFromCode('require(`left-pad`)');
expect(collectDependencies(ast).dependencies)
.toEqual(['left-pad']);
});
it('ignores template literals with interpolations', () => {
const ast = astFromCode('require(`left${"-"}pad`)');
expect(collectDependencies(ast).dependencies)
.toEqual([]);
});
it('ignores tagged template literals', () => {
const ast = astFromCode('require(tag`left-pad`)');
expect(collectDependencies(ast).dependencies)
.toEqual([]);
});
it('exposes a string as `dependencyMapName`', () => {
const ast = astFromCode('require("arbitrary")');
expect(collectDependencies(ast).dependencyMapName)
.toEqual(any(String));
});
it('exposes a string as `dependencyMapName` even without collecting dependencies', () => {
const ast = astFromCode('');
expect(collectDependencies(ast).dependencyMapName)
.toEqual(any(String));
});
it('replaces all required module ID strings with array lookups and keeps the ID as second argument', () => {
const ast = astFromCode(`
const a = require('b/lib/a');
const b = require(123);
exports.do = () => require("do");
if (!something) {
require("setup/something");
}
`);
const {dependencyMapName} = collectDependencies(ast);
expect(codeFromAst(ast)).toEqual(comparableCode(`
const a = require(${dependencyMapName}[0], 'b/lib/a');
const b = require(123);
exports.do = () => require(${dependencyMapName}[1], "do");
if (!something) {
require(${dependencyMapName}[2], "setup/something");
}
`));
});
});
describe('Dependency collection from optimized ASTs:', () => {
const dependencyMapName = 'arbitrary';
const {forOptimization} = collectDependencies;
let ast, names;
beforeEach(() => {
ast = astFromCode(`
const a = require(${dependencyMapName}[0], 'b/lib/a');
const b = require(123);
exports.do = () => require(${dependencyMapName}[1], "do");
if (!something) {
require(${dependencyMapName}[2], "setup/something");
}
`);
names = ['b/lib/a', 'do', 'setup/something'];
});
it('passes the `dependencyMapName` through', () => {
const result = forOptimization(ast, names, dependencyMapName);
expect(result.dependencyMapName).toEqual(dependencyMapName);
});
it('returns the list of passed in dependencies', () => {
const result = forOptimization(ast, names, dependencyMapName);
expect(result.dependencies).toEqual(names);
});
it('only returns dependencies that are in the code', () => {
ast = astFromCode(`require(${dependencyMapName}[1], 'do')`);
const result = forOptimization(ast, names, dependencyMapName);
expect(result.dependencies).toEqual(['do']);
});
it('replaces all call signatures inserted by a prior call to `collectDependencies`', () => {
forOptimization(ast, names, dependencyMapName);
expect(codeFromAst(ast)).toEqual(comparableCode(`
const a = require(${dependencyMapName}[0]);
const b = require(123);
exports.do = () => require(${dependencyMapName}[1]);
if (!something) {
require(${dependencyMapName}[2]);
}
`));
});
});

View File

@ -0,0 +1,97 @@
/**
* Copyright (c) 2016-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.
*/
'use strict';
jest.disableAutomock();
const optimizeModule = require('../optimize-module');
const transformModule = require('../transform-module');
const transform = require('../../../../../transformer.js');
const {SourceMapConsumer} = require('source-map');
const {objectContaining} = jasmine;
describe('optimizing JS modules', () => {
const filename = 'arbitrary/file.js';
const optimizationOptions = {
dev: false,
platform: 'android',
};
const originalCode =
`if (Platform.OS !== 'android') {
require('arbitrary-dev');
} else {
__DEV__ ? require('arbitrary-android-dev') : require('arbitrary-android-prod');
}`;
let transformResult;
beforeAll(done => {
transformModule(originalCode, {filename, transform}, (error, result) => {
if (error) {
throw error;
}
transformResult = JSON.stringify(result);
done();
});
});
it('copies everything from the transformed file, except for transform results', () => {
const result = optimizeModule(transformResult, optimizationOptions);
const expected = JSON.parse(transformResult);
delete expected.transformed;
expect(result).toEqual(objectContaining(expected));
});
describe('code optimization', () => {
let dependencyMapName, injectedVars, optimized, requireName;
beforeAll(() => {
const result = optimizeModule(transformResult, optimizationOptions);
optimized = result.transformed.default;
injectedVars = optimized.code.match(/function\(([^)]*)/)[1].split(',');
[,requireName,,, dependencyMapName] = injectedVars;
});
it('optimizes code', () => {
expect(optimized.code)
.toEqual(`__d(function(${injectedVars}){${requireName}(${dependencyMapName}[0])});`);
});
it('extracts dependencies', () => {
expect(optimized.dependencies).toEqual(['arbitrary-android-prod']);
});
it('creates source maps', () => {
const consumer = new SourceMapConsumer(optimized.map);
const column = optimized.code.lastIndexOf(requireName + '(');
const loc = findLast(originalCode, 'require');
expect(consumer.originalPositionFor({line: 1, column}))
.toEqual(objectContaining(loc));
});
it('does not extract dependencies for polyfills', () => {
const result = optimizeModule(
transformResult,
{...optimizationOptions, isPolyfill: true},
);
expect(result.transformed.default.dependencies).toEqual([]);
});
});
});
function findLast(code, needle) {
const lines = code.split(/(?:(?!.)\s)+/);
let line = lines.length;
while (line--) {
const column = lines[line].lastIndexOf(needle);
if (column !== -1) {
return {line: line + 1, column};
}
}
}

View File

@ -0,0 +1,232 @@
/**
* Copyright (c) 2016-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.
*/
'use strict';
jest.disableAutomock();
const transformModule = require('../transform-module');
const t = require('babel-types');
const {SourceMapConsumer} = require('source-map');
const {fn} = require('../../test-helpers');
const {parse} = require('babylon');
const generate = require('babel-generator').default;
const {traverse} = require('babel-core');
const {any, objectContaining} = jasmine;
describe('transforming JS modules:', () => {
const filename = 'arbitrary';
let transform;
beforeEach(() => {
transform = fn();
transform.stub.yields(null, transformResult());
});
const {bodyAst, sourceCode, transformedCode} = createTestData();
const options = variants => ({
filename,
transform,
variants,
});
const transformResult = (body = bodyAst) => ({
ast: t.file(t.program(body)),
});
it('passes through file name and code', done => {
transformModule(sourceCode, options(), (error, result) => {
expect(result).toEqual(objectContaining({
code: sourceCode,
file: filename,
}));
done();
});
});
it('exposes a haste ID if present', done => {
const hasteID = 'TheModule';
const codeWithHasteID = `/** @providesModule ${hasteID} */`;
transformModule(codeWithHasteID, options(), (error, result) => {
expect(result).toEqual(objectContaining({hasteID}));
done();
});
});
it('sets `type` to `"module"` by default', done => {
transformModule(sourceCode, options(), (error, result) => {
expect(result).toEqual(objectContaining({type: 'module'}));
done();
});
});
it('sets `type` to `"script"` if the input is a polyfill', done => {
transformModule(sourceCode, {...options(), polyfill: true}, (error, result) => {
expect(result).toEqual(objectContaining({type: 'script'}));
done();
});
});
it('calls the passed-in transform function with code, file name, and options for all passed in variants', done => {
const variants = {dev: {dev: true}, prod: {dev: false}};
transformModule(sourceCode, options(variants), () => {
expect(transform)
.toBeCalledWith({filename, sourceCode, options: variants.dev}, any(Function));
expect(transform)
.toBeCalledWith({filename, sourceCode, options: variants.prod}, any(Function));
done();
});
});
it('calls back with any error yielded by the transform function', done => {
const error = new Error();
transform.stub.yields(error);
transformModule(sourceCode, options(), e => {
expect(e).toBe(error);
done();
});
});
it('wraps the code produced by the transform function into a module factory', done => {
transformModule(sourceCode, options(), (error, result) => {
expect(error).toEqual(null);
const {code, dependencyMapName} = result.transformed.default;
expect(code.replace(/\s+/g, ''))
.toEqual(
`__d(function(global,require,module,exports,${
dependencyMapName}){${transformedCode}});`
);
done();
});
});
it('wraps the code produced by the transform function into an immediately invoked function expression for polyfills', done => {
transformModule(sourceCode, {...options(), polyfill: true}, (error, result) => {
expect(error).toEqual(null);
const {code} = result.transformed.default;
expect(code.replace(/\s+/g, ''))
.toEqual(`(function(global){${transformedCode}})(this);`);
done();
});
});
it('creates source maps', done => {
transformModule(sourceCode, options(), (error, result) => {
const {code, map} = result.transformed.default;
const column = code.indexOf('code');
const consumer = new SourceMapConsumer(map);
expect(consumer.originalPositionFor({line: 1, column}))
.toEqual(objectContaining({line: 1, column: sourceCode.indexOf('code')}));
done();
});
});
it('extracts dependencies (require calls)', done => {
const dep1 = 'foo', dep2 = 'bar';
const code = `require('${dep1}'),require('${dep2}')`;
const {body} = parse(code).program;
transform.stub.yields(null, transformResult(body));
transformModule(code, options(), (error, result) => {
expect(result.transformed.default)
.toEqual(objectContaining({dependencies: [dep1, dep2]}));
done();
});
});
it('transforms for all variants', done => {
const variants = {dev: {dev: true}, prod: {dev: false}};
transform.stub
.withArgs(filename, sourceCode, variants.dev)
.yields(null, transformResult(bodyAst))
.withArgs(filename, sourceCode, variants.prod)
.yields(null, transformResult([]));
transformModule(sourceCode, options(variants), (error, result) => {
const {dev, prod} = result.transformed;
expect(dev.code.replace(/\s+/g, ''))
.toEqual(
`__d(function(global,require,module,exports,${
dev.dependencyMapName}){arbitrary(code);});`
);
expect(prod.code.replace(/\s+/g, ''))
.toEqual(
`__d(function(global,require,module,exports,${
prod.dependencyMapName}){arbitrary(code);});`
);
done();
});
});
it('prefixes JSON files with `module.exports = `', done => {
const json = '{"foo":"bar"}';
transformModule(json, {...options(), filename: 'some.json'}, (error, result) => {
const {code} = result.transformed.default;
expect(code.replace(/\s+/g, ''))
.toEqual(
'__d(function(global,require,module,exports){' +
`module.exports=${json}});`
);
done();
});
});
it('does not create source maps for JSON files', done => {
transformModule('{}', {...options(), filename: 'some.json'}, (error, result) => {
expect(result.transformed.default)
.toEqual(objectContaining({map: null}));
done();
});
});
it('adds package data for `package.json` files', done => {
const pkg = {
name: 'package-name',
main: 'package/main',
browser: {browser: 'defs'},
'react-native': {'react-native': 'defs'},
};
transformModule(
JSON.stringify(pkg),
{...options(), filename: 'arbitrary/package.json'},
(error, result) => {
expect(result.package).toEqual(pkg);
done();
},
);
});
});
function createTestData() {
// creates test data with an transformed AST, so that we can test source
// map generation.
const sourceCode = 'some(arbitrary(code));';
const fileAst = parse(sourceCode);
traverse(fileAst, {
CallExpression(path) {
if (path.node.callee.name === 'some') {
path.replaceWith(path.node.arguments[0]);
}
}
});
return {
bodyAst: fileAst.program.body,
sourceCode,
transformedCode: generate(fileAst).code,
};
}

View File

@ -0,0 +1,89 @@
/**
* Copyright (c) 2016-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.
*/
'use strict';
jest
.disableAutomock()
.setMock('fs', jest.genMockFromModule('fs'))
.mock('mkdirp');
const wrapWorkerFn = require('../wrap-worker-fn');
const {dirname} = require('path');
const {fn} = require('../../test-helpers');
const {any} = jasmine;
describe('wrapWorkerFn:', () => {
const infile = '/arbitrary/in/file';
const outfile = '/arbitrary/in/file';
let workerFn, wrapped;
beforeEach(() => {
workerFn = fn();
workerFn.stub.yields();
wrapped = wrapWorkerFn(workerFn);
});
const fs = require('fs');
const mkdirp = require('mkdirp');
it('reads the passed-in file synchronously as UTF-8', done => {
wrapped(infile, outfile, {}, () => {
expect(fs.readFileSync).toBeCalledWith(infile, 'utf8');
done();
});
});
it('calls the worker function with file contents and options', done => {
const contents = 'arbitrary(contents);';
const options = {arbitrary: 'options'};
fs.readFileSync.mockReturnValue(contents);
wrapped(infile, outfile, options, () => {
expect(workerFn).toBeCalledWith(contents, options, any(Function));
done();
});
});
it('passes through any error that the worker function calls back with', done => {
const error = new Error();
workerFn.stub.yields(error);
wrapped(infile, outfile, {}, e => {
expect(e).toBe(error);
done();
});
});
it('writes the result to disk', done => {
const result = {arbitrary: 'result'};
workerFn.stub.yields(null, result);
wrapped(infile, outfile, {}, () => {
expect(mkdirp.sync).toBeCalledWith(dirname(outfile));
expect(fs.writeFileSync).toBeCalledWith(outfile, JSON.stringify(result), 'utf8');
done();
});
});
it('calls back with any error thrown by `mkdirp.sync`', done => {
const error = new Error();
mkdirp.sync.mockImplementationOnce(() => { throw error; });
wrapped(infile, outfile, {}, e => {
expect(e).toBe(error);
done();
});
});
it('calls back with any error thrown by `fs.writeFileSync`', done => {
const error = new Error();
fs.writeFileSync.mockImplementationOnce(() => { throw error; });
wrapped(infile, outfile, {}, e => {
expect(e).toBe(error);
done();
});
});
});

View File

@ -0,0 +1,150 @@
/**
* Copyright (c) 2016-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 nullthrows = require('fbjs/lib/nullthrows');
const {traverse, types} = require('babel-core');
type AST = Object;
class Replacement {
nameToIndex: Map<string, number>;
nextIndex: number;
constructor() {
this.nameToIndex = new Map();
this.nextIndex = 0;
}
isRequireCall(callee, firstArg) {
return (
callee.type === 'Identifier' && callee.name === 'require' &&
firstArg && isLiteralString(firstArg)
);
}
getIndex(stringLiteralOrTemplateLiteral) {
const name = stringLiteralOrTemplateLiteral.quasis
? stringLiteralOrTemplateLiteral.quasis[0].value.cooked
: stringLiteralOrTemplateLiteral.value;
let index = this.nameToIndex.get(name);
if (index !== undefined) {
return index;
}
index = this.nextIndex++;
this.nameToIndex.set(name, index);
return index;
}
getNames() {
return Array.from(this.nameToIndex.keys());
}
makeArgs(newId, oldId, dependencyMapIdentifier) {
const mapLookup = createMapLookup(dependencyMapIdentifier, newId);
return [mapLookup, oldId];
}
}
class ProdReplacement {
replacement: Replacement;
names: Array<string>;
constructor(names) {
this.replacement = new Replacement();
this.names = names;
}
isRequireCall(callee, firstArg) {
return (
callee.type === 'Identifier' &&
callee.name === 'require' &&
firstArg &&
firstArg.type === 'MemberExpression' &&
firstArg.property &&
firstArg.property.type === 'NumericLiteral'
);
}
getIndex(memberExpression) {
const id = memberExpression.property.value;
if (id in this.names) {
return this.replacement.getIndex({value: this.names[id]});
}
throw new Error(
`${id} is not a known module ID. Existing mappings: ${
this.names.map((n, i) => `${i} => ${n}`).join(', ')}`
);
}
getNames() {
return this.replacement.getNames();
}
makeArgs(newId, _, dependencyMapIdentifier) {
const mapLookup = createMapLookup(dependencyMapIdentifier, newId);
return [mapLookup];
}
}
function createMapLookup(dependencyMapIdentifier, propertyIdentifier) {
return types.memberExpression(
dependencyMapIdentifier,
propertyIdentifier,
true,
);
}
function collectDependencies(ast, replacement, dependencyMapIdentifier) {
const traversalState = {dependencyMapIdentifier};
traverse(ast, {
Program(path, state) {
if (!state.dependencyMapIdentifier) {
state.dependencyMapIdentifier =
path.scope.generateUidIdentifier('dependencyMap');
}
},
CallExpression(path, state) {
const node = path.node;
const arg = node.arguments[0];
if (replacement.isRequireCall(node.callee, arg)) {
const index = replacement.getIndex(arg);
node.arguments = replacement.makeArgs(
types.numericLiteral(index),
arg,
state.dependencyMapIdentifier,
);
}
},
}, null, traversalState);
return {
dependencies: replacement.getNames(),
dependencyMapName: nullthrows(traversalState.dependencyMapIdentifier).name,
};
}
function isLiteralString(node) {
return node.type === 'StringLiteral' ||
node.type === 'TemplateLiteral' && node.quasis.length === 1;
}
exports = module.exports =
(ast: AST) => collectDependencies(ast, new Replacement());
exports.forOptimization =
(ast: AST, names: Array<string>, dependencyMapName?: string) =>
collectDependencies(
ast,
new ProdReplacement(names),
dependencyMapName ? types.identifier(dependencyMapName) : undefined,
);

View File

@ -0,0 +1,26 @@
/**
* Copyright (c) 2016-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 babelGenerate = require('babel-generator').default;
function generate(ast: Object, filename: string, sourceCode: string) {
return babelGenerate(ast, {
comments: false,
compact: true,
filename,
sourceFileName: filename,
sourceMaps: true,
sourceMapTarget: filename,
}, sourceCode);
}
module.exports = generate;

View File

@ -0,0 +1,108 @@
/**
* Copyright (c) 2016-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 babel = require('babel-core');
const collectDependencies = require('./collect-dependencies');
const constantFolding = require('../../JSTransformer/worker/constant-folding').plugin;
const generate = require('./generate');
const inline = require('../../JSTransformer/worker/inline').plugin;
const minify = require('../../JSTransformer/worker/minify');
const sourceMap = require('source-map');
import type {TransformedFile, TransformResult} from '../types.flow';
export type OptimizationOptions = {|
dev: boolean,
isPolyfill?: boolean,
platform: string,
|};
function optimizeModule(
data: string | TransformedFile,
optimizationOptions: OptimizationOptions,
): TransformedFile {
if (typeof data === 'string') {
data = JSON.parse(data);
}
const {code, file, transformed} = data;
const result = {...data, transformed: {}};
//$FlowIssue #14545724
Object.entries(transformed).forEach(([k, t: TransformResult]: [*, TransformResult]) => {
result.transformed[k] = optimize(t, file, code, optimizationOptions);
});
return result;
}
function optimize(transformed, file, originalCode, options): TransformResult {
const {code, dependencyMapName, map} = transformed;
const optimized = optimizeCode(code, map, file, options);
let dependencies;
if (options.isPolyfill) {
dependencies = [];
} else {
({dependencies} = collectDependencies.forOptimization(
optimized.ast,
transformed.dependencies,
dependencyMapName,
));
}
const inputMap = transformed.map;
const gen = generate(optimized.ast, file, originalCode);
const min = minify(
file,
gen.code,
inputMap && mergeSourceMaps(file, inputMap, gen.map),
);
return {code: min.code, map: inputMap && min.map, dependencies};
}
function optimizeCode(code, map, filename, inliningOptions) {
return babel.transform(code, {
plugins: [
[constantFolding],
[inline, {...inliningOptions, isWrapped: true}],
],
babelrc: false,
code: false,
filename,
});
}
function mergeSourceMaps(file, originalMap, secondMap) {
const merged = new sourceMap.SourceMapGenerator();
const inputMap = new sourceMap.SourceMapConsumer(originalMap);
new sourceMap.SourceMapConsumer(secondMap)
.eachMapping(mapping => {
const original = inputMap.originalPositionFor({
line: mapping.originalLine,
column: mapping.originalColumn,
});
if (original.line == null) {
return;
}
merged.addMapping({
generated: {line: mapping.generatedLine, column: mapping.generatedColumn},
original: {line: original.line, column: original.column || 0},
source: file,
name: original.name || mapping.name,
});
});
return merged.toJSON();
}
module.exports = optimizeModule;

View File

@ -0,0 +1,165 @@
/**
* Copyright (c) 2016-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 babel = require('babel-core');
const collectDependencies = require('./collect-dependencies');
const docblock = require('../../node-haste/DependencyGraph/docblock');
const generate = require('./generate');
const series = require('async/series');
const {basename} = require('path');
import type {
Callback,
TransformedFile,
TransformFn,
TransformFnResult,
TransformResult,
TransformVariants,
} from '../types.flow';
export type TransformOptions = {|
filename: string,
polyfill?: boolean,
transform: TransformFn,
variants?: TransformVariants,
|};
const defaultVariants = {default: {}};
const moduleFactoryParameters = ['global', 'require', 'module', 'exports'];
const polyfillFactoryParameters = ['global'];
function transformModule(
code: string,
options: TransformOptions,
callback: Callback<TransformedFile>,
): void {
if (options.filename.endsWith('.json')) {
return transformJSON(code, options, callback);
}
const {filename, transform, variants = defaultVariants} = options;
const tasks = {};
Object.keys(variants).forEach(name => {
tasks[name] = cb => transform({
filename,
sourceCode: code,
options: variants[name],
}, cb);
});
series(tasks, (error, results: {[key: string]: TransformFnResult}) => {
if (error) {
callback(error);
return;
}
const transformed: {[key: string]: TransformResult} = {};
//$FlowIssue #14545724
Object.entries(results).forEach(([key, value]: [*, TransformFnResult]) => {
transformed[key] = makeResult(value.ast, filename, code, options.polyfill);
});
const annotations = docblock.parseAsObject(docblock.extract(code));
callback(null, {
code,
file: filename,
hasteID: annotations.providesModule || annotations.provide || null,
transformed,
type: options.polyfill ? 'script' : 'module',
});
});
}
function transformJSON(json, options, callback) {
const value = JSON.parse(json);
const {filename} = options;
const code =
`__d(function(${moduleFactoryParameters.join(', ')}) { module.exports = \n${
json
}\n});`;
const moduleData = {
code,
map: null, // no source map for JSON files!
dependencies: [],
};
const transformed = {};
Object
.keys(options.variants || defaultVariants)
.forEach(key => (transformed[key] = moduleData));
const result: TransformedFile = {
code: json,
file: filename,
hasteID: value.name,
transformed,
type: 'module',
};
if (basename(filename) === 'package.json') {
result.package = {
name: value.name,
main: value.main,
browser: value.browser,
'react-native': value['react-native'],
};
}
callback(null, result);
}
function makeResult(ast, filename, sourceCode, isPolyfill = false) {
let dependencies, dependencyMapName, file;
if (isPolyfill) {
dependencies = [];
file = wrapPolyfill(ast);
} else {
({dependencies, dependencyMapName} = collectDependencies(ast));
file = wrapModule(ast, dependencyMapName);
}
const gen = generate(file, filename, sourceCode);
return {code: gen.code, map: gen.map, dependencies, dependencyMapName};
}
function wrapModule(file, dependencyMapName) {
const t = babel.types;
const params = moduleFactoryParameters.concat(dependencyMapName);
const factory = functionFromProgram(file.program, params);
const def = t.callExpression(t.identifier('__d'), [factory]);
return t.file(t.program([t.expressionStatement(def)]));
}
function wrapPolyfill(file) {
const t = babel.types;
const factory = functionFromProgram(file.program, polyfillFactoryParameters);
const iife = t.callExpression(factory, [t.identifier('this')]);
return t.file(t.program([t.expressionStatement(iife)]));
}
function functionFromProgram(program, parameters) {
const t = babel.types;
return t.functionExpression(
t.identifier(''),
parameters.map(makeIdentifier),
t.blockStatement(program.body, program.directives),
);
}
function makeIdentifier(name) {
return babel.types.identifier(name);
}
module.exports = transformModule;

View File

@ -0,0 +1,62 @@
/**
* Copyright (c) 2016-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 fs = require('fs');
const mkdirp = require('mkdirp');
const {dirname} = require('path');
import type {Callback} from '../types.flow';
type Path = string;
type WorkerFn<Options> = (
fileContents: string,
options: Options,
callback: Callback<Object>,
) => void;
export type WorkerFnWithIO<Options> = (
infile: Path,
outfile: Path,
options: Options,
callback: Callback<>,
) => void;
function wrapWorkerFn<Options>(
workerFunction: WorkerFn<Options>,
): WorkerFnWithIO<Options> {
return (
infile: Path,
outfile: Path,
options: Options,
callback: Callback<>,
) => {
const contents = fs.readFileSync(infile, 'utf8');
workerFunction(contents, options, (error, result) => {
if (error) {
callback(error);
return;
}
try {
mkdirp.sync(dirname(outfile));
fs.writeFileSync(outfile, JSON.stringify(result), 'utf8');
} catch (writeError) {
callback(writeError);
return;
}
callback(null);
});
};
}
module.exports = wrapWorkerFn;

View File

@ -0,0 +1,548 @@
/**
* 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.
*/
'use strict';
jest.unmock('../');
jest.unmock('../../../../defaults');
jest.mock('path');
const {join: pathJoin} = require.requireActual('path');
const DependencyGraph = jest.fn();
jest.setMock('../../node-haste', DependencyGraph);
let Module;
let Polyfill;
describe('Resolver', function() {
let Resolver, path;
beforeEach(function() {
Resolver = require('../');
path = require('path');
DependencyGraph.mockClear();
Module = jest.fn(function() {
this.getName = jest.fn();
this.getDependencies = jest.fn();
this.isPolyfill = jest.fn().mockReturnValue(false);
this.isJSON = jest.fn().mockReturnValue(false);
});
Polyfill = jest.fn(function() {
var polyfill = new Module();
polyfill.isPolyfill.mockReturnValue(true);
return polyfill;
});
DependencyGraph.replacePatterns = require.requireActual('../../node-haste/lib/replacePatterns');
DependencyGraph.prototype.createPolyfill = jest.fn();
DependencyGraph.prototype.getDependencies = jest.fn();
// For the polyfillDeps
path.join = jest.fn((a, b) => b);
DependencyGraph.prototype.load = jest.fn(() => Promise.resolve());
});
class ResolutionResponseMock {
constructor({dependencies, mainModuleId}) {
this.dependencies = dependencies;
this.mainModuleId = mainModuleId;
this.getModuleId = createGetModuleId();
}
prependDependency(dependency) {
this.dependencies.unshift(dependency);
}
finalize() {
return Promise.resolve(this);
}
getResolvedDependencyPairs() {
return [];
}
}
function createModule(id, dependencies) {
var module = new Module({});
module.path = id;
module.getName.mockImplementation(() => Promise.resolve(id));
module.getDependencies.mockImplementation(() => Promise.resolve(dependencies));
return module;
}
function createJsonModule(id) {
const module = createModule(id, []);
module.isJSON.mockReturnValue(true);
return module;
}
function createPolyfill(id, dependencies) {
var polyfill = new Polyfill({});
polyfill.getName = jest.fn(() => Promise.resolve(id));
polyfill.getDependencies =
jest.fn(() => Promise.resolve(dependencies));
return polyfill;
}
describe('getDependencies', function() {
it('forwards transform options to the dependency graph', function() {
const transformOptions = {arbitrary: 'options'};
const platform = 'ios';
const entry = '/root/index.js';
DependencyGraph.prototype.getDependencies.mockImplementation(
() => Promise.reject());
new Resolver({projectRoot: '/root'})
.getDependencies(entry, {platform}, transformOptions);
expect(DependencyGraph.prototype.getDependencies).toBeCalledWith({
entryPath: entry,
platform: platform,
transformOptions: transformOptions,
recursive: true,
});
});
it('passes custom platforms to the dependency graph', function() {
new Resolver({ // eslint-disable-line no-new
projectRoot: '/root',
platforms: ['ios', 'windows', 'vr'],
});
const platforms = DependencyGraph.mock.calls[0][0].platforms;
expect(platforms).toEqual(['ios', 'windows', 'vr']);
});
it('should get dependencies with polyfills', function() {
var module = createModule('index');
var deps = [module];
var depResolver = new Resolver({
projectRoot: '/root',
});
DependencyGraph.prototype.getDependencies.mockImplementation(function() {
return Promise.resolve(new ResolutionResponseMock({
dependencies: deps,
mainModuleId: 'index',
}));
});
return depResolver
.getDependencies(
'/root/index.js',
{ dev: false },
undefined,
undefined,
createGetModuleId()
).then(function(result) {
expect(result.mainModuleId).toEqual('index');
expect(result.dependencies[result.dependencies.length - 1]).toBe(module);
expect(
DependencyGraph
.prototype
.createPolyfill
.mock
.calls
.map((call) => call[0]))
.toEqual([
{ id: 'polyfills/polyfills.js',
file: 'polyfills/polyfills.js',
dependencies: []
},
{ id: 'polyfills/console.js',
file: 'polyfills/console.js',
dependencies: [
'polyfills/polyfills.js'
],
},
{ id: 'polyfills/error-guard.js',
file: 'polyfills/error-guard.js',
dependencies: [
'polyfills/polyfills.js',
'polyfills/console.js'
],
},
{ id: 'polyfills/Number.es6.js',
file: 'polyfills/Number.es6.js',
dependencies: [
'polyfills/polyfills.js',
'polyfills/console.js',
'polyfills/error-guard.js'
],
},
{ id: 'polyfills/String.prototype.es6.js',
file: 'polyfills/String.prototype.es6.js',
dependencies: [
'polyfills/polyfills.js',
'polyfills/console.js',
'polyfills/error-guard.js',
'polyfills/Number.es6.js',
],
},
{ id: 'polyfills/Array.prototype.es6.js',
file: 'polyfills/Array.prototype.es6.js',
dependencies: [
'polyfills/polyfills.js',
'polyfills/console.js',
'polyfills/error-guard.js',
'polyfills/Number.es6.js',
'polyfills/String.prototype.es6.js',
],
},
{ id: 'polyfills/Array.es6.js',
file: 'polyfills/Array.es6.js',
dependencies: [
'polyfills/polyfills.js',
'polyfills/console.js',
'polyfills/error-guard.js',
'polyfills/Number.es6.js',
'polyfills/String.prototype.es6.js',
'polyfills/Array.prototype.es6.js',
],
},
{ id: 'polyfills/Object.es7.js',
file: 'polyfills/Object.es7.js',
dependencies: [
'polyfills/polyfills.js',
'polyfills/console.js',
'polyfills/error-guard.js',
'polyfills/Number.es6.js',
'polyfills/String.prototype.es6.js',
'polyfills/Array.prototype.es6.js',
'polyfills/Array.es6.js',
],
},
{ id: 'polyfills/babelHelpers.js',
file: 'polyfills/babelHelpers.js',
dependencies: [
'polyfills/polyfills.js',
'polyfills/console.js',
'polyfills/error-guard.js',
'polyfills/Number.es6.js',
'polyfills/String.prototype.es6.js',
'polyfills/Array.prototype.es6.js',
'polyfills/Array.es6.js',
'polyfills/Object.es7.js',
],
},
].map(({id, file, dependencies}) => ({
id: pathJoin(__dirname, '..', id),
file: pathJoin(__dirname, '..', file),
dependencies: dependencies.map((d => pathJoin(__dirname, '..', d))),
})));
});
});
it('should get dependencies with polyfills', function() {
var module = createModule('index');
var deps = [module];
var depResolver = new Resolver({
projectRoot: '/root',
});
DependencyGraph.prototype.getDependencies.mockImplementation(function() {
return Promise.resolve(new ResolutionResponseMock({
dependencies: deps,
mainModuleId: 'index',
}));
});
const polyfill = {};
DependencyGraph.prototype.createPolyfill.mockReturnValueOnce(polyfill);
return depResolver
.getDependencies(
'/root/index.js',
{ dev: true },
undefined,
undefined,
createGetModuleId()
).then(function(result) {
expect(result.mainModuleId).toEqual('index');
expect(DependencyGraph.mock.instances[0].getDependencies)
.toBeCalledWith({entryPath: '/root/index.js', recursive: true});
expect(result.dependencies[0]).toBe(polyfill);
expect(result.dependencies[result.dependencies.length - 1])
.toBe(module);
});
});
it('should pass in more polyfills', function() {
var module = createModule('index');
var deps = [module];
var depResolver = new Resolver({
projectRoot: '/root',
polyfillModuleNames: ['some module'],
});
DependencyGraph.prototype.getDependencies.mockImplementation(function() {
return Promise.resolve(new ResolutionResponseMock({
dependencies: deps,
mainModuleId: 'index',
}));
});
return depResolver
.getDependencies(
'/root/index.js',
{ dev: false },
undefined,
undefined,
createGetModuleId()
).then((result) => {
expect(result.mainModuleId).toEqual('index');
expect(DependencyGraph.prototype.createPolyfill.mock.calls[result.dependencies.length - 2]).toEqual([
{ file: 'some module',
id: 'some module',
dependencies: [
'polyfills/polyfills.js',
'polyfills/console.js',
'polyfills/error-guard.js',
'polyfills/Number.es6.js',
'polyfills/String.prototype.es6.js',
'polyfills/Array.prototype.es6.js',
'polyfills/Array.es6.js',
'polyfills/Object.es7.js',
'polyfills/babelHelpers.js',
].map(d => pathJoin(__dirname, '..', d))
},
]);
});
});
});
describe('wrapModule', function() {
let depResolver;
beforeEach(() => {
depResolver = new Resolver({
depResolver,
projectRoot: '/root',
});
});
it('should resolve modules', function() {
/*eslint-disable */
var code = [
// require
'require("x")',
'require("y");require(\'abc\');',
'require( \'z\' )',
'require( "a")',
'require("b" )',
].join('\n');
/*eslint-disable */
function *findDependencyOffsets() {
const re = /(['"']).*?\1/g;
let match;
while ((match = re.exec(code))) {
yield match.index;
}
}
const dependencyOffsets = Array.from(findDependencyOffsets());
const module = createModule('test module', ['x', 'y']);
const resolutionResponse = new ResolutionResponseMock({
dependencies: [module],
mainModuleId: 'test module',
});
resolutionResponse.getResolvedDependencyPairs = (module) => {
return [
['x', createModule('changed')],
['y', createModule('Y')],
['abc', createModule('abc')]
];
}
const moduleIds = new Map(
resolutionResponse
.getResolvedDependencyPairs()
.map(([importId, module]) => [
importId,
padRight(resolutionResponse.getModuleId(module), importId.length + 2),
])
);
return depResolver.wrapModule({
resolutionResponse,
module: module,
name: 'test module',
code,
meta: {dependencyOffsets},
dev: false,
}).then(({code: processedCode}) => {
expect(processedCode).toEqual([
'__d(/* test module */function(global, require, module, exports) {' +
// require
`require(${moduleIds.get('x')}) // ${moduleIds.get('x').trim()} = x`,
`require(${moduleIds.get('y')});require(${moduleIds.get('abc')
}); // ${moduleIds.get('abc').trim()} = abc // ${moduleIds.get('y').trim()} = y`,
'require( \'z\' )',
'require( "a")',
'require("b" )',
`}, ${resolutionResponse.getModuleId(module)});`,
].join('\n'));
});
});
it('should add module transport names as fourth argument to `__d`', () => {
const module = createModule('test module');
const code = 'arbitrary(code)'
const resolutionResponse = new ResolutionResponseMock({
dependencies: [module],
mainModuleId: 'test module',
});
return depResolver.wrapModule({
resolutionResponse,
code,
module,
name: 'test module',
dev: true,
}).then(({code: processedCode}) =>
expect(processedCode).toEqual([
'__d(/* test module */function(global, require, module, exports) {' +
code,
`}, ${resolutionResponse.getModuleId(module)}, null, "test module");`
].join('\n'))
);
});
it('should pass through passed-in source maps', () => {
const module = createModule('test module');
const resolutionResponse = new ResolutionResponseMock({
dependencies: [module],
mainModuleId: 'test module',
});
const inputMap = {version: 3, mappings: 'ARBITRARY'};
return depResolver.wrapModule({
resolutionResponse,
module,
name: 'test module',
code: 'arbitrary(code)',
map: inputMap,
}).then(({map}) => expect(map).toBe(inputMap));
});
it('should resolve polyfills', function () {
const depResolver = new Resolver({
projectRoot: '/root',
});
const polyfill = createPolyfill('test polyfill', []);
const code = [
'global.fetch = () => 1;',
].join('');
return depResolver.wrapModule({
module: polyfill,
code
}).then(({code: processedCode}) => {
expect(processedCode).toEqual([
'(function(global) {',
'global.fetch = () => 1;',
"\n})(typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : this);",
].join(''));
});
});
describe('JSON files:', () => {
const code = JSON.stringify({arbitrary: "data"});
const id = 'arbitrary.json';
let depResolver, module, resolutionResponse;
beforeEach(() => {
depResolver = new Resolver({projectRoot: '/root'});
module = createJsonModule(id);
resolutionResponse = new ResolutionResponseMock({
dependencies: [module],
mainModuleId: id,
});
});
it('should prefix JSON files with `module.exports=`', () => {
return depResolver
.wrapModule({resolutionResponse, module, name: id, code, dev: false})
.then(({code: processedCode}) =>
expect(processedCode).toEqual([
`__d(/* ${id} */function(global, require, module, exports) {`,
`module.exports = ${code}\n}, ${resolutionResponse.getModuleId(module)});`,
].join('')));
});
});
describe('minification:', () => {
const code ='arbitrary(code)';
const id = 'arbitrary.js';
let depResolver, minifyCode, module, resolutionResponse, sourceMap;
beforeEach(() => {
minifyCode = jest.fn((filename, code, map) =>
Promise.resolve({code, map}));
depResolver = new Resolver({
projectRoot: '/root',
minifyCode,
});
module = createModule(id);
module.path = '/arbitrary/path.js';
resolutionResponse = new ResolutionResponseMock({
dependencies: [module],
mainModuleId: id,
});
sourceMap = {version: 3, sources: ['input'], mappings: 'whatever'};
});
it('should invoke the minifier with the wrapped code', () => {
const wrappedCode =
`__d(/* ${id} */function(global, require, module, exports) {${
code}\n}, ${resolutionResponse.getModuleId(module)});`
return depResolver
.wrapModule({
resolutionResponse,
module,
name: id,
code,
map: sourceMap,
minify: true,
dev: false,
}).then(() => {
expect(minifyCode).toBeCalledWith(module.path, wrappedCode, sourceMap);
});
});
it('should use minified code', () => {
const minifiedCode = 'minified(code)';
const minifiedMap = {version: 3, file: ['minified']};
minifyCode.mockReturnValue(Promise.resolve({code: minifiedCode, map: minifiedMap}));
return depResolver
.wrapModule({resolutionResponse, module, name: id, code, minify: true})
.then(({code, map}) => {
expect(code).toEqual(minifiedCode);
expect(map).toEqual(minifiedMap);
});
});
});
});
function createGetModuleId() {
let nextId = 1;
const knownIds = new Map();
function createId(path) {
const id = nextId;
nextId += 1;
knownIds.set(path, id);
return id;
}
return ({path}) => knownIds.get(path) || createId(path);
}
function padRight(value, width) {
const s = String(value);
const diff = width - s.length;
return diff > 0 ? s + Array(diff + 1).join(' ') : s;
}
});

View File

@ -0,0 +1,289 @@
/**
* 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 DependencyGraph = require('../node-haste');
const defaults = require('../../../defaults');
const pathJoin = require('path').join;
import type ResolutionResponse from '../node-haste/DependencyGraph/ResolutionResponse';
import type Module from '../node-haste/Module';
import type {SourceMap} from '../lib/SourceMap';
import type {Options as TransformOptions} from '../JSTransformer/worker/worker';
import type {Reporter} from '../lib/reporting';
import type {TransformCode} from '../node-haste/Module';
import type Cache from '../node-haste/Cache';
import type GlobalTransformCache from '../lib/GlobalTransformCache';
type MinifyCode = (filePath: string, code: string, map: SourceMap) =>
Promise<{code: string, map: SourceMap}>;
type Options = {
assetExts: Array<string>,
blacklistRE?: RegExp,
cache: Cache,
extraNodeModules?: {},
globalTransformCache: ?GlobalTransformCache,
minifyCode: MinifyCode,
platforms: Array<string>,
polyfillModuleNames?: Array<string>,
projectRoots: Array<string>,
providesModuleNodeModules?: Array<string>,
reporter: Reporter,
resetCache: boolean,
transformCacheKey: string,
transformCode: TransformCode,
watch?: boolean,
};
class Resolver {
_depGraph: DependencyGraph;
_minifyCode: MinifyCode;
_polyfillModuleNames: Array<string>;
constructor(opts: Options) {
this._depGraph = new DependencyGraph({
assetDependencies: ['react-native/Libraries/Image/AssetRegistry'],
assetExts: opts.assetExts,
cache: opts.cache,
extraNodeModules: opts.extraNodeModules,
globalTransformCache: opts.globalTransformCache,
ignoreFilePath: function(filepath) {
return filepath.indexOf('__tests__') !== -1 ||
(opts.blacklistRE != null && opts.blacklistRE.test(filepath));
},
moduleOptions: {
cacheTransformResults: true,
resetCache: opts.resetCache,
},
platforms: opts.platforms,
preferNativePlatform: true,
providesModuleNodeModules: opts.providesModuleNodeModules || defaults.providesModuleNodeModules,
reporter: opts.reporter,
resetCache: opts.resetCache,
roots: opts.projectRoots,
transformCacheKey: opts.transformCacheKey,
transformCode: opts.transformCode,
watch: opts.watch || false,
});
this._minifyCode = opts.minifyCode;
this._polyfillModuleNames = opts.polyfillModuleNames || [];
this._depGraph.load().catch(err => {
console.error(err.message + '\n' + err.stack);
process.exit(1);
});
}
getShallowDependencies(
entryFile: string,
transformOptions: TransformOptions,
): Array<string> {
return this._depGraph.getShallowDependencies(entryFile, transformOptions);
}
getModuleForPath(entryFile: string): Module {
return this._depGraph.getModuleForPath(entryFile);
}
getDependencies(
entryPath: string,
options: {platform: string, recursive?: boolean},
transformOptions: TransformOptions,
onProgress?: ?(finishedModules: number, totalModules: number) => mixed,
getModuleId: mixed,
): Promise<ResolutionResponse> {
const {platform, recursive = true} = options;
return this._depGraph.getDependencies({
entryPath,
platform,
transformOptions,
recursive,
onProgress,
}).then(resolutionResponse => {
this._getPolyfillDependencies().reverse().forEach(
polyfill => resolutionResponse.prependDependency(polyfill)
);
resolutionResponse.getModuleId = getModuleId;
return resolutionResponse.finalize();
});
}
getModuleSystemDependencies({dev = true}: {dev?: boolean}): Array<Module> {
const prelude = dev
? pathJoin(__dirname, 'polyfills/prelude_dev.js')
: pathJoin(__dirname, 'polyfills/prelude.js');
const moduleSystem = defaults.moduleSystem;
return [
prelude,
moduleSystem,
].map(moduleName => this._depGraph.createPolyfill({
file: moduleName,
id: moduleName,
dependencies: [],
}));
}
_getPolyfillDependencies(): Array<Module> {
const polyfillModuleNames = defaults.polyfills.concat(this._polyfillModuleNames);
return polyfillModuleNames.map(
(polyfillModuleName, idx) => this._depGraph.createPolyfill({
file: polyfillModuleName,
id: polyfillModuleName,
dependencies: polyfillModuleNames.slice(0, idx),
})
);
}
resolveRequires(
resolutionResponse: ResolutionResponse,
module: Module,
code: string,
dependencyOffsets: Array<number> = [],
): string {
const resolvedDeps = Object.create(null);
// here, we build a map of all require strings (relative and absolute)
// to the canonical ID of the module they reference
resolutionResponse.getResolvedDependencyPairs(module)
.forEach(([depName, depModule]) => {
if (depModule) {
/* $FlowFixMe: `getModuleId` is monkey-patched so may not exist */
resolvedDeps[depName] = resolutionResponse.getModuleId(depModule);
}
});
// if we have a canonical ID for the module imported here,
// we use it, so that require() is always called with the same
// id for every module.
// Example:
// -- in a/b.js:
// require('./c') => require(3);
// -- in b/index.js:
// require('../a/c') => require(3);
return dependencyOffsets.reduceRight(
([unhandled, handled], offset) => [
unhandled.slice(0, offset),
replaceDependencyID(unhandled.slice(offset) + handled, resolvedDeps),
],
[code, ''],
).join('');
}
wrapModule({
resolutionResponse,
module,
name,
map,
code,
meta = {},
dev = true,
minify = false,
}: {
resolutionResponse: ResolutionResponse,
module: Module,
name: string,
map: SourceMap,
code: string,
meta?: {
dependencyOffsets?: Array<number>,
},
dev?: boolean,
minify?: boolean,
}) {
if (module.isJSON()) {
code = `module.exports = ${code}`;
}
if (module.isPolyfill()) {
code = definePolyfillCode(code);
} else {
/* $FlowFixMe: `getModuleId` is monkey-patched so may not exist */
const moduleId = resolutionResponse.getModuleId(module);
code = this.resolveRequires(
resolutionResponse,
module,
code,
meta.dependencyOffsets
);
code = defineModuleCode(moduleId, code, name, dev);
}
return minify
? this._minifyCode(module.path, code, map)
: Promise.resolve({code, map});
}
minifyModule(
{path, code, map}: {path: string, code: string, map: SourceMap},
): Promise<{code: string, map: SourceMap}> {
return this._minifyCode(path, code, map);
}
getDependencyGraph(): DependencyGraph {
return this._depGraph;
}
}
function defineModuleCode(moduleName, code, verboseName = '', dev = true) {
return [
`__d(/* ${verboseName} */`,
'function(global, require, module, exports) {', // module factory
code,
'\n}, ',
`${JSON.stringify(moduleName)}`, // module id, null = id map. used in ModuleGraph
dev ? `, null, ${JSON.stringify(verboseName)}` : '',
');',
].join('');
}
function definePolyfillCode(code,) {
return [
'(function(global) {',
code,
`\n})(typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : this);`,
].join('');
}
const reDepencencyString = /^(['"])([^'"']*)\1/;
function replaceDependencyID(stringWithDependencyIDAtStart, resolvedDeps) {
const match = reDepencencyString.exec(stringWithDependencyIDAtStart);
const dependencyName = match && match[2];
if (match != null && dependencyName in resolvedDeps) {
const {length} = match[0];
const id = String(resolvedDeps[dependencyName]);
return (
padRight(id, length) +
stringWithDependencyIDAtStart
.slice(length)
.replace(/$/m, ` // ${id} = ${dependencyName}`)
);
} else {
return stringWithDependencyIDAtStart;
}
}
function padRight(string, length) {
return string.length < length
? string + Array(length - string.length + 1).join(' ')
: string;
}
module.exports = Resolver;

View File

@ -0,0 +1,87 @@
/**
* Copyright (c) 2013-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.
*
* @provides Array.es6
* @polyfill
*/
/* eslint-disable */
/**
* Creates an array from array like objects.
*
* https://people.mozilla.org/~jorendorff/es6-draft.html#sec-array.from
*/
if (!Array.from) {
Array.from = function(arrayLike /*, mapFn, thisArg */) {
if (arrayLike == null) {
throw new TypeError('Object is null or undefined');
}
// Optional args.
var mapFn = arguments[1];
var thisArg = arguments[2];
var C = this;
var items = Object(arrayLike);
var symbolIterator = typeof Symbol === 'function'
? Symbol.iterator
: '@@iterator';
var mapping = typeof mapFn === 'function';
var usingIterator = typeof items[symbolIterator] === 'function';
var key = 0;
var ret;
var value;
if (usingIterator) {
ret = typeof C === 'function'
? new C()
: [];
var it = items[symbolIterator]();
var next;
while (!(next = it.next()).done) {
value = next.value;
if (mapping) {
value = mapFn.call(thisArg, value, key);
}
ret[key] = value;
key += 1;
}
ret.length = key;
return ret;
}
var len = items.length;
if (isNaN(len) || len < 0) {
len = 0;
}
ret = typeof C === 'function'
? new C(len)
: new Array(len);
while (key < len) {
value = items[key];
if (mapping) {
value = mapFn.call(thisArg, value, key);
}
ret[key] = value;
key += 1;
}
ret.length = key;
return ret;
};
}

View File

@ -0,0 +1,96 @@
/**
* 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.
*
* @provides Array.prototype.es6
* @polyfill
*/
/* eslint-disable */
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findIndex
function findIndex(predicate, context) {
if (this == null) {
throw new TypeError(
'Array.prototype.findIndex called on null or undefined'
);
}
if (typeof predicate !== 'function') {
throw new TypeError('predicate must be a function');
}
var list = Object(this);
var length = list.length >>> 0;
for (var i = 0; i < length; i++) {
if (predicate.call(context, list[i], i, list)) {
return i;
}
}
return -1;
}
if (!Array.prototype.findIndex) {
Object.defineProperty(Array.prototype, 'findIndex', {
enumerable: false,
writable: true,
configurable: true,
value: findIndex
});
}
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find
if (!Array.prototype.find) {
Object.defineProperty(Array.prototype, 'find', {
enumerable: false,
writable: true,
configurable: true,
value: function(predicate, context) {
if (this == null) {
throw new TypeError(
'Array.prototype.find called on null or undefined'
);
}
var index = findIndex.call(this, predicate, context);
return index === -1 ? undefined : this[index];
}
});
}
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/includes
if (!Array.prototype.includes) {
Object.defineProperty(Array.prototype, 'includes', {
enumerable: false,
writable: true,
configurable: true,
value: function (searchElement) {
var O = Object(this);
var len = parseInt(O.length) || 0;
if (len === 0) {
return false;
}
var n = parseInt(arguments[1]) || 0;
var k;
if (n >= 0) {
k = n;
} else {
k = len + n;
if (k < 0) {
k = 0;
}
}
var currentElement;
while (k < len) {
currentElement = O[k];
if (searchElement === currentElement ||
(searchElement !== searchElement && currentElement !== currentElement)) {
return true;
}
k++;
}
return false;
}
});
}

View File

@ -0,0 +1,41 @@
/**
* 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.
*
* @provides Number.es6
* @polyfill
*/
/* eslint-disable strict */
if (Number.EPSILON === undefined) {
Object.defineProperty(Number, 'EPSILON', {
value: Math.pow(2, -52),
});
}
if (Number.MAX_SAFE_INTEGER === undefined) {
Object.defineProperty(Number, 'MAX_SAFE_INTEGER', {
value: Math.pow(2, 53) - 1,
});
}
if (Number.MIN_SAFE_INTEGER === undefined) {
Object.defineProperty(Number, 'MIN_SAFE_INTEGER', {
value: -(Math.pow(2, 53) - 1),
});
}
if (!Number.isNaN) {
// https://github.com/dherman/tc39-codex-wiki/blob/master/data/es6/number/index.md#polyfill-for-numberisnan
const globalIsNaN = global.isNaN;
Object.defineProperty(Number, 'isNaN', {
configurable: true,
enumerable: false,
value: function isNaN(value) {
return typeof value === 'number' && globalIsNaN(value);
},
writable: true,
});
}

View File

@ -0,0 +1,60 @@
/**
* 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.
*
* @provides Object.es7
* @polyfill
*/
(function() {
'use strict';
const hasOwnProperty = Object.prototype.hasOwnProperty;
/**
* Returns an array of the given object's own enumerable entries.
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/entries
*/
if (typeof Object.entries !== 'function') {
Object.entries = function(object) {
// `null` and `undefined` values are not allowed.
if (object == null) {
throw new TypeError('Object.entries called on non-object');
}
const entries = [];
for (const key in object) {
if (hasOwnProperty.call(object, key)) {
entries.push([key, object[key]]);
}
}
return entries;
};
}
/**
* Returns an array of the given object's own enumerable entries.
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/values
*/
if (typeof Object.values !== 'function') {
Object.values = function(object) {
// `null` and `undefined` values are not allowed.
if (object == null) {
throw new TypeError('Object.values called on non-object');
}
const values = [];
for (const key in object) {
if (hasOwnProperty.call(object, key)) {
values.push(object[key]);
}
}
return values;
};
}
})();

View File

@ -0,0 +1,93 @@
/**
* Copyright (c) 2013-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.
*
* @provides String.prototype.es6
* @polyfill
*/
/* eslint-disable strict, no-extend-native, no-bitwise */
/*
* NOTE: We use (Number(x) || 0) to replace NaN values with zero.
*/
if (!String.prototype.startsWith) {
String.prototype.startsWith = function(search) {
'use strict';
if (this == null) {
throw TypeError();
}
var string = String(this);
var pos = arguments.length > 1 ?
(Number(arguments[1]) || 0) : 0;
var start = Math.min(Math.max(pos, 0), string.length);
return string.indexOf(String(search), pos) === start;
};
}
if (!String.prototype.endsWith) {
String.prototype.endsWith = function(search) {
'use strict';
if (this == null) {
throw TypeError();
}
var string = String(this);
var stringLength = string.length;
var searchString = String(search);
var pos = arguments.length > 1 ?
(Number(arguments[1]) || 0) : stringLength;
var end = Math.min(Math.max(pos, 0), stringLength);
var start = end - searchString.length;
if (start < 0) {
return false;
}
return string.lastIndexOf(searchString, start) === start;
};
}
if (!String.prototype.repeat) {
String.prototype.repeat = function(count) {
'use strict';
if (this == null) {
throw TypeError();
}
var string = String(this);
count = Number(count) || 0;
if (count < 0 || count === Infinity) {
throw RangeError();
}
if (count === 1) {
return string;
}
var result = '';
while (count) {
if (count & 1) {
result += string;
}
if ((count >>= 1)) {
string += string;
}
}
return result;
};
}
if (!String.prototype.includes) {
String.prototype.includes = function(search, start) {
'use strict';
if (typeof start !== 'number') {
start = 0;
}
if (start + search.length > this.length) {
return false;
} else {
return this.indexOf(search, start) !== -1;
}
};
}

View File

@ -0,0 +1,127 @@
/**
* Copyright (c) 2013-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.
*
* @emails oncall+jsinfra
*/
/* eslint-disable fb-www/object-create-only-one-param */
'use strict';
jest.disableAutomock();
describe('Object (ES7)', () => {
beforeEach(() => {
delete Object.entries;
delete Object.values;
jest.resetModules();
require('../Object.es7');
});
describe('Object.entries', () => {
it('should have a length of 1', () => {
expect(Object.entries.length).toBe(1);
});
it('should check for type', () => {
expect(Object.entries.bind(null, null)).toThrow(TypeError(
'Object.entries called on non-object'
));
expect(Object.entries.bind(null, undefined)).toThrow(TypeError(
'Object.entries called on non-object'
));
expect(Object.entries.bind(null, [])).not.toThrow();
expect(Object.entries.bind(null, () => {})).not.toThrow();
expect(Object.entries.bind(null, {})).not.toThrow();
expect(Object.entries.bind(null, 'abc')).not.toThrow();
});
it('should return enumerable entries', () => {
const foo = Object.defineProperties({}, {
x: {value: 10, enumerable: true},
y: {value: 20},
});
expect(Object.entries(foo)).toEqual([['x', 10]]);
const bar = {x: 10, y: 20};
expect(Object.entries(bar)).toEqual([['x', 10], ['y', 20]]);
});
it('should work with proto-less objects', () => {
const foo = Object.create(null, {
x: {value: 10, enumerable: true},
y: {value: 20},
});
expect(Object.entries(foo)).toEqual([['x', 10]]);
});
it('should return only own entries', () => {
const foo = Object.create({z: 30}, {
x: {value: 10, enumerable: true},
y: {value: 20},
});
expect(Object.entries(foo)).toEqual([['x', 10]]);
});
it('should convert to object primitive string', () => {
expect(Object.entries('ab')).toEqual([['0', 'a'], ['1', 'b']]);
});
});
describe('Object.values', () => {
it('should have a length of 1', () => {
expect(Object.values.length).toBe(1);
});
it('should check for type', () => {
expect(Object.values.bind(null, null)).toThrow(TypeError(
'Object.values called on non-object'
));
expect(Object.values.bind(null, [])).not.toThrow();
expect(Object.values.bind(null, () => {})).not.toThrow();
expect(Object.values.bind(null, {})).not.toThrow();
});
it('should return enumerable values', () => {
const foo = Object.defineProperties({}, {
x: {value: 10, enumerable: true},
y: {value: 20},
});
expect(Object.values(foo)).toEqual([10]);
const bar = {x: 10, y: 20};
expect(Object.values(bar)).toEqual([10, 20]);
});
it('should work with proto-less objects', () => {
const foo = Object.create(null, {
x: {value: 10, enumerable: true},
y: {value: 20},
});
expect(Object.values(foo)).toEqual([10]);
});
it('should return only own values', () => {
const foo = Object.create({z: 30}, {
x: {value: 10, enumerable: true},
y: {value: 20},
});
expect(Object.values(foo)).toEqual([10]);
});
it('should convert to object primitive string', () => {
expect(Object.values('ab')).toEqual(['a', 'b']);
});
});
});

View File

@ -0,0 +1,237 @@
/**
* 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.
*
* @polyfill
*/
/* eslint-disable */
// Created by running:
// require('babel-core').buildExternalHelpers('_extends classCallCheck createClass createRawReactElement defineProperty get inherits interopRequireDefault interopRequireWildcard objectWithoutProperties possibleConstructorReturn slicedToArray taggedTemplateLiteral toArray toConsumableArray '.split(' '))
// then replacing the `global` reference in the last line to also use `this`.
//
// actually, that's a lie, because babel6 omits _extends and createRawReactElement
var babelHelpers = global.babelHelpers = {};
babelHelpers.typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) {
return typeof obj;
} : function (obj) {
return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
};
babelHelpers.createRawReactElement = (function () {
var REACT_ELEMENT_TYPE = typeof Symbol === "function" && Symbol.for && Symbol.for("react.element") || 0xeac7;
return function createRawReactElement(type, key, props) {
return {
$$typeof: REACT_ELEMENT_TYPE,
type: type,
key: key,
ref: null,
props: props,
_owner: null
};
};
})();
babelHelpers.classCallCheck = function (instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
};
babelHelpers.createClass = (function () {
function defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor) descriptor.writable = true;
Object.defineProperty(target, descriptor.key, descriptor);
}
}
return function (Constructor, protoProps, staticProps) {
if (protoProps) defineProperties(Constructor.prototype, protoProps);
if (staticProps) defineProperties(Constructor, staticProps);
return Constructor;
};
})();
babelHelpers.defineProperty = function (obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, {
value: value,
enumerable: true,
configurable: true,
writable: true
});
} else {
obj[key] = value;
}
return obj;
};
babelHelpers._extends = babelHelpers.extends = Object.assign || function (target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i];
for (var key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = source[key];
}
}
}
return target;
};
babelHelpers.get = function get(object, property, receiver) {
if (object === null) object = Function.prototype;
var desc = Object.getOwnPropertyDescriptor(object, property);
if (desc === undefined) {
var parent = Object.getPrototypeOf(object);
if (parent === null) {
return undefined;
} else {
return get(parent, property, receiver);
}
} else if ("value" in desc) {
return desc.value;
} else {
var getter = desc.get;
if (getter === undefined) {
return undefined;
}
return getter.call(receiver);
}
};
babelHelpers.inherits = function (subClass, superClass) {
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function, not " + typeof superClass);
}
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
enumerable: false,
writable: true,
configurable: true
}
});
if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
};
babelHelpers.interopRequireDefault = function (obj) {
return obj && obj.__esModule ? obj : {
default: obj
};
};
babelHelpers.interopRequireWildcard = function (obj) {
if (obj && obj.__esModule) {
return obj;
} else {
var newObj = {};
if (obj != null) {
for (var key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key];
}
}
newObj.default = obj;
return newObj;
}
};
babelHelpers.objectWithoutProperties = function (obj, keys) {
var target = {};
for (var i in obj) {
if (keys.indexOf(i) >= 0) continue;
if (!Object.prototype.hasOwnProperty.call(obj, i)) continue;
target[i] = obj[i];
}
return target;
};
babelHelpers.possibleConstructorReturn = function (self, call) {
if (!self) {
throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
}
return call && (typeof call === "object" || typeof call === "function") ? call : self;
};
babelHelpers.slicedToArray = (function () {
function sliceIterator(arr, i) {
var _arr = [];
var _n = true;
var _d = false;
var _e = undefined;
try {
for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) {
_arr.push(_s.value);
if (i && _arr.length === i) break;
}
} catch (err) {
_d = true;
_e = err;
} finally {
try {
if (!_n && _i["return"]) _i["return"]();
} finally {
if (_d) throw _e;
}
}
return _arr;
}
return function (arr, i) {
if (Array.isArray(arr)) {
return arr;
} else if (Symbol.iterator in Object(arr)) {
return sliceIterator(arr, i);
} else {
throw new TypeError("Invalid attempt to destructure non-iterable instance");
}
};
})();
babelHelpers.taggedTemplateLiteral = function (strings, raw) {
return Object.freeze(Object.defineProperties(strings, {
raw: {
value: Object.freeze(raw)
}
}));
};
babelHelpers.toArray = function (arr) {
return Array.isArray(arr) ? arr : Array.from(arr);
};
babelHelpers.toConsumableArray = function (arr) {
if (Array.isArray(arr)) {
for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) arr2[i] = arr[i];
return arr2;
} else {
return Array.from(arr);
}
};

View File

@ -0,0 +1,515 @@
/**
* 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.
*
* @provides console
* @polyfill
* @nolint
*/
/* eslint-disable */
/**
* This pipes all of our console logging functions to native logging so that
* JavaScript errors in required modules show up in Xcode via NSLog.
*/
const inspect = (function() {
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
//
// https://github.com/joyent/node/blob/master/lib/util.js
function inspect(obj, opts) {
var ctx = {
seen: [],
stylize: stylizeNoColor
};
return formatValue(ctx, obj, opts.depth);
}
function stylizeNoColor(str, styleType) {
return str;
}
function arrayToHash(array) {
var hash = {};
array.forEach(function(val, idx) {
hash[val] = true;
});
return hash;
}
function formatValue(ctx, value, recurseTimes) {
// Primitive types cannot have properties
var primitive = formatPrimitive(ctx, value);
if (primitive) {
return primitive;
}
// Look up the keys of the object.
var keys = Object.keys(value);
var visibleKeys = arrayToHash(keys);
// IE doesn't make error fields non-enumerable
// http://msdn.microsoft.com/en-us/library/ie/dww52sbt(v=vs.94).aspx
if (isError(value)
&& (keys.indexOf('message') >= 0 || keys.indexOf('description') >= 0)) {
return formatError(value);
}
// Some type of object without properties can be shortcutted.
if (keys.length === 0) {
if (isFunction(value)) {
var name = value.name ? ': ' + value.name : '';
return ctx.stylize('[Function' + name + ']', 'special');
}
if (isRegExp(value)) {
return ctx.stylize(RegExp.prototype.toString.call(value), 'regexp');
}
if (isDate(value)) {
return ctx.stylize(Date.prototype.toString.call(value), 'date');
}
if (isError(value)) {
return formatError(value);
}
}
var base = '', array = false, braces = ['{', '}'];
// Make Array say that they are Array
if (isArray(value)) {
array = true;
braces = ['[', ']'];
}
// Make functions say that they are functions
if (isFunction(value)) {
var n = value.name ? ': ' + value.name : '';
base = ' [Function' + n + ']';
}
// Make RegExps say that they are RegExps
if (isRegExp(value)) {
base = ' ' + RegExp.prototype.toString.call(value);
}
// Make dates with properties first say the date
if (isDate(value)) {
base = ' ' + Date.prototype.toUTCString.call(value);
}
// Make error with message first say the error
if (isError(value)) {
base = ' ' + formatError(value);
}
if (keys.length === 0 && (!array || value.length == 0)) {
return braces[0] + base + braces[1];
}
if (recurseTimes < 0) {
if (isRegExp(value)) {
return ctx.stylize(RegExp.prototype.toString.call(value), 'regexp');
} else {
return ctx.stylize('[Object]', 'special');
}
}
ctx.seen.push(value);
var output;
if (array) {
output = formatArray(ctx, value, recurseTimes, visibleKeys, keys);
} else {
output = keys.map(function(key) {
return formatProperty(ctx, value, recurseTimes, visibleKeys, key, array);
});
}
ctx.seen.pop();
return reduceToSingleString(output, base, braces);
}
function formatPrimitive(ctx, value) {
if (isUndefined(value))
return ctx.stylize('undefined', 'undefined');
if (isString(value)) {
var simple = '\'' + JSON.stringify(value).replace(/^"|"$/g, '')
.replace(/'/g, "\\'")
.replace(/\\"/g, '"') + '\'';
return ctx.stylize(simple, 'string');
}
if (isNumber(value))
return ctx.stylize('' + value, 'number');
if (isBoolean(value))
return ctx.stylize('' + value, 'boolean');
// For some reason typeof null is "object", so special case here.
if (isNull(value))
return ctx.stylize('null', 'null');
}
function formatError(value) {
return '[' + Error.prototype.toString.call(value) + ']';
}
function formatArray(ctx, value, recurseTimes, visibleKeys, keys) {
var output = [];
for (var i = 0, l = value.length; i < l; ++i) {
if (hasOwnProperty(value, String(i))) {
output.push(formatProperty(ctx, value, recurseTimes, visibleKeys,
String(i), true));
} else {
output.push('');
}
}
keys.forEach(function(key) {
if (!key.match(/^\d+$/)) {
output.push(formatProperty(ctx, value, recurseTimes, visibleKeys,
key, true));
}
});
return output;
}
function formatProperty(ctx, value, recurseTimes, visibleKeys, key, array) {
var name, str, desc;
desc = Object.getOwnPropertyDescriptor(value, key) || { value: value[key] };
if (desc.get) {
if (desc.set) {
str = ctx.stylize('[Getter/Setter]', 'special');
} else {
str = ctx.stylize('[Getter]', 'special');
}
} else {
if (desc.set) {
str = ctx.stylize('[Setter]', 'special');
}
}
if (!hasOwnProperty(visibleKeys, key)) {
name = '[' + key + ']';
}
if (!str) {
if (ctx.seen.indexOf(desc.value) < 0) {
if (isNull(recurseTimes)) {
str = formatValue(ctx, desc.value, null);
} else {
str = formatValue(ctx, desc.value, recurseTimes - 1);
}
if (str.indexOf('\n') > -1) {
if (array) {
str = str.split('\n').map(function(line) {
return ' ' + line;
}).join('\n').substr(2);
} else {
str = '\n' + str.split('\n').map(function(line) {
return ' ' + line;
}).join('\n');
}
}
} else {
str = ctx.stylize('[Circular]', 'special');
}
}
if (isUndefined(name)) {
if (array && key.match(/^\d+$/)) {
return str;
}
name = JSON.stringify('' + key);
if (name.match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)) {
name = name.substr(1, name.length - 2);
name = ctx.stylize(name, 'name');
} else {
name = name.replace(/'/g, "\\'")
.replace(/\\"/g, '"')
.replace(/(^"|"$)/g, "'");
name = ctx.stylize(name, 'string');
}
}
return name + ': ' + str;
}
function reduceToSingleString(output, base, braces) {
var numLinesEst = 0;
var length = output.reduce(function(prev, cur) {
numLinesEst++;
if (cur.indexOf('\n') >= 0) numLinesEst++;
return prev + cur.replace(/\u001b\[\d\d?m/g, '').length + 1;
}, 0);
if (length > 60) {
return braces[0] +
(base === '' ? '' : base + '\n ') +
' ' +
output.join(',\n ') +
' ' +
braces[1];
}
return braces[0] + base + ' ' + output.join(', ') + ' ' + braces[1];
}
// NOTE: These type checking functions intentionally don't use `instanceof`
// because it is fragile and can be easily faked with `Object.create()`.
function isArray(ar) {
return Array.isArray(ar);
}
function isBoolean(arg) {
return typeof arg === 'boolean';
}
function isNull(arg) {
return arg === null;
}
function isNullOrUndefined(arg) {
return arg == null;
}
function isNumber(arg) {
return typeof arg === 'number';
}
function isString(arg) {
return typeof arg === 'string';
}
function isSymbol(arg) {
return typeof arg === 'symbol';
}
function isUndefined(arg) {
return arg === void 0;
}
function isRegExp(re) {
return isObject(re) && objectToString(re) === '[object RegExp]';
}
function isObject(arg) {
return typeof arg === 'object' && arg !== null;
}
function isDate(d) {
return isObject(d) && objectToString(d) === '[object Date]';
}
function isError(e) {
return isObject(e) &&
(objectToString(e) === '[object Error]' || e instanceof Error);
}
function isFunction(arg) {
return typeof arg === 'function';
}
function isPrimitive(arg) {
return arg === null ||
typeof arg === 'boolean' ||
typeof arg === 'number' ||
typeof arg === 'string' ||
typeof arg === 'symbol' || // ES6 symbol
typeof arg === 'undefined';
}
function objectToString(o) {
return Object.prototype.toString.call(o);
}
function hasOwnProperty(obj, prop) {
return Object.prototype.hasOwnProperty.call(obj, prop);
}
return inspect;
})();
const OBJECT_COLUMN_NAME = '(index)';
const LOG_LEVELS = {
trace: 0,
info: 1,
warn: 2,
error: 3
};
const INSPECTOR_LEVELS = [];
INSPECTOR_LEVELS[LOG_LEVELS.trace] = 'debug';
INSPECTOR_LEVELS[LOG_LEVELS.info] = 'log';
INSPECTOR_LEVELS[LOG_LEVELS.warn] = 'warning';
INSPECTOR_LEVELS[LOG_LEVELS.error] = 'error';
// Strip the inner function in getNativeLogFunction(), if in dev also
// strip method printing to originalConsole.
const INSPECTOR_FRAMES_TO_SKIP = __DEV__ ? 2 : 1;
function setupConsole(global) {
if (!global.nativeLoggingHook) {
return;
}
function getNativeLogFunction(level) {
return function() {
let str;
if (arguments.length === 1 && typeof arguments[0] === 'string') {
str = arguments[0];
} else {
str = Array.prototype.map.call(arguments, function(arg) {
return inspect(arg, {depth: 10});
}).join(', ');
}
let logLevel = level;
if (str.slice(0, 9) === 'Warning: ' && logLevel >= LOG_LEVELS.error) {
// React warnings use console.error so that a stack trace is shown,
// but we don't (currently) want these to show a redbox
// (Note: Logic duplicated in ExceptionsManager.js.)
logLevel = LOG_LEVELS.warn;
}
if (global.__inspectorLog) {
global.__inspectorLog(
INSPECTOR_LEVELS[logLevel],
str,
[].slice.call(arguments),
INSPECTOR_FRAMES_TO_SKIP);
}
global.nativeLoggingHook(str, logLevel);
};
}
function repeat(element, n) {
return Array.apply(null, Array(n)).map(function() { return element; });
};
function consoleTablePolyfill(rows) {
// convert object -> array
if (!Array.isArray(rows)) {
var data = rows;
rows = [];
for (var key in data) {
if (data.hasOwnProperty(key)) {
var row = data[key];
row[OBJECT_COLUMN_NAME] = key;
rows.push(row);
}
}
}
if (rows.length === 0) {
global.nativeLoggingHook('', LOG_LEVELS.info);
return;
}
var columns = Object.keys(rows[0]).sort();
var stringRows = [];
var columnWidths = [];
// Convert each cell to a string. Also
// figure out max cell width for each column
columns.forEach(function(k, i) {
columnWidths[i] = k.length;
for (var j = 0; j < rows.length; j++) {
var cellStr = (rows[j][k] || '?').toString();
stringRows[j] = stringRows[j] || [];
stringRows[j][i] = cellStr;
columnWidths[i] = Math.max(columnWidths[i], cellStr.length);
}
});
// Join all elements in the row into a single string with | separators
// (appends extra spaces to each cell to make separators | alligned)
function joinRow(row, space) {
var cells = row.map(function(cell, i) {
var extraSpaces = repeat(' ', columnWidths[i] - cell.length).join('');
return cell + extraSpaces;
});
space = space || ' ';
return cells.join(space + '|' + space);
};
var separators = columnWidths.map(function(columnWidth) {
return repeat('-', columnWidth).join('');
});
var separatorRow = joinRow(separators, '-');
var header = joinRow(columns);
var table = [header, separatorRow];
for (var i = 0; i < rows.length; i++) {
table.push(joinRow(stringRows[i]));
}
// Notice extra empty line at the beginning.
// Native logging hook adds "RCTLog >" at the front of every
// logged string, which would shift the header and screw up
// the table
global.nativeLoggingHook('\n' + table.join('\n'), LOG_LEVELS.info);
}
// Preserve the original `console` as `originalConsole`
var originalConsole = global.console;
var descriptor = Object.getOwnPropertyDescriptor(global, 'console');
if (descriptor) {
Object.defineProperty(global, 'originalConsole', descriptor);
}
global.console = {
error: getNativeLogFunction(LOG_LEVELS.error),
info: getNativeLogFunction(LOG_LEVELS.info),
log: getNativeLogFunction(LOG_LEVELS.info),
warn: getNativeLogFunction(LOG_LEVELS.warn),
trace: getNativeLogFunction(LOG_LEVELS.trace),
debug: getNativeLogFunction(LOG_LEVELS.trace),
table: consoleTablePolyfill
};
// If available, also call the original `console` method since that is
// sometimes useful. Ex: on OS X, this will let you see rich output in
// the Safari Web Inspector console.
if (__DEV__ && originalConsole) {
Object.keys(console).forEach(methodName => {
var reactNativeMethod = console[methodName];
if (originalConsole[methodName]) {
console[methodName] = function() {
originalConsole[methodName](...arguments);
reactNativeMethod.apply(console, arguments);
};
}
});
}
}
if (typeof module !== 'undefined') {
module.exports = setupConsole;
} else {
setupConsole(global);
}

View File

@ -0,0 +1,88 @@
/**
* 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.
*
* @polyfill
*/
/* eslint-disable strict */
let _inGuard = 0;
/**
* This is the error handler that is called when we encounter an exception
* when loading a module. This will report any errors encountered before
* ExceptionsManager is configured.
*/
let _globalHandler = function onError(e) {
throw e;
};
/**
* The particular require runtime that we are using looks for a global
* `ErrorUtils` object and if it exists, then it requires modules with the
* error handler specified via ErrorUtils.setGlobalHandler by calling the
* require function with applyWithGuard. Since the require module is loaded
* before any of the modules, this ErrorUtils must be defined (and the handler
* set) globally before requiring anything.
*/
const ErrorUtils = {
setGlobalHandler: function(fun) {
_globalHandler = fun;
},
getGlobalHandler: function() {
return _globalHandler;
},
reportError: function(error) {
_globalHandler && _globalHandler(error);
},
reportFatalError: function(error) {
_globalHandler && _globalHandler(error, true);
},
applyWithGuard: function(fun, context, args) {
try {
_inGuard++;
return fun.apply(context, args);
} catch (e) {
ErrorUtils.reportError(e);
} finally {
_inGuard--;
}
},
applyWithGuardIfNeeded: function(fun, context, args) {
if (ErrorUtils.inGuard()) {
return fun.apply(context, args);
} else {
ErrorUtils.applyWithGuard(fun, context, args);
}
},
inGuard: function() {
return _inGuard;
},
guard: function(fun, name, context) {
if (typeof fun !== 'function') {
console.warn('A function must be passed to ErrorUtils.guard, got ', fun);
return null;
}
name = name || fun.name || '<generated guard>';
function guarded() {
return (
ErrorUtils.applyWithGuard(
fun,
context || this,
arguments,
null,
name
)
);
}
return guarded;
}
};
global.ErrorUtils = ErrorUtils;

View File

@ -0,0 +1,69 @@
/**
* 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.
*
* @provides Object.es6
* @polyfill
*/
/* eslint-disable strict */
// WARNING: This is an optimized version that fails on hasOwnProperty checks
// and non objects. It's not spec-compliant. It's a perf optimization.
// This is only needed for iOS 8 and current Android JSC.
Object.assign = function(target, sources) {
if (__DEV__) {
if (target == null) {
throw new TypeError('Object.assign target cannot be null or undefined');
}
if (typeof target !== 'object' && typeof target !== 'function') {
throw new TypeError(
'In this environment the target of assign MUST be an object.' +
'This error is a performance optimization and not spec compliant.'
);
}
}
for (var nextIndex = 1; nextIndex < arguments.length; nextIndex++) {
var nextSource = arguments[nextIndex];
if (nextSource == null) {
continue;
}
if (__DEV__) {
if (typeof nextSource !== 'object' &&
typeof nextSource !== 'function') {
throw new TypeError(
'In this environment the sources for assign MUST be an object.' +
'This error is a performance optimization and not spec compliant.'
);
}
}
// We don't currently support accessors nor proxies. Therefore this
// copy cannot throw. If we ever supported this then we must handle
// exceptions and side-effects.
for (var key in nextSource) {
if (__DEV__) {
var hasOwnProperty = Object.prototype.hasOwnProperty;
if (!hasOwnProperty.call(nextSource, key)) {
throw new TypeError(
'One of the sources for assign has an enumerable key on the ' +
'prototype chain. Are you trying to assign a prototype property? ' +
'We don\'t allow it, as this is an edge case that we do not support. ' +
'This error is a performance optimization and not spec compliant.'
);
}
}
target[key] = nextSource[key];
}
}
return target;
};

View File

@ -0,0 +1,16 @@
/**
* Copyright (c) 2013-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.
*
* @polyfill
*/
/* eslint-disable strict */
global.__DEV__ = false;
global.__BUNDLE_START_TIME__ = Date.now();

View File

@ -0,0 +1,16 @@
/**
* Copyright (c) 2013-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.
*
* @polyfill
*/
/* eslint-disable strict */
global.__DEV__ = true;
global.__BUNDLE_START_TIME__ = Date.now();

View File

@ -0,0 +1,285 @@
/**
* Copyright (c) 2013-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.
*
* @polyfill
* @flow
*/
'use strict';
type DependencyMap = Array<ModuleID>;
type Exports = any;
type FactoryFn = (
global: Object,
require: RequireFn,
moduleObject: {exports: {}},
exports: {},
dependencyMap: ?DependencyMap,
) => void;
type HotModuleReloadingAcceptFn = Function;
type HotModuleReloadingData = {|
acceptCallback: ?HotModuleReloadingAcceptFn,
accept: (callback: HotModuleReloadingAcceptFn) => void,
|};
type Module = {
exports: Exports,
hot?: HotModuleReloadingData,
};
type ModuleID = number;
type ModuleDefinition = {|
dependencyMap: ?DependencyMap,
exports: Exports,
factory: FactoryFn,
hasError: boolean,
hot?: HotModuleReloadingData,
isInitialized: boolean,
verboseName?: string,
|};
type ModuleMap =
{[key: ModuleID]: (ModuleDefinition)};
type RequireFn = (id: ModuleID | VerboseModuleNameForDev) => Exports;
type VerboseModuleNameForDev = string;
global.require = require;
global.__d = define;
const modules: ModuleMap = Object.create(null);
if (__DEV__) {
var verboseNamesToModuleIds: {[key: string]: number} = Object.create(null);
}
function define(
factory: FactoryFn,
moduleId: number,
dependencyMap?: DependencyMap,
) {
if (moduleId in modules) {
// prevent repeated calls to `global.nativeRequire` to overwrite modules
// that are already loaded
return;
}
modules[moduleId] = {
dependencyMap,
exports: undefined,
factory,
hasError: false,
isInitialized: false,
};
if (__DEV__) {
// HMR
modules[moduleId].hot = createHotReloadingObject();
// DEBUGGABLE MODULES NAMES
// we take `verboseName` from `arguments` to avoid an unused named parameter
// in `define` in production.
const verboseName: string | void = arguments[3];
if (verboseName) {
modules[moduleId].verboseName = verboseName;
verboseNamesToModuleIds[verboseName] = moduleId;
}
}
}
function require(moduleId: ModuleID | VerboseModuleNameForDev) {
if (__DEV__ && typeof moduleId === 'string') {
const verboseName = moduleId;
moduleId = verboseNamesToModuleIds[moduleId];
if (moduleId == null) {
throw new Error(`Unknown named module: '${verboseName}'`);
} else {
console.warn(
`Requiring module '${verboseName}' by name is only supported for ` +
'debugging purposes and will BREAK IN PRODUCTION!'
);
}
}
//$FlowFixMe: at this point we know that moduleId is a number
const moduleIdReallyIsNumber: number = moduleId;
const module = modules[moduleIdReallyIsNumber];
return module && module.isInitialized
? module.exports
: guardedLoadModule(moduleIdReallyIsNumber, module);
}
let inGuard = false;
function guardedLoadModule(moduleId: ModuleID , module) {
if (!inGuard && global.ErrorUtils) {
inGuard = true;
let returnValue;
try {
returnValue = loadModuleImplementation(moduleId, module);
} catch (e) {
global.ErrorUtils.reportFatalError(e);
}
inGuard = false;
return returnValue;
} else {
return loadModuleImplementation(moduleId, module);
}
}
function loadModuleImplementation(moduleId, module) {
const nativeRequire = global.nativeRequire;
if (!module && nativeRequire) {
nativeRequire(moduleId);
module = modules[moduleId];
}
if (!module) {
throw unknownModuleError(moduleId);
}
if (module.hasError) {
throw moduleThrewError(moduleId);
}
// `require` calls int the require polyfill itself are not analyzed and
// replaced so that they use numeric module IDs.
// The systrace module will expose itself on the require function so that
// it can be used here.
// TODO(davidaurelio) Scan polyfills for dependencies, too (t9759686)
if (__DEV__) {
var {Systrace} = require;
}
// We must optimistically mark module as initialized before running the
// factory to keep any require cycles inside the factory from causing an
// infinite require loop.
module.isInitialized = true;
const exports = module.exports = {};
const {factory, dependencyMap} = module;
try {
if (__DEV__) {
// $FlowFixMe: we know that __DEV__ is const and `Systrace` exists
Systrace.beginEvent('JS_require_' + (module.verboseName || moduleId));
}
const moduleObject: Module = {exports};
if (__DEV__ && module.hot) {
moduleObject.hot = module.hot;
}
// keep args in sync with with defineModuleCode in
// packager/react-packager/src/Resolver/index.js
// and packager/react-packager/src/ModuleGraph/worker.js
factory(global, require, moduleObject, exports, dependencyMap);
// avoid removing factory in DEV mode as it breaks HMR
if (!__DEV__) {
// $FlowFixMe: This is only sound because we never access `factory` again
module.factory = undefined;
}
if (__DEV__) {
// $FlowFixMe: we know that __DEV__ is const and `Systrace` exists
Systrace.endEvent();
}
return (module.exports = moduleObject.exports);
} catch (e) {
module.hasError = true;
module.isInitialized = false;
module.exports = undefined;
throw e;
}
}
function unknownModuleError(id) {
let message = 'Requiring unknown module "' + id + '".';
if (__DEV__) {
message +=
'If you are sure the module is there, try restarting the packager or running "npm install".';
}
return Error(message);
}
function moduleThrewError(id) {
return Error('Requiring module "' + id + '", which threw an exception.');
}
if (__DEV__) {
require.Systrace = { beginEvent: () => {}, endEvent: () => {} };
// HOT MODULE RELOADING
var createHotReloadingObject = function() {
const hot: HotModuleReloadingData = {
acceptCallback: null,
accept: callback => { hot.acceptCallback = callback; },
};
return hot;
};
const acceptAll = function(
dependentModules,
inverseDependencies,
) {
if (!dependentModules || dependentModules.length === 0) {
return true;
}
const notAccepted = dependentModules.filter(
module => !accept(module, /*factory*/ undefined, inverseDependencies));
const parents = [];
for (let i = 0; i < notAccepted.length; i++) {
// if the module has no parents then the change cannot be hot loaded
if (inverseDependencies[notAccepted[i]].length === 0) {
return false;
}
parents.push(...inverseDependencies[notAccepted[i]]);
}
return acceptAll(parents, inverseDependencies);
};
const accept = function(
id: ModuleID,
factory?: FactoryFn,
inverseDependencies: {[key: ModuleID]: Array<ModuleID>},
) {
const mod = modules[id];
if (!mod && factory) { // new modules need a factory
define(factory, id);
return true; // new modules don't need to be accepted
}
const {hot} = mod;
if (!hot) {
console.warn(
'Cannot accept module because Hot Module Replacement ' +
'API was not installed.'
);
return false;
}
// replace and initialize factory
if (factory) {
mod.factory = factory;
}
mod.hasError = false;
mod.isInitialized = false;
require(id);
if (hot.acceptCallback) {
hot.acceptCallback();
return true;
} else {
// need to have inverseDependencies to bubble up accept
if (!inverseDependencies) {
throw new Error('Undefined `inverseDependencies`');
}
// accept parent modules recursively up until all siblings are accepted
return acceptAll(inverseDependencies[id], inverseDependencies);
}
};
global.__accept = accept;
}

View File

@ -0,0 +1,84 @@
/**
* 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.
*/
'use strict';
const CRLF = '\r\n';
const BOUNDARY = '3beqjf3apnqeu3h5jqorms4i';
class MultipartResponse {
static wrap(req, res) {
if (acceptsMultipartResponse(req)) {
return new MultipartResponse(res);
}
// Ugly hack, ideally wrap function should always return a proxy
// object with the same interface
res.writeChunk = () => {}; // noop
return res;
}
constructor(res) {
this.res = res;
this.headers = {};
res.writeHead(200, {
'Content-Type': `multipart/mixed; boundary="${BOUNDARY}"`,
});
res.write('If you are seeing this, your client does not support multipart response');
}
writeChunk(headers, data, isLast = false) {
let chunk = `${CRLF}--${BOUNDARY}${CRLF}`;
if (headers) {
chunk += MultipartResponse.serializeHeaders(headers) + CRLF + CRLF;
}
if (data) {
chunk += data;
}
if (isLast) {
chunk += `${CRLF}--${BOUNDARY}--${CRLF}`;
}
this.res.write(chunk);
}
writeHead(status, headers) {
// We can't actually change the response HTTP status code
// because the headers have already been sent
this.setHeader('X-Http-Status', status);
if (!headers) {
return;
}
for (const key in headers) {
this.setHeader(key, headers[key]);
}
}
setHeader(name, value) {
this.headers[name] = value;
}
end(data) {
this.writeChunk(this.headers, data, true);
this.res.end();
}
static serializeHeaders(headers) {
return Object.keys(headers)
.map((key) => `${key}: ${headers[key]}`)
.join(CRLF);
}
}
function acceptsMultipartResponse(req) {
return req.headers && req.headers.accept === 'multipart/mixed';
}
module.exports = MultipartResponse;

View File

@ -0,0 +1,150 @@
/**
* 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.
*/
'use strict';
jest.dontMock('../MultipartResponse');
const MultipartResponse = require('../MultipartResponse');
describe('MultipartResponse', () => {
it('forwards calls to response', () => {
const nreq = mockNodeRequest({accept: 'text/html'});
const nres = mockNodeResponse();
const res = MultipartResponse.wrap(nreq, nres);
expect(res).toBe(nres);
res.writeChunk({}, 'foo');
expect(nres.write).not.toBeCalled();
});
it('writes multipart response', () => {
const nreq = mockNodeRequest({accept: 'multipart/mixed'});
const nres = mockNodeResponse();
const res = MultipartResponse.wrap(nreq, nres);
expect(res).not.toBe(nres);
res.setHeader('Result-Header-1', 1);
res.writeChunk({foo: 'bar'}, 'first chunk');
res.writeChunk({test: 2}, 'second chunk');
res.writeChunk(null, 'empty headers third chunk');
res.setHeader('Result-Header-2', 2);
res.end('Hello, world!');
expect(nres.toString()).toEqual([
'HTTP/1.1 200',
'Content-Type: multipart/mixed; boundary="3beqjf3apnqeu3h5jqorms4i"',
'',
'If you are seeing this, your client does not support multipart response',
'--3beqjf3apnqeu3h5jqorms4i',
'foo: bar',
'',
'first chunk',
'--3beqjf3apnqeu3h5jqorms4i',
'test: 2',
'',
'second chunk',
'--3beqjf3apnqeu3h5jqorms4i',
'empty headers third chunk',
'--3beqjf3apnqeu3h5jqorms4i',
'Result-Header-1: 1',
'Result-Header-2: 2',
'',
'Hello, world!',
'--3beqjf3apnqeu3h5jqorms4i--',
'',
].join('\r\n'));
});
it('sends status code as last chunk header', () => {
const nreq = mockNodeRequest({accept: 'multipart/mixed'});
const nres = mockNodeResponse();
const res = MultipartResponse.wrap(nreq, nres);
res.writeChunk({foo: 'bar'}, 'first chunk');
res.writeHead(500, {
'Content-Type': 'application/json; boundary="3beqjf3apnqeu3h5jqorms4i"',
});
res.end('{}');
expect(nres.toString()).toEqual([
'HTTP/1.1 200',
'Content-Type: multipart/mixed; boundary="3beqjf3apnqeu3h5jqorms4i"',
'',
'If you are seeing this, your client does not support multipart response',
'--3beqjf3apnqeu3h5jqorms4i',
'foo: bar',
'',
'first chunk',
'--3beqjf3apnqeu3h5jqorms4i',
'X-Http-Status: 500',
'Content-Type: application/json; boundary="3beqjf3apnqeu3h5jqorms4i"',
'',
'{}',
'--3beqjf3apnqeu3h5jqorms4i--',
'',
].join('\r\n'));
});
it('supports empty responses', () => {
const nreq = mockNodeRequest({accept: 'multipart/mixed'});
const nres = mockNodeResponse();
const res = MultipartResponse.wrap(nreq, nres);
res.writeHead(304, {
'Content-Type': 'application/json; boundary="3beqjf3apnqeu3h5jqorms4i"',
});
res.end();
expect(nres.toString()).toEqual([
'HTTP/1.1 200',
'Content-Type: multipart/mixed; boundary="3beqjf3apnqeu3h5jqorms4i"',
'',
'If you are seeing this, your client does not support multipart response',
'--3beqjf3apnqeu3h5jqorms4i',
'X-Http-Status: 304',
'Content-Type: application/json; boundary="3beqjf3apnqeu3h5jqorms4i"',
'',
'',
'--3beqjf3apnqeu3h5jqorms4i--',
'',
].join('\r\n'));
});
});
function mockNodeRequest(headers = {}) {
return {headers};
}
function mockNodeResponse() {
let status = 200;
let headers = {};
let body = '';
return {
writeHead: jest.fn((st, hdrs) => {
status = st;
headers = {...headers, ...hdrs};
}),
setHeader: jest.fn((key, val) => { headers[key] = val; }),
write: jest.fn((data) => { body += data; }),
end: jest.fn((data) => { body += (data || ''); }),
// For testing only
toString() {
return [
`HTTP/1.1 ${status}`,
MultipartResponse.serializeHeaders(headers),
'',
body,
].join('\r\n');
}
};
}

View File

@ -0,0 +1,545 @@
/**
* 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.
*/
'use strict';
jest.disableAutomock();
jest.setMock('worker-farm', function() { return () => {}; })
.setMock('timers', { setImmediate: (fn) => setTimeout(fn, 0) })
.setMock('uglify-js')
.setMock('crypto')
.setMock('source-map', { SourceMapConsumer: function(fn) {}})
.mock('../../Bundler')
.mock('../../AssetServer')
.mock('../../lib/declareOpts')
.mock('../../node-haste')
.mock('../../Logger')
.mock('../../lib/GlobalTransformCache');
describe('processRequest', () => {
let SourceMapConsumer, Bundler, Server, AssetServer, Promise;
beforeEach(() => {
jest.resetModules();
SourceMapConsumer = require('source-map').SourceMapConsumer;
Bundler = require('../../Bundler');
Server = require('../');
AssetServer = require('../../AssetServer');
Promise = require('promise');
});
let server;
const options = {
projectRoots: ['root'],
blacklistRE: null,
cacheVersion: null,
polyfillModuleNames: null,
reporter: require('../../lib/reporting').nullReporter,
};
const makeRequest = (reqHandler, requrl, reqOptions) => new Promise(resolve =>
reqHandler(
{ url: requrl, headers:{}, ...reqOptions },
{
statusCode: 200,
headers: {},
getHeader(header) { return this.headers[header]; },
setHeader(header, value) { this.headers[header] = value; },
writeHead(statusCode) { this.statusCode = statusCode; },
end(body) {
this.body = body;
resolve(this);
},
},
{ next: () => {} },
)
);
const invalidatorFunc = jest.fn();
let requestHandler;
beforeEach(() => {
Bundler.prototype.bundle = jest.fn(() =>
Promise.resolve({
getSource: () => 'this is the source',
getSourceMap: () => {},
getSourceMapString: () => 'this is the source map',
getEtag: () => 'this is an etag',
}));
Bundler.prototype.invalidateFile = invalidatorFunc;
Bundler.prototype.getResolver =
jest.fn().mockReturnValue({
getDependencyGraph: jest.fn().mockReturnValue({
getHasteMap: jest.fn().mockReturnValue({on: jest.fn()}),
load: jest.fn(() => Promise.resolve()),
}),
});
server = new Server(options);
requestHandler = server.processRequest.bind(server);
});
it('returns JS bundle source on request of *.bundle', () => {
return makeRequest(
requestHandler,
'mybundle.bundle?runModule=true',
null
).then(response =>
expect(response.body).toEqual('this is the source')
);
});
it('returns JS bundle source on request of *.bundle (compat)', () => {
return makeRequest(
requestHandler,
'mybundle.runModule.bundle'
).then(response =>
expect(response.body).toEqual('this is the source')
);
});
it('returns ETag header on request of *.bundle', () => {
return makeRequest(
requestHandler,
'mybundle.bundle?runModule=true'
).then(response => {
expect(response.getHeader('ETag')).toBeDefined();
});
});
it('returns 304 on request of *.bundle when if-none-match equals the ETag', () => {
return makeRequest(
requestHandler,
'mybundle.bundle?runModule=true',
{ headers : { 'if-none-match' : 'this is an etag' } }
).then(response => {
expect(response.statusCode).toEqual(304);
});
});
it('returns sourcemap on request of *.map', () => {
return makeRequest(
requestHandler,
'mybundle.map?runModule=true'
).then(response =>
expect(response.body).toEqual('this is the source map')
);
});
it('works with .ios.js extension', () => {
return makeRequest(
requestHandler,
'index.ios.includeRequire.bundle'
).then(response => {
expect(response.body).toEqual('this is the source');
expect(Bundler.prototype.bundle).toBeCalledWith({
entryFile: 'index.ios.js',
inlineSourceMap: false,
minify: false,
generateSourceMaps: false,
hot: false,
runModule: true,
sourceMapUrl: 'index.ios.includeRequire.map',
dev: true,
platform: undefined,
onProgress: jasmine.any(Function),
runBeforeMainModule: ['InitializeCore'],
unbundle: false,
entryModuleOnly: false,
isolateModuleIDs: false,
assetPlugins: [],
});
});
});
it('passes in the platform param', function() {
return makeRequest(
requestHandler,
'index.bundle?platform=ios'
).then(function(response) {
expect(response.body).toEqual('this is the source');
expect(Bundler.prototype.bundle).toBeCalledWith({
entryFile: 'index.js',
inlineSourceMap: false,
minify: false,
generateSourceMaps: false,
hot: false,
runModule: true,
sourceMapUrl: 'index.map?platform=ios',
dev: true,
platform: 'ios',
onProgress: jasmine.any(Function),
runBeforeMainModule: ['InitializeCore'],
unbundle: false,
entryModuleOnly: false,
isolateModuleIDs: false,
assetPlugins: [],
});
});
});
it('passes in the assetPlugin param', function() {
return makeRequest(
requestHandler,
'index.bundle?assetPlugin=assetPlugin1&assetPlugin=assetPlugin2'
).then(function(response) {
expect(response.body).toEqual('this is the source');
expect(Bundler.prototype.bundle).toBeCalledWith({
entryFile: 'index.js',
inlineSourceMap: false,
minify: false,
generateSourceMaps: false,
hot: false,
runModule: true,
sourceMapUrl: 'index.map?assetPlugin=assetPlugin1&assetPlugin=assetPlugin2',
dev: true,
platform: undefined,
onProgress: jasmine.any(Function),
runBeforeMainModule: ['InitializeCore'],
unbundle: false,
entryModuleOnly: false,
isolateModuleIDs: false,
assetPlugins: ['assetPlugin1', 'assetPlugin2'],
});
});
});
describe('file changes', () => {
it('invalides files in bundle when file is updated', () => {
return makeRequest(
requestHandler,
'mybundle.bundle?runModule=true'
).then(() => {
server.onFileChange('all', options.projectRoots[0] + '/path/file.js');
expect(invalidatorFunc.mock.calls[0][0]).toEqual('root/path/file.js');
});
});
it('does not rebuild the bundles that contain a file when that file is changed', () => {
const bundleFunc = jest.fn();
bundleFunc
.mockReturnValueOnce(
Promise.resolve({
getSource: () => 'this is the first source',
getSourceMap: () => {},
getSourceMapString: () => 'this is the source map',
getEtag: () => () => 'this is an etag',
})
)
.mockReturnValue(
Promise.resolve({
getSource: () => 'this is the rebuilt source',
getSourceMap: () => {},
getSourceMapString: () => 'this is the source map',
getEtag: () => () => 'this is an etag',
})
);
Bundler.prototype.bundle = bundleFunc;
server = new Server(options);
requestHandler = server.processRequest.bind(server);
makeRequest(requestHandler, 'mybundle.bundle?runModule=true')
.done(response => {
expect(response.body).toEqual('this is the first source');
expect(bundleFunc.mock.calls.length).toBe(1);
});
jest.runAllTicks();
server.onFileChange('all', options.projectRoots[0] + 'path/file.js');
jest.runAllTimers();
jest.runAllTicks();
expect(bundleFunc.mock.calls.length).toBe(1);
makeRequest(requestHandler, 'mybundle.bundle?runModule=true')
.done(response =>
expect(response.body).toEqual('this is the rebuilt source')
);
jest.runAllTicks();
});
it(
'does not rebuild the bundles that contain a file ' +
'when that file is changed, even when hot loading is enabled',
() => {
const bundleFunc = jest.fn();
bundleFunc
.mockReturnValueOnce(
Promise.resolve({
getSource: () => 'this is the first source',
getSourceMap: () => {},
getSourceMapString: () => 'this is the source map',
getEtag: () => () => 'this is an etag',
})
)
.mockReturnValue(
Promise.resolve({
getSource: () => 'this is the rebuilt source',
getSourceMap: () => {},
getSourceMapString: () => 'this is the source map',
getEtag: () => () => 'this is an etag',
})
);
Bundler.prototype.bundle = bundleFunc;
server = new Server(options);
server.setHMRFileChangeListener(() => {});
requestHandler = server.processRequest.bind(server);
makeRequest(requestHandler, 'mybundle.bundle?runModule=true')
.done(response => {
expect(response.body).toEqual('this is the first source');
expect(bundleFunc.mock.calls.length).toBe(1);
});
jest.runAllTicks();
server.onFileChange('all', options.projectRoots[0] + 'path/file.js');
jest.runAllTimers();
jest.runAllTicks();
expect(bundleFunc.mock.calls.length).toBe(1);
server.setHMRFileChangeListener(null);
makeRequest(requestHandler, 'mybundle.bundle?runModule=true')
.done(response => {
expect(response.body).toEqual('this is the rebuilt source');
expect(bundleFunc.mock.calls.length).toBe(2);
});
jest.runAllTicks();
});
});
describe('/onchange endpoint', () => {
let EventEmitter;
let req;
let res;
beforeEach(() => {
EventEmitter = require.requireActual('events').EventEmitter;
req = scaffoldReq(new EventEmitter());
req.url = '/onchange';
res = {
writeHead: jest.fn(),
end: jest.fn()
};
});
it('should hold on to request and inform on change', () => {
server.processRequest(req, res);
server.onFileChange('all', options.projectRoots[0] + 'path/file.js');
jest.runAllTimers();
expect(res.end).toBeCalledWith(JSON.stringify({changed: true}));
});
it('should not inform changes on disconnected clients', () => {
server.processRequest(req, res);
req.emit('close');
jest.runAllTimers();
server.onFileChange('all', options.projectRoots[0] + 'path/file.js');
jest.runAllTimers();
expect(res.end).not.toBeCalled();
});
});
describe('/assets endpoint', () => {
it('should serve simple case', () => {
const req = scaffoldReq({url: '/assets/imgs/a.png'});
const res = {end: jest.fn(), setHeader: jest.fn()};
AssetServer.prototype.get.mockImplementation(() => Promise.resolve('i am image'));
server.processRequest(req, res);
jest.runAllTimers();
expect(res.end).toBeCalledWith('i am image');
});
it('should parse the platform option', () => {
const req = scaffoldReq({url: '/assets/imgs/a.png?platform=ios'});
const res = {end: jest.fn(), setHeader: jest.fn()};
AssetServer.prototype.get.mockImplementation(() => Promise.resolve('i am image'));
server.processRequest(req, res);
jest.runAllTimers();
expect(AssetServer.prototype.get).toBeCalledWith('imgs/a.png', 'ios');
expect(res.end).toBeCalledWith('i am image');
});
it('should serve range request', () => {
const req = scaffoldReq({
url: '/assets/imgs/a.png?platform=ios',
headers: {range: 'bytes=0-3'},
});
const res = {end: jest.fn(), writeHead: jest.fn(), setHeader: jest.fn()};
const mockData = 'i am image';
AssetServer.prototype.get.mockImplementation(() => Promise.resolve(mockData));
server.processRequest(req, res);
jest.runAllTimers();
expect(AssetServer.prototype.get).toBeCalledWith('imgs/a.png', 'ios');
expect(res.end).toBeCalledWith(mockData.slice(0, 4));
});
it('should serve assets files\'s name contain non-latin letter', () => {
const req = scaffoldReq({url: '/assets/imgs/%E4%B8%BB%E9%A1%B5/logo.png'});
const res = {end: jest.fn(), setHeader: jest.fn()};
AssetServer.prototype.get.mockImplementation(() => Promise.resolve('i am image'));
server.processRequest(req, res);
jest.runAllTimers();
expect(AssetServer.prototype.get).toBeCalledWith(
'imgs/\u{4E3B}\u{9875}/logo.png',
undefined
);
expect(res.end).toBeCalledWith('i am image');
});
});
describe('buildbundle(options)', () => {
it('Calls the bundler with the correct args', () => {
return server.buildBundle({
entryFile: 'foo file'
}).then(() =>
expect(Bundler.prototype.bundle).toBeCalledWith({
entryFile: 'foo file',
inlineSourceMap: false,
minify: false,
hot: false,
runModule: true,
dev: true,
platform: undefined,
runBeforeMainModule: ['InitializeCore'],
unbundle: false,
entryModuleOnly: false,
isolateModuleIDs: false,
assetPlugins: [],
})
);
});
});
describe('buildBundleFromUrl(options)', () => {
it('Calls the bundler with the correct args', () => {
return server.buildBundleFromUrl('/path/to/foo.bundle?dev=false&runModule=false')
.then(() =>
expect(Bundler.prototype.bundle).toBeCalledWith({
entryFile: 'path/to/foo.js',
inlineSourceMap: false,
minify: false,
generateSourceMaps: true,
hot: false,
runModule: false,
sourceMapUrl: '/path/to/foo.map?dev=false&runModule=false',
dev: false,
platform: undefined,
runBeforeMainModule: ['InitializeCore'],
unbundle: false,
entryModuleOnly: false,
isolateModuleIDs: false,
assetPlugins: [],
})
);
});
});
describe('/symbolicate endpoint', () => {
it('should symbolicate given stack trace', () => {
const body = JSON.stringify({stack: [{
file: 'http://foo.bundle?platform=ios',
lineNumber: 2100,
column: 44,
customPropShouldBeLeftUnchanged: 'foo',
}]});
SourceMapConsumer.prototype.originalPositionFor = jest.fn((frame) => {
expect(frame.line).toEqual(2100);
expect(frame.column).toEqual(44);
return {
source: 'foo.js',
line: 21,
column: 4,
};
});
return makeRequest(
requestHandler,
'/symbolicate',
{ rawBody: body }
).then(response => {
expect(JSON.parse(response.body)).toEqual({
stack: [{
file: 'foo.js',
lineNumber: 21,
column: 4,
customPropShouldBeLeftUnchanged: 'foo',
}]
});
});
});
it('ignores `/debuggerWorker.js` stack frames', () => {
const body = JSON.stringify({stack: [{
file: 'http://localhost:8081/debuggerWorker.js',
lineNumber: 123,
column: 456,
}]});
return makeRequest(
requestHandler,
'/symbolicate',
{ rawBody: body }
).then(response => {
expect(JSON.parse(response.body)).toEqual({
stack: [{
file: 'http://localhost:8081/debuggerWorker.js',
lineNumber: 123,
column: 456,
}]
});
});
});
});
describe('/symbolicate handles errors', () => {
it('should symbolicate given stack trace', () => {
const body = 'clearly-not-json';
console.error = jest.fn();
return makeRequest(
requestHandler,
'/symbolicate',
{ rawBody: body }
).then(response => {
expect(response.statusCode).toEqual(500);
expect(JSON.parse(response.body)).toEqual({
error: jasmine.any(String),
});
expect(console.error).toBeCalled();
});
});
});
// ensures that vital properties exist on fake request objects
function scaffoldReq(req) {
if (!req.headers) {
req.headers = {};
}
return req;
}
});

View File

@ -0,0 +1,972 @@
/**
* 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 AssetServer = require('../AssetServer');
const getPlatformExtension = require('../node-haste').getPlatformExtension;
const Bundler = require('../Bundler');
const MultipartResponse = require('./MultipartResponse');
const SourceMapConsumer = require('source-map').SourceMapConsumer;
const declareOpts = require('../lib/declareOpts');
const defaults = require('../../../defaults');
const mime = require('mime-types');
const path = require('path');
const terminal = require('../lib/terminal');
const url = require('url');
const debug = require('debug')('RNP:Server');
import type Module from '../node-haste/Module';
import type {Stats} from 'fs';
import type {IncomingMessage, ServerResponse} from 'http';
import type ResolutionResponse from '../node-haste/DependencyGraph/ResolutionResponse';
import type Bundle from '../Bundler/Bundle';
import type {Reporter} from '../lib/reporting';
import type {GetTransformOptions} from '../Bundler';
import type GlobalTransformCache from '../lib/GlobalTransformCache';
const {
createActionStartEntry,
createActionEndEntry,
log,
} = require('../Logger');
function debounceAndBatch(fn, delay) {
let timeout, args = [];
return (value) => {
args.push(value);
clearTimeout(timeout);
timeout = setTimeout(() => {
const a = args;
args = [];
fn(a);
}, delay);
};
}
type Options = {
assetExts?: Array<string>,
blacklistRE?: RegExp,
cacheVersion?: string,
extraNodeModules?: {},
getTransformOptions?: GetTransformOptions,
globalTransformCache: ?GlobalTransformCache,
moduleFormat?: string,
platforms?: Array<string>,
polyfillModuleNames?: Array<string>,
projectRoots: Array<string>,
providesModuleNodeModules?: Array<string>,
reporter: Reporter,
resetCache?: boolean,
silent?: boolean,
transformModulePath?: string,
transformTimeoutInterval?: number,
watch?: boolean,
};
const bundleOpts = declareOpts({
sourceMapUrl: {
type: 'string',
required: false,
},
entryFile: {
type: 'string',
required: true,
},
dev: {
type: 'boolean',
default: true,
},
minify: {
type: 'boolean',
default: false,
},
runModule: {
type: 'boolean',
default: true,
},
inlineSourceMap: {
type: 'boolean',
default: false,
},
platform: {
type: 'string',
required: true,
},
runBeforeMainModule: {
type: 'array',
default: defaults.runBeforeMainModule,
},
unbundle: {
type: 'boolean',
default: false,
},
hot: {
type: 'boolean',
default: false,
},
entryModuleOnly: {
type: 'boolean',
default: false,
},
isolateModuleIDs: {
type: 'boolean',
default: false,
},
resolutionResponse: {
type: 'object',
},
generateSourceMaps: {
type: 'boolean',
required: false,
},
assetPlugins: {
type: 'array',
default: [],
},
onProgress: {
type: 'function',
},
});
const dependencyOpts = declareOpts({
platform: {
type: 'string',
required: true,
},
dev: {
type: 'boolean',
default: true,
},
entryFile: {
type: 'string',
required: true,
},
recursive: {
type: 'boolean',
default: true,
},
hot: {
type: 'boolean',
default: false,
},
minify: {
type: 'boolean',
default: undefined,
},
});
const bundleDeps = new WeakMap();
const NODE_MODULES = `${path.sep}node_modules${path.sep}`;
class Server {
_opts: {
assetExts: Array<string>,
blacklistRE: ?RegExp,
cacheVersion: string,
extraNodeModules: {},
getTransformOptions?: GetTransformOptions,
moduleFormat: string,
platforms: Array<string>,
polyfillModuleNames: Array<string>,
projectRoots: Array<string>,
providesModuleNodeModules?: Array<string>,
reporter: Reporter,
resetCache: boolean,
silent: boolean,
transformModulePath: ?string,
transformTimeoutInterval: ?number,
watch: boolean,
};
_projectRoots: Array<string>;
_bundles: {};
_changeWatchers: Array<{
req: IncomingMessage,
res: ServerResponse,
}>;
_fileChangeListeners: Array<(filePath: string) => mixed>;
_assetServer: AssetServer;
_bundler: Bundler;
_debouncedFileChangeHandler: (filePath: string) => mixed;
_hmrFileChangeListener: (type: string, filePath: string) => mixed;
_reporter: Reporter;
constructor(options: Options) {
this._opts = {
assetExts: options.assetExts || defaults.assetExts,
blacklistRE: options.blacklistRE,
cacheVersion: options.cacheVersion || '1.0',
extraNodeModules: options.extraNodeModules || {},
getTransformOptions: options.getTransformOptions,
globalTransformCache: options.globalTransformCache,
moduleFormat: options.moduleFormat != null ? options.moduleFormat : 'haste',
platforms: options.platforms || defaults.platforms,
polyfillModuleNames: options.polyfillModuleNames || [],
projectRoots: options.projectRoots,
providesModuleNodeModules: options.providesModuleNodeModules,
reporter: options.reporter,
resetCache: options.resetCache || false,
silent: options.silent || false,
transformModulePath: options.transformModulePath,
transformTimeoutInterval: options.transformTimeoutInterval,
watch: options.watch || false,
};
const processFileChange =
({type, filePath, stat}) => this.onFileChange(type, filePath, stat);
this._reporter = options.reporter;
this._projectRoots = this._opts.projectRoots;
this._bundles = Object.create(null);
this._changeWatchers = [];
this._fileChangeListeners = [];
this._assetServer = new AssetServer({
assetExts: this._opts.assetExts,
projectRoots: this._opts.projectRoots,
});
const bundlerOpts = Object.create(this._opts);
bundlerOpts.assetServer = this._assetServer;
bundlerOpts.allowBundleUpdates = this._opts.watch;
bundlerOpts.globalTransformCache = options.globalTransformCache;
bundlerOpts.watch = this._opts.watch;
bundlerOpts.reporter = options.reporter;
this._bundler = new Bundler(bundlerOpts);
// changes to the haste map can affect resolution of files in the bundle
const dependencyGraph = this._bundler.getResolver().getDependencyGraph();
dependencyGraph.load().then(() => {
dependencyGraph.getWatcher().on(
'change',
({eventsQueue}) => eventsQueue.forEach(processFileChange),
);
dependencyGraph.getHasteMap().on('change', () => {
debug('Clearing bundle cache due to haste map change');
this._clearBundles();
});
});
this._debouncedFileChangeHandler = debounceAndBatch(filePaths => {
// only clear bundles for non-JS changes
if (filePaths.every(RegExp.prototype.test, /\.js(?:on)?$/i)) {
for (const key in this._bundles) {
this._bundles[key].then(bundle => {
const deps = bundleDeps.get(bundle);
filePaths.forEach(filePath => {
// $FlowFixMe(>=0.37.0)
if (deps.files.has(filePath)) {
// $FlowFixMe(>=0.37.0)
deps.outdated.add(filePath);
}
});
}).catch(e => {
debug(`Could not update bundle: ${e}, evicting from cache`);
delete this._bundles[key];
});
}
} else {
debug('Clearing bundles due to non-JS change');
this._clearBundles();
}
this._informChangeWatchers();
}, 50);
}
end(): mixed {
return this._bundler.end();
}
setHMRFileChangeListener(
listener: (type: string, filePath: string) => mixed,
) {
this._hmrFileChangeListener = listener;
}
addFileChangeListener(listener: (filePath: string) => mixed) {
if (this._fileChangeListeners.indexOf(listener) === -1) {
this._fileChangeListeners.push(listener);
}
}
buildBundle(options: {
entryFile: string,
platform?: string,
}): Promise<Bundle> {
return this._bundler.getResolver().getDependencyGraph().load().then(() => {
if (!options.platform) {
options.platform = getPlatformExtension(options.entryFile);
}
const opts = bundleOpts(options);
const building = this._bundler.bundle(opts);
building.then(bundle => {
const modules = bundle.getModules();
const nonVirtual = modules.filter(m => !m.virtual);
bundleDeps.set(bundle, {
files: new Map(
nonVirtual
.map(({sourcePath, meta = {dependencies: []}}) =>
[sourcePath, meta.dependencies])
),
idToIndex: new Map(modules.map(({id}, i) => [id, i])),
dependencyPairs: new Map(
nonVirtual
.filter(({meta}) => meta && meta.dependencyPairs)
.map(m => [m.sourcePath, m.meta.dependencyPairs])
),
outdated: new Set(),
});
});
return building;
});
}
buildBundleFromUrl(reqUrl: string): Promise<mixed> {
const options = this._getOptionsFromUrl(reqUrl);
return this.buildBundle(options);
}
buildBundleForHMR(
options: {platform: ?string},
host: string,
port: number,
): Promise<string> {
return this._bundler.hmrBundle(options, host, port);
}
getShallowDependencies(options: {
entryFile: string,
platform?: string,
}): Promise<mixed> {
return Promise.resolve().then(() => {
if (!options.platform) {
options.platform = getPlatformExtension(options.entryFile);
}
const opts = dependencyOpts(options);
return this._bundler.getShallowDependencies(opts);
});
}
getModuleForPath(entryFile: string): Module {
return this._bundler.getModuleForPath(entryFile);
}
getDependencies(options: {
entryFile: string,
platform?: string,
}): Promise<ResolutionResponse> {
return Promise.resolve().then(() => {
if (!options.platform) {
options.platform = getPlatformExtension(options.entryFile);
}
const opts = dependencyOpts(options);
return this._bundler.getDependencies(opts);
});
}
getOrderedDependencyPaths(options: {}): Promise<mixed> {
return Promise.resolve().then(() => {
const opts = dependencyOpts(options);
return this._bundler.getOrderedDependencyPaths(opts);
});
}
onFileChange(type: string, filePath: string, stat: Stats) {
this._assetServer.onFileChange(type, filePath, stat);
this._bundler.invalidateFile(filePath);
// If Hot Loading is enabled avoid rebuilding bundles and sending live
// updates. Instead, send the HMR updates right away and clear the bundles
// cache so that if the user reloads we send them a fresh bundle
if (this._hmrFileChangeListener) {
// Clear cached bundles in case user reloads
this._clearBundles();
this._hmrFileChangeListener(type, filePath);
return;
} else if (type !== 'change' && filePath.indexOf(NODE_MODULES) !== -1) {
// node module resolution can be affected by added or removed files
debug('Clearing bundles due to potential node_modules resolution change');
this._clearBundles();
}
Promise.all(
this._fileChangeListeners.map(listener => listener(filePath))
).then(
() => this._onFileChangeComplete(filePath),
() => this._onFileChangeComplete(filePath)
);
}
_onFileChangeComplete(filePath: string) {
// Make sure the file watcher event runs through the system before
// we rebuild the bundles.
this._debouncedFileChangeHandler(filePath);
}
_clearBundles() {
this._bundles = Object.create(null);
}
_informChangeWatchers() {
const watchers = this._changeWatchers;
const headers = {
'Content-Type': 'application/json; charset=UTF-8',
};
watchers.forEach(function(w) {
w.res.writeHead(205, headers);
w.res.end(JSON.stringify({ changed: true }));
});
this._changeWatchers = [];
}
_processDebugRequest(reqUrl: string, res: ServerResponse) {
let ret = '<!doctype html>';
const pathname = url.parse(reqUrl).pathname;
/* $FlowFixMe: pathname would be null for an invalid URL */
const parts = pathname.split('/').filter(Boolean);
if (parts.length === 1) {
ret += '<div><a href="/debug/bundles">Cached Bundles</a></div>';
res.end(ret);
} else if (parts[1] === 'bundles') {
ret += '<h1> Cached Bundles </h1>';
Promise.all(Object.keys(this._bundles).map(optionsJson =>
this._bundles[optionsJson].then(p => {
ret += '<div><h2>' + optionsJson + '</h2>';
ret += p.getDebugInfo();
})
)).then(
() => res.end(ret),
e => {
res.writeHead(500);
res.end('Internal Error');
terminal.log(e.stack); // eslint-disable-line no-console-disallow
}
);
} else {
res.writeHead(404);
res.end('Invalid debug request');
return;
}
}
_processOnChangeRequest(req: IncomingMessage, res: ServerResponse) {
const watchers = this._changeWatchers;
watchers.push({
req: req,
res: res,
});
req.on('close', () => {
for (let i = 0; i < watchers.length; i++) {
if (watchers[i] && watchers[i].req === req) {
watchers.splice(i, 1);
break;
}
}
});
}
_rangeRequestMiddleware(
req: IncomingMessage,
res: ServerResponse,
data: string,
assetPath: string,
) {
if (req.headers && req.headers.range) {
const [rangeStart, rangeEnd] = req.headers.range.replace(/bytes=/, '').split('-');
const dataStart = parseInt(rangeStart, 10);
const dataEnd = rangeEnd ? parseInt(rangeEnd, 10) : data.length - 1;
const chunksize = (dataEnd - dataStart) + 1;
res.writeHead(206, {
'Accept-Ranges': 'bytes',
'Content-Length': chunksize.toString(),
'Content-Range': `bytes ${dataStart}-${dataEnd}/${data.length}`,
'Content-Type': mime.lookup(path.basename(assetPath[1])),
});
return data.slice(dataStart, dataEnd + 1);
}
return data;
}
_processAssetsRequest(req: IncomingMessage, res: ServerResponse) {
const urlObj = url.parse(decodeURI(req.url), true);
/* $FlowFixMe: could be empty if the url is invalid */
const assetPath: string = urlObj.pathname.match(/^\/assets\/(.+)$/);
const processingAssetRequestLogEntry =
log(createActionStartEntry({
action_name: 'Processing asset request',
asset: assetPath[1],
}));
/* $FlowFixMe: query may be empty for invalid URLs */
this._assetServer.get(assetPath[1], urlObj.query.platform)
.then(
data => {
// Tell clients to cache this for 1 year.
// This is safe as the asset url contains a hash of the asset.
if (process.env.REACT_NATIVE_ENABLE_ASSET_CACHING === true) {
res.setHeader('Cache-Control', 'max-age=31536000');
}
res.end(this._rangeRequestMiddleware(req, res, data, assetPath));
process.nextTick(() => {
log(createActionEndEntry(processingAssetRequestLogEntry));
});
},
error => {
console.error(error.stack);
res.writeHead(404);
res.end('Asset not found');
}
);
}
optionsHash(options: {}) {
// onProgress is a function, can't be serialized
return JSON.stringify(Object.assign({}, options, { onProgress: null }));
}
_useCachedOrUpdateOrCreateBundle(options: {
entryFile: string,
platform?: string,
}): Promise<Bundle> {
const optionsJson = this.optionsHash(options);
const bundleFromScratch = () => {
const building = this.buildBundle(options);
this._bundles[optionsJson] = building;
return building;
};
if (optionsJson in this._bundles) {
return this._bundles[optionsJson].then(bundle => {
const deps = bundleDeps.get(bundle);
// $FlowFixMe(>=0.37.0)
const {dependencyPairs, files, idToIndex, outdated} = deps;
if (outdated.size) {
const updatingExistingBundleLogEntry =
log(createActionStartEntry({
action_name: 'Updating existing bundle',
outdated_modules: outdated.size,
}));
this._reporter.update({
type: 'bundle_update_existing',
entryFilePath: options.entryFile,
outdatedModuleCount: outdated.size,
});
debug('Attempt to update existing bundle');
const changedModules =
Array.from(outdated, this.getModuleForPath, this);
// $FlowFixMe(>=0.37.0)
deps.outdated = new Set();
const opts = bundleOpts(options);
const {platform, dev, minify, hot} = opts;
// Need to create a resolution response to pass to the bundler
// to process requires after transform. By providing a
// specific response we can compute a non recursive one which
// is the least we need and improve performance.
const bundlePromise = this._bundles[optionsJson] =
this.getDependencies({
platform, dev, hot, minify,
entryFile: options.entryFile,
recursive: false,
}).then(response => {
debug('Update bundle: rebuild shallow bundle');
changedModules.forEach(m => {
response.setResolvedDependencyPairs(
m,
dependencyPairs.get(m.path),
{ignoreFinalized: true},
);
});
return this.buildBundle({
...options,
resolutionResponse: response.copy({
dependencies: changedModules,
}),
}).then(updateBundle => {
const oldModules = bundle.getModules();
const newModules = updateBundle.getModules();
for (let i = 0, n = newModules.length; i < n; i++) {
const moduleTransport = newModules[i];
const {meta, sourcePath} = moduleTransport;
if (outdated.has(sourcePath)) {
/* $FlowFixMe: `meta` could be empty */
if (!contentsEqual(meta.dependencies, new Set(files.get(sourcePath)))) {
// bail out if any dependencies changed
return Promise.reject(Error(
`Dependencies of ${sourcePath} changed from [${
/* $FlowFixMe: `get` can return empty */
files.get(sourcePath).join(', ')
}] to [${
/* $FlowFixMe: `meta` could be empty */
meta.dependencies.join(', ')
}]`
));
}
oldModules[idToIndex.get(moduleTransport.id)] = moduleTransport;
}
}
bundle.invalidateSource();
log(createActionEndEntry(updatingExistingBundleLogEntry));
debug('Successfully updated existing bundle');
return bundle;
});
}).catch(e => {
debug('Failed to update existing bundle, rebuilding...', e.stack || e.message);
return bundleFromScratch();
});
return bundlePromise;
} else {
debug('Using cached bundle');
return bundle;
}
});
}
return bundleFromScratch();
}
processRequest(
req: IncomingMessage,
res: ServerResponse,
next: () => mixed,
) {
const urlObj = url.parse(req.url, true);
const {host} = req.headers;
debug(`Handling request: ${host ? 'http://' + host : ''}${req.url}`);
/* $FlowFixMe: Could be empty if the URL is invalid. */
const pathname: string = urlObj.pathname;
let requestType;
if (pathname.match(/\.bundle$/)) {
requestType = 'bundle';
} else if (pathname.match(/\.map$/)) {
requestType = 'map';
} else if (pathname.match(/\.assets$/)) {
requestType = 'assets';
} else if (pathname.match(/^\/debug/)) {
this._processDebugRequest(req.url, res);
return;
} else if (pathname.match(/^\/onchange\/?$/)) {
this._processOnChangeRequest(req, res);
return;
} else if (pathname.match(/^\/assets\//)) {
this._processAssetsRequest(req, res);
return;
} else if (pathname === '/symbolicate') {
this._symbolicate(req, res);
return;
} else {
next();
return;
}
const options = this._getOptionsFromUrl(req.url);
this._reporter.update({
type: 'bundle_requested',
entryFilePath: options.entryFile,
});
const requestingBundleLogEntry =
log(createActionStartEntry({
action_name: 'Requesting bundle',
bundle_url: req.url,
entry_point: options.entryFile,
}));
let reportProgress = () => {};
if (!this._opts.silent) {
reportProgress = (transformedFileCount, totalFileCount) => {
this._reporter.update({
type: 'bundle_transform_progressed',
entryFilePath: options.entryFile,
transformedFileCount,
totalFileCount,
});
};
}
const mres = MultipartResponse.wrap(req, res);
options.onProgress = (done, total) => {
reportProgress(done, total);
mres.writeChunk({'Content-Type': 'application/json'}, JSON.stringify({done, total}));
};
debug('Getting bundle for request');
const building = this._useCachedOrUpdateOrCreateBundle(options);
building.then(
p => {
this._reporter.update({
type: 'bundle_built',
entryFilePath: options.entryFile,
});
if (requestType === 'bundle') {
debug('Generating source code');
const bundleSource = p.getSource({
inlineSourceMap: options.inlineSourceMap,
minify: options.minify,
dev: options.dev,
});
debug('Writing response headers');
const etag = p.getEtag();
mres.setHeader('Content-Type', 'application/javascript');
mres.setHeader('ETag', etag);
if (req.headers['if-none-match'] === etag) {
debug('Responding with 304');
mres.writeHead(304);
mres.end();
} else {
mres.end(bundleSource);
}
debug('Finished response');
log(createActionEndEntry(requestingBundleLogEntry));
} else if (requestType === 'map') {
const sourceMap = p.getSourceMapString({
minify: options.minify,
dev: options.dev,
});
mres.setHeader('Content-Type', 'application/json');
mres.end(sourceMap);
log(createActionEndEntry(requestingBundleLogEntry));
} else if (requestType === 'assets') {
const assetsList = JSON.stringify(p.getAssets());
mres.setHeader('Content-Type', 'application/json');
mres.end(assetsList);
log(createActionEndEntry(requestingBundleLogEntry));
}
},
error => this._handleError(mres, this.optionsHash(options), error)
).catch(error => {
process.nextTick(() => {
throw error;
});
});
}
_symbolicate(req: IncomingMessage, res: ServerResponse) {
const symbolicatingLogEntry =
log(createActionStartEntry('Symbolicating'));
/* $FlowFixMe: where is `rowBody` defined? Is it added by
* the `connect` framework? */
Promise.resolve(req.rawBody).then(body => {
const stack = JSON.parse(body).stack;
// In case of multiple bundles / HMR, some stack frames can have
// different URLs from others
const urlIndexes = {};
const uniqueUrls = [];
stack.forEach(frame => {
const sourceUrl = frame.file;
// Skip `/debuggerWorker.js` which drives remote debugging because it
// does not need to symbolication.
// Skip anything except http(s), because there is no support for that yet
if (!urlIndexes.hasOwnProperty(sourceUrl) &&
!sourceUrl.endsWith('/debuggerWorker.js') &&
sourceUrl.startsWith('http')) {
urlIndexes[sourceUrl] = uniqueUrls.length;
uniqueUrls.push(sourceUrl);
}
});
const sourceMaps = uniqueUrls.map(
sourceUrl => this._sourceMapForURL(sourceUrl)
);
return Promise.all(sourceMaps).then(consumers => {
return stack.map(frame => {
const sourceUrl = frame.file;
if (!urlIndexes.hasOwnProperty(sourceUrl)) {
return frame;
}
const idx = urlIndexes[sourceUrl];
const consumer = consumers[idx];
const original = consumer.originalPositionFor({
line: frame.lineNumber,
column: frame.column,
});
if (!original) {
return frame;
}
return Object.assign({}, frame, {
file: original.source,
lineNumber: original.line,
column: original.column,
});
});
});
}).then(
stack => {
res.end(JSON.stringify({stack: stack}));
process.nextTick(() => {
log(createActionEndEntry(symbolicatingLogEntry));
});
},
error => {
console.error(error.stack || error);
res.statusCode = 500;
res.end(JSON.stringify({error: error.message}));
}
);
}
_sourceMapForURL(reqUrl: string): Promise<SourceMapConsumer> {
const options = this._getOptionsFromUrl(reqUrl);
const building = this._useCachedOrUpdateOrCreateBundle(options);
return building.then(p => {
const sourceMap = p.getSourceMap({
minify: options.minify,
dev: options.dev,
});
return new SourceMapConsumer(sourceMap);
});
}
_handleError(res: ServerResponse, bundleID: string, error: {
status: number,
type: string,
description: string,
filename: string,
lineNumber: number,
errors: Array<{description: string, filename: string, lineNumber: number}>,
}) {
res.writeHead(error.status || 500, {
'Content-Type': 'application/json; charset=UTF-8',
});
if (error.type === 'TransformError' ||
error.type === 'NotFoundError' ||
error.type === 'UnableToResolveError') {
error.errors = [{
description: error.description,
filename: error.filename,
lineNumber: error.lineNumber,
}];
res.end(JSON.stringify(error));
if (error.type === 'NotFoundError') {
delete this._bundles[bundleID];
}
} else {
console.error(error.stack || error);
res.end(JSON.stringify({
type: 'InternalError',
message: 'react-packager has encountered an internal error, ' +
'please check your terminal error output for more details',
}));
}
}
_getOptionsFromUrl(reqUrl: string): {
sourceMapUrl: string,
entryFile: string,
dev: boolean,
minify: boolean,
hot: boolean,
runModule: boolean,
inlineSourceMap: boolean,
platform?: string,
entryModuleOnly: boolean,
generateSourceMaps: boolean,
assetPlugins: Array<string>,
onProgress?: (doneCont: number, totalCount: number) => mixed,
} {
// `true` to parse the query param as an object.
const urlObj = url.parse(reqUrl, true);
/* $FlowFixMe: `pathname` could be empty for an invalid URL */
const pathname = decodeURIComponent(urlObj.pathname);
// Backwards compatibility. Options used to be as added as '.' to the
// entry module name. We can safely remove these options.
const entryFile = pathname.replace(/^\//, '').split('.').filter(part => {
if (part === 'includeRequire' || part === 'runModule' ||
part === 'bundle' || part === 'map' || part === 'assets') {
return false;
}
return true;
}).join('.') + '.js';
const sourceMapUrlObj = Object.assign({}, urlObj);
sourceMapUrlObj.pathname = pathname.replace(/\.bundle$/, '.map');
// try to get the platform from the url
/* $FlowFixMe: `query` could be empty for an invalid URL */
const platform = urlObj.query.platform ||
getPlatformExtension(pathname);
/* $FlowFixMe: `query` could be empty for an invalid URL */
const assetPlugin = urlObj.query.assetPlugin;
const assetPlugins = Array.isArray(assetPlugin) ?
assetPlugin :
(typeof assetPlugin === 'string') ? [assetPlugin] : [];
const dev = this._getBoolOptionFromQuery(urlObj.query, 'dev', true);
const minify = this._getBoolOptionFromQuery(urlObj.query, 'minify', false);
return {
sourceMapUrl: url.format(sourceMapUrlObj),
entryFile: entryFile,
dev,
minify,
hot: this._getBoolOptionFromQuery(urlObj.query, 'hot', false),
runModule: this._getBoolOptionFromQuery(urlObj.query, 'runModule', true),
inlineSourceMap: this._getBoolOptionFromQuery(
urlObj.query,
'inlineSourceMap',
false
),
platform: platform,
entryModuleOnly: this._getBoolOptionFromQuery(
urlObj.query,
'entryModuleOnly',
false,
),
generateSourceMaps: minify || !dev || this._getBoolOptionFromQuery(urlObj.query, 'babelSourcemap', false),
assetPlugins,
};
}
_getBoolOptionFromQuery(query: ?{}, opt: string, defaultVal: boolean): boolean {
/* $FlowFixMe: `query` could be empty when it comes from an invalid URL */
if (query[opt] == null) {
return defaultVal;
}
return query[opt] === 'true' || query[opt] === '1';
}
}
function contentsEqual(array: Array<mixed>, set: Set<mixed>): boolean {
return array.length === set.size && array.every(set.has, set);
}
module.exports = Server;

View File

@ -0,0 +1,11 @@
/**
* 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.
*/
'use strict';
module.exports = () => () => {};

View File

@ -0,0 +1,104 @@
/**
* Copyright (c) 2016-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 invariant = require('fbjs/lib/invariant');
type ProcessBatch<TItem, TResult> = (
batch: Array<TItem>,
callback: (error?: Error, orderedResults?: Array<TResult>) => mixed,
) => mixed;
type BatchProcessorOptions = {
maximumDelayMs: number,
maximumItems: number,
concurrency: number,
};
/**
* We batch items together trying to minimize their processing, for example as
* network queries. For that we wait a small moment before processing a batch.
* We limit also the number of items we try to process in a single batch so that
* if we have many items pending in a short amount of time, we can start
* processing right away.
*/
class BatchProcessor<TItem, TResult> {
_options: BatchProcessorOptions;
_processBatch: ProcessBatch<TItem, TResult>;
_queue: Array<{
item: TItem,
callback: (error?: Error, result?: TResult) => mixed,
}>;
_timeoutHandle: ?number;
_currentProcessCount: number;
constructor(
options: BatchProcessorOptions,
processBatch: ProcessBatch<TItem, TResult>,
) {
this._options = options;
this._processBatch = processBatch;
this._queue = [];
this._timeoutHandle = null;
this._currentProcessCount = 0;
(this: any)._processQueue = this._processQueue.bind(this);
}
_processQueue() {
this._timeoutHandle = null;
while (
this._queue.length > 0 &&
this._currentProcessCount < this._options.concurrency
) {
this._currentProcessCount++;
const jobs = this._queue.splice(0, this._options.maximumItems);
const items = jobs.map(job => job.item);
this._processBatch(items, (error, results) => {
invariant(
results == null || results.length === items.length,
'Not enough results returned.',
);
for (let i = 0; i < items.length; ++i) {
jobs[i].callback(error, results && results[i]);
}
this._currentProcessCount--;
this._processQueueOnceReady();
});
}
}
_processQueueOnceReady() {
if (this._queue.length >= this._options.maximumItems) {
clearTimeout(this._timeoutHandle);
process.nextTick(this._processQueue);
return;
}
if (this._timeoutHandle == null) {
this._timeoutHandle = setTimeout(
this._processQueue,
this._options.maximumDelayMs,
);
}
}
queue(
item: TItem,
callback: (error?: Error, result?: TResult) => mixed,
) {
this._queue.push({item, callback});
this._processQueueOnceReady();
}
}
module.exports = BatchProcessor;

Some files were not shown because too many files have changed in this diff Show More