具有预定义允许组件的 MDX 的 Astro 布局

问题描述 投票:0回答:2

使用 Astro,我有一个布局文件:/src/layouts/MdxLayout.astro,我尝试设置允许的组件按钮和链接,如果从页面文件传入,则要渲染。

---
import BaseLayout from '@/layouts/Base/BaseLayout.astro';
import Button from '@/components/Button/Button';
import Link from "@/components/Link/Link";
---

<BaseLayout components={{ Button: Button, Link: Link }}>
  <slot />
</BaseLayout>

希望这能在 /src/pages/mdx.mdx:

中起作用
---
layout: '@/layouts/MdxLayout.astro'
title: "Hello, World!"
---

# Lorem ipsum

<Button>Click me</Button>

因此,我不必向我希望使用这些组件的所有 mdx 文件添加导入(即:

import Button from '@/components/Button/Button'
)。如果没有这个导入,我将面临错误:

预期要定义的组件按钮:您可能忘记导入、传递或提供它。

我的问题:有什么方法可以在布局文件中预定义组件,然后可以在使用此布局的 mdx 文件中使用这些组件,而无需将它们导入到 mdx 文件中?

astrojs mdxjs
2个回答
1
投票

这是可能的,但不是天生的。您有三个选择:

  • 使用自定义 RemarkRehype 插件
  • 使用提供此功能的现有库
  • 将现有 HTML 标签映射到自定义组件

使用评论或重新炒作

您可以创建自定义 Remark/Rehype 插件,以使用组件的导入语句更新树。比如:

import type { Root } from 'hast';
import type { Plugin as UnifiedPlugin } from 'unified';
import { visit } from 'unist-util-visit';

export const rehypeAutoImportComponents: UnifiedPlugin<[], Root> =
  () => (tree, file) => {
    const importsStatements = [];

    visit(tree, 'mdxJsxFlowElement', (node, index, nodeParent) => {
      if (node.name === 'Button') importsStatements.push('import Button from "@/components/Button/Button"');
    });

    tree.children.unshift(...importsStatements);
  };

您需要弄清楚如何解析路径别名以及如何避免重复的导入语句。

然后更新你的 Astro 配置文件:

import { rehypeAutoImportComponents } from './src/utils/rehype-auto-import-components';

export default defineConfig({
  markdown: {
    rehypePlugins: [
      rehypeAutoImportComponents
    ],
  },
});

您还可以向插件添加一个选项,以直接在 Astro 配置文件中传递组件,而不是在插件中对它们进行硬编码。

注意:使用此解决方案,您可以删除

components
中的
<BaseLayout ... />
属性。

现有库

我还没有测试过它们,但你可以寻找

astro-auto-import
Astro-M²DX
例如。

将 HTML 标签映射到自定义组件

另一种选择是将 HTML 标签映射到自定义组件:

---
import BaseLayout from '@/layouts/Base/BaseLayout.astro';
import Button from '@/components/Button/Button';
import Link from "@/components/Link/Link";
---

<BaseLayout components={{ a: Link, button: Button }}>
  <slot />
</BaseLayout>

但是,为了能够映射 HTML 标签(即

<button />
),您需要一个插件来禁用 MDXJS默认行为(忽略 HTML 标签)。比如:

import type { Root } from 'hast';
import type { Plugin as UnifiedPlugin } from 'unified';
import { visit } from 'unist-util-visit';

export const rehypeDisableExplicitJsx: UnifiedPlugin<[], Root> =
  () => (tree) => {
    visit(tree, ['mdxJsxFlowElement', 'mdxJsxTextElement'], (node) => {
      if (node.data && '_mdxExplicitJsx' in node.data) {
        delete node.data._mdxExplicitJsx;
      }
    });
  };

然后你可以将此插件添加到你的 Astro 配置中:

import { rehypeDisableExplicitJsx } from './src/utils/rehype-disable-explicit-jsx';

export default defineConfig({
  markdown: {
    rehypePlugins: [
      rehypeDisableExplicitJsx
    ],
  },
});

这样,您的 MDX 文件就可以如下所示:

---
layout: '@/layouts/MdxLayout.astro'
title: "Hello, World!"
---

# Lorem ipsum

<button>Click me</button>

