/*
* Copyright 2011 The Closure Compiler Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.javascript.jscomp;
import com.google.common.collect.ImmutableList;
import com.google.javascript.rhino.Node;
/**
* Unit tests for {@link ProcessCommonJSModules}
*/
public final class ProcessCommonJSModulesTest extends CompilerTestCase {
private ImmutableList<String> moduleRoots = null;
@Override
protected CompilerOptions getOptions() {
CompilerOptions options = super.getOptions();
// Trigger module processing after parsing.
options.setProcessCommonJSModules(true);
if (moduleRoots != null) {
options.setModuleRoots(moduleRoots);
}
return options;
}
@Override
protected CompilerPass getProcessor(Compiler compiler) {
return new CompilerPass() {
@Override
public void process(Node externs, Node root) {
// No-op, CommonJS module handling is done directly after parsing.
}
};
}
@Override
protected int getNumRepetitions() {
return 1;
}
void testModules(String input, String expected) {
ProcessEs6ModulesTest.testModules(this, input, expected);
}
public void testWithoutExports() {
setFilename("test");
testModules(
"var name = require('./other'); name()",
LINE_JOINER.join(
"goog.require('module$other');",
"var name = module$other;",
"module$other();"));
test(
ImmutableList.of(
SourceFile.fromCode(Compiler.joinPathParts("mod", "name.js"), ""),
SourceFile.fromCode(
Compiler.joinPathParts("test", "sub.js"),
LINE_JOINER.join(
"var name = require('../mod/name');",
"(function() { module$mod$name(); })();"))),
ImmutableList.of(
SourceFile.fromCode(
Compiler.joinPathParts("mod", "name.js"),
LINE_JOINER.join(
"/** @fileoverview",
" * @suppress {missingProvide|missingRequire}",
" */",
"goog.provide('module$mod$name');")),
SourceFile.fromCode(
Compiler.joinPathParts("test", "sub.js"),
LINE_JOINER.join(
"goog.require('module$mod$name');",
"var name = module$mod$name;",
"(function() { module$mod$name(); })();"))));
}
public void testExports() {
setFilename("test");
testModules(
LINE_JOINER.join(
"var name = require('./other');",
"exports.foo = 1;"),
LINE_JOINER.join(
"goog.provide('module$test');",
"goog.require('module$other');",
"/** @const */ var module$test = {};",
"var name$$module$test = module$other;",
"module$test.foo = 1;"));
testModules(
LINE_JOINER.join(
"var name = require('./other');",
"module.exports = function() {};"),
LINE_JOINER.join(
"goog.provide('module$test');",
"goog.require('module$other');",
"var name$$module$test = module$other;",
"var module$test = function () {};"));
}
public void testExportsInExpression() {
setFilename("test");
testModules(
LINE_JOINER.join(
"var name = require('./other');",
"var e;",
"e = module.exports = function() {};"),
LINE_JOINER.join(
"goog.provide('module$test');",
"goog.require('module$other');",
"var module$test;",
"var name$$module$test = module$other;",
"var e$$module$test;",
"e$$module$test = module$test = function () {};"));
testModules(
LINE_JOINER.join(
"var name = require('./other');",
"var e = module.exports = function() {};"),
LINE_JOINER.join(
"goog.provide('module$test');",
"goog.require('module$other');",
"var module$test;",
"var name$$module$test = module$other;",
"var e$$module$test = module$test = function () {};"));
testModules(
LINE_JOINER.join(
"var name = require('./other');",
"(module.exports = function() {})();"),
LINE_JOINER.join(
"goog.provide('module$test');",
"goog.require('module$other');",
"var module$test;",
"var name$$module$test = module$other;",
"(module$test = function () {})();"));
}
public void testPropertyExports() {
setFilename("test");
testModules(
LINE_JOINER.join(
"exports.one = 1;",
"module.exports.obj = {};",
"module.exports.obj.two = 2;"),
LINE_JOINER.join(
"goog.provide('module$test');",
"/** @const */ var module$test = {};",
"module$test.one = 1;",
"module$test.obj = {};",
"module$test.obj.two = 2;"));
}
/**
* This rewriting actually produces broken code. The direct assignment to module.exports
* overwrites the property assignment to exports. However this pattern isn't prevalent and hard to
* account for so we'll just see what happens.
*/
public void testModuleExportsWrittenWithExportsRefs() {
setFilename("test");
testModules(
LINE_JOINER.join(
"exports.one = 1;",
"module.exports = {};"),
LINE_JOINER.join(
"goog.provide('module$test');",
"/** @const */ var module$test={};",
"module$test.one = 1;"));
}
public void testVarRenaming() {
setFilename("test");
testModules(
LINE_JOINER.join(
"module.exports = {};",
"var a = 1, b = 2;",
"(function() { var a; b = 4})();"),
LINE_JOINER.join(
"goog.provide('module$test');",
"/** @const */ var module$test={};",
"var a$$module$test = 1, b$$module$test = 2;",
"(function() { var a; b$$module$test = 4})();"));
}
public void testDash() {
setFilename("test-test");
testModules(
LINE_JOINER.join(
"var name = require('./other');",
"exports.foo = 1;"),
LINE_JOINER.join(
"goog.provide('module$test_test');",
"goog.require('module$other');",
"/** @const */ var module$test_test = {};",
"var name$$module$test_test = module$other;",
"module$test_test.foo = 1;"));
}
public void testIndex() {
setFilename("foo/index");
testModules(
LINE_JOINER.join(
"var name = require('../other');",
"exports.bar = 1;"),
LINE_JOINER.join(
"goog.provide('module$foo$index');",
"goog.require('module$other');",
"/** @const */ var module$foo$index={};",
"var name$$module$foo$index = module$other;",
"module$foo$index.bar = 1;"));
}
public void testVarJsdocGoesOnAssignment() {
testModules(
LINE_JOINER.join(
"/**",
" * @const",
" * @enum {number}",
" */",
"var MyEnum = { ONE: 1, TWO: 2 };",
"module.exports = {MyEnum: MyEnum};"),
LINE_JOINER.join(
"goog.provide('module$testcode');",
"/** @const */",
"var module$testcode = {};",
"/**",
" * @const",
" * @enum {number}",
" */",
"(module$testcode.MyEnum = {ONE:1, TWO:2});"));
}
public void testModuleName() {
setFilename("foo/bar");
testModules(
LINE_JOINER.join(
"var name = require('../other');",
"module.exports = name;"),
LINE_JOINER.join(
"goog.provide('module$foo$bar');",
"goog.require('module$other');",
"var name$$module$foo$bar = module$other;",
"var module$foo$bar = module$other;"));
test(
ImmutableList.of(
SourceFile.fromCode(Compiler.joinPathParts("foo", "name.js"), ""),
SourceFile.fromCode(
Compiler.joinPathParts("foo", "bar.js"),
LINE_JOINER.join("var name = require('./name');", "module.exports = name;"))),
ImmutableList.of(
SourceFile.fromCode(
Compiler.joinPathParts("foo", "name.js"),
LINE_JOINER.join(
"/** @fileoverview",
" * @suppress {missingProvide|missingRequire}",
" */",
"goog.provide('module$foo$name');")),
SourceFile.fromCode(
Compiler.joinPathParts("foo", "bar.js"),
LINE_JOINER.join(
"goog.provide('module$foo$bar');",
"goog.require('module$foo$name');",
"var name$$module$foo$bar = module$foo$name;",
"var module$foo$bar = module$foo$name;"))));
}
public void testModuleExportsScope() {
setFilename("test");
testModules(
LINE_JOINER.join(
"var foo = function (module) {",
" module.exports = {};",
"};",
"module.exports = foo;"),
LINE_JOINER.join(
"goog.provide('module$test');",
"var module$test = function (module) {",
" module.exports={};",
"};"));
testModules(
LINE_JOINER.join(
"var foo = function () {",
" var module = {};",
" module.exports = {};",
"};",
"module.exports = foo;"),
LINE_JOINER.join(
"goog.provide('module$test');",
"var module$test = function() {",
" var module={};",
" module.exports={}",
"};"));
testModules(
LINE_JOINER.join(
"var foo = function () {",
" if (true) var module = {};",
" module.exports = {};",
"};",
"module.exports = foo;"),
LINE_JOINER.join(
"goog.provide('module$test');",
"var module$test = function() {",
" if (true) var module={};",
" module.exports={}",
"};"));
}
public void testUMDPatternConversion() {
setFilename("test");
testModules(
LINE_JOINER.join(
"var foobar = {foo: 'bar'};",
"if (typeof module === 'object' && module.exports) {",
" module.exports = foobar;",
"} else if (typeof define === 'function' && define.amd) {",
" define([], function() {return foobar;});",
"} else {",
" this.foobar = foobar;",
"}"),
LINE_JOINER.join(
"goog.provide('module$test');",
"var module$test = {foo: 'bar'};"));
testModules(
LINE_JOINER.join(
"var foobar = {foo: 'bar'};",
"if (typeof define === 'function' && define.amd) {",
" define([], function() {return foobar;});",
"} else if (typeof module === 'object' && module.exports) {",
" module.exports = foobar;",
"} else {",
" this.foobar = foobar;",
"}"),
LINE_JOINER.join(
"goog.provide('module$test');",
"var module$test = {foo: 'bar'};"));
testModules(
LINE_JOINER.join(
"var foobar = {foo: 'bar'};",
"if (typeof module === 'object' && module.exports) {",
" module.exports = foobar;",
"}",
"if (typeof define === 'function' && define.amd) {",
" define([], function () {return foobar;});",
"}"),
LINE_JOINER.join(
"goog.provide('module$test');",
"var module$test = {foo: 'bar'};"));
}
public void testEs6ObjectShorthand() {
setLanguage(
CompilerOptions.LanguageMode.ECMASCRIPT_2015, CompilerOptions.LanguageMode.ECMASCRIPT5);
setFilename("test");
testModules(
LINE_JOINER.join(
"function foo() {}",
"module.exports = {",
" prop: 'value',",
" foo",
"};"),
LINE_JOINER.join(
"goog.provide('module$test');",
"/** @const */ var module$test = {};",
"module$test.foo = function () {};",
"module$test.prop = 'value';"));
testModules(
LINE_JOINER.join(
"module.exports = {",
" prop: 'value',",
" foo() {",
" console.log('bar');",
" }",
"};"),
LINE_JOINER.join(
"goog.provide('module$test');",
"/** @const */ var module$test = {};",
"module$test.prop = 'value';",
"module$test.foo = function() {",
" console.log('bar');",
"};"));
testModules(
LINE_JOINER.join(
"var a = require('./other');",
"module.exports = {a: a};"),
LINE_JOINER.join(
"goog.provide('module$test');",
"goog.require('module$other');",
"/** @const */ var module$test = {};",
"var a$$module$test = module$other;",
"module$test.a = module$other;"));
testModules(
LINE_JOINER.join(
"var a = require('./other');",
"module.exports = {a};"),
LINE_JOINER.join(
"goog.provide('module$test');",
"goog.require('module$other');",
"/** @const */ var module$test = {};",
"var a$$module$test = module$other;",
"module$test.a = module$other;"));
testModules(
LINE_JOINER.join(
"var a = 4;",
"module.exports = {a};"),
LINE_JOINER.join(
"goog.provide('module$test');",
"/** @const */ var module$test = {};",
"module$test.a = 4;"));
}
public void testRequireResultUnused() {
setFilename("test");
testModules("require('./other');", "goog.require('module$other');");
}
public void testRequireEnsure() {
setFilename("test");
testModules(
LINE_JOINER.join(
"require.ensure(['./other'], function(require) {",
" var other = require('./other');",
" var bar = other;",
"});"),
LINE_JOINER.join(
"goog.require('module$other');",
"(function() {",
" var other=module$other;",
" var bar = module$other;",
"})()"));
}
public void testFunctionRewriting() {
setFilename("test");
testModules(
LINE_JOINER.join(
"function foo() {}",
"foo.prototype = new Date();",
"module.exports = foo;"),
LINE_JOINER.join(
"goog.provide('module$test');",
"var module$test = function() {};",
"module$test.prototype = new Date();"));
testModules(
LINE_JOINER.join(
"function foo() {}",
"foo.prototype = new Date();",
"module.exports = {foo: foo};"),
LINE_JOINER.join(
"goog.provide('module$test');",
"/** @const */ var module$test = {};",
"module$test.foo = function () {}",
"module$test.foo.prototype = new Date();"));
}
public void testFunctionHoisting() {
setFilename("test");
testModules(
LINE_JOINER.join(
"module.exports = foo;",
"function foo() {}",
"foo.prototype = new Date();"),
LINE_JOINER.join(
"goog.provide('module$test');",
"var module$test = function() {};",
"module$test.prototype = new Date();"));
testModules(
LINE_JOINER.join(
"function foo() {}",
"Object.assign(foo, { bar: foobar });",
"function foobar() {}",
"module.exports = foo;",
"module.exports.bar = foobar;"),
LINE_JOINER.join(
"goog.provide('module$test');",
"var module$test = function () {};",
"module$test.bar = function() {};",
"Object.assign(module$test, { bar: module$test.bar });"));
}
public void testClassRewriting() {
setLanguage(
CompilerOptions.LanguageMode.ECMASCRIPT_2015, CompilerOptions.LanguageMode.ECMASCRIPT5);
setFilename("test");
testModules(
LINE_JOINER.join(
"class foo extends Array {}",
"module.exports = foo;"),
LINE_JOINER.join(
"goog.provide('module$test');",
"let module$test = class extends Array {}"));
testModules(
LINE_JOINER.join(
"class foo {}",
"module.exports = foo;"),
LINE_JOINER.join(
"goog.provide('module$test');",
"let module$test = class {}"));
testModules(
LINE_JOINER.join(
"class foo {}",
"module.exports.foo = foo;"),
LINE_JOINER.join(
"goog.provide('module$test');",
"/** @const */ var module$test = {};",
"module$test.foo = class {};"));
testModules(
LINE_JOINER.join(
"module.exports = class Foo {",
" /** @this {Foo} */",
" bar() { return 'bar'; }",
"};"),
LINE_JOINER.join(
"goog.provide('module$test');",
"var module$test = class {",
" /** @this {module$test} */",
" bar() { return 'bar'; }",
"};"));
}
public void testMultipleAssignments() {
setLanguage(
CompilerOptions.LanguageMode.ECMASCRIPT_2015, CompilerOptions.LanguageMode.ECMASCRIPT5);
setExpectParseWarningsThisTest();
setFilename("test");
testModules(
LINE_JOINER.join(
"/** @constructor */ function Hello() {}",
"module.exports = Hello;",
"/** @constructor */ function Bar() {} ",
"Bar.prototype.foobar = function() { alert('foobar'); };",
"exports = Bar;"),
LINE_JOINER.join(
"goog.provide('module$test')",
"var module$test = /** @constructor */ function(){};",
"/** @constructor */ function Bar$$module$test(){}",
"Bar$$module$test.prototype.foobar = function() { alert('foobar'); };",
"exports = Bar$$module$test;"));
}
public void testDestructuringImports() {
setLanguage(
CompilerOptions.LanguageMode.ECMASCRIPT_2015, CompilerOptions.LanguageMode.ECMASCRIPT5);
setFilename("test");
testModules(
LINE_JOINER.join(
"const {foo, bar} = require('./other');",
"var baz = foo + bar;"),
LINE_JOINER.join(
"goog.require('module$other');",
"const {foo, bar} = module$other;",
"var baz = module$other.foo + module$other.bar;"));
}
public void testAnnotationsCopied() {
setLanguage(
CompilerOptions.LanguageMode.ECMASCRIPT_2015, CompilerOptions.LanguageMode.ECMASCRIPT5);
setFilename("test");
testModules(
LINE_JOINER.join(
"/** @interface */ var a;",
"/** @type {string} */ a.prototype.foo;",
"module.exports.a = a;"),
LINE_JOINER.join(
"goog.provide('module$test');",
"/** @const */ var module$test = {};",
"/** @interface */ module$test.a;",
"/** @type {string} */ module$test.a.prototype.foo;"));
}
public void testUMDRemoveIIFE() {
setFilename("test");
testModules(
LINE_JOINER.join(
"(function(){",
"var foobar = {foo: 'bar'};",
"if (typeof module === 'object' && module.exports) {",
" module.exports = foobar;",
"} else if (typeof define === 'function' && define.amd) {",
" define([], function() {return foobar;});",
"} else {",
" this.foobar = foobar;",
"}})()"),
LINE_JOINER.join(
"goog.provide('module$test');",
"var module$test = {foo: 'bar'};"));
testModules(
LINE_JOINER.join(
";;;(function(){",
"var foobar = {foo: 'bar'};",
"if (typeof module === 'object' && module.exports) {",
" module.exports = foobar;",
"} else if (typeof define === 'function' && define.amd) {",
" define([], function() {return foobar;});",
"} else {",
" this.foobar = foobar;",
"}})()"),
LINE_JOINER.join(
"goog.provide('module$test');",
"var module$test = {foo: 'bar'};"));
testModules(
LINE_JOINER.join(
"(function(){",
"var foobar = {foo: 'bar'};",
"if (typeof module === 'object' && module.exports) {",
" module.exports = foobar;",
"} else if (typeof define === 'function' && define.amd) {",
" define([], function() {return foobar;});",
"} else {",
" this.foobar = foobar;",
"}}.call(this))"),
LINE_JOINER.join(
"goog.provide('module$test');",
"var module$test = {foo: 'bar'};"));
testModules(
LINE_JOINER.join(
";;;(function(global){",
"var foobar = {foo: 'bar'};",
"global.foobar = foobar;",
"if (typeof module === 'object' && module.exports) {",
" module.exports = foobar;",
"} else if (typeof define === 'function' && define.amd) {",
" define([], function() {return foobar;});",
"} else {",
" global.foobar = foobar;",
"}})(this)"),
LINE_JOINER.join(
"goog.provide('module$test');",
"var module$test = {foo: 'bar'};",
"this.foobar = module$test;"));
testModules(
LINE_JOINER.join(
"(function(global){",
"var foobar = {foo: 'bar'};",
"global.foobar = foobar;",
"if (typeof module === 'object' && module.exports) {",
" module.exports = foobar;",
"} else if (typeof define === 'function' && define.amd) {",
" define([], function() {return foobar;});",
"} else {",
" global.foobar = foobar;",
"}}.call(this, this))"),
LINE_JOINER.join(
"goog.provide('module$test');",
"var module$test = {foo: 'bar'};",
"this.foobar = module$test;"));
// We can't remove IIFEs explict calls that don't use "this"
testModules(
LINE_JOINER.join(
"(function(){",
"var foobar = {foo: 'bar'};",
"if (typeof module === 'object' && module.exports) {",
" module.exports = foobar;",
"} else if (typeof define === 'function' && define.amd) {",
" define([], function() {return foobar;});",
"} else {",
" this.foobar = foobar;",
"}}.call(window))"),
LINE_JOINER.join(
"goog.provide('module$test');",
"var module$test = {};",
"(function(){",
" module$test={foo:\"bar\"};",
"}).call(window);"));
// Can't remove IIFEs when there are sibling statements
testModules(
LINE_JOINER.join(
"(function(){",
"var foobar = {foo: 'bar'};",
"if (typeof module === 'object' && module.exports) {",
" module.exports = foobar;",
"} else if (typeof define === 'function' && define.amd) {",
" define([], function() {return foobar;});",
"} else {",
" this.foobar = foobar;",
"}})();",
"alert('foo');"),
LINE_JOINER.join(
"goog.provide('module$test');",
"var module$test = {};",
"(function(){",
" module$test={foo:\"bar\"};",
"})();",
"alert('foo');"));
// Can't remove IIFEs when there are sibling statements
testModules(
LINE_JOINER.join(
"alert('foo');",
"(function(){",
"var foobar = {foo: 'bar'};",
"if (typeof module === 'object' && module.exports) {",
" module.exports = foobar;",
"} else if (typeof define === 'function' && define.amd) {",
" define([], function() {return foobar;});",
"} else {",
" this.foobar = foobar;",
"}})();"),
LINE_JOINER.join(
"goog.provide('module$test');",
"var module$test = {};",
"alert('foo');",
"(function(){",
" module$test={foo:\"bar\"};",
"})();"));
// Annotations for local names should be preserved
testModules(
LINE_JOINER.join(
"(function(global){",
"/** @param {...*} var_args */",
"function log(var_args) {}",
"var foobar = {foo: 'bar', log: function() { log.apply(null, arguments); } };",
"global.foobar = foobar;",
"if (typeof module === 'object' && module.exports) {",
" module.exports = foobar;",
"} else if (typeof define === 'function' && define.amd) {",
" define([], function() {return foobar;});",
"} else {",
" global.foobar = foobar;",
"}}.call(this, this))"),
LINE_JOINER.join(
"goog.provide('module$test');",
"/** @param {...*} var_args */",
"function log$$module$test(var_args){}",
"var module$test = {",
" foo: 'bar',",
" log: function() { log$$module$test.apply(null,arguments); }",
"};",
"this.foobar = module$test;"));
}
public void testParamShadow() {
setFilename("test");
testModules(
LINE_JOINER.join(
"/** @constructor */ function Foo() {}",
"/** @constructor */ function Bar(Foo) { this.foo = new Foo(); }",
"Foo.prototype.test = new Bar(Foo);",
"module.exports = Foo;"),
LINE_JOINER.join(
"goog.provide('module$test');",
"var module$test = /** @constructor */ function () {};",
"/** @constructor */ function Bar$$module$test(Foo) { this.foo = new Foo(); }",
"module$test.prototype.test = new Bar$$module$test(module$test);"));
}
public void testIssue2308() {
setFilename("test");
testModules(
"exports.y = null; var x; x = exports.y;",
LINE_JOINER.join(
"goog.provide('module$test');",
"/** @const */ var module$test = {};",
"module$test.y = null;",
"var x$$module$test;",
"x$$module$test = module$test.y"));
}
public void testAbsoluteImportsWithModuleRoots() {
moduleRoots = ImmutableList.of("/base");
test(
ImmutableList.of(
SourceFile.fromCode(Compiler.joinPathParts("base", "mod", "name.js"), ""),
SourceFile.fromCode(
Compiler.joinPathParts("base", "test", "sub.js"),
LINE_JOINER.join(
"var name = require('/mod/name');",
"(function() { module$mod$name(); })();"))),
ImmutableList.of(
SourceFile.fromCode(
Compiler.joinPathParts("base", "mod", "name.js"),
LINE_JOINER.join(
"/** @fileoverview",
" * @suppress {missingProvide|missingRequire}",
" */",
"goog.provide('module$mod$name');")),
SourceFile.fromCode(
Compiler.joinPathParts("base", "test", "sub.js"),
LINE_JOINER.join(
"goog.require('module$mod$name');",
"var name = module$mod$name;",
"(function() { module$mod$name(); })();"))));
}
}