mirror of https://github.com/status-im/metro.git
Merge branch 'master' of ../react-native
This commit is contained in:
commit
a35016bd49
|
@ -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).
|
|
@ -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;
|
|
@ -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;
|
|
@ -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',
|
||||
];
|
|
@ -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
|
|
@ -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
|
|
@ -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 "$@"
|
|
@ -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
|
|
@ -0,0 +1,8 @@
|
|||
*~
|
||||
*.swm
|
||||
*.swn
|
||||
*.swp
|
||||
*.DS_STORE
|
||||
npm-debug.log
|
||||
.cache
|
||||
node_modules
|
|
@ -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');
|
|
@ -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);
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"presets": [ "react-native" ],
|
||||
"plugins": []
|
||||
}
|
307
packages/metro-bundler/react-packager/src/AssetServer/__tests__/AssetServer-test.js
vendored
Normal file
307
packages/metro-bundler/react-packager/src/AssetServer/__tests__/AssetServer-test.js
vendored
Normal 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)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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,
|
||||
});
|
||||
}
|
351
packages/metro-bundler/react-packager/src/Bundler/__tests__/Bundler-test.js
vendored
Normal file
351
packages/metro-bundler/react-packager/src/Bundler/__tests__/Bundler-test.js
vendored
Normal 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',
|
||||
]));
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
126
packages/metro-bundler/react-packager/src/Bundler/source-map/__tests__/B64Builder-test.js
vendored
Normal file
126
packages/metro-bundler/react-packager/src/Bundler/source-map/__tests__/B64Builder-test.js
vendored
Normal 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');
|
||||
});
|
113
packages/metro-bundler/react-packager/src/Bundler/source-map/__tests__/Generator-test.js
vendored
Normal file
113
packages/metro-bundler/react-packager/src/Bundler/source-map/__tests__/Generator-test.js
vendored
Normal 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));
|
||||
});
|
||||
});
|
85
packages/metro-bundler/react-packager/src/Bundler/source-map/__tests__/source-map-test.js
vendored
Normal file
85
packages/metro-bundler/react-packager/src/Bundler/source-map/__tests__/source-map-test.js
vendored
Normal 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');
|
|
@ -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;
|
|
@ -0,0 +1 @@
|
|||
{"main": "source-map.js"}
|
|
@ -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;
|
|
@ -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,
|
||||
};
|
|
@ -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');
|
|
@ -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');
|
|
@ -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, {});
|
||||
};
|
90
packages/metro-bundler/react-packager/src/JSTransformer/__tests__/Transformer-test.js
vendored
Normal file
90
packages/metro-bundler/react-packager/src/JSTransformer/__tests__/Transformer-test.js
vendored
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
122
packages/metro-bundler/react-packager/src/JSTransformer/worker/__tests__/constant-folding-test.js
vendored
Normal file
122
packages/metro-bundler/react-packager/src/JSTransformer/worker/__tests__/constant-folding-test.js
vendored
Normal 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\');}}');
|
||||
});
|
||||
});
|
|
@ -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]);
|
||||
});
|
||||
});
|
306
packages/metro-bundler/react-packager/src/JSTransformer/worker/__tests__/inline-test.js
vendored
Normal file
306
packages/metro-bundler/react-packager/src/JSTransformer/worker/__tests__/inline-test.js
vendored
Normal 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"')));
|
||||
});
|
||||
});
|
57
packages/metro-bundler/react-packager/src/JSTransformer/worker/__tests__/minify-test.js
vendored
Normal file
57
packages/metro-bundler/react-packager/src/JSTransformer/worker/__tests__/minify-test.js
vendored
Normal 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]});
|
||||
});
|
||||
});
|
234
packages/metro-bundler/react-packager/src/JSTransformer/worker/__tests__/worker-test.js
vendored
Normal file
234
packages/metro-bundler/react-packager/src/JSTransformer/worker/__tests__/worker-test.js
vendored
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
91
packages/metro-bundler/react-packager/src/JSTransformer/worker/constant-folding.js
vendored
Normal file
91
packages/metro-bundler/react-packager/src/JSTransformer/worker/constant-folding.js
vendored
Normal 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;
|
47
packages/metro-bundler/react-packager/src/JSTransformer/worker/extract-dependencies.js
vendored
Normal file
47
packages/metro-bundler/react-packager/src/JSTransformer/worker/extract-dependencies.js
vendored
Normal 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;
|
|
@ -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');
|
|
@ -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;
|
|
@ -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;
|
|
@ -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
|
|
@ -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],
|
||||
};
|
|
@ -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,
|
||||
};
|
|
@ -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),
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
};
|
|
@ -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};
|
||||
}
|
|
@ -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();`
|
||||
);
|
||||
}
|
376
packages/metro-bundler/react-packager/src/ModuleGraph/__tests__/Graph-test.js
vendored
Normal file
376
packages/metro-bundler/react-packager/src/ModuleGraph/__tests__/Graph-test.js
vendored
Normal 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;
|
||||
}
|
119
packages/metro-bundler/react-packager/src/ModuleGraph/__tests__/ModuleGraph-test.js
vendored
Normal file
119
packages/metro-bundler/react-packager/src/ModuleGraph/__tests__/ModuleGraph-test.js
vendored
Normal 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,
|
||||
});
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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[\///]/, '');
|
||||
}
|
65
packages/metro-bundler/react-packager/src/ModuleGraph/node-haste/ModuleCache.js
vendored
Normal file
65
packages/metro-bundler/react-packager/src/ModuleGraph/node-haste/ModuleCache.js
vendored
Normal 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;
|
||||
}
|
||||
};
|
138
packages/metro-bundler/react-packager/src/ModuleGraph/node-haste/Package.js
vendored
Normal file
138
packages/metro-bundler/react-packager/src/ModuleGraph/node-haste/Package.js
vendored
Normal 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 };
|
||||
}
|
|
@ -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,
|
||||
}
|
101
packages/metro-bundler/react-packager/src/ModuleGraph/node-haste/node-haste.js
vendored
Normal file
101
packages/metro-bundler/react-packager/src/ModuleGraph/node-haste/node-haste.js
vendored
Normal 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),
|
||||
);
|
||||
};
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
{"main":"node-haste.js"}
|
83
packages/metro-bundler/react-packager/src/ModuleGraph/output/__tests__/util-test.js
vendored
Normal file
83
packages/metro-bundler/react-packager/src/ModuleGraph/output/__tests__/util-test.js
vendored
Normal 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);
|
||||
});
|
||||
});
|
47
packages/metro-bundler/react-packager/src/ModuleGraph/output/as-plain-bundle.js
vendored
Normal file
47
packages/metro-bundler/react-packager/src/ModuleGraph/output/as-plain-bundle.js
vendored
Normal 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;
|
||||
}
|
|
@ -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 || [],
|
||||
});
|
|
@ -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',
|
||||
}
|
||||
};
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
{"main": "ModuleGraph.js"}
|
|
@ -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}));
|
|
@ -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, ' ');
|
|
@ -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,
|
||||
};
|
|
@ -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>);
|
133
packages/metro-bundler/react-packager/src/ModuleGraph/worker/__tests__/collect-dependencies-test.js
vendored
Normal file
133
packages/metro-bundler/react-packager/src/ModuleGraph/worker/__tests__/collect-dependencies-test.js
vendored
Normal 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]);
|
||||
}
|
||||
`));
|
||||
});
|
||||
});
|
97
packages/metro-bundler/react-packager/src/ModuleGraph/worker/__tests__/optimize-module-test.js
vendored
Normal file
97
packages/metro-bundler/react-packager/src/ModuleGraph/worker/__tests__/optimize-module-test.js
vendored
Normal 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};
|
||||
}
|
||||
}
|
||||
}
|
232
packages/metro-bundler/react-packager/src/ModuleGraph/worker/__tests__/transform-module-test.js
vendored
Normal file
232
packages/metro-bundler/react-packager/src/ModuleGraph/worker/__tests__/transform-module-test.js
vendored
Normal 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,
|
||||
};
|
||||
}
|
89
packages/metro-bundler/react-packager/src/ModuleGraph/worker/__tests__/wrap-worker-fn-test.js
vendored
Normal file
89
packages/metro-bundler/react-packager/src/ModuleGraph/worker/__tests__/wrap-worker-fn-test.js
vendored
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
150
packages/metro-bundler/react-packager/src/ModuleGraph/worker/collect-dependencies.js
vendored
Normal file
150
packages/metro-bundler/react-packager/src/ModuleGraph/worker/collect-dependencies.js
vendored
Normal 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,
|
||||
);
|
|
@ -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;
|
108
packages/metro-bundler/react-packager/src/ModuleGraph/worker/optimize-module.js
vendored
Normal file
108
packages/metro-bundler/react-packager/src/ModuleGraph/worker/optimize-module.js
vendored
Normal 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;
|
165
packages/metro-bundler/react-packager/src/ModuleGraph/worker/transform-module.js
vendored
Normal file
165
packages/metro-bundler/react-packager/src/ModuleGraph/worker/transform-module.js
vendored
Normal 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;
|
62
packages/metro-bundler/react-packager/src/ModuleGraph/worker/wrap-worker-fn.js
vendored
Normal file
62
packages/metro-bundler/react-packager/src/ModuleGraph/worker/wrap-worker-fn.js
vendored
Normal 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;
|
548
packages/metro-bundler/react-packager/src/Resolver/__tests__/Resolver-test.js
vendored
Normal file
548
packages/metro-bundler/react-packager/src/Resolver/__tests__/Resolver-test.js
vendored
Normal 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;
|
||||
}
|
||||
});
|
|
@ -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;
|
|
@ -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;
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
|
@ -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,
|
||||
});
|
||||
}
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
})();
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
}
|
|
@ -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']);
|
||||
});
|
||||
});
|
||||
});
|
237
packages/metro-bundler/react-packager/src/Resolver/polyfills/babelHelpers.js
vendored
Normal file
237
packages/metro-bundler/react-packager/src/Resolver/polyfills/babelHelpers.js
vendored
Normal 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);
|
||||
}
|
||||
};
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
|
@ -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;
|
||||
};
|
|
@ -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();
|
|
@ -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();
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
150
packages/metro-bundler/react-packager/src/Server/__tests__/MultipartResponse-test.js
vendored
Normal file
150
packages/metro-bundler/react-packager/src/Server/__tests__/MultipartResponse-test.js
vendored
Normal 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');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
});
|
|
@ -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;
|
|
@ -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 = () => () => {};
|
|
@ -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
Loading…
Reference in New Issue