421 lines
15 KiB
JavaScript
421 lines
15 KiB
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 { 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) {
|
|
if (node.tag === "MDCSlot") {
|
|
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')"
|
|
);
|
|
}
|
|
const { bundledLanguagesInfo } = await import('shiki/langs');
|
|
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 { createJavaScriptRegexEngine } from 'shiki/engine/javascript'" : "import { createWasmOnigEngine } from 'shiki/engine/oniguruma'",
|
|
code,
|
|
"const bundledLangs = {",
|
|
...Array.from(langsMap.entries()).map(([name, lang]) => typeof lang === "string" ? JSON.stringify(name) + `: () => import('shiki/langs/${lang}.mjs'),` : JSON.stringify(name) + ": " + JSON.stringify(lang) + ","),
|
|
"}",
|
|
"const bundledThemes = {",
|
|
...themes.map((theme) => typeof theme === "string" ? JSON.stringify(theme) + `: () => import('shiki/themes/${theme}.mjs').then(r => r.default),` : 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 = createWasmOnigEngine(() => 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 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
|
|
});
|
|
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: "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" }]);
|
|
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 };
|