react/vendor/browser-transforms.js

336 lines
9.4 KiB
JavaScript

/**
* Copyright 2013-2015, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
/* jshint browser: true */
/* jslint evil: true */
/*eslint-disable no-eval */
/*eslint-disable block-scoped-var */
'use strict';
var ReactTools = require('../main');
var inlineSourceMap = require('./inline-source-map');
var headEl;
var dummyAnchor;
var inlineScriptCount = 0;
// The source-map library relies on Object.defineProperty, but IE8 doesn't
// support it fully even with es5-sham. Indeed, es5-sham's defineProperty
// throws when Object.prototype.__defineGetter__ is missing, so we skip building
// the source map in that case.
var supportsAccessors = Object.prototype.hasOwnProperty('__defineGetter__');
/**
* Run provided code through jstransform.
*
* @param {string} source Original source code
* @param {object?} options Options to pass to jstransform
* @return {object} object as returned from jstransform
*/
function transformReact(source, options) {
options = options || {};
// Force the sourcemaps option manually. We don't want to use it if it will
// break (see above note about supportsAccessors). We'll only override the
// value here if sourceMap was specified and is truthy. This guarantees that
// we won't override any user intent (since this method is exposed publicly).
if (options.sourceMap) {
options.sourceMap = supportsAccessors;
}
// Otherwise just pass all options straight through to react-tools.
return ReactTools.transformWithDetails(source, options);
}
/**
* Eval provided source after transforming it.
*
* @param {string} source Original source code
* @param {object?} options Options to pass to jstransform
*/
function exec(source, options) {
return eval(transformReact(source, options).code);
}
/**
* This method returns a nicely formated line of code pointing to the exact
* location of the error `e`. The line is limited in size so big lines of code
* are also shown in a readable way.
*
* Example:
* ... x', overflow:'scroll'}} id={} onScroll={this.scroll} class=" ...
* ^
*
* @param {string} code The full string of code
* @param {Error} e The error being thrown
* @return {string} formatted message
* @internal
*/
function createSourceCodeErrorMessage(code, e) {
var sourceLines = code.split('\n');
// e.lineNumber is non-standard so we can't depend on its availability. If
// we're in a browser where it isn't supported, don't even bother trying to
// format anything. We may also hit a case where the line number is reported
// incorrectly and is outside the bounds of the actual code. Handle that too.
if (!e.lineNumber || e.lineNumber > sourceLines.length) {
return '';
}
var erroneousLine = sourceLines[e.lineNumber - 1];
// Removes any leading indenting spaces and gets the number of
// chars indenting the `erroneousLine`
var indentation = 0;
erroneousLine = erroneousLine.replace(/^\s+/, function(leadingSpaces) {
indentation = leadingSpaces.length;
return '';
});
// Defines the number of characters that are going to show
// before and after the erroneous code
var LIMIT = 30;
var errorColumn = e.column - indentation;
if (errorColumn > LIMIT) {
erroneousLine = '... ' + erroneousLine.slice(errorColumn - LIMIT);
errorColumn = 4 + LIMIT;
}
if (erroneousLine.length - errorColumn > LIMIT) {
erroneousLine = erroneousLine.slice(0, errorColumn + LIMIT) + ' ...';
}
var message = '\n\n' + erroneousLine + '\n';
message += new Array(errorColumn - 1).join(' ') + '^';
return message;
}
/**
* Actually transform the code.
*
* @param {string} code
* @param {string?} url
* @param {object?} options
* @return {string} The transformed code.
* @internal
*/
function transformCode(code, url, options) {
try {
var transformed = transformReact(code, options);
} catch(e) {
e.message += '\n at ';
if (url) {
if ('fileName' in e) {
// We set `fileName` if it's supported by this error object and
// a `url` was provided.
// The error will correctly point to `url` in Firefox.
e.fileName = url;
}
e.message += url + ':' + e.lineNumber + ':' + e.columnNumber;
} else {
e.message += location.href;
}
e.message += createSourceCodeErrorMessage(code, e);
throw e;
}
if (!transformed.sourceMap) {
return transformed.code;
}
var source;
if (url == null) {
source = 'Inline JSX script';
inlineScriptCount++;
if (inlineScriptCount > 1) {
source += ' (' + inlineScriptCount + ')';
}
} else if (dummyAnchor) {
// Firefox has problems when the sourcemap source is a proper URL with a
// protocol and hostname, so use the pathname. We could use just the
// filename, but hopefully using the full path will prevent potential
// issues where the same filename exists in multiple directories.
dummyAnchor.href = url;
source = dummyAnchor.pathname.substr(1);
}
return (
transformed.code +
'\n' +
inlineSourceMap(transformed.sourceMap, code, source)
);
}
/**
* Appends a script element at the end of the <head> with the content of code,
* after transforming it.
*
* @param {string} code The original source code
* @param {string?} url Where the code came from. null if inline
* @param {object?} options Options to pass to jstransform
* @internal
*/
function run(code, url, options) {
var scriptEl = document.createElement('script');
scriptEl.text = transformCode(code, url, options);
headEl.appendChild(scriptEl);
}
/**
* Load script from the provided url and pass the content to the callback.
*
* @param {string} url The location of the script src
* @param {function} callback Function to call with the content of url
* @internal
*/
function load(url, successCallback, errorCallback) {
var xhr;
xhr = window.ActiveXObject ? new window.ActiveXObject('Microsoft.XMLHTTP')
: new XMLHttpRequest();
// async, however scripts will be executed in the order they are in the
// DOM to mirror normal script loading.
xhr.open('GET', url, true);
if ('overrideMimeType' in xhr) {
xhr.overrideMimeType('text/plain');
}
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if (xhr.status === 0 || xhr.status === 200) {
successCallback(xhr.responseText);
} else {
errorCallback();
throw new Error('Could not load ' + url);
}
}
};
return xhr.send(null);
}
/**
* Loop over provided script tags and get the content, via innerHTML if an
* inline script, or by using XHR. Transforms are applied if needed. The scripts
* are executed in the order they are found on the page.
*
* @param {array} scripts The <script> elements to load and run.
* @internal
*/
function loadScripts(scripts) {
var result = [];
var count = scripts.length;
function check() {
var script, i;
for (i = 0; i < count; i++) {
script = result[i];
if (script.loaded && !script.executed) {
script.executed = true;
run(script.content, script.url, script.options);
} else if (!script.loaded && !script.error && !script.async) {
break;
}
}
}
scripts.forEach(function(script, i) {
var options = {
sourceMap: true
};
if (/;harmony=true(;|$)/.test(script.type)) {
options.harmony = true;
}
if (/;stripTypes=true(;|$)/.test(script.type)) {
options.stripTypes = true;
}
// script.async is always true for non-javascript script tags
var async = script.hasAttribute('async');
if (script.src) {
result[i] = {
async: async,
error: false,
executed: false,
content: null,
loaded: false,
url: script.src,
options: options
};
load(script.src, function(content) {
result[i].loaded = true;
result[i].content = content;
check();
}, function() {
result[i].error = true;
check();
});
} else {
result[i] = {
async: async,
error: false,
executed: false,
content: script.innerHTML,
loaded: true,
url: null,
options: options
};
}
});
check();
}
/**
* Find and run all script tags with type="text/jsx".
*
* @internal
*/
function runScripts() {
var scripts = document.getElementsByTagName('script');
// Array.prototype.slice cannot be used on NodeList on IE8
var jsxScripts = [];
for (var i = 0; i < scripts.length; i++) {
if (/^text\/jsx(;|$)/.test(scripts.item(i).type)) {
jsxScripts.push(scripts.item(i));
}
}
if (jsxScripts.length < 1) {
return;
}
console.warn(
'You are using the in-browser JSX transformer. Be sure to precompile ' +
'your JSX for production - ' +
'http://facebook.github.io/react/docs/tooling-integration.html#jsx'
);
loadScripts(jsxScripts);
}
// Listen for load event if we're in a browser and then kick off finding and
// running of scripts.
if (typeof window !== 'undefined' && window !== null) {
headEl = document.getElementsByTagName('head')[0];
dummyAnchor = document.createElement('a');
if (window.addEventListener) {
window.addEventListener('DOMContentLoaded', runScripts, false);
} else {
window.attachEvent('onload', runScripts);
}
}
module.exports = {
transform: transformReact,
exec: exec
};