import type { CodeRunner, CodeRunnerOutput, CodeRunnerOutputs, CodeRunnerOutputText } from '@slidev/types' import type { CodeToHastOptions } from 'shiki' import type ts from 'typescript' import { createSingletonPromise } from '@antfu/utils' import { ref } from 'vue' import deps from '#slidev/monaco-run-deps' import setups from '#slidev/setups/code-runners' import { configs } from '../env' export default createSingletonPromise(async () => { const runners: Record = { javascript: runTypeScript, js: runTypeScript, typescript: runTypeScript, ts: runTypeScript, } const { defaultHighlightOptions, getEagerHighlighter } = await (await import('./shiki')).default() const highlighter = await getEagerHighlighter() const highlight = (code: string, lang: string, options?: Partial) => { return highlighter.codeToHtml(code, { ...defaultHighlightOptions, lang, ...options, }) } const run = async (code: string, lang: string, options: Record): Promise => { try { const runner = runners[lang] if (!runner) throw new Error(`Runner for language "${lang}" not found`) return await runner( code, { options, highlight, run: async (code, lang) => { return await run(code, lang, options) }, }, ) } catch (e) { console.error(e) return { error: `${e}`, } } } for (const setup of setups) { const result = await setup(runners) Object.assign(runners, result) } return { highlight, run, } }) // Ported from https://github.com/microsoft/TypeScript-Website/blob/v2/packages/playground/src/sidebar/runtime.ts function runJavaScript(code: string): CodeRunnerOutputs { const result = ref([]) const onError = (error: any) => result.value.push({ error: String(error) }) const logger = (...objs: any[]) => result.value.push(objs.map(printObject)) const vmConsole = Object.assign({}, console) vmConsole.info = vmConsole.log = vmConsole.debug = vmConsole.warn = vmConsole.error = logger vmConsole.clear = () => result.value.length = 0 try { const wrappedCode = `return async (console, __slidev_import, __slidev_on_error) => { ${configs.monacoRunUseStrict ? `"use strict";` : ''} try { ${fixupCode(code)} } catch (e) { __slidev_on_error(e) } }` // eslint-disable-next-line no-new-func ;(new Function(wrappedCode)())(vmConsole, (specifier: string) => { const mod = deps[specifier] if (!mod) throw new Error(`Module not found: ${specifier}.\nAvailable modules: ${Object.keys(deps).join(', ')}. Please refer to https://sli.dev/custom/config-code-runners#additional-runner-dependencies`) return mod }, onError) } catch (error) { onError(error) } function printObject(arg: any): CodeRunnerOutputText { if (typeof arg === 'string') { return { text: arg, } } return { text: objectToText(arg), highlightLang: 'javascript', } } function objectToText(arg: any): string { let textRep = '' if (arg instanceof Error) { textRep = `Error: ${JSON.stringify(arg.message)}` } else if (arg === null || arg === undefined || typeof arg === 'symbol') { textRep = String(arg) } else if (Array.isArray(arg)) { textRep = `[${arg.map(objectToText).join(', ')}]` } else if (arg instanceof Set) { const setIter = [...arg] textRep = `Set (${arg.size}) {${setIter.map(objectToText).join(', ')}}` } else if (arg instanceof Map) { const mapIter = [...arg.entries()] textRep = `Map (${arg.size}) {${mapIter .map(([k, v]) => `${objectToText(k)} => ${objectToText(v)}`) .join(', ') }}` } else if (arg instanceof RegExp) { textRep = arg.toString() } else if (typeof arg === 'string') { textRep = JSON.stringify(arg) } else if (typeof arg === 'object') { const name = arg.constructor?.name ?? '' // No one needs to know an obj is an obj const nameWithoutObject = name && name === 'Object' ? '' : name const prefix = nameWithoutObject ? `${nameWithoutObject}: ` : '' // JSON.stringify omits any keys with a value of undefined. To get around this, we replace undefined with the text __undefined__ and then do a global replace using regex back to keyword undefined textRep = prefix + JSON.stringify(arg, (_, value) => (value === undefined ? '__undefined__' : value), 2).replace( /"__undefined__"/g, 'undefined', ) textRep = String(textRep) } else { textRep = String(arg) } return textRep } function fixupCode(code: string) { // The reflect-metadata runtime is available, so allow that to go through code = code.replace(`import "reflect-metadata"`, '').replace(`require("reflect-metadata")`, '') // Transpiled typescript sometimes contains an empty export, remove it. code = code.replace('export {};', '') return code } return result } let tsModule: typeof import('typescript') | undefined export async function runTypeScript(code: string) { tsModule ??= await import('typescript') code = tsModule.transpileModule(code, { compilerOptions: { module: tsModule.ModuleKind.ESNext, target: tsModule.ScriptTarget.ES2022, }, transformers: { after: [transformImports], }, }).outputText const importRegex = /\bimport\s*\((.+)\)/g code = code.replace(importRegex, (_full, specifier) => `__slidev_import(${specifier})`) return runJavaScript(code) } /** * Transform import statements to dynamic imports */ function transformImports(context: ts.TransformationContext): ts.Transformer { const { factory } = context const { isImportDeclaration, isNamedImports, NodeFlags } = tsModule! return (sourceFile: ts.SourceFile) => { const statements = [...sourceFile.statements] for (let i = 0; i < statements.length; i++) { const statement = statements[i] if (!isImportDeclaration(statement)) continue let bindingPattern: ts.ObjectBindingPattern | ts.Identifier const namedBindings = statement.importClause?.namedBindings const bindings: ts.BindingElement[] = [] if (statement.importClause?.name) bindings.push(factory.createBindingElement(undefined, factory.createIdentifier('default'), statement.importClause.name)) if (namedBindings) { if (isNamedImports(namedBindings)) { for (const specifier of namedBindings.elements) bindings.push(factory.createBindingElement(undefined, specifier.propertyName, specifier.name)) bindingPattern = factory.createObjectBindingPattern(bindings) } else { bindingPattern = factory.createIdentifier(namedBindings.name.text) } } else { bindingPattern = factory.createObjectBindingPattern(bindings) } const newStatement = factory.createVariableStatement( undefined, factory.createVariableDeclarationList( [ factory.createVariableDeclaration( bindingPattern, undefined, undefined, factory.createAwaitExpression( factory.createCallExpression( factory.createIdentifier('import'), undefined, [statement.moduleSpecifier], ), ), ), ], NodeFlags.Const, ), ) statements[i] = newStatement } return factory.updateSourceFile(sourceFile, statements) } }