Move fbobjc bundle command to cli

Reviewed By: @frantic

Differential Revision: D2457072
This commit is contained in:
Martín Bigio 2015-10-05 09:15:09 -07:00 committed by facebook-github-bot-0
parent fe477fbdb4
commit 7ad418a396
15 changed files with 590 additions and 20 deletions

View File

@ -8,9 +8,7 @@
*/
'use strict';
require('babel-core/register')({
only: /react-packager\/src/
});
require('babel-core/register')();
useGracefulFs();

View File

@ -8,11 +8,8 @@
*/
'use strict';
require('babel-core/register')({
only: [
/react-native-github\/private-cli\/src/
],
});
// trigger babel-core/register
require('../packager/react-packager');
var cli = require('./src/cli');
var fs = require('fs');

View File

@ -0,0 +1,15 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
'use strict';
function sign(source) {
return source;
}
module.exports = sign;

View File

@ -0,0 +1,59 @@
/**
* 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('../getAssetDestPathAndroid');
const getAssetDestPathAndroid = require('../getAssetDestPathAndroid');
describe('getAssetDestPathAndroid', () => {
it('should use the right destination folder', () => {
const asset = {
name: 'icon',
type: 'png',
httpServerLocation: '/assets/test',
};
const expectDestPathForScaleToStartWith = (scale, path) => {
if (!getAssetDestPathAndroid(asset, scale).startsWith(path)) {
throw new Error(`asset for scale ${scale} should start with path '${path}'`);
}
};
expectDestPathForScaleToStartWith(1, 'drawable-mdpi');
expectDestPathForScaleToStartWith(1.5, 'drawable-hdpi');
expectDestPathForScaleToStartWith(2, 'drawable-xhdpi');
expectDestPathForScaleToStartWith(3, 'drawable-xxhdpi');
expectDestPathForScaleToStartWith(4, 'drawable-xxxhdpi');
});
it('should lowercase path', () => {
const asset = {
name: 'Icon',
type: 'png',
httpServerLocation: '/assets/App/Test',
};
expect(getAssetDestPathAndroid(asset, 1)).toBe(
'drawable-mdpi/app_test_icon.png'
);
});
it('should remove `assets/` prefix', () => {
const asset = {
name: 'icon',
type: 'png',
httpServerLocation: '/assets/RKJSModules/Apps/AndroidSample/Assets',
};
expect(
getAssetDestPathAndroid(asset, 1).startsWith('assets_')
).toBeFalsy();
});
});

View File

@ -0,0 +1,36 @@
/**
* 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('../getAssetDestPathIOS');
const getAssetDestPathIOS = require('../getAssetDestPathIOS');
describe('getAssetDestPathIOS', () => {
it('should build correct path', () => {
const asset = {
name: 'icon',
type: 'png',
httpServerLocation: '/assets/test',
};
expect(getAssetDestPathIOS(asset, 1)).toBe('assets/test/icon.png');
});
it('should consider scale', () => {
const asset = {
name: 'icon',
type: 'png',
httpServerLocation: '/assets/test',
};
expect(getAssetDestPathIOS(asset, 2)).toBe('assets/test/icon@2x.png');
expect(getAssetDestPathIOS(asset, 3)).toBe('assets/test/icon@3x.png');
});
});

View File

@ -0,0 +1,63 @@
/**
* 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('../saveBundleAndMap')
.dontMock('os-tmpdir')
.dontMock('temp');
jest.mock('fs');
const saveBundleAndMap = require('../saveBundleAndMap');
const fs = require('fs');
const temp = require('temp');
const code = 'const foo = "bar";';
const map = JSON.stringify({
version: 3,
file: 'foo.js.map',
sources: ['foo.js'],
sourceRoot: '/',
names: ['bar'],
mappings: 'AAA0B,kBAAhBA,QAAOC,SACjBD,OAAOC,OAAO'
});
describe('saveBundleAndMap', () => {
beforeEach(() => {
fs.writeFileSync = jest.genMockFn();
});
it('should save bundle', () => {
const codeWithMap = {code: code};
const bundleOutput = temp.path({suffix: '.bundle'});
saveBundleAndMap(
codeWithMap,
'ios',
bundleOutput
);
expect(fs.writeFileSync.mock.calls[0]).toEqual([bundleOutput, code]);
});
it('should save sourcemaps if required so', () => {
const codeWithMap = {code: code, map: map};
const bundleOutput = temp.path({suffix: '.bundle'});
const sourceMapOutput = temp.path({suffix: '.map'});
saveBundleAndMap(
codeWithMap,
'ios',
bundleOutput,
sourceMapOutput
);
expect(fs.writeFileSync.mock.calls[1]).toEqual([sourceMapOutput, map]);
});
});

View File

@ -0,0 +1,96 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
'use strict';
const log = require('../util/log').out('bundle');
const parseCommandLine = require('../../../packager/parseCommandLine');
const processBundle = require('./processBundle');
const Promise = require('promise');
const ReactPackager = require('../../../packager/react-packager');
const saveBundleAndMap = require('./saveBundleAndMap');
/**
* Builds the bundle starting to look for dependencies at the given entry path.
*/
function bundle(argv, config) {
return new Promise((resolve, reject) => {
_bundle(argv, config, resolve, reject);
});
}
function _bundle(argv, config, resolve, reject) {
const args = parseCommandLine([
{
command: 'entry-file',
description: 'Path to the root JS file, either absolute or relative to JS root',
type: 'string',
required: true,
}, {
command: 'platform',
description: 'Either "ios" or "android"',
type: 'string',
required: true,
}, {
command: 'dev',
description: 'If false, warnings are disabled and the bundle is minified',
default: true,
}, {
command: 'bundle-output',
description: 'File name where to store the resulting bundle, ex. /tmp/groups.bundle',
type: 'string',
required: true,
}, {
command: 'sourcemap-output',
description: 'File name where to store the sourcemap file for resulting bundle, ex. /tmp/groups.map',
type: 'string',
}, {
command: 'assets-dest',
description: 'Directory name where to store assets referenced in the bundle',
type: 'string',
}
], argv);
// This is used by a bazillion of npm modules we don't control so we don't
// have other choice than defining it as an env variable here.
process.env.NODE_ENV = args.dev ? 'development' : 'production';
const options = {
projectRoots: config.getProjectRoots(),
assetRoots: config.getAssetRoots(),
blacklistRE: config.getBlacklistRE(),
transformModulePath: config.getTransformModulePath(),
};
const requestOpts = {
entryFile: args['entry-file'],
dev: args.dev,
minify: !args.dev,
platform: args.platform,
};
resolve(ReactPackager.createClientFor(options).then(client => {
log('Created ReactPackager');
return client.buildBundle(requestOpts)
.then(outputBundle => {
log('Closing client');
client.close();
return outputBundle;
})
.then(outputBundle => processBundle(outputBundle, !args.dev))
.then(outputBundle => saveBundleAndMap(
outputBundle,
args.platform,
args['bundle-output'],
args['sourcemap-output'],
args['assets-dest']
));
}));
}
module.exports = bundle;

