diff --git a/demo/starter/slides.md b/demo/starter/slides.md index 4f692c6e67..dbc604d3a1 100644 --- a/demo/starter/slides.md +++ b/demo/starter/slides.md @@ -576,20 +576,21 @@ Add `{monaco}` to the code block to turn it into an editor: import { ref } from 'vue' import hello from './external' -const code = ref('const a = 1') -hello() +const code = ref(hello()) ``` Use `{monaco-run}` to create an editor that can execute the code directly in the slide: ```ts {monaco-run} +import { version } from 'vue' + function fibonacci(n: number): number { return n <= 1 ? n : fibonacci(n - 1) + fibonacci(n - 2) // you know, this is NOT the best way to do it :P } -console.log(Array.from({ length: 10 }, (_, i) => fibonacci(i + 1))) +console.log(version, Array.from({ length: 10 }, (_, i) => fibonacci(i + 1))) ``` --- diff --git a/packages/client/setup/code-runners.ts b/packages/client/setup/code-runners.ts index 992aedfeda..c201fe5d97 100644 --- a/packages/client/setup/code-runners.ts +++ b/packages/client/setup/code-runners.ts @@ -1,13 +1,14 @@ -import { createSingletonPromise } from '@antfu/utils' +import { createSingletonPromise, ensurePrefix, slash } from '@antfu/utils' import type { CodeRunner, CodeRunnerContext, CodeRunnerOutput, CodeRunnerOutputText, CodeRunnerOutputs } from '@slidev/types' import type { CodeToHastOptions } from 'shiki' +import type ts from 'typescript' import { isDark } from '../logic/dark' import setups from '#slidev/setups/code-runners' export default createSingletonPromise(async () => { const runners: Record = { - javascript: runJavaScript, - js: runJavaScript, + javascript: runTypeScript, + js: runTypeScript, typescript: runTypeScript, ts: runTypeScript, } @@ -24,6 +25,18 @@ export default createSingletonPromise(async () => { ...options, }) + const resolveId = async (specifier: string) => { + if (!/^(@[^\/:]+?\/)?[^\/:]+$/.test(specifier)) + return specifier + const res = await fetch(`/@slidev/resolve-id/${specifier}`) + if (!res.ok) + return null + const id = await res.text() + if (!id) + return null + return `/@fs${ensurePrefix('/', slash(id))}` + } + const run = async (code: string, lang: string, options: Record): Promise => { try { const runner = runners[lang] @@ -34,6 +47,7 @@ export default createSingletonPromise(async () => { { options, highlight, + resolveId, run: async (code, lang) => { return await run(code, lang, options) }, @@ -60,7 +74,7 @@ export default createSingletonPromise(async () => { }) // Ported from https://github.com/microsoft/TypeScript-Website/blob/v2/packages/playground/src/sidebar/runtime.ts -export async function runJavaScript(code: string): Promise { +async function runJavaScript(code: string): Promise { const allLogs: CodeRunnerOutput[] = [] const replace = {} as any @@ -159,10 +173,80 @@ export async function runJavaScript(code: string): Promise { let tsModule: typeof import('typescript') | undefined export async function runTypeScript(code: string, context: CodeRunnerContext) { - const { transpile } = tsModule ??= await import('typescript') - code = transpile(code, { - module: tsModule.ModuleKind.ESNext, - target: tsModule.ScriptTarget.ES2022, - }) - return await context.run(code, 'javascript') + tsModule ??= await import('typescript') + + code = tsModule.transpileModule(code, { + compilerOptions: { + module: tsModule.ModuleKind.ESNext, + target: tsModule.ScriptTarget.ES2022, + }, + transformers: { + after: [transfromImports], + }, + }).outputText + + const importRegex = /import\s*\(\s*(['"])(.+?)['"]\s*\)/g + const idMap: Record = {} + for (const [,,specifier] of code.matchAll(importRegex)!) + idMap[specifier] = await context.resolveId(specifier) ?? specifier + code = code.replace(importRegex, (_full, quote, specifier) => `import(${quote}${idMap[specifier] ?? specifier}${quote})`) + + return await runJavaScript(code) +} + +/** + * Transform import statements to dynamic imports + */ +function transfromImports(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) + } } diff --git a/packages/slidev/node/vite/loaders.ts b/packages/slidev/node/vite/loaders.ts index a85bf053be..55f84d185d 100644 --- a/packages/slidev/node/vite/loaders.ts +++ b/packages/slidev/node/vite/loaders.ts @@ -172,6 +172,18 @@ export function createSlidesLoader( next() }) + + server.middlewares.use(async (req, res, next) => { + const match = req.url?.match(/^\/\@slidev\/resolve-id\/(.*)$/) + if (!match) + return next() + + const [, specifier] = match + const resolved = await server!.pluginContainer.resolveId(specifier) + res.statusCode = 200 + res.write(resolved?.id ?? '') + return res.end() + }) }, async handleHotUpdate(ctx) { diff --git a/packages/types/src/code-runner.ts b/packages/types/src/code-runner.ts index 589c0c0ca9..e885e6feef 100644 --- a/packages/types/src/code-runner.ts +++ b/packages/types/src/code-runner.ts @@ -10,6 +10,10 @@ export interface CodeRunnerContext { * Highlight code with shiki. */ highlight: (code: string, lang: string, options?: Partial) => Promise + /** + * Resolve the import path of a module. + */ + resolveId: (specifer: string) => Promise /** * Use (other) code runner to run code. */