blob: 51ea6d54afd6e0dd0b0a45ac8184467b4754fa20 [file]
// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
// sandbox.js provides a JSON-RPC 2.0 wrapper around functionality exposed
// by ddc_module.loader.js
//
// This is intended to run inside the _sandboxed iframe_ and allows the host to
// load modules, run code, hot-reload, receive console messages, etc. inside the
// iframe using RPC over a `MessageChannel` exchanged through `postMessage`.
//
// The client side of these RPC calls is wrapped by [Sandbox] from
// `package:dartpad`.
(function () {
// Port for JSON-RPC 2.0 communication with host.
let rpcPort = null;
const errorCode = {
// JSON-RPC 2.0 Spec.
METHOD_NOT_FOUND: -32601,
INVALID_PARAMS: -32602,
SERVER_ERROR: -32000,
// pkg/dartpad/lib/src/exceptions.yaml
MODULE_LOADER_NOT_AVAILABLE: 8001,
FLUTTER_LOADER_NOT_AVAILABLE: 8002,
MODULE_LOADING_FAILED: 8100,
EXECUTION_FAILED: 8200,
HOT_RESTART_FAILED: 8300,
HOT_RELOAD_FAILED: 8400,
};
class RpcError extends Error {
constructor(message, code = errorCode.SERVER_ERROR) {
super(message);
this.code = code;
this.name = "RpcError";
}
}
// Registry of RPC methods
const rpcMethods = {};
async function onRcpMessage(ev) {
const m = JSON.parse(ev.data);
// Ignore invalid messages or responses from the host
if (!m || m.jsonrpc !== '2.0' || !m.method) return;
const handler = rpcMethods[m.method];
try {
if (!handler) {
throw new RpcError(
`Method not found: ${m.method}`,
errorCode.METHOD_NOT_FOUND
);
}
// Execute the registered method
const result = await handler(m.params);
// If it's a request (has an id), send a success response
if (m.id !== undefined) {
rpcPort.postMessage(JSON.stringify({
jsonrpc: '2.0',
id: m.id,
result: result ?? {}
}));
}
} catch (e) {
if (m.id === undefined) {
console.error(`RPC Notification Error (${m.method}):`, e);
return;
}
const code = e instanceof RpcError ? e.code : errorCode.SERVER_ERROR;
const message = e instanceof Error ? e.message : String(e);
rpcPort.postMessage(JSON.stringify({
jsonrpc: '2.0',
id: m.id,
error: { code, message }
}));
}
}
function sendNotification(method, params) {
if (!rpcPort) return;
rpcPort.postMessage(JSON.stringify({
jsonrpc: '2.0',
method: method,
params: params
}));
}
// Serialize arg similar to what console.log would do.
function safeSerialize(arg) {
if (arg === null) return 'null';
if (arg === undefined) return 'undefined';
if (arg instanceof Error) return arg.stack || arg.toString();
if (typeof arg === 'function') return `[Function: ${arg.name || 'anonymous'}]`;
if (arg instanceof HTMLElement) return `<${arg.tagName.toLowerCase()}>`;
if (typeof arg === 'object') {
try {
return JSON.stringify(arg);
} catch (_) {
return Object.prototype.toString.call(arg);
}
}
return `${arg}`;
}
const originalConsole = {
log: console.log,
info: console.info,
warn: console.warn,
error: console.error
};
// Proxy console over RPC
for (const level of Object.keys(originalConsole)) {
console[level] = function (...args) {
// Format message as a single string
const message = args.map(safeSerialize).join(' ');
sendNotification('console', { level, message });
// Pass to actual browser console for DevTools debugging
originalConsole[level].apply(console, args);
};
}
// Catch unhandled browser errors and route them to our console proxy
window.addEventListener('error', (e) => console.error(`Uncaught: ${e.message}`));
window.addEventListener('unhandledrejection', (e) => console.error(`Unhandled Rejection: ${e.reason}`));
// Create a blob URL and register it with DDC's internal loader.
function createAndRegisterBlob(moduleName, code) {
const blob = new Blob([code], { type: 'application/javascript' });
const newUrl = URL.createObjectURL(blob);
if (self.$dartLoader) {
// Clean up old mappings and free memory
const oldUrl = self.$dartLoader.moduleIdToUrl.get(moduleName);
if (oldUrl) {
self.$dartLoader.urlToModuleId.delete(oldUrl);
// TODO(jonasfj): We should consider cleaning up <script> tags, if
// nothing else just to keep the DOM clean.
if (oldUrl.startsWith('blob:')) {
try {
URL.revokeObjectURL(oldUrl);
} catch (e) {
// ignore
}
}
}
// Register new mappings so DDC stack traces work
self.$dartLoader.moduleIdToUrl.set(moduleName, newUrl);
self.$dartLoader.urlToModuleId.set(newUrl, moduleName);
}
return newUrl;
}
rpcMethods.loadModule = async (params) => {
const { code, moduleName } = params;
if (!code) {
throw new RpcError("'code' is required.", errorCode.INVALID_PARAMS);
}
if (!moduleName) {
throw new RpcError("'moduleName' is required.", errorCode.INVALID_PARAMS);
}
const url = createAndRegisterBlob(moduleName, code);
await new Promise((resolve) =>
// TODO(jonasfj): Handle script loading failure and throw
// MODULE_LOADING_FAILED. Requires us to duplicate logic
// from DDC module loader.
self.$dartLoader.forceLoadScript(url, resolve),
);
return {};
};
rpcMethods.runMain = async (params) => {
const { libraryUri, options = {} } = params;
if (!libraryUri) {
throw new RpcError(
"libraryUri is required to run code.",
errorCode.INVALID_PARAMS
);
}
if (!self.dartDevEmbedder) {
throw new RpcError(
"dartDevEmbedder is not initialized.",
errorCode.MODULE_LOADER_NOT_AVAILABLE
);
}
try {
self.dartDevEmbedder.runMain(libraryUri, options);
return { status: 'running' };
} catch (e) {
throw new RpcError(e.message || String(e), errorCode.EXECUTION_FAILED);
}
};
rpcMethods.runApp = async (params) => {
const { libraryUri, options = {} } = params;
if (!libraryUri) {
throw new RpcError(
"libraryUri is required to run code.",
errorCode.INVALID_PARAMS
);
}
if (!self.dartDevEmbedder) {
throw new RpcError(
"dartDevEmbedder is not initialized.",
errorCode.MODULE_LOADER_NOT_AVAILABLE
);
}
if (!self._flutter || !self._flutter.loader) {
throw new RpcError(
"flutter.js is not loaded!",
errorCode.FLUTTER_LOADER_NOT_AVAILABLE
);
}
// To run a flutter app, you don't call the entrypoint, instead you call
// a wrapper (or bootstrap script) that starts flutter engine and calls the
// entrypoint.
const bootstrapWrapperUri = libraryUri + '.virtual-bootstrap-wrapper.dart';
const libraryUriJson = JSON.stringify(bootstrapWrapperUri);
const optionsJson = JSON.stringify(options);
const url = URL.createObjectURL(new Blob([`
try {
self.dartDevEmbedder.runMain(${libraryUriJson}, ${optionsJson});
} catch (e) {
console.error('runMain() inside runApp() failed: ', e.message || String(e));
}
`], { type: 'application/javascript' }));
try {
const engineInitializer = await new Promise((resolve) => {
self._flutter.loader.loadEntrypoint({
entrypointUrl: url,
onEntrypointLoaded: resolve,
});
});
const appRunner = await engineInitializer.initializeEngine(
self.dartpadFlutterConfiguration,
);
await appRunner.runApp();
return { status: 'running' };
} catch (e) {
throw new RpcError(e.message || String(e), errorCode.EXECUTION_FAILED);
} finally {
URL.revokeObjectURL(url);
}
};
rpcMethods.hotRestart = async (params) => {
const { code, moduleName } = params;
if (!self.dartDevEmbedder) {
throw new RpcError(
"dartDevEmbedder is not initialized.",
errorCode.MODULE_LOADER_NOT_AVAILABLE
);
}
if (code && !moduleName) {
throw new RpcError("'moduleName' is required.", errorCode.INVALID_PARAMS);
}
try {
// Define the official DDC hook for reloading modules during restart
const reloadModules = (appName, callback) => {
if (code) {
const url = createAndRegisterBlob(moduleName, code);
self.$dartLoader.forceLoadScript(url, callback);
} else {
callback();
}
};
self.$dartReloadModifiedModules = reloadModules;
await self.dartDevEmbedder.hotRestart();
if (self.$dartReloadModifiedModules === reloadModules) {
self.$dartReloadModifiedModules = null;
}
return { generation: self.dartDevEmbedder.hotRestartGeneration };
} catch (e) {
throw new RpcError(e.message || String(e), errorCode.HOT_RESTART_FAILED);
}
};
rpcMethods.hotReload = async (params) => {
const { code, librariesToReload = [], moduleName } = params;
if (!self.dartDevEmbedder) {
throw new RpcError(
"dartDevEmbedder is not initialized.",
errorCode.MODULE_LOADER_NOT_AVAILABLE
);
}
const filesToLoad = [];
if (code) {
if (!moduleName) {
throw new RpcError("'moduleName' is required.", errorCode.INVALID_PARAMS);
}
filesToLoad.push(createAndRegisterBlob(moduleName, code));
}
try {
await self.dartDevEmbedder.hotReload(filesToLoad, librariesToReload);
if (self.dartDevEmbedder.debugger.extensionNames.includes('ext.flutter.reassemble')) {
await self.dartDevEmbedder.debugger.invokeExtension('ext.flutter.reassemble', '{}');
}
return { generation: self.dartDevEmbedder.hotReloadGeneration };
} catch (e) {
throw new RpcError(e.message || String(e), errorCode.HOT_RELOAD_FAILED);
}
};
rpcMethods.appMetrics = async () => {
if (!self.dart_library || !self.dart_library.appMetrics) return {};
return self.dart_library.appMetrics();
};
rpcMethods.getHotRestartGeneration = async () => {
if (!self.dartDevEmbedder) return { generation: 0 };
return { generation: self.dartDevEmbedder.hotRestartGeneration };
};
rpcMethods.getHotReloadGeneration = async () => {
if (!self.dartDevEmbedder) return { generation: 0 };
return { generation: self.dartDevEmbedder.hotReloadGeneration };
};
function onWindowMessage(ev) {
if (ev.source !== window.parent) {
console.warn('Rejected connect message from untrusted source.');
return;
}
// We expect the Dart host to send {'action': 'connect'}
if (ev.data?.action !== 'connect') {
console.warn('Received non-connect message:', ev);
return;
}
if (ev.ports?.length !== 1) {
console.error('Connect message missing port:', ev);
return;
}
window.removeEventListener('message', onWindowMessage);
rpcPort = ev.ports[0];
rpcPort.onmessage = onRcpMessage;
rpcPort.start();
}
window.addEventListener('message', onWindowMessage);
})();