Skip to content
Merged
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
13 changes: 7 additions & 6 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 All @@ -362,7 +363,7 @@
"@types/path-browserify": "^1.0.0",
"@vscode/extension-telemetry": "^0.7.5",
"makecode-browser": "^1.3.3",
"makecode-core": "^1.7.2",
"makecode-core": "1.7.8",
"path-browserify": "^1.0.1"
}
}
28 changes: 28 additions & 0 deletions src/localtypings/simulatorExtensionMessages.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
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;
webConfig: any;
}

type SimulatorExtensionMessage = TargetConfigMessage;
type SimulatorExtensionResponse = TargetConfigResponse | VSCodeResponse;
140 changes: 140 additions & 0 deletions src/sim/frames.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
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<TargetConfigResponse>;


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 { config, webConfig } = await targetConfigAsync();
const simInfo = config.packages.approvedRepoLib[channel];

if (simInfo.simx) {
startSimulatorExtension(
getSimxUrl(webConfig, 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 getSimxUrl(webConfig: any, repo: string, index = "index.html") {
const simUrl = new URL(webConfig.simUrl);

// 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 79 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 = 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;
}

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 ];
4 changes: 4 additions & 0 deletions src/web/makecodeOperations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ export function getSimHtmlAsync(folder: vscode.WorkspaceFolder, cancellationToke
return enqueueOperationAsync(folder, () => cmd.getSimHTML({}), cancellationToken);
}

export function getWebConfigAsync(folder: vscode.WorkspaceFolder, cancellationToken?: vscode.CancellationToken) {
return enqueueOperationAsync(folder, () => cmd.getWebConfig({}), cancellationToken);
}

export function getAssetEditorHtmlAsync(folder: vscode.WorkspaceFolder, cancellationToken?: vscode.CancellationToken) {
return enqueueOperationAsync(folder, () => cmd.getAssetEditorHTML({}), cancellationToken);
}
Expand Down
Loading
Loading