Skip to content

Commit

Permalink
feat: allow importing local packages in runnable code editor (#1502)
Browse files Browse the repository at this point in the history
  • Loading branch information
KermanX committed Apr 8, 2024
1 parent 745f614 commit 0c0ecf8
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 13 deletions.
7 changes: 4 additions & 3 deletions demo/starter/slides.md
Expand Up @@ -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)))
```

---
Expand Down
104 changes: 94 additions & 10 deletions 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<string, CodeRunner> = {
javascript: runJavaScript,
js: runJavaScript,
javascript: runTypeScript,
js: runTypeScript,
typescript: runTypeScript,
ts: runTypeScript,
}
Expand All @@ -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<string, unknown>): Promise<CodeRunnerOutputs> => {
try {
const runner = runners[lang]
Expand All @@ -34,6 +47,7 @@ export default createSingletonPromise(async () => {
{
options,
highlight,
resolveId,
run: async (code, lang) => {
return await run(code, lang, options)
},
Expand All @@ -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<CodeRunnerOutputs> {
async function runJavaScript(code: string): Promise<CodeRunnerOutputs> {
const allLogs: CodeRunnerOutput[] = []

const replace = {} as any
Expand Down Expand Up @@ -159,10 +173,80 @@ export async function runJavaScript(code: string): Promise<CodeRunnerOutputs> {
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<string, string> = {}
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<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)
}
}
12 changes: 12 additions & 0 deletions packages/slidev/node/vite/loaders.ts
Expand Up @@ -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) {
Expand Down
4 changes: 4 additions & 0 deletions packages/types/src/code-runner.ts
Expand Up @@ -10,6 +10,10 @@ export interface CodeRunnerContext {
* Highlight code with shiki.
*/
highlight: (code: string, lang: string, options?: Partial<CodeToHastOptions>) => Promise<string>
/**
* Resolve the import path of a module.
*/
resolveId: (specifer: string) => Promise<string | null>
/**
* Use (other) code runner to run code.
*/
Expand Down

0 comments on commit 0c0ecf8

Please sign in to comment.