diff options
Diffstat (limited to 'packages/integrations/mdx/src/rehype-apply-frontmatter-export.ts')
-rw-r--r-- | packages/integrations/mdx/src/rehype-apply-frontmatter-export.ts | 113 |
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; +} |