返回 slidev
code-runners.ts
根目录 / packages / client / setup / code-runners.ts
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
246 lines TYPESCRIPT