diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 83bf7693c..fd3c809f2 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -90,6 +90,21 @@ } } }, + "babel-core": { + "version": "6.21.0", + "from": "babel-core@6.21.0", + "resolved": "https://registry.npmjs.org/babel-core/-/babel-core-6.21.0.tgz" + }, + "babel-loader": { + "version": "6.21.0", + "from": "babel-loader@6.2.10", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-6.2.10.tgz" + }, + "babel-preset-env": { + "version": "1.1.8", + "from": "babel-preset-env@1.1.8", + "resolved": "https://registry.npmjs.org/babel-preset-env/-/babel-preset-env-1.1.8.tgz" + }, "clean-css": { "version": "3.4.18", "from": "clean-css@3.4.18", @@ -820,32 +835,10 @@ } } }, - "falafel": { - "version": "1.2.0", - "from": "falafel@1.2.0", - "resolved": "https://registry.npmjs.org/falafel/-/falafel-1.2.0.tgz", - "dependencies": { - "acorn": { - "version": "1.2.2", - "from": "acorn@>=1.0.3 <2.0.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-1.2.2.tgz" - }, - "foreach": { - "version": "2.0.5", - "from": "foreach@>=2.0.5 <3.0.0", - "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz" - }, - "isarray": { - "version": "0.0.1", - "from": "isarray@0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz" - }, - "object-keys": { - "version": "1.0.9", - "from": "object-keys@>=1.0.6 <2.0.0", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.9.tgz" - } - } + "falafel-loader": { + "version": "0.0.3", + "from": "falafel-loader@0.0.3", + "resolved": "https://registry.npmjs.org/falafel-loader/-/falafel-loader-0.0.3.tgz" }, "form-data": { "version": "0.1.4", @@ -1412,6 +1405,16 @@ } } }, + "json-loader": { + "version": "0.5.4", + "from": "json-loader@0.5.4", + "resolved": "https://registry.npmjs.org/json-loader/-/json-loader-0.5.4.tgz" + }, + "memory-fs": { + "version": "0.4.1", + "from": "memory-fs@0.4.1", + "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz" + }, "minimal-request": { "version": "2.2.0", "from": "minimal-request@2.2.0", @@ -3423,6 +3426,11 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz" } } + }, + "webpack": { + "version": "1.14.0", + "from": "webpack@1.14.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-1.14.0.tgz" } } } diff --git a/package.json b/package.json index d38352e16..448e74b5e 100644 --- a/package.json +++ b/package.json @@ -52,16 +52,21 @@ "accept-language-parser": "1.1.2", "async": "1.5.2", "aws-sdk": "2.6.11", + "babel-core": "6.21.0", + "babel-loader": "6.2.10", + "babel-preset-env": "1.1.8", "clean-css": "3.4.18", "colors": "1.1.2", "dependency-graph": "0.5.0", "detective": "4.3.1", "express": "3.21.2", - "falafel": "1.2.0", + "falafel-loader": "0.0.3", "form-data": "0.1.4", "fs-extra": "0.30.0", "handlebars": "4.0.5", "jade": "1.11.0", + "json-loader": "0.5.4", + "memory-fs": "0.4.1", "minimal-request": "2.2.0", "multer": "0.1.4", "nice-cache": "0.0.5", @@ -78,6 +83,7 @@ "targz": "1.0.1", "uglify-js": "2.6.4", "underscore": "1.8.3", - "watch": "0.19.1" + "watch": "0.19.1", + "webpack": "1.14.0" } } diff --git a/src/cli/domain/package-server-script/bundle/config/externalDependenciesHandlers.js b/src/cli/domain/package-server-script/bundle/config/externalDependenciesHandlers.js new file mode 100644 index 000000000..f883d65b4 --- /dev/null +++ b/src/cli/domain/package-server-script/bundle/config/externalDependenciesHandlers.js @@ -0,0 +1,36 @@ +/* + * External Dependencies handler for webpack + * Returns an array with handlers to indicates dependencies that should not be + * bundled by webPack but instead remain requested by the resulting bundle. + * For more info http://webpack.github.io/docs/configuration.html#externals + * +*/ +'use strict'; +var format = require('stringformat'); +var _ = require('underscore'); +var strings = require('../../../../../resources'); + + +module.exports = function externalDependenciesHandlers(dependencies){ + var deps = dependencies || {}; + + var missingExternalDependecy = function(dep, dependencies) { + return !_.contains(_.keys(dependencies), dep); + }; + + return [ + function(context, req, callback) { + if (/^[a-z@][a-z\-\/0-9]+$/i.test(req)) { + var dependencyName = req; + if (/\//g.test(dependencyName)) { + dependencyName = dependencyName.substring(0, dependencyName.indexOf('/')); + } + if (missingExternalDependecy(dependencyName, deps)) { + return callback(new Error(format(strings.errors.cli.SERVERJS_DEPENDENCY_NOT_DECLARED, JSON.stringify(dependencyName)))); + } + } + callback(); + }, + /^[a-z@][a-z\-\/0-9]+$/i + ]; +}; diff --git a/src/cli/domain/package-server-script/bundle/config/index.js b/src/cli/domain/package-server-script/bundle/config/index.js new file mode 100644 index 000000000..6c29bbd54 --- /dev/null +++ b/src/cli/domain/package-server-script/bundle/config/index.js @@ -0,0 +1,63 @@ +/*jshint camelcase:false */ +'use strict'; + +var webpack = require('webpack'); +var path = require('path'); +var wrapLoops = require('./wrapLoops'); +var externalDependenciesHandlers = require('./externalDependenciesHandlers'); + +module.exports = function webpackConfigGenerator(params){ + return { + entry: params.dataPath, + target: 'node', + output: { + path: '/build', + filename: params.fileName, + libraryTarget: 'commonjs2', + }, + externals: externalDependenciesHandlers(params.dependencies), + module: { + loaders: [ + { + test: /\.json$/, + exclude: /node_modules/, + loader: 'json-loader' + }, + { + test: /\.js?$/, + exclude: /node_modules/, + loaders: [ + 'falafel-loader', + 'babel-loader?' + JSON.stringify({ + cacheDirectory: true, + 'presets': [ + [require.resolve('babel-preset-env'), { + 'targets': { + 'node': 4 + } + }] + ] + }) + ], + } + ] + }, + plugins: [ + new webpack.optimize.OccurenceOrderPlugin(), + new webpack.optimize.UglifyJsPlugin({ + compressor: { + warnings: false, + screw_ie8: true + }, + sourceMap: false + }), + new webpack.DefinePlugin({ + 'process.env.NODE_ENV': JSON.stringify('production') + }) + ], + falafel: wrapLoops, + resolveLoader: { + root: path.resolve(__dirname, '../../../../../../node_modules') + } + }; +}; diff --git a/src/cli/domain/package-server-script/bundle/config/wrapLoops.js b/src/cli/domain/package-server-script/bundle/config/wrapLoops.js new file mode 100644 index 000000000..8c5a08749 --- /dev/null +++ b/src/cli/domain/package-server-script/bundle/config/wrapLoops.js @@ -0,0 +1,27 @@ +'use strict'; + + +var CONST_MAX_ITERATIONS = require('../../../../../resources/settings').maxLoopIterations; + +module.exports = function wrapLoops(node){ + var loopKeywords = ['WhileStatement', 'ForStatement', 'DoWhileStatement']; + + if(loopKeywords.indexOf(node.type) > -1){ + node.update( + 'var __ITER = ' + CONST_MAX_ITERATIONS + ';' + + node.source() + ); + } + + if(!node.parent){ + return; + } + + if(loopKeywords.indexOf(node.parent.type) > -1 && node.type === 'BlockStatement'){ + node.update('{ if(__ITER <=0){ throw new Error("Loop exceeded maximum ' + + 'allowed iterations"); } ' + + node.source().substr(1).slice(0, -1) + + ' __ITER--; }' + ); + } +}; diff --git a/src/cli/domain/package-server-script/bundle/index.js b/src/cli/domain/package-server-script/bundle/index.js new file mode 100644 index 000000000..bfb183b24 --- /dev/null +++ b/src/cli/domain/package-server-script/bundle/index.js @@ -0,0 +1,40 @@ +/*jshint camelcase:false */ +'use strict'; +var webpackConfig = require('./config'); +var console = require('console'); +var MemoryFS = require('memory-fs'); +var webpack = require('webpack'); + +var memoryFs = new MemoryFS(); + +module.exports = function bundle(params, callBack) { + var config = webpackConfig(params); + var compiler = webpack(config); + compiler.outputFileSystem = memoryFs; + + compiler.run(function(error, stats){ + var softError; + var warning; + + // handleFatalError + if (error) { + return callBack(error); + } + + var info = stats.toJson(); + // handleSoftErrors + if (stats.hasErrors()) { + softError = info.errors.toString(); + return callBack(softError); + } + // handleWarnings + if (stats.hasWarnings()) { + warning = info.warnings.toString(); + } + + console.log(stats.toString(params.webpack.stats)); + + var serverContentBundled = memoryFs.readFileSync('/build/server.js', 'UTF8'); + callBack(warning, serverContentBundled); + }); +}; diff --git a/src/cli/domain/package-server-script/compress.js b/src/cli/domain/package-server-script/compress.js deleted file mode 100644 index ab334560b..000000000 --- a/src/cli/domain/package-server-script/compress.js +++ /dev/null @@ -1,18 +0,0 @@ -'use strict'; - -var uglifyJs = require('uglify-js'); -var format = require('stringformat'); -var strings = require('../../../resources'); - -var compress = function(code, fileName){ - try { - return uglifyJs.minify(code, { fromString: true }).code; - } catch (e){ - if(!!e.line && !!e.col){ - throw new Error(format(strings.errors.cli.SERVERJS_PARSING_ERROR, fileName, e.line, e.col, e.message)); - } - throw e; - } -}; - -module.exports = compress; diff --git a/src/cli/domain/package-server-script/getLocalDependencies.js b/src/cli/domain/package-server-script/getLocalDependencies.js deleted file mode 100644 index f89040564..000000000 --- a/src/cli/domain/package-server-script/getLocalDependencies.js +++ /dev/null @@ -1,47 +0,0 @@ -'use strict'; - -var detective = require('detective'); -var requirePackageName = require('require-package-name'); -var format = require('stringformat'); -var _ = require('underscore'); -var strings = require('../../../resources'); -var compress = require('./compress'); - -var isLocalFile = function(f){ - return _.first(f) === '/' || _.first(f) === '.'; -}; - -var getRequiredContent = function(componentPath, required, fs, path){ - var ext = path.extname(required).toLowerCase(); - if(ext === ''){ - required += '.json'; - } else if(ext !== '.json'){ - throw new Error(strings.errors.cli.SERVERJS_REQUIRE_JS_NOT_ALLOWED); - } - - var requiredPath = path.resolve(componentPath, required); - if(!fs.existsSync(requiredPath)){ - throw new Error(format(strings.errors.cli.SERVERJS_REQUIRE_JSON_NOT_FOUND, required)); - } - - return fs.readJsonSync(requiredPath); -}; - -var getLocalDependencies = function(fs, path, componentPath, serverContent, fileName){ - var requires = { files: {}, modules: [] }; - var localRequires = detective(compress(serverContent, fileName)); - - _.forEach(localRequires, function(required){ - if(isLocalFile(required)) { - requires.files[required] = getRequiredContent(componentPath, required, fs, path); - } else { - var packageName = requirePackageName(required); - requires.modules.push(packageName); - } - }); - return requires; -}; - -module.exports = function(fs, path){ - return getLocalDependencies.bind(null, fs, path); -}; diff --git a/src/cli/domain/package-server-script/getSandBoxedJs/index.js b/src/cli/domain/package-server-script/getSandBoxedJs/index.js deleted file mode 100644 index b1dc96862..000000000 --- a/src/cli/domain/package-server-script/getSandBoxedJs/index.js +++ /dev/null @@ -1,21 +0,0 @@ -'use strict'; - -var _ = require('underscore'); -var wrapLoops = require('./wrapLoops'); -var compress = require('../compress'); - - -var getSandBoxedJs = function(wrappedRequires, serverContent, fileName, maxLoopIterations){ - if(_.keys(wrappedRequires).length > 0){ - serverContent = 'var __sandboxedRequire = require, ' + - ' __localRequires=' + JSON.stringify(wrappedRequires) + ';' + - 'require=function(x){' + - ' return __localRequires[x] ? __localRequires[x] : __sandboxedRequire(x);' + - '};' + - '\n' + serverContent; - } - - return compress(wrapLoops(serverContent, maxLoopIterations), fileName); -}; - -module.exports = getSandBoxedJs; diff --git a/src/cli/domain/package-server-script/getSandBoxedJs/wrapLoops.js b/src/cli/domain/package-server-script/getSandBoxedJs/wrapLoops.js deleted file mode 100644 index dd09cd206..000000000 --- a/src/cli/domain/package-server-script/getSandBoxedJs/wrapLoops.js +++ /dev/null @@ -1,24 +0,0 @@ -'use strict'; - -var falafel = require('falafel'); - -var wrapLoops = function(code, CONST_MAX_ITERATIONS){ - var loopKeywords = ['WhileStatement', 'ForStatement', 'DoWhileStatement']; - return falafel(code, function (node) { - if(loopKeywords.indexOf(node.type) > -1){ - node.update('{ var __ITER = ' + CONST_MAX_ITERATIONS + ';' - + node.source() + '}'); - } - - if(!node.parent){ - return; - } - - if(loopKeywords.indexOf(node.parent.type) > -1 && node.type === 'BlockStatement'){ - node.update('{ if(__ITER <=0){ throw new Error("loop exceeded maximum allowed iterations"); } ' - + node.source() + ' __ITER--; }'); - } - }).toString(); -}; - -module.exports = wrapLoops; diff --git a/src/cli/domain/package-server-script/index.js b/src/cli/domain/package-server-script/index.js index 4b474fc72..63c39f12a 100644 --- a/src/cli/domain/package-server-script/index.js +++ b/src/cli/domain/package-server-script/index.js @@ -1,48 +1,41 @@ 'use strict'; -var format = require('stringformat'); var fs = require('fs-extra'); var path = require('path'); - -var CONST_MAX_ITERATIONS = require('../../../resources/settings').maxLoopIterations; var hashBuilder = require('../../../utils/hash-builder'); -var strings = require('../../../resources'); - -var getSandBoxedJs = require('./getSandBoxedJs'); -var getLocalDependencies = require('./getLocalDependencies')(fs, path); -var missingDependencies = require('./missingDependencies'); - - -module.exports = function(params, callback){ - var dataPath = path.join(params.componentPath, params.ocOptions.files.data); - var fileName = 'server.js'; - var wrappedRequires; - var sandboxedJs; - var serverContent = fs.readFileSync(dataPath).toString(); - - try { - wrappedRequires = getLocalDependencies(params.componentPath, serverContent, params.ocOptions.files.data); - } catch(e){ - return callback(e); - } - - var missingDeps = missingDependencies(wrappedRequires.modules, params.dependencies); - - if(missingDeps.length > 0){ - return callback(new Error(format(strings.errors.cli.SERVERJS_DEPENDENCY_NOT_DECLARED, JSON.stringify(missingDeps)))); - } - - try { - sandboxedJs = getSandBoxedJs(wrappedRequires.files, serverContent, params.ocOptions.files.data, CONST_MAX_ITERATIONS); - } catch(e){ - return callback(e); +var bundle = require('./bundle'); + +var webpackDefaults = { + stats: { + chunks: false, + colors: true, + version: false, + hash: false } +}; - fs.writeFile(path.join(params.publishPath, fileName), sandboxedJs, function(err, res){ - callback(err, { - type: 'node.js', - hashKey: hashBuilder.fromString(sandboxedJs), - src: fileName - }); +module.exports = function packageServerScript(params, callback){ + var fileName = 'server.js'; + var publishPath = params.publishPath; + + var bundleParams = { + webpack: params.webpack || webpackDefaults, + dependencies: params.dependencies || {}, + fileName: fileName, + dataPath: path.join(params.componentPath, params.ocOptions.files.data) + }; + + bundle(bundleParams, function(err, bundledServer){ + if (err) { + return callback(err); + } else { + fs.writeFile(path.join(publishPath, fileName), bundledServer, function(err, res){ + callback(err, { + type: 'node.js', + hashKey: hashBuilder.fromString(bundledServer), + src: fileName + }); + }); + } }); }; diff --git a/src/cli/domain/package-server-script/missingDependencies.js b/src/cli/domain/package-server-script/missingDependencies.js deleted file mode 100644 index 8d3cb0699..000000000 --- a/src/cli/domain/package-server-script/missingDependencies.js +++ /dev/null @@ -1,11 +0,0 @@ -'use strict'; - -var _ = require('underscore'); - -var missingDependencies = function(requires, dependencies){ - return _.filter(requires, function(dep){ - return !_.contains(_.keys(dependencies), dep); - }); -}; - -module.exports = missingDependencies; diff --git a/src/resources/index.js b/src/resources/index.js index 6fc851ad3..1d0c4680c 100644 --- a/src/resources/index.js +++ b/src/resources/index.js @@ -80,9 +80,6 @@ module.exports = { PUBLISHING_FAIL: 'An error happened when publishing the component: {0}', REGISTRY_NOT_FOUND: 'oc registries not found. Run "oc registry add "', SERVERJS_DEPENDENCY_NOT_DECLARED: 'Missing dependencies from package.json => {0}', - SERVERJS_PARSING_ERROR: 'Javascript error found in {0} [{1},{2}]: {3}]', - SERVERJS_REQUIRE_JS_NOT_ALLOWED: 'Requiring local js files is not allowed. Keep it small.', - SERVERJS_REQUIRE_JSON_NOT_FOUND: '{0} not found. Only json files are require-able.', TEMPLATE_NOT_FOUND: 'file {0} not found', TEMPLATE_TYPE_NOT_VALID: 'the template is not valid. Allowed values are handlebars and jade' }, diff --git a/test/integration/cli-domain-package-server-script/cli-domain-package-server-script.js b/test/integration/cli-domain-package-server-script/cli-domain-package-server-script.js new file mode 100644 index 000000000..6ed547dd7 --- /dev/null +++ b/test/integration/cli-domain-package-server-script/cli-domain-package-server-script.js @@ -0,0 +1,390 @@ +'use strict'; + +var expect = require('chai').expect; +var fs = require('fs-extra'); +var path = require('path'); +var packageServerScript = require('../../../src/cli/domain/package-server-script/index.js'); +var hashBuilder = require('../../../src/utils/hash-builder'); + +var serverName = 'server.js'; +var componentName = 'component'; +var componentPath = path.resolve(__dirname, componentName); +var publishPath = path.resolve(componentPath, '_package'); +var webpackOptions = { + stats: 'none' +}; + +describe('cli : domain : package-server-script', function(){ + beforeEach(function(done){ + if(!fs.existsSync(componentPath)) { + fs.mkdirSync(componentPath); + fs.mkdirSync(path.resolve(componentPath, '_package')); + } + done(); + }); + + afterEach(function(done){ + if(fs.existsSync(componentPath)) { + fs.removeSync(componentPath); + } + done(); + }); + + describe('when packaging component\'s server.js', function(){ + this.timeout(15000); + + + describe('when component implements not-valid javascript', function(){ + var serverContent = '\nmodule.exports.data=function(context,cb){\nreturn cb(null,data; };'; + + beforeEach(function(done){ + fs.writeFileSync(path.resolve(componentPath, serverName), serverContent); + done(); + }); + + it('should throw an error with error details', function(done){ + try { + packageServerScript( + { + componentPath: componentPath, + ocOptions: { + files: { + data: serverName + } + }, + publishPath: publishPath, + webpack: webpackOptions + }, + function(err, res){ + try { + expect(err.toString()).to.contain.contain('Unexpected token, expected , (3:19)'); + return done(); + } catch(e) { + return done(e); + } + return done('error'); + } + ); + } catch (e) { + expect(e).to.contain.contain('Unexpected token, expected , (3:19)'); + return done(); + } + }); + }); + + describe('when component does not require any json', function(){ + var serverContent = '\nmodule.exports.data=function(context,cb){\nreturn cb(null, {name:\'John\'});\n};'; + + beforeEach(function(done){ + fs.writeFileSync(path.resolve(componentPath, serverName), serverContent); + done(); + }); + + it('should save compiled data provider and return a hash for the script', function(done){ + packageServerScript( + { + componentPath: componentPath, + ocOptions: { + files: { + data: serverName + } + }, + publishPath: publishPath, + webpack: webpackOptions + }, + function(err, res){ + if (err) { + return done(err); + } + try { + expect(res.type).to.equal('node.js'); + expect(res.src).to.equal('server.js'); + + var compiledContent = fs.readFileSync(path.resolve(publishPath, res.src), {encoding: 'utf8'}); + expect(res.hashKey).to.equal(hashBuilder.fromString(compiledContent)); + done(); + } catch(e) { + done(e); + } + } + ); + }); + }); + + describe('when component require a json file', function(){ + var user = {first: 'John',last:'Doe'}; + var jsonContent = JSON.stringify(user); + var serverContent = 'var user = require(\'./user\');\nmodule.exports.data=function(){return user.first;};'; + + beforeEach(function(done){ + fs.writeFileSync(path.resolve(componentPath, 'user.json'), jsonContent); + fs.writeFileSync(path.resolve(componentPath, serverName), serverContent); + done(); + }); + + afterEach(function(done){ + require.cache[path.resolve(publishPath, serverName)] = null; + done(); + }); + + it('should save compiled and minified data provider encapsulating json content', function(done){ + packageServerScript( + { + componentPath: componentPath, + ocOptions: { + files: { + data: serverName + } + }, + publishPath: publishPath, + webpack: webpackOptions + }, + function(err, res){ + if (err) { + return done(err); + } + try { + var name = user.first; + var bundle = require(path.resolve(publishPath, res.src)); + expect(bundle.data()).to.be.equal(name); + + var compiledContent = fs.readFileSync(path.resolve(publishPath, res.src), {encoding: 'utf8'}); + expect(compiledContent).to.not.contain('user'); + done(); + } catch(e) { + done(e); + } + } + ); + }); + }); + + describe('when component does require an npm module', function(){ + var serverContent = 'var _ =require(\'underscore\');' + + '\nvar user = {name:\'John\'};\nmodule.exports.data=function(context,cb){' + + '\nreturn cb(null, _.has(user, \'name\'));\n};'; + + beforeEach(function(done){ + fs.writeFileSync(path.resolve(componentPath, serverName), serverContent); + done(); + }); + + it('should save compiled data provider', function(done){ + var dependencies = {underscore: '1.8.3'}; + + packageServerScript( + { + componentPath: componentPath, + ocOptions: { + files: { + data: serverName + } + }, + publishPath: publishPath, + webpack: webpackOptions, + dependencies: dependencies + }, + function(err, res){ + if (err) { + return done(err); + } + try { + expect(res.type).to.equal('node.js'); + expect(res.src).to.equal('server.js'); + + var compiledContent = fs.readFileSync(path.resolve(publishPath, res.src), {encoding: 'utf8'}); + expect(res.hashKey).to.equal(hashBuilder.fromString(compiledContent)); + done(); + } catch(e) { + done(e); + } + } + ); + }); + + describe('and required dependencies are not present in the package.json', function(){ + it('should throw an error with details', function(done){ + var dependencies = {lodash: '1.0.0'}; + + packageServerScript( + { + componentPath: componentPath, + ocOptions: { + files: { + data: serverName + } + }, + publishPath: publishPath, + webpack: webpackOptions, + dependencies: dependencies + }, + function(err, res){ + expect(err).to.not.be.null; + expect(err).to.contain('Missing dependencies from package.json => "underscore"'); + done(); + } + ); + }); + }); + }); + + describe('when component does require a relative path from an npm module', function(){ + var serverContent = 'var data=require(\'react-dom/server\');module.exports.data=function(context,cb){return cb(null,data); };'; + + beforeEach(function(done){ + fs.writeFileSync(path.resolve(componentPath, serverName), serverContent); + done(); + }); + + describe('and required dependencies are not present in the package.json', function(){ + it('should throw an error with details', function(done){ + var dependencies = {'react': '15.4.2'}; + + packageServerScript( + { + componentPath: componentPath, + ocOptions: { + files: { + data: serverName + } + }, + publishPath: publishPath, + webpack: webpackOptions, + dependencies: dependencies + }, + function(err, res){ + expect(err).to.not.be.null; + expect(err.toString()).to.contain('Missing dependencies from package.json => "react-dom"'); + done(); + } + ); + }); + }); + }); + + describe('when component require a local js module', function(){ + var jsContent = 'var user = {first: \'John\',last:\'Doe\'};\nmodule.exports = user'; + var serverContent = 'var user = require(\'./user\');\nmodule.exports.data=function(){return user.first;};'; + + beforeEach(function(done){ + fs.writeFileSync(path.resolve(componentPath, 'user.js'), jsContent); + fs.writeFileSync(path.resolve(componentPath, serverName), serverContent); + done(); + }); + + afterEach(function(done){ + require.cache[path.resolve(publishPath, serverName)] = null; + done(); + }); + + it('should save compiled data provider encapsulating js module content', function(done){ + packageServerScript( + { + componentPath: componentPath, + ocOptions: { + files: { + data: serverName + } + }, + publishPath: publishPath, + webpack: webpackOptions + }, + function(err, res){ + if (err) { + return done(err); + } + try { + var name = 'John'; + var bundle = require(path.resolve(publishPath, res.src)); + expect(bundle.data()).to.be.equal(name); + done(); + } catch(e) { + done(e); + } + } + ); + }); + }); + + describe('when component uses es2015 javascript syntax', function(){ + var serverContent = 'const {first, last} = {first: "John", last: "Doe"};\nconst data = (context,cb) => cb(null, first, last)\nexport {data}'; + + beforeEach(function(done){ + fs.writeFileSync(path.resolve(componentPath, serverName), serverContent); + done(); + }); + + it('should transpile it to es2015 through Babel', function(done){ + packageServerScript( + { + componentPath: componentPath, + ocOptions: { + files: { + data: serverName + } + }, + publishPath: publishPath, + webpack: webpackOptions + }, + function(err, res){ + if (err) { + return done(err); + } + try { + var compiledContent = fs.readFileSync(path.resolve(publishPath, res.src), {encoding: 'utf8'}); + expect(compiledContent).to.not.contain('=>'); + expect(compiledContent).to.not.contain('const'); + expect(compiledContent).to.contain('var'); + expect(compiledContent).to.contain('function'); + done(); + } catch(e) { + done(e); + } + } + ); + }); + }); + + describe('when component code includes a loop', function(){ + var serverContent = 'module.exports.data=function(context,cb){ var x,y,z;' + + 'while(true){ x = 234; }' + + 'for(var i=1e12;;){ y = 546; }' + + 'do { z = 342; } while(true);' + + '}'; + + beforeEach(function(done){ + fs.writeFileSync(path.resolve(componentPath, serverName), serverContent); + done(); + }); + + it('should wrap while/do/for;; loops with an iterator limit', function(done){ + packageServerScript( + { + componentPath: componentPath, + ocOptions: { + files: { + data: serverName + } + }, + publishPath: publishPath, + webpack: webpackOptions + }, + function(err, res){ + if (err) { + return done(err); + } + try { + var compiledContent = fs.readFileSync(path.resolve(publishPath, res.src), {encoding: 'utf8'}); + expect(compiledContent).to.contain('for(var r,a,t,i=1e9;;){if(i<=0)throw new Error(\"loop exceeded maximum allowed iterations\");r=234,i--}'); + expect(compiledContent).to.contain('for(var i=1e9;;){if(i<=0)throw new Error(\"loop exceeded maximum allowed iterations\");a=546,i--}'); + expect(compiledContent).to.contain('for(var i=1e9;;){if(i<=0)throw new Error(\"loop exceeded maximum allowed iterations\");t=342,i--}'); + done(); + } catch(e) { + done(); + } + } + ); + }); + }); + }); +}); diff --git a/test/unit/cli-domain-package-server-script.js b/test/unit/cli-domain-package-server-script.js index fc366859a..153eef81c 100644 --- a/test/unit/cli-domain-package-server-script.js +++ b/test/unit/cli-domain-package-server-script.js @@ -6,282 +6,114 @@ var path = require('path'); var sinon = require('sinon'); var uglifyJs = require('uglify-js'); var _ = require('underscore'); +var falafelLoader = require('falafel-loader'); -var fsMock, - packageServerScript; +var externalDependenciesHandlers = + require('../../src/cli/domain/package-server-script/bundle/config/externalDependenciesHandlers'); +var wrapLoops = + require('../../src/cli/domain/package-server-script/bundle/config/wrapLoops'); +var webpackConfigGenerator = + require('../../src/cli/domain/package-server-script/bundle/config'); -var initialise = function(fs){ - fsMock = _.extend({ - existsSync: sinon.stub().returns(true), - readFileSync: sinon.stub().returns('file content'), - readJsonSync: sinon.stub().returns({ content: true }), - writeFile: sinon.stub().yields(null, 'ok') - }, fs || {}); +describe('cli : domain : package-server-script ', function(){ - packageServerScript = injectr('../../src/cli/domain/package-server-script/index.js', { - 'fs-extra': fsMock, - path: { - extname: path.extname, - join: path.join, - resolve: function(){ - return _.toArray(arguments).join('/'); - } - } - }); -}; - -describe('cli : domain : package-server-script', function(){ - - describe('when packaging component\'s server.js', function(){ - - describe('when component implements not-valid javascript', function(){ - - var error, - serverjs = 'var data=require(\'request\');\nmodule.exports.data=function(context,cb){\nreturn cb(null,data; };'; - - beforeEach(function(done){ - - initialise({ readFileSync: sinon.stub().returns(serverjs) }); - - packageServerScript({ - componentPath: '/path/to/component/', - ocOptions: { - files: { - data: 'myserver.js' - } - }, - publishPath: '/path/to/component/_package/' - }, function(e, r){ - error = e; - done(); - }); - }); - - it('should throw an error with error details', function(){ - expect(error.toString()).to.equal('Error: Javascript error found in myserver.js [3,19]: SyntaxError: Unexpected token punc «;», expected punc «,»]'); - }); - }); - - describe('when component does not require any json', function(){ - - var result, - serverjs = 'module.exports.data=function(context,cb){return cb(null, {name:\'John\'}); };'; - - beforeEach(function(done){ - - initialise({ readFileSync: sinon.stub().returns(serverjs) }); - - packageServerScript({ - componentPath: '/path/to/component/', - ocOptions: { - files: { - data: 'server.js' - } - }, - publishPath: '/path/to/component/_package/' - }, function(e, r){ - result = r; - done(); - }); - }); + describe('bundle/config/externalDependenciesHandlers when configured with a dependencies hash', function(){ + var handler = externalDependenciesHandlers({lodash: '4.17.14'}); - it('should save compiled data provider', function(){ - expect(fsMock.writeFile.args[0][1]).to.equal('module.exports.data=function(n,e){return e(null,{name:"John"})};'); - }); - - it('should return hash for script', function(){ - expect(result.hashKey).not.be.empty; - }); + it('should return an array containing a function and a regular expression ', function(){ + expect(handler).to.be.an('array'); + expect(handler.length).to.be.equal(2); + expect(handler[1] instanceof RegExp).to.be.true; + expect(handler[0]).to.be.a('function'); }); - describe('when component requires a json', function(){ - - var requiredJson = { hello: 'world' }, - serverjs = 'var data = require(\'./someJson\'); module.exports.data=function(context,cb){return cb(null,{}); };'; - - beforeEach(function(done){ - - initialise({ - readFileSync: sinon.stub().returns(serverjs), - readJsonSync: sinon.stub().returns(requiredJson) - }); - - packageServerScript({ - componentPath: '/path/to/component/', - ocOptions: { - files: { - data: 'server.js' - } - }, - publishPath: '/path/to/component/_package/' - }, done); + describe('its regular expression', function(){ + var regex = handler[1]; + it('should match npm module names', function() { + expect(regex.test('lodash')).to.be.true; + expect(regex.test('lodash/fp/curryN')).to.be.true; + expect(regex.test('@cycle/xstream-run')).to.be.true; }); - - it('should save compiled and minified data provider encapsulating json content', function(){ - var written = fsMock.writeFile.args[0][1]; - - expect(written).to.contain('var __sandboxedRequire=require,__localRequires={"./someJson":{hello:"world"}};' - + 'require=function(e){return __localRequires[e]?__localRequires[e]:__sandboxedRequire(e)};var data=require("./someJson");' - + 'module.exports.data=function(e,r){return r(null,{})};'); + it('should not match local modules', function() { + expect(regex.test('/myModule')).to.be.false; + expect(regex.test('./myModule')).to.be.false; }); }); - describe('when component requires an npm module', function(){ - - var error, - serverjs = 'var data=require(\'request\');module.exports.data=function(context,cb){return cb(null,data); };'; - - beforeEach(function(done){ - - initialise({ readFileSync: sinon.stub().returns(serverjs) }); - - packageServerScript({ - componentPath: '/path/to/component/', - ocOptions: { - files: { - data: 'server.js' - } - }, - publishPath: '/path/to/component/_package/' - }, function(e, r){ - error = e; + describe('its function', function() { + var missingDephandler = handler[0]; + it('should return an error if a specific npm-module is missing from the given dependencies', function(done) { + missingDephandler('_', 'underscore', function(err){ + expect(err.toString()).to.be.equal('Error: Missing dependencies from package.json => \"underscore\"'); done(); }); }); - - it('should throw an error when the dependency is not present in the package.json', function(){ - expect(error.toString()).to.equal('Error: Missing dependencies from package.json => ["request"]'); - }); - }); - - describe('when component requires a relative path from an npm module', function(){ - - var error, - serverjs = 'var data=require(\'react-dom/server\');module.exports.data=function(context,cb){return cb(null,data); };'; - - beforeEach(function(done){ - - initialise({ readFileSync: sinon.stub().returns(serverjs) }); - - packageServerScript({ - componentPath: '/path/to/component/', - ocOptions: { - files: { - data: 'server.js' - } - }, - publishPath: '/path/to/component/_package/' - }, function(e, r){ - error = e; - done(); - }); - }); - - it('should throw an error when the dependency is not present in the package.json', function(){ - expect(error.toString()).to.equal('Error: Missing dependencies from package.json => ["react-dom"]'); - }); - }); - - describe('when component requires a js file', function(){ - - var serverjs = 'var data=require(\'./hi.js\');module.exports.data=function(context,cb){return cb(null,data); };', - error; - - beforeEach(function(done){ - - initialise({ readFileSync: sinon.stub().returns(serverjs) }); - - packageServerScript({ - componentPath: '/path/to/component/', - ocOptions: { - files: { - data: 'server.js' - } - }, - publishPath: '/path/to/component/_package/' - }, function(e, r){ - error = e; - done(); + it('should invoke the callback with no arguments if module is not missing from the given dependencies', function(done) { + missingDephandler('_', 'lodash', function(err){ + expect(err).to.be.an('undefined'); + return done(); }); }); - - it('should not package component and respond with error', function(){ - expect(error.toString()).to.equal('Error: Requiring local js files is not allowed. Keep it small.'); - }); }); + }); - describe('when component requires a file without extension that is not found as json', function(){ - - var serverjs = 'var data=require(\'./hi\');module.exports.data=function(context,cb){return cb(null,data); };', - error; - - beforeEach(function(done){ - - initialise({ - readFileSync: sinon.stub().returns(serverjs), - existsSync: sinon.stub().returns(false) - }); - - packageServerScript({ - componentPath: '/path/to/component/', - ocOptions: { - files: { - data: 'server.js' - } - }, - publishPath: '/path/to/component/_package/' - }, function(e, r){ - error = e; - done(); - }); - }); + describe('bundle/config/wrapLoops', function (){ - it('should not package component and respond with error', function(){ - expect(error.toString()).to.equal('Error: ./hi.json not found. Only json files are require-able.'); - }); + it('should be a function with arity 1', function() { + expect(wrapLoops).to.be.a('function'); + expect(wrapLoops.length).to.be.equal(1); }); describe('when component code includes a loop', function(){ + var mockFalafel = function(){ + this.options = { + falafel: wrapLoops + }; + this.cacheable = function(){}; + this.loader = falafelLoader; + }; + var falafel = new mockFalafel(); var serverjs = 'module.exports.data=function(context,cb){ var x,y,z;' - + 'while(true){ x = 234; } ' - + 'for(var i=1e12;;){ y = 546; }' - + 'do { z = 342; } while(true);' - + 'return cb(null,data); };', - result; + + 'while(true){ x = 234; } ' + + 'for(var i=1e12;;){ y = 546; }' + + 'do { z = 342; } while(true);' + + 'return cb(null,data); };'; - beforeEach(function(done){ + var result = falafel.loader(serverjs); - initialise({ - readFileSync: sinon.stub().returns(serverjs), - existsSync: sinon.stub().returns(false) - }); + it('should wrap the while loop with an iterator limit', function() { + expect(result).to.contain('var x,y,z;var __ITER = 1000000000;while(true){ if(__ITER <=0)' + + '{ throw new Error("Loop exceeded maximum allowed iterations"); } x = 234; __ITER--; }'); + }); - packageServerScript({ - componentPath: '/path/to/component/', - ocOptions: { - files: { - data: 'server.js' - } - }, - publishPath: '/path/to/component/_package/' - }, function(e, r){ - result = r; - done(); - }); + it('should wrap the for loop with an iterator limit', function(){ + expect(result).to.contain('for(var i=1e12;;){ if(__ITER <=0)' + + '{ throw new Error("Loop exceeded maximum allowed iterations"); } y = 546; __ITER--; }'); }); - it('should wrap the while loop with an iterator limit (and convert it to a for loop)', function(){ - expect(fsMock.writeFile.firstCall.args[1]).to.contain('for(var r,a,t,i=1e9;;){if(i<=0)throw new Error(\"loop exceeded maximum allowed iterations\");r=234,i--}'); + it('should wrap the do loop with an iterator limit (and convert it to a for loop)', function(){ + expect(result).to.contain('var __ITER = 1000000000;do { if(__ITER <=0)' + + '{ throw new Error("Loop exceeded maximum allowed iterations"); } z = 342; __ITER--; } while(true)'); }); + }); + }); - it('should wrap the for loop with an iterator limit', function(){ - expect(fsMock.writeFile.firstCall.args[1]).to.contain('for(var i=1e9;;){if(i<=0)throw new Error(\"loop exceeded maximum allowed iterations\");a=546,i--}'); + describe('bundle/config', function(){ + + describe('when configured', function(){ + var config = webpackConfigGenerator({ + webpack: { stats: 'none' }, + dependencies: {}, + fileName: 'server.js', + dataPath: '/path/to/server.js' }); - it('should wrap the do loop with an iterator limit (and convert it to a for loop)', function(){ - expect(fsMock.writeFile.firstCall.args[1]).to.contain('for(var i=1e9;;){if(i<=0)throw new Error(\"loop exceeded maximum allowed iterations\");t=342,i--}'); + it('should return a proper configuration options for webpack', function(){ + expect(config.entry).to.be.equal('/path/to/server.js'); + expect(config.output.filename).to.be.equal('server.js'); + expect(config.externals).to.be.an('array'); }); }); });