From 211763394a809e92109530532cb621e1a4d7bc31 Mon Sep 17 00:00:00 2001 From: _Kerman Date: Mon, 5 Aug 2024 13:12:58 +0800 Subject: [PATCH 01/14] feat: very basic functions --- packages/client/logic/utils.ts | 2 +- packages/parser/package.json | 4 + packages/slidev/node/options.ts | 2 + packages/slidev/node/vite/compilerFlagsVue.ts | 3 +- packages/slidev/node/vite/extendConfig.ts | 2 +- packages/types/src/options.ts | 1 + packages/web/index.html | 15 + packages/web/package.json | 24 ++ packages/web/polyfills/node-module.ts | 1 + packages/web/polyfills/rollup-pluginutils.ts | 1 + packages/web/polyfills/unplugin.ts | 1 + packages/web/src/compiler/file.ts | 61 ++++ packages/web/src/compiler/jsx.ts | 10 + packages/web/src/compiler/md.ts | 67 +++++ packages/web/src/compiler/ts.ts | 8 + packages/web/src/compiler/utils.ts | 11 + packages/web/src/compiler/vue.ts | 281 ++++++++++++++++++ packages/web/src/configs.ts | 0 packages/web/src/configs/plugins.ts | 13 + packages/web/src/configs/slidev.ts | 7 + packages/web/src/custom-components.ts | 11 + packages/web/src/main.ts | 10 + packages/web/src/shiki.ts | 38 +++ packages/web/src/slides.ts | 27 ++ packages/web/src/styles.ts | 11 + packages/web/src/title-renderer.vue | 12 + packages/web/vite.config.ts | 100 +++++++ pnpm-lock.yaml | 169 ++++++++--- tsconfig.json | 1 - 29 files changed, 849 insertions(+), 44 deletions(-) create mode 100644 packages/web/index.html create mode 100644 packages/web/package.json create mode 100644 packages/web/polyfills/node-module.ts create mode 100644 packages/web/polyfills/rollup-pluginutils.ts create mode 100644 packages/web/polyfills/unplugin.ts create mode 100644 packages/web/src/compiler/file.ts create mode 100644 packages/web/src/compiler/jsx.ts create mode 100644 packages/web/src/compiler/md.ts create mode 100644 packages/web/src/compiler/ts.ts create mode 100644 packages/web/src/compiler/utils.ts create mode 100644 packages/web/src/compiler/vue.ts create mode 100644 packages/web/src/configs.ts create mode 100644 packages/web/src/configs/plugins.ts create mode 100644 packages/web/src/configs/slidev.ts create mode 100644 packages/web/src/custom-components.ts create mode 100644 packages/web/src/main.ts create mode 100644 packages/web/src/shiki.ts create mode 100644 packages/web/src/slides.ts create mode 100644 packages/web/src/styles.ts create mode 100644 packages/web/src/title-renderer.vue create mode 100644 packages/web/vite.config.ts 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 16e6ee9466..e47900d02d 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", diff --git a/packages/slidev/node/options.ts b/packages/slidev/node/options.ts index a76e19635e..02993453ad 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') @@ -76,6 +77,7 @@ export async function createDataUtils(data: SlidevData, clientRoot: string, root return { ...await setupShiki(roots), + defines: getDefine(options), 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/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..699c1c6c73 --- /dev/null +++ b/packages/web/package.json @@ -0,0 +1,24 @@ +{ + "name": "@slidev/compiler", + "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:*", + "@types/babel__standalone": "^7.1.7", + "@vue/babel-plugin-jsx": "^1.2.2", + "ohash": "^1.1.3", + "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-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..fe51cb3740 --- /dev/null +++ b/packages/web/src/compiler/file.ts @@ -0,0 +1,61 @@ +// 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(filename: string, code: string): Promise { + if (!code.trim()) { + return {} + } + + if (filename.endsWith('.css')) { + return { + css: code, + } + } + + if (isJsLikeFile(filename)) { + const isJSX = isJsxFile(filename) + if (isTsFile(filename)) { + code = await transformTS(code, isJSX) + } + if (isJSX) { + code = await import('./jsx').then(m => m.transformJSX(code)) + } + return { + js: code, + } + } + + if (filename.endsWith('.json')) { + let parsed + try { + parsed = JSON.parse(code) + } + catch (err: any) { + console.error(`Error parsing ${filename}`, err.message) + return { + errors: [err.message], + } + } + return { + js: `export default ${JSON.stringify(parsed)}`, + } + } + + if (filename.endsWith('.vue')) { + return compileVue(filename, code) + } + + if (filename.endsWith('.md')) { + return compileMd(filename, code) + } +} 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..b24c84f4dd --- /dev/null +++ b/packages/web/src/compiler/md.ts @@ -0,0 +1,67 @@ +import { unpluginFactory } from 'unplugin-vue-markdown' +import type { UnpluginOptions } from 'unplugin' +import { computed } from 'vue' +import { mdOptions } from '../configs/plugins' +import type { CompileResult } from './file' +import { compileVue } from './vue' + +const plugin = computed(() => { + 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) { + // await useMarkdownItPlugins(md, options, markdownTransformMap) + await mdOptions?.markdownItSetup?.(md) + }, + transforms: { + ...mdOptions?.transforms, + // before(code, id) { + // // Skip entry Markdown files + // if (options.data.markdownFiles[id]) + // return '' + + // 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: options.data.slides[+match[1] - 1], + // options, + // } + + // for (const transformer of transformers) { + // 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 { + const vue = ((await plugin.value.transform?.call({} as any, code, 'file.md')) as any).code + return compileVue(filename, vue) +} 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