@jetbrains/websandbox
Version:
A sandbox library for runnung javascript inside HTML5 sandboxed iframe
388 lines (381 loc) • 13.8 kB
JavaScript
(function webpackUniversalModuleDefinition(root, factory) {
if(typeof exports === 'object' && typeof module === 'object')
module.exports = factory();
else if(typeof define === 'function' && define.amd)
define([], factory);
else if(typeof exports === 'object')
exports["Websandbox"] = factory();
else
root["Websandbox"] = factory();
})(self, () => {
return /******/ (() => { // webpackBootstrap
/******/ "use strict";
/******/ // The require scope
/******/ var __webpack_require__ = {};
/******/
/************************************************************************/
/******/ /* webpack/runtime/define property getters */
/******/ (() => {
/******/ // define getter functions for harmony exports
/******/ __webpack_require__.d = (exports, definition) => {
/******/ for(var key in definition) {
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
/******/ }
/******/ }
/******/ };
/******/ })();
/******/
/******/ /* webpack/runtime/hasOwnProperty shorthand */
/******/ (() => {
/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
/******/ })();
/******/
/******/ /* webpack/runtime/make namespace object */
/******/ (() => {
/******/ // define __esModule on exports
/******/ __webpack_require__.r = (exports) => {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/ })();
/******/
/************************************************************************/
var __webpack_exports__ = {};
// ESM COMPAT FLAG
__webpack_require__.r(__webpack_exports__);
// EXPORTS
__webpack_require__.d(__webpack_exports__, {
"default": () => (/* binding */ lib_frame)
});
;// ./lib/object-path.ts
const PATH_REG = /([.[\]:;'"\s])/;
function escapePathPart(pathPart) {
if (!PATH_REG.test(pathPart)) {
return pathPart;
}
const escaped = pathPart.replace(new RegExp(PATH_REG.source, 'g'), '\\$1');
return `["${escaped}"]`;
}
function unescapePathPart(pathPart) {
return pathPart.replace(/^\["/, '').replace(/"]$/, '').replace(/\\/, '');
}
function splitPath(path) {
const result = [];
let lastEnd = 0;
for (let i = 0; i < path.length; i++) {
const char = path[i];
if (PATH_REG.test(char) && path[i - 1] !== '\\') {
result.push(path.substring(lastEnd, i));
lastEnd = i + 1;
}
}
result.push(path.substring(lastEnd, path.length));
return result.filter(pathPart => !!pathPart).map(pathPart => pathPart.replace(/\\/g, ''));
}
/**
* Extracts object property value by given path. Supports nested and array values: 'foo[0].bar'
* @param {Object} object source object
* @param {string} path path to value
* @return {any | null} value by given path
* */
function propertyByPath(object, path) {
return splitPath(path).reduce((acc, pathPart) => {
if (acc) {
return acc[pathPart];
}
return null;
}, object);
}
;// ./lib/connection.ts
const TYPE_MESSAGE = 'message';
const TYPE_RESPONSE = 'response';
const TYPE_SET_INTERFACE = 'set-interface';
const TYPE_SERVICE_MESSAGE = 'service-message';
// @ts-expect-error this is IE11 obsolete check. It is not typed
const isIE11 = !!window.MSInputMethodContext && !!document.documentMode;
const defaultOptions = {
//Will not affect IE11 because there sandboxed iframe has not 'null' origin
//but base URL of iframe's src
allowedSenderOrigin: undefined
};
class Connection {
constructor(postMessage, registerOnMessageListener, options = {}) {
this.remote = {};
this.serviceMethods = {};
this.localApi = {};
this.callbacks = {};
this._resolveRemoteMethodsPromise = null;
this.options = Object.assign(Object.assign({}, defaultOptions), options);
//Random number between 0 and 100000
this.incrementalID = Math.floor(Math.random() * 100000);
this.postMessage = postMessage;
this.remoteMethodsWaitPromise = new Promise(resolve => {
this._resolveRemoteMethodsPromise = resolve;
});
registerOnMessageListener((e) => this.onMessageListener(e));
}
/**
* Listens to remote messages. Calls local method if it is called outside or call stored callback if it is response.
* @param e - onMessage event
*/
onMessageListener(e) {
const data = e.data;
const { allowedSenderOrigin } = this.options;
if (allowedSenderOrigin && e.origin !== allowedSenderOrigin && !isIE11) {
return;
}
if (data.type === TYPE_RESPONSE) {
this.popCallback(data.callId, data.success, data.result);
}
else if (data.type === TYPE_MESSAGE) {
this
.callLocalApi(data.methodName, data.arguments)
.then(res => this.responseOtherSide(data.callId, res))
.catch(err => this.responseOtherSide(data.callId, err, false));
}
else if (data.type === TYPE_SET_INTERFACE) {
this.setInterface(data.apiMethods);
this.responseOtherSide(data.callId);
}
else if (data.type === TYPE_SERVICE_MESSAGE) {
this
.callLocalServiceMethod(data.methodName, data.arguments)
.then(res => this.responseOtherSide(data.callId, res))
.catch(err => this.responseOtherSide(data.callId, err, false));
}
}
postMessageToOtherSide(dataToPost) {
this.postMessage(dataToPost, '*');
}
/**
* Sets remote interface methods
* @param remote - hash with keys of remote API methods. Values is ignored
*/
setInterface(remoteMethods) {
var _a;
this.remote = {};
remoteMethods.forEach((key) => {
// If key is nested, we need to create nested structure
const parts = splitPath(key);
let current = this.remote;
for (let i = 0; i < parts.length - 1; i++) {
const part = parts[i];
if (!current[part] || typeof current[part] !== 'object') {
current[part] = {};
}
current = current[part];
}
current[parts[parts.length - 1]] = this.createMethodWrapper(key);
});
(_a = this._resolveRemoteMethodsPromise) === null || _a === void 0 ? void 0 : _a.call(this);
}
getMethodsFromInterface(api) {
return Object.keys(api).reduce((acc, key) => {
if (typeof api[key] === 'object') {
acc.push(...this.getMethodsFromInterface(api[key]).map(subKey => `${key}.${subKey}`));
}
else {
acc.push(key);
}
return acc;
}, []);
}
setLocalApi(api) {
return new Promise((resolve, reject) => {
const id = this.registerCallback(resolve, reject);
this.postMessageToOtherSide({
callId: id,
apiMethods: this.getMethodsFromInterface(api),
type: TYPE_SET_INTERFACE
});
}).then(() => this.localApi = api);
}
setServiceMethods(api) {
this.serviceMethods = api;
}
/**
* Calls local method
* @param methodName
* @param args
* @returns {Promise.<*>|string}
*/
callLocalApi(methodName, args) {
const method = propertyByPath(this.localApi, methodName);
if (!method) {
throw new Error(`Local method "${methodName}" is not registered`);
}
return Promise.resolve(method.call(this, ...args));
}
/**
* Calls local method registered as "service method"
* @param methodName
* @param args
* @returns {Promise.<*>}
*/
callLocalServiceMethod(methodName, args) {
const method = propertyByPath(this.serviceMethods, methodName);
if (!method) {
throw new Error(`Service method ${methodName} is not registered`);
}
return Promise.resolve(method.call(this, ...args));
}
/**
* Wraps remote method with callback storing code
* @param methodName - method to wrap
* @returns {Function} - function to call as remote API interface
*/
createMethodWrapper(methodName) {
return (...args) => {
return this.callRemoteMethod(methodName, ...args);
};
}
/**
* Calls other side with arguments provided
* @param id
* @param methodName
* @param args
*/
callRemoteMethod(methodName, ...args) {
return new Promise((resolve, reject) => {
const id = this.registerCallback(resolve, reject);
this.postMessageToOtherSide({
callId: id,
methodName: methodName,
type: TYPE_MESSAGE,
arguments: args
});
});
}
/**
* Calls remote service method
* @param methodName
* @param args
* @returns {*}
*/
callRemoteServiceMethod(methodName, ...args) {
return new Promise((resolve, reject) => {
const id = this.registerCallback(resolve, reject);
this.postMessageToOtherSide({
callId: id,
methodName: methodName,
type: TYPE_SERVICE_MESSAGE,
arguments: args
});
});
}
/**
* Respond to remote call
* @param id - remote call ID
* @param result - result to pass to calling function
*/
responseOtherSide(id, result, success = true) {
if (result instanceof Error) {
// Error could be non-serializable, so we copy properties manually
result = [...Object.keys(result), 'message'].reduce((acc, it) => {
acc[it] = result[it];
return acc;
}, {});
}
const doPost = () => this.postMessage({
callId: id,
type: TYPE_RESPONSE,
success,
result
}, '*');
try {
doPost();
}
catch (err) {
console.error('Failed to post response, recovering...', err); // eslint-disable-line no-console
if (err instanceof DOMException) {
result = JSON.parse(JSON.stringify(result));
doPost();
}
}
}
/*
* Stores callbacks to call later when remote call will be answered
*/
registerCallback(successCallback, failureCallback) {
const id = (++this.incrementalID).toString();
this.callbacks[id] = { successCallback, failureCallback };
return id;
}
/**
* Calls and delete stored callback
* @param id - call id
* @param success - was call successful
* @param result - result of remote call
*/
popCallback(id, success, result) {
if (success) {
this.callbacks[id].successCallback(result);
}
else {
this.callbacks[id].failureCallback(result);
}
delete this.callbacks[id];
}
}
/* harmony default export */ const connection = (Connection);
;// ./lib/frame.ts
class Frame {
constructor() {
this.connection = new connection(window.parent.postMessage.bind(window.parent), listener => {
const sourceCheckListener = (event) => {
if (event.source !== window.parent) {
return;
}
return listener(event);
};
window.addEventListener('message', sourceCheckListener);
});
this.connection.setServiceMethods({
runCode: (code) => this.runCode(code),
importScript: (path) => this.importScript(path),
injectStyle: (style) => this.injectStyle(style),
importStyle: (path) => this.importStyle(path)
});
this.connection.callRemoteServiceMethod('iframeInitialized');
}
/**
* Creates script tag with passed code and attaches it. Runs synchronous
* @param code
*/
runCode(code) {
const scriptTag = document.createElement('script');
scriptTag.innerHTML = code;
document.getElementsByTagName('head')[0].appendChild(scriptTag);
}
importScript(scriptUrl) {
const scriptTag = document.createElement('script');
scriptTag.src = scriptUrl;
document.getElementsByTagName('head')[0].appendChild(scriptTag);
return new Promise(resolve => scriptTag.onload = () => resolve());
}
injectStyle(style) {
const styleTag = document.createElement('style');
styleTag.innerHTML = style;
document.getElementsByTagName('head')[0].appendChild(styleTag);
}
importStyle(styleUrl) {
const linkTag = document.createElement('link');
linkTag.rel = 'stylesheet';
linkTag.href = styleUrl;
document.getElementsByTagName('head')[0].appendChild(linkTag);
}
}
// @ts-expect-error we explicitly export library to global namespace because
const Websandbox = window.Websandbox || new Frame();
// @ts-expect-error we explicitly export library to global namespace because
// Webpack won't do it for us when this file is loaded via code-loader
window.Websandbox = Websandbox;
/* harmony default export */ const lib_frame = (Websandbox);
/******/ return __webpack_exports__;
/******/ })()
;
});
//# sourceMappingURL=frame.js.map