diff --git a/netlify.toml b/netlify.toml index f95b9a6833..880427a1e8 100755 --- a/netlify.toml +++ b/netlify.toml @@ -1,6 +1,6 @@ [build] publish = "docs/.vitepress/dist" -command = "pnpm run docs:build" +command = "pnpm run website:build" [build.environment] NODE_VERSION = "20" @@ -28,6 +28,11 @@ from = "/demo/vue-runner/*" to = "/demo/vue-runner/index.html" status = 200 +[[redirects]] +from = "/web/*" +to = "/web/index.html" +status = 200 + [[redirects]] from = "https://slidev.antfu.me/*" to = "https://sli.dev/:splat" diff --git a/package.json b/package.json index 099592d073..a39a9dc3e5 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,10 @@ "lint:fix": "nr lint --fix", "typecheck": "vue-tsc --noEmit", "docs": "pnpm -C docs run dev", - "docs:build": "pnpm run --filter {./docs}... build && pnpm demo:build", + "docs:build": "pnpm run --filter {./docs}... build", + "web": "pnpm -C packages/web run dev", + "web:build": "zx ./scripts/web.mjs", + "website:build": "pnpm docs:build && pnpm demo:build && pnpm web:build", "release": "bumpp package.json packages/*/package.json docs/package.json --all -x \"zx scripts/update-versions.mjs\"", "test": "vitest test", "prepare": "simple-git-hooks" diff --git a/packages/client/logic/utils.ts b/packages/client/logic/utils.ts index c5228b1e5d..0d01830245 100644 --- a/packages/client/logic/utils.ts +++ b/packages/client/logic/utils.ts @@ -1,4 +1,4 @@ -import { parseRangeString } from '@slidev/parser/core' +import { parseRangeString } from '@slidev/parser/utils' import { useTimestamp } from '@vueuse/core' import { computed, ref } from 'vue' diff --git a/packages/parser/package.json b/packages/parser/package.json index f8df8fc47a..936289d10c 100644 --- a/packages/parser/package.json +++ b/packages/parser/package.json @@ -23,6 +23,10 @@ "./fs": { "types": "./dist/fs.d.mts", "import": "./dist/fs.mjs" + }, + "./utils": { + "types": "./dist/utils.d.mts", + "import": "./dist/utils.mjs" } }, "main": "dist/index.mjs", @@ -36,7 +40,7 @@ "node": ">=18.0.0" }, "scripts": { - "build": "rimraf dist && tsup src/index.ts src/core.ts src/fs.ts", + "build": "rimraf dist && tsup src/index.ts src/core.ts src/fs.ts src/utils.ts", "dev": "nr build --watch", "prepublishOnly": "npm run build" }, diff --git a/packages/slidev/node/options.ts b/packages/slidev/node/options.ts index a76e19635e..43d7330aad 100644 --- a/packages/slidev/node/options.ts +++ b/packages/slidev/node/options.ts @@ -9,6 +9,7 @@ import { getThemeMeta, resolveTheme } from './integrations/themes' import { resolveAddons } from './integrations/addons' import { getRoots, resolveEntry } from './resolver' import setupShiki from './setups/shiki' +import { getDefine } from './vite/extendConfig' const debug = Debug('slidev:options') @@ -61,13 +62,13 @@ export async function resolveOptions( themeRoots, addonRoots, roots, - utils: await createDataUtils(data, rootsInfo.clientRoot, roots), + utils: await createDataUtils(data, rootsInfo.clientRoot, roots, mode), } return resolved } -export async function createDataUtils(data: SlidevData, clientRoot: string, roots: string[]): Promise { +export async function createDataUtils(data: SlidevData, clientRoot: string, roots: string[], mode: string): Promise { const monacoTypesIgnorePackagesMatches = (data.config.monacoTypesIgnorePackages || []) .map(i => mm.matcher(i)) @@ -76,6 +77,7 @@ export async function createDataUtils(data: SlidevData, clientRoot: string, root return { ...await setupShiki(roots), + defines: getDefine({ data, clientRoot, mode } as any), isMonacoTypesIgnored: pkg => monacoTypesIgnorePackagesMatches.some(i => i(pkg)), getLayouts: () => { const now = Date.now() diff --git a/packages/slidev/node/vite/compilerFlagsVue.ts b/packages/slidev/node/vite/compilerFlagsVue.ts index 7d70a38afe..cdf122036d 100644 --- a/packages/slidev/node/vite/compilerFlagsVue.ts +++ b/packages/slidev/node/vite/compilerFlagsVue.ts @@ -1,7 +1,6 @@ import type { Plugin } from 'vite' import { objectEntries } from '@antfu/utils' import type { ResolvedSlidevOptions } from '@slidev/types' -import { getDefine } from './extendConfig' /** * Replace compiler flags like `__DEV__` in Vue SFC @@ -9,7 +8,7 @@ import { getDefine } from './extendConfig' export function createVueCompilerFlagsPlugin( options: ResolvedSlidevOptions, ): Plugin[] { - const define = objectEntries(getDefine(options)) + const define = objectEntries(options.utils.defines) return [ { name: 'slidev:flags', diff --git a/packages/slidev/node/vite/extendConfig.ts b/packages/slidev/node/vite/extendConfig.ts index 8a02be1c09..1f3eb3ae4c 100644 --- a/packages/slidev/node/vite/extendConfig.ts +++ b/packages/slidev/node/vite/extendConfig.ts @@ -70,7 +70,7 @@ export function createConfigPlugin(options: ResolvedSlidevOptions): Plugin { name: 'slidev:config', async config(config) { const injection: InlineConfig = { - define: getDefine(options), + define: options.utils.defines, resolve: { alias: [ { diff --git a/packages/slidev/node/vite/layoutWrapper.ts b/packages/slidev/node/vite/layoutWrapper.ts index 0a92d41258..812850554f 100644 --- a/packages/slidev/node/vite/layoutWrapper.ts +++ b/packages/slidev/node/vite/layoutWrapper.ts @@ -1,15 +1,19 @@ import type { ResolvedSlidevOptions } from '@slidev/types' import type { Plugin } from 'vite' import { bold, gray, red, yellow } from 'kolorist' -import { toAtFS } from '../resolver' +import { ensurePrefix, slash } from '@antfu/utils' import { regexSlideSourceId, templateImportContextUtils, templateInitContext, templateInjectionMarker } from './common' +export function toAtFS(path: string) { + return `/@fs${ensurePrefix('/', slash(path))}` +} + export function createLayoutWrapperPlugin( { data, utils }: ResolvedSlidevOptions, ): Plugin { return { name: 'slidev:layout-wrapper', - async transform(code, id) { + transform(code, id) { const match = id.match(regexSlideSourceId) if (!match) return @@ -17,7 +21,7 @@ export function createLayoutWrapperPlugin( if (type !== 'md') return const index = +no - 1 - const layouts = await utils.getLayouts() + const layouts = utils.getLayouts() const rawLayoutName = data.slides[index]?.frontmatter?.layout ?? data.slides[0]?.frontmatter?.default?.layout let layoutName = rawLayoutName || (index === 0 ? 'cover' : 'default') if (!layouts[layoutName]) { diff --git a/packages/slidev/node/vite/loaders.ts b/packages/slidev/node/vite/loaders.ts index 0d26241e12..551918c3b8 100644 --- a/packages/slidev/node/vite/loaders.ts +++ b/packages/slidev/node/vite/loaders.ts @@ -200,7 +200,7 @@ export function createSlidesLoader( } Object.assign(data, newData) - Object.assign(utils, createDataUtils(newData, clientRoot, roots)) + Object.assign(utils, createDataUtils(newData, clientRoot, roots, mode)) if (hmrSlidesIndexes.size > 0) moduleIds.add(templateTitleRendererMd.id) diff --git a/packages/types/src/options.ts b/packages/types/src/options.ts index 15f26266e9..c0523b0df4 100644 --- a/packages/types/src/options.ts +++ b/packages/types/src/options.ts @@ -48,6 +48,7 @@ export interface ResolvedSlidevOptions extends RootsInfo, SlidevEntryOptions { export interface ResolvedSlidevUtils { shiki: HighlighterGeneric shikiOptions: MarkdownItShikiOptions + defines: Record isMonacoTypesIgnored: (pkg: string) => boolean getLayouts: () => Record } diff --git a/packages/web/index.html b/packages/web/index.html new file mode 100644 index 0000000000..631365bd17 --- /dev/null +++ b/packages/web/index.html @@ -0,0 +1,15 @@ + + + + + + + Slidev Online + + +
+ +
+ + + diff --git a/packages/web/package.json b/packages/web/package.json new file mode 100644 index 0000000000..f870578eef --- /dev/null +++ b/packages/web/package.json @@ -0,0 +1,27 @@ +{ + "name": "@slidev/web", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build" + }, + "devDependencies": { + "@antfu/utils": "^0.7.10", + "@babel/standalone": "^7.25.3", + "@slidev/client": "workspace:*", + "@slidev/parser": "workspace:*", + "@slidev/theme-seriph": "^0.25.0", + "@types/babel__standalone": "^7.1.7", + "@vue/babel-plugin-jsx": "^1.2.2", + "magic-string": "^0.30.10", + "ohash": "^1.1.3", + "pathe": "^1.1.2", + "sucrase": "^3.35.0", + "typescript": "^5.5.4", + "unocss": "^0.61.5", + "unplugin-vue-markdown": "^0.26.2", + "vite": "^5.3.4", + "vue": "^3.4.33", + "vue-router": "^4.4.0" + } +} diff --git a/packages/web/polyfills/node-assert.ts b/packages/web/polyfills/node-assert.ts new file mode 100644 index 0000000000..63e04681dc --- /dev/null +++ b/packages/web/polyfills/node-assert.ts @@ -0,0 +1,2 @@ +export default function () { +} diff --git a/packages/web/polyfills/node-module.ts b/packages/web/polyfills/node-module.ts new file mode 100644 index 0000000000..745adcfc06 --- /dev/null +++ b/packages/web/polyfills/node-module.ts @@ -0,0 +1 @@ +export const createRequire = () => null diff --git a/packages/web/polyfills/rollup-pluginutils.ts b/packages/web/polyfills/rollup-pluginutils.ts new file mode 100644 index 0000000000..c6f304aebb --- /dev/null +++ b/packages/web/polyfills/rollup-pluginutils.ts @@ -0,0 +1 @@ +export const createFilter = () => () => true diff --git a/packages/web/polyfills/unplugin.ts b/packages/web/polyfills/unplugin.ts new file mode 100644 index 0000000000..511c274925 --- /dev/null +++ b/packages/web/polyfills/unplugin.ts @@ -0,0 +1 @@ +export const createUnplugin = () => null diff --git a/packages/web/src/compiler/file.ts b/packages/web/src/compiler/file.ts new file mode 100644 index 0000000000..16bfdcbc48 --- /dev/null +++ b/packages/web/src/compiler/file.ts @@ -0,0 +1,65 @@ +// Ported from https://github.com/vuejs/repl/blob/main/src/transform.ts + +import { compileMd } from './md' +import { transformTS } from './ts' +import { isJsLikeFile, isJsxFile, isTsFile } from './utils' +import { compileVue } from './vue' + +export interface CompileResult { + js?: string + css?: string + errors?: (string | Error)[] +} + +export async function compileFile(filepath: string, code: string): Promise { + if (!code.trim()) { + return {} + } + + if (filepath.endsWith('.css')) { + return { + css: code, + } + } + + if (isJsLikeFile(filepath)) { + const isJSX = isJsxFile(filepath) + if (isTsFile(filepath)) { + code = await transformTS(code, isJSX) + } + if (isJSX) { + code = await import('./jsx').then(m => m.transformJSX(code)) + } + return { + js: code, + } + } + + if (filepath.endsWith('.json')) { + let parsed + try { + parsed = JSON.parse(code) + } + catch (err: any) { + console.error(`Error parsing ${filepath}`, err.message) + return { + errors: [err.message], + } + } + return { + js: `export default ${JSON.stringify(parsed)}`, + } + } + + if (filepath.endsWith('.vue')) { + return compileVue(filepath, code) + } + + if (filepath.endsWith('.md')) { + return compileMd(filepath, code) + } + + return { + errors: [`Unknown file extension for ${filepath}`], + } +} diff --git a/packages/web/src/compiler/jsx.ts b/packages/web/src/compiler/jsx.ts new file mode 100644 index 0000000000..f2f1c5ef75 --- /dev/null +++ b/packages/web/src/compiler/jsx.ts @@ -0,0 +1,10 @@ +// Ported from https://github.com/vuejs/repl/blob/main/src/jsx.ts + +import { transform } from '@babel/standalone' +import jsx from '@vue/babel-plugin-jsx' + +export async function transformJSX(src: string) { + return transform(src, { + plugins: [jsx], + }).code! +} diff --git a/packages/web/src/compiler/md.ts b/packages/web/src/compiler/md.ts new file mode 100644 index 0000000000..140dbd16aa --- /dev/null +++ b/packages/web/src/compiler/md.ts @@ -0,0 +1,124 @@ +import { unpluginFactory } from 'unplugin-vue-markdown' +import type { UnpluginOptions } from 'unplugin' +import { computed } from 'vue' +import { taskLists as MarkdownItTaskList } from '@hedgedoc/markdown-it-plugins' +import MarkdownItMdc from 'markdown-it-mdc' +// @ts-expect-error missing types +import MarkdownItFootnote from 'markdown-it-footnote' +import MarkdownItKatex from '@slidev/cli/node/syntax/markdown-it/markdown-it-katex' +// import MarkdownItVDrag from '@slidev/cli/node/syntax/markdown-it/markdown-it-v-drag' +// import MarkdownItShiki from '@slidev/cli/node/syntax/markdown-it/markdown-it-shiki' +import MarkdownItLink from '@slidev/cli/node/syntax/markdown-it/markdown-it-link' +import MarkdownItEscapeInlineCode from '@slidev/cli/node/syntax/markdown-it/markdown-it-escape-code' +import type { MarkdownTransformContext } from '@slidev/types' +import MagicString from 'magic-string-stack' +import { regexSlideSourceId } from '@slidev/cli/node/vite/common' +import { transformMonaco } from '@slidev/cli/node/syntax/transform/monaco' +import { transformCodeWrapper } from '@slidev/cli/node/syntax/transform/code-wrapper' +import { transformPageCSS } from '@slidev/cli/node/syntax/transform/in-page-css' +import { transformKaTexWrapper } from '@slidev/cli/node/syntax/transform/katex-wrapper' +// import { transformMagicMove } from '@slidev/cli/node/syntax/transform/magic-move' +import { transformMermaid } from '@slidev/cli/node/syntax/transform/mermaid' +// import { transformPlantUml } from '@slidev/cli/node/syntax/transform/plant-uml' +import { transformSlotSugar } from '@slidev/cli/node/syntax/transform/slot-sugar' +// import { transformSnippet } from '@slidev/cli/node/syntax/transform/snippet' +import { slidesInfo } from '../slides' +import { mdOptions } from '../configs/plugins' +import { layoutWrapperPlugin } from '../layouts' +import type { CompileResult } from './file' +import { compileVue } from './vue' + +const transformers = computed(() => [ + // transformSnippet, + // transformMagicMove, + + transformMermaid, + // transformPlantUml, + transformMonaco, + + transformCodeWrapper, + transformKaTexWrapper, + transformPageCSS, + transformSlotSugar, +]) + +const plugin = computed(() => { + const markdownTransformMap = new Map() + return unpluginFactory({ + include: [/\.md$/], + wrapperClasses: '', + headEnabled: false, + frontmatter: false, + escapeCodeTagInterpolation: false, + markdownItOptions: { + quotes: '""\'\'', + html: true, + xhtmlOut: true, + linkify: true, + ...mdOptions?.markdownItOptions, + }, + ...mdOptions, + async markdownItSetup(md) { + // md.use(await MarkdownItShiki(options)) + + md.use(MarkdownItLink) + md.use(MarkdownItEscapeInlineCode) + md.use(MarkdownItFootnote) + md.use(MarkdownItTaskList, { enabled: true, lineNumber: true, label: true }) + + md.use(MarkdownItKatex, []) + // md.use(MarkdownItVDrag, markdownTransformMap) + + md.use(MarkdownItMdc) + + await mdOptions?.markdownItSetup?.(md) + }, + transforms: { + ...mdOptions?.transforms, + before(code, id) { + code = mdOptions?.transforms?.before?.(code, id) ?? code + + const match = id.match(regexSlideSourceId) + if (!match) + return code + + const s = new MagicString(code) + markdownTransformMap.set(id, s) + const ctx: MarkdownTransformContext = { + s, + slide: slidesInfo.value[+match[1] - 1], + options: { + data: { + config: { + monaco: true, + }, + }, + } as any, + } + + for (const transformer of transformers.value) { + if (!transformer) + continue + transformer(ctx) + if (!ctx.s.isEmpty()) + ctx.s.commit() + } + + return s.toString() + }, + }, + }, { + framework: 'vite', + }) as UnpluginOptions +}) + +export async function compileMd(filename: string, code: string): Promise { + let vue = (await (plugin.value.transform?.call({ + error: console.error, + } as any, code, filename)) as any).code + + // @ts-expect-error uwu + vue = layoutWrapperPlugin.transform!.call({} as any, vue, filename) + + return await compileVue(filename, vue) +} diff --git a/packages/web/src/compiler/note.ts b/packages/web/src/compiler/note.ts new file mode 100644 index 0000000000..7677e05117 --- /dev/null +++ b/packages/web/src/compiler/note.ts @@ -0,0 +1,28 @@ +import markdownItLink from '@slidev/cli/node/syntax/markdown-it/markdown-it-link' +import type { Token } from 'markdown-it' +import MarkdownIt from 'markdown-it' + +export const sharedMd = MarkdownIt({ html: true }) +sharedMd.use(markdownItLink) + +export function stringifyMarkdownTokens(tokens: Token[]) { + return tokens.map(token => token.children + ?.filter(t => ['text', 'code_inline'].includes(t.type) && !t.content.match(/^\s*$/)) + .map(t => t.content.trim()) + .join(' ')) + .filter(Boolean) + .join(' ') +} + +export function renderNote(text: string = '') { + let clickCount = 0 + const html = sharedMd.render(text + // replace [click] marker with span + .replace(/\[click(?::(\d+))?\]/gi, (_, count = 1) => { + clickCount += Number(count) + return `` + }), + ) + + return html +} diff --git a/packages/web/src/compiler/ts.ts b/packages/web/src/compiler/ts.ts new file mode 100644 index 0000000000..2a08e51c93 --- /dev/null +++ b/packages/web/src/compiler/ts.ts @@ -0,0 +1,8 @@ +import { transform } from 'sucrase' + +export async function transformTS(src: string, isJSX?: boolean) { + return transform(src, { + transforms: ['typescript', ...(isJSX ? (['jsx'] as const) : [])], + jsxRuntime: 'preserve', + }).code +} diff --git a/packages/web/src/compiler/utils.ts b/packages/web/src/compiler/utils.ts new file mode 100644 index 0000000000..38e7aff1c5 --- /dev/null +++ b/packages/web/src/compiler/utils.ts @@ -0,0 +1,11 @@ +// Ported from https://github.com/vuejs/repl/blob/main/src/transform.ts + +export function isJsLikeFile(filename: string | undefined | null) { + return !!(filename && /\.[jt]sx?$/.test(filename)) +} +export function isTsFile(filename: string | undefined | null) { + return !!(filename && /(?:\.|\b)tsx?$/.test(filename)) +} +export function isJsxFile(filename: string | undefined | null) { + return !!(filename && /(?:\.|\b)[jt]sx$/.test(filename)) +} diff --git a/packages/web/src/compiler/vue.ts b/packages/web/src/compiler/vue.ts new file mode 100644 index 0000000000..55778fdace --- /dev/null +++ b/packages/web/src/compiler/vue.ts @@ -0,0 +1,281 @@ +// Ported from https://github.com/vuejs/repl/blob/main/src/transform.ts + +import type { BindingMetadata, CompilerOptions, SFCDescriptor } from 'vue/compiler-sfc' +import * as compiler from 'vue/compiler-sfc' +import { toArray } from '@antfu/utils' +import { hash } from 'ohash' +import { sfcOptions } from '../configs/plugins' +import { transformTS } from './ts' +import type { CompileResult } from './file' +import { isJsxFile, isTsFile } from './utils' + +export const COMP_IDENTIFIER = `__sfc__` + +async function doCompileScript( + descriptor: SFCDescriptor, + id: string, + ssr: boolean, + isTS: boolean, + isJSX: boolean, +): Promise<[code: string, bindings: BindingMetadata | undefined]> { + if (descriptor.script || descriptor.scriptSetup) { + const expressionPlugins: CompilerOptions['expressionPlugins'] = [] + if (isTS) { + expressionPlugins.push('typescript') + } + if (isJSX) { + expressionPlugins.push('jsx') + } + + const compiledScript = compiler.compileScript(descriptor, { + inlineTemplate: true, + ...sfcOptions?.script, + id, + genDefaultAs: COMP_IDENTIFIER, + templateOptions: { + ...sfcOptions?.template, + ssr, + ssrCssVars: descriptor.cssVars, + compilerOptions: { + ...sfcOptions?.template?.compilerOptions, + expressionPlugins, + }, + }, + }) + let code = compiledScript.content + if (isTS) { + code = await transformTS(code, isJSX) + } + if (isJSX) { + code = await import('./jsx').then(m => m.transformJSX(code)) + } + + return [code, compiledScript.bindings] + } + else { + return [`\nconst ${COMP_IDENTIFIER} = {}`, undefined] + } +} + +async function doCompileTemplate( + descriptor: SFCDescriptor, + id: string, + bindingMetadata: BindingMetadata | undefined, + ssr: boolean, + isTS: boolean, + isJSX: boolean, +) { + const expressionPlugins: CompilerOptions['expressionPlugins'] = [] + if (isTS) { + expressionPlugins.push('typescript') + } + if (isJSX) { + expressionPlugins.push('jsx') + } + + let { code, errors } = compiler.compileTemplate({ + isProd: false, + ...sfcOptions?.template, + ast: descriptor.template!.ast, + source: descriptor.template!.content, + filename: descriptor.filename, + id, + scoped: descriptor.styles.some(s => s.scoped), + slotted: descriptor.slotted, + ssr, + ssrCssVars: descriptor.cssVars, + compilerOptions: { + ...sfcOptions?.template?.compilerOptions, + bindingMetadata, + expressionPlugins, + }, + }) + if (errors.length) { + return errors + } + + const fnName = ssr ? `ssrRender` : `render` + + code + = `\n${code.replace( + /\nexport (function|const) (render|ssrRender)/, + `$1 ${fnName}`, + )}` + `\n${COMP_IDENTIFIER}.${fnName} = ${fnName}` + + if (isTS) { + code = await transformTS(code, isJSX) + } + if (isJSX) { + code = await import('./jsx').then(m => m.transformJSX(code)) + } + + return code +} + +function isCustomElement(filename: string) { + const filter = sfcOptions.customElement || /\.ce\.vue$/ + if (filter === true) { + return true + } + return toArray(filter).some((f) => { + if (typeof f === 'string') { + return filename.includes(f) + } + return f.test(filename) + }) +} + +export async function compileVue(filename: string, code: string): Promise { + const id = hash(filename) + const { errors, descriptor } = compiler.parse(code, { + filename, + sourceMap: true, + templateParseOptions: sfcOptions?.template?.compilerOptions, + }) + if (errors.length) { + return { + errors, + } + } + + const styleLangs = descriptor.styles.map(s => s.lang).filter(Boolean) + const templateLang = descriptor.template?.lang + if (styleLangs.length && templateLang) { + return { + errors: [ + `lang="${styleLangs.join( + ',', + )}" pre-processors for