diff --git a/docs/custom/config-code-runners.md b/docs/custom/config-code-runners.md index fa7484cf1d..d1108664b3 100644 --- a/docs/custom/config-code-runners.md +++ b/docs/custom/config-code-runners.md @@ -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. +::: diff --git a/docs/custom/index.md b/docs/custom/index.md index 54e8725b8e..1eaaa9533e 100644 --- a/docs/custom/index.md +++ b/docs/custom/index.md @@ -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 diff --git a/packages/client/constants.ts b/packages/client/constants.ts index 8f5caa8c90..0284c72506 100644 --- a/packages/client/constants.ts +++ b/packages/client/constants.ts @@ -61,6 +61,9 @@ export const HEADMATTER_FIELDS = [ 'highlighter', 'lineNumbers', 'monaco', + 'monacoTypesSource', + 'monacoTypesAdditionalPackages', + 'monacoRunAdditionalDeps', 'remoteAssets', 'selectable', 'record', diff --git a/packages/client/setup/code-runners.ts b/packages/client/setup/code-runners.ts index f297353936..3553bdf623 100644 --- a/packages/client/setup/code-runners.ts +++ b/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 () => { @@ -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): Promise => { try { const runner = runners[lang] @@ -47,7 +36,6 @@ export default createSingletonPromise(async () => { { options, highlight, - resolveId, run: async (code, lang) => { return await run(code, lang, options) }, @@ -85,16 +73,20 @@ async function runJavaScript(code: string): Promise { 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), } } @@ -175,7 +167,7 @@ async function runJavaScript(code: string): Promise { 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, { @@ -188,11 +180,8 @@ export async function runTypeScript(code: string, context: CodeRunnerContext) { }, }).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})`) + const importRegex = /import\s*\((.+)\)/g + code = code.replace(importRegex, (_full, specifier) => `__slidev_import(${specifier})`) return await runJavaScript(code) } diff --git a/packages/parser/src/config.ts b/packages/parser/src/config.ts index df2e644730..6e3c26032b 100644 --- a/packages/parser/src/config.ts +++ b/packages/parser/src/config.ts @@ -12,6 +12,7 @@ export function getDefaultConfig(): SlidevConfig { monaco: true, monacoTypesSource: 'local', monacoTypesAdditionalPackages: [], + monacoRunAdditionalDeps: [], download: false, export: {} as ResolvedExportOptions, info: false, diff --git a/packages/parser/src/core.ts b/packages/parser/src/core.ts index 287559e025..d687b01d41 100644 --- a/packages/parser/src/core.ts +++ b/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` @@ -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(/ [k, v || b[k]]) +function scanMonacoReferencedMods(md: string) { + const types = new Set() + const deps = new Set() + 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' diff --git a/packages/slidev/node/syntax/transform/monaco.ts b/packages/slidev/node/syntax/transform/monaco.ts index 093a1756c5..5cb4d4cd8a 100644 --- a/packages/slidev/node/syntax/transform/monaco.ts +++ b/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' @@ -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() - - 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) -} diff --git a/packages/slidev/node/syntax/transform/snippet.ts b/packages/slidev/node/syntax/transform/snippet.ts index 0a05e18066..babe7e2606 100644 --- a/packages/slidev/node/syntax/transform/snippet.ts +++ b/packages/slidev/node/syntax/transform/snippet.ts @@ -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') @@ -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) diff --git a/packages/slidev/node/virtual/index.ts b/packages/slidev/node/virtual/index.ts index 080be10f2d..462bb73302 100644 --- a/packages/slidev/node/virtual/index.ts +++ b/packages/slidev/node/virtual/index.ts @@ -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' @@ -12,6 +13,7 @@ import { templateTitleRenderer, templateTitleRendererMd } from './titles' export const templates = [ templateShiki, templateMonacoTypes, + templateMonacoRunDeps, templateConfigs, templateStyle, templateGlobalBottom, diff --git a/packages/slidev/node/virtual/monaco-deps.ts b/packages/slidev/node/virtual/monaco-deps.ts new file mode 100644 index 0000000000..dddac4a171 --- /dev/null +++ b/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 + }, +} diff --git a/packages/slidev/node/virtual/monaco-types.ts b/packages/slidev/node/virtual/monaco-types.ts index 06eb448bd3..c98adc90dd 100644 --- a/packages/slidev/node/virtual/monaco-types.ts +++ b/packages/slidev/node/virtual/monaco-types.ts @@ -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' @@ -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 diff --git a/packages/slidev/node/virtual/types.ts b/packages/slidev/node/virtual/types.ts index 1a38b63127..256761d873 100644 --- a/packages/slidev/node/virtual/types.ts +++ b/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 + getContent: (options: ResolvedSlidevOptions, ctx: VirtualModuleTempalteContext, pluginCtx: PluginContext) => Awaitable } export interface VirtualModuleTempalteContext { diff --git a/packages/slidev/node/vite/loaders.ts b/packages/slidev/node/vite/loaders.ts index ddf1d6cc9d..9ced0e0053 100644 --- a/packages/slidev/node/vite/loaders.ts +++ b/packages/slidev/node/vite/loaders.ts @@ -2,7 +2,6 @@ import path from 'node:path' import type { Connect, HtmlTagDescriptor, ModuleNode, Plugin, Update, ViteDevServer } from 'vite' import { isString, isTruthy, notNullish, range } from '@antfu/utils' import fg from 'fast-glob' -import fs from 'fs-extra' import Markdown from 'markdown-it' import { bold, gray, red, yellow } from 'kolorist' @@ -20,6 +19,8 @@ import type { VirtualModuleTempalteContext } from '../virtual/types' import { templateTitleRendererMd } from '../virtual/titles' import { VIRTUAL_SLIDE_PREFIX, templateSlides } from '../virtual/slides' import { templateConfigs } from '../virtual/configs' +import { templateMonacoRunDeps } from '../virtual/monaco-deps' +import { templateMonacoTypes } from '../virtual/monaco-types' const regexId = /^\/\@slidev\/slide\/(\d+)\.(md|json)(?:\?import)?$/ const regexIdQuery = /(\d+?)\.(md|json|frontmatter)$/ @@ -105,7 +106,7 @@ export function createSlidesLoader( let skipHmr: { filePath: string, fileContent: string } | null = null - const { data, clientRoot, userRoot, roots, mode } = options + const { data, clientRoot, roots, mode } = options const templateCtx: VirtualModuleTempalteContext = { md, @@ -194,20 +195,6 @@ export function createSlidesLoader( next() }) - - const snippetsPath = path.resolve(userRoot, 'snippets/__importer__.ts') - - server.middlewares.use(async (req, res, next) => { - const match = req.url?.match(/^\/\@slidev\/resolve-id\?specifier=(.*)$/) - if (!match) - return next() - - const [, specifier] = match - const resolved = await server!.pluginContainer.resolveId(specifier, snippetsPath) - res.statusCode = 200 - res.write(resolved?.id ?? '') - return res.end() - }) }, async handleHotUpdate(ctx) { @@ -257,15 +244,6 @@ export function createSlidesLoader( && a.content.trim() === b.content.trim() && a.title?.trim() === b.title?.trim() && equal(a.frontmatter, b.frontmatter) - && Object.entries(a.snippetsUsed ?? {}).every(([file, oldContent]) => { - try { - const newContent = fs.readFileSync(file, 'utf-8') - return oldContent === newContent - } - catch { - return false - } - }) ) { if (a.note !== b.note) { ctx.server.hot.send( @@ -311,6 +289,7 @@ export function createSlidesLoader( hmrPages.clear() const moduleEntries = [ + ...ctx.modules.filter(i => i.id === templateMonacoRunDeps.id || i.id === templateMonacoTypes.id), ...vueModules, ...Array.from(moduleIds).map(id => ctx.server.moduleGraph.getModuleById(id)), ] @@ -332,7 +311,7 @@ export function createSlidesLoader( const template = templates.find(i => i.id === id) if (template) { return { - code: await template.getContent(options, templateCtx), + code: await template.getContent(options, templateCtx, this), map: { mappings: '' }, } } diff --git a/packages/types/client.d.ts b/packages/types/client.d.ts index e13f7197d0..6923fefc28 100644 --- a/packages/types/client.d.ts +++ b/packages/types/client.d.ts @@ -118,3 +118,8 @@ declare module '#slidev/styles' { declare module '#slidev/monaco-types' { // side-effects only } + +declare module '#slidev/monaco-run-deps' { + const modules: Recored + export default modules +} diff --git a/packages/types/src/code-runner.ts b/packages/types/src/code-runner.ts index e885e6feef..589c0c0ca9 100644 --- a/packages/types/src/code-runner.ts +++ b/packages/types/src/code-runner.ts @@ -10,10 +10,6 @@ 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. */ diff --git a/packages/types/src/config.ts b/packages/types/src/config.ts index d9b4d9fead..2c6db962bf 100644 --- a/packages/types/src/config.ts +++ b/packages/types/src/config.ts @@ -52,6 +52,12 @@ export interface SlidevConfig { * @default [] */ monacoTypesAdditionalPackages: string[] + /** + * Additional local modules to load as dependencies of monaco runnable + * + * @default [] + */ + monacoRunAdditionalDeps: string[] /** * Show a download button in the SPA build, * could also be a link to custom pdf diff --git a/packages/types/src/types.ts b/packages/types/src/types.ts index 4b2eaa05c4..265215a8e1 100644 --- a/packages/types/src/types.ts +++ b/packages/types/src/types.ts @@ -65,9 +65,15 @@ export interface SlidevThemeMeta { export type SlidevThemeConfig = Record -export interface SlidevFeatureFlags { +export interface SlidevDetectedFeatures { katex: boolean - monaco: boolean + /** + * `false` or referenced module specifiers + */ + monaco: false | { + types: string[] + deps: string[] + } tweet: boolean mermaid: boolean } @@ -89,7 +95,7 @@ export interface SlidevData { entry: SlidevMarkdown config: SlidevConfig headmatter: Record - features: SlidevFeatureFlags + features: SlidevDetectedFeatures themeMeta?: SlidevThemeMeta markdownFiles: Record watchFiles: string[]