astro/packages/integrations/mdx/src/plugins.ts

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

323 lines
10 KiB
TypeScript
Raw Normal View History

import { rehypeHeadingIds } from '@astrojs/markdown-remark';
import type { Image } from 'mdast';
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';
2022-09-26 22:25:48 +00:00
import type { AstroConfig, MarkdownAstroData } from 'astro';
import type { Literal, MemberExpression } from 'estree';
import { visit } from 'unist-util-visit';
2022-09-26 22:25:48 +00:00
import { visit as estreeVisit } from 'estree-util-visit';
import { bold, yellow } from 'kleur/colors';
import rehypeRaw from 'rehype-raw';
import remarkGfm from 'remark-gfm';
import remarkSmartypants from 'remark-smartypants';
2022-09-26 22:25:48 +00:00
import type { Data, VFile } from 'vfile';
import { MdxOptions } from './index.js';
import { rehypeInjectHeadingsExport } from './rehype-collect-headings.js';
import rehypeMetaString from './rehype-meta-string.js';
import remarkPrism from './remark-prism.js';
import remarkShiki from './remark-shiki.js';
import { jsToTreeNode, isRelativePath } from './utils.js';
import { pathToFileURL } from 'node:url';
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: {} };
}
};
}
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 = [
Content Collections (#5291) * feat: port content plugin as-is * feat: add `fetchContent` to `.astro` * refactor: move more logic to content-internals * refactor: move internals -> content dir * feat: nested dir support * feat: add with-content playground * edit: update README * fix: serialize route pattern for Netlify edge Co-authored-by: Jackie Macharia <jackiewmacharia> * wip: add ?astro-asset-ssr flag * wip: exclude astro-ssr-asset styles from build * revert: SSR asset flag babel changes * feat: only load CSS when mdx is rendered! * fix(vercel): Include all files inside `dist/` instead of only `entry.mjs` (#5175) * chore: only-allow pnpm (#5131) * [MDX] Support `recmaPlugins` config (#5146) * feat: support recma plugins * chore: add `test:match` to MDX * chore: changeset * docs: add recmaPlugins to README * removes default content-visibility styles from image components (#5180) * wip: check renderContent works * nit: injectDelayedAssetPlugin doesn't need enforce * feat: render content with generated entries * feat: renderContent style injection POC * wip: scrappy renderContent binding * deps: parse-imports * feat: only bind renderContent when imported * feat: new-and-improved render-content * fix: update example to use render-content * refactor: extract normalizeFilename helper * feat: move renderContent to `.astro` * feat: add `getStaticPaths` demo 👀 * chore: remove console log * refactor: internals -> internal * fix: style bleed on dynamic routes * chore: remove console log * refactor: move rendercontentmap inside same file * fix: style bleed in dev! * feat: clean up example * refactor: curry generateContent * nit: add datetime demo * refactor: rename and move content plugins * feat: granular content updates in development * chore: console.log * refactor: move write logic, fix edge cases * fix: use z.any() when ~schema is missing * refactor: clean up logging * fix: add delayed asset flag to render map * fix: use new server loader * chore: import type * fix: split delayed assets to separate chunks * chore: bad merge changesets * feat: rework content map to Vite globs * fix: formatted errors * fix: get `renderContent` working with new APIs * refactor: rename to getCollection, getEntry, renderEntry * feat: streamline type alias! * fix: check schema import is an import * refactor: change ~schema -> index with defineCollection * feat: auto apply env.d.ts reference * feat: expose `headings` on `renderEntry` * refactor: expose zod from astro/zod * feat: add Collection util type * refactor: generated types in src/content/ * edit: remove env.d.ts change from with-content * feat: collectionToPaths utility * refactor: migrate `examples/blog` * refactor: simplify [...slug] * fix: invalid date * fix: renderEntry * feat: migrate examples/docs to content! * refactor: union -> enum * fix: remove duplicate header * chore: changeset * Revert "fix: serialize route pattern for Netlify edge" This reverts commit ac46a0d3238af1e6f8c3b8f235163385be1ff732. * fix: lint * fix: include zod in package files * chore: push type gen for astro check * feat: move to single defineCollections file * feat: move examples to new collections model * chore: type check valid collection names * fix: ignore collections path in err logs * refactor: content/index -> content/config * refactor: defineCollections -> defineCollection * fix: update docs to defineCollection * chore: update with-content to new `astro:content` setup * fix: renderEntry styles * fix: bad Collection type * fix: blog types generated * feat: expose injectedFrontmatter on renderEntry * chore: `import type` * chore: add MDX to changeset * fix: template package file * fix: unneeded log on content config change * chore: add `injectedFrontmatter` to generated types * fix: escape import.meta.env in `body` * fix: escape import.meta.env in frontmatter * chore: unused import * fix: use URL for config pathname * feat: allow zod async transforms * deps: devalue * feat: custom slugs + better type checking * fix: update types on config added or removed * chore: add error msg for MDX preview release * refactor: restore old normalizeFilename * fix: add loadContentConfig error classes back * Add initial tests for renderEntry * add fixture tests * Use head propagation APIs in renderEntry * chore: new type gen * fix: sort content keys before writing * fix: avoid writing '.' collection with warning * chore: docs types generated * fix: generated types only once in build * fix: use relative path to config for git * chore: update generated types * fix: glob pattern on valid exts * feat: warn on possible collection typos * chore: better error on bad lazy import * fix: clear gray-matter cache for rawData * fix: wait for config file while generating * test: get collection and get entry basic * test: multiple collections and entries * test: add publishedDate test on entry * tests: getStaticPaths integration * test: ssr endpoint integration * chore: consistent test names * refactor: simplify generates... test * chore: tests lockfile * feat: add `experimental` flag * chore: revert blog and docs examples * chore: add experimental flag to with-content * refactor: reuse contentFileExts * refactor: make contentCollectionCss required * chore: remove parse-imports dependency * chore: remove unresolved idea redundancy * chore: update changeset to minor v * chore: clean up with-content pass 1 * refactor: de-monolith vite-plugin-content * chore: remove maximum jank warning (it's acceptable jank) * refactor: extract types-generator to separate file * refactor: onEvent -> handleEvent * edit: change content log to "types generated" * fix: more experimental checks in prod build * refactor: 2 jsToTreeNode calls -> 1 * feat: change with-content example to blog example * fix: Content component type * Fix use of renderEntry within a slot * fix: warn for non-underscore _ files, refine logs * chore: Content type in tests * fix: types no updating with config changes * fix: remove `result: SSRResult` type * chore: entry var shadowing * fix: bad experimental flag call * wip: skip render entry tests * fix: path resolution for .relative on windows * chore: remove dead code * wip: log entryPath into * fix: use URL for entry * refactor: getEntryInfo normalization * refactor: move debounce to queueEvent only * fix: resolve loader on config error * fix: pathToFileURL -> new URL * fix: dangerous pathname comparison * refactor: move rawEvent -> event mapping to queueEvent * fix: bad type * wip: remove mkdir call * wip: log content paths * fix: more bad paths * wip: more logs * fix: correct glob path * fix: normalize virtual mod path * fix: content server file paths * chore: remove render entry test skip * wip: skip renderEntry tests (windows issues?) * chore: remove console log * feat: renderEntry -> render * chore: add `--experimental-content-collections` * refactor: only skip renderEntry tests on windows * refactor: index.ts barrel export * fix: cli type error * fix: proper test skip on windows * refactor: remove collectionToPaths :( * refactor: use AstroError for frontmatter error * nit: its schema, .* * fix: bad isWindows import * fix: await mod -> mod = await Co-authored-by: Juan Martín Seery <me@juanm04.com> Co-authored-by: 花果山大圣 <316783812@qq.com> Co-authored-by: Tony Sullivan <tony.f.sullivan@outlook.com> Co-authored-by: Matthew Phillips <matthew@skypack.dev> Co-authored-by: bholmesdev <bholmesdev@gmail.com>
2022-12-16 19:19:53 +00:00
jsToTreeNode(
`export const frontmatter = ${JSON.stringify(
frontmatter
)};\nexport const _internal = { injectedFrontmatter: ${JSON.stringify(
injectedFrontmatter
)} };`
),
];
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';
Content Collections (#5291) * feat: port content plugin as-is * feat: add `fetchContent` to `.astro` * refactor: move more logic to content-internals * refactor: move internals -> content dir * feat: nested dir support * feat: add with-content playground * edit: update README * fix: serialize route pattern for Netlify edge Co-authored-by: Jackie Macharia <jackiewmacharia> * wip: add ?astro-asset-ssr flag * wip: exclude astro-ssr-asset styles from build * revert: SSR asset flag babel changes * feat: only load CSS when mdx is rendered! * fix(vercel): Include all files inside `dist/` instead of only `entry.mjs` (#5175) * chore: only-allow pnpm (#5131) * [MDX] Support `recmaPlugins` config (#5146) * feat: support recma plugins * chore: add `test:match` to MDX * chore: changeset * docs: add recmaPlugins to README * removes default content-visibility styles from image components (#5180) * wip: check renderContent works * nit: injectDelayedAssetPlugin doesn't need enforce * feat: render content with generated entries * feat: renderContent style injection POC * wip: scrappy renderContent binding * deps: parse-imports * feat: only bind renderContent when imported * feat: new-and-improved render-content * fix: update example to use render-content * refactor: extract normalizeFilename helper * feat: move renderContent to `.astro` * feat: add `getStaticPaths` demo 👀 * chore: remove console log * refactor: internals -> internal * fix: style bleed on dynamic routes * chore: remove console log * refactor: move rendercontentmap inside same file * fix: style bleed in dev! * feat: clean up example * refactor: curry generateContent * nit: add datetime demo * refactor: rename and move content plugins * feat: granular content updates in development * chore: console.log * refactor: move write logic, fix edge cases * fix: use z.any() when ~schema is missing * refactor: clean up logging * fix: add delayed asset flag to render map * fix: use new server loader * chore: import type * fix: split delayed assets to separate chunks * chore: bad merge changesets * feat: rework content map to Vite globs * fix: formatted errors * fix: get `renderContent` working with new APIs * refactor: rename to getCollection, getEntry, renderEntry * feat: streamline type alias! * fix: check schema import is an import * refactor: change ~schema -> index with defineCollection * feat: auto apply env.d.ts reference * feat: expose `headings` on `renderEntry` * refactor: expose zod from astro/zod * feat: add Collection util type * refactor: generated types in src/content/ * edit: remove env.d.ts change from with-content * feat: collectionToPaths utility * refactor: migrate `examples/blog` * refactor: simplify [...slug] * fix: invalid date * fix: renderEntry * feat: migrate examples/docs to content! * refactor: union -> enum * fix: remove duplicate header * chore: changeset * Revert "fix: serialize route pattern for Netlify edge" This reverts commit ac46a0d3238af1e6f8c3b8f235163385be1ff732. * fix: lint * fix: include zod in package files * chore: push type gen for astro check * feat: move to single defineCollections file * feat: move examples to new collections model * chore: type check valid collection names * fix: ignore collections path in err logs * refactor: content/index -> content/config * refactor: defineCollections -> defineCollection * fix: update docs to defineCollection * chore: update with-content to new `astro:content` setup * fix: renderEntry styles * fix: bad Collection type * fix: blog types generated * feat: expose injectedFrontmatter on renderEntry * chore: `import type` * chore: add MDX to changeset * fix: template package file * fix: unneeded log on content config change * chore: add `injectedFrontmatter` to generated types * fix: escape import.meta.env in `body` * fix: escape import.meta.env in frontmatter * chore: unused import * fix: use URL for config pathname * feat: allow zod async transforms * deps: devalue * feat: custom slugs + better type checking * fix: update types on config added or removed * chore: add error msg for MDX preview release * refactor: restore old normalizeFilename * fix: add loadContentConfig error classes back * Add initial tests for renderEntry * add fixture tests * Use head propagation APIs in renderEntry * chore: new type gen * fix: sort content keys before writing * fix: avoid writing '.' collection with warning * chore: docs types generated * fix: generated types only once in build * fix: use relative path to config for git * chore: update generated types * fix: glob pattern on valid exts * feat: warn on possible collection typos * chore: better error on bad lazy import * fix: clear gray-matter cache for rawData * fix: wait for config file while generating * test: get collection and get entry basic * test: multiple collections and entries * test: add publishedDate test on entry * tests: getStaticPaths integration * test: ssr endpoint integration * chore: consistent test names * refactor: simplify generates... test * chore: tests lockfile * feat: add `experimental` flag * chore: revert blog and docs examples * chore: add experimental flag to with-content * refactor: reuse contentFileExts * refactor: make contentCollectionCss required * chore: remove parse-imports dependency * chore: remove unresolved idea redundancy * chore: update changeset to minor v * chore: clean up with-content pass 1 * refactor: de-monolith vite-plugin-content * chore: remove maximum jank warning (it's acceptable jank) * refactor: extract types-generator to separate file * refactor: onEvent -> handleEvent * edit: change content log to "types generated" * fix: more experimental checks in prod build * refactor: 2 jsToTreeNode calls -> 1 * feat: change with-content example to blog example * fix: Content component type * Fix use of renderEntry within a slot * fix: warn for non-underscore _ files, refine logs * chore: Content type in tests * fix: types no updating with config changes * fix: remove `result: SSRResult` type * chore: entry var shadowing * fix: bad experimental flag call * wip: skip render entry tests * fix: path resolution for .relative on windows * chore: remove dead code * wip: log entryPath into * fix: use URL for entry * refactor: getEntryInfo normalization * refactor: move debounce to queueEvent only * fix: resolve loader on config error * fix: pathToFileURL -> new URL * fix: dangerous pathname comparison * refactor: move rawEvent -> event mapping to queueEvent * fix: bad type * wip: remove mkdir call * wip: log content paths * fix: more bad paths * wip: more logs * fix: correct glob path * fix: normalize virtual mod path * fix: content server file paths * chore: remove render entry test skip * wip: skip renderEntry tests (windows issues?) * chore: remove console log * feat: renderEntry -> render * chore: add `--experimental-content-collections` * refactor: only skip renderEntry tests on windows * refactor: index.ts barrel export * fix: cli type error * fix: proper test skip on windows * refactor: remove collectionToPaths :( * refactor: use AstroError for frontmatter error * nit: its schema, .* * fix: bad isWindows import * fix: await mod -> mod = await Co-authored-by: Juan Martín Seery <me@juanm04.com> Co-authored-by: 花果山大圣 <316783812@qq.com> Co-authored-by: Tony Sullivan <tony.f.sullivan@outlook.com> Co-authored-by: Matthew Phillips <matthew@skypack.dev> Co-authored-by: bholmesdev <bholmesdev@gmail.com>
2022-12-16 19:19:53 +00:00
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);
};
}
/**
* `src/content/` does not support relative image paths.
* This plugin throws an error if any are found
*/
function toRemarkContentRelImageError({ srcDir }: { srcDir: URL }) {
const contentDir = new URL('content/', srcDir);
return function remarkContentRelImageError() {
return (tree: any, vfile: VFile) => {
const isContentFile = pathToFileURL(vfile.path).href.startsWith(contentDir.href);
if (!isContentFile) return;
const relImagePaths = new Set<string>();
visit(tree, 'image', function raiseError(node: Image) {
if (isRelativePath(node.url)) {
relImagePaths.add(node.url);
}
});
if (relImagePaths.size === 0) return;
const errorMessage =
`Relative image paths are not supported in the content/ directory. Place local images in the public/ directory and use absolute paths (see https://docs.astro.build/en/guides/images/#in-markdown-files):\n` +
[...relImagePaths].map((path) => JSON.stringify(path)).join(',\n');
throw new Error(errorMessage);
};
};
}
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 ?? [])];
// Apply last in case user plugins resolve relative image paths
if (config.experimental.contentCollections) {
remarkPlugins.push(toRemarkContentRelImageError(config));
}
return remarkPlugins;
}
export function getRehypePlugins(
mdxOptions: MdxOptions,
config: AstroConfig
): MdxRollupPluginOptions['rehypePlugins'] {
let rehypePlugins: PluggableList = [
// ensure `data.meta` is preserved in `properties.metastring` for rehype syntax highlighters
rehypeMetaString,
// 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 ?? []),
// getHeadings() is guaranteed by TS, so this must be included.
// We run `rehypeHeadingIds` _last_ to respect any custom IDs set by user plugins.
rehypeHeadingIds,
rehypeInjectHeadingsExport,
];
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');
}
}