diff options
author | 2022-09-26 18:23:47 -0400 | |
---|---|---|
committer | 2022-09-26 18:23:47 -0400 | |
commit | 58a2dca2286cb14f6211cf51267c02447e78433a (patch) | |
tree | 2595109fafef818469382b1600205665dde264d0 /packages/integrations/mdx/src/plugins.ts | |
parent | b73ec1417117ccc479183fec6efcf06990e5b905 (diff) | |
download | astro-58a2dca2286cb14f6211cf51267c02447e78433a.tar.gz astro-58a2dca2286cb14f6211cf51267c02447e78433a.tar.zst astro-58a2dca2286cb14f6211cf51267c02447e78433a.zip |
Fix: correctly transform `import.meta.env.*` in MDX (#4858)
* fix: serialize route pattern for Netlify edge
Co-authored-by: Jackie Macharia <jackiewmacharia>
* fix: escape import.meta.env in MDX compiler output
* test: env vars in mdx
* chore: changeset
* deps: estree-util-visit, @types/estree
* feat: inject import.meta.env w/ recma
* feat: pull importMetaEnv from vite + astro configs
* test: `import.meta.env` in JSX
* fix: lockfile
* chore: update changeset
* fix: remove stray stashed commit
Diffstat (limited to 'packages/integrations/mdx/src/plugins.ts')
-rw-r--r-- | packages/integrations/mdx/src/plugins.ts | 273 |
1 files changed, 273 insertions, 0 deletions
diff --git a/packages/integrations/mdx/src/plugins.ts b/packages/integrations/mdx/src/plugins.ts new file mode 100644 index 000000000..f6911ba99 --- /dev/null +++ b/packages/integrations/mdx/src/plugins.ts @@ -0,0 +1,273 @@ +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'); + } +} |