aboutsummaryrefslogtreecommitdiff
path: root/packages/integrations/mdx/src/rehype-apply-frontmatter-export.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/integrations/mdx/src/rehype-apply-frontmatter-export.ts')
-rw-r--r--packages/integrations/mdx/src/rehype-apply-frontmatter-export.ts113
1 files changed, 113 insertions, 0 deletions
diff --git a/packages/integrations/mdx/src/rehype-apply-frontmatter-export.ts b/packages/integrations/mdx/src/rehype-apply-frontmatter-export.ts
new file mode 100644
index 000000000..5880c30b3
--- /dev/null
+++ b/packages/integrations/mdx/src/rehype-apply-frontmatter-export.ts
@@ -0,0 +1,113 @@
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { isFrontmatterValid } from '@astrojs/markdown-remark';
+import type { Root, RootContent } from 'hast';
+import type { VFile } from 'vfile';
+import { jsToTreeNode } from './utils.js';
+
+// Passed metadata to help determine adding charset utf8 by default
+declare module 'vfile' {
+ interface DataMap {
+ applyFrontmatterExport?: {
+ srcDir?: URL;
+ };
+ }
+}
+
+const exportConstPartialTrueRe = /export\s+const\s+partial\s*=\s*true/;
+
+export function rehypeApplyFrontmatterExport() {
+ return function (tree: Root, vfile: VFile) {
+ const frontmatter = vfile.data.astro?.frontmatter;
+ if (!frontmatter || !isFrontmatterValid(frontmatter))
+ throw new Error(
+ // Copied from Astro core `errors-data`
+ // TODO: find way to import error data from core
+ '[MDX] A remark or rehype plugin attempted to inject invalid frontmatter. Ensure "astro.frontmatter" is set to a valid JSON object that is not `null` or `undefined`.',
+ );
+ const extraChildren: RootContent[] = [
+ jsToTreeNode(`export const frontmatter = ${JSON.stringify(frontmatter)};`),
+ ];
+ if (frontmatter.layout) {
+ extraChildren.unshift(
+ jsToTreeNode(
+ // NOTE: Use `__astro_*` import names to prevent conflicts with user code
+ /** @see 'vite-plugin-markdown' for layout props reference */
+ `\
+import { jsx as __astro_layout_jsx__ } from 'astro/jsx-runtime';
+import __astro_layout_component__ from ${JSON.stringify(frontmatter.layout)};
+
+export default function ({ children }) {
+ const { layout, ...content } = frontmatter;
+ content.file = file;
+ content.url = url;
+ return __astro_layout_jsx__(__astro_layout_component__, {
+ file,
+ url,
+ content,
+ frontmatter: content,
+ headings: getHeadings(),
+ 'server:root': true,
+ children,
+ });
+};`,
+ ),
+ );
+ } else if (shouldAddCharset(tree, vfile)) {
+ extraChildren.unshift({
+ type: 'mdxJsxFlowElement',
+ name: 'meta',
+ attributes: [
+ {
+ type: 'mdxJsxAttribute',
+ name: 'charset',
+ value: 'utf-8',
+ },
+ ],
+ children: [],
+ });
+ }
+ tree.children = extraChildren.concat(tree.children);
+ };
+}
+
+/**
+ * If this is a page (e.g. in src/pages), has no layout frontmatter (handled before calling this function),
+ * has no leading component that looks like a wrapping layout, and `partial` isn't set to true, we default to
+ * adding charset=utf-8 like markdown so that users don't have to worry about it for MDX pages without layouts.
+ */
+function shouldAddCharset(tree: Root, vfile: VFile) {
+ const srcDirUrl = vfile.data.applyFrontmatterExport?.srcDir;
+ if (!srcDirUrl) return false;
+
+ const hasConstPartialTrue = tree.children.some(
+ (node) => node.type === 'mdxjsEsm' && exportConstPartialTrueRe.test(node.value),
+ );
+ if (hasConstPartialTrue) return false;
+
+ // NOTE: the pages directory is a non-configurable Astro behaviour
+ const pagesDir = path.join(fileURLToPath(srcDirUrl), 'pages').replace(/\\/g, '/');
+ // `vfile.path` comes from Vite, which is a normalized path (no backslashes)
+ const filePath = vfile.path;
+ if (!filePath.startsWith(pagesDir)) return false;
+
+ const hasLeadingUnderscoreInPath = filePath
+ .slice(pagesDir.length)
+ .replace(/\\/g, '/')
+ .split('/')
+ .some((part) => part.startsWith('_'));
+ if (hasLeadingUnderscoreInPath) return false;
+
+ // Bail if the first content found is a wrapping layout component
+ for (const child of tree.children) {
+ if (child.type === 'element') break;
+ if (child.type === 'mdxJsxFlowElement') {
+ // If is fragment or lowercase tag name (html tags), skip and assume there's no layout
+ if (child.name == null) break;
+ if (child.name[0] === child.name[0].toLowerCase()) break;
+ return false;
+ }
+ }
+
+ return true;
+}