Skip to content

Commit

Permalink
Merge pull request #197 from JetBrains/fix-browser-platforms
Browse files Browse the repository at this point in the history
Add JS and Wasm for 2.0.x version
  • Loading branch information
zoobestik authored Dec 21, 2023
2 parents f1bc675 + 54a2ecb commit cfb7e5c
Show file tree
Hide file tree
Showing 13 changed files with 197 additions and 40 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"@babel/plugin-transform-runtime": "~7.22.4",
"@babel/preset-env": "~7.22.4",
"@babel/runtime-corejs2": "~7.22.2",
"@playwright/test": "^1.37.1",
"@playwright/test": "^1.40.1",
"@typescript-eslint/eslint-plugin": "^6.4.0",
"@typescript-eslint/parser": "^6.4.0",
"babel-loader": "^9.1.3",
Expand Down
2 changes: 1 addition & 1 deletion src/executable-code/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export default class ExecutableCode {
const args = targetNode.hasAttribute(ATTRIBUTES.ARGUMENTS) ? targetNode.getAttribute(ATTRIBUTES.ARGUMENTS) : "";
const hiddenDependencies = this.getHiddenDependencies(targetNode);
const outputHeight = targetNode.getAttribute(ATTRIBUTES.OUTPUT_HEIGHT) || null;
const targetPlatform = getTargetById(targetNode.getAttribute(ATTRIBUTES.PLATFORM));
const targetPlatform = getTargetById(targetNode.getAttribute(ATTRIBUTES.PLATFORM)) || TargetPlatforms.JAVA;
const targetNodeStyle = targetNode.getAttribute(ATTRIBUTES.STYLE);
const jsLibs = this.getJsLibraries(targetNode, targetPlatform);
const isFoldedButton = targetNode.getAttribute(ATTRIBUTES.FOLDED_BUTTON) !== "false";
Expand Down
10 changes: 3 additions & 7 deletions src/lib/crosslink.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { compressToBase64 } from 'lz-string';

import { isKeyOfObject } from '../utils/types';
import { TargetPlatforms, TargetPlatformsKeys } from '../utils/platforms';
import { getTargetById, TargetPlatformsKeys } from '../utils/platforms';

import {
escapeRegExp,
Expand Down Expand Up @@ -34,11 +33,8 @@ export function generateCrosslink(code: string, options?: LinkOptions) {

if (options && options.targetPlatform) {
const target =
options.targetPlatform && options.targetPlatform.toUpperCase();

if (!isKeyOfObject(target, TargetPlatforms))
throw new Error('Invalid target platform');

options.targetPlatform && getTargetById(options.targetPlatform);
if (!target) throw new Error('Invalid target platform');
opts.targetPlatform = options.targetPlatform;
}

Expand Down
4 changes: 2 additions & 2 deletions src/utils/platforms/TargetPlatform.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
export default class TargetPlatform {
private id: string;
private printableName: string;
id: string;
printableName: string;

constructor(id: string, printableName: string) {
this.id = id;
Expand Down
10 changes: 4 additions & 6 deletions src/utils/platforms/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import { isKeyOfObject } from '../types';

import TargetPlatform from './TargetPlatform';
import { TargetPlatforms } from './TargetPlatforms';
import { isKeyOfObject } from '../types';

export function getTargetById(id?: string | null) {
const key = id && id.toUpperCase();
return key && isKeyOfObject(key, TargetPlatforms)
? TargetPlatforms[key]
: TargetPlatforms.JAVA;
const key = id && id.toUpperCase().replace(/-/g, '_');

return isKeyOfObject(key, TargetPlatforms) ? TargetPlatforms[key] : null;
}

export function isJavaRelated(platform: TargetPlatform) {
Expand Down
13 changes: 6 additions & 7 deletions src/webdemo-api.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,27 +50,26 @@ export default class WebDemoApi {
* @returns {*|PromiseLike<T>|Promise<T>}
*/
static translateKotlinToJs(code, compilerVersion, platform, args, hiddenDependencies) {
const MINIMAL_MINOR_VERSION_IR = 5
const MINIMAL_MINOR_VERSION_WASM = 9
const minor = parseInt(compilerVersion.split(".")[1]);
const MINIMAL_VERSION_IR = '1.5.0';
const MINIMAL_VERSION_WASM = '1.9.0';

if (platform === TargetPlatforms.JS_IR && minor < MINIMAL_MINOR_VERSION_IR) {
if (platform === TargetPlatforms.JS_IR && compilerVersion < MINIMAL_VERSION_IR) {
return Promise.resolve({
output: "",
errors: [{
severity: "ERROR",
message: `JS IR compiler backend accessible only since 1.${MINIMAL_MINOR_VERSION_IR}.0 version`
message: `JS IR compiler backend accessible only since ${MINIMAL_VERSION_IR} version`
}],
jsCode: ""
})
}

if (platform === TargetPlatforms.WASM && minor < MINIMAL_MINOR_VERSION_WASM) {
if (platform === TargetPlatforms.WASM && compilerVersion < MINIMAL_VERSION_WASM) {
return Promise.resolve({
output: "",
errors: [{
severity: "ERROR",
message: `Wasm compiler backend accessible only since 1.${MINIMAL_MINOR_VERSION_WASM}.0 version`
message: `Wasm compiler backend accessible only since ${MINIMAL_VERSION_WASM} version`
}],
jsCode: ""
})
Expand Down
4 changes: 2 additions & 2 deletions tests/crosslink.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ test.describe('crosslink: library', () => {
checkLink(generateCrosslink('simple'), { code: 'simple' });

// Pass platforms with codeWithSample
checkLink(generateCrosslink('platform', { targetPlatform: 'JAVA' }), {
checkLink(generateCrosslink('platform', { targetPlatform: 'js-ir' }), {
code: 'platform',
targetPlatform: 'JAVA',
targetPlatform: 'js-ir',
});

// Invalid target
Expand Down
139 changes: 139 additions & 0 deletions tests/restrictions.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { expect, Page, test } from '@playwright/test';

import { readFileSync } from 'fs';
import { join } from 'path';

import { gotoHtmlWidget } from './utlis/server/playground';

import { RESULT_SELECTOR, WIDGET_SELECTOR } from './utlis/selectors';

import { prepareNetwork, printlnCode } from './utlis';
import { mockRunRequest, waitRunRequest } from './utlis/mocks/compiler';
import { runButton } from './utlis/interactions';
import { makeJSPrintCode } from './utlis/mocks/result';

const OUTPUTS = Object.freeze({
'js-ir': {
jsCode: makeJSPrintCode('Hello, world!'),
errors: { 'File.kt': [] },
exception: null,
text: '<outStream>Hello, world!\n</outStream>',
},
wasm: JSON.parse(
readFileSync(join(__dirname, 'utlis/mocks/wasm.json'), 'utf-8'),
),
});

const VERSIONS = [
{ version: '1.3.10' },
{ version: '1.9.20', latestStable: true },
{ version: '2.0.1' },
] as const;

test.describe('platform restrictions', () => {
test.beforeEach(async ({ page, baseURL }) => {
await prepareNetwork(page, baseURL, {
versions: (route) =>
route.fulfill({
body: JSON.stringify(VERSIONS),
}),
}); // offline mode
});

test('JS_IR for unsupported version', async ({ page }) => {
await shouldFailedRun(
page,
'js-ir',
'1.3.10',
'JS IR compiler backend accessible only since 1.5.0 version',
);
});

test('JS_IR for supported by minor version', async ({ page }) => {
await shouldSuccessRun(page, 'js-ir', '1.9.0');
});

test('JS_IR for supported by major version', async ({ page }) => {
await shouldSuccessRun(page, 'js-ir', '2.0.1');
});

test('WASM for unsupported version', async ({ page }) => {
await shouldFailedRun(
page,
'wasm',
'1.3.10',
'Wasm compiler backend accessible only since 1.9.0 version',
);
});

test('WASM for supported by minor version', async ({ page, browserName }) => {
test.skip(
browserName !== 'chromium',
"WASM doesn't supported in this browser",
);
await shouldSuccessRun(page, 'wasm', '1.9.0');
});

test('WASM for supported by major version', async ({ page, browserName }) => {
test.skip(
browserName !== 'chromium',
"WASM doesn't supported in this browser",
);
await shouldSuccessRun(page, 'wasm', '2.0.1');
});
});

async function shouldSuccessRun(
page: Page,
platform: keyof typeof OUTPUTS,
version: string,
) {
await gotoHtmlWidget(
page,
{ selector: 'code', version: version },
/* language=html */ `
<code data-target-platform='${platform}'>${printlnCode(
'Hello, world!',
)}</code>
`,
);

const resolveRun = await mockRunRequest(page);

const editor = page.locator(WIDGET_SELECTOR);

await Promise.all([waitRunRequest(page), runButton(editor)]);

resolveRun({
json: Object.freeze(OUTPUTS[platform]),
});

// playground loaded
await expect(editor.locator(RESULT_SELECTOR)).toBeVisible();
await expect(editor.locator(RESULT_SELECTOR)).toContainText('Hello, world!');
}

async function shouldFailedRun(
page: Page,
platform: string,
version: string,
text: string,
) {
await gotoHtmlWidget(
page,
{ selector: 'code', version: version },
/* language=html */ `
<code data-target-platform='${platform}'>${printlnCode(
'Hello, world!',
)}</code>
`,
);

const editor = page.locator(WIDGET_SELECTOR);
await runButton(editor);

await expect(editor.locator(RESULT_SELECTOR)).toBeVisible();
await expect(
editor.locator(RESULT_SELECTOR).locator('.test-fail'),
).toContainText(text);
}
5 changes: 4 additions & 1 deletion tests/utlis/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,12 @@ export async function refuseExternalUrls(
export async function prepareNetwork(
page: Page | BrowserContext,
baseURL: string,
options?: {
versions: Parameters<typeof mockVersions>[1];
},
) {
const unRefuse = await refuseExternalUrls(page, baseURL);
const unVersions = await mockVersions(page);
const unVersions = await mockVersions(page, options?.versions);

return async () => {
await unVersions();
Expand Down
4 changes: 3 additions & 1 deletion tests/utlis/mocks/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ function isRunRequest(url: URL | string) {

return (
uri.host === API_HOST &&
uri.pathname.match(/^\/?\/api\/\d+\.\d+\.\d+\/compiler\/run$/) !== null
(uri.pathname.match(/^\/?\/api\/\d+\.\d+\.\d+\/compiler\/run$/) !== null ||
uri.pathname.match(/^\/?\/api\/\d+\.\d+\.\d+\/compiler\/translate$/) !==
null)
);
}

Expand Down
3 changes: 3 additions & 0 deletions tests/utlis/mocks/result.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function makeJSPrintCode(text: string) {
return `var moduleId = function (_) {\n 'use strict';\n //region block: pre-declaration\n setMetadataFor(Unit, 'Unit', objectMeta);\n setMetadataFor(BaseOutput, 'BaseOutput', classMeta);\n setMetadataFor(NodeJsOutput, 'NodeJsOutput', classMeta, BaseOutput);\n setMetadataFor(BufferedOutput, 'BufferedOutput', classMeta, BaseOutput, VOID, BufferedOutput);\n setMetadataFor(BufferedOutputToConsoleLog, 'BufferedOutputToConsoleLog', classMeta, BufferedOutput, VOID, BufferedOutputToConsoleLog);\n //endregion\n function Unit() {\n }\n protoOf(Unit).toString = function () {\n return 'kotlin.Unit';\n };\n var Unit_instance;\n function Unit_getInstance() {\n return Unit_instance;\n }\n function get_output() {\n _init_properties_console_kt__rfg7jv();\n return output;\n }\n var output;\n function BaseOutput() {\n }\n function NodeJsOutput(outputStream) {\n BaseOutput.call(this);\n this.outputStream_1 = outputStream;\n }\n protoOf(NodeJsOutput).print_o1pwgy_k$ = function (message) {\n // Inline function 'kotlin.io.String' call\n var messageString = String(message);\n this.outputStream_1.write(messageString);\n };\n function BufferedOutputToConsoleLog() {\n BufferedOutput.call(this);\n }\n protoOf(BufferedOutputToConsoleLog).print_o1pwgy_k$ = function (message) {\n // Inline function 'kotlin.io.String' call\n var s = String(message);\n // Inline function 'kotlin.text.nativeLastIndexOf' call\n // Inline function 'kotlin.js.asDynamic' call\n var i = s.lastIndexOf('\\n', 0);\n if (i >= 0) {\n var tmp = this;\n var tmp_0 = this.buffer_1;\n // Inline function 'kotlin.text.substring' call\n // Inline function 'kotlin.js.asDynamic' call\n tmp.buffer_1 = tmp_0 + s.substring(0, i);\n this.flush_shahbo_k$();\n // Inline function 'kotlin.text.substring' call\n var this_0 = s;\n var startIndex = i + 1 | 0;\n // Inline function 'kotlin.js.asDynamic' call\n s = this_0.substring(startIndex);\n }\n this.buffer_1 = this.buffer_1 + s;\n };\n protoOf(BufferedOutputToConsoleLog).flush_shahbo_k$ = function () {\n console.log(this.buffer_1);\n this.buffer_1 = '';\n };\n function BufferedOutput() {\n BaseOutput.call(this);\n this.buffer_1 = '';\n }\n protoOf(BufferedOutput).print_o1pwgy_k$ = function (message) {\n var tmp = this;\n var tmp_0 = this.buffer_1;\n // Inline function 'kotlin.io.String' call\n tmp.buffer_1 = tmp_0 + String(message);\n };\n function print(message) {\n _init_properties_console_kt__rfg7jv();\n get_output().print_o1pwgy_k$(message);\n }\n var properties_initialized_console_kt_gll9dl;\n function _init_properties_console_kt__rfg7jv() {\n if (!properties_initialized_console_kt_gll9dl) {\n properties_initialized_console_kt_gll9dl = true;\n // Inline function 'kotlin.run' call\n // Inline function 'kotlin.contracts.contract' call\n // Inline function 'kotlin.io.output.<anonymous>' call\n var isNode = typeof process !== 'undefined' && process.versions && !!process.versions.node;\n output = isNode ? new NodeJsOutput(process.stdout) : new BufferedOutputToConsoleLog();\n }\n }\n function implement(interfaces) {\n var maxSize = 1;\n var masks = [];\n var inductionVariable = 0;\n var last = interfaces.length;\n while (inductionVariable < last) {\n var i = interfaces[inductionVariable];\n inductionVariable = inductionVariable + 1 | 0;\n var currentSize = maxSize;\n var tmp1_elvis_lhs = i.prototype.$imask$;\n var imask = tmp1_elvis_lhs == null ? i.$imask$ : tmp1_elvis_lhs;\n if (!(imask == null)) {\n masks.push(imask);\n currentSize = imask.length;\n }\n var iid = i.$metadata$.iid;\n var tmp;\n if (iid == null) {\n tmp = null;\n } else {\n // Inline function 'kotlin.let' call\n // Inline function 'kotlin.contracts.contract' call\n // Inline function 'kotlin.js.implement.<anonymous>' call\n tmp = bitMaskWith(iid);\n }\n var iidImask = tmp;\n if (!(iidImask == null)) {\n masks.push(iidImask);\n currentSize = Math.max(currentSize, iidImask.length);\n }\n if (currentSize > maxSize) {\n maxSize = currentSize;\n }\n }\n return compositeBitMask(maxSize, masks);\n }\n function bitMaskWith(activeBit) {\n var numberIndex = activeBit >> 5;\n var intArray = new Int32Array(numberIndex + 1 | 0);\n var positionInNumber = activeBit & 31;\n var numberWithSettledBit = 1 << positionInNumber;\n intArray[numberIndex] = intArray[numberIndex] | numberWithSettledBit;\n return intArray;\n }\n function compositeBitMask(capacity, masks) {\n var tmp = 0;\n var tmp_0 = new Int32Array(capacity);\n while (tmp < capacity) {\n var tmp_1 = tmp;\n var result = 0;\n var inductionVariable = 0;\n var last = masks.length;\n while (inductionVariable < last) {\n var mask = masks[inductionVariable];\n inductionVariable = inductionVariable + 1 | 0;\n if (tmp_1 < mask.length) {\n result = result | mask[tmp_1];\n }\n }\n tmp_0[tmp_1] = result;\n tmp = tmp + 1 | 0;\n }\n return tmp_0;\n }\n function protoOf(constructor) {\n return constructor.prototype;\n }\n function defineProp(obj, name, getter, setter) {\n return Object.defineProperty(obj, name, {configurable: true, get: getter, set: setter});\n }\n function objectCreate(proto) {\n return Object.create(proto);\n }\n function classMeta(name, defaultConstructor, associatedObjectKey, associatedObjects, suspendArity) {\n return createMetadata('class', name, defaultConstructor, associatedObjectKey, associatedObjects, suspendArity, null);\n }\n function createMetadata(kind, name, defaultConstructor, associatedObjectKey, associatedObjects, suspendArity, iid) {\n var undef = VOID;\n return {kind: kind, simpleName: name, associatedObjectKey: associatedObjectKey, associatedObjects: associatedObjects, suspendArity: suspendArity, $kClass$: undef, defaultConstructor: defaultConstructor, iid: iid};\n }\n function setMetadataFor(ctor, name, metadataConstructor, parent, interfaces, defaultConstructor, associatedObjectKey, associatedObjects, suspendArity) {\n if (!(parent == null)) {\n ctor.prototype = Object.create(parent.prototype);\n ctor.prototype.constructor = ctor;\n }\n var metadata = metadataConstructor(name, defaultConstructor, associatedObjectKey, associatedObjects, suspendArity == null ? [] : suspendArity);\n ctor.$metadata$ = metadata;\n if (!(interfaces == null)) {\n var receiver = !(metadata.iid == null) ? ctor : ctor.prototype;\n receiver.$imask$ = implement(interfaces);\n }\n }\n function objectMeta(name, defaultConstructor, associatedObjectKey, associatedObjects, suspendArity) {\n return createMetadata('object', name, defaultConstructor, associatedObjectKey, associatedObjects, suspendArity, null);\n }\n function get_VOID() {\n _init_properties_void_kt__3zg9as();\n return VOID;\n }\n var VOID;\n var properties_initialized_void_kt_e4ret2;\n function _init_properties_void_kt__3zg9as() {\n if (!properties_initialized_void_kt_e4ret2) {\n properties_initialized_void_kt_e4ret2 = true;\n VOID = void 0;\n }\n }\n function main() {\n print('${text}');\n }\n //region block: init\n Unit_instance = new Unit();\n //endregion\nif (typeof get_output !== "undefined") {\n get_output();\n output = new BufferedOutput();\n _.output = get_output();\n}\n main();\n return _;\n}(typeof moduleId === 'undefined' ? {} : moduleId);\nmoduleId.output?.buffer_1;\n\n` as const;
}
Loading

0 comments on commit cfb7e5c

Please sign in to comment.