Resolve "Monaco Playground Typing and Rollup Bundles"
This commit is contained in:
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user