Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add basic support for simx #179

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,7 @@ dist
# TernJS port file
.tern-port

.vscode-test-web
.vscode-test-web

resources/sim.js
resources/sim.js.map
11 changes: 6 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -332,13 +332,14 @@
"test": "vscode-test-web --browserType=chromium --extensionDevelopmentPath=. --extensionTestsPath=dist/web/test/suite/index.js",
"pretest": "yarn run compile-web",
"vscode:prepublish": "yarn run package-web",
"compile-web": "webpack",
"watch-web": "webpack --watch",
"package-web": "webpack --mode production --devtool hidden-source-map",
"compile-web": "yarn run simulator && webpack",
"watch-web": "yarn run simulator && webpack --watch",
"package-web": "yarn run simulator && webpack --mode production --devtool hidden-source-map",
"lint": "eslint src --ext ts",
"run-in-browser": "vscode-test-web --browserType=chromium --extensionDevelopmentPath=.",
"run-in-browser": "yarn run simulator && vscode-test-web --browserType=chromium --extensionDevelopmentPath=.",
"generate-l10n": "vscode-l10n-dev export --outDir ./l10n ./src",
"generate-l10n-ploc": "vscode-l10n-dev generate-pseudo -o ./l10n ./l10n/bundle.l10n.json ./package.nls.json"
"generate-l10n-ploc": "vscode-l10n-dev generate-pseudo -o ./l10n ./l10n/bundle.l10n.json ./package.nls.json",
"simulator": "cd src/sim && webpack"
},
"devDependencies": {
"@types/mocha": "^10.0.0",
Expand Down
27 changes: 27 additions & 0 deletions src/localtypings/simulatorExtensionMessages.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
interface VSCodeAPI {
postMessage(data: any): void;
}

interface BaseMessage {
type: "simulator-extension"
src: "simulator" | "extension";
action: string;
id?: string;
}

interface VSCodeResponse extends BaseMessage {
success: boolean;
id: string;
}

interface TargetConfigMessage extends BaseMessage {
action: "targetConfig";
}

interface TargetConfigResponse extends VSCodeResponse {
action: "targetConfig";
config: any;
}

type SimulatorExtensionMessage = TargetConfigMessage;
type SimulatorExtensionResponse = TargetConfigResponse | VSCodeResponse;
144 changes: 144 additions & 0 deletions src/sim/frames.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { getTargetConfigAsync } from "./service";

const FRAME_DATA_MESSAGE_CHANNEL = "messagechannel"

Check warning on line 3 in src/sim/frames.ts

View workflow job for this annotation

GitHub Actions / buildpr

Missing semicolon
const FRAME_ASPECT_RATIO = "aspectratio"

Check warning on line 4 in src/sim/frames.ts

View workflow job for this annotation

GitHub Actions / buildpr

Missing semicolon
const MESSAGE_SOURCE = "pxtdriver"

Check warning on line 5 in src/sim/frames.ts

View workflow job for this annotation

GitHub Actions / buildpr

Missing semicolon
const PERMANENT = "permanent"

Check warning on line 6 in src/sim/frames.ts

View workflow job for this annotation

GitHub Actions / buildpr

Missing semicolon


let simFrames: {[index: string]: HTMLIFrameElement} = {};
let _targetConfig: Promise<any>;


function nextId() {
return crypto.randomUUID();
}

let pendingFrames: {[index: string]: Promise<void>} = {};

export function handleMessagePacket(message: any, source?: Window) {
if (message.type === "messagepacket") {
const channel = message.channel as string;

if (!pendingFrames[channel]) {
pendingFrames[channel] = (async () => {
const targetConfig = await targetConfigAsync();
const simInfo = targetConfig.packages.approvedRepoLib[channel];

if (simInfo.simx) {
startSimulatorExtension(
getSimxUrl(channel, simInfo.simx.index),
true,
undefined,
channel
);
}
})();
}

pendingFrames[channel].then(() => {
for (const frame of Object.keys(simFrames)) {
const contentWindow = simFrames[frame].contentWindow;

if (contentWindow && contentWindow !== source) {
simFrames[frame].contentWindow?.postMessage(message, "*");
}
}
})

Check warning on line 47 in src/sim/frames.ts

View workflow job for this annotation

GitHub Actions / buildpr

Missing semicolon

const simFrame = getSimFrame();

if (simFrame.contentWindow && simFrame.contentWindow !== source) {
simFrame.contentWindow.postMessage(message, "*")

Check warning on line 52 in src/sim/frames.ts

View workflow job for this annotation

GitHub Actions / buildpr

Missing semicolon
}
}
}

function getSimFrame() {
return document.getElementById("simframe") as HTMLIFrameElement;
}

function getSimUrl(): URL {
return new URL(getSimFrame().src);
}


function getSimxUrl(repo: string, index = "index.html") {
const simUrl = getSimUrl();

// Ensure we preserve upload target path (/app/<sha>---simulator)
const simPath = simUrl.pathname.replace(/---?.*/, "");
// Construct the path. The "-" element delineates the extension key from the resource name.
const simxPath = [simPath, "simx", repo, "-", index].join("/");
// Create the fully-qualified URL, preserving the origin by removing all leading slashes
return new URL(simxPath.replace(/^\/+/, ""), simUrl.origin).toString();
}


function createFrame(url: string): HTMLDivElement {
const wrapper = document.createElement("div") as HTMLDivElement;
wrapper.className = `simframe ui embed`;

const frame = document.createElement('iframe') as HTMLIFrameElement;
frame.id = 'sim-frame-' + nextId()

Check warning on line 83 in src/sim/frames.ts

View workflow job for this annotation

GitHub Actions / buildpr

Missing semicolon
frame.title = "Simulator";
frame.allowFullscreen = true;
frame.setAttribute('allow', 'autoplay;microphone');
frame.setAttribute('sandbox', 'allow-same-origin allow-scripts');
frame.className = 'no-select';

let furl = url;
furl += '#' + frame.id;

frame.src = furl;
frame.frameBorder = "0";
// frame.dataset['runid'] = this.runId;
frame.dataset['origin'] = new URL(furl).origin || "*";
frame.dataset['loading'] = "true";

wrapper.appendChild(frame);

const i = document.createElement("i");
i.className = "videoplay xicon icon";
i.style.display = "none";
wrapper.appendChild(i);

const l = document.createElement("div");
l.className = "ui active loader";
i.style.display = "none";
wrapper.appendChild(l);

return wrapper;
}

function startSimulatorExtension(url: string, permanent: boolean, aspectRatio?: number, messageChannel?: string) {
const root = document.getElementById("root");
if (root) {
root.classList.add("simx");
}

aspectRatio = aspectRatio || 1.22;
let wrapper = createFrame(url);
getContainer().appendChild(wrapper);
const messageFrame = wrapper.firstElementChild as HTMLIFrameElement;
messageFrame.dataset[FRAME_DATA_MESSAGE_CHANNEL] = messageChannel;
messageFrame.dataset[FRAME_ASPECT_RATIO] = aspectRatio + "";
wrapper.classList.add("simmsg");
if (permanent) {
messageFrame.dataset[PERMANENT] = "true";
}

simFrames[messageChannel!] = messageFrame;
}

function getContainer() {
return document.getElementById("simulator-extension-frames") as HTMLDivElement;
}

async function targetConfigAsync() {
if (!_targetConfig) {
_targetConfig = await getTargetConfigAsync();
}

return _targetConfig;
}
26 changes: 26 additions & 0 deletions src/sim/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { handleMessagePacket } from "./frames";
import { initService } from "./service";

const acquireApi = (window as any).acquireVsCodeApi;
const vscode = acquireApi();

(window as any).acquireVsCodeApi = () => vscode;

window.addEventListener("message", function (m) {
handleMessagePacket(m.data, m.source as Window);
});

document.addEventListener("DOMContentLoaded", function (event) {
const fs = document.getElementById("fullscreen");
if (fs) {
fs.remove();
}

const simFrame = document.getElementById("simframe") as HTMLIFrameElement;

const framesContainer = document.createElement("div");
framesContainer.id = "simulator-extension-frames";
simFrame.insertAdjacentElement("afterend", framesContainer);
});

initService(vscode);
50 changes: 50 additions & 0 deletions src/sim/service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/// <reference path="../localtypings/simulatorExtensionMessages.d.ts" />

let vscode: VSCodeAPI;
const pendingMessages: {[index: string]: (resp: SimulatorExtensionResponse) => void} = {};

export async function getTargetConfigAsync() {
const resp = await sendMessageAsync({
type: "simulator-extension",
src: "simulator",
action: "targetConfig"
}) as TargetConfigResponse;

return resp.config;
}

function sendMessageAsync(message: SimulatorExtensionMessage): Promise<VSCodeResponse> {
return new Promise((resolve, reject) => {
const toSend: BaseMessage = {
...message,
id: crypto.randomUUID()
};

pendingMessages[toSend.id!] = resp => {
if (resp.success) {
resolve(resp);
}
else {
reject(resp);
}
};

vscode.postMessage(toSend);
});
}

export function initService(api: VSCodeAPI) {
vscode = api;

window.addEventListener("message", function (m) {
if (m.data?.type === "simulator-extension") {
const response = m.data as SimulatorExtensionResponse;

const handler = pendingMessages[response.id];
if (handler) {
delete pendingMessages[response.id];
handler(response);
}
}
});
}
17 changes: 17 additions & 0 deletions src/sim/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "ES2020",
"outDir": "../../dist/sim",
"lib": [
"ES2020", "DOM"
],
"sourceMap": true,
"rootDir": ".",
"strict": true /* enable all strict type-checking options */
/* Additional Checks */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
}
}
76 changes: 76 additions & 0 deletions src/sim/webpack.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

