236 lines
7.3 KiB
TypeScript
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)
|
||
|
|
}
|
||
|
|
}
|