Remove react-packager indirection.

Summary:
This moves the `src` directory one level up and removes the `react-packager` folder. Personally, I always disliked this indirection. I'm reorganizing some things in RNP, so this seems to make sense.

Not sure if I forgot to update any paths. Can anyone advice if there are more places that need change?

Reviewed By: jeanlauliac

Differential Revision: D4487867

fbshipit-source-id: d63f9c79d6238300df9632d2e6a4e6a4196d5ccb
This commit is contained in:
Christoph Pojer
2017-02-02 05:30:03 -08:00
committed by Facebook Github Bot
parent 0ecc4047af
commit a2c84d14ce
156 changed files with 121 additions and 81 deletions

View File

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

View File

@@ -0,0 +1,101 @@
/**
* Copyright (c) 2016-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
*/
'use strict';
const defaults = require('../../defaults');
const nullthrows = require('fbjs/lib/nullthrows');
const parallel = require('async/parallel');
const seq = require('async/seq');
const {virtualModule} = require('./output/util');
import type {
Callback,
GraphFn,
GraphResult,
Module,
} from './types.flow';
type BuildFn = (
entryPoints: Iterable<string>,
options: BuildOptions,
callback: Callback<{modules: Iterable<Module>, entryModules: Iterable<Module>}>,
) => void;
type BuildOptions = {|
optimize?: boolean,
platform?: string,
|};
exports.createBuildSetup = (
graph: GraphFn,
translateDefaultsPath: string => string = x => x,
): BuildFn =>
(entryPoints, options, callback) => {
const {
optimize = false,
platform = defaults.platforms[0],
} = options;
const graphOptions = {optimize};
const graphWithOptions =
(entry, cb) => graph(entry, platform, graphOptions, cb);
const graphOnlyModules = seq(graphWithOptions, getModules);
parallel({
graph: cb => graphWithOptions(
concat(defaults.runBeforeMainModule, entryPoints),
cb,
),
moduleSystem: cb => graphOnlyModules(
[translateDefaultsPath(defaults.moduleSystem)],
cb,
),
polyfills: cb => graphOnlyModules(
defaults.polyfills.map(translateDefaultsPath),
cb,
),
}, (
error: ?Error,
result?: {graph: GraphResult, moduleSystem: Array<Module>, polyfills: Array<Module>},
) => {
if (error) {
callback(error);
return;
}
const {
graph: {modules, entryModules},
moduleSystem,
polyfills,
} = nullthrows(result);
callback(null, {
entryModules,
modules: concat([prelude(optimize)], moduleSystem, polyfills, modules),
});
});
};
const getModules = (x, cb) => cb(null, x.modules);
function* concat<T>(...iterables: Array<Iterable<T>>): Iterable<T> {
for (const it of iterables) {
yield* it;
}
}
function prelude(optimize) {
return virtualModule(
`var __DEV__=${String(!optimize)},__BUNDLE_START_TIME__=Date.now();`
);
}

View File

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

View File

