diff --git a/.gitignore b/.gitignore index 15f77e32..d7aad45e 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ target/ # misc .DS_Store +.env.local .env.development.local .env.test.local .env.production.local diff --git a/README.md b/README.md index 9fb08cf4..54d77b5e 100644 --- a/README.md +++ b/README.md @@ -60,3 +60,60 @@ dfx deploy --argument '(null)' - Clone the package-set repo: https://github.com/dfinity/vessel-package-set - Make sure [`dhall` and `dhall-to-json` are installed](https://docs.dhall-lang.org/tutorials/Getting-started_Generate-JSON-or-YAML.html#os-x) with `apt` or `brew` - `dhall-to-json --file vessel-package-set/src/packages.dhall > motoko-playground/src/config/package-set.json` + +## Editor Integrations + +Motoko Playground supports +limited [cross-origin communication](https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage). If you are +building a custom smart contract editor or similar application, you can use the following code to start deploying a project using Motoko Playground: + +```js +const PLAYGROUND_ORIGIN = 'https://m7sm4-2iaaa-aaaab-qabra-cai.raw.ic0.app' +const APP_ID = 'MyEditor' + +const userFiles = { + 'Main.mo': 'actor { public func hello() : async Text { "Hello World" } }', +} + +const playground = window.open(`${PLAYGROUND_ORIGIN}?post=${APP_ID}`, 'playground') + +// Call repeatedly until loaded (interval ID used for acknowledgement) +const ack = setInterval(() => { + const request = { + type: 'workplace', + acknowledge: ack, + deploy: true, + actions: [{ + type: 'loadProject', + payload: { + files: userFiles, + } + }] + } + const data = `${APP_ID}${JSON.stringify(request)}` + console.log('Request data:', data) + playground.postMessage(data, PLAYGROUND_ORIGIN) +}, 1000) + +// Listen for acknowledgement +const responseListener = ({source, origin, data}) => { + if( + typeof data === 'string' && + data.startsWith(APP_ID) && + source === playground && + origin === PLAYGROUND_ORIGIN + ) { + console.log('Response data:', data) + // Parse JSON part of message + const response = JSON.parse(data.substring(APP_ID.length)) + if(response.acknowledge === ack) { + clearInterval(ack) + window.removeEventListener('message', responseListener) + } + } +} +window.addEventListener('message', responseListener) +``` + +Note: this works for `localhost`out of the box. If you would like to use this feature in production, please submit a PR +adding your application's public URL to [`src/integrations/allowedOrigins.js`](src/integrations/allowedOrigins.js). diff --git a/src/App.tsx b/src/App.tsx index 93489cd5..8bd0ca39 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -66,16 +66,16 @@ const urlParams = new URLSearchParams(window.location.search); const hasUrlParams = !!( urlParams.get("git") || urlParams.get("tag") || - urlParams.get("editor") + urlParams.get("post") ); async function fetchFromUrlParams( dispatch: (WorkplaceReducerAction) => void ): Promise | undefined> { const git = urlParams.get("git"); const tag = urlParams.get("tag"); - const editor = urlParams.get("editor"); - if (editor) { - return setupEditorIntegration(editor, dispatch); + const editorKey = urlParams.get("post"); + if (editorKey) { + return setupEditorIntegration(editorKey, dispatch); } if (git) { const repo = { diff --git a/src/integrations/allowedOriginPrefixes.js b/src/integrations/allowedOrigins.js similarity index 74% rename from src/integrations/allowedOriginPrefixes.js rename to src/integrations/allowedOrigins.js index 2e6edc8b..a0d624c1 100644 --- a/src/integrations/allowedOriginPrefixes.js +++ b/src/integrations/allowedOrigins.js @@ -2,9 +2,9 @@ // please submit a PR including the URL prefix for your application. // Read more: https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage#security_concerns -const ALLOWED_ORIGIN_PREFIXES = [ - "http://localhost", // Local machine +const ALLOWED_ORIGINS = [ + /^https?:\/\/(localhost|127.0.0.1)(:[0-9]+)?$/, // Localhost "https://blocks-editor.github.io", // Blocks (visual Motoko smart contract editor) ]; -export default ALLOWED_ORIGIN_PREFIXES; +export default ALLOWED_ORIGINS; diff --git a/src/integrations/editorIntegration.ts b/src/integrations/editorIntegration.ts index 572c25f6..e78c19db 100644 --- a/src/integrations/editorIntegration.ts +++ b/src/integrations/editorIntegration.ts @@ -1,17 +1,21 @@ import { WorkplaceReducerAction } from "../contexts/WorkplaceState"; -import ALLOWED_ORIGIN_PREFIXES from "./allowedOriginPrefixes"; +import ALLOWED_ORIGINS from "./allowedOrigins"; type EditorIntegrationHooks = { deploy: () => Promise; }; -type EditorIntegrationMessage = { +type EditorIntegrationRequest = { type: "workplace"; acknowledge: number; actions: [WorkplaceReducerAction]; deploy?: boolean; }; +type EditorIntegrationResponse = { + acknowledge: number; +}; + export const INTEGRATION_HOOKS: Partial = {}; // Cached return value to ensure at most one initialization @@ -31,10 +35,9 @@ export async function setupEditorIntegration( if (previousResult) { return previousResult; } - const messagePrefix = `editor_${editorKey}:`; // Handle JSON messages from the external editor - const handleMessage = async (message: EditorIntegrationMessage) => { + const handleMessage = async (message: EditorIntegrationRequest) => { if (message.type == "workplace") { message.actions.forEach((action) => { dispatch(action); @@ -55,22 +58,29 @@ export async function setupEditorIntegration( try { // Ensure the message is from an allowed origin if ( - !ALLOWED_ORIGIN_PREFIXES.some((prefix) => origin.startsWith(prefix)) + !ALLOWED_ORIGINS.some((allowed) => + allowed instanceof RegExp + ? allowed.test(origin) + : allowed === origin + ) ) { return; } - // Validate and parse integration message - if (typeof data === "string" && data.startsWith(messagePrefix)) { - const message = JSON.parse(data.substring(messagePrefix.length)); + // Validate and parse integration message (example: `CustomEditor{"type":"workplace","actions":[...]}`) + if (typeof data === "string" && data.startsWith(editorKey)) { + const message = JSON.parse(data.substring(editorKey.length)); if (process.env.NODE_ENV === "development") { console.log("Received integration message:", message); } await handleMessage(message); if (!(source instanceof MessagePort)) { - // Send acknowledgement + // Send response (example: `CustomEditor{"acknowledge":123}`) + const response: EditorIntegrationResponse = { + acknowledge: message.acknowledge, + }; source?.postMessage( - `${messagePrefix}acknowledge:${message.acknowledge}`, + `${editorKey}${JSON.stringify(response)}`, origin ); }