Skip to content

Commit

Permalink
feat: bundle monaco runnable deps (#1530)
Browse files Browse the repository at this point in the history
Co-authored-by: Anthony Fu <anthonyfu117@hotmail.com>
  • Loading branch information
KermanX and antfu committed Apr 16, 2024
1 parent b2dc044 commit 0f9b47c
Show file tree
Hide file tree
Showing 17 changed files with 128 additions and 93 deletions.
14 changes: 14 additions & 0 deletions docs/custom/config-code-runners.md
Expand Up @@ -54,3 +54,17 @@ export interface CodeRunnerContext {
## Runner Output

The runner can either return a text or HTML output, or an element to be mounted. Refer to https://github.com/slidevjs/slidev/blob/main/packages/types/src/code-runner.ts for more details.

## Additional Runner Dependencies

By default, Slidev will scan the Markdown source and automatically import the necessary dependencies for the code runners. If you want to manually import dependencies, you can use the `monacoRunAdditionalDeps` option in the slide frontmatter:

```yaml
monacoRunAdditionalDeps:
- ./path/to/dependency
- lodash-es
```

::: tip
The paths are resolved relative to the `snippets` directory. And the names of the deps should be exactly the same as imported ones in the code.
:::
2 changes: 2 additions & 0 deletions docs/custom/index.md
Expand Up @@ -49,6 +49,8 @@ monaco: true
monacoTypesSource: local
# explicitly specify extra local packages to import the types for
monacoTypesAdditionalPackages: []
# explicitly specify extra local modules as dependency of monaco runnable
monacoRunAdditionalDeps: []
# download remote assets in local using vite-plugin-remote-assets, can be boolean, 'dev' or 'build'
remoteAssets: false
# controls whether texts in slides are selectable
Expand Down
3 changes: 3 additions & 0 deletions packages/client/constants.ts
Expand Up @@ -61,6 +61,9 @@ export const HEADMATTER_FIELDS = [
'highlighter',
'lineNumbers',
'monaco',
'monacoTypesSource',
'monacoTypesAdditionalPackages',
'monacoRunAdditionalDeps',
'remoteAssets',
'selectable',
'record',
Expand Down
39 changes: 14 additions & 25 deletions packages/client/setup/code-runners.ts
@@ -1,8 +1,9 @@
import { createSingletonPromise, ensurePrefix, slash } from '@antfu/utils'
import type { CodeRunner, CodeRunnerContext, CodeRunnerOutput, CodeRunnerOutputText, CodeRunnerOutputs } from '@slidev/types'
import { createSingletonPromise } from '@antfu/utils'
import type { CodeRunner, CodeRunnerOutput, CodeRunnerOutputText, CodeRunnerOutputs } from '@slidev/types'
import type { CodeToHastOptions } from 'shiki'
import type ts from 'typescript'
import { isDark } from '../logic/dark'
import deps from '#slidev/monaco-run-deps'
import setups from '#slidev/setups/code-runners'

export default createSingletonPromise(async () => {
Expand All @@ -25,18 +26,6 @@ export default createSingletonPromise(async () => {
...options,
})

const resolveId = async (specifier: string) => {
if (!'./'.includes(specifier[0]) && !/^(@[^\/:]+?\/)?[^\/:]+$/.test(specifier))
return specifier // this might be a url or something else
const res = await fetch(`/@slidev/resolve-id?specifier=${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 @@ -47,7 +36,6 @@ export default createSingletonPromise(async () => {
{
options,
highlight,
resolveId,
run: async (code, lang) => {
return await run(code, lang, options)
},
Expand Down Expand Up @@ -85,16 +73,20 @@ async function runJavaScript(code: string): Promise<CodeRunnerOutputs> {
replace.clear = () => allLogs.length = 0
const vmConsole = Object.assign({}, console, replace)
try {
const safeJS = `return async (console) => {
window.console = console
const safeJS = `return async (console, __slidev_import) => {
${sanitizeJS(code)}
}`
// eslint-disable-next-line no-new-func
await (new Function(safeJS)())(vmConsole)
await (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
})
}
catch (error) {
return {
error: `ERROR: ${error}`,
error: String(error),
}
}

Expand Down Expand Up @@ -175,7 +167,7 @@ async function runJavaScript(code: string): Promise<CodeRunnerOutputs> {

let tsModule: typeof import('typescript') | undefined

export async function runTypeScript(code: string, context: CodeRunnerContext) {
export async function runTypeScript(code: string) {
tsModule ??= await import('typescript')

code = tsModule.transpileModule(code, {
Expand All @@ -188,11 +180,8 @@ export async function runTypeScript(code: string, context: CodeRunnerContext) {
},
}).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})`)
const importRegex = /import\s*\((.+)\)/g
code = code.replace(importRegex, (_full, specifier) => `__slidev_import(${specifier})`)

return await runJavaScript(code)
}
Expand Down
1 change: 1 addition & 0 deletions packages/parser/src/config.ts
Expand Up @@ -12,6 +12,7 @@ export function getDefaultConfig(): SlidevConfig {
monaco: true,
monacoTypesSource: 'local',
monacoTypesAdditionalPackages: [],
monacoRunAdditionalDeps: [],
download: false,
export: {} as ResolvedExportOptions,
info: false,
Expand Down
35 changes: 29 additions & 6 deletions packages/parser/src/core.ts
@@ -1,6 +1,6 @@
import YAML from 'yaml'
import { ensurePrefix, objectMap } from '@antfu/utils'
import type { FrontmatterStyle, SlidevFeatureFlags, SlidevMarkdown, SlidevPreparserExtension, SourceSlideInfo } from '@slidev/types'
import { ensurePrefix } from '@antfu/utils'
import type { FrontmatterStyle, SlidevDetectedFeatures, SlidevMarkdown, SlidevPreparserExtension, SourceSlideInfo } from '@slidev/types'

export function stringify(data: SlidevMarkdown) {
return `${data.slides.map(stringifySlide).join('\n').trim()}\n`
Expand Down Expand Up @@ -61,10 +61,10 @@ function matter(code: string) {
}
}

export function detectFeatures(code: string): SlidevFeatureFlags {
export function detectFeatures(code: string): SlidevDetectedFeatures {
return {
katex: !!code.match(/\$.*?\$/) || !!code.match(/\$\$/),
monaco: !!code.match(/{monaco.*}/),
monaco: code.match(/{monaco.*}/) ? scanMonacoReferencedMods(code) : false,
tweet: !!code.match(/<Tweet\b/),
mermaid: !!code.match(/^```mermaid/m),
}
Expand Down Expand Up @@ -186,8 +186,31 @@ export async function parse(
}
}

export function mergeFeatureFlags(a: SlidevFeatureFlags, b: SlidevFeatureFlags): SlidevFeatureFlags {
return objectMap(a, (k, v) => [k, v || b[k]])
function scanMonacoReferencedMods(md: string) {
const types = new Set<string>()
const deps = new Set<string>()
md.replace(
/^```(\w+?)\s*{monaco(.*?)}[\s\n]*([\s\S]+?)^```/mg,
(full, lang = 'ts', kind: string, code: string) => {
lang = lang.trim()
const isDep = kind === '-run'
if (['js', 'javascript', 'ts', 'typescript'].includes(lang)) {
for (const [, , specifier] of code.matchAll(/\s+from\s+(["'])([\/\.\w@-]+)\1/g)) {
if (specifier) {
if (!'./'.includes(specifier))
types.add(specifier) // All local TS files are loaded by globbing
if (isDep)
deps.add(specifier)
}
}
}
return ''
},
)
return {
types: Array.from(types),
deps: Array.from(deps),
}
}

export * from './utils'
Expand Down
23 changes: 0 additions & 23 deletions packages/slidev/node/syntax/transform/monaco.ts
@@ -1,4 +1,3 @@
import { isTruthy } from '@antfu/utils'
import lz from 'lz-string'
import type { MarkdownTransformContext } from '@slidev/types'

Expand Down Expand Up @@ -41,25 +40,3 @@ export function transformMonaco(ctx: MarkdownTransformContext, enabled = true) {
},
)
}

// types auto discovery for TypeScript monaco
export function scanMonacoModules(md: string) {
const typeModules = new Set<string>()

md.replace(
/^```(\w+?)\s*{monaco([\w:,-]*)}[\s\n]*([\s\S]+?)^```/mg,
(full, lang = 'ts', options: string, code: string) => {
options = options || ''
lang = lang.trim()
if (lang === 'ts' || lang === 'typescript') {
Array.from(code.matchAll(/\s+from\s+(["'])([\/\w@-]+)\1/g))
.map(i => i[2])
.filter(isTruthy)
.map(i => typeModules.add(i))
}
return ''
},
)

return Array.from(typeModules)
}
9 changes: 6 additions & 3 deletions packages/slidev/node/syntax/transform/snippet.ts
Expand Up @@ -3,6 +3,7 @@
import path from 'node:path'
import fs from 'fs-extra'
import type { MarkdownTransformContext, ResolvedSlidevOptions } from '@slidev/types'
import { slash } from '@antfu/utils'

function dedent(text: string): string {
const lines = text.split('\n')
Expand Down Expand Up @@ -94,9 +95,11 @@ export function transformSnippet(ctx: MarkdownTransformContext, options: Resolve
(full, filepath = '', regionName = '', lang = '', meta = '') => {
const firstLine = `\`\`\`${lang || path.extname(filepath).slice(1)} ${meta}`

const src = /^\@[\/]/.test(filepath)
? path.resolve(options.userRoot, filepath.slice(2))
: path.resolve(dir, filepath)
const src = slash(
/^\@[\/]/.test(filepath)
? path.resolve(options.userRoot, filepath.slice(2))
: path.resolve(dir, filepath),
)

data.watchFiles.push(src)

Expand Down
2 changes: 2 additions & 0 deletions packages/slidev/node/virtual/index.ts
Expand Up @@ -2,6 +2,7 @@ import { templateConfigs } from './configs'
import { templateLegacyRoutes, templateLegacyTitles } from './deprecated'
import { templateGlobalBottom, templateGlobalTop, templateNavControls } from './global-components'
import { templateLayouts } from './layouts'
import { templateMonacoRunDeps } from './monaco-deps'
import { templateMonacoTypes } from './monaco-types'
import { templateSetups } from './setups'
import { templateShiki } from './shiki'
Expand All @@ -12,6 +13,7 @@ import { templateTitleRenderer, templateTitleRendererMd } from './titles'
export const templates = [
templateShiki,
templateMonacoTypes,
templateMonacoRunDeps,
templateConfigs,
templateStyle,
templateGlobalBottom,
Expand Down
27 changes: 27 additions & 0 deletions packages/slidev/node/virtual/monaco-deps.ts
@@ -0,0 +1,27 @@
import { resolve } from 'node:path'
import { uniq } from '@antfu/utils'
import type { VirtualModuleTemplate } from './types'

export const templateMonacoRunDeps: VirtualModuleTemplate = {
id: '/@slidev/monaco-run-deps',
getContent: async ({ userRoot, data }, _ctx, pluginCtx) => {
if (!data.features.monaco)
return ''
const deps = uniq(data.features.monaco.deps.concat(data.config.monacoTypesAdditionalPackages))
const importerPath = resolve(userRoot, './snippets/__importer__.ts')
let result = ''
for (let i = 0; i < deps.length; i++) {
const specifier = deps[i]
const resolved = await pluginCtx.resolve(specifier, importerPath)
if (!resolved)
continue
result += `import * as vendored${i} from ${JSON.stringify(resolved.id)}\n`
}
result += 'export default {\n'
for (let i = 0; i < deps.length; i++)
result += `${JSON.stringify(deps[i])}: vendored${i},\n`

result += '}\n'
return result
},
}
5 changes: 3 additions & 2 deletions packages/slidev/node/virtual/monaco-types.ts
Expand Up @@ -4,12 +4,13 @@ import { join, resolve } from 'node:path'
import fg from 'fast-glob'
import { uniq } from '@antfu/utils'
import { toAtFS } from '../resolver'
import { scanMonacoModules } from '../syntax/transform/monaco'
import type { VirtualModuleTemplate } from './types'

export const templateMonacoTypes: VirtualModuleTemplate = {
id: '/@slidev/monaco-types',
getContent: async ({ userRoot, data }) => {
if (!data.features.monaco)
return ''
const typesRoot = join(userRoot, 'snippets')
const files = await fg(['**/*.ts', '**/*.mts', '**/*.cts'], { cwd: typesRoot })
let result = 'import { addFile } from "@slidev/client/setup/monaco.ts"\n'
Expand All @@ -23,7 +24,7 @@ export const templateMonacoTypes: VirtualModuleTemplate = {
// Dependencies
const deps = [...data.config.monacoTypesAdditionalPackages]
if (data.config.monacoTypesSource === 'local')
deps.push(...scanMonacoModules(data.slides.map(s => s.source.raw).join()))
deps.push(...data.features.monaco.types)

// Copied from https://github.com/microsoft/TypeScript-Website/blob/v2/packages/ata/src/edgeCases.ts
// Converts some of the known global imports to node so that we grab the right info
Expand Down
3 changes: 2 additions & 1 deletion packages/slidev/node/virtual/types.ts
@@ -1,10 +1,11 @@
import type { Awaitable } from '@antfu/utils'
import type MarkdownIt from 'markdown-it'
import type { PluginContext } from 'rollup'
import type { ResolvedSlidevOptions } from '@slidev/types'

export interface VirtualModuleTemplate {
id: string
getContent: (options: ResolvedSlidevOptions, ctx: VirtualModuleTempalteContext) => Awaitable<string>
getContent: (options: ResolvedSlidevOptions, ctx: VirtualModuleTempalteContext, pluginCtx: PluginContext) => Awaitable<string>
}

export interface VirtualModuleTempalteContext {
Expand Down

0 comments on commit 0f9b47c

Please sign in to comment.