需要注意的是,对于更复杂的组件(当不存在 HTML 标签时),您将需要额外的逻辑。您可以将 div 映射到自定义组件,在其中检查特定类/属性以返回正确的组件。


0
投票

谢谢阿曼德的回答,我让它与这个 rehype 插件一起工作:


import { parse, resolve } from 'node:path';

import { parse as parseJs } from 'acorn';
import type { VFile } from 'vfile';

const resolveModulePath = (path: string) => {
  // Resolve relative paths
  if (path.startsWith('.')) return resolve(path);
  // Don’t resolve other paths (e.g. npm modules)
  return path;
};

type NamedImportConfig = string | [from: string, as: string];
type ImportsConfig = (string | Record<string, string | NamedImportConfig[]>)[];

/**
 * Use a filename to generate a default import name.
 * @example
 * getDefaultImportName('/path/to/cool-component.astro');
 * // => coolcomponent
 */
function getDefaultImportName(path: string): string {
  return parse(path).name.replaceAll(/[^\w\d]/g, '');
}

/**
 * Create an import statement.
 * @param imported Stuff to import (e.g. `Thing` or `{ Named }`)
 * @param module Module to import from (e.g. `module-thing`)
 */
function formatImport(imported: string, module: string): string {
  return `import ${imported} from ${JSON.stringify(module)};`;
}

/** Get the parts for a named import statement from config. */
function formatNamedImports(namedImport: NamedImportConfig[]): string {
  const imports: string[] = [];
  for (const imp of namedImport) {
    if (typeof imp === 'string') {
      imports.push(imp);
    } else {
      const [from, as] = imp;
      imports.push(`${from} as ${as}`);
    }
  }
  return `{ ${imports.join(', ')} }`;
}

/** Generate imports from a full imports config array. */
function processImportsConfig(config: ImportsConfig) {
  const imports = [];
  for (const option of config) {
    if (typeof option === 'string') {
      imports.push(formatImport(getDefaultImportName(option), resolveModulePath(option)));
    } else {
      for (const path in option) {
        const namedImportsOrNamespace = option[path];
        if (typeof namedImportsOrNamespace === 'string') {
          imports.push(formatImport(`* as ${namedImportsOrNamespace}`, resolveModulePath(path)));
        } else {
          const importString = formatNamedImports(namedImportsOrNamespace);
          imports.push(formatImport(importString, resolveModulePath(path)));
        }
      }
    }
  }
  return imports;
}

/** Get an MDX node representing a block of imports based on user config. */
function generateImportsNode(config: ImportsConfig) {
  const imports = processImportsConfig(config);
  const js = imports.join('\n');
  return {
    type: 'mdxjsEsm',
    value: '',
    data: {
      estree: {
        ...parseJs(js, { ecmaVersion: 'latest', sourceType: 'module' }),
        type: 'Program',
        sourceType: 'module',
      },
    },
  };
}

type MfxFile = VFile & { data?: { astro?: { frontmatter?: { layout?: string } } } };

type PluginConfig = {
  [layoutName: string]: string[];
};

export function AutoImportComponentsPerLayout(pluginConfig: PluginConfig) {
  return function (tree: { children: unknown[] }, vfile: MfxFile) {
    const fileLayout: string =
      vfile?.data?.astro?.frontmatter?.layout?.split('/').pop()?.split('.')[0] || '';

    if (!fileLayout) {
      return;
    }

    // for each key in PluginConfig loop and add the array of imports to the imports array
    for (const key in pluginConfig) {
      if (Object.prototype.hasOwnProperty.call(pluginConfig, key)) {
        if (key === fileLayout) {
          const imports: string[] = [];
          const components: string[] = pluginConfig[key];
          components.forEach((component: string) => {
            imports.push(`@/components/${component}/${component}.tsx`);
          });
          const importsNode = generateImportsNode(imports);
          tree?.children.unshift(importsNode);
        }
      }
    }
  };
}

可以像这样使用:

  integrations: [
    mdx({
      rehypePlugins: [[AutoImportComponentsPerLayout, { BaseLayout: ["Button", "Card"], AnotherLayout: ["Button"] }  ]]
    })
  ],

© www.soinside.com 2019 - 2024. All rights reserved.