From 1c16124e1c908e83a7ddf26e7c7f945e9423d390 Mon Sep 17 00:00:00 2001 From: David Aurelio Date: Thu, 17 Nov 2016 08:48:29 -0800 Subject: [PATCH] Add tests for worker code Summary: This adds unit tests for the new buck worker code, including a test for source map generation. Reviewed By: cpojer Differential Revision: D4193657 fbshipit-source-id: 06f7bfb5efa4f411178543a728ac7e42511caa3c --- react-packager/src/ModuleGraph/types.flow.js | 5 +- .../worker/__tests__/optimize-module-test.js | 104 ++++++++ .../worker/__tests__/transform-module-test.js | 232 ++++++++++++++++++ .../src/ModuleGraph/worker/optimize-module.js | 4 +- .../ModuleGraph/worker/transform-module.js | 2 +- 5 files changed, 341 insertions(+), 6 deletions(-) create mode 100644 react-packager/src/ModuleGraph/worker/__tests__/optimize-module-test.js create mode 100644 react-packager/src/ModuleGraph/worker/__tests__/transform-module-test.js diff --git a/react-packager/src/ModuleGraph/types.flow.js b/react-packager/src/ModuleGraph/types.flow.js index c920906f..a1af5edf 100644 --- a/react-packager/src/ModuleGraph/types.flow.js +++ b/react-packager/src/ModuleGraph/types.flow.js @@ -93,10 +93,9 @@ export type PackageData = {| 'react-native'?: Object | string, |}; -export type TransformFnResult = {| +export type TransformFnResult = { ast: Object, - map?: Object, -|}; +}; export type TransformFn = ( data: {| diff --git a/react-packager/src/ModuleGraph/worker/__tests__/optimize-module-test.js b/react-packager/src/ModuleGraph/worker/__tests__/optimize-module-test.js new file mode 100644 index 00000000..ff9c5af5 --- /dev/null +++ b/react-packager/src/ModuleGraph/worker/__tests__/optimize-module-test.js @@ -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. + */ +'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', done => { + optimizeModule(transformResult, optimizationOptions, (error, result) => { + const expected = JSON.parse(transformResult); + delete expected.transformed; + expect(result).toEqual(objectContaining(expected)); + done(); + }); + }); + + describe('code optimization', () => { + let dependencyMapName, injectedVars, optimized, requireName; + beforeAll(done => { + optimizeModule(transformResult, optimizationOptions, (error, result) => { + optimized = result.transformed.default; + injectedVars = optimized.code.match(/function\(([^)]*)/)[1].split(','); + [requireName,,,, dependencyMapName] = injectedVars; + done(); + }); + }); + + 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', done => { + optimizeModule( + transformResult, + {...optimizationOptions, isPolyfill: true}, + (error, result) => { + expect(result.transformed.default.dependencies).toEqual([]); + done(); + }, + ); + }); + }); +}); + +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}; + } + } +} diff --git a/react-packager/src/ModuleGraph/worker/__tests__/transform-module-test.js b/react-packager/src/ModuleGraph/worker/__tests__/transform-module-test.js new file mode 100644 index 00000000..b7c56b99 --- /dev/null +++ b/react-packager/src/ModuleGraph/worker/__tests__/transform-module-test.js @@ -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 `isPolyfill` to `false` by default', done => { + transformModule(sourceCode, options(), (error, result) => { + expect(result).toEqual(objectContaining({isPolyfill: false})); + done(); + }); + }); + + it('sets `isPolyfill` to `true` if the input is a polyfill', done => { + transformModule(sourceCode, {...options(), polyfill: true}, (error, result) => { + expect(result).toEqual(objectContaining({isPolyfill: true})); + 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(require,module,global,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(require,module,global,exports,${ + dev.dependencyMapName}){arbitrary(code);});` + ); + expect(prod.code.replace(/\s+/g, '')) + .toEqual( + `__d(function(require,module,global,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(require,module,global,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, + }; +} diff --git a/react-packager/src/ModuleGraph/worker/optimize-module.js b/react-packager/src/ModuleGraph/worker/optimize-module.js index 4873757d..2f616568 100644 --- a/react-packager/src/ModuleGraph/worker/optimize-module.js +++ b/react-packager/src/ModuleGraph/worker/optimize-module.js @@ -21,9 +21,9 @@ const sourceMap = require('source-map'); import type {Callback, TransformedFile, TransformResult} from '../types.flow'; export type OptimizationOptions = {| - dev?: boolean, + dev: boolean, isPolyfill?: boolean, - platform?: string, + platform: string, |}; function optimizeModule( diff --git a/react-packager/src/ModuleGraph/worker/transform-module.js b/react-packager/src/ModuleGraph/worker/transform-module.js index ee7c58bc..805f75ba 100644 --- a/react-packager/src/ModuleGraph/worker/transform-module.js +++ b/react-packager/src/ModuleGraph/worker/transform-module.js @@ -88,7 +88,7 @@ function transformJSON(json, options, callback) { const code = `__d(function(${moduleFactoryParameters.join(', ')}) { module.exports = \n${ json - }\n})`; + }\n});`; const moduleData = { code,