/app/scripts/vendor/hm.js

https://bitbucket.org/ceoaliongroo/torredelprior · JavaScript · 459 lines · 439 code · 7 blank · 13 comment · 7 complexity · 1e89fc681249dae1c7c138c9b1931aa3 MD5 · raw file

  1. /**
  2. * @license hm 0.2.1 Copyright (c) 2010-2012, The Dojo Foundation All Rights Reserved.
  3. * Available via the MIT or new BSD license.
  4. * see: http://github.com/jrburke/require-hm for details
  5. */
  6. /*jslint plusplus: true, regexp: true */
  7. /*global require, XMLHttpRequest, ActiveXObject, define, process, window,
  8. console */
  9. define(['esprima', 'module'], function (esprima, module) {
  10. 'use strict';
  11. var fs, getXhr,
  12. progIds = ['Msxml2.XMLHTTP', 'Microsoft.XMLHTTP', 'Msxml2.XMLHTTP.4.0'],
  13. exportRegExp = /export\s+([A-Za-z\d\_]+)(\s+([A-Za-z\d\_]+))?/g,
  14. commentRegExp = /(\/\*([\s\S]*?)\*\/|[^\:]\/\/(.*)$)/mg,
  15. importModuleRegExp = /module|import/g,
  16. commaRegExp = /\,\s*$/,
  17. spaceRegExp = /\s+/,
  18. quoteRegExp = /['"]/,
  19. endingPuncRegExp = /[\,\;]\s*$/,
  20. moduleNameRegExp = /['"]([^'"]+)['"]/,
  21. startQuoteRegExp = /^['"]/,
  22. braceRegExp = /[\{\}]/g,
  23. buildMap = {},
  24. fetchText = function () {
  25. throw new Error('Environment unsupported.');
  26. };
  27. if (typeof window !== "undefined" && window.navigator && window.document) {
  28. // Browser action
  29. getXhr = function () {
  30. //Would love to dump the ActiveX crap in here. Need IE 6 to die first.
  31. var xhr, i, progId;
  32. if (typeof XMLHttpRequest !== "undefined") {
  33. return new XMLHttpRequest();
  34. } else {
  35. for (i = 0; i < 3; i++) {
  36. progId = progIds[i];
  37. try {
  38. xhr = new ActiveXObject(progId);
  39. } catch (e) {}
  40. if (xhr) {
  41. progIds = [progId]; // so faster next time
  42. break;
  43. }
  44. }
  45. }
  46. if (!xhr) {
  47. throw new Error("getXhr(): XMLHttpRequest not available");
  48. }
  49. return xhr;
  50. };
  51. fetchText = function (url, callback) {
  52. var xhr = getXhr();
  53. xhr.open('GET', url, true);
  54. xhr.onreadystatechange = function (evt) {
  55. //Do not explicitly handle errors, those should be
  56. //visible via console output in the browser.
  57. if (xhr.readyState === 4) {
  58. callback(xhr.responseText);
  59. }
  60. };
  61. xhr.send(null);
  62. };
  63. } else if (typeof process !== "undefined" &&
  64. process.versions &&
  65. !!process.versions.node) {
  66. //Using special require.nodeRequire, something added by r.js.
  67. fs = require.nodeRequire('fs');
  68. fetchText = function (path, callback) {
  69. callback(fs.readFileSync(path, 'utf8'));
  70. };
  71. }
  72. /**
  73. * Helper function for iterating over an array. If the func returns
  74. * a true value, it will break out of the loop.
  75. */
  76. function each(ary, func) {
  77. if (ary) {
  78. var i;
  79. for (i = 0; i < ary.length; i += 1) {
  80. if (ary[i] && func(ary[i], i, ary)) {
  81. break;
  82. }
  83. }
  84. }
  85. }
  86. /**
  87. * Cycles over properties in an object and calls a function for each
  88. * property value. If the function returns a truthy value, then the
  89. * iteration is stopped.
  90. */
  91. function eachProp(obj, func) {
  92. var prop;
  93. for (prop in obj) {
  94. if (obj.hasOwnProperty(prop)) {
  95. if (func(obj[prop], prop)) {
  96. break;
  97. }
  98. }
  99. }
  100. }
  101. /**
  102. * Inserts the hm! loader plugin prefix if necessary. If
  103. * there is already a ! in the string, then leave it be, and if it
  104. * starts with a ! it means "use normal AMD loading for this dependency".
  105. *
  106. * @param {String} id
  107. * @returns id
  108. */
  109. function cleanModuleId(id) {
  110. id = moduleNameRegExp.exec(id)[1];
  111. var index = id.indexOf('!');
  112. if (index === -1) {
  113. // Needs the hm prefix.
  114. id = 'hm!' + id;
  115. } else if (index === 0) {
  116. //Normal AMD loading, strip off the ! sign.
  117. id = id.substring(1);
  118. }
  119. return id;
  120. }
  121. function convertImportSyntax(tokens, start, end, moduleTarget) {
  122. var token = tokens[start],
  123. cursor = start,
  124. replacement = '',
  125. localVars = {},
  126. moduleRef,
  127. moduleId,
  128. star,
  129. currentVar;
  130. //Convert module target to an AMD usable name. If a string,
  131. //then needs to be accessed via require()
  132. if (startQuoteRegExp.test(moduleTarget)) {
  133. moduleId = cleanModuleId(moduleTarget);
  134. moduleRef = 'require("' + moduleId + '")';
  135. } else {
  136. moduleRef = moduleTarget;
  137. }
  138. if (token.type === 'Punctuator' && token.value === '*') {
  139. //import * from z
  140. //If not using a module ID that is a require call, then
  141. //discard it.
  142. if (moduleId) {
  143. star = moduleId;
  144. replacement = '/*IMPORTSTAR:' + star + '*/\n';
  145. } else {
  146. throw new Error('import * on local reference ' + moduleTarget +
  147. ' no supported.');
  148. }
  149. } else if (token.type === 'Identifier') {
  150. //import y from z
  151. replacement += 'var ' + token.value + ' = ' +
  152. moduleRef + '.' + token.value + ';';
  153. } else if (token.type === 'Punctuator' && token.value === '{') {
  154. //import {y} from z
  155. //import {x, y} from z
  156. //import {x: localX, y: localY} from z
  157. cursor += 1;
  158. token = tokens[cursor];
  159. while (cursor !== end && token.value !== '}') {
  160. if (token.type === 'Identifier') {
  161. if (currentVar) {
  162. localVars[currentVar] = token.value;
  163. currentVar = null;
  164. } else {
  165. currentVar = token.value;
  166. }
  167. } else if (token.type === 'Punctuator') {
  168. if (token.value === ',') {
  169. if (currentVar) {
  170. localVars[currentVar] = currentVar;
  171. currentVar = null;
  172. }
  173. }
  174. }
  175. cursor += 1;
  176. token = tokens[cursor];
  177. }
  178. if (currentVar) {
  179. localVars[currentVar] = currentVar;
  180. }
  181. //Now serialize the localVars
  182. eachProp(localVars, function (localName, importProp) {
  183. replacement += 'var ' + localName + ' = ' +
  184. moduleRef + '.' + importProp + ';\n';
  185. });
  186. } else {
  187. throw new Error('Invalid import: import ' +
  188. token.value + ' ' + tokens[start + 1].value +
  189. ' ' + tokens[start + 2].value);
  190. }
  191. return {
  192. star: star,
  193. replacement: replacement
  194. };
  195. }
  196. function convertModuleSyntax(tokens, i) {
  197. //Converts `foo = 'bar'` to `foo = require('bar')`
  198. var varName = tokens[i],
  199. eq = tokens[i + 1],
  200. id = tokens[i + 2];
  201. if (varName.type === 'Identifier' &&
  202. eq.type === 'Punctuator' && eq.value === '=' &&
  203. id.type === 'String') {
  204. return varName.value + ' = require("' + cleanModuleId(id.value) + '")';
  205. } else {
  206. throw new Error('Invalid module reference: module ' +
  207. varName.value + ' ' + eq.value + ' ' + id.value);
  208. }
  209. }
  210. function compile(path, text) {
  211. var stars = [],
  212. moduleMap = {},
  213. transforms = {},
  214. targets = [],
  215. currentIndex = 0,
  216. //Remove comments from the text to be scanned
  217. scanText = text.replace(commentRegExp, ""),
  218. transformedText = text,
  219. transformInputText,
  220. startIndex,
  221. segmentIndex,
  222. match,
  223. tempText,
  224. transformed,
  225. tokens;
  226. try {
  227. tokens = esprima.parse(text, {
  228. tokens: true,
  229. range: true
  230. }).tokens;
  231. } catch (e) {
  232. throw new Error('Esprima cannot parse: ' + path + ': ' + e);
  233. }
  234. each(tokens, function (token, i) {
  235. if (token.type !== 'Keyword' && token.type !== 'Identifier') {
  236. //Not relevant, skip
  237. return;
  238. }
  239. var next = tokens[i + 1],
  240. next2 = tokens[i + 2],
  241. next3 = tokens[i + 3],
  242. cursor = i,
  243. replacement,
  244. moduleTarget,
  245. target,
  246. convertedImport;
  247. if (token.value === 'export') {
  248. // EXPORTS
  249. if (next.type === 'Keyword') {
  250. if (next.value === 'var' || next.value === 'let') {
  251. targets.push({
  252. start: token.range[0],
  253. end: next2.range[0],
  254. replacement: 'exports.'
  255. });
  256. } else if (next.value === 'function' && next2.type === 'Identifier') {
  257. targets.push({
  258. start: token.range[0],
  259. end: next2.range[1],
  260. replacement: 'exports.' + next2.value +
  261. ' = function '
  262. });
  263. } else {
  264. throw new Error('Invalid export: ' + token.value +
  265. ' ' + next.value + ' ' + tokens[i + 2]);
  266. }
  267. } else if (next.type === 'Identifier') {
  268. targets.push({
  269. start: token.range[0],
  270. end: next.range[1],
  271. replacement: 'exports.' + next.value +
  272. ' = ' + next.value
  273. });
  274. } else {
  275. throw new Error('Invalid export: ' + token.value +
  276. ' ' + next.value + ' ' + tokens[i + 2]);
  277. }
  278. } else if (token.value === 'module') {
  279. // MODULE
  280. // module Bar = "bar.js";
  281. replacement = 'var ';
  282. target = {
  283. start: token.range[0]
  284. };
  285. while (token.value === 'module' || (token.type === 'Punctuator'
  286. && token.value === ',')) {
  287. cursor = cursor + 1;
  288. replacement += convertModuleSyntax(tokens, cursor);
  289. token = tokens[cursor + 3];
  290. //Current module spec does not allow for
  291. //module a = 'a', b = 'b';
  292. //must end in semicolon. But keep this in case for later,
  293. //as comma separators would be nice.
  294. //esprima will throw if comma is not allowed.
  295. if ((token.type === 'Punctuator' && token.value === ',')) {
  296. replacement += ',\n';
  297. }
  298. }
  299. target.end = token.range[0];
  300. target.replacement = replacement;
  301. targets.push(target);
  302. } else if (token.value === 'import') {
  303. // IMPORT
  304. //import * from z;
  305. //import y from z;
  306. //import {y} from z;
  307. //import {x, y} from z;
  308. //import {x: localX, y: localY} from z;
  309. cursor = i;
  310. //Find the "from" in the stream
  311. while (tokens[cursor] &&
  312. (tokens[cursor].type !== 'Identifier' ||
  313. tokens[cursor].value !== 'from')) {
  314. cursor += 1;
  315. }
  316. //Increase cursor one more value to find the module target
  317. moduleTarget = tokens[cursor + 1].value;
  318. convertedImport = convertImportSyntax(tokens, i + 1, cursor - 1, moduleTarget);
  319. replacement = convertedImport.replacement;
  320. if (convertedImport.star) {
  321. stars.push(convertedImport.star);
  322. }
  323. targets.push({
  324. start: token.range[0],
  325. end: tokens[cursor + 3].range[0],
  326. replacement: replacement
  327. });
  328. }
  329. });
  330. //Now sort all the targets, but by start position, with the
  331. //furthest start position first, since we need to transpile
  332. //in reverse order.
  333. targets.sort(function (a, b) {
  334. return a.start > b.start ? -1 : 1;
  335. });
  336. //Now walk backwards through targets and do source modifications
  337. //to AMD. Going backwards is important since the modifications will
  338. //modify the length of the string.
  339. each(targets, function (target, i) {
  340. transformedText = transformedText.substring(0, target.start) +
  341. target.replacement +
  342. transformedText.substring(target.end, transformedText.length);
  343. });
  344. return {
  345. text: "define(function (require, exports, module) {\n" +
  346. transformedText +
  347. '\n});',
  348. stars: stars
  349. };
  350. }
  351. function finishLoad(require, load, name, transformedText, text, isBuild) {
  352. //Hold on to the transformed text if a build.
  353. if (isBuild) {
  354. buildMap[name] = transformedText;
  355. }
  356. load.fromText(name, transformedText);
  357. if (module.config().logTransform) {
  358. console.log("INPUT:\n" + text + "\n\nTRANSFORMED:\n" + transformedText);
  359. }
  360. //Give result to load. Need to wait until the module
  361. //is fully parsed, which will happen after this
  362. //execution.
  363. require([name], function (value) {
  364. load(value);
  365. });
  366. }
  367. return {
  368. version: '0.2.1',
  369. write: function (pluginName, name, write) {
  370. if (buildMap.hasOwnProperty(name)) {
  371. var text = buildMap[name];
  372. write.asModule(pluginName + "!" + name, text);
  373. }
  374. },
  375. load: function (name, require, load, config) {
  376. var path = require.toUrl(name + '.hm');
  377. fetchText(path, function (text) {
  378. var result = compile(path, text),
  379. transformedText = result.text;
  380. //IE with conditional comments on cannot handle the
  381. //sourceURL trick, so skip it if enabled.
  382. /*@if (@_jscript) @else @*/
  383. if (!config.isBuild) {
  384. transformedText += "\r\n//@ sourceURL=" + path;
  385. }
  386. /*@end@*/
  387. if (result.stars && result.stars.length) {
  388. //First load any imports that require recursive analysis
  389. //TODO: this will break if there is a circular
  390. //dependency with each file doing an import * on each other.
  391. require(result.stars, function () {
  392. var i, star, mod, starText, prop;
  393. //Now fix up the import * items for each module.
  394. for (i = 0; i < result.stars.length; i++) {
  395. star = result.stars[i];
  396. starText = '';
  397. mod = arguments[i];
  398. for (prop in mod) {
  399. if (mod.hasOwnProperty(prop)) {
  400. starText += 'var ' + prop + ' = require("' + star + '").' + prop + '; ';
  401. }
  402. }
  403. transformedText = transformedText.replace('/*IMPORTSTAR:' + star + '*/', starText);
  404. }
  405. finishLoad(require, load, name, transformedText, text, config.isBuild);
  406. });
  407. } else {
  408. finishLoad(require, load, name, transformedText, text, config.isBuild);
  409. }
  410. });
  411. }
  412. };
  413. });