@@ -0,0 +1,119 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
'use strict';
jest.disableAutomock();
const ModuleGraph = require('../ModuleGraph');
const defaults = require('../../../defaults');
const FILE_TYPE = 'module';
describe('build setup', () => {
const buildSetup = ModuleGraph.createBuildSetup(graph);
const noOptions = {};
const noEntryPoints = [];
it('adds a prelude containing start time and `__DEV__` to the build', done => {
buildSetup(noEntryPoints, noOptions, (error, result) => {
expect(error).toEqual(null);
const [prelude] = result.modules;
expect(prelude).toEqual({
dependencies: [],
file: {
code: 'var __DEV__=true,__BUNDLE_START_TIME__=Date.now();',
path: '',
type: 'script',
},
});
done();
});
});
it('sets `__DEV__` to false in the prelude if optimization is enabled', done => {
buildSetup(noEntryPoints, {optimize: true}, (error, result) => {
const [prelude] = result.modules;
expect(prelude.file.code)
.toEqual('var __DEV__=false,__BUNDLE_START_TIME__=Date.now();');
done();
});
});
it('places the module system implementation directly after the prelude', done => {
buildSetup(noEntryPoints, noOptions, (error, result) => {
const [, moduleSystem] = result.modules;
expect(moduleSystem).toEqual({
dependencies: [],
file: {
code: '',
path: defaults.moduleSystem,
type: FILE_TYPE,
},
});
done();
});
});
it('places polyfills after the module system', done => {
buildSetup(noEntryPoints, noOptions, (error, result) => {
const polyfills =
Array.from(result.modules).slice(2, 2 + defaults.polyfills.length);
expect(polyfills).toEqual(defaults.polyfills.map(moduleFromPath));
done();
});
});
it('places all modules from `defaults.runBeforeMainModule` after the polyfills', done => {
buildSetup(noEntryPoints, noOptions, (error, result) => {
const additionalModules =
Array.from(result.modules).slice(-defaults.runBeforeMainModule.length);
expect(additionalModules)
.toEqual(defaults.runBeforeMainModule.map(moduleFromPath));
done();
});
});
it('places all entry points at the end', done => {
const entryPoints = ['a', 'b', 'c'];
buildSetup(entryPoints, noOptions, (error, result) => {
expect(Array.from(result.modules).slice(-3))
.toEqual(entryPoints.map(moduleFromPath));
done();
});
});
it('concatenates `runBeforeMainModule` and entry points as `entryModules`', done => {
const entryPoints = ['a', 'b', 'c'];
buildSetup(entryPoints, noOptions, (error, result) => {
expect(Array.from(result.entryModules)).toEqual(
defaults.runBeforeMainModule.concat(entryPoints).map(moduleFromPath));
done();
});
});
});
function moduleFromPath(path) {
return {
dependencies: [],
file: {
code: '',
path,
type: FILE_TYPE,
},
};
}
function graph(entryPoints, platform, options, callback) {
const modules = Array.from(entryPoints, moduleFromPath);
callback(null, {
entryModules: modules,
modules,
});
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,71 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
*/
'use strict';
'use strict';
import DependencyGraphHelpers from '../../node-haste/DependencyGraph/DependencyGraphHelpers';
type ModuleID = string;
export type Path = string;
type Platform = string;
type Platforms = Set<Platform>;
export type Extensions = Array<string>;
export type Module = {
path: Path,
type: 'Module',
getName(): Promise<ModuleID>,
getPackage(): ?Package,
isHaste(): Promise<boolean>,
};
export type Package = {
path: Path,
root: Path,
type: 'Package',
getMain(): Promise<Path>,
getName(): Promise<ModuleID>,
isHaste(): Promise<boolean>,
redirectRequire(id: ModuleID): Promise<Path | false>,
};
// when changing this to `type`, the code does not typecheck any more
export interface ModuleCache {
getAssetModule(path: Path): Module,
getModule(path: Path): Module,
getPackage(path: Path): Package,
getPackageOf(path: Path): ?Package,
}
export type FastFS = {
dirExists(path: Path): boolean,
closest(path: string, fileName: string): ?string,
fileExists(path: Path): boolean,
getAllFiles(): Array<Path>,
matches(directory: Path, pattern: RegExp): Array<Path>,
};
type HasteMapOptions = {|
extensions: Extensions,
files: Array<string>,
helpers: DependencyGraphHelpers,
moduleCache: ModuleCache,
platforms: Platforms,
preferNativePlatform: true,
|};
declare class HasteMap {
// node-haste/DependencyGraph/HasteMap.js
build(): Promise<Object>,
constructor(options: HasteMapOptions): void,
}

View File

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

View File

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

View File

@@ -0,0 +1,83 @@
/**
* Copyright (c) 2016-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
'use strict';
jest.disableAutomock();
const {match} = require('sinon');
const {fn} = require('../../test-helpers');
const {
addModuleIdsToModuleWrapper,
createIdForPathFn,
} = require('../util');
const {any} = jasmine;
describe('`addModuleIdsToModuleWrapper`:', () => {
const path = 'path/to/file';
const createModule = (dependencies = []) => ({
dependencies,
file: {code: '__d(function(){});', isModule: true, path},
});
it('completes the module wrapped with module ID, and an array of dependency IDs', () => {
const dependencies = [
{id: 'a', path: 'path/to/a.js'},
{id: 'b', path: 'location/of/b.js'},
];
const module = createModule(dependencies);
const idForPath = fn();
idForPath.stub
.withArgs(match({path})).returns(12)
.withArgs(match({path: dependencies[0].path})).returns(345)
.withArgs(match({path: dependencies[1].path})).returns(6);
expect(addModuleIdsToModuleWrapper(module, idForPath))
.toEqual('__d(function(){},12,[345,6]);');
});
it('omits the array of dependency IDs if it is empty', () => {
const module = createModule();
expect(addModuleIdsToModuleWrapper(module, () => 98))
.toEqual(`__d(function(){},${98});`);
});
});
describe('`createIdForPathFn`', () => {
let idForPath;
beforeEach(() => {
idForPath = createIdForPathFn();
});
it('returns a number for a string', () => {
expect(idForPath({path: 'arbitrary'})).toEqual(any(Number));
});
it('returns consecutive numbers', () => {
const strings = [
'arbitrary string',
'looking/like/a/path',
'/absolute/path/to/file.js',
'/more files/are here',
];
strings.forEach((string, i) => {
expect(idForPath({path: string})).toEqual(i);
});
});
it('returns the same id if the same string is passed in again', () => {
const path = 'this/is/an/arbitrary/path.js';
const id = idForPath({path});
idForPath({path: '/other/file'});
idForPath({path: 'and/another/file'});
expect(idForPath({path})).toEqual(id);
});
});

View File

@@ -0,0 +1,47 @@
/**
* Copyright (c) 2016-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
*/
'use strict';
const {createIndexMap} = require('./source-map');
const {addModuleIdsToModuleWrapper} = require('./util');
import type {OutputFn} from '../types.flow';
module.exports = (
(modules, filename, idForPath) => {
let code = '';
let line = 0;
const sections = [];
for (const module of modules) {
const {file} = module;
const moduleCode = file.type === 'module'
? addModuleIdsToModuleWrapper(module, idForPath)
: file.code;
code += moduleCode + '\n';
if (file.map) {
sections.push({
map: file.map,
offset: {column: 0, line}
});
}
line += countLines(moduleCode);
}
return {code, map: createIndexMap({file: filename, sections})};
}: OutputFn);
const reLine = /^/gm;
function countLines(string: string): number {
//$FlowFixMe This regular expression always matches
return string.match(reLine).length;
}

View File

@@ -0,0 +1,48 @@
/**
* Copyright (c) 2016-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
*/
'use strict';
type CreateIndexMapOptions = {|
file?: string,
sections?: Array<IndexMapSection>
|};
type IndexMap = MapBase & {
sections: Array<IndexMapSection>,
};
type IndexMapSection = {
map: IndexMap | MappingsMap,
offset: {line: number, column: number},
};
type MapBase = {
// always the first entry in the source map entry object per
// https://fburl.com/source-map-spec#heading=h.qz3o9nc69um5
version: 3,
file?: string,
};
type MappingsMap = MapBase & {
mappings: string,
names: Array<string>,
sourceRoot?: string,
sources: Array<string>,
sourcesContent?: Array<?string>,
};
export type SourceMap = IndexMap | MappingsMap;
exports.createIndexMap = (opts?: CreateIndexMapOptions): IndexMap => ({
version: 3,
file: opts && opts.file,
sections: opts && opts.sections || [],
});

View File

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

View File

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

View File

@@ -0,0 +1,15 @@
/**
* Copyright (c) 2016-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
'use strict';
const {Console} = require('console');
const {Writable} = require('stream');
const write = (_, __, callback) => callback();
module.exports = new Console(new Writable({write, writev: write}));

View File

@@ -0,0 +1,23 @@
/**
* Copyright (c) 2016-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
'use strict';
const generate = require('babel-generator').default;
const stub = require('sinon/lib/sinon/stub');
exports.fn = () => {
const s = stub();
const f = jest.fn(s);
f.stub = s;
return f;
};
const generateOptions = {concise: true};
exports.codeFromAst = ast => generate(ast, generateOptions).code;
exports.comparableCode = code => code.trim().replace(/\s\s+/g, ' ');

View File

@@ -0,0 +1,134 @@
/**
* Copyright (c) 2016-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
*/
'use strict';
import type {SourceMap} from './output/source-map';
import type {Console} from 'console';
export type Callback<A = void, B = void>
= (Error => void)
& ((null | void, A, B) => void);
type Dependency = {|
id: string,
path: string,
|};
export type File = {|
code: string,
map?: ?Object,
path: string,
type: FileTypes,
|};
type FileTypes = 'module' | 'script';
export type GraphFn = (
entryPoints: Iterable<string>,
platform: string,
options?: ?GraphOptions,
callback?: Callback<GraphResult>,
) => void;
type GraphOptions = {|
cwd?: string,
log?: Console,
optimize?: boolean,
skip?: Set<string>,
|};
export type GraphResult = {
entryModules: Array<Module>,
modules: Array<Module>,
};
export type IdForPathFn = {path: string} => number;
export type LoadFn = (
file: string,
options: LoadOptions,
callback: Callback<File, Array<string>>,
) => void;
type LoadOptions = {|
log?: Console,
optimize?: boolean,
platform?: string,
|};
export type Module = {|
dependencies: Array<Dependency>,
file: File,
|};
export type OutputFn = (
modules: Iterable<Module>,
filename?: string,
idForPath: IdForPathFn,
) => OutputResult;
type OutputResult = {
code: string,
map: SourceMap,
};
export type PackageData = {|
browser?: Object | string,
main?: string,
name?: string,
'react-native'?: Object | string,
|};
export type ResolveFn = (
id: string,
source: string,
platform: string,
options?: ResolveOptions,
callback: Callback<string>,
) => void;
type ResolveOptions = {
log?: Console,
};
export type TransformFn = (
data: {|
filename: string,
options?: Object,
plugins?: Array<string | Object | [string | Object, any]>,
sourceCode: string,
|},
callback: Callback<TransformFnResult>
) => void;
export type TransformFnResult = {
ast: Object,
};
export type TransformResult = {|
code: string,
dependencies: Array<string>,
dependencyMapName?: string,
map: ?Object,
|};
export type TransformResults = {[string]: TransformResult};
export type TransformVariants = {[key: string]: Object};
export type TransformedFile = {
code: string,
file: string,
hasteID: ?string,
package?: PackageData,
transformed: TransformResults,
type: FileTypes,
};

View File

@@ -0,0 +1,25 @@
/**
* Copyright (c) 2016-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
*/
'use strict';
const asyncify = require('async/asyncify');
const optimizeModule = require('./worker/optimize-module');
const transformModule = require('./worker/transform-module');
const wrapWorkerFn = require('./worker/wrap-worker-fn');
import type {OptimizationOptions} from './worker/optimize-module';
import type {TransformOptions} from './worker/transform-module';
import type {WorkerFnWithIO} from './worker/wrap-worker-fn';
exports.optimizeModule =
(wrapWorkerFn(asyncify(optimizeModule)): WorkerFnWithIO<OptimizationOptions>);
exports.transformModule =
(wrapWorkerFn(transformModule): WorkerFnWithIO<TransformOptions>);

View File

@@ -0,0 +1,133 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
'use strict';
jest.disableAutomock();
const collectDependencies = require('../collect-dependencies');
const astFromCode = require('babylon').parse;
const {codeFromAst, comparableCode} = require('../../test-helpers');
const {any} = expect;
describe('dependency collection from ASTs:', () => {
it('collects dependency identifiers from the code', () => {
const ast = astFromCode(`
const a = require('b/lib/a');
exports.do = () => require("do");
if (!something) {
require("setup/something");
}
`);
expect(collectDependencies(ast).dependencies)
.toEqual(['b/lib/a', 'do', 'setup/something']);
});
it('supports template literals as arguments', () => {
const ast = astFromCode('require(`left-pad`)');
expect(collectDependencies(ast).dependencies)
.toEqual(['left-pad']);
});
it('ignores template literals with interpolations', () => {
const ast = astFromCode('require(`left${"-"}pad`)');
expect(collectDependencies(ast).dependencies)
.toEqual([]);
});
it('ignores tagged template literals', () => {
const ast = astFromCode('require(tag`left-pad`)');
expect(collectDependencies(ast).dependencies)
.toEqual([]);
});
it('exposes a string as `dependencyMapName`', () => {
const ast = astFromCode('require("arbitrary")');
expect(collectDependencies(ast).dependencyMapName)
.toEqual(any(String));
});
it('exposes a string as `dependencyMapName` even without collecting dependencies', () => {
const ast = astFromCode('');
expect(collectDependencies(ast).dependencyMapName)
.toEqual(any(String));
});
it('replaces all required module ID strings with array lookups and keeps the ID as second argument', () => {
const ast = astFromCode(`
const a = require('b/lib/a');
const b = require(123);
exports.do = () => require("do");
if (!something) {
require("setup/something");
}
`);
const {dependencyMapName} = collectDependencies(ast);
expect(codeFromAst(ast)).toEqual(comparableCode(`
const a = require(${dependencyMapName}[0], 'b/lib/a');
const b = require(123);
exports.do = () => require(${dependencyMapName}[1], "do");
if (!something) {
require(${dependencyMapName}[2], "setup/something");
}
`));
});
});
describe('Dependency collection from optimized ASTs:', () => {
const dependencyMapName = 'arbitrary';
const {forOptimization} = collectDependencies;
let ast, names;
beforeEach(() => {
ast = astFromCode(`
const a = require(${dependencyMapName}[0], 'b/lib/a');
const b = require(123);
exports.do = () => require(${dependencyMapName}[1], "do");
if (!something) {
require(${dependencyMapName}[2], "setup/something");
}
`);
names = ['b/lib/a', 'do', 'setup/something'];
});
it('passes the `dependencyMapName` through', () => {
const result = forOptimization(ast, names, dependencyMapName);
expect(result.dependencyMapName).toEqual(dependencyMapName);
});
it('returns the list of passed in dependencies', () => {
const result = forOptimization(ast, names, dependencyMapName);
expect(result.dependencies).toEqual(names);
});
it('only returns dependencies that are in the code', () => {
ast = astFromCode(`require(${dependencyMapName}[1], 'do')`);
const result = forOptimization(ast, names, dependencyMapName);
expect(result.dependencies).toEqual(['do']);
});
it('replaces all call signatures inserted by a prior call to `collectDependencies`', () => {
forOptimization(ast, names, dependencyMapName);
expect(codeFromAst(ast)).toEqual(comparableCode(`
const a = require(${dependencyMapName}[0]);
const b = require(123);
exports.do = () => require(${dependencyMapName}[1]);
if (!something) {
require(${dependencyMapName}[2]);
}
`));
});
});

View File

@@ -0,0 +1,97 @@
/**
* Copyright (c) 2016-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
'use strict';
jest.disableAutomock();
const optimizeModule = require('../optimize-module');
const transformModule = require('../transform-module');
const transform = require('../../../../transformer.js');
const {SourceMapConsumer} = require('source-map');
const {objectContaining} = jasmine;
describe('optimizing JS modules', () => {
const filename = 'arbitrary/file.js';
const optimizationOptions = {
dev: false,
platform: 'android',
};
const originalCode =
`if (Platform.OS !== 'android') {
require('arbitrary-dev');
} else {
__DEV__ ? require('arbitrary-android-dev') : require('arbitrary-android-prod');
}`;
let transformResult;
beforeAll(done => {
transformModule(originalCode, {filename, transform}, (error, result) => {
if (error) {
throw error;
}
transformResult = JSON.stringify(result);
done();
});
});
it('copies everything from the transformed file, except for transform results', () => {
const result = optimizeModule(transformResult, optimizationOptions);
const expected = JSON.parse(transformResult);
delete expected.transformed;
expect(result).toEqual(objectContaining(expected));
});
describe('code optimization', () => {
let dependencyMapName, injectedVars, optimized, requireName;
beforeAll(() => {
const result = optimizeModule(transformResult, optimizationOptions);
optimized = result.transformed.default;
injectedVars = optimized.code.match(/function\(([^)]*)/)[1].split(',');
[,requireName,,, dependencyMapName] = injectedVars;
});
it('optimizes code', () => {
expect(optimized.code)
.toEqual(`__d(function(${injectedVars}){${requireName}(${dependencyMapName}[0])});`);
});
it('extracts dependencies', () => {
expect(optimized.dependencies).toEqual(['arbitrary-android-prod']);
});
it('creates source maps', () => {
const consumer = new SourceMapConsumer(optimized.map);
const column = optimized.code.lastIndexOf(requireName + '(');
const loc = findLast(originalCode, 'require');
expect(consumer.originalPositionFor({line: 1, column}))
.toEqual(objectContaining(loc));
});
it('does not extract dependencies for polyfills', () => {
const result = optimizeModule(
transformResult,
{...optimizationOptions, isPolyfill: true},
);
expect(result.transformed.default.dependencies).toEqual([]);
});
});
});
function findLast(code, needle) {
const lines = code.split(/(?:(?!.)\s)+/);
let line = lines.length;
while (line--) {
const column = lines[line].lastIndexOf(needle);
if (column !== -1) {
return {line: line + 1, column};
}
}
}

View File

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

View File

@@ -0,0 +1,89 @@
/**
* Copyright (c) 2016-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
'use strict';
jest
.disableAutomock()
.setMock('fs', jest.genMockFromModule('fs'))
.mock('mkdirp');
const wrapWorkerFn = require('../wrap-worker-fn');
const {dirname} = require('path');
const {fn} = require('../../test-helpers');
const {any} = jasmine;
describe('wrapWorkerFn:', () => {
const infile = '/arbitrary/in/file';
const outfile = '/arbitrary/in/file';
let workerFn, wrapped;
beforeEach(() => {
workerFn = fn();
workerFn.stub.yields();
wrapped = wrapWorkerFn(workerFn);
});
const fs = require('fs');
const mkdirp = require('mkdirp');
it('reads the passed-in file synchronously as UTF-8', done => {
wrapped(infile, outfile, {}, () => {
expect(fs.readFileSync).toBeCalledWith(infile, 'utf8');
done();
});
});
it('calls the worker function with file contents and options', done => {
const contents = 'arbitrary(contents);';
const options = {arbitrary: 'options'};
fs.readFileSync.mockReturnValue(contents);
wrapped(infile, outfile, options, () => {
expect(workerFn).toBeCalledWith(contents, options, any(Function));
done();
});
});
it('passes through any error that the worker function calls back with', done => {
const error = new Error();
workerFn.stub.yields(error);
wrapped(infile, outfile, {}, e => {
expect(e).toBe(error);
done();
});
});
it('writes the result to disk', done => {
const result = {arbitrary: 'result'};
workerFn.stub.yields(null, result);
wrapped(infile, outfile, {}, () => {
expect(mkdirp.sync).toBeCalledWith(dirname(outfile));
expect(fs.writeFileSync).toBeCalledWith(outfile, JSON.stringify(result), 'utf8');
done();
});
});
it('calls back with any error thrown by `mkdirp.sync`', done => {
const error = new Error();
mkdirp.sync.mockImplementationOnce(() => { throw error; });
wrapped(infile, outfile, {}, e => {
expect(e).toBe(error);
done();
});
});
it('calls back with any error thrown by `fs.writeFileSync`', done => {
const error = new Error();
fs.writeFileSync.mockImplementationOnce(() => { throw error; });
wrapped(infile, outfile, {}, e => {
expect(e).toBe(error);
done();
});
});
});

View File

@@ -0,0 +1,150 @@
/**
* Copyright (c) 2016-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
*/
'use strict';
const nullthrows = require('fbjs/lib/nullthrows');
const {traverse, types} = require('babel-core');
type AST = Object;
class Replacement {
nameToIndex: Map<string, number>;
nextIndex: number;
constructor() {
this.nameToIndex = new Map();
this.nextIndex = 0;
}
isRequireCall(callee, firstArg) {
return (
callee.type === 'Identifier' && callee.name === 'require' &&
firstArg && isLiteralString(firstArg)
);
}
getIndex(stringLiteralOrTemplateLiteral) {
const name = stringLiteralOrTemplateLiteral.quasis
? stringLiteralOrTemplateLiteral.quasis[0].value.cooked
: stringLiteralOrTemplateLiteral.value;
let index = this.nameToIndex.get(name);
if (index !== undefined) {
return index;
}
index = this.nextIndex++;
this.nameToIndex.set(name, index);
return index;
}
getNames() {
return Array.from(this.nameToIndex.keys());
}
makeArgs(newId, oldId, dependencyMapIdentifier) {
const mapLookup = createMapLookup(dependencyMapIdentifier, newId);
return [mapLookup, oldId];
}
}
class ProdReplacement {
replacement: Replacement;
names: Array<string>;
constructor(names) {
this.replacement = new Replacement();
this.names = names;
}
isRequireCall(callee, firstArg) {
return (
callee.type === 'Identifier' &&
callee.name === 'require' &&
firstArg &&
firstArg.type === 'MemberExpression' &&
firstArg.property &&
firstArg.property.type === 'NumericLiteral'
);
}
getIndex(memberExpression) {
const id = memberExpression.property.value;
if (id in this.names) {
return this.replacement.getIndex({value: this.names[id]});
}
throw new Error(
`${id} is not a known module ID. Existing mappings: ${
this.names.map((n, i) => `${i} => ${n}`).join(', ')}`
);
}
getNames() {
return this.replacement.getNames();
}
makeArgs(newId, _, dependencyMapIdentifier) {
const mapLookup = createMapLookup(dependencyMapIdentifier, newId);
return [mapLookup];
}
}
function createMapLookup(dependencyMapIdentifier, propertyIdentifier) {
return types.memberExpression(
dependencyMapIdentifier,
propertyIdentifier,
true,
);
}
function collectDependencies(ast, replacement, dependencyMapIdentifier) {
const traversalState = {dependencyMapIdentifier};
traverse(ast, {
Program(path, state) {
if (!state.dependencyMapIdentifier) {
state.dependencyMapIdentifier =
path.scope.generateUidIdentifier('dependencyMap');
}
},
CallExpression(path, state) {
const node = path.node;
const arg = node.arguments[0];
if (replacement.isRequireCall(node.callee, arg)) {
const index = replacement.getIndex(arg);
node.arguments = replacement.makeArgs(
types.numericLiteral(index),
arg,
state.dependencyMapIdentifier,
);
}
},
}, null, traversalState);
return {
dependencies: replacement.getNames(),
dependencyMapName: nullthrows(traversalState.dependencyMapIdentifier).name,
};
}
function isLiteralString(node) {
return node.type === 'StringLiteral' ||
node.type === 'TemplateLiteral' && node.quasis.length === 1;
}
exports = module.exports =
(ast: AST) => collectDependencies(ast, new Replacement());
exports.forOptimization =
(ast: AST, names: Array<string>, dependencyMapName?: string) =>
collectDependencies(
ast,
new ProdReplacement(names),
dependencyMapName ? types.identifier(dependencyMapName) : undefined,
);

View File

@@ -0,0 +1,26 @@
/**
* Copyright (c) 2016-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
*/
'use strict';
const babelGenerate = require('babel-generator').default;
function generate(ast: Object, filename: string, sourceCode: string) {
return babelGenerate(ast, {
comments: false,
compact: true,
filename,
sourceFileName: filename,
sourceMaps: true,
sourceMapTarget: filename,
}, sourceCode);
}
module.exports = generate;

View File

@@ -0,0 +1,108 @@
/**
* Copyright (c) 2016-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
*/
'use strict';
const babel = require('babel-core');
const collectDependencies = require('./collect-dependencies');
const constantFolding = require('../../JSTransformer/worker/constant-folding').plugin;
const generate = require('./generate');
const inline = require('../../JSTransformer/worker/inline').plugin;
const minify = require('../../JSTransformer/worker/minify');
const sourceMap = require('source-map');
import type {TransformedFile, TransformResult} from '../types.flow';
export type OptimizationOptions = {|
dev: boolean,
isPolyfill?: boolean,
platform: string,
|};
function optimizeModule(
data: string | TransformedFile,
optimizationOptions: OptimizationOptions,
): TransformedFile {
if (typeof data === 'string') {
data = JSON.parse(data);
}
const {code, file, transformed} = data;
const result = {...data, transformed: {}};
//$FlowIssue #14545724
Object.entries(transformed).forEach(([k, t: TransformResult]: [*, TransformResult]) => {
result.transformed[k] = optimize(t, file, code, optimizationOptions);
});
return result;
}
function optimize(transformed, file, originalCode, options): TransformResult {
const {code, dependencyMapName, map} = transformed;
const optimized = optimizeCode(code, map, file, options);
let dependencies;
if (options.isPolyfill) {
dependencies = [];
} else {
({dependencies} = collectDependencies.forOptimization(
optimized.ast,
transformed.dependencies,
dependencyMapName,
));
}
const inputMap = transformed.map;
const gen = generate(optimized.ast, file, originalCode);
const min = minify(
file,
gen.code,
inputMap && mergeSourceMaps(file, inputMap, gen.map),
);
return {code: min.code, map: inputMap && min.map, dependencies};
}
function optimizeCode(code, map, filename, inliningOptions) {
return babel.transform(code, {
plugins: [
[constantFolding],
[inline, {...inliningOptions, isWrapped: true}],
],
babelrc: false,
code: false,
filename,
});
}
function mergeSourceMaps(file, originalMap, secondMap) {
const merged = new sourceMap.SourceMapGenerator();
const inputMap = new sourceMap.SourceMapConsumer(originalMap);
new sourceMap.SourceMapConsumer(secondMap)
.eachMapping(mapping => {
const original = inputMap.originalPositionFor({
line: mapping.originalLine,
column: mapping.originalColumn,
});
if (original.line == null) {
return;
}
merged.addMapping({
generated: {line: mapping.generatedLine, column: mapping.generatedColumn},
original: {line: original.line, column: original.column || 0},
source: file,
name: original.name || mapping.name,
});
});
return merged.toJSON();
}
module.exports = optimizeModule;

View File

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

View File

@@ -0,0 +1,62 @@
/**
* Copyright (c) 2016-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @flow
*/
'use strict';
const fs = require('fs');
const mkdirp = require('mkdirp');
const {dirname} = require('path');
import type {Callback} from '../types.flow';
type Path = string;
type WorkerFn<Options> = (
fileContents: string,
options: Options,
callback: Callback<Object>,
) => void;
export type WorkerFnWithIO<Options> = (
infile: Path,
outfile: Path,
options: Options,
callback: Callback<>,
) => void;
function wrapWorkerFn<Options>(
workerFunction: WorkerFn<Options>,
): WorkerFnWithIO<Options> {
return (
infile: Path,
outfile: Path,
options: Options,
callback: Callback<>,
) => {
const contents = fs.readFileSync(infile, 'utf8');
workerFunction(contents, options, (error, result) => {
if (error) {
callback(error);
return;
}
try {
mkdirp.sync(dirname(outfile));
fs.writeFileSync(outfile, JSON.stringify(result), 'utf8');
} catch (writeError) {
callback(writeError);
return;
}
callback(null);
});
};
}
module.exports = wrapWorkerFn;