@nuxtjs/mdc
Version:
Nuxt MDC module
432 lines (424 loc) • 15.7 kB
JavaScript
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 };