View File

@ -0,0 +1,43 @@
/**
* 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 path = require('path');
function getAndroidAssetSuffix(scale) {
switch (scale) {
case 0.75: return 'ldpi';
case 1: return 'mdpi';
case 1.5: return 'hdpi';
case 2: return 'xhdpi';
case 3: return 'xxhdpi';
case 4: return 'xxxhdpi';
}
}
function getAssetDestPathAndroid(asset, scale) {
var suffix = getAndroidAssetSuffix(scale);
if (!suffix) {
throw new Error(
'Don\'t know which android drawable suffix to use for asset: ' +
JSON.stringify(asset)
);
}
const androidFolder = 'drawable-' + suffix;
// TODO: reuse this logic from https://fburl.com/151101135
const fileName = (asset.httpServerLocation.substr(1) + '/' + asset.name)
.toLowerCase()
.replace(/\//g, '_') // Encode folder structure in file name
.replace(/([^a-z0-9_])/g, '') // Remove illegal chars
.replace(/^assets_/, ''); // Remove "assets_" prefix
return path.join(androidFolder, fileName + '.' + asset.type);
}
module.exports = getAssetDestPathAndroid;

View File

@ -0,0 +1,19 @@
/**
* 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 path = require('path');
function getAssetDestPathIOS(asset, scale) {
const suffix = scale === 1 ? '' : '@' + scale + 'x';
const fileName = asset.name + suffix + '.' + asset.type;
return path.join(asset.httpServerLocation.substr(1), fileName);
}
module.exports = getAssetDestPathIOS;

View File

@ -0,0 +1,29 @@
/**
* 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 log = require('../util/log').out('bundle');
function processBundle(input, shouldMinify) {
log('start');
let bundle;
if (shouldMinify) {
bundle = input.getMinifiedSourceAndMap();
} else {
bundle = {
code: input.getSource(),
map: JSON.stringify(input.getSourceMap()),
};
}
bundle.assets = input.getAssets();
log('finish');
return bundle;
}
module.exports = processBundle;

View File

@ -0,0 +1,96 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
'use strict';
const execFile = require('child_process').execFile;
const fs = require('fs');
const getAssetDestPathAndroid = require('./getAssetDestPathAndroid');
const getAssetDestPathIOS = require('./getAssetDestPathIOS');
const log = require('../util/log').out('bundle');
const path = require('path');
const sign = require('./sign');
function saveBundleAndMap(
codeWithMap,
platform,
bundleOutput,
sourcemapOutput,
assetsDest
) {
log('Writing bundle output to:', bundleOutput);
fs.writeFileSync(bundleOutput, sign(codeWithMap.code));
log('Done writing bundle output');
if (sourcemapOutput) {
log('Writing sourcemap output to:', sourcemapOutput);
fs.writeFileSync(sourcemapOutput, codeWithMap.map);
log('Done writing sourcemap output');
}
if (!assetsDest) {
console.warn('Assets destination folder is not set, skipping...');
return Promise.resolve();
}
const getAssetDestPath = platform === 'android'
? getAssetDestPathAndroid
: getAssetDestPathIOS;
const filesToCopy = Object.create(null); // Map src -> dest
codeWithMap.assets
.filter(asset => !asset.deprecated)
.forEach(asset =>
asset.scales.forEach((scale, idx) => {
const src = asset.files[idx];
const dest = path.join(assetsDest, getAssetDestPath(asset, scale));
filesToCopy[src] = dest;
})
);
return copyAll(filesToCopy);
}
function copyAll(filesToCopy) {
const queue = Object.keys(filesToCopy);
if (queue.length === 0) {
return Promise.resolve();
}
log('Copying ' + queue.length + ' asset files');
return new Promise((resolve, reject) => {
const copyNext = (error) => {
if (error) {
return reject(error);
}
if (queue.length === 0) {
log('Done copying assets');
resolve();
} else {
const src = queue.shift();
const dest = filesToCopy[src];
copy(src, dest, copyNext);
}
};
copyNext();
});
}
function copy(src, dest, callback) {
const destDir = path.dirname(dest);
execFile('mkdir', ['-p', destDir], err => {
if (err) {
return callback(err);
}
fs.createReadStream(src)
.pipe(fs.createWriteStream(dest))
.on('finish', callback);
});
}
module.exports = saveBundleAndMap;

View File

@ -0,0 +1,21 @@
/**
* 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 signedsource = require('./signedsource');
const util = require('util');
function sign(source) {
var ssToken = util.format('<<%sSource::*O*zOeWoEQle#+L!plEphiEmie@IsG>>', 'Signed');
var signedPackageText =
source + util.format('\n__SSTOKENSTRING = "@%s %s";\n', 'generated', ssToken);
return signedsource.sign(signedPackageText).signed_data;
}
module.exports = sign;

View File

@ -0,0 +1,96 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
'use strict';
var TOKEN = '<<SignedSource::*O*zOeWoEQle#+L!plEphiEmie@IsG>>',
OLDTOKEN = '<<SignedSource::*O*zOeWoEQle#+L!plEphiEmie@I>>',
TOKENS = [TOKEN, OLDTOKEN],
PATTERN = new RegExp('@'+'generated (?:SignedSource<<([a-f0-9]{32})>>)');
exports.SIGN_OK = {message:'ok'};
exports.SIGN_UNSIGNED = new Error('unsigned');
exports.SIGN_INVALID = new Error('invalid');
// Thrown by sign(). Primarily for unit tests.
exports.TokenNotFoundError = new Error(
'Code signing placeholder not found (expected to find \''+TOKEN+'\')');
var md5_hash_hex;
// MD5 hash function for Node.js. To port this to other platforms, provide an
// alternate code path for defining the md5_hash_hex function.
var crypto = require('crypto');
md5_hash_hex = function md5_hash_hex(data, input_encoding) {
var md5sum = crypto.createHash('md5');
md5sum.update(data, input_encoding);
return md5sum.digest('hex');
};
// Returns the signing token to be embedded, generally in a header comment,
// in the file you wish to be signed.
//
// @return str to be embedded in to-be-signed file
function signing_token() {
return '@'+'generated '+TOKEN;
}
exports.signing_token = signing_token;
// Determine whether a file is signed. This does NOT verify the signature.
//
// @param str File contents as a string.
// @return bool True if the file has a signature.
function is_signed(file_data) {
return !!PATTERN.exec(file_data);
}
exports.is_signed = is_signed;
// Sign a source file which you have previously embedded a signing token
// into. Signing modifies only the signing token, so the semantics of the
// file will not change if you've put it in a comment.
//
// @param str File contents as a string (with embedded token).
// @return str Signed data.
function sign(file_data) {
var first_time = file_data.indexOf(TOKEN) !== -1;
if (!first_time) {
if (is_signed(file_data))
file_data = file_data.replace(PATTERN, signing_token());
else
throw exports.TokenNotFoundError;
}
var signature = md5_hash_hex(file_data, 'utf8');
var signed_data = file_data.replace(TOKEN, 'SignedSource<<'+signature+'>>');
return { first_time: first_time, signed_data: signed_data };
}
exports.sign = sign;
// Verify a file's signature.
//
// @param str File contents as a string.
// @return Returns SIGN_OK if the data contains a valid signature,
// SIGN_UNSIGNED if it contains no signature, or SIGN_INVALID if
// it contains an invalid signature.
function verify_signature(file_data) {
var match = PATTERN.exec(file_data);
if (!match)
return exports.SIGN_UNSIGNED;
// Replace the signature with the TOKEN, then hash and see if it matches
// the value in the file. For backwards compatibility, also try with
// OLDTOKEN if that doesn't match.
var k, token, with_token, actual_md5, expected_md5 = match[1];
for (k in TOKENS) {
token = TOKENS[k];
with_token = file_data.replace(PATTERN, '@'+'generated '+token);
actual_md5 = md5_hash_hex(with_token, 'utf8');
if (expected_md5 === actual_md5)
return exports.SIGN_OK;
}
return exports.SIGN_INVALID;
}
exports.verify_signature = verify_signature;

View File

@ -8,11 +8,13 @@
*/
'use strict';
const bundle = require('./bundle/bundle');
const Config = require('./util/Config');
const dependencies = require('./dependencies/dependencies');
const Promise = require('promise');
const documentedCommands = {
bundle: bundle,
dependencies: dependencies,
};

