diff --git a/.gitignore b/.gitignore index 2a324d4..e4db436 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ test/output test/output.js test/output.js.map dist +.idea/ diff --git a/README.md b/README.md index 646ecb6..cbe75e0 100644 --- a/README.md +++ b/README.md @@ -259,6 +259,14 @@ builder.bundle('app/**/* - [app/**/*]', 'dependencies.js', { minify: true, sourc The above means _take the tree of app and all its dependencies, and subtract just the modules in app_, thus leaving us with just the tree of dependencies of the app package. +#### Example - Multiple Common Bundles + +Parentheses are supported, so the following would bundle everything in common with `page1` and `page2`, and also everything in common between `page3` and `page4`: + +```javascript +builder.bundle('(app/page1.js & app/page2.js) + (app/page3.js & app/page4.js)', 'common.js'); +``` + #### Example - Direct Trace API Instead of using the arithmetic syntax, we can construct the trace ourselves. diff --git a/lib/arithmetic.js b/lib/arithmetic.js index d55d2a3..5d08378 100644 --- a/lib/arithmetic.js +++ b/lib/arithmetic.js @@ -13,37 +13,108 @@ var verifyTree = require('./utils').verifyTree; function parseExpression(expressionString) { expressionString = ' + ' + expressionString; - var args = expressionString.split(/ [\+\-\&] /); + var index = 0; var operations = []; + var operatorRegex = /[\+\-\&]/; + var errorMessagesFromIndex = 3; - var curLen = 0; - for (var i = 0; i < args.length - 1; i++) { - var operator = expressionString[curLen + 1]; - var moduleName = args[i + 1]; + function getNextIdentifier() { + eatWhitespace(); + var firstChar = expressionString.charAt(index); - curLen += moduleName.length + 3; + if (operatorRegex.test(firstChar)){ + throw 'Syntax Error: Identifier or sub expression expected after <' + expressionString.slice(errorMessagesFromIndex).substr(0, index - errorMessagesFromIndex) + '> but found <' + firstChar + '> instead'; + } - if (operator !== '+' && operator !== '-' && operator !== '&') - throw new TypeError('Expected operator before ' + operator); - if (!moduleName) - throw new TypeError('A module name is needed after ' + operator); + if (firstChar === '(') { + var closingParenIndex = index, + numOpenBeforeSelf = 0; + + while (++closingParenIndex < expressionString.length){ + if (expressionString.charAt(closingParenIndex) === '('){ + numOpenBeforeSelf++; + } else if (expressionString.charAt(closingParenIndex) === ')') { + if (numOpenBeforeSelf){ + numOpenBeforeSelf--; + } else { + break; + } + } + } + if (expressionString.charAt(closingParenIndex) !== ')'){ + throw 'Syntax Error: Expression <' + expressionString.substr(index) + '> is never terminated. Did you forget to add a closing ")"?'; + } - // detect [moduleName] syntax for individual modules not trees - var singleModule = moduleName.substr(0, 1) == '[' && moduleName.substr(moduleName.length - 1, 1) == ']'; - if (singleModule) - moduleName = moduleName.substr(1, moduleName.length - 2); + var wholeExpression = expressionString.substring(index + 1, closingParenIndex); + index = closingParenIndex + 1; + return { bulkOperation: wholeExpression }; + } - var canonicalized = moduleName.substr(0, 1) == '`' && moduleName.substr(moduleName.length - 1, 1) == '`'; - if (canonicalized) - moduleName = moduleName.substr(1, moduleName.length - 2); + var result = ""; + //scan the identifier + for (; index < expressionString.length; index++) { + var currentChar = expressionString.charAt(index); + //can have spaces in file names - so we need whitespace, operator, whitespace. + if (/^\s+[\+\-\&]\s+/.test(expressionString.substr(index))) { + return result; + } else { + result += currentChar; + } + } + return result.replace(/\s+$/, ''); //it appears as though trailing whitespace is trimmed downstream, but I'm snipping here to be safe + } - operations.push({ - operator: operator, - moduleName: moduleName, - singleModule: singleModule, - canonicalized: canonicalized - }); + function getNextOperator() { + eatWhitespace(); + if (index === expressionString.length) return null; + + var candidateResult = expressionString.charAt(index++); //all operators are single characters at the moment + + if (!operatorRegex.test(candidateResult)){ + throw 'Syntax Error: An operator was expected after <' + expressionString.slice(errorMessagesFromIndex).substr(0, index - 1 - errorMessagesFromIndex) + '> but found <' + expressionString.substring(index - 1) + '> instead'; + } + + return candidateResult; + } + + function eatWhitespace() { + //wind past whitespace + for (; index < expressionString.length; index++) { + if (/\S/.test(expressionString.charAt(index))) { + break; + } + } + } + + var operator; + while (index < expressionString.length && (operator = getNextOperator())) { + var moduleNameOrSubExpression = getNextIdentifier(); + + if (typeof moduleNameOrSubExpression === 'object'){ + operations.push({ + operator: operator, + bulkOperation: moduleNameOrSubExpression.bulkOperation + }); + } else { + // detect [moduleName] syntax for individual modules not trees + var singleModule = moduleNameOrSubExpression.substr(0, 1) == '[' && moduleNameOrSubExpression.substr(moduleNameOrSubExpression.length - 1, 1) == ']'; + if (singleModule) { + moduleNameOrSubExpression = moduleNameOrSubExpression.substr(1, moduleNameOrSubExpression.length - 2); + } + + var canonicalized = moduleNameOrSubExpression.substr(0, 1) == '`' && moduleNameOrSubExpression.substr(moduleNameOrSubExpression.length - 1, 1) == '`'; + if (canonicalized) { + moduleNameOrSubExpression = moduleNameOrSubExpression.substr(1, moduleNameOrSubExpression.length - 2); + } + + operations.push({ + operator: operator, + moduleName: moduleNameOrSubExpression, + singleModule: singleModule, + canonicalized: canonicalized + }); + } } return operations; @@ -149,42 +220,61 @@ function expandGlobAndCanonicalize(builder, operation) { }); } -exports.traceExpression = function(builder, expression, traceOpts) { - if (!expression) - throw new Error('A module expression must be provided to trace.'); + exports.traceExpression = function(builder, expression, traceOpts) { + if (!expression) + throw new Error('A module expression must be provided to trace.'); + + return Promise + .resolve(expandAndCanonicalizeExpression(builder, expression)) + .then(function processExpandedOperations(expandedOperations) { + // chain the operations, applying them with the trace of the next module + return expandedOperations.reduce(function(p, op) { + return p.then(function(curTree) { + // tree . module + if (op.singleModule) + return getTreeModuleOperation(builder, op.operator)(curTree, op.moduleName); + + if (op.operationsTree){ + return processExpandedOperations(op.operationsTree).then(function(expandedTree){ + return getTreeOperation(op.operator)(curTree, expandedTree); + }); + } + // tree . tree + return builder.tracer.traceCanonical(op.moduleName, traceOpts) + .then(function(nextTrace) { + return getTreeOperation(op.operator)(curTree, nextTrace.tree); + }); + }); + }, Promise.resolve({})); + }); + }; +function expandAndCanonicalizeExpression(builder, expression){ var operations = parseExpression(expression); + var expandPromise = Promise.resolve(1); var expandedOperations = []; - // expand any globbing operations in the expression - var expandPromise = Promise.resolve(); - operations.forEach(function(operation) { - expandPromise = expandPromise.then(function() { - return Promise.resolve(expandGlobAndCanonicalize(builder, operation)) - .then(function(expanded) { - expandedOperations = expandedOperations.concat(expanded); + operations.forEach(function(operation){ + if (operation.bulkOperation) { + var expandedTreePromise = expandAndCanonicalizeExpression(builder, operation.bulkOperation); + expandPromise = expandPromise.then(function() { + return Promise.resolve(expandedTreePromise) + .then(function(expressionsOperations){ + expandedOperations = expandedOperations.concat({ operator: operation.operator, operationsTree: expressionsOperations }); + }); }); - }); - }); - - return expandPromise.then(function() { - // chain the operations, applying them with the trace of the next module - return expandedOperations.reduce(function(p, op) { - return p.then(function(curTree) { - // tree . module - if (op.singleModule) - return getTreeModuleOperation(builder, op.operator)(curTree, op.moduleName); - - // tree . tree - return builder.tracer.traceCanonical(op.moduleName, traceOpts) - .then(function(nextTrace) { - return getTreeOperation(op.operator)(curTree, nextTrace.tree); - }); - }); - }, Promise.resolve({})); + } else { + expandPromise = expandPromise.then(function() { + return Promise.resolve(expandGlobAndCanonicalize(builder, operation)) + .then(function (expanded) { + expandedOperations = expandedOperations.concat(expanded); + }); + }) + } }); -}; + return Promise.resolve(expandPromise).then(function(){ return expandedOperations; }); +} // returns a new tree containing tree1 n tree2 exports.intersectTrees = intersectTrees; @@ -210,7 +300,7 @@ function intersectTrees(tree1, tree2) { } return intersectTree; -}; +} // returns a new tree containing tree1 + tree2 exports.addTrees = addTrees; @@ -249,7 +339,7 @@ function subtractTrees(tree1, tree2) { } return subtractTree; -}; +} // pre-order tree traversal with a visitor and stop condition exports.traverseTree = traverseTree; diff --git a/test/arithmetic.js b/test/arithmetic.js index ade36d5..97fa960 100644 --- a/test/arithmetic.js +++ b/test/arithmetic.js @@ -36,7 +36,8 @@ suite('Bundle Expressions', function() { builder.trace('*.js - [amd-*] - [sfx-format-*]') .then(function(tree) { assert.deepEqual(Object.keys(tree).sort(), [ - 'Buffer.js', 'amd.js', 'babel', 'cjs-globals.js', 'cjs-resolve.js', 'cjs.js', 'component.jsx!jsx.js', 'file.json', 'first.js', + 'Buffer.js', 'amd.js', 'babel', 'cjs space.js', 'cjs-1.js', 'cjs-2.js', 'cjs-3.js', 'cjs-4.js', 'cjs-5.js', 'cjs-globals.js', 'cjs-in-12.js', 'cjs-in-13.js', + 'cjs-resolve.js', 'cjs.js', 'component.jsx!jsx.js', 'file.json', 'first.js', 'global-inner.js', 'global-outer.js', 'global.js', 'jquery-cdn', 'jquery.js', 'json-plugin.js', 'jsx.js', 'plugin.js', 'runtime.js', 'second.js', 'some.js!plugin.js', 'text-plugin.js', 'text.txt!text-plugin.js', 'third.js', 'umd.js']); }) @@ -50,4 +51,231 @@ suite('Bundle Expressions', function() { }) .then(done, done); }); -}); \ No newline at end of file + + test('cjs bundles added', function(done){ + builder.trace('cjs-1.js + cjs-2.js + cjs-3.js') + .then(function(tree) { + assert.deepEqual(Object.keys(tree).sort(), [ + 'cjs-1.js', 'cjs-2.js', 'cjs-3.js', 'cjs-in-12.js', 'cjs-in-13.js']); + }) + .then(done, done); + }); + + test('cjs bundles added with parens and extra spaces', function(done){ + builder.trace(' ( cjs-1.js & cjs-2.js ) + ( cjs-1.js & cjs-3.js)') + .then(function(tree) { + assert.deepEqual(Object.keys(tree).sort(), ['cjs-in-12.js', 'cjs-in-13.js']); + }) + .then(done, done); + }); + + test('cjs bundles added with single-value parameters', function(done){ + builder.trace('(cjs-1.js) + (cjs-2.js) + (cjs-3.js)') + .then(function(tree) { + assert.deepEqual(Object.keys(tree).sort(), [ + 'cjs-1.js', 'cjs-2.js', 'cjs-3.js', 'cjs-in-12.js', 'cjs-in-13.js']); + }) + .then(done, done); + }); + + test('cjs bundles added with parens', function(done){ + builder.trace('(cjs-1.js & cjs-2.js) + (cjs-1.js & cjs-3.js)') + .then(function(tree) { + assert.deepEqual(Object.keys(tree).sort(), ['cjs-in-12.js', 'cjs-in-13.js']); + }) + .then(done, done); + }); + + test('cjs bundles added with parens 2', function(done){ + builder.trace('(cjs-1.js & cjs-2.js)') + .then(function(tree) { + assert.deepEqual(Object.keys(tree).sort(), ['cjs-in-12.js']); + }) + .then(done, done); + }); + + test('cjs bundles added with parens 3', function(done){ + builder.trace('(cjs-1.js & cjs-2.js) + cjs-in-13.js - cjs-in-13.js') + .then(function(tree) { + assert.deepEqual(Object.keys(tree).sort(), ['cjs-in-12.js']); + }) + .then(done, done); + }); + + test('cjs bundles added with parens 4', function(done){ + builder.trace('(cjs-1.js & cjs-2.js) + cjs-in-13.js - cjs-in-13.js') + .then(function(tree) { + assert.deepEqual(Object.keys(tree).sort(), ['cjs-in-12.js']); + }) + .then(done, done); + }); + + test('cjs bundles added with parens 5', function(done){ + builder.trace('cjs-in-13.js + (cjs-1.js & cjs-2.js) - cjs-in-13.js') + .then(function(tree) { + assert.deepEqual(Object.keys(tree).sort(), ['cjs-in-12.js']); + }) + .then(done, done); + }); + + test('cjs bundles added with multiple parens', function(done){ + builder.trace('cjs-in-13.js + (cjs-1.js & cjs-2.js) - (cjs-in-13.js)') + .then(function(tree) { + assert.deepEqual(Object.keys(tree).sort(), ['cjs-in-12.js']); + }) + .then(done, done); + }); + + test('cjs bundles added with multiple parens 2', function(done){ + builder.trace('(cjs-1.js & cjs-2.js) + (cjs-1.js & cjs-3.js) - cjs-in-12.js') + .then(function(tree) { + assert.deepEqual(Object.keys(tree).sort(), ['cjs-in-13.js']); + }) + .then(done, done); + }); + + test('cjs bundles with nested parens', function(done){ + builder.trace('(cjs-1.js + cjs-2.js - ([cjs-1.js] + [cjs-2.js])) - (cjs-in-12.js)') + .then(function(tree) { + assert.deepEqual(Object.keys(tree).sort(), ['cjs-in-13.js']); + }) + .then(done, done); + }); + + test('cjs bundles with nested parens 2', function(done){ + builder.trace('(cjs-1.js + cjs-2.js - ([cjs-1.js] + [cjs-2.js])) - (cjs-in-12.js) + (cjs-4.js + cjs-5.js)') + .then(function(tree) { + assert.deepEqual(Object.keys(tree).sort(), ['cjs-4.js', 'cjs-5.js', 'cjs-in-13.js']); + }) + .then(done, done); + }); + + test('cjs bundles with nested parens 3', function(done){ + builder.trace('((cjs-1.js + cjs-2.js - ([cjs-1.js] + [cjs-2.js])) - (cjs-in-12.js) + (cjs-4.js + cjs-5.js)) - ([cjs-4.js] + [cjs-5.js])') + .then(function(tree) { + assert.deepEqual(Object.keys(tree).sort(), ['cjs-in-13.js']); + }) + .then(done, done); + }); + + test('cjs bundles with nested parens 4', function(done){ + builder.trace('((cjs-1.js + cjs-2.js - ([cjs-1.js] + [cjs-2.js] + ([cjs-4.js] + [cjs-5.js]))) - (cjs-4.js + cjs-5.js))') + .then(function(tree) { + assert.deepEqual(Object.keys(tree).sort(), ['cjs-in-12.js', 'cjs-in-13.js']); + }) + .then(done, done); + }); + + + test('ibid with single module subtracted', function(done){ + builder.trace('(cjs-1.js + cjs-2.js - ([cjs-1.js] + [cjs-2.js])) - ([cjs-in-12.js])') + .then(function(tree) { + assert.deepEqual(Object.keys(tree).sort(), ['cjs-in-13.js']); + }) + .then(done, done); + }); + + test('cjs bundles added with nested parens', function(done){ + builder.trace('(cjs-1.js + cjs-2.js - (cjs-1.js & cjs-2.js))') + .then(function(tree) { + assert.deepEqual(Object.keys(tree).sort(), ['cjs-1.js', 'cjs-2.js', 'cjs-in-13.js']); + }) + .then(done, done); + }); + + test('cjs bundles with parens and single modules', function(done){ + builder.trace('(cjs-1.js + cjs-2.js) - ([cjs-1.js] + [cjs-2.js])') + .then(function(tree) { + assert.deepEqual(Object.keys(tree).sort(), ['cjs-in-12.js', 'cjs-in-13.js']); + }) + .then(done, done); + }); + + test('file with space', function(done){ + builder.trace('cjs-1.js + cjs space.js') + .then(function(tree) { + assert.deepEqual(Object.keys(tree).sort(), [ + 'cjs space.js', 'cjs-1.js', 'cjs-in-12.js', 'cjs-in-13.js']); + }) + .then(done, done); + }); + + test('file with space 2', function(done){ + builder.trace('cjs-1.js + cjs space.js ') + .then(function(tree) { + assert.deepEqual(Object.keys(tree).sort(), [ + 'cjs space.js', 'cjs-1.js', 'cjs-in-12.js', 'cjs-in-13.js']); + }) + .then(done, done); + }); + + test('file with space 3', function(done){ + builder.trace('cjs-1.js + cjs space.js + cjs-2.js') + .then(function(tree) { + assert.deepEqual(Object.keys(tree).sort(), [ + 'cjs space.js', 'cjs-1.js', 'cjs-2.js', 'cjs-in-12.js', 'cjs-in-13.js']); + }) + .then(done, done); + }); + + test('file with space 4', function(done){ + builder.trace('cjs-1.js + cjs space.js + cjs-2.js ') + .then(function(tree) { + assert.deepEqual(Object.keys(tree).sort(), [ + 'cjs space.js', 'cjs-1.js', 'cjs-2.js', 'cjs-in-12.js', 'cjs-in-13.js']); + }) + .then(done, done); + }); + +}); + +suite('Bundle Expression Validation', function() { + test('missing identifier 1', function(){ + return validateInvalidExpression('cjs-1.js + +'); + }); + + test('missing identifier 2', function(){ + return validateInvalidExpression('cjs-1.js + -'); + }); + + test('missing identifier 3', function(){ + return validateInvalidExpression('cjs-1.js + &'); + }); + + test('unclosed parens 1', function(){ + return validateInvalidExpression('(cjs-1.js + cjs-2.js'); + }); + + test('unclosed parens 2', function(){ + return validateInvalidExpression('(cjs-1.js + (cjs-2.js + cjs-3.js'); + }); + + test('unclosed parens 3', function(){ + return validateInvalidExpression('(cjs-1.js + (cjs-2.js + cjs-3.js)'); + }); + + test('unclosed parens 4', function(){ + return validateInvalidExpression('(cjs-2.js + cjs-3.js) + (cjs-1.js + (cjs-2.js + cjs-3.js)'); + }); + + function validateInvalidExpression(expression){ + return Promise + .resolve() + .then(function(){ return builder.trace(expression) }) + .then( + function(){ return Promise.reject('Invalid expression <' + expression + '> was parsed without error'); }, //it worked but shouldn't have + function(err){ + //uncomment this line to view the Syntax Errors' wordings in the test console + //console.log(err); + if (typeof err !== 'string' || !/^Syntax Error/i.test(err)){ + return Promise.reject('Syntax error was expected, but not generated') + } else { + return Promise.resolve(1); + } + } + ) + } + +}); + + diff --git a/test/fixtures/test-tree/cjs space.js b/test/fixtures/test-tree/cjs space.js new file mode 100644 index 0000000..66db397 --- /dev/null +++ b/test/fixtures/test-tree/cjs space.js @@ -0,0 +1,2 @@ +module.exports = { name: 'cjs space' }; + diff --git a/test/fixtures/test-tree/cjs-1.js b/test/fixtures/test-tree/cjs-1.js new file mode 100644 index 0000000..5ac73c8 --- /dev/null +++ b/test/fixtures/test-tree/cjs-1.js @@ -0,0 +1,4 @@ +var shared1 = require('./cjs-in-12.js'); +var shared2 = require('./cjs-in-13.js'); + +module.exports = { name: 'cjs1' }; diff --git a/test/fixtures/test-tree/cjs-2.js b/test/fixtures/test-tree/cjs-2.js new file mode 100644 index 0000000..7e6f547 --- /dev/null +++ b/test/fixtures/test-tree/cjs-2.js @@ -0,0 +1,3 @@ +var shared1 = require('./cjs-in-12.js'); + +module.exports = { name: 'cjs2' }; diff --git a/test/fixtures/test-tree/cjs-3.js b/test/fixtures/test-tree/cjs-3.js new file mode 100644 index 0000000..10b5d9d --- /dev/null +++ b/test/fixtures/test-tree/cjs-3.js @@ -0,0 +1,4 @@ +var shared1 = require('./cjs-in-13.js'); + +module.exports = { name: 'cjs3' }; + diff --git a/test/fixtures/test-tree/cjs-4.js b/test/fixtures/test-tree/cjs-4.js new file mode 100644 index 0000000..1d0bef1 --- /dev/null +++ b/test/fixtures/test-tree/cjs-4.js @@ -0,0 +1 @@ +module.exports = { name: 'cjs4' }; diff --git a/test/fixtures/test-tree/cjs-5.js b/test/fixtures/test-tree/cjs-5.js new file mode 100644 index 0000000..1f27aef --- /dev/null +++ b/test/fixtures/test-tree/cjs-5.js @@ -0,0 +1 @@ +module.exports = { name: 'cjs5' }; diff --git a/test/fixtures/test-tree/cjs-in-12.js b/test/fixtures/test-tree/cjs-in-12.js new file mode 100644 index 0000000..2f81c6d --- /dev/null +++ b/test/fixtures/test-tree/cjs-in-12.js @@ -0,0 +1 @@ +module.exports = { name: 'cjs-in-12' }; diff --git a/test/fixtures/test-tree/cjs-in-13.js b/test/fixtures/test-tree/cjs-in-13.js new file mode 100644 index 0000000..a81e3ba --- /dev/null +++ b/test/fixtures/test-tree/cjs-in-13.js @@ -0,0 +1 @@ +module.exports = { name: 'cjs-in-13' };