UNPKG

cram

Version:

An AMD-compatible build tool.

292 lines (242 loc) 9.42 kB
/** MIT License (c) copyright 2010-2013 B Cavalier & J Hann */ define([], function () { /* * This function should work for most AMD and UMD formats. * It won't work for UMD that has a dependency list, yet references * the dependency list or the factory as variables. These need to be * literals. For instance, the following will not work: * var deps = ['foo', 'bar']; * var factory = function (foo, bar) {}; * define(deps, factory); * * The following will work, of course: * define(['foo', 'bar'], function (foo, bar) {}); * * Comments and line feeds amongst the define() args are handled. * * Things we need to know about the module: * - where to insert (or replace) module id * - what are the dependencies * - r-value requires should be moved to dep list and * variable name assigned. then, substitute require('...') * with variable */ var findDefinesRx, removeCommentsRx, cleanDepsRx, splitArgsRx, countParensRx; // Find all of these signatures (plus some more variants): // define("id", ["dep1", "dep2"], function (dep1, dep2) {... // define("id", function (require) {... // define("id", function factoryName (require) {... // define("id", 5... // define("id", {... // define("id", "foo"... // define("id", new Date()... // define("id", factoryName... // define(["dep1", "dep2"], function (dep1, dep2) {... // define(function (require) {... // define(function factoryName (require) {... // define(5... // define({... // define("foo"... // define(new Date()... // define(factoryName... // also, this special wire-specific pattern: // define(['a', 'b'], { /* require() in object literal */ }); // TODO: capture entire AMD define text instead of .count so it can be recreated better findDefinesRx = new RegExp( // filter out some false positives that we can't eliminate in the // rest of the regexps since we may grab too many chars if we do. // these are not captured. '[.$_]require|[.$_]define' // also find "require('id')" (r-val) + '|\\brequire\\s*\\(\\s*["\']([^"\']+)["\']\\s*\\)' // find "define ( 'id'? , [deps]? ," capturing id and deps + '|(\\bdefine)\\s*\\(' // define call + '|\'([^\']*)\'\\s*,|["]([^"]*)["]\\s*,' // id (with comma) + '|(?:\\[\\s*([^\\]]*?)\\s*\\]\\s*,)' // deps array (with comma) // find "function name? (args?) {" OR "{" // args doesn't match on quotes ([^)\'"]*) to prevent this snippet from // lodash from capturing an extra quote: `'function(' + x + ') {\n'` + '|(?:(function)\\s*[0-9a-zA-Z_$]*\\s*\\(([^)\'"]*)\\))?\\s*({)' // block comments + '|(\\/\\*)|(\\*\\/)' // line comments + '|(\\/{2})|(\\n|\\r|$)' // regexp literals. to disambiguate division sign, check for leading // operators. Note: this will falsely identify a division sign // at the start of a line as a regexp when there is a second division // sign on the same line. Seriously edgy case, imho. + '|[+\\-*\/=\\,%&|^!(;\\{\\[<>]\\s*(\\/)[^\\/\\n]+\\/' // TODO: escaped slashes // quotes and double-quotes // escaped quotes are captured, too, but double escapes are ignored + '|(?:\\\\\\\\)|(\\\\["\'])|(["\'])' // parens (we need to count them to find the end of the "define(") + '|(\\()|(\\))', 'g' ); removeCommentsRx = /\/\*[\s\S]*?\*\/|\/\/.*?[\n\r]/g; cleanDepsRx = /\s*["']\s*/g; splitArgsRx = /\s*,\s*/; countParensRx = /\(|\)/; function scan (source) { var modules, module, pCount; // states: var inComment, inString, inDefine, inFactory; // transitions: // TODO: hoist amdParser so it can be tested // starting state: trigger: ending state: // ----------------------------------------------------------------------------------- // <default state> --[find "define("]--> inDefine // <default state> --[find "require(id)"]--> <throw / exit> // inDefine --[find module id]--> inDefine // inDefine --[find dep array]--> inDefine // inDefine --[find end of factory]--> inFactory // inDefine --[pCount reaches 0]--> <default state> (we may have not found a factory) // inDefine --[find start of comment]--> inComment + inDefine // inDefine --[find start of string]--> inString + inDefine // inDefine --[find "define("]--> <throw / exit> // inFactory --[pCount reaches 0]--> <default state> // inFactory --[find start of comment]--> inComment + inFactory // inFactory --[find start of string]--> inString + inFactory // inFactory --[find "require(id)"]--> inFactory // inFactory --[find "define("]--> <throw / exit> // inComment + <previous state> --[end of comment]--> <previous state> // inString + <previous state> --[end of string]--> <previous state> modules = []; pCount = 0; // use .replace() as a fast parsing mechanism, feeding our state machine: source.replace(findDefinesRx, amdParser); return modules; function amdParser (m, rval, def, id1, id2, deps, factory, args, fStart, bcStart, bcEnd, lcStart, lcEnd, rx, escQ, q, pStart, pEnd, matchPos, source) { var id = id1 || id2; // optimization: skip over escaped quotes if (escQ) return ''; // optimization: skip over regexps, but count parens if (rx) { pCount += countParensInString(m); return ''; } // optimization: fStart matches "{" (a lot!), so quit early when not inDefine if (!inDefine && fStart) return ''; // optimization: inComment and inString are treated as a separate set of // states from the primary states (inDefine, inFactory). // if either of these is true, don't replace (or push) the previous state. // just "carry" it along until we leave inComment or inString. // this is simpler and faster than pushing/popping the previous state. if (inComment) { // check if we're leaving a comment if ((inComment == '/*' && bcEnd != null) || (inComment == '//' && lcEnd != null)) inComment = ''; } else if (inString) { // check if we're leaving the string if (inString == q) inString = ''; } else if (inFactory) { if (def) throw new Error('embedded define() or parsing error.'); if (rval) { if (!module.requires) module.requires = []; module.requires.push({ id: rval, pos: matchPos, count: m.length }); } else { checkParens(pStart, pEnd); checkCommentsAndStrings(bcStart || lcStart, q); } } else if (inDefine) { if (def) throw new Error('embedded define() or parsing error.'); if (id) { module.id = id; } else if (deps) { captureDeps(deps); captureSignatureCount(matchPos, m.length); } else if (fStart) { if (factory) module.factory = !!factory; if (args) captureArgs(args); // the -1 is here to remove final "{" which is not inserted // by the normalize step. this will get fixed when we start // capturing the entire define() text. See TODO above. captureSignatureCount(matchPos, m.length - 1); toFactory(); } else { checkParens(pStart, pEnd); checkCommentsAndStrings(bcStart || lcStart, q); } } else /* outside of a define, factory, comment, or string */ { if (rval) { if (!modules.warnings) modules.warnings = []; modules.warnings.push('sync require() found in the global scope or in an external factory.'); } if (def) { toDefine(); captureSignatureCount(matchPos, m.length); } else { checkParens(pStart, pEnd); checkCommentsAndStrings(bcStart || lcStart, q); } } return ''; function toGlobal () { inFactory = inDefine = false; } function toDefine () { module = {}; modules.push(module); inFactory = false; inDefine = true; pCount = 1; // reset to easily find the end } function toFactory () { inFactory = true; inDefine = false; } function captureSignatureCount (pos, length) { // this will get recomputed multiple times if there are deps or a factory // if this is the first time, record start of "define(" if (module.pos == null) module.pos = pos; // the first time here, this just captures length module.count = pos - module.pos + length; } function captureArgs (args) { module.argList = args.replace(removeCommentsRx, '').split(splitArgsRx); } function captureDeps (deps) { module.depList = deps.replace(removeCommentsRx, '') .replace(cleanDepsRx, '') .split(splitArgsRx); } function checkParens (enter, exit) { if (enter) { pCount++; } else if (exit) { pCount--; if (pCount === 0) { // we hit the end of the "define(" toGlobal(); } } } function checkCommentsAndStrings (commentType, stringType) { if (commentType) inComment = commentType; if (stringType) inString = stringType; } } } return scan; function countParensInString (str) { var matches = str.match(countParensRx); return matches ? matches.reduce(reduceParens, 0) : 0; } function reduceParens (count, p) { return count + (p === '(' ? 1 : -1) } });