| 1 | import type { CodeRunner, CodeRunnerOutput, CodeRunnerOutputs, CodeRunnerOutputText } from '@slidev/types' |
| 2 | import type { CodeToHastOptions } from 'shiki' |
| 3 | import type ts from 'typescript' |
| 4 | import { createSingletonPromise } from '@antfu/utils' |
| 5 | import { ref } from 'vue' |
| 6 | import deps from '#slidev/monaco-run-deps' |
| 7 | import setups from '#slidev/setups/code-runners' |
| 8 | import { configs } from '../env' |
| 9 | |
| 10 | export default createSingletonPromise(async () => { |
| 11 | const runners: Record<string, CodeRunner> = { |
| 12 | javascript: runTypeScript, |
| 13 | js: runTypeScript, |
| 14 | typescript: runTypeScript, |
| 15 | ts: runTypeScript, |
| 16 | } |
| 17 | |
| 18 | const { defaultHighlightOptions, getEagerHighlighter } = await (await import('./shiki')).default() |
| 19 | |
| 20 | const highlighter = await getEagerHighlighter() |
| 21 | const highlight = (code: string, lang: string, options?: Partial<CodeToHastOptions>) => { |
| 22 | return highlighter.codeToHtml(code, { |
| 23 | ...defaultHighlightOptions, |
| 24 | lang, |
| 25 | ...options, |
| 26 | }) |
| 27 | } |
| 28 | |
| 29 | const run = async (code: string, lang: string, options: Record<string, unknown>): Promise<CodeRunnerOutputs> => { |
| 30 | try { |
| 31 | const runner = runners[lang] |
| 32 | if (!runner) |
| 33 | throw new Error(`Runner for language "${lang}" not found`) |
| 34 | return await runner( |
| 35 | code, |
| 36 | { |
| 37 | options, |
| 38 | highlight, |
| 39 | run: async (code, lang) => { |
| 40 | return await run(code, lang, options) |
| 41 | }, |
| 42 | }, |
| 43 | ) |
| 44 | } |
| 45 | catch (e) { |
| 46 | console.error(e) |
| 47 | return { |
| 48 | error: `${e}`, |
| 49 | } |
| 50 | } |
| 51 | } |
| 52 | |
| 53 | for (const setup of setups) { |
| 54 | const result = await setup(runners) |
| 55 | Object.assign(runners, result) |
| 56 | } |
| 57 | |
| 58 | return { |
| 59 | highlight, |
| 60 | run, |
| 61 | } |
| 62 | }) |
| 63 | |
| 64 | // Ported from https://github.com/microsoft/TypeScript-Website/blob/v2/packages/playground/src/sidebar/runtime.ts |
| 65 | function runJavaScript(code: string): CodeRunnerOutputs { |
| 66 | const result = ref<CodeRunnerOutput[]>([]) |
| 67 | |
| 68 | const onError = (error: any) => result.value.push({ error: String(error) }) |
| 69 | const logger = (...objs: any[]) => result.value.push(objs.map(printObject)) |
| 70 | const vmConsole = Object.assign({}, console) |
| 71 | vmConsole.info = vmConsole.log = vmConsole.debug = vmConsole.warn = vmConsole.error = logger |
| 72 | vmConsole.clear = () => result.value.length = 0 |
| 73 | try { |
| 74 | const wrappedCode = `return async (console, __slidev_import, __slidev_on_error) => { |
| 75 | ${configs.monacoRunUseStrict ? `"use strict";` : ''} |
| 76 | try { |
| 77 | ${fixupCode(code)} |
| 78 | } catch (e) { |
| 79 | __slidev_on_error(e) |
| 80 | } |
| 81 | }` |
| 82 | // eslint-disable-next-line no-new-func |
| 83 | ;(new Function(wrappedCode)())(vmConsole, (specifier: string) => { |
| 84 | const mod = deps[specifier] |
| 85 | if (!mod) |
| 86 | 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`) |
| 87 | return mod |
| 88 | }, onError) |
| 89 | } |
| 90 | catch (error) { |
| 91 | onError(error) |
| 92 | } |
| 93 | |
| 94 | function printObject(arg: any): CodeRunnerOutputText { |
| 95 | if (typeof arg === 'string') { |
| 96 | return { |
| 97 | text: arg, |
| 98 | } |
| 99 | } |
| 100 | return { |
| 101 | text: objectToText(arg), |
| 102 | highlightLang: 'javascript', |
| 103 | } |
| 104 | } |
| 105 | |
| 106 | function objectToText(arg: any): string { |
| 107 | let textRep = '' |
| 108 | if (arg instanceof Error) { |
| 109 | textRep = `Error: ${JSON.stringify(arg.message)}` |
| 110 | } |
| 111 | else if (arg === null || arg === undefined || typeof arg === 'symbol') { |
| 112 | textRep = String(arg) |
| 113 | } |
| 114 | else if (Array.isArray(arg)) { |
| 115 | textRep = `[${arg.map(objectToText).join(', ')}]` |
| 116 | } |
| 117 | else if (arg instanceof Set) { |
| 118 | const setIter = [...arg] |
| 119 | textRep = `Set (${arg.size}) {${setIter.map(objectToText).join(', ')}}` |
| 120 | } |
| 121 | else if (arg instanceof Map) { |
| 122 | const mapIter = [...arg.entries()] |
| 123 | textRep |
| 124 | = `Map (${arg.size}) {${mapIter |
| 125 | .map(([k, v]) => `${objectToText(k)} => ${objectToText(v)}`) |
| 126 | .join(', ') |
| 127 | }}` |
| 128 | } |
| 129 | else if (arg instanceof RegExp) { |
| 130 | textRep = arg.toString() |
| 131 | } |
| 132 | else if (typeof arg === 'string') { |
| 133 | textRep = JSON.stringify(arg) |
| 134 | } |
| 135 | else if (typeof arg === 'object') { |
| 136 | const name = arg.constructor?.name ?? '' |
| 137 | // No one needs to know an obj is an obj |
| 138 | const nameWithoutObject = name && name === 'Object' ? '' : name |
| 139 | const prefix = nameWithoutObject ? `${nameWithoutObject}: ` : '' |
| 140 | |
| 141 | // 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 |
| 142 | textRep |
| 143 | = prefix |
| 144 | + JSON.stringify(arg, (_, value) => (value === undefined ? '__undefined__' : value), 2).replace( |
| 145 | /"__undefined__"/g, |
| 146 | 'undefined', |
| 147 | ) |
| 148 | |
| 149 | textRep = String(textRep) |
| 150 | } |
| 151 | else { |
| 152 | textRep = String(arg) |
| 153 | } |
| 154 | return textRep |
| 155 | } |
| 156 | |
| 157 | function fixupCode(code: string) { |
| 158 | // The reflect-metadata runtime is available, so allow that to go through |
| 159 | code = code.replace(`import "reflect-metadata"`, '').replace(`require("reflect-metadata")`, '') |
| 160 | // Transpiled typescript sometimes contains an empty export, remove it. |
| 161 | code = code.replace('export {};', '') |
| 162 | |
| 163 | return code |
| 164 | } |
| 165 | |
| 166 | return result |
| 167 | } |
| 168 | |
| 169 | let tsModule: typeof import('typescript') | undefined |
| 170 | |
| 171 | export async function runTypeScript(code: string) { |
| 172 | tsModule ??= await import('typescript') |
| 173 | |
| 174 | code = tsModule.transpileModule(code, { |
| 175 | compilerOptions: { |
| 176 | module: tsModule.ModuleKind.ESNext, |
| 177 | target: tsModule.ScriptTarget.ES2022, |
| 178 | }, |
| 179 | transformers: { |
| 180 | after: [transformImports], |
| 181 | }, |
| 182 | }).outputText |
| 183 | |
| 184 | const importRegex = /\bimport\s*\((.+)\)/g |
| 185 | code = code.replace(importRegex, (_full, specifier) => `__slidev_import(${specifier})`) |
| 186 | |
| 187 | return runJavaScript(code) |
| 188 | } |
| 189 | |
| 190 | /** |
| 191 | * Transform import statements to dynamic imports |
| 192 | */ |
| 193 | function transformImports(context: ts.TransformationContext): ts.Transformer<ts.SourceFile> { |
| 194 | const { factory } = context |
| 195 | const { isImportDeclaration, isNamedImports, NodeFlags } = tsModule! |
| 196 | return (sourceFile: ts.SourceFile) => { |
| 197 | const statements = [...sourceFile.statements] |
| 198 | for (let i = 0; i < statements.length; i++) { |
| 199 | const statement = statements[i] |
| 200 | if (!isImportDeclaration(statement)) |
| 201 | continue |
| 202 | let bindingPattern: ts.ObjectBindingPattern | ts.Identifier |
| 203 | const namedBindings = statement.importClause?.namedBindings |
| 204 | const bindings: ts.BindingElement[] = [] |
| 205 | if (statement.importClause?.name) |
| 206 | bindings.push(factory.createBindingElement(undefined, factory.createIdentifier('default'), statement.importClause.name)) |
| 207 | if (namedBindings) { |
| 208 | if (isNamedImports(namedBindings)) { |
| 209 | for (const specifier of namedBindings.elements) |
| 210 | bindings.push(factory.createBindingElement(undefined, specifier.propertyName, specifier.name)) |
| 211 | bindingPattern = factory.createObjectBindingPattern(bindings) |
| 212 | } |
| 213 | else { |
| 214 | bindingPattern = factory.createIdentifier(namedBindings.name.text) |
| 215 | } |
| 216 | } |
| 217 | else { |
| 218 | bindingPattern = factory.createObjectBindingPattern(bindings) |
| 219 | } |
| 220 | |
| 221 | const newStatement = factory.createVariableStatement( |
| 222 | undefined, |
| 223 | factory.createVariableDeclarationList( |
| 224 | [ |
| 225 | factory.createVariableDeclaration( |
| 226 | bindingPattern, |
| 227 | undefined, |
| 228 | undefined, |
| 229 | factory.createAwaitExpression( |
| 230 | factory.createCallExpression( |
| 231 | factory.createIdentifier('import'), |
| 232 | undefined, |
| 233 | [statement.moduleSpecifier], |
| 234 | ), |
| 235 | ), |
| 236 | ), |
| 237 | ], |
| 238 | NodeFlags.Const, |
| 239 | ), |
| 240 | ) |
| 241 | statements[i] = newStatement |
| 242 | } |
| 243 | return factory.updateSourceFile(sourceFile, statements) |
| 244 | } |
| 245 | } |
| 246 |