2026-05-31 13:54:31 +02:00

236 lines
7.3 KiB
TypeScript

import type { CodeRunner, CodeRunnerOutput, CodeRunnerOutputs, CodeRunnerOutputText } from '@slidev/types'
import type ts from 'typescript'
import deps from '#slidev/monaco-run-deps'
import setups from '#slidev/setups/code-runners'
import { createSingletonPromise } from '@antfu/utils'
import { ref } from 'vue'
export default createSingletonPromise(async () => {
const runners: Record<string, CodeRunner> = {
javascript: runTypeScript,
js: runTypeScript,
typescript: runTypeScript,
ts: runTypeScript,
}
const { getHighlighter } = await import('#slidev/shiki')
const highlight = await getHighlighter()
const run = async (code: string, lang: string, options: Record<string, unknown>): Promise<CodeRunnerOutputs> => {
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<CodeRunnerOutput[]>([])
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 safeJS = `return async (console, __slidev_import, __slidev_on_error) => {
try {
${sanitizeJS(code)}
} catch (e) {
__slidev_on_error(e)
}
}`
// eslint-disable-next-line no-new-func
;(new Function(safeJS)())(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 sanitizeJS(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 = /import\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<ts.SourceFile> {
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)
}
}