UNPKG

@nuxtjs/mdc

Version:
432 lines (424 loc) 15.7 kB
import fs$1, { existsSync } from 'node:fs'; import { extendViteConfig, useNitro, defineNuxtModule, createResolver, addServerHandler, addComponent, addImports, addServerImports, addComponentsDir, addTemplate } from '@nuxt/kit'; import { defu } from 'defu'; import { resolve } from 'pathe'; import fs from 'node:fs/promises'; import { bundledLanguagesInfo } from 'shiki/bundle/full'; import { pascalCase } from 'scule'; export { defineConfig } from './config.mjs'; const registerMDCSlotTransformer = (resolver) => { extendViteConfig((config) => { const compilerOptions = config.vue.template.compilerOptions; compilerOptions.nodeTransforms = [ function viteMDCSlot(node, context) { const isVueSlotWithUnwrap = node.tag === "slot" && node.props.find((p) => p.name === "mdc-unwrap" || p.name === "bind" && p.rawName === ":mdc-unwrap"); const isMDCSlot = node.tag === "MDCSlot"; if (isVueSlotWithUnwrap || isMDCSlot) { const transform = context.ssr ? context.nodeTransforms.find((nt) => nt.name === "ssrTransformSlotOutlet") : context.nodeTransforms.find((nt) => nt.name === "transformSlotOutlet"); return () => { node.tag = "slot"; node.type = 1; node.tagType = 2; transform?.(node, context); const codegen = context.ssr ? node.ssrCodegenNode : node.codegenNode; codegen.callee = context.ssr ? "_ssrRenderMDCSlot" : "_renderMDCSlot"; const importExp = context.ssr ? "{ ssrRenderSlot as _ssrRenderMDCSlot }" : "{ renderSlot as _renderMDCSlot }"; if (!context.imports.some((i) => String(i.exp) === importExp)) { context.imports.push({ exp: importExp, path: resolver.resolve(`./runtime/utils/${context.ssr ? "ssrSlot" : "slot"}`) }); } }; } if (context.nodeTransforms[0].name !== "viteMDCSlot") { const index = context.nodeTransforms.findIndex((f) => f.name === "viteMDCSlot"); const nt = context.nodeTransforms.splice(index, 1); context.nodeTransforms.unshift(nt[0]); } } ]; }); }; async function mdcConfigs({ options }) { return [ "let configs", "export function getMdcConfigs () {", "if (!configs) {", " configs = Promise.all([", ...options.configs.map((item) => ` import('${item}').then(m => m.default),`), " ])", "}", "return configs", "}" ].join("\n"); } async function mdcHighlighter({ options: { shikiPath, options, useWasmAssets } }) { if (!options || !options.highlighter) return "export default () => { throw new Error('[@nuxtjs/mdc] No highlighter specified') }"; if (options.highlighter === "shiki") { const file = [ shikiPath, shikiPath + ".mjs" ].find((file2) => existsSync(file2)); if (!file) throw new Error(`[@nuxtjs/mdc] Could not find shiki highlighter: ${shikiPath}`); let code = await fs.readFile(file, "utf-8"); if (useWasmAssets) { code = code.replace( /import\((['"])shiki\/wasm\1\)/, // We can remove the .client condition once Vite supports WASM ESM import "import.meta.client ? import('shiki/wasm') : import('shiki/onig.wasm')" ); } code = code.replace( /from\s+(['"])shiki\1/, 'from "shiki/engine/javascript"' ); const langsMap = /* @__PURE__ */ new Map(); options.langs?.forEach((lang) => { if (typeof lang === "string") { const info = bundledLanguagesInfo.find((i) => i.aliases?.includes?.(lang) || i.id === lang); if (!info) { throw new Error(`[@nuxtjs/mdc] Could not find shiki language: ${lang}`); } langsMap.set(info.id, info.id); for (const alias of info.aliases || []) { langsMap.set(alias, info.id); } } else { langsMap.set(lang.name, lang); } }); const themes = Array.from(/* @__PURE__ */ new Set([ ...typeof options?.theme === "string" ? [options?.theme] : Object.values(options?.theme || {}), ...options?.themes || [] ])); const { shikiEngine = "oniguruma" } = options; return [ "import { getMdcConfigs } from '#mdc-configs'", shikiEngine === "javascript" ? "" : "import { createOnigurumaEngine } from 'shiki/engine/oniguruma'", code, "const bundledLangs = {", ...Array.from(langsMap.entries()).map(([name, lang]) => typeof lang === "string" ? JSON.stringify(name) + `: () => import('@shikijs/langs/${lang}').then(r => r.default || r),` : JSON.stringify(name) + ": " + JSON.stringify(lang) + ","), "}", "const bundledThemes = {", ...themes.map((theme) => typeof theme === "string" ? JSON.stringify(theme) + `: () => import('@shikijs/themes/${theme}').then(r => r.default || r),` : JSON.stringify(theme.name) + ": " + JSON.stringify(theme) + ","), "}", "const options = " + JSON.stringify({ theme: options.theme, wrapperStyle: options.wrapperStyle }), shikiEngine === "javascript" ? "const engine = createJavaScriptRegexEngine({ forgiving: true })" : `const engine = createOnigurumaEngine(() => import('shiki/wasm'))`, "const highlighter = createShikiHighlighter({ bundledLangs, bundledThemes, options, getMdcConfigs, engine })", "export default highlighter" ].join("\n"); } if (options.highlighter === "custom") { return [ "import { getMdcConfigs } from '#mdc-configs'", "export default async function (...args) {", " const configs = await getMdcConfigs()", " for (const config of configs) {", " if (config.highlighter) {", " return config.highlighter(...args)", " }", " }", " throw new Error('[@nuxtjs/mdc] No custom highlighter specified')", "}" ].join("\n"); } return "export { default } from " + JSON.stringify(options.highlighter); } async function mdcImports({ options }) { const imports = []; const { imports: remarkImports, definitions: remarkDefinitions } = processUnistPlugins(options.remarkPlugins); const { imports: rehypeImports, definitions: rehypeDefinitions } = processUnistPlugins(options.rehypePlugins); return [ ...remarkImports, ...rehypeImports, ...imports, "", "export const remarkPlugins = {", ...remarkDefinitions, "}", "", "export const rehypePlugins = {", ...rehypeDefinitions, "}", "", `export const highlight = ${JSON.stringify({ theme: options.highlight?.theme, wrapperStyle: options.highlight?.wrapperStyle })}` ].join("\n"); } function processUnistPlugins(plugins) { const imports = []; const definitions = []; Object.entries(plugins).forEach(([name, plugin]) => { const instanceName = `_${pascalCase(name).replace(/\W/g, "")}`; if (plugin) { imports.push(`import ${instanceName} from '${plugin.src || name}'`); if (Object.keys(plugin).length) { definitions.push(` '${name}': { instance: ${instanceName}, options: ${JSON.stringify(plugin.options || plugin)} },`); } else { definitions.push(` '${name}': { instance: ${instanceName} },`); } } else { definitions.push(` '${name}': false,`); } }); return { imports, definitions }; } function addWasmSupport(nuxt) { nuxt.hook("ready", () => { const nitro = useNitro(); const _addWasmSupport = (_nitro) => { if (nitro.options.experimental?.wasm) { return; } _nitro.options.externals = _nitro.options.externals || {}; _nitro.options.externals.inline = _nitro.options.externals.inline || []; _nitro.options.externals.inline.push((id) => id.endsWith(".wasm")); _nitro.hooks.hook("rollup:before", async (_, rollupConfig) => { const { rollup: unwasm } = await import('unwasm/plugin'); rollupConfig.plugins = rollupConfig.plugins || []; rollupConfig.plugins.push( unwasm({ ..._nitro.options.wasm }) ); }); }; _addWasmSupport(nitro); nitro.hooks.hook("prerender:init", (prerenderer) => { _addWasmSupport(prerenderer); }); }); } const DefaultHighlightLangs = [ "js", "jsx", "json", "ts", "tsx", "vue", "css", "html", "bash", "md", "mdc", "yaml" ]; const module = defineNuxtModule({ meta: { name: "@nuxtjs/mdc", configKey: "mdc" }, // Default configuration options of the Nuxt module defaults: { remarkPlugins: { "remark-emoji": {} }, rehypePlugins: {}, highlight: false, headings: { anchorLinks: { h1: false, h2: true, h3: true, h4: true, h5: false, h6: false } }, keepComments: false, components: { prose: true, map: {} } }, async setup(options, nuxt) { resolveOptions(options); const resolver = createResolver(import.meta.url); nuxt.options.runtimeConfig.public.mdc = defu(nuxt.options.runtimeConfig.public.mdc, { components: { prose: options.components.prose, map: options.components.map }, headings: options.headings }); nuxt.options.build.transpile ||= []; nuxt.options.build.transpile.push("yaml"); if (options.highlight) { addWasmSupport(nuxt); if (options.highlight?.noApiRoute !== true) { addServerHandler({ route: "/api/_mdc/highlight", handler: resolver.resolve("./runtime/highlighter/event-handler") }); } options.rehypePlugins ||= {}; options.rehypePlugins.highlight ||= {}; options.rehypePlugins.highlight.src ||= await resolver.resolvePath("./runtime/highlighter/rehype-nuxt"); options.rehypePlugins.highlight.options ||= {}; } const registerTemplate = (options2) => { const name = options2.filename.replace(/\.m?js$/, ""); const alias = "#" + name; const results = addTemplate({ ...options2, write: true // Write to disk for Nitro to consume }); nuxt.options.nitro.alias ||= {}; nuxt.options.nitro.externals ||= {}; nuxt.options.nitro.externals.inline ||= []; nuxt.options.alias[alias] = results.dst; nuxt.options.nitro.alias[alias] = nuxt.options.alias[alias]; nuxt.options.nitro.externals.inline.push(nuxt.options.alias[alias]); nuxt.options.nitro.externals.inline.push(alias); return results; }; const mdcConfigs$1 = []; for (const layer of nuxt.options._layers) { let path = resolve(layer.config.srcDir, "mdc.config.ts"); if (fs$1.existsSync(path)) { mdcConfigs$1.push(path); } else { path = resolve(layer.config.srcDir, "mdc.config.js"); if (fs$1.existsSync(path)) { mdcConfigs$1.push(path); } } } await nuxt.callHook("mdc:configSources", mdcConfigs$1); registerTemplate({ filename: "mdc-configs.mjs", getContents: mdcConfigs, options: { configs: mdcConfigs$1 } }); const nitroPreset = nuxt.options.nitro.preset || process.env.NITRO_PRESET || process.env.SERVER_PRESET || ""; const useWasmAssets = !nuxt.options.dev && (!!nuxt.options.nitro.experimental?.wasm || ["cloudflare-pages", "cloudflare-module", "cloudflare"].includes(nitroPreset)); registerTemplate({ filename: "mdc-highlighter.mjs", getContents: mdcHighlighter, options: { shikiPath: resolver.resolve("../dist/runtime/highlighter/shiki.js"), options: options.highlight, useWasmAssets } }); registerTemplate({ filename: "mdc-imports.mjs", getContents: mdcImports, options }); addComponent({ name: "MDC", filePath: resolver.resolve("./runtime/components/MDC") }); addComponent({ name: "MDCCached", filePath: resolver.resolve("./runtime/components/MDCCached") }); addComponent({ name: "MDCRenderer", filePath: resolver.resolve("./runtime/components/MDCRenderer") }); addComponent({ name: "MDCSlot", filePath: resolver.resolve("./runtime/components/MDCSlot") }); addImports({ from: resolver.resolve("./runtime/utils/node"), name: "flatUnwrap", as: "unwrapSlot" }); addImports({ from: resolver.resolve("./runtime/parser"), name: "parseMarkdown", as: "parseMarkdown" }); addServerImports([{ from: resolver.resolve("./runtime/parser"), name: "parseMarkdown", as: "parseMarkdown" }]); addImports({ from: resolver.resolve("./runtime/stringify"), name: "stringifyMarkdown", as: "stringifyMarkdown" }); addServerImports([{ from: resolver.resolve("./runtime/stringify"), name: "stringifyMarkdown", as: "stringifyMarkdown" }]); if (options.components?.prose) { addComponentsDir({ path: resolver.resolve("./runtime/components/prose"), pathPrefix: false, prefix: "", global: true }); } addTemplate({ filename: "mdc-image-component.mjs", write: true, getContents: ({ app }) => { const image = app.components.find((c) => c.pascalName === "NuxtImg" && !c.filePath.includes("nuxt/dist/app")); return image ? `export { default } from "${image.filePath}"` : 'export default "img"'; } }); extendViteConfig((config) => { const include = [ "remark-gfm", // from runtime/parser/index.ts "remark-emoji", // from runtime/parser/index.ts "remark-mdc", // from runtime/parser/index.ts "remark-rehype", // from runtime/parser/index.ts "rehype-raw", // from runtime/parser/index.ts "parse5", // transitive deps of rehype "unist-util-visit", // from runtime/highlighter/rehype.ts "unified", // deps by all the plugins "debug" // deps by many libraries but it's not an ESM ]; const exclude = [ "@nuxtjs/mdc" // package itself, it's a build time module ]; config.optimizeDeps ||= {}; config.optimizeDeps.exclude ||= []; config.optimizeDeps.include ||= []; for (const pkg of include) { if (!config.optimizeDeps.include.includes(pkg)) { config.optimizeDeps.include.push("@nuxtjs/mdc > " + pkg); } } for (const pkg of exclude) { if (!config.optimizeDeps.exclude.includes(pkg)) { config.optimizeDeps.exclude.push(pkg); } } }); const _layers = [...nuxt.options._layers].reverse(); for (const layer of _layers) { const srcDir = layer.config.srcDir; const globalComponents = resolver.resolve(srcDir, "components/mdc"); const dirStat = await fs$1.promises.stat(globalComponents).catch(() => null); if (dirStat && dirStat.isDirectory()) { nuxt.hook("components:dirs", (dirs) => { dirs.unshift({ path: globalComponents, global: true, pathPrefix: false, prefix: "" }); }); } } registerMDCSlotTransformer(resolver); } }); function resolveOptions(options) { if (options.highlight !== false) { options.highlight ||= {}; options.highlight.highlighter ||= "shiki"; options.highlight.theme ||= { default: "github-light", dark: "github-dark" }; options.highlight.shikiEngine ||= "oniguruma"; options.highlight.langs ||= DefaultHighlightLangs; if (options.highlight.preload) { options.highlight.langs.push(...options.highlight.preload || []); } } } export { DefaultHighlightLangs, module as default };