diff --git a/README.md b/README.md index aef24655..ee5e3b2d 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,8 @@ playground('.selector', options) - `onOpenConsole` — Is called after the console's opened. +- `onOutputUpdate` - Is called after the output is added to DOM. + - `getJsCode(code)` — Is called after compilation Kotlin to JS. Use for target platform `js`. _code_ — converted JS code from Kotlin. diff --git a/package.json b/package.json index b3d3714d..dd9d6e7e 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "flatten": "^1.0.2", "github-markdown-css": "^3.0.1", "html-webpack-plugin": "^3.2.0", - "is-empty-object": "^1.1.1", + "jsonpipe": "2.2.0", "markdown-it": "^8.4.2", "markdown-it-highlightjs": "^3.0.0", "monkberry": "4.0.8", diff --git a/src/config.js b/src/config.js index 67ee07c5..0014199f 100644 --- a/src/config.js +++ b/src/config.js @@ -11,9 +11,16 @@ export const RUNTIME_CONFIG = {...getConfigFromElement(currentScript)}; */ export const API_URLS = { server: RUNTIME_CONFIG.server || __WEBDEMO_URL__, + asyncServer: RUNTIME_CONFIG.asyncServer || __ASYNC_SERVER_URL__, get COMPILE() { return `${this.server}/kotlinServer?type=run&runConf=`; }, + get COMPILE_ASYNC() { + if (this.asyncServer) { + return `${this.asyncServer}/kotlinServer?type=run&runConf=` + } + return null; + }, get HIGHLIGHT() { return `${this.server}/kotlinServer?type=highlight&runConf=`; }, diff --git a/src/executable-code/executable-fragment.js b/src/executable-code/executable-fragment.js index b52e71ab..a8c9948a 100644 --- a/src/executable-code/executable-fragment.js +++ b/src/executable-code/executable-fragment.js @@ -1,9 +1,9 @@ -import merge from 'deepmerge'; import CodeMirror from 'codemirror'; import Monkberry from 'monkberry'; import directives from 'monkberry-directives'; import 'monkberry-events'; import ExecutableCodeTemplate from './executable-fragment.monk'; +import Exception from './exception'; import WebDemoApi from '../webdemo-api'; import TargetPlatform from "../target-platform"; import JsExecutor from "../js-executor" @@ -21,6 +21,9 @@ const KEY_CODES = { F9: 120 }; const DEBOUNCE_TIME = 500; +const CODE_OUTPUT_CLASS_NAME = "code-output" +const STANDARD_OUTPUT_CLASS_NAME = "standard-output" +const ERROR_OUTPUT_CLASS_NAME = "error-output" const SELECTORS = { JS_CODE_OUTPUT_EXECUTOR: ".js-code-output-executor", @@ -51,9 +54,12 @@ export default class ExecutableFragment extends ExecutableCodeTemplate { code: '', foldButtonHover: false, folded: true, + exception: null, output: null, + errors: [] }; instance.codemirror = new CodeMirror(); + instance.element = element instance.on('click', SELECTORS.FOLD_BUTTON, () => { instance.update({folded: !instance.state.folded}); @@ -123,16 +129,20 @@ export default class ExecutableFragment extends ExecutableCodeTemplate { } } - this.state = merge.all([this.state, state, { - isShouldBeFolded: this.isShouldBeFolded && state.isFoldedButton - }]); - + if (state.output === null) { + this.removeAllOutputNodes() + } + this.applyStateUpdate(state) super.update(this.state); + this.renderNewOutputNodes(state) + if (!this.initialized) { this.initializeCodeMirror(state); this.initialized = true; } else { - this.showDiagnostics(state.errors); + if (state.errors !== undefined) { // rerender errors if the array was explicitly changed + this.showDiagnostics(this.state.errors); + } if (state.folded === undefined) { return } @@ -181,6 +191,81 @@ export default class ExecutableFragment extends ExecutableCodeTemplate { } } + removeAllOutputNodes() { + const outputNode = this.element.getElementsByClassName(CODE_OUTPUT_CLASS_NAME).item(0) + if (outputNode) { + outputNode.innerHTML = "" + } + } + + applyStateUpdate(stateUpdate) { + const filteredStateUpdate = Object.keys(stateUpdate) + .reduce((result, key) => { + if (stateUpdate[key] !== undefined) { + result[key] = stateUpdate[key] + } + return result + }, {}) + + if (filteredStateUpdate.errors === null) { + filteredStateUpdate.errors = [] + } else if (filteredStateUpdate.errors !== undefined) { + this.state.errors.push(...filteredStateUpdate.errors) + filteredStateUpdate.errors = this.state.errors + } + + Object.assign(this.state, filteredStateUpdate, { + isShouldBeFolded: this.isShouldBeFolded && filteredStateUpdate.isFoldedButton + }) + } + + appendOneNodeToDOM(parent, newNode) { // used for streaming + const isMergeable = newNode.className.startsWith(STANDARD_OUTPUT_CLASS_NAME) || + newNode.className.startsWith(ERROR_OUTPUT_CLASS_NAME) + + if (isMergeable && parent.lastChild && parent.lastChild.className === newNode.className) { + parent.lastChild.textContent += newNode.textContent + } else { + parent.appendChild(newNode) + } + } + + appendMultipleNodesToDOM(parent, newNodes) { // used for synchronous batch output update + const documentFragment = document.createDocumentFragment() + while (newNodes.item(0)) { + documentFragment.append(newNodes.item(0)) + } + parent.appendChild(documentFragment) + } + + renderNewOutputNodes(stateUpdate) { + if (stateUpdate.output) { + const parent = this.element.getElementsByClassName(CODE_OUTPUT_CLASS_NAME).item(0) + const template = document.createElement("template"); + template.innerHTML = stateUpdate.output.trim(); + if (template.content.childElementCount !== 1) { // synchronous mode + this.appendMultipleNodesToDOM(parent, template.content.childNodes) + } else { // streaming + this.appendOneNodeToDOM(parent, template.content.firstChild) + } + } + + if (stateUpdate.exception) { + const outputNode = this.element.getElementsByClassName(CODE_OUTPUT_CLASS_NAME).item(0) + const exceptionView = Monkberry.render(Exception, outputNode, { + 'directives': directives + }) + exceptionView.update({ + ...stateUpdate.exception, + onExceptionClick: this.onExceptionClick.bind(this) + }) + } + + if ((stateUpdate.output || stateUpdate.exception) && this.state.onOutputUpdate) { + this.state.onOutputUpdate() + } + } + markPlaceHolders() { let taskRanges = this.getTaskRanges(); this.codemirror.setValue(this.codemirror.getValue() @@ -226,11 +311,11 @@ export default class ExecutableFragment extends ExecutableCodeTemplate { } onConsoleCloseButtonEnter() { - const {jsLibs, onCloseConsole, targetPlatform } = this.state; + const {jsLibs, onCloseConsole, targetPlatform} = this.state; // creates a new iframe and removes the old one, thereby stops execution of any running script if (targetPlatform === TargetPlatform.CANVAS || targetPlatform === TargetPlatform.JS) this.jsExecutor.reloadIframeScripts(jsLibs, this.getNodeForMountIframe()); - this.update({output: "", openConsole: false, exception: null}); + this.update({output: null, openConsole: false, exception: null}); if (onCloseConsole) onCloseConsole(); } @@ -248,6 +333,9 @@ export default class ExecutableFragment extends ExecutableCodeTemplate { return } this.update({ + errors: null, + output: null, + exception: null, waitingForOutput: true, openConsole: false }); @@ -261,18 +349,26 @@ export default class ExecutableFragment extends ExecutableCodeTemplate { theme, hiddenDependencies, onTestPassed, - onTestFailed).then( + onTestFailed, state => { - state.waitingForOutput = false; - if (state.output || state.exception) { + if (state.waitingForOutput) { + this.update(state) + return + } + // no more chunks will be received + + const hasOutput = state.output || this.state.output + const hasException = state.exception || this.state.exception + const hasErrors = this.state.errors.length > 0 || (state.errors && state.errors.length > 0) + + if (hasOutput || hasException) { state.openConsole = true; - } else { - if (onCloseConsole) onCloseConsole(); + } else if (onCloseConsole) { + onCloseConsole() } - if ((state.errors.length > 0 || state.exception) && onError) onError(); + if ((hasErrors || hasException) && onError) onError(); this.update(state); - }, - () => this.update({waitingForOutput: false}) + } ) } else { this.jsExecutor.reloadIframeScripts(jsLibs, this.getNodeForMountIframe()); @@ -357,7 +453,7 @@ export default class ExecutableFragment extends ExecutableCodeTemplate { return; } diagnostics.forEach(diagnostic => { - const interval = diagnostic.interval; + const interval = Object.assign({}, diagnostic.interval); interval.start = this.recalculatePosition(interval.start); interval.end = this.recalculatePosition(interval.end); diff --git a/src/executable-code/executable-fragment.monk b/src/executable-code/executable-fragment.monk index e23c71a4..39ae8aff 100644 --- a/src/executable-code/executable-fragment.monk +++ b/src/executable-code/executable-fragment.monk @@ -1,5 +1,3 @@ -{% import Exception from './exception' %} -
{% if (!highlightOnly) %} @@ -18,26 +16,15 @@ {% if (openConsole) %}
{% endif %} + {% if output || exception %} +
+
+
+ {% endif %} {% if (waitingForOutput) %}
- {% else %} - {% if (output && output != "") || exception %} -
-
- {% unsafe output %} - - {% if exception %} - - {% endif %} -
-
- {% endif %} {% endif %}
diff --git a/src/js-executor/index.js b/src/js-executor/index.js index 06638f1a..1f78285c 100644 --- a/src/js-executor/index.js +++ b/src/js-executor/index.js @@ -2,7 +2,7 @@ import './index.scss' import {API_URLS} from "../config"; import TargetPlatform from "../target-platform"; import {showJsException} from "../view/output-view"; -import {processingHtmlBrackets} from "../utils"; +import {escapeBrackets} from "../utils"; const INIT_SCRIPT = "if(kotlin.BufferedOutput!==undefined){kotlin.out = new kotlin.BufferedOutput()}" + "else{kotlin.kotlin.io.output = new kotlin.kotlin.io.BufferedOutput()}"; @@ -37,7 +37,7 @@ export default class JsExecutor { if (loadedScripts === jsLibs.size + 2) { try { const output = this.iframe.contentWindow.eval(jsCode); - return output ? `${processingHtmlBrackets(output)}` : ""; + return output ? `${escapeBrackets(output)}` : ""; } catch (e) { if (onError) onError(); let exceptionOutput = showJsException(e); diff --git a/src/utils.js b/src/utils.js index d35fbe4f..52d43746 100644 --- a/src/utils.js +++ b/src/utils.js @@ -102,7 +102,7 @@ export function insertAfter(newNode, referenceNode) { * @param string * @returns {*} */ -export function processingHtmlBrackets(string) { +export function escapeBrackets(string) { const tagsToReplace = { "<": "<", ">": ">" @@ -134,7 +134,7 @@ export function unEscapeString(string) { } /** - * convert all `<` and `>` to `<` and `>` + * convert all `&lt;` and `&gt;` to `<` and `>` * @param string * @returns {*} */ diff --git a/src/view/output-view.js b/src/view/output-view.js index c3b85959..ca070e33 100644 --- a/src/view/output-view.js +++ b/src/view/output-view.js @@ -1,12 +1,11 @@ -import {arrayFrom, convertToHtmlTag, processingHtmlBrackets} from "../utils"; -import isEmptyObject from "is-empty-object" +import {convertToHtmlTag, escapeBrackets} from "../utils"; import escapeHtml from "escape-html" const ACCESS_CONTROL_EXCEPTION = "java.security.AccessControlException"; const SECURITY_MESSAGE = "Access control exception due to security reasons in web playground"; const UNHANDLED_JS_EXCEPTION = "Unhandled JavaScript exception"; -const NO_TEST_FOUND = "No tests methods are found"; +const NO_TEST_FOUND = "No test methods were found"; const ANGLE_BRACKETS_LEFT_HTML = "<"; const ANGLE_BRACKETS_RIGHT_HTML = ">"; @@ -16,14 +15,31 @@ const TEST_STATUS = { PASSED : { value: "OK", text: "Passed" } }; -const BUG_FLAG = `${ANGLE_BRACKETS_LEFT_HTML}errStream${ANGLE_BRACKETS_RIGHT_HTML}BUG${ANGLE_BRACKETS_LEFT_HTML}/errStream${ANGLE_BRACKETS_RIGHT_HTML}`; -const BUG_REPORT_MESSAGE = `${ANGLE_BRACKETS_LEFT_HTML}errStream${ANGLE_BRACKETS_RIGHT_HTML}Hey! It seems you just found a bug! \uD83D\uDC1E\n` + - `Please click here to submit it ` + +const BUG_FLAG = 'BUG' +const BUG_REPORT_MESSAGE = 'Hey! It seems you just found a bug! \uD83D\uDC1E\n' + + `Please go here -> http://kotl.in/issue to submit it ` + `to the issue tracker and one day we fix it, hopefully \uD83D\uDE09\n` + - `✅ Don't forget to attach code to the issue${ANGLE_BRACKETS_LEFT_HTML}/errStream${ANGLE_BRACKETS_RIGHT_HTML}\n`; + `✅ Don't forget to attach code to the issue\n`; -export function processJVMOutput(output, theme) { - let processedOutput = processingHtmlBrackets(output); // don't need to escape `&` +export function processJVMStdout(output, theme) { + const processedOutput = escapeHtml(output); + return `${processedOutput}` +} + +export function createErrorText(output, theme) { + const processedOutput = escapeHtml(output); + return `${processedOutput}` +} + +export function processJVMStderr(output, theme) { + if (output === BUG_FLAG) { + output = BUG_REPORT_MESSAGE + } + return createErrorText(output, theme) +} + +export function processBatchJVMOutput(output, theme) { + let processedOutput = escapeBrackets(output); // don't need to escape `&` return processedOutput .split(BUG_FLAG).join(BUG_REPORT_MESSAGE) .split(`${ANGLE_BRACKETS_LEFT_HTML}outStream${ANGLE_BRACKETS_RIGHT_HTML}`).join(``) @@ -32,37 +48,49 @@ export function processJVMOutput(output, theme) { .split(`${ANGLE_BRACKETS_LEFT_HTML}/errStream${ANGLE_BRACKETS_RIGHT_HTML}`).join(""); } -export function processJUnitResults(data, onTestPassed, onTestFailed) { - let result = ""; - let totalTime = 0; - let passed = true; - if (isEmptyObject(data)) return NO_TEST_FOUND; - for (let testClass in data) { - let listOfResults = arrayFrom(data[testClass]); - result += listOfResults.reduce((previousTest, currentTest) => { - totalTime = totalTime + (currentTest.executionTime / 1000); - if (currentTest.status === TEST_STATUS.ERROR.value || currentTest.status === TEST_STATUS.FAIL.value) passed = false; - switch (currentTest.status) { - case TEST_STATUS.FAIL.value: - return previousTest + buildOutputTestLine(TEST_STATUS.FAIL.text, currentTest.methodName, currentTest.comparisonFailure.message); - case TEST_STATUS.ERROR.value: - return previousTest + buildOutputTestLine(TEST_STATUS.ERROR.text, currentTest.methodName, currentTest.exception.message); - case TEST_STATUS.PASSED.value: - return previousTest + buildOutputTestLine(TEST_STATUS.PASSED.text, currentTest.methodName, ""); - } - }, ""); +export function processJUnitTotalResults(testResults, theme, onTestPassed, onTestFailed) { + if (testResults.testsRun === 0) { + return createErrorText(NO_TEST_FOUND, theme) } - if (passed && onTestPassed) onTestPassed(); - if (!passed && onTestFailed) onTestFailed(); - let testTime = `
Total test time: ${totalTime}s
`; - return testTime + result; + if (testResults.success) { + if (onTestPassed) onTestPassed() + } else { + if (onTestFailed) onTestFailed() + } + return `
Total test time: ${testResults.totalTime}s
` +} + +export function processJUnitTestResult(testRunInfo, testResults, needToEscape) { + let output = ""; + testResults.testsRun++ + testResults.totalTime += testRunInfo.executionTime / 1000 + switch (testRunInfo.status) { + case TEST_STATUS.FAIL.value: + testResults.success = false; + output = buildOutputTestLine(TEST_STATUS.FAIL.text, testRunInfo.methodName, testRunInfo.comparisonFailure.message, needToEscape); + break; + case TEST_STATUS.ERROR.value: + testResults.success = false; + output = buildOutputTestLine(TEST_STATUS.ERROR.text, testRunInfo.methodName, testRunInfo.exception.message, needToEscape); + break; + case TEST_STATUS.PASSED.value: + output = buildOutputTestLine(TEST_STATUS.PASSED.text, testRunInfo.methodName, "", needToEscape); + } + return output; } -function buildOutputTestLine(status, method, message) { +function buildOutputTestLine(status, method, message, needToEscape) { + let escapedMessage; + if (needToEscape) { + escapedMessage = escapeHtml(message) + } else { + escapedMessage = convertToHtmlTag(message) // synchronous mode escapes some text on the server side + } + return `
-
${status}: ${method}${message ? ': ' + convertToHtmlTag(message) : ''}
+
${status}: ${method}${message ? ': ' + escapedMessage : ''}
`; } diff --git a/src/webdemo-api.js b/src/webdemo-api.js index 7eb5a03d..38417716 100644 --- a/src/webdemo-api.js +++ b/src/webdemo-api.js @@ -3,13 +3,19 @@ import URLSearchParams from 'url-search-params'; import TargetPlatform from "./target-platform"; import {API_URLS} from "./config"; import flatten from 'flatten' +import jsonpipe from 'jsonpipe' import { findSecurityException, getExceptionCauses, processErrors, - processJUnitResults, - processJVMOutput + processJUnitTestResult, + processJUnitTotalResults, + processJVMStdout, + processJVMStderr, + createErrorText, + processBatchJVMOutput } from "./view/output-view"; +import {arrayFrom} from "./utils"; /** * @typedef {Object} KotlinVersion @@ -66,47 +72,24 @@ export default class WebDemoApi { } /** - * Request on execute Kotlin code. + * Request to execute Kotlin code. * - * @param code - string - * @param compilerVersion - string kotlin compiler - * @param platform - TargetPlatform - * @param args - command line arguments - * @param theme - theme of editor - * @param onTestPassed - function will call after test's passed - * @param onTestFailed - function will call after test's failed + * @param code - string + * @param compilerVersion - string kotlin compiler + * @param platform - TargetPlatform + * @param args - command line arguments + * @param theme - editor theme + * @param onTestPassed - a function that will be called if all tests pass + * @param onTestFailed - a function that will be called if some tests fail (but after all tests are executed) * @param hiddenDependencies - read only additional files - * @returns {*|PromiseLike|Promise} + * @param callback - a callback for output chunks */ - static executeKotlinCode(code, compilerVersion, platform, args, theme, hiddenDependencies, onTestPassed, onTestFailed) { - return executeCode(API_URLS.COMPILE, code, compilerVersion, platform, args, hiddenDependencies).then(function (data) { - let output = ""; - let errorsAndWarnings = flatten(Object.values(data.errors)); - let errors = errorsAndWarnings.filter(error => error.severity === "ERROR"); - if (errors.length > 0) { - output = processErrors(errors, theme); - } else { - switch (platform) { - case TargetPlatform.JAVA: - if (data.text) output = processJVMOutput(data.text, theme); - break; - case TargetPlatform.JUNIT: - data.testResults ? output = processJUnitResults(data.testResults, onTestPassed, onTestFailed) : output = processJVMOutput(data.text, theme); - break; - } - } - let exceptions = null; - if (data.exception != null) { - exceptions = findSecurityException(data.exception); - exceptions.causes = getExceptionCauses(exceptions); - exceptions.cause = undefined; - } - return { - errors: errorsAndWarnings, - output: output, - exception: exceptions - } - }) + static executeKotlinCode(code, compilerVersion, platform, args, theme, hiddenDependencies, onTestPassed, onTestFailed, callback) { + if (API_URLS.COMPILE_ASYNC) { + executeKotlinCodeStreaming(code, compilerVersion, platform, args, theme, hiddenDependencies, onTestPassed, onTestFailed, callback) + } else { + executeKotlinCodeSync(code, compilerVersion, platform, args, theme, hiddenDependencies, onTestPassed, onTestFailed, callback) + } } /** @@ -141,7 +124,113 @@ export default class WebDemoApi { } } -function executeCode(url, code, compilerVersion, targetPlatform, args, hiddenDependencies, options) { +function processJUnitResults(data, theme, onTestPassed, onTestFailed) { // use for synchronous output only + const testResults = { + testsRun: 0, + totalTime: 0, + success: true + } + let output = "" + for (let testClass in data) { + arrayFrom(data[testClass]).forEach(testResult => { + output += processJUnitTestResult(testResult, testResults, false) + }) + } + output += processJUnitTotalResults(testResults, theme, onTestPassed, onTestFailed) + return output +} + +function executeKotlinCodeSync(code, compilerVersion, platform, args, theme, hiddenDependencies, onTestPassed, onTestFailed, callback) { + executeCode(API_URLS.COMPILE, code, compilerVersion, platform, args, hiddenDependencies).then(function (data) { + let output = ""; + let errorsAndWarnings = flatten(Object.values(data.errors)); + let errors = errorsAndWarnings.filter(error => error.severity === "ERROR"); + if (errors.length > 0) { + output = processErrors(errors, theme); + } else { + switch (platform) { + case TargetPlatform.JAVA: + if (data.text) output = processBatchJVMOutput(data.text, theme); + break; + case TargetPlatform.JUNIT: + if (data.testResults || !data.text) { + output = processJUnitResults(data.testResults, theme, onTestPassed, onTestFailed) + } else { + output = processBatchJVMOutput(data.text, theme); + } + break; + } + } + let exceptions = null; + if (data.exception != null) { + exceptions = findSecurityException(data.exception); + exceptions.causes = getExceptionCauses(exceptions); + exceptions.cause = undefined; + } + callback({ + waitingForOutput: false, + errors: errorsAndWarnings, + output: output, + exception: exceptions + }) + }) +} + + +function executeKotlinCodeStreaming(code, compilerVersion, platform, args, theme, hiddenDependencies, onTestPassed, onTestFailed, callback) { + const testResults = { + testsRun: 0, + totalTime: 0, + success: true + } + executeCodeStreaming(API_URLS.COMPILE, code, compilerVersion, platform, args, hiddenDependencies, result => { + let output; + if (result.errorText) { + output = createErrorText(result.errorText, theme) + } else if (platform === TargetPlatform.JUNIT) { + output = processJUnitTotalResults(testResults, theme, onTestPassed, onTestFailed) + } + callback({ + waitingForOutput: false, + output: output + }) + }, result => { + const data = result.data + + let errorsAndWarnings + let errors = [] + if (data.hasOwnProperty('errors')) { + errorsAndWarnings = flatten(Object.values(data.errors)); + errors = errorsAndWarnings.filter(error => error.severity === "ERROR"); + } + + let output; + if (errors.length > 0) { + output = processErrors(errors, theme); + } else if (data.hasOwnProperty('errStream')) { + output = processJVMStderr(data.errStream, theme) + } else if (data.hasOwnProperty('outStream')) { + output = processJVMStdout(data.outStream) + } else if (data.hasOwnProperty('testResult') && platform === TargetPlatform.JUNIT) { + output = processJUnitTestResult(data.testResult, testResults, true) + } + + let exception; + if (data.hasOwnProperty('exception')) { + exception = findSecurityException(data.exception); + exception.causes = getExceptionCauses(exception); + exception.cause = undefined; + } + callback({ + waitingForOutput: true, + errors: errorsAndWarnings, + output: output, + exception: exception + }) + }) +} + +function createBodyForCodeExecution(code, compilerVersion, targetPlatform, args, hiddenDependencies, options) { const files = [buildFileObject(code, DEFAULT_FILE_NAME)] .concat(hiddenDependencies.map((file, index) => buildFileObject(file, `hiddenDependency${index}.kt`))); const projectJson = JSON.stringify({ @@ -164,6 +253,12 @@ function executeCode(url, code, compilerVersion, targetPlatform, args, hiddenDep body.set(option, options[option]) } } + return body +} + +function executeCode(url, code, compilerVersion, targetPlatform, args, hiddenDependencies, options) { + const body = createBodyForCodeExecution(code, compilerVersion, targetPlatform, args, hiddenDependencies, options) + return fetch(url + targetPlatform.id, { method: 'POST', body: body.toString(), @@ -173,6 +268,33 @@ function executeCode(url, code, compilerVersion, targetPlatform, args, hiddenDep }).then(response => response.json()) } +function executeCodeStreaming(url, code, compilerVersion, targetPlatform, args, hiddenDependencies, onDone, onData) { + const body = createBodyForCodeExecution(code, compilerVersion, targetPlatform, args, hiddenDependencies) + + let xmlHttpRequest; + xmlHttpRequest = jsonpipe.flow(url + targetPlatform.id, { + success: data => { + onData({'data': data}) + }, + complete: () => { + if (xmlHttpRequest && xmlHttpRequest.status === 0) { + onDone({errorText: "REQUEST CANCELLED"}) + } else if (xmlHttpRequest && (xmlHttpRequest.status < 200 || xmlHttpRequest.status > 299)) { + onDone({errorText: `SERVER RETURNED CODE ${xmlHttpRequest.status}`}) + } else { + onDone({}) + } + }, + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', + 'Enable-Streaming': 'true' + }, + data: body.toString(), + withCredentials: false + }); +} + /** * * Build file object. diff --git a/webpack.config.js b/webpack.config.js index 19e8cd16..bfbd04e8 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -9,6 +9,7 @@ module.exports = (params = {}) => { const isServer = process.argv[1].includes('webpack-dev-server'); const libraryName = 'KotlinPlayground'; const webDemoUrl = params.webDemoUrl || 'https://try.kotlinlang.org'; + const asyncServerUrl = params.asyncServerUrl; const examplesPath = isServer ? '' : 'examples/'; const config = { @@ -75,6 +76,7 @@ module.exports = (params = {}) => { new webpack.DefinePlugin({ __WEBDEMO_URL__: JSON.stringify(webDemoUrl), + __ASYNC_SERVER_URL__: JSON.stringify(asyncServerUrl), __IS_PRODUCTION__: isProduction, __LIBRARY_NAME__: JSON.stringify(libraryName), 'process.env': { diff --git a/yarn.lock b/yarn.lock index 2f58577e..7a469274 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3430,10 +3430,6 @@ is-directory@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" -is-empty-object@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/is-empty-object/-/is-empty-object-1.1.1.tgz#86d5d4d5c5229cea31ec2772f528bf5efc519f23" - is-extendable@^0.1.0, is-extendable@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" @@ -3683,6 +3679,11 @@ jsonify@~0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" +jsonpipe@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/jsonpipe/-/jsonpipe-2.2.0.tgz#70a1344b9f9111d8ff4a7c23a95f22c90d87d214" + integrity sha512-K+vnwaebP0Zu61Q6Dmdw6QLrHQVTVun7PbiUSqQmdcyOx00KmHCJnY+x1oWxAtmOrGTOkW5TcpGcJ5NV8ugCFQ== + jsprim@^1.2.2: version "1.4.1" resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"