Resolve "Monaco Playground Typing and Rollup Bundles"

This commit is contained in:
2025-07-13 06:17:53 +00:00
committed by Jim Hamill
parent 50fb4020b6
commit aa3967a060
14 changed files with 572 additions and 225 deletions
@@ -0,0 +1,140 @@
<template>
<div class="code-editor">
<div ref="editorContainer" class="code-editor" />
<slot />
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue';
import loader, { Monaco } from '@monaco-editor/loader';
import { ImportedLib } from '../utils/useXOCashLibs';
export type CodeEditorProps = {
// List of .d.ts files to import into the editor.
libs?: Record<string, ImportedLib>;
// Color scheme of the editor
isDark?: boolean;
}
const props = withDefaults(defineProps<CodeEditorProps>(), {
libs: () => ({}),
isDark: false,
});
const code = defineModel<string>();
const monacoInstance = ref<Monaco | null>(null);
const editorContainer = ref<HTMLElement | null>(null);
const emit = defineEmits<{
(e: 'execute'): void;
}>();
watch(() => props.libs, async () => {
updateEditorTypes();
});
function updateEditorTypes() {
monacoInstance.value?.languages.typescript.typescriptDefaults.setExtraLibs(
Object.entries(props.libs).map(([name, content]) => {
// Dont add the library if it is empty
if (!content) return null;
const moduleContent = `declare module "${name}" { ${content.libTypes} }`;
return { uri: `inmemory://model/${name}.d.ts`, content: moduleContent };
}).filter((lib) => lib !== null)
);
}
onMounted(() => {
// Load Monaco Editor
loader.init().then((monaco) => {
monacoInstance.value = monaco;
// Throw if our editor container is not found
if (!editorContainer.value) {
throw new Error('Editor container not found');
}
// Disable unnecessary validations
monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({
noSemanticValidation: false,
noSyntaxValidation: false,
diagnosticCodesToIgnore: [ 1375 ] // Ignore "top-level await" error
});
// Add our typescript compiler options
monaco.languages.typescript.typescriptDefaults.setCompilerOptions({
target: monaco.languages.typescript.ScriptTarget.ESNext,
allowNonTsExtensions: true,
moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs,
module: monaco.languages.typescript.ModuleKind.ESNext,
noEmit: true,
lib: ["esnext", "dom"],
checkJs: true,
isolatedModules: true,
allowSyntheticDefaultImports: true
});
// Enable eager mode. Tries to move LSP on worker thread
monaco.languages.typescript.typescriptDefaults.setEagerModelSync(true);
// Add extra libs to the editor
updateEditorTypes();
// Define a theme to change the background color of the editor
monaco.editor.defineTheme('xo-sandbox', {
base: props.isDark ? 'vs-dark' : 'vs',
inherit: true,
rules: [],
colors: {
"editor.background": props.isDark ? '#161618' : '#f5f5f5'
}
});
// Create our editor into the container
const editor = monaco.editor.create(editorContainer.value, {
value: code.value,
language: 'typescript',
theme: 'xo-sandbox',
automaticLayout: true,
scrollBeyondLastLine: false,
suggest: {
showClasses: true,
showFunctions: true,
showVariables: true,
showModules: true,
showProperties: true,
showWords: true,
},
});
// Add control + S keybinding to execute code
editor.addAction({
id: `execute-code`,
label: 'Execute Code',
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS],
run: () => {
emit('execute')
}
});
// Update the code value when the editor content changes
editor.onDidChangeModelContent(() => {
code.value = editor.getValue();
});
});
})
</script>
<style scoped>
.code-editor {
height: 100%;
width: 100%;
}
</style>
@@ -1,19 +1,7 @@
<template>
<div class="code-sandbox" :style="sandboxStyle">
<div class="editor-container" :style="editorContainerStyle">
<v-ace-editor
v-model:value="code"
:style="editorStyle"
lang="javascript"
:theme="isDark ? 'github_dark' : 'github'"
@init="editorInit"
:options="{
showGutter: true,
minLines: 1,
showPrintMargin: false
}"
@keydown="handleExecuteHotkey"
/>
<code-editor v-model="code" @execute="executeCode" :style="editorStyle" :libs="libs" :is-dark="isDark"/>
<div class="editor-buttons">
<button v-if="!hideRunButton" @click="executeCode" class="primary-button">Run Code</button>
</div>
@@ -52,45 +40,36 @@
<script setup lang="ts">
import { useData } from 'vitepress';
import { ref, onMounted, onBeforeUnmount, computed } from 'vue';
import { VAceEditor } from 'vue3-ace-editor';
import ace from 'ace-builds';
import 'ace-builds/src-noconflict/theme-github';
import 'ace-builds/src-noconflict/theme-github_dark';
import 'ace-builds/src-noconflict/mode-javascript';
import { ref, onMounted, onBeforeUnmount, computed, version } from 'vue';
// Import the iframe HTML template
// NOTE: Having this in an external file is many-fold neater than stashing it all in here.
import iframeTemplateHtml from './CodeSandboxTemplate.html?raw';
import CodeEditor from './CodeEditor.vue';
import { ImportedLib, useXOCashLibs } from '../utils/useXOCashLibs';
const { isDark } = useData();
const xocashLibs = useXOCashLibs();
export type CodeSandboxProps = {
enableNetwork?: boolean;
fullHeight?: boolean;
height?: string;
editorHeight?: string;
outputHeight?: string;
hideRunButton?: boolean;
libs?: Record<string, ImportedLib>;
}
// Define props for the component
const props = defineProps({
enableNetwork: {
type: Boolean,
default: true
},
fullHeight: {
type: Boolean,
default: false
},
height: {
type: String,
default: 'auto'
},
editorHeight: {
type: String,
default: 'auto'
},
outputHeight: {
type: String,
default: 'auto'
},
hideRunButton: {
type: Boolean,
default: false,
}
const props = withDefaults(defineProps<CodeSandboxProps>(), {
enableNetwork: true,
fullHeight: false,
height: 'auto',
editorHeight: 'auto',
outputHeight: 'auto',
hideRunButton: false,
libs: (): Record<string, ImportedLib> => ({}),
});
// Define types for console logs
@@ -100,7 +79,8 @@ interface ConsoleLog {
}
// Define share URL base
const SHARE_BASE_URL = 'https://stack.xo.cash/playground.html';
// const SHARE_BASE_URL = 'https://stack.xo.cash/playground.html';
const SHARE_BASE_URL = 'http://localhost:5173/playground.html';
// Define a unique ID to pass into the IFrame.
// NOTE: We do this so that we can reliably identify where the postMessage came from.
@@ -111,7 +91,6 @@ const thisSandboxID = generateSandboxId();
const codeRef = ref(null);
const code = ref<string>('');
const logs = ref<ConsoleLog[]>([]);
const theme = ref('github_dark');
const showLogs = ref(false);
const sandboxIframe = ref<HTMLIFrameElement | null>(null);
@@ -169,14 +148,6 @@ const clearLogs = () => {
logs.value = [];
};
function editorInit(editor: any) {
editor.session.setTabSize(2);
editor.renderer.setScrollMargin(5, 5, 0, 0);
// Make sure the editor properly reflects its container size
editor.renderer.updateFull();
}
// Generate a shareable URL with the current code
const shareCode = () => {
try {
@@ -185,7 +156,13 @@ const shareCode = () => {
const base64Code = btoa(unescape(encodeURIComponent(code.value)));
// Generate the shareable URL with the Base64 encoded content in the hash fragment
const shareUrl = `${SHARE_BASE_URL}#code=${encodeURIComponent(base64Code)}`;
const baseURL = `${SHARE_BASE_URL}#code=${encodeURIComponent(base64Code)}`;
const url = new URL(baseURL);
url.searchParams.set('version', xocashLibs['@xocash/stack'].selectedVersion);
const shareUrl = url.toString();
// Check URL length - hash fragments can be longer, but still have limits
if (shareUrl.length > 10000) {
@@ -252,8 +229,20 @@ const executeCode = () => {
sandboxIframe.value.sandbox.add('allow-popups');
}
let embeddedCode = code.value;
// Replace our imported libs with the actual version.
Object.entries(props.libs).forEach(([lib, { selectedVersion }]) => {
const libUrl = `https://cdn.jsdelivr.net/npm/${lib}@${selectedVersion}/+esm`;
// Only replace in import statements to avoid replacing other occurrences
const importRegex = new RegExp(`import\\s+.+\\s+from\\s+['"]${lib.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}['"]`, 'g');
embeddedCode = embeddedCode.replace(importRegex, (match) => {
return match.replace(lib, libUrl);
});
});
// Create the HTML content
const iframeContent = createIframeContent(thisSandboxID, code.value);
const iframeContent = createIframeContent(thisSandboxID, embeddedCode);
// Create a Blob containing the HTML content
const blob = new Blob([iframeContent], { type: 'text/html' });
@@ -305,30 +294,6 @@ const handleMessage = (event: MessageEvent) => {
}
};
// Platform detection for keyboard shortcuts
const isMac = computed(() => {
// Modern browsers
if ('userAgentData' in navigator) {
return (navigator as any).userAgentData.platform === 'macOS';
}
// Legacy browsers
return /Mac|iPod|iPhone|iPad/.test(navigator.platform);
});
// Handle keyboard shortcuts for code execution
const handleExecuteHotkey = (event: KeyboardEvent) => {
// Check if the save hotkey was pressed
const isSaveShortcut = event.key.toLowerCase() === 's' &&
(isMac.value ? event.metaKey : event.ctrlKey);
// If the save hotkey was pressed, then execute the code
if (isSaveShortcut) {
event.preventDefault();
executeCode();
}
};
// Lifecycle hooks
onMounted(() => {
// Check if there's code in the URL hash fragment
@@ -366,6 +331,15 @@ onMounted(() => {
// No code in hash or query, load from slot
loadCodeFromSlot();
}
}
// Get the selected version from the url if it exists
const urlParams = new URLSearchParams(window.location.search);
const versionParam = urlParams.get('version');
if (versionParam) {
xocashLibs['@xocash/stack'].selectedVersion = versionParam;
}
// Add event listener for messages from the iframe
@@ -104,19 +104,9 @@
<!-- Module script for ESM support -->
<script type="module">
// Helper function to handle both dynamic and static imports
async function runUserCode() {
try {
// Execute the user code
__userCode__;
} catch (error) {
console.error(error.message);
}
}
runUserCode().catch((error) => {
console.error('Error in user code execution:', error.message);
});
// Execute user code directly at top level to support ES6 imports
// Error handling is managed by global error handlers above
__userCode__;
</script>
</body>
</html>
@@ -6,6 +6,9 @@
<img class="dark-only" src="/logo-dark.svg" alt="XO Stack Logo">
XO Stack - Playground
</div>
<select class="version-select" v-model="xocashLibs['@xocash/stack'].selectedVersion">
<option v-for="version in xocashLibs['@xocash/stack'].versions" :key="version" :value="version">{{ version }}</option>
</select>
<div class="button-container">
<button class="primary-button" @click="shareCode">Share</button>
<button class="primary-button" @click="executeCode">Run Code</button>
@@ -18,6 +21,8 @@
outputHeight="20%"
fullHeight
hideRunButton
:libs="libs"
:isDark="isDark"
>
<slot></slot>
</CodeSandbox>
@@ -25,21 +30,28 @@
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { ref, computed } from 'vue';
import { useData } from 'vitepress';
import CodeSandbox from '@theme/components/CodeSandbox.vue'
import { useXOCashLibs } from '../utils/useXOCashLibs';
import CodeSandbox from './CodeSandbox.vue'
const xocashLibs = useXOCashLibs();
const libs = computed(() => {
return { ...xocashLibs }
});
const { isDark } = useData();
const sandbox = ref<InstanceType<typeof CodeSandbox> | null>(null);
async function executeCode() {
sandbox.value.executeCode();
sandbox.value?.executeCode();
}
async function shareCode() {
sandbox.value.shareCode()
sandbox.value?.shareCode()
}
</script>
@@ -101,4 +113,14 @@ display: flex; align-items: center;
.console-output {
white-space: pre-wrap;
}
.version-select {
margin-right: 10px;
background-color: var(--vp-c-bg);
color: var(--vp-c-text-1);
border: 1px solid var(--vp-c-border);
border-radius: 4px;
padding: 4px 8px;
font-size: 14px;
font-family: var(--vp-font-family-base);
}
</style>
@@ -0,0 +1,72 @@
import { watch, onMounted, reactive } from 'vue';
export type ImportedLib = {
versions: string[];
selectedVersion: string;
libTypes: string;
};
export type XOCashLibs = {
'@xocash/stack': ImportedLib;
'@xocash/primitives': ImportedLib;
}
const xocashLibs = reactive<XOCashLibs>({
'@xocash/stack': {
versions: [],
selectedVersion: '',
libTypes: '',
},
'@xocash/primitives': {
versions: [],
selectedVersion: '',
libTypes: '',
},
});
export const useXOCashLibs = (): XOCashLibs => {
// Get versions from npm registry
const getVersions = async (): Promise<string[]> => {
const response = await fetch('https://registry.npmjs.org/@xocash/stack');
const data = await response.json();
const versions = Object.keys(data.versions);
return versions;
};
const getTypes = async (packageName: string, version: string) => {
const response = await fetch(
`https://cdn.jsdelivr.net/npm/${packageName}@${version}/dist/types-bundled.d.ts`,
);
const content = await response.text();
return content;
};
// Update the types file when the selected version changes.
watch(
() => xocashLibs['@xocash/stack'].selectedVersion,
async (newVersion) => {
const stackTypes = await getTypes('@xocash/stack', newVersion);
const primitivesTypes = await getTypes('@xocash/primitives', newVersion);
// Only set the types file if the selected version has not been changed while fetching the types.
if (xocashLibs['@xocash/stack'].selectedVersion === newVersion) {
xocashLibs['@xocash/stack'].libTypes = stackTypes;
xocashLibs['@xocash/primitives'].libTypes = primitivesTypes;
}
},
);
// Fetch the versions and set the selected version when the component is mounted.
onMounted(async () => {
xocashLibs['@xocash/stack'].versions = await getVersions();
xocashLibs['@xocash/stack'].selectedVersion =
xocashLibs['@xocash/stack'].versions[xocashLibs['@xocash/stack'].versions.length - 1];
});
return xocashLibs;
};
+1 -1
View File
@@ -14,7 +14,7 @@ import Playground from '@theme/components/Playground.vue'
```
// Import the Electrum Blockchain Adapter from XO Stack.
const { BlockchainElectrum } = await import('https://cdn.jsdelivr.net/npm/@xocash/stack@0.0.2-development.95/+esm');
import { BlockchainElectrum } from '@xocash/stack';
// Instantiate and start the Electrum Blockchain Adapter.
const blockchain = new BlockchainElectrum();
+30 -6
View File
@@ -4,6 +4,9 @@ aside: false
<script lang="ts" setup>
import CodeSandbox from '@theme/components/CodeSandbox.vue'
import { useXOCashLibs } from '@theme/utils/useXOCashLibs';
const xocashLibs = useXOCashLibs();
</script>
# Sandbox
@@ -20,10 +23,17 @@ This'll be very useful for:
The below is Address mess.
<!-- NOTE: It is absolutely crucial you leave a blank line before and after the backticks! Otherwise, you'll end up with errors.-->
<CodeSandbox>
<CodeSandbox
:libs="xocashLibs"
height="50em"
editorHeight="30em"
outputHeight="20em"
fullHeight
>
```
const { Address } = await import('https://cdn.jsdelivr.net/npm/@xocash/stack@0.0.2-development.95/+esm');
import { Address } from '@xocash/stack';
const lockscriptHex = Address.from('bitcoincash:qr2084lauctzarqk20kqlhef8gyneqhceqy75wvw8q').toLockscriptHex();
@@ -43,10 +53,17 @@ console.info('yep');
We can have more than one per page too. For example:
<!-- NOTE: It is absolutely crucial you leave a blank line before and after the backticks! Otherwise, you'll end up with errors.-->
<CodeSandbox>
<CodeSandbox
:libs="xocashLibs"
height="50em"
editorHeight="30em"
outputHeight="20em"
fullHeight
>
```
const { BlockchainElectrum } = await import('https://cdn.jsdelivr.net/npm/@xocash/stack@0.0.2-development.95/+esm');
import { BlockchainElectrum } from '@xocash/stack';
const blockchain = new BlockchainElectrum();
@@ -64,10 +81,17 @@ console.log('Transaction Hex', tx.toHex());
The below demonstrates instantiating a wallet.
<!-- NOTE: It is absolutely crucial you leave a blank line before and after the backticks! Otherwise, you'll end up with errors.-->
<CodeSandbox>
<CodeSandbox
:libs="xocashLibs"
height="50em"
editorHeight="30em"
outputHeight="20em"
fullHeight
>
```
const { BlockchainElectrum, WalletP2PKH } = await import('https://cdn.jsdelivr.net/npm/@xocash/stack@0.0.2-development.95/+esm');
import { BlockchainElectrum, WalletP2PKH } from '@xocash/stack';
const wallet = WalletP2PKH.generateRandom({
blockchain: new BlockchainElectrum