//@ts-check
"use strict";

//@ts-check
/** @typedef {import("webpack").Configuration} WebpackConfig **/

const path = require("path");
const webpack = require("webpack");

/** @type WebpackConfig */
const webExtensionConfig = {
mode: "none", // this leaves the source code as close as possible to the original (when packaging we set this to "production")
target: "web", // extensions run in a webworker context
entry: {
"extension": "./index.ts",
},
output: {
filename: "sim.js",
path: path.join(__dirname, "../../resources"),
libraryTarget: "commonjs",
devtoolModuleFilenameTemplate: "../../[resource-path]"
},
resolve: {
mainFields: ["browser", "module", "main"], // look for `browser` entry point in imported node modules
extensions: [".ts", ".js"], // support ts-files and js-files
alias: {
// provides alternate implementation for node module and source files
},
fallback: {
// Webpack 5 no longer polyfills Node.js core modules automatically.
// see https://webpack.js.org/configuration/resolve/#resolvefallback
// for the list of Node.js core module polyfills.
"assert": require.resolve("assert"),
"path": require.resolve("path-browserify")
}
},
module: {
rules: [{
test: /\.ts$/,
exclude: /node_modules/,
use: [{
loader: "ts-loader"
}]
},
{
// Make sure our marketplace icon is copied to final output
test: /\.(jpe?g|gif|png|svg)$/,
loader: "file"
}]
},
plugins: [
new webpack.optimize.LimitChunkCountPlugin({
maxChunks: 1 // disable chunks by default since web extensions must be a single bundle
}),
new webpack.ProvidePlugin({
process: "process/browser", // provide a shim for the global `process` variable
}),
],
externals: {
"vscode": "commonjs vscode", // ignored because it doesn"t exist
},
performance: {
hints: false
},
devtool: "nosources-source-map", // create a source map that points to the original source file
infrastructureLogging: {
level: "log", // enables logging required for problem matchers
},
};

module.exports = [ webExtensionConfig ];
Loading
Loading