/** * @license hm 0.2.1 Copyright (c) 2010-2012, The Dojo Foundation All Rights Reserved. * Available via the MIT or new BSD license. * see: http://github.com/jrburke/require-hm for details */ /*jslint plusplus: true, regexp: true */ /*global require, XMLHttpRequest, ActiveXObject, define, process, window, console */ define(['esprima', 'module'], function (esprima, module) { 'use strict'; var fs, getXhr, progIds = ['Msxml2.XMLHTTP', 'Microsoft.XMLHTTP', 'Msxml2.XMLHTTP.4.0'], exportRegExp = /export\s+([A-Za-z\d\_]+)(\s+([A-Za-z\d\_]+))?/g, commentRegExp = /(\/\*([\s\S]*?)\*\/|[^\:]\/\/(.*)$)/mg, importModuleRegExp = /module|import/g, commaRegExp = /\,\s*$/, spaceRegExp = /\s+/, quoteRegExp = /['"]/, endingPuncRegExp = /[\,\;]\s*$/, moduleNameRegExp = /['"]([^'"]+)['"]/, startQuoteRegExp = /^['"]/, braceRegExp = /[\{\}]/g, buildMap = {}, fetchText = function () { throw new Error('Environment unsupported.'); }; if (typeof window !== "undefined" && window.navigator && window.document) { // Browser action getXhr = function () { //Would love to dump the ActiveX crap in here. Need IE 6 to die first. var xhr, i, progId; if (typeof XMLHttpRequest !== "undefined") { return new XMLHttpRequest(); } else { for (i = 0; i < 3; i++) { progId = progIds[i]; try { xhr = new ActiveXObject(progId); } catch (e) {} if (xhr) { progIds = [progId]; // so faster next time break; } } } if (!xhr) { throw new Error("getXhr(): XMLHttpRequest not available"); } return xhr; }; fetchText = function (url, callback) { var xhr = getXhr(); xhr.open('GET', url, true); xhr.onreadystatechange = function (evt) { //Do not explicitly handle errors, those should be //visible via console output in the browser. if (xhr.readyState === 4) { callback(xhr.responseText); } }; xhr.send(null); }; } else if (typeof process !== "undefined" && process.versions && !!process.versions.node) { //Using special require.nodeRequire, something added by r.js. fs = require.nodeRequire('fs'); fetchText = function (path, callback) { callback(fs.readFileSync(path, 'utf8')); }; } /** * Helper function for iterating over an array. If the func returns * a true value, it will break out of the loop. */ function each(ary, func) { if (ary) { var i; for (i = 0; i < ary.length; i += 1) { if (ary[i] && func(ary[i], i, ary)) { break; } } } } /** * Cycles over properties in an object and calls a function for each * property value. If the function returns a truthy value, then the * iteration is stopped. */ function eachProp(obj, func) { var prop; for (prop in obj) { if (obj.hasOwnProperty(prop)) { if (func(obj[prop], prop)) { break; } } } } /** * Inserts the hm! loader plugin prefix if necessary. If * there is already a ! in the string, then leave it be, and if it * starts with a ! it means "use normal AMD loading for this dependency". * * @param {String} id * @returns id */ function cleanModuleId(id) { id = moduleNameRegExp.exec(id)[1]; var index = id.indexOf('!'); if (index === -1) { // Needs the hm prefix. id = 'hm!' + id; } else if (index === 0) { //Normal AMD loading, strip off the ! sign. id = id.substring(1); } return id; } function convertImportSyntax(tokens, start, end, moduleTarget) { var token = tokens[start], cursor = start, replacement = '', localVars = {}, moduleRef, moduleId, star, currentVar; //Convert module target to an AMD usable name. If a string, //then needs to be accessed via require() if (startQuoteRegExp.test(moduleTarget)) { moduleId = cleanModuleId(moduleTarget); moduleRef = 'require("' + moduleId + '")'; } else { moduleRef = moduleTarget; } if (token.type === 'Punctuator' && token.value === '*') { //import * from z //If not using a module ID that is a require call, then //discard it. if (moduleId) { star = moduleId; replacement = '/*IMPORTSTAR:' + star + '*/\n'; } else { throw new Error('import * on local reference ' + moduleTarget + ' no supported.'); } } else if (token.type === 'Identifier') { //import y from z replacement += 'var ' + token.value + ' = ' + moduleRef + '.' + token.value + ';'; } else if (token.type === 'Punctuator' && token.value === '{') { //import {y} from z //import {x, y} from z //import {x: localX, y: localY} from z cursor += 1; token = tokens[cursor]; while (cursor !== end && token.value !== '}') { if (token.type === 'Identifier') { if (currentVar) { localVars[currentVar] = token.value; currentVar = null; } else { currentVar = token.value; } } else if (token.type === 'Punctuator') { if (token.value === ',') { if (currentVar) { localVars[currentVar] = currentVar; currentVar = null; } } } cursor += 1; token = tokens[cursor]; } if (currentVar) { localVars[currentVar] = currentVar; } //Now serialize the localVars eachProp(localVars, function (localName, importProp) { replacement += 'var ' + localName + ' = ' + moduleRef + '.' + importProp + ';\n'; }); } else { throw new Error('Invalid import: import ' + token.value + ' ' + tokens[start + 1].value + ' ' + tokens[start + 2].value); } return { star: star, replacement: replacement }; } function convertModuleSyntax(tokens, i) { //Converts `foo = 'bar'` to `foo = require('bar')` var varName = tokens[i], eq = tokens[i + 1], id = tokens[i + 2]; if (varName.type === 'Identifier' && eq.type === 'Punctuator' && eq.value === '=' && id.type === 'String') { return varName.value + ' = require("' + cleanModuleId(id.value) + '")'; } else { throw new Error('Invalid module reference: module ' + varName.value + ' ' + eq.value + ' ' + id.value); } } function compile(path, text) { var stars = [], moduleMap = {}, transforms = {}, targets = [], currentIndex = 0, //Remove comments from the text to be scanned scanText = text.replace(commentRegExp, ""), transformedText = text, transformInputText, startIndex, segmentIndex, match, tempText, transformed, tokens; try { tokens = esprima.parse(text, { tokens: true, range: true }).tokens; } catch (e) { throw new Error('Esprima cannot parse: ' + path + ': ' + e); } each(tokens, function (token, i) { if (token.type !== 'Keyword' && token.type !== 'Identifier') { //Not relevant, skip return; } var next = tokens[i + 1], next2 = tokens[i + 2], next3 = tokens[i + 3], cursor = i, replacement, moduleTarget, target, convertedImport; if (token.value === 'export') { // EXPORTS if (next.type === 'Keyword') { if (next.value === 'var' || next.value === 'let') { targets.push({ start: token.range[0], end: next2.range[0], replacement: 'exports.' }); } else if (next.value === 'function' && next2.type === 'Identifier') { targets.push({ start: token.range[0], end: next2.range[1], replacement: 'exports.' + next2.value + ' = function ' }); } else { throw new Error('Invalid export: ' + token.value + ' ' + next.value + ' ' + tokens[i + 2]); } } else if (next.type === 'Identifier') { targets.push({ start: token.range[0], end: next.range[1], replacement: 'exports.' + next.value + ' = ' + next.value }); } else { throw new Error('Invalid export: ' + token.value + ' ' + next.value + ' ' + tokens[i + 2]); } } else if (token.value === 'module') { // MODULE // module Bar = "bar.js"; replacement = 'var '; target = { start: token.range[0] }; while (token.value === 'module' || (token.type === 'Punctuator' && token.value === ',')) { cursor = cursor + 1; replacement += convertModuleSyntax(tokens, cursor); token = tokens[cursor + 3]; //Current module spec does not allow for //module a = 'a', b = 'b'; //must end in semicolon. But keep this in case for later, //as comma separators would be nice. //esprima will throw if comma is not allowed. if ((token.type === 'Punctuator' && token.value === ',')) { replacement += ',\n'; } } target.end = token.range[0]; target.replacement = replacement; targets.push(target); } else if (token.value === 'import') { // IMPORT //import * from z; //import y from z; //import {y} from z; //import {x, y} from z; //import {x: localX, y: localY} from z; cursor = i; //Find the "from" in the stream while (tokens[cursor] && (tokens[cursor].type !== 'Identifier' || tokens[cursor].value !== 'from')) { cursor += 1; } //Increase cursor one more value to find the module target moduleTarget = tokens[cursor + 1].value; convertedImport = convertImportSyntax(tokens, i + 1, cursor - 1, moduleTarget); replacement = convertedImport.replacement; if (convertedImport.star) { stars.push(convertedImport.star); } targets.push({ start: token.range[0], end: tokens[cursor + 3].range[0], replacement: replacement }); } }); //Now sort all the targets, but by start position, with the //furthest start position first, since we need to transpile //in reverse order. targets.sort(function (a, b) { return a.start > b.start ? -1 : 1; }); //Now walk backwards through targets and do source modifications //to AMD. Going backwards is important since the modifications will //modify the length of the string. each(targets, function (target, i) { transformedText = transformedText.substring(0, target.start) + target.replacement + transformedText.substring(target.end, transformedText.length); }); return { text: "define(function (require, exports, module) {\n" + transformedText + '\n});', stars: stars }; } function finishLoad(require, load, name, transformedText, text, isBuild) { //Hold on to the transformed text if a build. if (isBuild) { buildMap[name] = transformedText; } load.fromText(name, transformedText); if (module.config().logTransform) { console.log("INPUT:\n" + text + "\n\nTRANSFORMED:\n" + transformedText); } //Give result to load. Need to wait until the module //is fully parsed, which will happen after this //execution. require([name], function (value) { load(value); }); } return { version: '0.2.1', write: function (pluginName, name, write) { if (buildMap.hasOwnProperty(name)) { var text = buildMap[name]; write.asModule(pluginName + "!" + name, text); } }, load: function (name, require, load, config) { var path = require.toUrl(name + '.hm'); fetchText(path, function (text) { var result = compile(path, text), transformedText = result.text; //IE with conditional comments on cannot handle the //sourceURL trick, so skip it if enabled. /*@if (@_jscript) @else @*/ if (!config.isBuild) { transformedText += "\r\n//@ sourceURL=" + path; } /*@end@*/ if (result.stars && result.stars.length) { //First load any imports that require recursive analysis //TODO: this will break if there is a circular //dependency with each file doing an import * on each other. require(result.stars, function () { var i, star, mod, starText, prop; //Now fix up the import * items for each module. for (i = 0; i < result.stars.length; i++) { star = result.stars[i]; starText = ''; mod = arguments[i]; for (prop in mod) { if (mod.hasOwnProperty(prop)) { starText += 'var ' + prop + ' = require("' + star + '").' + prop + '; '; } } transformedText = transformedText.replace('/*IMPORTSTAR:' + star + '*/', starText); } finishLoad(require, load, name, transformedText, text, config.isBuild); }); } else { finishLoad(require, load, name, transformedText, text, config.isBuild); } }); } }; });