274 lines
8.5 KiB
TypeScript
274 lines
8.5 KiB
TypeScript
|
import type { MemberExpression, Literal } from 'estree';
|
||
|
import type { MarkdownAstroData, AstroConfig } from 'astro';
|
||
|
import type { Data, VFile } from 'vfile';
|
||
|
import { visit as estreeVisit } from 'estree-util-visit';
|
||
|
import { jsToTreeNode } from './utils.js';
|
||
|
import { nodeTypes } from '@mdx-js/mdx';
|
||
|
import type { PluggableList } from '@mdx-js/mdx/lib/core.js';
|
||
|
import type { Options as MdxRollupPluginOptions } from '@mdx-js/rollup';
|
||
|
import { bold, yellow } from 'kleur/colors';
|
||
|
import rehypeRaw from 'rehype-raw';
|
||
|
import remarkGfm from 'remark-gfm';
|
||
|
import remarkSmartypants from 'remark-smartypants';
|
||
|
import rehypeCollectHeadings from './rehype-collect-headings.js';
|
||
|
import remarkPrism from './remark-prism.js';
|
||
|
import remarkShiki from './remark-shiki.js';
|
||
|
import { MdxOptions } from './index.js';
|
||
|
|
||
|
export function recmaInjectImportMetaEnvPlugin({
|
||
|
importMetaEnv,
|
||
|
}: {
|
||
|
importMetaEnv: Record<string, any>;
|
||
|
}) {
|
||
|
return (tree: any) => {
|
||
|
estreeVisit(tree, (node) => {
|
||
|
if (node.type === 'MemberExpression') {
|
||
|
// attempt to get "import.meta.env" variable name
|
||
|
const envVarName = getImportMetaEnvVariableName(node as MemberExpression);
|
||
|
if (typeof envVarName === 'string') {
|
||
|
// clear object keys to replace with envVarLiteral
|
||
|
for (const key in node) {
|
||
|
delete (node as any)[key];
|
||
|
}
|
||
|
const envVarLiteral: Literal = {
|
||
|
type: 'Literal',
|
||
|
value: importMetaEnv[envVarName],
|
||
|
raw: JSON.stringify(importMetaEnv[envVarName]),
|
||
|
};
|
||
|
Object.assign(node, envVarLiteral);
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
};
|
||
|
}
|
||
|
|
||
|
export function remarkInitializeAstroData() {
|
||
|
return function (tree: any, vfile: VFile) {
|
||
|
if (!vfile.data.astro) {
|
||
|
vfile.data.astro = { frontmatter: {} };
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
|
||
|
const EXPORT_NAME = 'frontmatter';
|
||
|
|
||
|
export function rehypeApplyFrontmatterExport(pageFrontmatter: Record<string, any>) {
|
||
|
return function (tree: any, vfile: VFile) {
|
||
|
const { frontmatter: injectedFrontmatter } = safelyGetAstroData(vfile.data);
|
||
|
const frontmatter = { ...injectedFrontmatter, ...pageFrontmatter };
|
||
|
const exportNodes = [
|
||
|
jsToTreeNode(`export const ${EXPORT_NAME} = ${JSON.stringify(frontmatter)};`),
|
||
|
];
|
||
|
if (frontmatter.layout) {
|
||
|
// NOTE(bholmesdev) 08-22-2022
|
||
|
// Using an async layout import (i.e. `const Layout = (await import...)`)
|
||
|
// Preserves the dev server import cache when globbing a large set of MDX files
|
||
|
// Full explanation: 'https://github.com/withastro/astro/pull/4428'
|
||
|
exportNodes.unshift(
|
||
|
jsToTreeNode(
|
||
|
/** @see 'vite-plugin-markdown' for layout props reference */
|
||
|
`import { jsx as layoutJsx } from 'astro/jsx-runtime';
|
||
|
|
||
|
export default async function ({ children }) {
|
||
|
const Layout = (await import(${JSON.stringify(frontmatter.layout)})).default;
|
||
|
const { layout, ...content } = frontmatter;
|
||
|
content.file = file;
|
||
|
content.url = url;
|
||
|
content.astro = {};
|
||
|
Object.defineProperty(content.astro, 'headings', {
|
||
|
get() {
|
||
|
throw new Error('The "astro" property is no longer supported! To access "headings" from your layout, try using "Astro.props.headings."')
|
||
|
}
|
||
|
});
|
||
|
Object.defineProperty(content.astro, 'html', {
|
||
|
get() {
|
||
|
throw new Error('The "astro" property is no longer supported! To access "html" from your layout, try using "Astro.props.compiledContent()."')
|
||
|
}
|
||
|
});
|
||
|
Object.defineProperty(content.astro, 'source', {
|
||
|
get() {
|
||
|
throw new Error('The "astro" property is no longer supported! To access "source" from your layout, try using "Astro.props.rawContent()."')
|
||
|
}
|
||
|
});
|
||
|
return layoutJsx(Layout, {
|
||
|
file,
|
||
|
url,
|
||
|
content,
|
||
|
frontmatter: content,
|
||
|
headings: getHeadings(),
|
||
|
'server:root': true,
|
||
|
children,
|
||
|
});
|
||
|
};`
|
||
|
)
|
||
|
);
|
||
|
}
|
||
|
tree.children = exportNodes.concat(tree.children);
|
||
|
};
|
||
|
}
|
||
|
|
||
|
const DEFAULT_REMARK_PLUGINS: PluggableList = [remarkGfm, remarkSmartypants];
|
||
|
const DEFAULT_REHYPE_PLUGINS: PluggableList = [];
|
||
|
|
||
|
export async function getRemarkPlugins(
|
||
|
mdxOptions: MdxOptions,
|
||
|
config: AstroConfig
|
||
|
): Promise<MdxRollupPluginOptions['remarkPlugins']> {
|
||
|
let remarkPlugins: PluggableList = [
|
||
|
// Set "vfile.data.astro" for plugins to inject frontmatter
|
||
|
remarkInitializeAstroData,
|
||
|
];
|
||
|
switch (mdxOptions.extendPlugins) {
|
||
|
case false:
|
||
|
break;
|
||
|
case 'astroDefaults':
|
||
|
remarkPlugins = [...remarkPlugins, ...DEFAULT_REMARK_PLUGINS];
|
||
|
break;
|
||
|
default:
|
||
|
remarkPlugins = [
|
||
|
...remarkPlugins,
|
||
|
...(markdownShouldExtendDefaultPlugins(config) ? DEFAULT_REMARK_PLUGINS : []),
|
||
|
...ignoreStringPlugins(config.markdown.remarkPlugins ?? []),
|
||
|
];
|
||
|
break;
|
||
|
}
|
||
|
if (config.markdown.syntaxHighlight === 'shiki') {
|
||
|
remarkPlugins.push([await remarkShiki(config.markdown.shikiConfig)]);
|
||
|
}
|
||
|
if (config.markdown.syntaxHighlight === 'prism') {
|
||
|
remarkPlugins.push(remarkPrism);
|
||
|
}
|
||
|
|
||
|
remarkPlugins = [...remarkPlugins, ...(mdxOptions.remarkPlugins ?? [])];
|
||
|
return remarkPlugins;
|
||
|
}
|
||
|
|
||
|
export function getRehypePlugins(
|
||
|
mdxOptions: MdxOptions,
|
||
|
config: AstroConfig
|
||
|
): MdxRollupPluginOptions['rehypePlugins'] {
|
||
|
let rehypePlugins: PluggableList = [
|
||
|
// getHeadings() is guaranteed by TS, so we can't allow user to override
|
||
|
rehypeCollectHeadings,
|
||
|
// rehypeRaw allows custom syntax highlighters to work without added config
|
||
|
[rehypeRaw, { passThrough: nodeTypes }] as any,
|
||
|
];
|
||
|
switch (mdxOptions.extendPlugins) {
|
||
|
case false:
|
||
|
break;
|
||
|
case 'astroDefaults':
|
||
|
rehypePlugins = [...rehypePlugins, ...DEFAULT_REHYPE_PLUGINS];
|
||
|
break;
|
||
|
default:
|
||
|
rehypePlugins = [
|
||
|
...rehypePlugins,
|
||
|
...(markdownShouldExtendDefaultPlugins(config) ? DEFAULT_REHYPE_PLUGINS : []),
|
||
|
...ignoreStringPlugins(config.markdown.rehypePlugins ?? []),
|
||
|
];
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
rehypePlugins = [...rehypePlugins, ...(mdxOptions.rehypePlugins ?? [])];
|
||
|
return rehypePlugins;
|
||
|
}
|
||
|
|
||
|
function markdownShouldExtendDefaultPlugins(config: AstroConfig): boolean {
|
||
|
return (
|
||
|
config.markdown.extendDefaultPlugins ||
|
||
|
(config.markdown.remarkPlugins.length === 0 && config.markdown.rehypePlugins.length === 0)
|
||
|
);
|
||
|
}
|
||
|
|
||
|
function ignoreStringPlugins(plugins: any[]) {
|
||
|
let validPlugins: PluggableList = [];
|
||
|
let hasInvalidPlugin = false;
|
||
|
for (const plugin of plugins) {
|
||
|
if (typeof plugin === 'string') {
|
||
|
console.warn(yellow(`[MDX] ${bold(plugin)} not applied.`));
|
||
|
hasInvalidPlugin = true;
|
||
|
} else if (Array.isArray(plugin) && typeof plugin[0] === 'string') {
|
||
|
console.warn(yellow(`[MDX] ${bold(plugin[0])} not applied.`));
|
||
|
hasInvalidPlugin = true;
|
||
|
} else {
|
||
|
validPlugins.push(plugin);
|
||
|
}
|
||
|
}
|
||
|
if (hasInvalidPlugin) {
|
||
|
console.warn(
|
||
|
`To inherit Markdown plugins in MDX, please use explicit imports in your config instead of "strings." See Markdown docs: https://docs.astro.build/en/guides/markdown-content/#markdown-plugins`
|
||
|
);
|
||
|
}
|
||
|
return validPlugins;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Copied from markdown utils
|
||
|
* @see "vite-plugin-utils"
|
||
|
*/
|
||
|
function isValidAstroData(obj: unknown): obj is MarkdownAstroData {
|
||
|
if (typeof obj === 'object' && obj !== null && obj.hasOwnProperty('frontmatter')) {
|
||
|
const { frontmatter } = obj as any;
|
||
|
try {
|
||
|
// ensure frontmatter is JSON-serializable
|
||
|
JSON.stringify(frontmatter);
|
||
|
} catch {
|
||
|
return false;
|
||
|
}
|
||
|
return typeof frontmatter === 'object' && frontmatter !== null;
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Copied from markdown utils
|
||
|
* @see "vite-plugin-utils"
|
||
|
*/
|
||
|
function safelyGetAstroData(vfileData: Data): MarkdownAstroData {
|
||
|
const { astro } = vfileData;
|
||
|
|
||
|
if (!astro) return { frontmatter: {} };
|
||
|
if (!isValidAstroData(astro)) {
|
||
|
throw Error(
|
||
|
`[MDX] A remark or rehype plugin tried to add invalid frontmatter. Ensure "astro.frontmatter" is a JSON object!`
|
||
|
);
|
||
|
}
|
||
|
|
||
|
return astro;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Check if estree entry is "import.meta.env.VARIABLE"
|
||
|
* If it is, return the variable name (i.e. "VARIABLE")
|
||
|
*/
|
||
|
function getImportMetaEnvVariableName(node: MemberExpression): string | Error {
|
||
|
try {
|
||
|
// check for ".[ANYTHING]"
|
||
|
if (node.object.type !== 'MemberExpression' || node.property.type !== 'Identifier')
|
||
|
return new Error();
|
||
|
|
||
|
const nestedExpression = node.object;
|
||
|
// check for ".env"
|
||
|
if (nestedExpression.property.type !== 'Identifier' || nestedExpression.property.name !== 'env')
|
||
|
return new Error();
|
||
|
|
||
|
const envExpression = nestedExpression.object;
|
||
|
// check for ".meta"
|
||
|
if (
|
||
|
envExpression.type !== 'MetaProperty' ||
|
||
|
envExpression.property.type !== 'Identifier' ||
|
||
|
envExpression.property.name !== 'meta'
|
||
|
)
|
||
|
return new Error();
|
||
|
|
||
|
// check for "import"
|
||
|
if (envExpression.meta.name !== 'import') return new Error();
|
||
|
|
||
|
return node.property.name;
|
||
|
} catch (e) {
|
||
|
if (e instanceof Error) {
|
||
|
return e;
|
||
|
}
|
||
|
return new Error('Unknown parsing error');
|
||
|
}
|
||
|
}
|