View File

@ -18,13 +18,13 @@ const ReactPackager = require('../../../packager/react-packager');
/**
* Returns the dependencies an entry path has.
*/
function dependencies(argv, conf) {
function dependencies(argv, config) {
return new Promise((resolve, reject) => {
_dependencies(argv, conf, resolve, reject);
_dependencies(argv, config, resolve, reject);
});
}
function _dependencies(argv, conf, resolve, reject) {
function _dependencies(argv, config, resolve, reject) {
const args = parseCommandLine([
{
command: 'entry-file',
@ -47,14 +47,14 @@ function _dependencies(argv, conf, resolve, reject) {
reject(`File ${rootModuleAbsolutePath} does not exist`);
}
const config = {
projectRoots: conf.getProjectRoots(),
assetRoots: conf.getAssetRoots(),
blacklistRE: conf.getBlacklistRE(),
transformModulePath: conf.getTransformModulePath(),
const packageOpts = {
projectRoots: config.getProjectRoots(),
assetRoots: config.getAssetRoots(),
blacklistRE: config.getBlacklistRE(),
transformModulePath: config.getTransformModulePath(),
};
const relativePath = config.projectRoots.map(root =>
const relativePath = packageOpts.projectRoots.map(root =>
path.relative(
root,
rootModuleAbsolutePath
@ -73,7 +73,7 @@ function _dependencies(argv, conf, resolve, reject) {
log('Running ReactPackager');
log('Waiting for the packager.');
resolve(ReactPackager.createClientFor(config).then(client => {
resolve(ReactPackager.createClientFor(packageOpts).then(client => {
log('Packager client was created');
return client.getOrderedDependencyPaths(options)
.then(deps => {
@ -85,8 +85,8 @@ function _dependencies(argv, conf, resolve, reject) {
// Long term, we need either
// (a) JS code to not depend on anything outside this directory, or
// (b) Come up with a way to declare this dependency in Buck.
const isInsideProjectRoots = config.projectRoots.filter(root =>
modulePath.startsWith(root)
const isInsideProjectRoots = packageOpts.projectRoots.filter(
root => modulePath.startsWith(root)
).length > 0;
if (isInsideProjectRoots) {